Browse Source

Integrate Grid into Document (#1759)

* fix: cursor doesn't blink when opening selection menu

* feat: add board plugin

* feat: integrate board plugin into document

* feat: add i10n and fix known bugs

* feat: support jump to board page on document

* feat: disable editor scroll only when the board plugin is selected

* chore: dart fix

* chore: remove unused files

* fix: dart lint

* feat: integrate grid plugin into document

* feat: add more menu to grid plugins

* feat: refactor built-in page plugins, including board and grid

* feat: remove padding set up when plugin type equals to editor
Lucas.Xu 2 years ago
parent
commit
2e91dfb4be

+ 4 - 1
frontend/app_flowy/assets/translations/en.json

@@ -317,7 +317,10 @@
     },
     "slashMenu": {
       "board": {
-        "selectABoardToLinkTo": "Select a board to link to"
+        "selectABoardToLinkTo": "Select a Board to link to"
+      },
+      "grid": {
+        "selectAGridToLinkTo": "Select a Grid to link to"
       }
     }
   },

+ 6 - 0
frontend/app_flowy/lib/plugins/document/document_page.dart

@@ -1,5 +1,7 @@
 import 'package:app_flowy/plugins/document/presentation/plugins/board/board_menu_item.dart';
 import 'package:app_flowy/plugins/document/presentation/plugins/board/board_node_widget.dart';
+import 'package:app_flowy/plugins/document/presentation/plugins/grid/grid_menu_item.dart';
+import 'package:app_flowy/plugins/document/presentation/plugins/grid/grid_node_widget.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
@@ -111,6 +113,8 @@ class _DocumentPageState extends State<DocumentPage> {
         kCodeBlockType: CodeBlockNodeWidgetBuilder(),
         // Board
         kBoardType: BoardNodeWidgetBuilder(),
+        // Grid
+        kGridType: GridNodeWidgetBuilder(),
         // Card
         kCalloutType: CalloutNodeWidgetBuilder(),
       },
