diff --git a/frontend/appflowy_flutter/integration_test/database/database_calendar_test.dart b/frontend/appflowy_flutter/integration_test/database/database_calendar_test.dart index f882983712..33f06557be 100644 --- a/frontend/appflowy_flutter/integration_test/database/database_calendar_test.dart +++ b/frontend/appflowy_flutter/integration_test/database/database_calendar_test.dart @@ -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-folder/view.pbenum.dart'; +import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -147,6 +149,49 @@ void main() { 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 { await tester.initializeAppFlowy(); await tester.tapGoButton(); diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_bloc.dart index b8edc4229f..4a2674f1c5 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_bloc.dart @@ -64,6 +64,20 @@ class CalendarBloc extends Bloc { createEvent: (DateTime date) async { 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: () { emit(state.copyWith(editingEvent: null)); }, @@ -407,6 +421,12 @@ class CalendarEvent with _$CalendarEvent { const factory CalendarEvent.didReceiveDatabaseUpdate(DatabasePB database) = _ReceiveDatabaseUpdate; + + const factory CalendarEvent.duplicateEvent(String viewId, String rowId) = + _DuplicateEvent; + + const factory CalendarEvent.deleteEvent(String viewId, String rowId) = + _DeleteEvent; } @freezed diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_card.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_card.dart index 40769d2303..9542cafe98 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_card.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_card.dart @@ -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/plugins/database/application/database_controller.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_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import '../application/calendar_bloc.dart'; + import 'calendar_event_editor.dart'; class EventCard extends StatefulWidget { @@ -144,15 +146,18 @@ class _EventCardState extends State { asBarrier: true, margin: EdgeInsets.zero, offset: const Offset(10.0, 0), - popupBuilder: (BuildContext popoverContext) { + popupBuilder: (_) { final settings = context.watch().state.settings; if (settings == null) { return const SizedBox.shrink(); } - return CalendarEventEditor( - databaseController: widget.databaseController, - rowMeta: widget.event.event.rowMeta, - layoutSettings: settings, + return BlocProvider.value( + value: context.read(), + child: CalendarEventEditor( + databaseController: widget.databaseController, + rowMeta: widget.event.event.rowMeta, + layoutSettings: settings, + ), ); }, child: Container( diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_editor.dart index 5258e32d62..b701763a4a 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_editor.dart @@ -1,25 +1,25 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/flowy_svgs.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/database_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_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/widgets/cell/editable_cell_builder.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/cell/editable_cell_builder.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/util/field_type_extension.dart'; -import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class CalendarEventEditor extends StatelessWidget { @@ -86,17 +86,28 @@ class EventEditorControls extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ + FlowyIconButton( + width: 20, + icon: const FlowySvg(FlowySvgs.m_duplicate_s), + iconColorOnHover: Theme.of(context).colorScheme.onSecondary, + onPressed: () => context.read().add( + CalendarEvent.duplicateEvent( + rowController.viewId, + rowController.rowId, + ), + ), + ), + const HSpace(8.0), FlowyIconButton( width: 20, icon: const FlowySvg(FlowySvgs.delete_s), iconColorOnHover: Theme.of(context).colorScheme.onSecondary, - onPressed: () async { - final result = await RowBackendService.deleteRow( - rowController.viewId, - rowController.rowId, - ); - result.fold((l) => null, (err) => Log.error(err)); - }, + onPressed: () => context.read().add( + CalendarEvent.deleteEvent( + rowController.viewId, + rowController.rowId, + ), + ), ), const HSpace(8.0), FlowyIconButton( @@ -107,12 +118,10 @@ class EventEditorControls extends StatelessWidget { PopoverContainer.of(context).close(); FlowyOverlay.show( context: context, - builder: (BuildContext context) { - return RowDetailPage( - databaseController: databaseController, - rowController: rowController, - ); - }, + builder: (_) => RowDetailPage( + databaseController: databaseController, + rowController: rowController, + ), ); }, ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index 22dabd6517..e21e2fd313 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -81,9 +81,9 @@ class _AppFlowyEditorPageState extends State { InlinePageReferenceService( currentViewId: documentBloc.view.id, limitResults: 5, - ).inlinePageReferenceDelegate, - DateReferenceService(context).dateReferenceDelegate, - ReminderReferenceService(context).reminderReferenceDelegate, + ), + DateReferenceService(context), + ReminderReferenceService(context), ], ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart index bef7ca88d9..0dcb8dd3e6 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart @@ -32,13 +32,13 @@ Future showLinkToPageMenu( customTitle: titleFromPageType(pageType), insertPage: pageType != ViewLayoutPB.Document, limitResults: 15, - ).inlinePageReferenceDelegate, + ), ], ); final List initialResults = []; for (final handler in service.handlers) { - final group = await handler(); + final group = await handler.search(null); if (group.results.isNotEmpty) { initialResults.add(group); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/page_reference_commands.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/page_reference_commands.dart index f78b73bef7..42ee1a63d1 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/page_reference_commands.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/page_reference_commands.dart @@ -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/inline_actions_menu.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; const _bracketChar = '['; const _plusChar = '+'; @@ -89,7 +90,7 @@ Future inlinePageReferenceCommandHandler( InlinePageReferenceService( currentViewId: currentViewId, limitResults: 10, - ).inlinePageReferenceDelegate, + ), ], ); @@ -97,7 +98,7 @@ Future inlinePageReferenceCommandHandler( final List initialResults = []; for (final handler in service.handlers) { - final group = await handler(); + final group = await handler.search(null); if (group.results.isNotEmpty) { initialResults.add(group); diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/date_reference.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/date_reference.dart index 6d5012a5d6..6f3bf087a8 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/date_reference.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/date_reference.dart @@ -1,17 +1,19 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/date/date_service.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/mention/mention_block.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:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; final _keywords = [ LocaleKeys.inlineActions_date.tr().toLowerCase(), ]; -class DateReferenceService { +class DateReferenceService extends InlineActionsDelegate { DateReferenceService(this.context) { // Initialize locale _locale = context.locale.toLanguageTag(); @@ -27,7 +29,8 @@ class DateReferenceService { List options = []; - Future dateReferenceDelegate([ + @override + Future search([ String? search, ]) async { // Checks if Locale has changed since last diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart index caba7d6888..b8d94cee3c 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart @@ -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/inline_actions/inline_actions_menu.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_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-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:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; -class InlinePageReferenceService { +class InlinePageReferenceService extends InlineActionsDelegate { InlinePageReferenceService({ required this.currentViewId, this.viewLayout, @@ -51,12 +58,67 @@ class InlinePageReferenceService { List _items = []; List _filtered = []; + UserProfilePB? _user; + String? _workspaceId; + WorkspaceListener? _listener; + Future init() async { _items = await _generatePageItems(currentViewId, viewLayout); _filtered = limitResults > 0 ? _items.take(limitResults).toList() : _items; + + await _initWorkspaceListener(); + _initCompleter.complete(); } + Future _initWorkspaceListener() async { + final snapshot = await Future.wait([ + FolderEventGetCurrentWorkspaceSetting().send(), + getIt().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 search([ + String? search, + ]) async { + _filtered = await _filterItems(search); + + return InlineActionsResult( + title: customTitle?.isNotEmpty == true + ? customTitle! + : LocaleKeys.inlineActions_pageReference.tr(), + results: _filtered, + ); + } + + @override + Future dispose() async { + await _listener?.stop(); + } + Future> _filterItems(String? search) async { await _initCompleter.future; @@ -76,19 +138,6 @@ class InlinePageReferenceService { : items.toList(); } - Future inlinePageReferenceDelegate([ - String? search, - ]) async { - _filtered = await _filterItems(search); - - return InlineActionsResult( - title: customTitle?.isNotEmpty == true - ? customTitle! - : LocaleKeys.inlineActions_pageReference.tr(), - results: _filtered, - ); - } - Future> _generatePageItems( String currentViewId, ViewLayoutPB? viewLayout, diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/reminder_reference.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/reminder_reference.dart index cbb2c5e007..1fdda2a40e 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/reminder_reference.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/reminder_reference.dart @@ -1,9 +1,12 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/date/date_service.dart'; import 'package:appflowy/generated/locale_keys.g.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/mention/mention_block.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_extension.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:easy_localization/easy_localization.dart'; import 'package:fixnum/fixnum.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:nanoid/nanoid.dart'; @@ -20,7 +22,7 @@ final _keywords = [ LocaleKeys.inlineActions_reminder_shortKeyword.tr().toLowerCase(), ]; -class ReminderReferenceService { +class ReminderReferenceService extends InlineActionsDelegate { ReminderReferenceService(this.context) { // Initialize locale _locale = context.locale.toLanguageTag(); @@ -36,7 +38,8 @@ class ReminderReferenceService { List options = []; - Future reminderReferenceDelegate([ + @override + Future search([ String? search, ]) async { // Checks if Locale has changed since last diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_command.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_command.dart index c1004d63f2..845eaf8c69 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_command.dart @@ -41,7 +41,7 @@ Future inlineActionsCommandHandler( final List initialResults = []; for (final handler in service.handlers) { - final group = await handler(); + final group = await handler.search(null); if (group.results.isNotEmpty) { initialResults.add(group); diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_service.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_service.dart index d3a5623d2f..3bdd5bf61a 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_service.dart @@ -1,9 +1,6 @@ -import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart'; import 'package:flutter/material.dart'; -typedef InlineActionsDelegate = Future Function([ - String? search, -]); +import 'package:appflowy/plugins/inline_actions/service_handler.dart'; abstract class _InlineActionsProvider { void dispose(); @@ -26,7 +23,10 @@ class InlineActionsService extends _InlineActionsProvider { /// we set the [BuildContext] to null. /// @override - void dispose() { + Future dispose() async { + for (final handler in handlers) { + await handler.dispose(); + } context = null; } } diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/service_handler.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/service_handler.dart new file mode 100644 index 0000000000..de537fb964 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/service_handler.dart @@ -0,0 +1,7 @@ +import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart'; + +abstract class InlineActionsDelegate { + Future search(String? search); + + Future dispose() async {} +} diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_handler.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_handler.dart index 6f8babf22f..a19dffbd02 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_handler.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_handler.dart @@ -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/plugins/inline_actions/inline_actions_menu.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:easy_localization/easy_localization.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 const double _groupTextHeight = 14; // 12 height + 2 bottom spacing @@ -91,7 +92,7 @@ class _InlineActionsHandlerState extends State { Future _doSearch() async { final List newResults = []; for (final handler in widget.service.handlers) { - final group = await handler.call(_search); + final group = await handler.search(_search); if (group.results.isNotEmpty) { newResults.add(group); @@ -208,6 +209,10 @@ class _InlineActionsHandlerState extends State { results[groupIndex].results[handlerIndex]; KeyEventResult onKeyEvent(focus, KeyEvent event) { + if (event is! KeyDownEvent) { + return KeyEventResult.ignored; + } + const moveKeys = [ LogicalKeyboardKey.arrowUp, LogicalKeyboardKey.arrowDown, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart index 4aa2d41355..c8a44d2f75 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart @@ -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/locale_keys.g.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:easy_localization/easy_localization.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'; class PersonalFolder extends StatelessWidget { const PersonalFolder({ super.key, required this.views, + this.isHoverEnabled = true, }); final List views; + final bool isHoverEnabled; @override Widget build(BuildContext context) { @@ -60,6 +63,7 @@ class PersonalFolder extends StatelessWidget { }, onTertiarySelected: (view) => context.read().openTab(view), + isHoverEnabled: isHoverEnabled, ), ), ], diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart index 80cee5be5e..8a284464b8 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart @@ -1,3 +1,7 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/startup/startup.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; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; /// Home Sidebar is the left side bar of the home page. @@ -28,7 +31,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; /// - settings /// - scrollable document list /// - trash -class HomeSideBar extends StatelessWidget { +class HomeSideBar extends StatefulWidget { const HomeSideBar({ super.key, required this.userProfile, @@ -39,6 +42,43 @@ class HomeSideBar extends StatelessWidget { final WorkspaceSettingPB workspaceSetting; + @override + State createState() => _HomeSideBarState(); +} + +class _HomeSideBarState extends State { + 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 Widget build(BuildContext context) { return MultiBlocProvider( @@ -48,8 +88,8 @@ class HomeSideBar extends StatelessWidget { ), BlocProvider( create: (_) => MenuBloc( - user: userProfile, - workspaceId: workspaceSetting.workspaceId, + user: widget.userProfile, + workspaceId: widget.workspaceSetting.workspaceId, )..add(const MenuEvent.initial()), ), ], @@ -108,8 +148,14 @@ class HomeSideBar extends StatelessWidget { Padding( padding: menuHorizontalInset, child: FeatureFlag.collaborativeWorkspace.isOn - ? SidebarWorkspace(userProfile: userProfile, views: views) - : SidebarUser(userProfile: userProfile, views: views), + ? SidebarWorkspace( + userProfile: widget.userProfile, + views: views, + ) + : SidebarUser( + userProfile: widget.userProfile, + views: views, + ), ), const VSpace(20), @@ -118,9 +164,12 @@ class HomeSideBar extends StatelessWidget { child: Padding( padding: menuHorizontalInset, child: SingleChildScrollView( + controller: _scrollController, + physics: const ClampingScrollPhysics(), child: SidebarFolder( views: views, favoriteViews: favoriteViews, + isHoverEnabled: !isScrolling, ), ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_folder.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_folder.dart index 01ec648e74..397a3e3d90 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_folder.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_folder.dart @@ -1,20 +1,23 @@ +import 'package:flutter/material.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/sidebar/folder/favorite_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:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; class SidebarFolder extends StatelessWidget { const SidebarFolder({ super.key, required this.views, required this.favoriteViews, + this.isHoverEnabled = true, }); final List views; final List favoriteViews; + final bool isHoverEnabled; @override Widget build(BuildContext context) { @@ -38,7 +41,7 @@ class SidebarFolder extends StatelessWidget { const VSpace(10), ], // personal - PersonalFolder(views: views), + PersonalFolder(views: views, isHoverEnabled: isHoverEnabled), ], ); }, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart index 3a2afacbfe..2cdc373181 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart @@ -42,6 +42,7 @@ class ViewItem extends StatelessWidget { this.isDraggable = true, required this.isFeedback, this.height = 28.0, + this.isHoverEnabled = true, }); final ViewPB view; @@ -75,6 +76,8 @@ class ViewItem extends StatelessWidget { final double height; + final bool isHoverEnabled; + @override Widget build(BuildContext context) { return BlocProvider( @@ -101,6 +104,7 @@ class ViewItem extends StatelessWidget { isDraggable: isDraggable, isFeedback: isFeedback, height: height, + isHoverEnabled: isHoverEnabled, ); }, ), @@ -127,6 +131,7 @@ class InnerViewItem extends StatelessWidget { this.isFirstChild = false, required this.isFeedback, required this.height, + this.isHoverEnabled = true, }); final ViewPB view; @@ -148,6 +153,8 @@ class InnerViewItem extends StatelessWidget { final ViewItemOnSelected? onTertiarySelected; final double height; + final bool isHoverEnabled; + @override Widget build(BuildContext context) { Widget child = SingleInnerViewItem( @@ -264,6 +271,7 @@ class SingleInnerViewItem extends StatefulWidget { this.onTertiarySelected, required this.isFeedback, required this.height, + this.isHoverEnabled = true, }); final ViewPB view; @@ -282,6 +290,8 @@ class SingleInnerViewItem extends StatefulWidget { final FolderCategoryType categoryType; final double height; + final bool isHoverEnabled; + @override State createState() => _SingleInnerViewItemState(); } @@ -292,13 +302,16 @@ class _SingleInnerViewItemState extends State { @override Widget build(BuildContext context) { - if (widget.isFeedback) { - return _buildViewItem(false); - } - final isSelected = getIt().latestOpenView?.id == widget.view.id; + if (widget.isFeedback || !widget.isHoverEnabled) { + return _buildViewItem( + false, + !widget.isHoverEnabled ? isSelected : false, + ); + } + return FlowyHover( style: HoverStyle( hoverColor: Theme.of(context).colorScheme.secondary, diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index 89c89b9ff3..04c1b2b4fc 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -27,7 +27,8 @@ use crate::entities::{ UpdateViewParams, ViewPB, WorkspacePB, WorkspaceSettingPB, }; 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::{ send_notification, send_workspace_setting_notification, FolderNotification, @@ -991,7 +992,15 @@ impl FolderManager { send_notification(&view_pb.id, FolderNotification::DidUpdateView) .payload(view_pb) .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(()) }