feat: open a row as a full page (#5111)

* feat: open a row as a full page

* chore: don't set latest open view

* chore: fix calendar open

* chore: disable in relation

* chore: code cleanup

* chore: fix merge conflicts
This commit is contained in:
Richard Shiue 2024-04-29 13:44:42 +08:00 committed by GitHub
parent 3c446d5e78
commit 969726ef73
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 1118 additions and 113 deletions

View File

@ -77,7 +77,7 @@ class RecentViewBloc extends Bloc<RecentViewEvent, RecentViewState> {
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(

View File

@ -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<DesktopBoardContent> {
FlowyOverlay.show(
context: context,
builder: (_) => RowDetailPage(
databaseController: databaseController,
rowController: rowController,
builder: (_) => BlocProvider.value(
value: context.read<ViewBloc>(),
child: RowDetailPage(
databaseController: databaseController,
rowController: rowController,
),
),
);
}

View File

@ -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<ViewBloc>(),
child: RowDetailPage(
databaseController: databaseController,
rowController: rowController,
),
);
},
);

View File

@ -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<EventCard> {
if (settings == null) {
return const SizedBox.shrink();
}
return BlocProvider.value(
value: context.read<CalendarBloc>(),
return MultiBlocProvider(
providers: [
BlocProvider.value(
value: context.read<CalendarBloc>(),
),
BlocProvider.value(
value: context.read<ViewBloc>(),
),
],
child: CalendarEventEditor(
databaseController: widget.databaseController,
rowMeta: widget.event.event.rowMeta,

View File

@ -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<ViewBloc>(),
child: RowDetailPage(
databaseController: databaseController,
rowController: rowController,
),
),
);
},

View File

@ -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<ViewBloc>(),
child: RowDetailPage(
rowController: rowController,
databaseController: databaseController,
),
);
},
);
@ -424,10 +428,13 @@ class _UnscheduledEventsButtonState extends State<UnscheduledEventsButton> {
),
),
),
popupBuilder: (context) {
return UnscheduleEventsList(
databaseController: widget.databaseController,
unscheduleEvents: state.unscheduleEvents,
popupBuilder: (_) {
return BlocProvider.value(
value: context.read<ViewBloc>(),
child: UnscheduleEventsList(
databaseController: widget.databaseController,
unscheduleEvents: state.unscheduleEvents,
),
);
},
);

View File

@ -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<GridPage> {
FlowyOverlay.show(
context: context,
builder: (_) => RowDetailPage(
databaseController: context.read<GridBloc>().databaseController,
rowController: rowController,
builder: (_) => BlocProvider.value(
value: context.read<ViewBloc>(),
child: RowDetailPage(
databaseController: context.read<GridBloc>().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<ViewBloc>(),
child: RowDetailPage(
rowController: rowController,
databaseController: databaseController,
),
),
);
},

View File

@ -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<GridRow> {
child: RowContent(
fieldController: widget.fieldController,
cellBuilder: widget.cellBuilder,
onExpand: () => widget.openDetailPage(
context,
widget.cellBuilder,
),
onExpand: () => widget.openDetailPage(context),
),
),
],

View File

@ -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<DatabaseTabBarView> {
@override
Widget build(BuildContext context) {
return BlocProvider<DatabaseTabBarBloc>(
create: (context) => DatabaseTabBarBloc(view: widget.view)
..add(const DatabaseTabBarEvent.initial()),
return MultiBlocProvider(
providers: [
BlocProvider<DatabaseTabBarBloc>(
create: (context) => DatabaseTabBarBloc(view: widget.view)
..add(const DatabaseTabBarEvent.initial()),
),
BlocProvider<ViewBloc>(
create: (context) =>
ViewBloc(view: widget.view)..add(const ViewEvent.initial()),
),
],
child: MultiBlocListener(
listeners: [
BlocListener<DatabaseTabBarBloc, DatabaseTabBarState>(

View File

@ -29,6 +29,7 @@ class RelatedRowDetailPage extends StatelessWidget {
return RowDetailPage(
databaseController: databaseController,
rowController: rowController,
allowOpenAsFullPage: false,
);
},
);

View File

@ -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<RowBanner> createState() => _RowBannerState();
@ -84,6 +92,42 @@ class _RowBannerState extends State<RowBanner> {
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<ViewBloc>();
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<TabsBloc>().add(
TabsEvent.openPlugin(
plugin: DatabaseDocumentPlugin(
data: DatabaseDocumentContext(
view: viewBloc.state.view,
databaseId: databaseId,
rowId: widget.rowController.rowId,
documentId: documentId,
),
pluginType: PluginType.databaseDocument,
),
setLatest: false,
),
);
}
},
),
),
],
),
),

View File

@ -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<RowDetailPage> createState() => _RowDetailPageState();
@ -60,6 +62,7 @@ class _RowDetailPageState extends State<RowDetailPage> {
fieldController: widget.databaseController.fieldController,
rowController: widget.rowController,
cellBuilder: cellBuilder,
allowOpenAsFullPage: widget.allowOpenAsFullPage,
),
const VSpace(16),
Padding(

View File

@ -69,7 +69,7 @@ class _RowEditorState extends State<RowEditor> {
@override
void initState() {
super.initState();
documentBloc = DocumentBloc(view: widget.viewPB)
documentBloc = DocumentBloc(documentId: widget.viewPB.id)
..add(const DocumentEvent.initial());
}

View File

@ -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<DatabaseDocumentPage> createState() => _DatabaseDocumentPageState();
}
class _DatabaseDocumentPageState extends State<DatabaseDocumentPage> {
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<ActionNavigationBloc>(),
),
BlocProvider(
create: (_) => DocumentBloc(
databaseViewId: widget.databaseId,
rowId: widget.rowId,
documentId: widget.documentId,
)..add(const DocumentEvent.initial()),
),
],
child: BlocBuilder<DocumentBloc, DocumentState>(
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<ActionNavigationBloc, ActionNavigationState>(
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<RelatedRowDetailPageBloc, RelatedRowDetailPageState>(
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<DocumentBloc>().add(
const DocumentEvent.restorePage(),
),
onDelete: () => context.read<DocumentBloc>().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<DocumentBloc>().state.editorState;
if (editorState != null && widget.documentId == state.action?.objectId) {
editorState.updateSelectionWithReason(
Selection.collapsed(Position(path: [path])),
);
}
}
}
}

View File

@ -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<DocumentAppearanceCubit, DocumentAppearance>(
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<NavigationItem> get navigationItems => [this];
}
class DatabaseDocumentPluginConfig implements PluginConfig {
@override
bool get creatable => false;
}

View File

@ -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<DatabaseDocumentTitleBloc, DatabaseDocumentTitleState>(
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<Widget> _buildViewTitles(List<ViewPB> 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<DatabaseDocumentTitleBloc, DatabaseDocumentTitleState>(
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<TextCellBloc, TextCellState, String>(
selector: (state) => state.content,
builder: (context, content) {
final name = content.isEmpty
? LocaleKeys.grid_row_titlePlaceholder.tr()
: content;
return BlocBuilder<DatabaseDocumentTitleBloc,
DatabaseDocumentTitleState>(
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<DatabaseDocumentTitleBloc>()
.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<TabsBloc>().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<RenameRowPopover> createState() => _RenameRowPopoverState();
}
class _RenameRowPopoverState extends State<RenameRowPopover> {
@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,
),
),
],
);
}
}

View File

@ -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<DatabaseDocumentTitleEvent, DatabaseDocumentTitleState> {
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<DatabaseDocumentTitleEvent>((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) => <ViewPB>[]);
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<ViewPB> 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<ViewPB> 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,
);
}

View File

@ -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<DocumentEvent, DocumentState> {
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<DocumentEvent>(_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<DocumentEvent, DocumentState> {
late DocumentCollabAdapter _documentCollabAdapter;
late final TransactionAdapter _transactionAdapter = TransactionAdapter(
documentId: view.id,
documentId: documentId,
documentService: _documentService,
);
@ -85,9 +92,9 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
Future<void> 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<DocumentEvent, DocumentState> {
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<DocumentEvent, DocumentState> {
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<DocumentEvent, DocumentState> {
/// 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<DocumentEvent, DocumentState> {
/// Fetch document
Future<FlowyResult<EditorState?, FlowyError>> _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<DocumentEvent, DocumentState> {
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<DocumentEvent, DocumentState> {
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<DocumentEvent, DocumentState> {
userAvatar: user.iconUrl,
);
await _documentService.syncAwarenessStates(
documentId: view.id,
documentId: documentId,
metadata: jsonEncode(metadata.toJson()),
);
}

View File

@ -29,7 +29,7 @@ class DocumentCollabAdapter {
///
/// Only use in development
Future<EditorState?> 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<void> 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<void> 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;

View File

@ -11,7 +11,7 @@ class DocumentService {
Future<FlowyResult<void, FlowyError>> 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<FlowyResult<DocumentDataPB, FlowyError>> 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<FlowyResult<DocumentDataPB, FlowyError>> 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<FlowyResult<void, FlowyError>> 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;
}

View File

@ -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<int> {
class DocumentPlugin extends Plugin {
DocumentPlugin({
required ViewPB view,
required PluginType pluginType,

View File

@ -36,7 +36,7 @@ class DocumentPage extends StatefulWidget {
class _DocumentPageState extends State<DocumentPage>
with WidgetsBindingObserver {
EditorState? editorState;
late final documentBloc = DocumentBloc(view: widget.view)
late final documentBloc = DocumentBloc(documentId: widget.view.id)
..add(const DocumentEvent.initial());
@override

View File

@ -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<AppFlowyEditorPage> createState() => _AppFlowyEditorPageState();
}
@ -101,7 +104,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
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<AppFlowyEditorPage> {
/// - 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<AppFlowyEditorPage> {
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<AppFlowyEditorPage> {
@override
void dispose() {
if (!viewInfoBloc.isClosed) {
if (widget.useViewInfoBloc && !viewInfoBloc.isClosed) {
viewInfoBloc.add(const ViewInfoEvent.unregisterEditorState());
}

View File

@ -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(),

View File

@ -357,7 +357,7 @@ class _MentionDateBlockState extends State<MentionDateBlock> {
);
// Add new reminder
final viewId = rootContext.read<DocumentBloc>().view.id;
final viewId = rootContext.read<DocumentBloc>().documentId;
return rootContext.read<ReminderBloc>().add(
ReminderEvent.add(
reminder: ReminderPB(

View File

@ -139,7 +139,7 @@ class ReminderReferenceService extends InlineActionsDelegate {
return;
}
final viewId = context.read<DocumentBloc>().view.id;
final viewId = context.read<DocumentBloc>().documentId;
final reminder = _reminderFromDate(date, viewId, node);
final transaction = editorState.transaction

View File

@ -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<T> {
abstract class Plugin {
PluginId get id;
PluginWidgetBuilder get widgetBuilder;

View File

@ -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

View File

@ -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();

View File

@ -54,9 +54,11 @@ class TabsBloc extends Bloc<TabsEvent, TabsState> {
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);
}
},
);
},

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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(),
};

View File

@ -95,7 +95,7 @@ class ViewBackendService {
required ViewLayoutPB layoutType,
required String name,
}) {
return ViewBackendService.createView(
return createView(
layoutType: layoutType,
parentViewId: parentViewId,
name: name,

View File

@ -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) {

View File

@ -65,7 +65,7 @@ class NotificationsView extends StatelessWidget {
final documentService = DocumentService();
final documentFuture = documentService.openDocument(
viewId: reminder.objectId,
documentId: reminder.objectId,
);
Future<Node?>? nodeBuilder;

View File

@ -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();

View File

@ -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(),
);