diff --git a/frontend/appflowy_flutter/lib/mobile/application/recent/recent_view_bloc.dart b/frontend/appflowy_flutter/lib/mobile/application/recent/recent_view_bloc.dart index b45666ac4a..109c6e54b4 100644 --- a/frontend/appflowy_flutter/lib/mobile/application/recent/recent_view_bloc.dart +++ b/frontend/appflowy_flutter/lib/mobile/application/recent/recent_view_bloc.dart @@ -77,7 +77,7 @@ class RecentViewBloc extends Bloc { final ViewListener _viewListener; Future<(CoverType, String?)> getCover() async { - final result = await _service.getDocument(viewId: view.id); + final result = await _service.getDocument(documentId: view.id); final document = result.fold((s) => s.toDocument(), (f) => null); if (document != null) { final coverType = CoverType.fromString( diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart index b72b4b083c..fd4621be86 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart @@ -28,6 +28,7 @@ import 'package:flutter/material.dart' hide Card; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../../workspace/application/view/view_bloc.dart'; import '../../widgets/card/card.dart'; import '../../widgets/cell/card_cell_builder.dart'; import '../application/board_bloc.dart'; @@ -345,9 +346,12 @@ class _DesktopBoardContentState extends State { FlowyOverlay.show( context: context, - builder: (_) => RowDetailPage( - databaseController: databaseController, - rowController: rowController, + builder: (_) => BlocProvider.value( + value: context.read(), + child: RowDetailPage( + databaseController: databaseController, + rowController: rowController, + ), ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_hidden_groups.dart b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_hidden_groups.dart index 0469378c8b..443046f9a6 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_hidden_groups.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_hidden_groups.dart @@ -11,6 +11,7 @@ import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database/widgets/cell/card_cell_builder.dart'; import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/text_card_cell.dart'; import 'package:appflowy/plugins/database/widgets/row/row_detail.dart'; +import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:collection/collection.dart'; @@ -399,9 +400,12 @@ class HiddenGroupPopupItemList extends StatelessWidget { FlowyOverlay.show( context: context, builder: (_) { - return RowDetailPage( - databaseController: databaseController, - rowController: rowController, + return BlocProvider.value( + value: context.read(), + child: RowDetailPage( + databaseController: databaseController, + rowController: rowController, + ), ); }, ); 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 9542cafe98..7ef63aff08 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,16 +1,16 @@ -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'; import 'package:appflowy/plugins/database/widgets/card/card.dart'; import 'package:appflowy/plugins/database/widgets/cell/card_cell_builder.dart'; import 'package:appflowy/plugins/database/widgets/cell/card_cell_style_maps/calendar_card_cell_style.dart'; +import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; 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'; @@ -151,8 +151,15 @@ class _EventCardState extends State { if (settings == null) { return const SizedBox.shrink(); } - return BlocProvider.value( - value: context.read(), + return MultiBlocProvider( + providers: [ + BlocProvider.value( + value: context.read(), + ), + BlocProvider.value( + value: context.read(), + ), + ], child: CalendarEventEditor( databaseController: widget.databaseController, rowMeta: widget.event.event.rowMeta, 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 70cedae16b..10f078c805 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,3 +1,4 @@ +import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:flutter/material.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; @@ -125,9 +126,12 @@ class EventEditorControls extends StatelessWidget { PopoverContainer.of(context).close(); FlowyOverlay.show( context: context, - builder: (_) => RowDetailPage( - databaseController: databaseController, - rowController: rowController, + builder: (_) => BlocProvider.value( + value: context.read(), + child: RowDetailPage( + databaseController: databaseController, + rowController: rowController, + ), ), ); }, diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_page.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_page.dart index 1d1a147026..1b33f36e90 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_page.dart @@ -8,6 +8,7 @@ import 'package:appflowy/plugins/database/calendar/application/calendar_bloc.dar import 'package:appflowy/plugins/database/calendar/application/unschedule_event_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; +import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -353,9 +354,12 @@ void showEventDetails({ FlowyOverlay.show( context: context, builder: (BuildContext overlayContext) { - return RowDetailPage( - rowController: rowController, - databaseController: databaseController, + return BlocProvider.value( + value: context.read(), + child: RowDetailPage( + rowController: rowController, + databaseController: databaseController, + ), ); }, ); @@ -424,10 +428,13 @@ class _UnscheduledEventsButtonState extends State { ), ), ), - popupBuilder: (context) { - return UnscheduleEventsList( - databaseController: widget.databaseController, - unscheduleEvents: state.unscheduleEvents, + popupBuilder: (_) { + return BlocProvider.value( + value: context.read(), + child: UnscheduleEventsList( + databaseController: widget.databaseController, + unscheduleEvents: state.unscheduleEvents, + ), ); }, ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart index 44ff2147f9..2225aa96bf 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart @@ -1,3 +1,4 @@ +import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:flutter/material.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; @@ -186,9 +187,12 @@ class _GridPageState extends State { FlowyOverlay.show( context: context, - builder: (_) => RowDetailPage( - databaseController: context.read().databaseController, - rowController: rowController, + builder: (_) => BlocProvider.value( + value: context.read(), + child: RowDetailPage( + databaseController: context.read().databaseController, + rowController: rowController, + ), ), ); }); @@ -415,12 +419,15 @@ class _GridRowsState extends State<_GridRows> { isDraggable: isDraggable, rowController: rowController, cellBuilder: EditableCellBuilder(databaseController: databaseController), - openDetailPage: (context, cellBuilder) { + openDetailPage: (rowDetailContext) { FlowyOverlay.show( - context: context, - builder: (_) => RowDetailPage( - rowController: rowController, - databaseController: databaseController, + context: rowDetailContext, + builder: (_) => BlocProvider.value( + value: context.read(), + child: RowDetailPage( + rowController: rowController, + databaseController: databaseController, + ), ), ); }, diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/row.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/row.dart index d2c56dbfd8..340f613e84 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/row.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/row.dart @@ -39,7 +39,7 @@ class GridRow extends StatefulWidget { final RowId rowId; final RowController rowController; final EditableCellBuilder cellBuilder; - final void Function(BuildContext, EditableCellBuilder) openDetailPage; + final void Function(BuildContext context) openDetailPage; final int? index; final bool isDraggable; @@ -68,10 +68,7 @@ class _GridRowState extends State { child: RowContent( fieldController: widget.fieldController, cellBuilder: widget.cellBuilder, - onExpand: () => widget.openDetailPage( - context, - widget.cellBuilder, - ), + onExpand: () => widget.openDetailPage(context), ), ), ], diff --git a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart index 1595eff658..9dba23f044 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart @@ -5,6 +5,7 @@ import 'package:appflowy/plugins/shared/sync_indicator.dart'; import 'package:appflowy/plugins/util.dart'; import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; +import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view_info/view_info_bloc.dart'; import 'package:appflowy/workspace/presentation/home/home_stack.dart'; import 'package:appflowy/workspace/presentation/widgets/favorite_button.dart'; @@ -84,9 +85,17 @@ class _DatabaseTabBarViewState extends State { @override Widget build(BuildContext context) { - return BlocProvider( - create: (context) => DatabaseTabBarBloc(view: widget.view) - ..add(const DatabaseTabBarEvent.initial()), + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => DatabaseTabBarBloc(view: widget.view) + ..add(const DatabaseTabBarEvent.initial()), + ), + BlocProvider( + create: (context) => + ViewBloc(view: widget.view)..add(const ViewEvent.initial()), + ), + ], child: MultiBlocListener( listeners: [ BlocListener( diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/relation_row_detail.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/relation_row_detail.dart index e2c45ea8e5..256de6bc3c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/relation_row_detail.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/relation_row_detail.dart @@ -29,6 +29,7 @@ class RelatedRowDetailPage extends StatelessWidget { return RowDetailPage( databaseController: databaseController, rowController: rowController, + allowOpenAsFullPage: false, ); }, ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart index a4c9f596e3..82ae9b010f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart @@ -4,11 +4,17 @@ import 'package:appflowy/plugins/database/application/cell/cell_controller.dart' import 'package:appflowy/plugins/database/application/field/field_controller.dart'; import 'package:appflowy/plugins/database/application/row/row_banner_bloc.dart'; import 'package:appflowy/plugins/database/application/row/row_controller.dart'; +import 'package:appflowy/plugins/database/domain/database_view_service.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/cells/cell_container.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/row/row_action.dart'; +import 'package:appflowy/plugins/database_document/database_document_plugin.dart'; +import 'package:appflowy/startup/plugin/plugin.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; +import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -25,11 +31,13 @@ class RowBanner extends StatefulWidget { required this.fieldController, required this.rowController, required this.cellBuilder, + this.allowOpenAsFullPage = true, }); final FieldController fieldController; final RowController rowController; final EditableCellBuilder cellBuilder; + final bool allowOpenAsFullPage; @override State createState() => _RowBannerState(); @@ -84,6 +92,42 @@ class _RowBannerState extends State { right: 12, child: RowActionButton(rowController: widget.rowController), ), + if (widget.allowOpenAsFullPage) + Positioned( + top: 12, + left: 12, + child: FlowyIconButton( + width: 20, + height: 20, + icon: const FlowySvg(FlowySvgs.full_view_s), + onPressed: () async { + Navigator.of(context).pop(); + final viewBloc = context.read(); + final databaseId = await DatabaseViewBackendService( + viewId: widget.cellBuilder.databaseController.viewId, + ) + .getDatabaseId() + .then((value) => value.fold((s) => s, (f) => null)); + final documentId = widget.rowController.rowMeta.documentId; + if (databaseId != null) { + getIt().add( + TabsEvent.openPlugin( + plugin: DatabaseDocumentPlugin( + data: DatabaseDocumentContext( + view: viewBloc.state.view, + databaseId: databaseId, + rowId: widget.rowController.rowId, + documentId: documentId, + ), + pluginType: PluginType.databaseDocument, + ), + setLatest: false, + ), + ); + } + }, + ), + ), ], ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_detail.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_detail.dart index f973007471..845d2966f2 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_detail.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_detail.dart @@ -19,10 +19,12 @@ class RowDetailPage extends StatefulWidget with FlowyOverlayDelegate { super.key, required this.rowController, required this.databaseController, + this.allowOpenAsFullPage = true, }); final RowController rowController; final DatabaseController databaseController; + final bool allowOpenAsFullPage; @override State createState() => _RowDetailPageState(); @@ -60,6 +62,7 @@ class _RowDetailPageState extends State { fieldController: widget.databaseController.fieldController, rowController: widget.rowController, cellBuilder: cellBuilder, + allowOpenAsFullPage: widget.allowOpenAsFullPage, ), const VSpace(16), Padding( diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_document.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_document.dart index 0845a67c7a..6c5a9b1910 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_document.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_document.dart @@ -69,7 +69,7 @@ class _RowEditorState extends State { @override void initState() { super.initState(); - documentBloc = DocumentBloc(view: widget.viewPB) + documentBloc = DocumentBloc(documentId: widget.viewPB.id) ..add(const DocumentEvent.initial()); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_document/database_document_page.dart b/frontend/appflowy_flutter/lib/plugins/database_document/database_document_page.dart new file mode 100644 index 0000000000..9f600e4a4e --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_document/database_document_page.dart @@ -0,0 +1,217 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database/application/row/related_row_detail_bloc.dart'; +import 'package:appflowy/plugins/database/grid/application/row/row_detail_bloc.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_option_separator.dart'; +import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; +import 'package:appflowy/plugins/database/widgets/row/row_property.dart'; +import 'package:appflowy/plugins/document/application/document_bloc.dart'; +import 'package:appflowy/plugins/document/presentation/banner.dart'; +import 'package:appflowy/plugins/document/presentation/editor_notification.dart'; +import 'package:appflowy/plugins/document/presentation/editor_page.dart'; +import 'package:appflowy/plugins/document/presentation/editor_style.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart'; +import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide Log; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/widget/error_page.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +// This widget is largely copied from `plugins/document/document_page.dart` intentionally instead of opting for an abstraction. We can make an abstraction after the view refactor is done and there's more clarity in that department. + +class DatabaseDocumentPage extends StatefulWidget { + const DatabaseDocumentPage({ + super.key, + required this.view, + required this.databaseId, + required this.rowId, + required this.documentId, + this.initialSelection, + }); + + final ViewPB view; + final String databaseId; + final String rowId; + final String documentId; + final Selection? initialSelection; + + @override + State createState() => _DatabaseDocumentPageState(); +} + +class _DatabaseDocumentPageState extends State { + EditorState? editorState; + + @override + void initState() { + super.initState(); + EditorNotification.addListener(_onEditorNotification); + } + + @override + void dispose() { + EditorNotification.removeListener(_onEditorNotification); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider.value( + value: getIt(), + ), + BlocProvider( + create: (_) => DocumentBloc( + databaseViewId: widget.databaseId, + rowId: widget.rowId, + documentId: widget.documentId, + )..add(const DocumentEvent.initial()), + ), + ], + child: BlocBuilder( + builder: (context, state) { + if (state.isLoading) { + return const Center(child: CircularProgressIndicator.adaptive()); + } + + final editorState = state.editorState; + this.editorState = editorState; + final error = state.error; + if (error != null || editorState == null) { + Log.error(error); + return FlowyErrorPage.message( + error.toString(), + howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), + ); + } + + if (state.forceClose) { + return const SizedBox.shrink(); + } + + return BlocListener( + listener: _onNotificationAction, + listenWhen: (_, curr) => curr.action != null, + child: _buildEditorPage(context, state), + ); + }, + ), + ); + } + + Widget _buildEditorPage(BuildContext context, DocumentState state) { + final appflowyEditorPage = AppFlowyEditorPage( + editorState: state.editorState!, + styleCustomizer: EditorStyleCustomizer( + context: context, + // the 44 is the width of the left action list + padding: EditorStyleCustomizer.documentPadding, + ), + header: _buildDatabaseDataContent(context, state.editorState!), + initialSelection: widget.initialSelection, + useViewInfoBloc: false, + ); + + return Column( + children: [ + // Only show the indicator in integration test mode + // if (FlowyRunner.currentMode.isIntegrationTest) + // const DocumentSyncIndicator(), + + if (state.isDeleted) _buildBanner(context), + Expanded(child: appflowyEditorPage), + ], + ); + } + + Widget _buildDatabaseDataContent( + BuildContext context, + EditorState editorState, + ) { + return BlocProvider( + create: (context) => RelatedRowDetailPageBloc( + databaseId: widget.databaseId, + initialRowId: widget.rowId, + ), + child: BlocBuilder( + builder: (context, state) { + return state.when( + loading: () => const SizedBox.shrink(), + ready: (databaseController, rowController) { + return BlocProvider( + create: (context) => RowDetailBloc( + fieldController: databaseController.fieldController, + rowController: rowController, + ), + child: Padding( + padding: EdgeInsets.only( + top: 24, + left: EditorStyleCustomizer.documentPadding.left + 16 + 6, + right: EditorStyleCustomizer.documentPadding.right, + ), + child: Column( + children: [ + RowPropertyList( + viewId: databaseController.viewId, + fieldController: databaseController.fieldController, + cellBuilder: EditableCellBuilder( + databaseController: databaseController, + ), + ), + const TypeOptionSeparator(spacing: 24.0), + ], + ), + ), + ); + }, + ); + }, + ), + ); + } + + Widget _buildBanner(BuildContext context) { + return DocumentBanner( + onRestore: () => context.read().add( + const DocumentEvent.restorePage(), + ), + onDelete: () => context.read().add( + const DocumentEvent.deletePermanently(), + ), + ); + } + + void _onEditorNotification(EditorNotificationType type) { + final editorState = this.editorState; + if (editorState == null) { + return; + } + if (type == EditorNotificationType.undo) { + undoCommand.execute(editorState); + } else if (type == EditorNotificationType.redo) { + redoCommand.execute(editorState); + } else if (type == EditorNotificationType.exitEditing) { + editorState.selection = null; + } + } + + void _onNotificationAction( + BuildContext context, + ActionNavigationState state, + ) { + if (state.action != null && state.action!.type == ActionType.jumpToBlock) { + final path = state.action?.arguments?[ActionArgumentKeys.nodePath]; + + final editorState = context.read().state.editorState; + if (editorState != null && widget.documentId == state.action?.objectId) { + editorState.updateSelectionWithReason( + Selection.collapsed(Position(path: [path])), + ); + } + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_document/database_document_plugin.dart b/frontend/appflowy_flutter/lib/plugins/database_document/database_document_plugin.dart new file mode 100644 index 0000000000..ce3668b43d --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_document/database_document_plugin.dart @@ -0,0 +1,131 @@ +library document_plugin; + +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; +import 'package:appflowy/startup/plugin/plugin.dart'; +import 'package:appflowy/workspace/presentation/home/home_stack.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'database_document_page.dart'; +import 'presentation/database_document_title.dart'; + +// This widget is largely copied from `plugins/document/document_plugin.dart` intentionally instead of opting for an abstraction. We can make an abstraction after the view refactor is done and there's more clarity in that department. + +class DatabaseDocumentContext { + DatabaseDocumentContext({ + required this.view, + required this.databaseId, + required this.rowId, + required this.documentId, + }); + + final ViewPB view; + final String databaseId; + final String rowId; + final String documentId; +} + +class DatabaseDocumentPluginBuilder extends PluginBuilder { + @override + Plugin build(dynamic data) { + if (data is DatabaseDocumentContext) { + return DatabaseDocumentPlugin(pluginType: pluginType, data: data); + } + + throw FlowyPluginException.invalidData; + } + + @override + String get menuName => LocaleKeys.document_menuName.tr(); + + @override + FlowySvgData get icon => FlowySvgs.document_s; + + @override + PluginType get pluginType => PluginType.databaseDocument; +} + +class DatabaseDocumentPlugin extends Plugin { + DatabaseDocumentPlugin({ + required this.data, + required PluginType pluginType, + this.initialSelection, + }) : _pluginType = pluginType; + + final DatabaseDocumentContext data; + final PluginType _pluginType; + + final Selection? initialSelection; + + @override + PluginWidgetBuilder get widgetBuilder => DatabaseDocumentPluginWidgetBuilder( + view: data.view, + databaseId: data.databaseId, + rowId: data.rowId, + documentId: data.documentId, + initialSelection: initialSelection, + ); + + @override + PluginType get pluginType => _pluginType; + + @override + PluginId get id => data.rowId; +} + +class DatabaseDocumentPluginWidgetBuilder extends PluginWidgetBuilder + with NavigationItem { + DatabaseDocumentPluginWidgetBuilder({ + required this.view, + required this.databaseId, + required this.rowId, + required this.documentId, + this.initialSelection, + }); + + final ViewPB view; + final String databaseId; + final String rowId; + final String documentId; + final Selection? initialSelection; + + @override + EdgeInsets get contentPadding => EdgeInsets.zero; + + @override + Widget buildWidget({PluginContext? context, required bool shrinkWrap}) { + return BlocBuilder( + builder: (_, state) => DatabaseDocumentPage( + key: ValueKey(documentId), + view: view, + databaseId: databaseId, + documentId: documentId, + rowId: rowId, + initialSelection: initialSelection, + ), + ); + } + + @override + Widget get leftBarItem => + ViewTitleBarWithRow(view: view, databaseId: databaseId, rowId: rowId); + + @override + Widget tabBarItem(String pluginId) => const SizedBox.shrink(); + + @override + Widget? get rightBarItem => const SizedBox.shrink(); + + @override + List get navigationItems => [this]; +} + +class DatabaseDocumentPluginConfig implements PluginConfig { + @override + bool get creatable => false; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_document/presentation/database_document_title.dart b/frontend/appflowy_flutter/lib/plugins/database_document/presentation/database_document_title.dart new file mode 100644 index 0000000000..47cf99b293 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_document/presentation/database_document_title.dart @@ -0,0 +1,380 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/base/emoji/emoji_text.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/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/cells/cell_container.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart'; +import 'package:appflowy/startup/tasks/app_window_size_manager.dart'; +import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; +import 'package:appflowy/workspace/application/view/view_listener.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.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:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'database_document_title_bloc.dart'; + +// This widget is largely copied from `workspace/presentation/widgets/view_title_bar.dart` intentionally instead of opting for an abstraction. We can make an abstraction after the view refactor is done and there's more clarity in that department. + +// workspaces / ... / database view name / row name +class ViewTitleBarWithRow extends StatelessWidget { + const ViewTitleBarWithRow({ + super.key, + required this.view, + required this.databaseId, + required this.rowId, + }); + + final ViewPB view; + final String databaseId; + final String rowId; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => DatabaseDocumentTitleBloc( + view: view, + rowId: rowId, + ), + child: BlocBuilder( + builder: (context, state) { + if (state.ancestors.isEmpty) { + return const SizedBox.shrink(); + } + const maxWidth = WindowSizeManager.minWindowWidth - 200; + return LayoutBuilder( + builder: (context, constraints) { + return Visibility( + visible: maxWidth < constraints.maxWidth, + // if the width is too small, only show one view title bar without the ancestors + replacement: _ViewTitle( + key: ValueKey(state.ancestors.last), + view: state.ancestors.last, + maxTitleWidth: constraints.maxWidth - 50.0, + onUpdated: () {}, + ), + child: Row( + // refresh the view title bar when the ancestors changed + key: ValueKey(state.ancestors.hashCode), + children: _buildViewTitles(state.ancestors), + ), + ); + }, + ); + }, + ), + ); + } + + List _buildViewTitles(List views) { + // if the level is too deep, only show the root view, the database view and the row + return views.length > 2 + ? [ + _buildViewButton(views.first), + const FlowyText.regular('/'), + const FlowyText.regular(' ... /'), + _buildViewButton(views.last), + const FlowyText.regular('/'), + _buildRowName(), + ] + : [ + ...views + .map((e) => [_buildViewButton(e), const FlowyText.regular('/')]) + .flattened, + _buildRowName(), + ]; + } + + Widget _buildViewButton(ViewPB view) { + return FlowyTooltip( + message: view.name, + child: _ViewTitle( + view: view, + behavior: _ViewTitleBehavior.uneditable, + onUpdated: () {}, + ), + ); + } + + Widget _buildRowName() { + return BlocBuilder( + builder: (context, state) { + if (state.databaseController == null) { + return const SizedBox.shrink(); + } + return _RowName( + cellBuilder: EditableCellBuilder( + databaseController: state.databaseController!, + ), + primaryFieldId: state.fieldId!, + rowId: rowId, + ); + }, + ); + } +} + +class _RowName extends StatelessWidget { + const _RowName({ + required this.cellBuilder, + required this.primaryFieldId, + required this.rowId, + }); + + final EditableCellBuilder cellBuilder; + final String primaryFieldId; + final String rowId; + + @override + Widget build(BuildContext context) { + return cellBuilder.buildCustom( + CellContext( + fieldId: primaryFieldId, + rowId: rowId, + ), + skinMap: EditableCellSkinMap(textSkin: _TitleSkin()), + ); + } +} + +class _TitleSkin extends IEditableTextCellSkin { + @override + Widget build( + BuildContext context, + CellContainerNotifier cellContainerNotifier, + TextCellBloc bloc, + FocusNode focusNode, + TextEditingController textEditingController, + ) { + return BlocSelector( + selector: (state) => state.content, + builder: (context, content) { + final name = content.isEmpty + ? LocaleKeys.grid_row_titlePlaceholder.tr() + : content; + return BlocBuilder( + builder: (context, state) { + return FlowyTooltip( + message: name, + child: AppFlowyPopover( + constraints: const BoxConstraints( + maxWidth: 300, + maxHeight: 44, + ), + direction: PopoverDirection.bottomWithLeftAligned, + offset: const Offset(0, 18), + popupBuilder: (_) { + return RenameRowPopover( + textController: textEditingController, + icon: state.icon ?? "", + onUpdateIcon: (String icon) { + context + .read() + .add(DatabaseDocumentTitleEvent.updateIcon(icon)); + }, + onUpdateName: (text) => + bloc.add(TextCellEvent.updateText(text)), + ); + }, + child: FlowyButton( + useIntrinsicWidth: true, + onTap: () {}, + text: Row( + children: [ + EmojiText( + emoji: state.icon ?? "", + fontSize: 18.0, + ), + const HSpace(2.0), + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 180), + child: FlowyText.regular( + name, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ), + ); + }, + ); + }, + ); + } +} + +enum _ViewTitleBehavior { + editable, + uneditable, +} + +class _ViewTitle extends StatefulWidget { + const _ViewTitle({ + super.key, + required this.view, + this.behavior = _ViewTitleBehavior.editable, + this.maxTitleWidth = 180, + required this.onUpdated, + }); + + final ViewPB view; + final _ViewTitleBehavior behavior; + final double maxTitleWidth; + final VoidCallback onUpdated; + + @override + State<_ViewTitle> createState() => _ViewTitleState(); +} + +class _ViewTitleState extends State<_ViewTitle> { + late final viewListener = ViewListener(viewId: widget.view.id); + + String name = ''; + String icon = ''; + + @override + void initState() { + super.initState(); + + name = widget.view.name.isEmpty + ? LocaleKeys.document_title_placeholder.tr() + : widget.view.name; + icon = widget.view.icon.value; + + viewListener.start( + onViewUpdated: (view) { + if (name != view.name || icon != view.icon.value) { + widget.onUpdated(); + } + setState(() { + name = view.name.isEmpty + ? LocaleKeys.document_title_placeholder.tr() + : view.name; + icon = view.icon.value; + }); + }, + ); + } + + @override + void dispose() { + viewListener.stop(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + // root view + if (widget.view.parentViewId.isEmpty) { + return Row( + children: [ + FlowyText.regular(name), + const HSpace(4.0), + ], + ); + } + + final child = Row( + children: [ + EmojiText( + emoji: icon, + fontSize: 18.0, + ), + const HSpace(2.0), + ConstrainedBox( + constraints: BoxConstraints( + maxWidth: widget.maxTitleWidth, + ), + child: FlowyText.regular( + name, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ); + + return Listener( + onPointerDown: (_) => context.read().openPlugin(widget.view), + child: FlowyButton( + useIntrinsicWidth: true, + onTap: () {}, + text: child, + ), + ); + } +} + +class RenameRowPopover extends StatefulWidget { + const RenameRowPopover({ + super.key, + required this.textController, + required this.onUpdateName, + required this.onUpdateIcon, + required this.icon, + }); + + final TextEditingController textController; + final String icon; + + final void Function(String name) onUpdateName; + final void Function(String icon) onUpdateIcon; + + @override + State createState() => _RenameRowPopoverState(); +} + +class _RenameRowPopoverState extends State { + @override + void initState() { + super.initState(); + widget.textController.selection = TextSelection( + baseOffset: 0, + extentOffset: widget.textController.value.text.characters.length, + ); + } + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + EmojiPickerButton( + emoji: widget.icon, + direction: PopoverDirection.bottomWithCenterAligned, + offset: const Offset(0, 18), + defaultIcon: const FlowySvg(FlowySvgs.document_s), + onSubmitted: (emoji, _) { + widget.onUpdateIcon(emoji); + PopoverContainer.of(context).close(); + }, + ), + const HSpace(6), + SizedBox( + height: 36.0, + width: 220, + child: FlowyTextField( + controller: widget.textController, + maxLength: 256, + onSubmitted: (text) { + widget.onUpdateName(text); + PopoverContainer.of(context).close(); + }, + onCanceled: () => widget.onUpdateName(widget.textController.text), + showCounter: false, + ), + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_document/presentation/database_document_title_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_document/presentation/database_document_title_bloc.dart new file mode 100644 index 0000000000..5f8bf7ca08 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_document/presentation/database_document_title_bloc.dart @@ -0,0 +1,166 @@ +import 'package:appflowy/plugins/database/application/database_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/domain/field_service.dart'; +import 'package:appflowy/plugins/database/domain/row_meta_listener.dart'; +import 'package:appflowy/workspace/application/view/view_service.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'database_document_title_bloc.freezed.dart'; + +class DatabaseDocumentTitleBloc + extends Bloc { + DatabaseDocumentTitleBloc({ + required this.view, + required this.rowId, + }) : _metaListener = RowMetaListener(rowId), + super(DatabaseDocumentTitleState.initial()) { + _dispatch(); + _startListening(); + _init(); + } + + final ViewPB view; + final String rowId; + final RowMetaListener _metaListener; + + void _dispatch() { + on((event, emit) async { + event.when( + didUpdateAncestors: (ancestors) { + emit( + state.copyWith( + ancestors: ancestors, + ), + ); + }, + didUpdateRowTitleInfo: (databaseController, rowController, fieldId) { + emit( + state.copyWith( + databaseController: databaseController, + rowController: rowController, + fieldId: fieldId, + ), + ); + }, + didUpdateRowIcon: (icon) { + emit( + state.copyWith( + icon: icon, + ), + ); + }, + updateIcon: (icon) { + _updateMeta(icon); + }, + ); + }); + } + + void _startListening() { + _metaListener.start( + callback: (rowMeta) { + if (!isClosed) { + add(DatabaseDocumentTitleEvent.didUpdateRowIcon(rowMeta.icon)); + } + }, + ); + } + + void _init() async { + // get the database controller, row controller and primary field id + final databaseController = DatabaseController(view: view); + await databaseController.open().fold( + (s) => databaseController.setIsLoading(false), + (f) => null, + ); + final rowInfo = databaseController.rowCache.getRow(rowId); + if (rowInfo == null) { + return; + } + final rowController = RowController( + rowMeta: rowInfo.rowMeta, + viewId: view.id, + rowCache: databaseController.rowCache, + ); + final primaryFieldId = + await FieldBackendService.getPrimaryField(viewId: view.id).fold( + (primaryField) => primaryField.id, + (r) { + Log.error(r); + return null; + }, + ); + if (primaryFieldId != null) { + add( + DatabaseDocumentTitleEvent.didUpdateRowTitleInfo( + databaseController, + rowController, + primaryFieldId, + ), + ); + } + + // load ancestors + final ancestors = await ViewBackendService.getViewAncestors(view.id) + .fold((s) => s.items, (f) => []); + add(DatabaseDocumentTitleEvent.didUpdateAncestors(ancestors)); + + // initialize icon + if (rowInfo.rowMeta.icon.isNotEmpty) { + add(DatabaseDocumentTitleEvent.didUpdateRowIcon(rowInfo.rowMeta.icon)); + } + } + + /// Update the meta of the row and the view + void _updateMeta(String iconURL) { + RowBackendService(viewId: view.id) + .updateMeta( + iconURL: iconURL, + rowId: rowId, + ) + .fold((l) => null, (err) => Log.error(err)); + } +} + +@freezed +class DatabaseDocumentTitleEvent with _$DatabaseDocumentTitleEvent { + const factory DatabaseDocumentTitleEvent.didUpdateAncestors( + List ancestors, + ) = _DidUpdateAncestors; + const factory DatabaseDocumentTitleEvent.didUpdateRowTitleInfo( + DatabaseController databaseController, + RowController rowController, + String fieldId, + ) = _DidUpdateRowTitleInfo; + const factory DatabaseDocumentTitleEvent.didUpdateRowIcon( + String icon, + ) = _DidUpdateRowIcon; + const factory DatabaseDocumentTitleEvent.updateIcon( + String icon, + ) = _UpdateIcon; +} + +@freezed +class DatabaseDocumentTitleState with _$DatabaseDocumentTitleState { + const factory DatabaseDocumentTitleState({ + required List ancestors, + required DatabaseController? databaseController, + required RowController? rowController, + required String? fieldId, + required String? icon, + }) = _DatabaseDocumentTitleState; + + factory DatabaseDocumentTitleState.initial() => + const DatabaseDocumentTitleState( + ancestors: [], + databaseController: null, + rowController: null, + fieldId: null, + icon: null, + ); +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart index d511ca11d1..b5ecab33e3 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart @@ -22,7 +22,6 @@ import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-document/protobuf.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-user/protobuf.dart'; import 'package:appflowy_editor/appflowy_editor.dart' show @@ -41,19 +40,27 @@ part 'document_bloc.freezed.dart'; class DocumentBloc extends Bloc { DocumentBloc({ - required this.view, - }) : _documentListener = DocumentListener(id: view.id), - _syncStateListener = DocumentSyncStateListener(id: view.id), - _viewListener = ViewListener(viewId: view.id), + required this.documentId, + this.databaseViewId, + this.rowId, + }) : _documentListener = DocumentListener(id: documentId), + _syncStateListener = DocumentSyncStateListener(id: documentId), super(DocumentState.initial()) { + _viewListener = databaseViewId == null && rowId == null + ? ViewListener(viewId: documentId) + : null; on(_onDocumentEvent); } - final ViewPB view; + /// For a normal document, the document id is the same as the view id + final String documentId; + + final String? databaseViewId; + final String? rowId; final DocumentListener _documentListener; final DocumentSyncStateListener _syncStateListener; - final ViewListener _viewListener; + late final ViewListener? _viewListener; final DocumentService _documentService = DocumentService(); final TrashService _trashService = TrashService(); @@ -61,7 +68,7 @@ class DocumentBloc extends Bloc { late DocumentCollabAdapter _documentCollabAdapter; late final TransactionAdapter _transactionAdapter = TransactionAdapter( - documentId: view.id, + documentId: documentId, documentService: _documentService, ); @@ -85,9 +92,9 @@ class DocumentBloc extends Bloc { Future close() async { await _documentListener.stop(); await _syncStateListener.stop(); - await _viewListener.stop(); + await _viewListener?.stop(); await _transactionSubscription?.cancel(); - await _documentService.closeDocument(view: view); + await _documentService.closeDocument(viewId: documentId); _syncTimer?.cancel(); _syncTimer = null; state.editorState?.service.keyboardService?.closeKeyboard(); @@ -134,14 +141,18 @@ class DocumentBloc extends Bloc { emit(state.copyWith(isDeleted: false)); }, deletePermanently: () async { - final result = await _trashService.deleteViews([view.id]); - final forceClose = result.fold((l) => true, (r) => false); - emit(state.copyWith(forceClose: forceClose)); + if (databaseViewId == null && rowId == null) { + final result = await _trashService.deleteViews([documentId]); + final forceClose = result.fold((l) => true, (r) => false); + emit(state.copyWith(forceClose: forceClose)); + } }, restorePage: () async { - final result = await _trashService.putback(view.id); - final isDeleted = result.fold((l) => false, (r) => true); - emit(state.copyWith(isDeleted: isDeleted)); + if (databaseViewId == null && rowId == null) { + final result = await _trashService.putback(documentId); + final isDeleted = result.fold((l) => false, (r) => true); + emit(state.copyWith(isDeleted: isDeleted)); + } }, syncStateChanged: (syncState) { emit(state.copyWith(syncState: syncState.value)); @@ -149,7 +160,7 @@ class DocumentBloc extends Bloc { clearAwarenessStates: () async { // sync a null selection and a null meta to clear the awareness states await _documentService.syncAwarenessStates( - documentId: view.id, + documentId: documentId, ); }, syncAwarenessStates: () async { @@ -160,7 +171,7 @@ class DocumentBloc extends Bloc { /// subscribe to the view(document page) change void _onViewChanged() { - _viewListener.start( + _viewListener?.start( onViewMoveToTrash: (r) { r.map((r) => add(const DocumentEvent.moveToTrash())); }, @@ -202,7 +213,7 @@ class DocumentBloc extends Bloc { /// Fetch document Future> _fetchDocumentState() async { - final result = await _documentService.openDocument(viewId: view.id); + final result = await _documentService.openDocument(documentId: documentId); return result.fold( (s) async => FlowyResult.success(await _initAppFlowyEditorState(s)), (e) => FlowyResult.failure(e), @@ -218,7 +229,7 @@ class DocumentBloc extends Bloc { final editorState = EditorState(document: document); - _documentCollabAdapter = DocumentCollabAdapter(editorState, view.id); + _documentCollabAdapter = DocumentCollabAdapter(editorState, documentId); // subscribe to the document change from the editor _transactionSubscription = editorState.transactionStream.listen( @@ -358,7 +369,7 @@ class DocumentBloc extends Bloc { userAvatar: user.iconUrl, ); await _documentService.syncAwarenessStates( - documentId: view.id, + documentId: documentId, selection: selection, metadata: jsonEncode(metadata.toJson()), ); @@ -381,7 +392,7 @@ class DocumentBloc extends Bloc { userAvatar: user.iconUrl, ); await _documentService.syncAwarenessStates( - documentId: view.id, + documentId: documentId, metadata: jsonEncode(metadata.toJson()), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_collab_adapter.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_collab_adapter.dart index 738549875d..7feae779cc 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_collab_adapter.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_collab_adapter.dart @@ -29,7 +29,7 @@ class DocumentCollabAdapter { /// /// Only use in development Future syncV1() async { - final result = await _service.getDocument(viewId: docId); + final result = await _service.getDocument(documentId: docId); final document = result.fold((s) => s.toDocument(), (f) => null); if (document == null) { return null; @@ -69,7 +69,7 @@ class DocumentCollabAdapter { /// /// Diff the local document with the remote document and apply the changes Future syncV3({DocEventPB? docEvent}) async { - final result = await _service.getDocument(viewId: docId); + final result = await _service.getDocument(documentId: docId); final document = result.fold((s) => s.toDocument(), (f) => null); if (document == null) { return; @@ -104,7 +104,7 @@ class DocumentCollabAdapter { } Future forceReload() async { - final result = await _service.getDocument(viewId: docId); + final result = await _service.getDocument(documentId: docId); final document = result.fold((s) => s.toDocument(), (f) => null); if (document == null) { return; diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_service.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_service.dart index cf60a3c474..6a0b79c90e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_service.dart @@ -11,7 +11,7 @@ class DocumentService { Future> createDocument({ required ViewPB view, }) async { - final canOpen = await openDocument(viewId: view.id); + final canOpen = await openDocument(documentId: view.id); if (canOpen.isSuccess) { return FlowyResult.success(null); } @@ -21,17 +21,17 @@ class DocumentService { } Future> openDocument({ - required String viewId, + required String documentId, }) async { - final payload = OpenDocumentPayloadPB()..documentId = viewId; + final payload = OpenDocumentPayloadPB()..documentId = documentId; final result = await DocumentEventOpenDocument(payload).send(); return result; } Future> getDocument({ - required String viewId, + required String documentId, }) async { - final payload = OpenDocumentPayloadPB()..documentId = viewId; + final payload = OpenDocumentPayloadPB()..documentId = documentId; final result = await DocumentEventGetDocumentData(payload).send(); return result; } @@ -54,9 +54,9 @@ class DocumentService { } Future> closeDocument({ - required ViewPB view, + required String viewId, }) async { - final payload = ViewIdPB()..value = view.id; + final payload = ViewIdPB()..value = viewId; final result = await FolderEventCloseView(payload).send(); return result; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/document.dart b/frontend/appflowy_flutter/lib/plugins/document/document.dart index 9c56434655..e1501d6dbf 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document.dart @@ -40,13 +40,13 @@ class DocumentPluginBuilder extends PluginBuilder { FlowySvgData get icon => FlowySvgs.document_s; @override - PluginType get pluginType => PluginType.editor; + PluginType get pluginType => PluginType.document; @override ViewLayoutPB? get layoutType => ViewLayoutPB.Document; } -class DocumentPlugin extends Plugin { +class DocumentPlugin extends Plugin { DocumentPlugin({ required ViewPB view, required PluginType pluginType, diff --git a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart index 404dedda9c..d4de8314a2 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart @@ -36,7 +36,7 @@ class DocumentPage extends StatefulWidget { class _DocumentPageState extends State with WidgetsBindingObserver { EditorState? editorState; - late final documentBloc = DocumentBloc(view: widget.view) + late final documentBloc = DocumentBloc(documentId: widget.view.id) ..add(const DocumentEvent.initial()); @override 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 761e7d42d1..839831f220 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -76,6 +76,7 @@ class AppFlowyEditorPage extends StatefulWidget { this.showParagraphPlaceholder, this.placeholderText, this.initialSelection, + this.useViewInfoBloc = true, }); final Widget? header; @@ -91,6 +92,8 @@ class AppFlowyEditorPage extends StatefulWidget { /// final Selection? initialSelection; + final bool useViewInfoBloc; + @override State createState() => _AppFlowyEditorPageState(); } @@ -101,7 +104,7 @@ class _AppFlowyEditorPageState extends State { late final InlineActionsService inlineActionsService = InlineActionsService( context: context, handlers: [ - InlinePageReferenceService(currentViewId: documentBloc.view.id), + InlinePageReferenceService(currentViewId: documentBloc.documentId), DateReferenceService(context), ReminderReferenceService(context), ], @@ -186,14 +189,14 @@ class _AppFlowyEditorPageState extends State { /// - Using `[[` pageReferenceShortcutBrackets( context, - documentBloc.view.id, + documentBloc.documentId, styleCustomizer.inlineActionsMenuStyleBuilder(), ), /// - Using `+` pageReferenceShortcutPlusSign( context, - documentBloc.view.id, + documentBloc.documentId, styleCustomizer.inlineActionsMenuStyleBuilder(), ), ]; @@ -215,11 +218,13 @@ class _AppFlowyEditorPageState extends State { void initState() { super.initState(); - viewInfoBloc.add( - ViewInfoEvent.registerEditorState( - editorState: widget.editorState, - ), - ); + if (widget.useViewInfoBloc) { + viewInfoBloc.add( + ViewInfoEvent.registerEditorState( + editorState: widget.editorState, + ), + ); + } _initEditorL10n(); _initializeShortcuts(); @@ -262,7 +267,7 @@ class _AppFlowyEditorPageState extends State { @override void dispose() { - if (!viewInfoBloc.isClosed) { + if (widget.useViewInfoBloc && !viewInfoBloc.isClosed) { viewInfoBloc.add(const ViewInfoEvent.unregisterEditorState()); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/inline_database_menu_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/inline_database_menu_item.dart index e3ddab1469..e31bd172c6 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/inline_database_menu_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/inline_database_menu_item.dart @@ -19,7 +19,7 @@ SelectionMenuItem inlineGridMenuItem(DocumentBloc documentBloc) => keywords: ['grid', 'database'], handler: (editorState, menuService, context) async { // create the view inside current page - final parentViewId = documentBloc.view.id; + final parentViewId = documentBloc.documentId; final value = await ViewBackendService.createView( parentViewId: parentViewId, name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(), @@ -40,7 +40,7 @@ SelectionMenuItem inlineBoardMenuItem(DocumentBloc documentBloc) => keywords: ['board', 'kanban', 'database'], handler: (editorState, menuService, context) async { // create the view inside current page - final parentViewId = documentBloc.view.id; + final parentViewId = documentBloc.documentId; final value = await ViewBackendService.createView( parentViewId: parentViewId, name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(), @@ -61,7 +61,7 @@ SelectionMenuItem inlineCalendarMenuItem(DocumentBloc documentBloc) => keywords: ['calendar', 'database'], handler: (editorState, menuService, context) async { // create the view inside current page - final parentViewId = documentBloc.view.id; + final parentViewId = documentBloc.documentId; final value = await ViewBackendService.createView( parentViewId: parentViewId, name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart index 719e1d3bee..31fe8f0a4e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart @@ -357,7 +357,7 @@ class _MentionDateBlockState extends State { ); // Add new reminder - final viewId = rootContext.read().view.id; + final viewId = rootContext.read().documentId; return rootContext.read().add( ReminderEvent.add( reminder: ReminderPB( 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 683f3cf14a..319e6091c8 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 @@ -139,7 +139,7 @@ class ReminderReferenceService extends InlineActionsDelegate { return; } - final viewId = context.read().view.id; + final viewId = context.read().documentId; final reminder = _reminderFromDate(date, viewId, node); final transaction = editorState.transaction diff --git a/frontend/appflowy_flutter/lib/startup/plugin/plugin.dart b/frontend/appflowy_flutter/lib/startup/plugin/plugin.dart index 3f36378af3..e675cf1459 100644 --- a/frontend/appflowy_flutter/lib/startup/plugin/plugin.dart +++ b/frontend/appflowy_flutter/lib/startup/plugin/plugin.dart @@ -11,17 +11,18 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; export "./src/sandbox.dart"; enum PluginType { - editor, + document, blank, trash, grid, board, calendar, + databaseDocument, } typedef PluginId = String; -abstract class Plugin { +abstract class Plugin { PluginId get id; PluginWidgetBuilder get widgetBuilder; diff --git a/frontend/appflowy_flutter/lib/startup/tasks/load_plugin.dart b/frontend/appflowy_flutter/lib/startup/tasks/load_plugin.dart index b5fafd3c64..3899959b02 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/load_plugin.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/load_plugin.dart @@ -1,6 +1,7 @@ import 'package:appflowy/plugins/database/calendar/calendar.dart'; import 'package:appflowy/plugins/database/board/board.dart'; import 'package:appflowy/plugins/database/grid/grid.dart'; +import 'package:appflowy/plugins/database_document/database_document_plugin.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/plugins/blank/blank.dart'; @@ -24,6 +25,10 @@ class PluginLoadTask extends LaunchTask { builder: CalendarPluginBuilder(), config: CalendarPluginConfig(), ); + registerPlugin( + builder: DatabaseDocumentPluginBuilder(), + config: DatabaseDocumentPluginConfig(), + ); } @override diff --git a/frontend/appflowy_flutter/lib/workspace/application/export/document_exporter.dart b/frontend/appflowy_flutter/lib/workspace/application/export/document_exporter.dart index 13d07c62b9..74b8316a4b 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/export/document_exporter.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/export/document_exporter.dart @@ -35,7 +35,7 @@ class DocumentExporter { DocumentExportType type, ) async { final documentService = DocumentService(); - final result = await documentService.openDocument(viewId: view.id); + final result = await documentService.openDocument(documentId: view.id); return result.fold( (r) { final document = r.toDocument(); diff --git a/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_bloc.dart index 663843d252..36d64e6989 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_bloc.dart @@ -54,9 +54,11 @@ class TabsBloc extends Bloc { emit(state.openView(plugin, view)); _setLatestOpenView(view); }, - openPlugin: (Plugin plugin, ViewPB? view) { - emit(state.openPlugin(plugin: plugin)); - _setLatestOpenView(view); + openPlugin: (Plugin plugin, ViewPB? view, bool setLatest) { + emit(state.openPlugin(plugin: plugin, setLatest: setLatest)); + if (setLatest) { + _setLatestOpenView(view); + } }, ); }, diff --git a/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_event.dart b/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_event.dart index 8e344e361f..335c383f2e 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_event.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_event.dart @@ -10,6 +10,9 @@ class TabsEvent with _$TabsEvent { required Plugin plugin, required ViewPB view, }) = _OpenTab; - const factory TabsEvent.openPlugin({required Plugin plugin, ViewPB? view}) = - _OpenPlugin; + const factory TabsEvent.openPlugin({ + required Plugin plugin, + ViewPB? view, + @Default(true) bool setLatest, + }) = _OpenPlugin; } diff --git a/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_state.dart b/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_state.dart index dd08e505d6..cf4092eaaa 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_state.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_state.dart @@ -21,7 +21,7 @@ class TabsState { final selectExistingPlugin = _selectPluginIfOpen(plugin.id); if (selectExistingPlugin == null) { - _pageManagers.add(PageManager()..setPlugin(plugin)); + _pageManagers.add(PageManager()..setPlugin(plugin, true)); return copyWith(newIndex: pages - 1, pageManagers: [..._pageManagers]); } @@ -58,12 +58,12 @@ class TabsState { /// If the plugin is already open in a tab, then that tab /// will become selected. /// - TabsState openPlugin({required Plugin plugin}) { + TabsState openPlugin({required Plugin plugin, bool setLatest = true}) { final selectExistingPlugin = _selectPluginIfOpen(plugin.id); if (selectExistingPlugin == null) { final pageManagers = [..._pageManagers]; - pageManagers[currentIndex].setPlugin(plugin); + pageManagers[currentIndex].setPlugin(plugin, setLatest); return copyWith(pageManagers: pageManagers); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart index 522eed1b7a..ed762cdf67 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart @@ -10,11 +10,6 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; -enum FlowyPlugin { - editor, - kanban, -} - class PluginArgumentKeys { static String selection = "selection"; static String rowId = "row_id"; @@ -34,7 +29,7 @@ extension ViewExtension on ViewPB { PluginType get pluginType => switch (layout) { ViewLayoutPB.Board => PluginType.board, ViewLayoutPB.Calendar => PluginType.calendar, - ViewLayoutPB.Document => PluginType.editor, + ViewLayoutPB.Document => PluginType.document, ViewLayoutPB.Grid => PluginType.grid, _ => throw UnimplementedError(), }; diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart index 12c72e48ed..754d7fdb6a 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart @@ -95,7 +95,7 @@ class ViewBackendService { required ViewLayoutPB layoutType, required String name, }) { - return ViewBackendService.createView( + return createView( layoutType: layoutType, parentViewId: parentViewId, name: name, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart index 0574bb8ae4..535ff002de 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart @@ -174,12 +174,14 @@ class PageNotifier extends ChangeNotifier { /// This is the only place where the plugin is set. /// No need compare the old plugin with the new plugin. Just set it. - set plugin(Plugin newPlugin) { + void setPlugin(Plugin newPlugin, bool setLatest) { _plugin.dispose(); newPlugin.init(); - /// Set the plugin view as the latest view. - FolderEventSetLatestView(ViewIdPB(value: newPlugin.id)).send(); + // Set the plugin view as the latest view. + if (setLatest) { + FolderEventSetLatestView(ViewIdPB(value: newPlugin.id)).send(); + } _plugin = newPlugin; notifyListeners(); @@ -202,8 +204,8 @@ class PageManager { Plugin get plugin => _notifier.plugin; - void setPlugin(Plugin newPlugin) { - _notifier.plugin = newPlugin; + void setPlugin(Plugin newPlugin, bool setLatest) { + _notifier.setPlugin(newPlugin, setLatest); } void setStackWithId(String id) { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_view.dart index facd3a2f0b..42e5d50bfd 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_view.dart @@ -65,7 +65,7 @@ class NotificationsView extends StatelessWidget { final documentService = DocumentService(); final documentFuture = documentService.openDocument( - viewId: reminder.objectId, + documentId: reminder.objectId, ); Future? nodeBuilder; diff --git a/frontend/appflowy_flutter/test/bloc_test/home_test/home_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/home_test/home_bloc_test.dart index 0b8e49cafb..6a34bc68d0 100644 --- a/frontend/appflowy_flutter/test/bloc_test/home_test/home_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/home_test/home_bloc_test.dart @@ -49,7 +49,7 @@ void main() { assert(appBloc.state.lastCreatedView != null); final latestView = appBloc.state.lastCreatedView!; - final _ = DocumentBloc(view: latestView) + final _ = DocumentBloc(documentId: latestView.id) ..add(const DocumentEvent.initial()); await FolderEventSetLatestView(ViewIdPB(value: latestView.id)).send(); diff --git a/frontend/appflowy_flutter/test/bloc_test/home_test/view_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/home_test/view_bloc_test.dart index 4eaa893ae7..b816a8b68e 100644 --- a/frontend/appflowy_flutter/test/bloc_test/home_test/view_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/home_test/view_bloc_test.dart @@ -192,7 +192,7 @@ void main() { workspaceSetting.latestView.id == viewBloc.state.lastCreatedView!.id; // ignore: unused_local_variable - final documentBloc = DocumentBloc(view: document) + final documentBloc = DocumentBloc(documentId: document.id) ..add( const DocumentEvent.initial(), );