@@ -133,6 +137,8 @@ class _DocumentPageState extends State<DocumentPage> {
         emojiMenuItem,
         // Board
         boardMenuItem,
+        // Grid
+        gridMenuItem,
       ],
       themeData: theme.copyWith(extensions: [
         ...theme.extensions.values,

+ 1 - 1
frontend/app_flowy/lib/plugins/document/editor_styles.dart

@@ -9,7 +9,7 @@ EditorStyle customEditorTheme(BuildContext context) {
       ? EditorStyle.dark
       : EditorStyle.light;
   editorStyle = editorStyle.copyWith(
-    padding: const EdgeInsets.symmetric(horizontal: 40),
+    padding: const EdgeInsets.symmetric(horizontal: 100),
     textStyle: editorStyle.textStyle?.copyWith(
       fontFamily: 'poppins',
       fontSize: documentStyle.fontSize,

+ 160 - 0
frontend/app_flowy/lib/plugins/document/presentation/plugins/base/built_in_page_widget.dart

@@ -0,0 +1,160 @@
+import 'package:app_flowy/plugins/document/presentation/plugins/base/insert_page_command.dart';
+import 'package:app_flowy/startup/startup.dart';
+import 'package:app_flowy/workspace/application/app/app_service.dart';
+import 'package:app_flowy/workspace/application/view/view_ext.dart';
+import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
+import 'package:app_flowy/workspace/presentation/home/menu/menu.dart';
+import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
+import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart';
+import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:appflowy_popover/appflowy_popover.dart';
+import 'package:dartz/dartz.dart' as dartz;
+import 'package:flowy_infra_ui/style_widget/icon_button.dart';
+import 'package:flutter/material.dart';
+import 'package:app_flowy/generated/locale_keys.g.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra/image.dart';
+
+class BuiltInPageWidget extends StatefulWidget {
+  const BuiltInPageWidget({
+    Key? key,
+    required this.node,
+    required this.editorState,
+    required this.builder,
+  }) : super(key: key);
+
+  final Node node;
+  final EditorState editorState;
+  final Widget Function(ViewPB viewPB) builder;
+
+  @override
+  State<BuiltInPageWidget> createState() => _BuiltInPageWidgetState();
+}
+
+class _BuiltInPageWidgetState extends State<BuiltInPageWidget> {
+  final focusNode = FocusNode();
+
+  String get gridID {
+    return widget.node.attributes[kViewID];
+  }
+
+  String get appID {
+    return widget.node.attributes[kAppID];
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return FutureBuilder<dartz.Either<ViewPB, FlowyError>>(
+      builder: (context, snapshot) {
+        if (snapshot.hasData) {
+          final board = snapshot.data?.getLeftOrNull<ViewPB>();
+          if (board != null) {
+            return _build(context, board);
+          }
+        }
+        return const Center(
+          child: CircularProgressIndicator(),
+        );
+      },
+      future: AppService().getView(appID, gridID),
+    );
+  }
+
+  @override
+  void dispose() {
+    focusNode.dispose();
+    super.dispose();
+  }
+
+  Widget _build(BuildContext context, ViewPB viewPB) {
+    return MouseRegion(
+      onEnter: (event) {
+        widget.editorState.service.scrollService?.disable();
+      },
+      onExit: (event) {
+        widget.editorState.service.scrollService?.enable();
+      },
+      child: SizedBox(
+        height: 400,
+        child: Stack(
+          children: [
+            _buildMenu(context, viewPB),
+            _buildGrid(context, viewPB),
+          ],
+        ),
+      ),
+    );
+  }
+
+  Widget _buildGrid(BuildContext context, ViewPB viewPB) {
+    return Focus(
+      focusNode: focusNode,
+      onFocusChange: (value) {
+        if (value) {
+          widget.editorState.service.selectionService.clearSelection();
+        }
+      },
+      child: widget.builder(viewPB),
+    );
+  }
+
+  Widget _buildMenu(BuildContext context, ViewPB viewPB) {
+    return Positioned(
+      top: 5,
+      left: 5,
+      child: PopoverActionList<_ActionWrapper>(
+        direction: PopoverDirection.bottomWithCenterAligned,
+        actions:
+            _ActionType.values.map((action) => _ActionWrapper(action)).toList(),
+        buildChild: (controller) {
+          return FlowyIconButton(
+            tooltipText: LocaleKeys.tooltip_openMenu.tr(),
+            width: 25,
+            height: 30,
+            iconPadding: const EdgeInsets.all(3),
+            icon: svgWidget('editor/details'),
+            onPressed: () => controller.show(),
+          );
+        },
+        onSelected: (action, controller) async {
+          switch (action.inner) {
+            case _ActionType.openAsPage:
+              getIt<MenuSharedState>().latestOpenView = viewPB;
+              getIt<HomeStackManager>().setPlugin(viewPB.plugin());
+              break;
+            case _ActionType.delete:
+              final transaction = widget.editorState.transaction;
+              transaction.deleteNode(widget.node);
+              widget.editorState.apply(transaction);
+              break;
+          }
+          controller.close();
+        },
+      ),
+    );
+  }
+}
+
+enum _ActionType {
+  openAsPage,
+  delete,
+}
+
+class _ActionWrapper extends ActionCell {
+  final _ActionType inner;
+
+  _ActionWrapper(this.inner);
+
+  Widget? icon(Color iconColor) => null;
+
+  @override
+  String get name {
+    switch (inner) {
+      case _ActionType.openAsPage:
+        return LocaleKeys.tooltip_openAsPage.tr();
+      case _ActionType.delete:
+        return LocaleKeys.disclosureAction_delete.tr();
+    }
+  }
+}

+ 42 - 0
frontend/app_flowy/lib/plugins/document/presentation/plugins/base/insert_page_command.dart

@@ -0,0 +1,42 @@
+import 'package:app_flowy/plugins/document/presentation/plugins/board/board_node_widget.dart';
+import 'package:app_flowy/plugins/document/presentation/plugins/grid/grid_node_widget.dart';
+import 'package:appflowy_backend/protobuf/flowy-folder/app.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+
+const String kAppID = 'app_id';
+const String kViewID = 'view_id';
+
+extension InsertPage on EditorState {
+  void insertPage(AppPB appPB, ViewPB viewPB) {
+    final selection = service.selectionService.currentSelection.value;
+    final textNodes =
+        service.selectionService.currentSelectedNodes.whereType<TextNode>();
+    if (selection == null || textNodes.isEmpty) {
+      return;
+    }
+    final transaction = this.transaction;
+    transaction.insertNode(
+      selection.end.path,
+      Node(
+        type: _convertPageType(viewPB),
+        attributes: {
+          kAppID: appPB.id,
+          kViewID: viewPB.id,
+        },
+      ),
+    );
+    apply(transaction);
+  }
+
+  String _convertPageType(ViewPB viewPB) {
+    switch (viewPB.layout) {
+      case ViewLayoutTypePB.Grid:
+        return kGridType;
+      case ViewLayoutTypePB.Board:
+        return kBoardType;
+      default:
+        throw Exception('Unknown layout type');
+    }
+  }
+}

+ 186 - 0
frontend/app_flowy/lib/plugins/document/presentation/plugins/base/link_to_page_widget.dart

@@ -0,0 +1,186 @@
+import 'package:app_flowy/workspace/application/app/app_service.dart';
+import 'package:appflowy_backend/protobuf/flowy-folder/app.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:dartz/dartz.dart' as dartz;
+import 'package:flowy_infra/image.dart';
+import 'package:flowy_infra_ui/style_widget/button.dart';
+import 'package:flowy_infra_ui/style_widget/text.dart';
+import 'package:flutter/material.dart';
+import 'insert_page_command.dart';
+import 'package:app_flowy/generated/locale_keys.g.dart';
+import 'package:easy_localization/easy_localization.dart';
+
+EditorState? _editorState;
+OverlayEntry? _linkToPageMenu;
+
+void showLinkToPageMenu(
+  EditorState editorState,
+  SelectionMenuService menuService,
+  BuildContext context,
+  ViewLayoutTypePB pageType,
+) {
+  final aligment = menuService.alignment;
+  final offset = menuService.offset;
+  menuService.dismiss();
+
+  _editorState = editorState;
+
+  String hintText = '';
+  switch (pageType) {
+    case ViewLayoutTypePB.Grid:
+      hintText = LocaleKeys.document_slashMenu_grid_selectAGridToLinkTo.tr();
+      break;
+    case ViewLayoutTypePB.Board:
+      hintText = LocaleKeys.document_slashMenu_board_selectABoardToLinkTo.tr();
+      break;
+    default:
+      throw Exception('Unknown layout type');
+  }
+
+  _linkToPageMenu?.remove();
+  _linkToPageMenu = OverlayEntry(builder: (context) {
+    return Positioned(
+      top: aligment == Alignment.bottomLeft ? offset.dy : null,
+      bottom: aligment == Alignment.topLeft ? offset.dy : null,
+      left: offset.dx,
+      child: Material(
+        color: Colors.transparent,
+        child: LinkToPageMenu(
+          editorState: editorState,
+          layoutType: pageType,
+          hintText: hintText,
+          onSelected: (appPB, viewPB) {
+            editorState.insertPage(appPB, viewPB);
+          },
+        ),
+      ),
+    );
+  });
+
+  Overlay.of(context)?.insert(_linkToPageMenu!);
+
+  editorState.service.selectionService.currentSelection
+      .addListener(dismissLinkToPageMenu);
+}
+
+void dismissLinkToPageMenu() {
+  _linkToPageMenu?.remove();
+  _linkToPageMenu = null;
+
+  _editorState?.service.selectionService.currentSelection
+      .removeListener(dismissLinkToPageMenu);
+  _editorState = null;
+}
+
+class LinkToPageMenu extends StatefulWidget {
+  const LinkToPageMenu({
+    super.key,
+    required this.editorState,
+    required this.layoutType,
+    required this.hintText,
+    required this.onSelected,
+  });
+
+  final EditorState editorState;
+  final ViewLayoutTypePB layoutType;
+  final String hintText;
+  final void Function(AppPB appPB, ViewPB viewPB) onSelected;
+
+  @override
+  State<LinkToPageMenu> createState() => _LinkToPageMenuState();
+}
+
+class _LinkToPageMenuState extends State<LinkToPageMenu> {
+  EditorStyle get style => widget.editorState.editorStyle;
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      color: Colors.transparent,
+      width: 300,
+      child: Container(
+        padding: const EdgeInsets.fromLTRB(10, 6, 10, 6),
+        decoration: BoxDecoration(
+          color: style.selectionMenuBackgroundColor,
+          boxShadow: [
+            BoxShadow(
+              blurRadius: 5,
+              spreadRadius: 1,
+              color: Colors.black.withOpacity(0.1),
+            ),
+          ],
+          borderRadius: BorderRadius.circular(6.0),
+        ),
+        child: _buildListWidget(context),
+      ),
+    );
+  }
+
+  Widget _buildListWidget(BuildContext context) {
+    return FutureBuilder<List<dartz.Tuple2<AppPB, List<ViewPB>>>>(
+      builder: (context, snapshot) {
+        if (snapshot.hasData &&
+            snapshot.connectionState == ConnectionState.done) {
+          final apps = snapshot.data;
+          final children = <Widget>[
+            Padding(
+              padding: const EdgeInsets.symmetric(vertical: 4),
+              child: FlowyText.regular(
+                widget.hintText,
+                fontSize: 10,
+                color: Colors.grey,
+              ),
+            ),
+          ];
+          if (apps != null && apps.isNotEmpty) {
+            for (final app in apps) {
+              if (app.value2.isNotEmpty) {
+                children.add(
+                  Padding(
+                    padding: const EdgeInsets.symmetric(vertical: 4),
+                    child: FlowyText.regular(
+                      app.value1.name,
+                    ),
+                  ),
+                );
+                for (final value in app.value2) {
+                  children.add(
+                    FlowyButton(
+                      leftIcon: svgWidget(
+                        _iconName(value),
+                        color: Theme.of(context).colorScheme.onSurface,
+                      ),
+                      text: FlowyText.regular(value.name),
+                      onTap: () => widget.onSelected(app.value1, value),
+                    ),
+                  );
+                }
+              }
+            }
+          }
+          return Column(
+            crossAxisAlignment: CrossAxisAlignment.stretch,
+            children: children,
+          );
+        } else {
+          return const Center(
+            child: CircularProgressIndicator(),
+          );
+        }
+      },
+      future: AppService().fetchViews(widget.layoutType),
+    );
+  }
+
+  String _iconName(ViewPB viewPB) {
+    switch (viewPB.layout) {
+      case ViewLayoutTypePB.Grid:
+        return 'editor/grid';
+      case ViewLayoutTypePB.Board:
+        return 'editor/board';
+      default:
+        throw Exception('Unknown layout type');
+    }
+  }
+}

+ 10 - 176
frontend/app_flowy/lib/plugins/document/presentation/plugins/board/board_menu_item.dart

@@ -1,14 +1,9 @@
 import 'package:app_flowy/generated/locale_keys.g.dart';
-import 'package:app_flowy/plugins/document/presentation/plugins/board/board_node_widget.dart';
-import 'package:app_flowy/workspace/application/app/app_service.dart';
-import 'package:appflowy_backend/protobuf/flowy-folder/app.pb.dart';
+import 'package:app_flowy/plugins/document/presentation/plugins/base/link_to_page_widget.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:dartz/dartz.dart' as dartz;
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra/image.dart';
-import 'package:flowy_infra_ui/style_widget/button.dart';
-import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flutter/material.dart';
 
 SelectionMenuItem boardMenuItem = SelectionMenuItem(
@@ -22,174 +17,13 @@ SelectionMenuItem boardMenuItem = SelectionMenuItem(
           : editorState.editorStyle.selectionMenuItemIconColor,
     );
   },
-  keywords: ['board'],
-  handler: _showLinkToPageMenu,
-);
-
-EditorState? _editorState;
-OverlayEntry? _linkToPageMenu;
-void _dismissLinkToPageMenu() {
-  _linkToPageMenu?.remove();
-  _linkToPageMenu = null;
-
-  _editorState?.service.selectionService.currentSelection
-      .removeListener(_dismissLinkToPageMenu);
-  _editorState = null;
-}
-
-void _showLinkToPageMenu(
-  EditorState editorState,
-  SelectionMenuService menuService,
-  BuildContext context,
-) {
-  final aligment = menuService.alignment;
-  final offset = menuService.offset;
-  menuService.dismiss();
-
-  _editorState = editorState;
-
-  _linkToPageMenu?.remove();
-  _linkToPageMenu = OverlayEntry(builder: (context) {
-    return Positioned(
-      top: aligment == Alignment.bottomLeft ? offset.dy : null,
-      bottom: aligment == Alignment.topLeft ? offset.dy : null,
-      left: offset.dx,
-      child: Material(
-        color: Colors.transparent,
-        child: LinkToPageMenu(
-          editorState: editorState,
-        ),
-      ),
+  keywords: ['board', 'kanban'],
+  handler: (editorState, menuService, context) {
+    showLinkToPageMenu(
+      editorState,
+      menuService,
+      context,
+      ViewLayoutTypePB.Board,
     );
-  });
-
-  Overlay.of(context)?.insert(_linkToPageMenu!);
-
-  editorState.service.selectionService.currentSelection
-      .addListener(_dismissLinkToPageMenu);
-}
-
-class LinkToPageMenu extends StatefulWidget {
-  final EditorState editorState;
-
-  const LinkToPageMenu({
-    super.key,
-    required this.editorState,
-  });
-
-  @override
-  State<LinkToPageMenu> createState() => _LinkToPageMenuState();
-}
-
-class _LinkToPageMenuState extends State<LinkToPageMenu> {
-  EditorStyle get style => widget.editorState.editorStyle;
-
-  @override
-  Widget build(BuildContext context) {
-    return Container(
-      color: Colors.transparent,
-      width: 300,
-      child: Container(
-        padding: const EdgeInsets.fromLTRB(10, 6, 10, 6),
-        decoration: BoxDecoration(
-          color: style.selectionMenuBackgroundColor,
-          boxShadow: [
-            BoxShadow(
-              blurRadius: 5,
-              spreadRadius: 1,
-              color: Colors.black.withOpacity(0.1),
-            ),
-          ],
-          borderRadius: BorderRadius.circular(6.0),
-        ),
-        child: _buildBoardListWidget(context),
-      ),
-    );
-  }
-
-  Future<List<dartz.Tuple2<AppPB, List<ViewPB>>>> fetchBoards() async {
-    return AppService().fetchViews(ViewLayoutTypePB.Board);
-  }
-
-  Widget _buildBoardListWidget(BuildContext context) {
-    return FutureBuilder<List<dartz.Tuple2<AppPB, List<ViewPB>>>>(
-      builder: (context, snapshot) {
-        if (snapshot.hasData &&
-            snapshot.connectionState == ConnectionState.done) {
-          final apps = snapshot.data;
-          final children = <Widget>[
-            Padding(
-              padding: const EdgeInsets.symmetric(vertical: 4),
-              child: FlowyText.regular(
-                LocaleKeys.document_slashMenu_board_selectABoardToLinkTo.tr(),
-                fontSize: 10,
-                color: Colors.grey,
-              ),
-            ),
-          ];
-          if (apps != null && apps.isNotEmpty) {
-            for (final app in apps) {
-              if (app.value2.isNotEmpty) {
-                children.add(
-                  Padding(
-                    padding: const EdgeInsets.symmetric(vertical: 4),
-                    child: FlowyText.regular(
-                      app.value1.name,
-                    ),
-                  ),
-                );
-                for (final board in app.value2) {
-                  children.add(
-                    FlowyButton(
-                      leftIcon: svgWidget(
-                        'editor/board',
-                        color: Theme.of(context).colorScheme.onSurface,
-                      ),
-                      text: FlowyText.regular(board.name),
-                      onTap: () => widget.editorState.insertBoard(
-                        app.value1,
-                        board,
-                      ),
-                    ),
-                  );
-                }
-              }
-            }
-          }
-          return Column(
-            crossAxisAlignment: CrossAxisAlignment.stretch,
-            children: children,
-          );
-        } else {
-          return const Center(
-            child: CircularProgressIndicator(),
-          );
-        }
-      },
-      future: fetchBoards(),
-    );
-  }
-}
-
-extension on EditorState {
-  void insertBoard(AppPB appPB, ViewPB viewPB) {
-    final selection = service.selectionService.currentSelection.value;
-    final textNodes =
-        service.selectionService.currentSelectedNodes.whereType<TextNode>();
-    if (selection == null || textNodes.isEmpty) {
-      return;
-    }
-    final transaction = this.transaction;
-    transaction.insertNode(
-      selection.end.path,
-      Node(
-        type: kBoardType,
-        attributes: {
-          kAppID: appPB.id,
-          kBoardID: viewPB.id,
-        },
-      ),
-    );
-    apply(transaction);
-  }
-}
+  },
+);

+ 11 - 132
frontend/app_flowy/lib/plugins/document/presentation/plugins/board/board_node_widget.dart

@@ -1,19 +1,10 @@
 import 'package:app_flowy/plugins/board/presentation/board_page.dart';
-import 'package:app_flowy/startup/startup.dart';
-import 'package:app_flowy/workspace/application/app/app_service.dart';
-import 'package:app_flowy/workspace/application/view/view_ext.dart';
-import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
-import 'package:app_flowy/workspace/presentation/home/menu/menu.dart';
-import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart';
-import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
+import 'package:app_flowy/plugins/document/presentation/plugins/base/built_in_page_widget.dart';
+import 'package:app_flowy/plugins/document/presentation/plugins/base/insert_page_command.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:dartz/dartz.dart' as dartz;
-import 'package:flowy_infra_ui/style_widget/button.dart';
 import 'package:flutter/material.dart';
 
 const String kBoardType = 'board';
-const String kAppID = 'app_id';
-const String kBoardID = 'board_id';
 
 class BoardNodeWidgetBuilder extends NodeWidgetBuilder<Node> {
   @override
@@ -27,7 +18,7 @@ class BoardNodeWidgetBuilder extends NodeWidgetBuilder<Node> {
 
   @override
   NodeValidator<Node> get nodeValidator => (node) {
-        return node.attributes[kBoardID] is String &&
+        return node.attributes[kViewID] is String &&
             node.attributes[kAppID] is String;
       };
 }
@@ -46,130 +37,18 @@ class _BoardWidget extends StatefulWidget {
   State<_BoardWidget> createState() => _BoardWidgetState();
 }
 
-class _BoardWidgetState extends State<_BoardWidget> with SelectableMixin {
-  RenderBox get _renderBox => context.findRenderObject() as RenderBox;
-
-  String get boardID {
-    return widget.node.attributes[kBoardID];
-  }
-
-  String get appID {
-    return widget.node.attributes[kAppID];
-  }
-
-  late Future<dartz.Either<ViewPB, FlowyError>> board;
-
-  @override
-  void initState() {
-    super.initState();
-
-    board = _fetchBoard();
-  }
-
+class _BoardWidgetState extends State<_BoardWidget> {
   @override
   Widget build(BuildContext context) {
-    return FutureBuilder<dartz.Either<ViewPB, FlowyError>>(
-      builder: (context, snapshot) {
-        if (snapshot.hasData) {
-          final board = snapshot.data?.getLeftOrNull<ViewPB>();
-          if (board != null) {
-            return _buildBoard(context, board);
-          }
-        }
-        return const Center(
-          child: CircularProgressIndicator(),
+    return BuiltInPageWidget(
+      node: widget.node,
+      editorState: widget.editorState,
+      builder: (viewPB) {
+        return BoardPage(
+          key: ValueKey(viewPB.id),
+          view: viewPB,
         );
       },
-      future: board,
-    );
-  }
-
-  Future<dartz.Either<ViewPB, FlowyError>> _fetchBoard() async {
-    return AppService().getView(appID, boardID);
-  }
-
-  Widget _buildBoard(BuildContext context, ViewPB viewPB) {
-    return MouseRegion(
-      onHover: (event) {
-        if (widget.node.isSelected(widget.editorState)) {
-          widget.editorState.service.scrollService?.disable();
-        }
-      },
-      onExit: (event) {
-        widget.editorState.service.scrollService?.enable();
-      },
-      child: SizedBox(
-        height: 400,
-        child: Stack(
-          children: [
-            Positioned(
-              top: 0,
-              left: 20,
-              child: FlowyTextButton(
-                viewPB.name,
-                onPressed: () {
-                  getIt<MenuSharedState>().latestOpenView = viewPB;
-                  getIt<HomeStackManager>().setPlugin(viewPB.plugin());
-                },
-              ),
-            ),
-            BoardPage(
-              key: ValueKey(viewPB.id),
-              view: viewPB,
-              onEditStateChanged: () {
-                /// Clear selection when the edit state changes, otherwise the editor will prevent the keyboard event when the board is in edit mode.
-                widget.editorState.service.selectionService.clearSelection();
-              },
-            ),
-          ],
-        ),
-      ),
     );
   }
-
-  @override
-  bool get shouldCursorBlink => false;
-
-  @override
-  CursorStyle get cursorStyle => CursorStyle.borderLine;
-
-  @override
-  Position start() {
-    return Position(path: widget.node.path, offset: 0);
-  }
-
-  @override
-  Position end() {
-    return Position(path: widget.node.path, offset: 0);
-  }
-
-  @override
-  Position getPositionInOffset(Offset start) {
-    return end();
-  }
-
-  @override
-  List<Rect> getRectsInSelection(Selection selection) {
-    return [Offset.zero & _renderBox.size];
-  }
-
-  @override
-  Rect? getCursorRectInPosition(Position position) {
-    final size = _renderBox.size;
-    return Rect.fromLTWH(-size.width / 2.0, 0, size.width, size.height);
-  }
-
-  @override
-  Selection getSelectionInRange(Offset start, Offset end) {
-    return Selection.single(
-      path: widget.node.path,
-      startOffset: 0,
-      endOffset: 0,
-    );
-  }
-
-  @override
-  Offset localToGlobal(Offset offset) {
-    return _renderBox.localToGlobal(offset);
-  }
 }

+ 29 - 0
frontend/app_flowy/lib/plugins/document/presentation/plugins/grid/grid_menu_item.dart

@@ -0,0 +1,29 @@
+import 'package:app_flowy/generated/locale_keys.g.dart';
+import 'package:app_flowy/plugins/document/presentation/plugins/base/link_to_page_widget.dart';
+import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra/image.dart';
+import 'package:flutter/material.dart';
+
+SelectionMenuItem gridMenuItem = SelectionMenuItem(
+  name: () => LocaleKeys.grid_menuName.tr(),
+  icon: (editorState, onSelected) {
+    return svgWidget(
+      'editor/grid',
+      size: const Size.square(18.0),
+      color: onSelected
+          ? editorState.editorStyle.selectionMenuItemSelectedIconColor
+          : editorState.editorStyle.selectionMenuItemIconColor,
+    );
+  },
+  keywords: ['grid'],
+  handler: (editorState, menuService, context) {
+    showLinkToPageMenu(
+      editorState,
+      menuService,
+      context,
+      ViewLayoutTypePB.Grid,
+    );
+  },
+);

+ 54 - 0
frontend/app_flowy/lib/plugins/document/presentation/plugins/grid/grid_node_widget.dart

@@ -0,0 +1,54 @@
+import 'package:app_flowy/plugins/document/presentation/plugins/base/built_in_page_widget.dart';
+import 'package:app_flowy/plugins/document/presentation/plugins/base/insert_page_command.dart';
+import 'package:app_flowy/plugins/grid/presentation/grid_page.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flutter/material.dart';
+
+const String kGridType = 'grid';
+
+class GridNodeWidgetBuilder extends NodeWidgetBuilder<Node> {
+  @override
+  Widget build(NodeWidgetContext<Node> context) {
+    return _GridWidget(
+      key: context.node.key,
+      node: context.node,
+      editorState: context.editorState,
+    );
+  }
+
+  @override
+  NodeValidator<Node> get nodeValidator => (node) {
+        return node.attributes[kAppID] is String &&
+            node.attributes[kViewID] is String;
+      };
+}
+
+class _GridWidget extends StatefulWidget {
+  const _GridWidget({
+    Key? key,
+    required this.node,
+    required this.editorState,
+  }) : super(key: key);
+
+  final Node node;
+  final EditorState editorState;
+
+  @override
+  State<_GridWidget> createState() => _GridWidgetState();
+}
+
+class _GridWidgetState extends State<_GridWidget> {
+  @override
+  Widget build(BuildContext context) {
+    return BuiltInPageWidget(
+      node: widget.node,
+      editorState: widget.editorState,
+      builder: (viewPB) {
+        return GridPage(
+          key: ValueKey(viewPB.id),
+          view: viewPB,
+        );
+      },
+    );
+  }
+}

+ 10 - 3
frontend/app_flowy/lib/plugins/document/presentation/share/share_button.dart

@@ -105,8 +105,8 @@ class ShareActionList extends StatelessWidget {
             break;
           case ShareAction.copyLink:
             NavigatorAlertDialog(
-                    title: LocaleKeys.shareAction_workInProgress.tr())
-                .show(context);
+              title: LocaleKeys.shareAction_workInProgress.tr(),
+            ).show(context);
             break;
         }
         controller.close();
@@ -128,5 +128,12 @@ class ShareActionWrapper extends ActionCell {
   Widget? icon(Color iconColor) => null;
 
   @override
-  String get name => inner.name;
+  String get name {
+    switch (inner) {
+      case ShareAction.markdown:
+        return LocaleKeys.shareAction_markdown.tr();
+      case ShareAction.copyLink:
+        return LocaleKeys.shareAction_copyLink.tr();
+    }
+  }
 }

+ 4 - 4
frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart

@@ -31,10 +31,6 @@ import 'widgets/shortcuts.dart';
 import 'widgets/toolbar/grid_toolbar.dart';
 
 class GridPage extends StatefulWidget {
-  final ViewPB view;
-  final GridController gridController;
-  final VoidCallback? onDeleted;
-
   GridPage({
     required this.view,
     this.onDeleted,
@@ -42,6 +38,10 @@ class GridPage extends StatefulWidget {
   })  : gridController = GridController(view: view),
         super(key: key);
 
+  final ViewPB view;
+  final GridController gridController;
+  final VoidCallback? onDeleted;
+
   @override
   State<GridPage> createState() => _GridPageState();
 }

+ 7 - 3
frontend/app_flowy/lib/workspace/presentation/home/home_stack.dart

@@ -174,9 +174,13 @@ class HomeStackManager {
           index: getIt<PluginSandbox>().indexOf(notifier.plugin.ty),
           children: getIt<PluginSandbox>().supportPluginTypes.map((pluginType) {
             if (pluginType == notifier.plugin.ty) {
-              return notifier.plugin.display
-                  .buildWidget(PluginContext(onDeleted: onDeleted))
-                  .padding(horizontal: 40, vertical: 28);
+              final pluginWidget = notifier.plugin.display
+                  .buildWidget(PluginContext(onDeleted: onDeleted));
+              if (pluginType == PluginType.editor) {
+                return pluginWidget;
+              } else {
+                return pluginWidget.padding(horizontal: 40, vertical: 28);
+              }
             } else {
               return const BlankPage();
             }