Merge branch 'main' into workspace-invite

This commit is contained in:
Zack Fu Zi Xiang 2024-03-05 09:17:12 +08:00
commit d0c647203c
No known key found for this signature in database
19 changed files with 301 additions and 76 deletions

View File

@ -1,5 +1,7 @@
import 'package:appflowy/plugins/database/calendar/presentation/calendar_event_editor.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart'; import 'package:integration_test/integration_test.dart';
@ -147,6 +149,49 @@ void main() {
tester.assertNumberOfEventsOnSpecificDay(1, DateTime.now()); tester.assertNumberOfEventsOnSpecificDay(1, DateTime.now());
}); });
testWidgets('create and duplicate calendar event', (tester) async {
const customTitle = "EventTitleCustom";
await tester.initializeAppFlowy();
await tester.tapGoButton();
// Create the calendar view
await tester.createNewPageWithNameUnderParent(
layout: ViewLayoutPB.Calendar,
);
// Scroll until today's date cell is visible
await tester.scrollToToday();
// Hover over today's calendar cell
await tester.hoverOnTodayCalendarCell(
// Tap on create new event button
onHover: () async => tester.tapAddCalendarEventButton(),
);
// Make sure that the event editor popup is shown
tester.assertEventEditorOpen();
tester.assertNumberOfEventsInCalendar(1);
// Change the title of the event
await tester.editEventTitle(customTitle);
// Duplicate event
final duplicateBtnFinder = find
.descendant(
of: find.byType(CalendarEventEditor),
matching: find.byType(
FlowyIconButton,
),
)
.first;
await tester.tap(duplicateBtnFinder);
await tester.pumpAndSettle();
tester.assertNumberOfEventsInCalendar(2, title: customTitle);
});
testWidgets('rescheduling events', (tester) async { testWidgets('rescheduling events', (tester) async {
await tester.initializeAppFlowy(); await tester.initializeAppFlowy();
await tester.tapGoButton(); await tester.tapGoButton();

View File

@ -64,6 +64,20 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
createEvent: (DateTime date) async { createEvent: (DateTime date) async {
await _createEvent(date); await _createEvent(date);
}, },
duplicateEvent: (String viewId, String rowId) async {
final result = await RowBackendService.duplicateRow(viewId, rowId);
result.fold(
(_) => null,
(e) => Log.error('Failed to duplicate event: $e', e),
);
},
deleteEvent: (String viewId, String rowId) async {
final result = await RowBackendService.deleteRow(viewId, rowId);
result.fold(
(_) => null,
(e) => Log.error('Failed to delete event: $e', e),
);
},
newEventPopupDisplayed: () { newEventPopupDisplayed: () {
emit(state.copyWith(editingEvent: null)); emit(state.copyWith(editingEvent: null));
}, },
@ -407,6 +421,12 @@ class CalendarEvent with _$CalendarEvent {
const factory CalendarEvent.didReceiveDatabaseUpdate(DatabasePB database) = const factory CalendarEvent.didReceiveDatabaseUpdate(DatabasePB database) =
_ReceiveDatabaseUpdate; _ReceiveDatabaseUpdate;
const factory CalendarEvent.duplicateEvent(String viewId, String rowId) =
_DuplicateEvent;
const factory CalendarEvent.deleteEvent(String viewId, String rowId) =
_DeleteEvent;
} }
@freezed @freezed

View File

@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart'; import 'package:appflowy/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/application/row/row_cache.dart'; import 'package:appflowy/plugins/database/application/row/row_cache.dart';
@ -9,11 +11,11 @@ import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import '../application/calendar_bloc.dart'; import '../application/calendar_bloc.dart';
import 'calendar_event_editor.dart'; import 'calendar_event_editor.dart';
class EventCard extends StatefulWidget { class EventCard extends StatefulWidget {
@ -144,15 +146,18 @@ class _EventCardState extends State<EventCard> {
asBarrier: true, asBarrier: true,
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
offset: const Offset(10.0, 0), offset: const Offset(10.0, 0),
popupBuilder: (BuildContext popoverContext) { popupBuilder: (_) {
final settings = context.watch<CalendarBloc>().state.settings; final settings = context.watch<CalendarBloc>().state.settings;
if (settings == null) { if (settings == null) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
return CalendarEventEditor( return BlocProvider.value(
databaseController: widget.databaseController, value: context.read<CalendarBloc>(),
rowMeta: widget.event.event.rowMeta, child: CalendarEventEditor(
layoutSettings: settings, databaseController: widget.databaseController,
rowMeta: widget.event.event.rowMeta,
layoutSettings: settings,
),
); );
}, },
child: Container( child: Container(

View File

@ -1,25 +1,25 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart'; import 'package:appflowy/plugins/database/application/field/field_controller.dart';
import 'package:appflowy/plugins/database/application/row/row_controller.dart'; import 'package:appflowy/plugins/database/application/row/row_controller.dart';
import 'package:appflowy/plugins/database/application/row/row_service.dart'; import 'package:appflowy/plugins/database/calendar/application/calendar_bloc.dart';
import 'package:appflowy/plugins/database/calendar/application/calendar_event_editor_bloc.dart'; import 'package:appflowy/plugins/database/calendar/application/calendar_event_editor_bloc.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart';
import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart'; import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart';
import 'package:appflowy/plugins/database/widgets/row/row_detail.dart'; import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
import 'package:appflowy/util/field_type_extension.dart'; import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
class CalendarEventEditor extends StatelessWidget { class CalendarEventEditor extends StatelessWidget {
@ -86,17 +86,28 @@ class EventEditorControls extends StatelessWidget {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
FlowyIconButton(
width: 20,
icon: const FlowySvg(FlowySvgs.m_duplicate_s),
iconColorOnHover: Theme.of(context).colorScheme.onSecondary,
onPressed: () => context.read<CalendarBloc>().add(
CalendarEvent.duplicateEvent(
rowController.viewId,
rowController.rowId,
),
),
),
const HSpace(8.0),
FlowyIconButton( FlowyIconButton(
width: 20, width: 20,
icon: const FlowySvg(FlowySvgs.delete_s), icon: const FlowySvg(FlowySvgs.delete_s),
iconColorOnHover: Theme.of(context).colorScheme.onSecondary, iconColorOnHover: Theme.of(context).colorScheme.onSecondary,
onPressed: () async { onPressed: () => context.read<CalendarBloc>().add(
final result = await RowBackendService.deleteRow( CalendarEvent.deleteEvent(
rowController.viewId, rowController.viewId,
rowController.rowId, rowController.rowId,
); ),
result.fold((l) => null, (err) => Log.error(err)); ),
},
), ),
const HSpace(8.0), const HSpace(8.0),
FlowyIconButton( FlowyIconButton(
@ -107,12 +118,10 @@ class EventEditorControls extends StatelessWidget {
PopoverContainer.of(context).close(); PopoverContainer.of(context).close();
FlowyOverlay.show( FlowyOverlay.show(
context: context, context: context,
builder: (BuildContext context) { builder: (_) => RowDetailPage(
return RowDetailPage( databaseController: databaseController,
databaseController: databaseController, rowController: rowController,
rowController: rowController, ),
);
},
); );
}, },
), ),

View File

@ -81,9 +81,9 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
InlinePageReferenceService( InlinePageReferenceService(
currentViewId: documentBloc.view.id, currentViewId: documentBloc.view.id,
limitResults: 5, limitResults: 5,
).inlinePageReferenceDelegate, ),
DateReferenceService(context).dateReferenceDelegate, DateReferenceService(context),
ReminderReferenceService(context).reminderReferenceDelegate, ReminderReferenceService(context),
], ],
); );

View File

@ -32,13 +32,13 @@ Future<void> showLinkToPageMenu(
customTitle: titleFromPageType(pageType), customTitle: titleFromPageType(pageType),
insertPage: pageType != ViewLayoutPB.Document, insertPage: pageType != ViewLayoutPB.Document,
limitResults: 15, limitResults: 15,
).inlinePageReferenceDelegate, ),
], ],
); );
final List<InlineActionsResult> initialResults = []; final List<InlineActionsResult> initialResults = [];
for (final handler in service.handlers) { for (final handler in service.handlers) {
final group = await handler(); final group = await handler.search(null);
if (group.results.isNotEmpty) { if (group.results.isNotEmpty) {
initialResults.add(group); initialResults.add(group);

View File

@ -1,9 +1,10 @@
import 'package:flutter/material.dart';
import 'package:appflowy/plugins/inline_actions/handlers/inline_page_reference.dart'; import 'package:appflowy/plugins/inline_actions/handlers/inline_page_reference.dart';
import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';
import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';
import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
const _bracketChar = '['; const _bracketChar = '[';
const _plusChar = '+'; const _plusChar = '+';
@ -89,7 +90,7 @@ Future<bool> inlinePageReferenceCommandHandler(
InlinePageReferenceService( InlinePageReferenceService(
currentViewId: currentViewId, currentViewId: currentViewId,
limitResults: 10, limitResults: 10,
).inlinePageReferenceDelegate, ),
], ],
); );
@ -97,7 +98,7 @@ Future<bool> inlinePageReferenceCommandHandler(
final List<InlineActionsResult> initialResults = []; final List<InlineActionsResult> initialResults = [];
for (final handler in service.handlers) { for (final handler in service.handlers) {
final group = await handler(); final group = await handler.search(null);
if (group.results.isNotEmpty) { if (group.results.isNotEmpty) {
initialResults.add(group); initialResults.add(group);

View File

@ -1,17 +1,19 @@
import 'package:flutter/material.dart';
import 'package:appflowy/date/date_service.dart'; import 'package:appflowy/date/date_service.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';
import 'package:appflowy/plugins/inline_actions/service_handler.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
final _keywords = [ final _keywords = [
LocaleKeys.inlineActions_date.tr().toLowerCase(), LocaleKeys.inlineActions_date.tr().toLowerCase(),
]; ];
class DateReferenceService { class DateReferenceService extends InlineActionsDelegate {
DateReferenceService(this.context) { DateReferenceService(this.context) {
// Initialize locale // Initialize locale
_locale = context.locale.toLanguageTag(); _locale = context.locale.toLanguageTag();
@ -27,7 +29,8 @@ class DateReferenceService {
List<InlineActionsMenuItem> options = []; List<InlineActionsMenuItem> options = [];
Future<InlineActionsResult> dateReferenceDelegate([ @override
Future<InlineActionsResult> search([
String? search, String? search,
]) async { ]) async {
// Checks if Locale has changed since last // Checks if Locale has changed since last

View File

@ -9,16 +9,23 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/me
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';
import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';
import 'package:appflowy/plugins/inline_actions/service_handler.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy/workspace/application/workspace/workspace_listener.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart'; import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart';
import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flowy_infra_ui/widget/error_page.dart';
class InlinePageReferenceService { class InlinePageReferenceService extends InlineActionsDelegate {
InlinePageReferenceService({ InlinePageReferenceService({
required this.currentViewId, required this.currentViewId,
this.viewLayout, this.viewLayout,
@ -51,12 +58,67 @@ class InlinePageReferenceService {
List<InlineActionsMenuItem> _items = []; List<InlineActionsMenuItem> _items = [];
List<InlineActionsMenuItem> _filtered = []; List<InlineActionsMenuItem> _filtered = [];
UserProfilePB? _user;
String? _workspaceId;
WorkspaceListener? _listener;
Future<void> init() async { Future<void> init() async {
_items = await _generatePageItems(currentViewId, viewLayout); _items = await _generatePageItems(currentViewId, viewLayout);
_filtered = limitResults > 0 ? _items.take(limitResults).toList() : _items; _filtered = limitResults > 0 ? _items.take(limitResults).toList() : _items;
await _initWorkspaceListener();
_initCompleter.complete(); _initCompleter.complete();
} }
Future<void> _initWorkspaceListener() async {
final snapshot = await Future.wait([
FolderEventGetCurrentWorkspaceSetting().send(),
getIt<AuthService>().getUser(),
]);
final (workspaceSettings, userProfile) = (snapshot.first, snapshot.last);
_workspaceId = workspaceSettings.fold(
(s) => (s as WorkspaceSettingPB).workspaceId,
(e) => null,
);
_user = userProfile.fold((s) => s as UserProfilePB, (e) => null);
if (_user != null && _workspaceId != null) {
_listener = WorkspaceListener(
user: _user!,
workspaceId: _workspaceId!,
);
_listener!.start(
appsChanged: (_) async {
_items = await _generatePageItems(currentViewId, viewLayout);
_filtered =
limitResults > 0 ? _items.take(limitResults).toList() : _items;
},
);
}
}
@override
Future<InlineActionsResult> search([
String? search,
]) async {
_filtered = await _filterItems(search);
return InlineActionsResult(
title: customTitle?.isNotEmpty == true
? customTitle!
: LocaleKeys.inlineActions_pageReference.tr(),
results: _filtered,
);
}
@override
Future<void> dispose() async {
await _listener?.stop();
}
Future<List<InlineActionsMenuItem>> _filterItems(String? search) async { Future<List<InlineActionsMenuItem>> _filterItems(String? search) async {
await _initCompleter.future; await _initCompleter.future;
@ -76,19 +138,6 @@ class InlinePageReferenceService {
: items.toList(); : items.toList();
} }
Future<InlineActionsResult> inlinePageReferenceDelegate([
String? search,
]) async {
_filtered = await _filterItems(search);
return InlineActionsResult(
title: customTitle?.isNotEmpty == true
? customTitle!
: LocaleKeys.inlineActions_pageReference.tr(),
results: _filtered,
);
}
Future<List<InlineActionsMenuItem>> _generatePageItems( Future<List<InlineActionsMenuItem>> _generatePageItems(
String currentViewId, String currentViewId,
ViewLayoutPB? viewLayout, ViewLayoutPB? viewLayout,

View File

@ -1,9 +1,12 @@
import 'package:flutter/material.dart';
import 'package:appflowy/date/date_service.dart'; import 'package:appflowy/date/date_service.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/doc_bloc.dart'; import 'package:appflowy/plugins/document/application/doc_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';
import 'package:appflowy/plugins/inline_actions/service_handler.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:appflowy/user/application/reminder/reminder_extension.dart'; import 'package:appflowy/user/application/reminder/reminder_extension.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart';
@ -11,7 +14,6 @@ import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:nanoid/nanoid.dart'; import 'package:nanoid/nanoid.dart';
@ -20,7 +22,7 @@ final _keywords = [
LocaleKeys.inlineActions_reminder_shortKeyword.tr().toLowerCase(), LocaleKeys.inlineActions_reminder_shortKeyword.tr().toLowerCase(),
]; ];
class ReminderReferenceService { class ReminderReferenceService extends InlineActionsDelegate {
ReminderReferenceService(this.context) { ReminderReferenceService(this.context) {
// Initialize locale // Initialize locale
_locale = context.locale.toLanguageTag(); _locale = context.locale.toLanguageTag();
@ -36,7 +38,8 @@ class ReminderReferenceService {
List<InlineActionsMenuItem> options = []; List<InlineActionsMenuItem> options = [];
Future<InlineActionsResult> reminderReferenceDelegate([ @override
Future<InlineActionsResult> search([
String? search, String? search,
]) async { ]) async {
// Checks if Locale has changed since last // Checks if Locale has changed since last

View File

@ -41,7 +41,7 @@ Future<bool> inlineActionsCommandHandler(
final List<InlineActionsResult> initialResults = []; final List<InlineActionsResult> initialResults = [];
for (final handler in service.handlers) { for (final handler in service.handlers) {
final group = await handler(); final group = await handler.search(null);
if (group.results.isNotEmpty) { if (group.results.isNotEmpty) {
initialResults.add(group); initialResults.add(group);

View File

@ -1,9 +1,6 @@
import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
typedef InlineActionsDelegate = Future<InlineActionsResult> Function([ import 'package:appflowy/plugins/inline_actions/service_handler.dart';
String? search,
]);
abstract class _InlineActionsProvider { abstract class _InlineActionsProvider {
void dispose(); void dispose();
@ -26,7 +23,10 @@ class InlineActionsService extends _InlineActionsProvider {
/// we set the [BuildContext] to null. /// we set the [BuildContext] to null.
/// ///
@override @override
void dispose() { Future<void> dispose() async {
for (final handler in handlers) {
await handler.dispose();
}
context = null; context = null;
} }
} }

View File

@ -0,0 +1,7 @@
import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';
abstract class InlineActionsDelegate {
Future<InlineActionsResult> search(String? search);
Future<void> dispose() async {}
}

View File

@ -1,3 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';
import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';
@ -7,8 +10,6 @@ import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
/// All heights are in physical pixels /// All heights are in physical pixels
const double _groupTextHeight = 14; // 12 height + 2 bottom spacing const double _groupTextHeight = 14; // 12 height + 2 bottom spacing
@ -91,7 +92,7 @@ class _InlineActionsHandlerState extends State<InlineActionsHandler> {
Future<void> _doSearch() async { Future<void> _doSearch() async {
final List<InlineActionsResult> newResults = []; final List<InlineActionsResult> newResults = [];
for (final handler in widget.service.handlers) { for (final handler in widget.service.handlers) {
final group = await handler.call(_search); final group = await handler.search(_search);
if (group.results.isNotEmpty) { if (group.results.isNotEmpty) {
newResults.add(group); newResults.add(group);
@ -208,6 +209,10 @@ class _InlineActionsHandlerState extends State<InlineActionsHandler> {
results[groupIndex].results[handlerIndex]; results[groupIndex].results[handlerIndex];
KeyEventResult onKeyEvent(focus, KeyEvent event) { KeyEventResult onKeyEvent(focus, KeyEvent event) {
if (event is! KeyDownEvent) {
return KeyEventResult.ignored;
}
const moveKeys = [ const moveKeys = [
LogicalKeyboardKey.arrowUp, LogicalKeyboardKey.arrowUp,
LogicalKeyboardKey.arrowDown, LogicalKeyboardKey.arrowDown,

View File

@ -1,3 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/menu/menu_bloc.dart'; import 'package:appflowy/workspace/application/menu/menu_bloc.dart';
@ -8,17 +11,17 @@ import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
class PersonalFolder extends StatelessWidget { class PersonalFolder extends StatelessWidget {
const PersonalFolder({ const PersonalFolder({
super.key, super.key,
required this.views, required this.views,
this.isHoverEnabled = true,
}); });
final List<ViewPB> views; final List<ViewPB> views;
final bool isHoverEnabled;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -60,6 +63,7 @@ class PersonalFolder extends StatelessWidget {
}, },
onTertiarySelected: (view) => onTertiarySelected: (view) =>
context.read<TabsBloc>().openTab(view), context.read<TabsBloc>().openTab(view),
isHoverEnabled: isHoverEnabled,
), ),
), ),
], ],

View File

@ -1,3 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
@ -18,7 +22,6 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
show UserProfilePB; show UserProfilePB;
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
/// Home Sidebar is the left side bar of the home page. /// Home Sidebar is the left side bar of the home page.
@ -28,7 +31,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
/// - settings /// - settings
/// - scrollable document list /// - scrollable document list
/// - trash /// - trash
class HomeSideBar extends StatelessWidget { class HomeSideBar extends StatefulWidget {
const HomeSideBar({ const HomeSideBar({
super.key, super.key,
required this.userProfile, required this.userProfile,
@ -39,6 +42,43 @@ class HomeSideBar extends StatelessWidget {
final WorkspaceSettingPB workspaceSetting; final WorkspaceSettingPB workspaceSetting;
@override
State<HomeSideBar> createState() => _HomeSideBarState();
}
class _HomeSideBarState extends State<HomeSideBar> {
final _scrollController = ScrollController();
Timer? _srollDebounce;
bool isScrolling = false;
@override
void initState() {
super.initState();
_scrollController.addListener(_onScrollChanged);
}
void _onScrollChanged() {
setState(() => isScrolling = true);
_srollDebounce?.cancel();
_srollDebounce =
Timer(const Duration(milliseconds: 300), _setScrollStopped);
}
void _setScrollStopped() {
if (mounted) {
setState(() => isScrolling = false);
}
}
@override
void dispose() {
_srollDebounce?.cancel();
_scrollController.removeListener(_onScrollChanged);
_scrollController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MultiBlocProvider( return MultiBlocProvider(
@ -48,8 +88,8 @@ class HomeSideBar extends StatelessWidget {
), ),
BlocProvider( BlocProvider(
create: (_) => MenuBloc( create: (_) => MenuBloc(
user: userProfile, user: widget.userProfile,
workspaceId: workspaceSetting.workspaceId, workspaceId: widget.workspaceSetting.workspaceId,
)..add(const MenuEvent.initial()), )..add(const MenuEvent.initial()),
), ),
], ],
@ -108,8 +148,14 @@ class HomeSideBar extends StatelessWidget {
Padding( Padding(
padding: menuHorizontalInset, padding: menuHorizontalInset,
child: FeatureFlag.collaborativeWorkspace.isOn child: FeatureFlag.collaborativeWorkspace.isOn
? SidebarWorkspace(userProfile: userProfile, views: views) ? SidebarWorkspace(
: SidebarUser(userProfile: userProfile, views: views), userProfile: widget.userProfile,
views: views,
)
: SidebarUser(
userProfile: widget.userProfile,
views: views,
),
), ),
const VSpace(20), const VSpace(20),
@ -118,9 +164,12 @@ class HomeSideBar extends StatelessWidget {
child: Padding( child: Padding(
padding: menuHorizontalInset, padding: menuHorizontalInset,
child: SingleChildScrollView( child: SingleChildScrollView(
controller: _scrollController,
physics: const ClampingScrollPhysics(),
child: SidebarFolder( child: SidebarFolder(
views: views, views: views,
favoriteViews: favoriteViews, favoriteViews: favoriteViews,
isHoverEnabled: !isScrolling,
), ),
), ),
), ),

View File

@ -1,20 +1,23 @@
import 'package:flutter/material.dart';
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/favorite_folder.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/favorite_folder.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class SidebarFolder extends StatelessWidget { class SidebarFolder extends StatelessWidget {
const SidebarFolder({ const SidebarFolder({
super.key, super.key,
required this.views, required this.views,
required this.favoriteViews, required this.favoriteViews,
this.isHoverEnabled = true,
}); });
final List<ViewPB> views; final List<ViewPB> views;
final List<ViewPB> favoriteViews; final List<ViewPB> favoriteViews;
final bool isHoverEnabled;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -38,7 +41,7 @@ class SidebarFolder extends StatelessWidget {
const VSpace(10), const VSpace(10),
], ],
// personal // personal
PersonalFolder(views: views), PersonalFolder(views: views, isHoverEnabled: isHoverEnabled),
], ],
); );
}, },

View File

@ -42,6 +42,7 @@ class ViewItem extends StatelessWidget {
this.isDraggable = true, this.isDraggable = true,
required this.isFeedback, required this.isFeedback,
this.height = 28.0, this.height = 28.0,
this.isHoverEnabled = true,
}); });
final ViewPB view; final ViewPB view;
@ -75,6 +76,8 @@ class ViewItem extends StatelessWidget {
final double height; final double height;
final bool isHoverEnabled;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
@ -101,6 +104,7 @@ class ViewItem extends StatelessWidget {
isDraggable: isDraggable, isDraggable: isDraggable,
isFeedback: isFeedback, isFeedback: isFeedback,
height: height, height: height,
isHoverEnabled: isHoverEnabled,
); );
}, },
), ),
@ -127,6 +131,7 @@ class InnerViewItem extends StatelessWidget {
this.isFirstChild = false, this.isFirstChild = false,
required this.isFeedback, required this.isFeedback,
required this.height, required this.height,
this.isHoverEnabled = true,
}); });
final ViewPB view; final ViewPB view;
@ -148,6 +153,8 @@ class InnerViewItem extends StatelessWidget {
final ViewItemOnSelected? onTertiarySelected; final ViewItemOnSelected? onTertiarySelected;
final double height; final double height;
final bool isHoverEnabled;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget child = SingleInnerViewItem( Widget child = SingleInnerViewItem(
@ -264,6 +271,7 @@ class SingleInnerViewItem extends StatefulWidget {
this.onTertiarySelected, this.onTertiarySelected,
required this.isFeedback, required this.isFeedback,
required this.height, required this.height,
this.isHoverEnabled = true,
}); });
final ViewPB view; final ViewPB view;
@ -282,6 +290,8 @@ class SingleInnerViewItem extends StatefulWidget {
final FolderCategoryType categoryType; final FolderCategoryType categoryType;
final double height; final double height;
final bool isHoverEnabled;
@override @override
State<SingleInnerViewItem> createState() => _SingleInnerViewItemState(); State<SingleInnerViewItem> createState() => _SingleInnerViewItemState();
} }
@ -292,13 +302,16 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (widget.isFeedback) {
return _buildViewItem(false);
}
final isSelected = final isSelected =
getIt<MenuSharedState>().latestOpenView?.id == widget.view.id; getIt<MenuSharedState>().latestOpenView?.id == widget.view.id;
if (widget.isFeedback || !widget.isHoverEnabled) {
return _buildViewItem(
false,
!widget.isHoverEnabled ? isSelected : false,
);
}
return FlowyHover( return FlowyHover(
style: HoverStyle( style: HoverStyle(
hoverColor: Theme.of(context).colorScheme.secondary, hoverColor: Theme.of(context).colorScheme.secondary,

View File

@ -27,7 +27,8 @@ use crate::entities::{
UpdateViewParams, ViewPB, WorkspacePB, WorkspaceSettingPB, UpdateViewParams, ViewPB, WorkspacePB, WorkspaceSettingPB,
}; };
use crate::manager_observer::{ use crate::manager_observer::{
notify_child_views_changed, notify_parent_view_did_change, ChildViewChangeReason, notify_child_views_changed, notify_did_update_workspace, notify_parent_view_did_change,
ChildViewChangeReason,
}; };
use crate::notification::{ use crate::notification::{
send_notification, send_workspace_setting_notification, FolderNotification, send_notification, send_workspace_setting_notification, FolderNotification,
@ -991,7 +992,15 @@ impl FolderManager {
send_notification(&view_pb.id, FolderNotification::DidUpdateView) send_notification(&view_pb.id, FolderNotification::DidUpdateView)
.payload(view_pb) .payload(view_pb)
.send(); .send();
if let Ok(workspace_id) = self.get_current_workspace_id().await {
let folder = &self.mutex_folder.lock();
if let Some(folder) = folder.as_ref() {
notify_did_update_workspace(&workspace_id, folder);
}
}
} }
Ok(()) Ok(())
} }