浏览代码

feat: support table plugin (#3280)

Mohammad Zolfaghari 1 年之前
父节点
当前提交
df8642d446
共有 19 个文件被更改,包括 538 次插入43 次删除
  1. 1 0
      frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart
  2. 25 16
      frontend/appflowy_flutter/lib/plugins/document/application/editor_transaction_adapter.dart
  3. 35 7
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart
  4. 29 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart
  5. 1 1
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart
  6. 1 1
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_component.dart
  7. 1 1
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/database_view_block_component.dart
  8. 1 1
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart
  9. 3 10
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_toolbar_item.dart
  10. 4 1
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart
  11. 111 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_menu.dart
  12. 154 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_option_action.dart
  13. 1 1
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart
  14. 1 0
      frontend/appflowy_flutter/lib/workspace/application/appearance.dart
  15. 6 0
      frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension.dart
  16. 3 3
      frontend/appflowy_flutter/pubspec.lock
  17. 1 1
      frontend/appflowy_flutter/pubspec.yaml
  18. 152 0
      frontend/appflowy_flutter/test/unit_test/editor/transaction_adapter_test.dart
  19. 8 0
      frontend/resources/translations/en.json

+ 1 - 0
frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart

@@ -193,6 +193,7 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
     if (lastNode == null || lastNode.delta == null) {
       final transaction = editorState.transaction;
       transaction.insertNode([document.root.children.length], paragraphNode());
+      transaction.afterSelection = transaction.beforeSelection;
       await editorState.apply(transaction);
     }
   }

+ 25 - 16
frontend/appflowy_flutter/lib/plugins/document/application/editor_transaction_adapter.dart

@@ -1,5 +1,7 @@
-import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
+import 'dart:async';
+
 import 'package:appflowy/plugins/document/application/doc_service.dart';
+import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
 import 'package:appflowy_backend/log.dart';
 import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart';
 import 'package:appflowy_editor/appflowy_editor.dart'
@@ -12,10 +14,9 @@ import 'package:appflowy_editor/appflowy_editor.dart'
         DeleteOperation,
         PathExtensions,
         Node,
+        Path,
         composeAttributes;
 import 'package:collection/collection.dart';
-import 'dart:async';
-
 import 'package:nanoid/nanoid.dart';
 
 /// Uses to adjust the data structure between the editor and the backend.
@@ -46,7 +47,7 @@ class TransactionAdapter {
   }
 }
 
-extension on Operation {
+extension BlockAction on Operation {
   List<BlockActionPB> toBlockAction(EditorState editorState) {
     final op = this;
     if (op is InsertOperation) {
@@ -61,19 +62,22 @@ extension on Operation {
 }
 
 extension on InsertOperation {
-  List<BlockActionPB> toBlockAction(EditorState editorState) {
+  List<BlockActionPB> toBlockAction(
+    EditorState editorState, {
+    Node? previousNode,
+  }) {
+    Path currentPath = path;
     final List<BlockActionPB> actions = [];
-    // store the previous node for continuous insertion.
-    // because the backend needs to know the previous node's id.
-    Node? previousNode;
     for (final node in nodes) {
-      final parentId =
-          node.parent?.id ?? editorState.getNodeAtPath(path.parent)?.id ?? '';
+      final parentId = node.parent?.id ??
+          editorState.getNodeAtPath(currentPath.parent)?.id ??
+          '';
       var prevId = previousNode?.id ??
-          editorState.getNodeAtPath(path.previous)?.id ??
+          editorState.getNodeAtPath(currentPath.previous)?.id ??
           '';
       assert(parentId.isNotEmpty);
-      if (path.equals(path.previous) && !path.equals([0])) {
+      if (currentPath.equals(currentPath.previous) &&
+          !currentPath.equals([0])) {
         prevId = '';
       } else {
         assert(prevId.isNotEmpty && prevId != node.id);
@@ -89,12 +93,17 @@ extension on InsertOperation {
           ..payload = payload,
       );
       if (node.children.isNotEmpty) {
-        final childrenActions = node.children
-            .map((e) => InsertOperation(e.path, [e]).toBlockAction(editorState))
-            .expand((element) => element);
-        actions.addAll(childrenActions);
+        Node? prevChild;
+        for (final child in node.children) {
+          actions.addAll(
+            InsertOperation(currentPath + child.path, [child])
+                .toBlockAction(editorState, previousNode: prevChild),
+          );
+          prevChild = child;
+        }
       }
       previousNode = node;
+      currentPath = currentPath.next;
     }
     return actions;
   }

+ 35 - 7
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart

@@ -57,13 +57,18 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
   ];
 
   final List<ToolbarItem> toolbarItems = [
-    smartEditItem,
-    paragraphItem,
-    ...headingItems,
+    smartEditItem..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable,
+    paragraphItem..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable,
+    ...(headingItems
+      ..forEach(
+        (e) => e.isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable,
+      )),
     ...markdownFormatItems,
-    quoteItem,
-    bulletedListItem,
-    numberedListItem,
+    quoteItem..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable,
+    bulletedListItem
+      ..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable,
+    numberedListItem
+      ..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable,
     inlineMathEquationItem,
     linkItem,
     alignToolbarItem,
@@ -241,6 +246,28 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
           ),
         ),
       ),
+      TableBlockKeys.type: TableBlockComponentBuilder(
+        menuBuilder: (node, editorState, position, dir, onBuild, onClose) =>
+            TableMenu(
+          node: node,
+          editorState: editorState,
+          position: position,
+          dir: dir,
+          onBuild: onBuild,
+          onClose: onClose,
+        ),
+      ),
+      TableCellBlockKeys.type: TableCellBlockComponentBuilder(
+        menuBuilder: (node, editorState, position, dir, onBuild, onClose) =>
+            TableMenu(
+          node: node,
+          editorState: editorState,
+          position: position,
+          dir: dir,
+          onBuild: onBuild,
+          onClose: onClose,
+        ),
+      ),
       DatabaseBlockKeys.gridType: DatabaseViewBlockComponentBuilder(
         configuration: configuration.copyWith(
           padding: (_) => const EdgeInsets.symmetric(vertical: 10),
@@ -338,7 +365,8 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
         if (supportAlignBuilderType.contains(entry.key)) ...alignAction,
       ];
 
-      builder.showActions = (_) => true;
+      builder.showActions =
+          (node) => node.parent?.type != TableCellBlockKeys.type;
       builder.actionBuilder = (context, state) {
         final top = builder.configuration.padding(context.node).top;
         final padding = context.node.type == HeadingBlockKeys.type

+ 29 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart

@@ -0,0 +1,29 @@
+import 'package:appflowy_editor/appflowy_editor.dart';
+
+bool notShowInTable(EditorState editorState) {
+  final selection = editorState.selection;
+  if (selection == null) {
+    return false;
+  }
+  final nodes = editorState.getNodesInSelection(selection);
+  return nodes.every((element) {
+    if (element.type == TableBlockKeys.type) {
+      return false;
+    }
+    var parent = element.parent;
+    while (parent != null) {
+      if (parent.type == TableBlockKeys.type) {
+        return false;
+      }
+      parent = parent.parent;
+    }
+    return true;
+  });
+}
+
+bool onlyShowInSingleTextTypeSelectionAndExcludeTable(
+  EditorState editorState,
+) {
+  return onlyShowInSingleSelectionAndTextType(editorState) &&
+      notShowInTable(editorState);
+}

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart

@@ -195,7 +195,7 @@ class _CalloutBlockComponentWidgetState
       child: child,
     );
 
-    if (widget.actionBuilder != null) {
+    if (widget.showActions && widget.actionBuilder != null) {
       child = BlockComponentActionWrapper(
         node: widget.node,
         actionBuilder: widget.actionBuilder!,

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_component.dart

@@ -203,7 +203,7 @@ class _CodeBlockComponentWidgetState extends State<CodeBlockComponentWidget>
       child: child,
     );
 
-    if (widget.actionBuilder != null) {
+    if (widget.showActions && widget.actionBuilder != null) {
       child = BlockComponentActionWrapper(
         node: widget.node,
         actionBuilder: widget.actionBuilder!,

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/database_view_block_component.dart

@@ -88,7 +88,7 @@ class _DatabaseBlockComponentWidgetState
       child: child,
     );
 
-    if (widget.actionBuilder != null) {
+    if (widget.showActions && widget.actionBuilder != null) {
       child = BlockComponentActionWrapper(
         node: widget.node,
         actionBuilder: widget.actionBuilder!,

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart

@@ -144,7 +144,7 @@ class _MathEquationBlockComponentWidgetState
       ),
     );
 
-    if (widget.actionBuilder != null) {
+    if (widget.showActions && widget.actionBuilder != null) {
       child = BlockComponentActionWrapper(
         node: node,
         actionBuilder: widget.actionBuilder!,

+ 3 - 10
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_toolbar_item.dart

@@ -1,26 +1,19 @@
+import 'package:appflowy/generated/locale_keys.g.dart';
 import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_action.dart';
 import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
 import 'package:appflowy/user/application/user_service.dart';
 import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
+import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra_ui/style_widget/icon_button.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flutter/material.dart';
-import 'package:appflowy/generated/locale_keys.g.dart';
-import 'package:easy_localization/easy_localization.dart';
 
 final ToolbarItem smartEditItem = ToolbarItem(
   id: 'appflowy.editor.smart_edit',
   group: 0,
-  isActive: (editorState) {
-    final selection = editorState.selection;
-    if (selection == null) {
-      return false;
-    }
-    final nodes = editorState.getNodesInSelection(selection);
-    return nodes.every((element) => element.delta != null);
-  },
+  isActive: onlyShowInSingleSelectionAndTextType,
   builder: (context, editorState, _) => SmartEditActionList(
     editorState: editorState,
   ),

+ 4 - 1
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart

@@ -1,5 +1,7 @@
 export 'actions/block_action_list.dart';
 export 'actions/option_action.dart';
+export 'align_toolbar_item/align_toolbar_item.dart';
+export 'base/toolbar_extension.dart';
 export 'callout/callout_block_component.dart';
 export 'code_block/code_block_component.dart';
 export 'code_block/code_block_shortcut_event.dart';
@@ -19,11 +21,12 @@ export 'image/image_menu.dart';
 export 'image/image_selection_menu.dart';
 export 'inline_math_equation/inline_math_equation.dart';
 export 'inline_math_equation/inline_math_equation_toolbar_item.dart';
-export 'align_toolbar_item/align_toolbar_item.dart';
 export 'math_equation/math_equation_block_component.dart';
 export 'openai/widgets/auto_completion_node_widget.dart';
 export 'openai/widgets/smart_edit_node_widget.dart';
 export 'openai/widgets/smart_edit_toolbar_item.dart';
 export 'outline/outline_block_component.dart';
+export 'table/table_menu.dart';
+export 'table/table_option_action.dart';
 export 'toggle/toggle_block_component.dart';
 export 'toggle/toggle_block_shortcut_event.dart';

+ 111 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_menu.dart

@@ -0,0 +1,111 @@
+import 'package:flutter/material.dart';
+import 'package:appflowy_popover/appflowy_popover.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:appflowy/generated/flowy_svgs.g.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/table/table_option_action.dart';
+import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
+import 'dart:math' as math;
+
+const tableActions = <TableOptionAction>[
+  TableOptionAction.addAfter,
+  TableOptionAction.addBefore,
+  TableOptionAction.delete,
+  TableOptionAction.duplicate,
+  TableOptionAction.clear,
+  TableOptionAction.bgColor,
+];
+
+class TableMenu extends StatelessWidget {
+  const TableMenu({
+    super.key,
+    required this.node,
+    required this.editorState,
+    required this.position,
+    required this.dir,
+    this.onBuild,
+    this.onClose,
+  });
+
+  final Node node;
+  final EditorState editorState;
+  final int position;
+  final TableDirection dir;
+  final VoidCallback? onBuild;
+  final VoidCallback? onClose;
+
+  @override
+  Widget build(BuildContext context) {
+    final actions = tableActions.map((action) {
+      switch (action) {
+        case TableOptionAction.bgColor:
+          return TableColorOptionAction(
+            node: node,
+            editorState: editorState,
+            position: position,
+            dir: dir,
+          );
+        default:
+          return TableOptionActionWrapper(action);
+      }
+    }).toList();
+
+    return PopoverActionList<PopoverAction>(
+      direction: dir == TableDirection.col
+          ? PopoverDirection.bottomWithCenterAligned
+          : PopoverDirection.rightWithTopAligned,
+      actions: actions,
+      onPopupBuilder: onBuild,
+      onClosed: onClose,
+      onSelected: (action, controller) {
+        if (action is TableOptionActionWrapper) {
+          _onSelectAction(action.inner);
+          controller.close();
+        }
+      },
+      buildChild: (controller) => _buildOptionButton(controller, context),
+    );
+  }
+
+  Widget _buildOptionButton(
+    PopoverController controller,
+    BuildContext context,
+  ) {
+    return Card(
+      elevation: 1.0,
+      child: MouseRegion(
+        cursor: SystemMouseCursors.click,
+        child: GestureDetector(
+          onTap: () => controller.show(),
+          child: Transform.rotate(
+            angle: dir == TableDirection.col ? math.pi / 2 : 0,
+            child: const FlowySvg(
+              FlowySvgs.drag_element_s,
+              size: Size.square(18.0),
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+
+  void _onSelectAction(TableOptionAction action) {
+    switch (action) {
+      case TableOptionAction.addAfter:
+        TableActions.add(node, position + 1, editorState, dir);
+        break;
+      case TableOptionAction.addBefore:
+        TableActions.add(node, position, editorState, dir);
+        break;
+      case TableOptionAction.delete:
+        TableActions.delete(node, position, editorState, dir);
+        break;
+      case TableOptionAction.clear:
+        TableActions.clear(node, position, editorState, dir);
+        break;
+      case TableOptionAction.duplicate:
+        TableActions.duplicate(node, position, editorState, dir);
+        break;
+      default:
+    }
+  }
+}

+ 154 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_option_action.dart

@@ -0,0 +1,154 @@
+import 'package:appflowy/generated/flowy_svgs.g.dart';
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/extensions/flowy_tint_extension.dart';
+import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:appflowy_popover/appflowy_popover.dart';
+import 'package:collection/collection.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra/theme_extension.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flowy_infra_ui/style_widget/extension.dart';
+import 'package:flutter/material.dart';
+
+enum TableOptionAction {
+  addAfter,
+  addBefore,
+  delete,
+  duplicate,
+  clear,
+
+  /// row|cell background color
+  bgColor;
+
+  Widget icon(Color? color) {
+    switch (this) {
+      case TableOptionAction.addAfter:
+        return const FlowySvg(FlowySvgs.add_s);
+      case TableOptionAction.addBefore:
+        return const FlowySvg(FlowySvgs.add_s);
+      case TableOptionAction.delete:
+        return const FlowySvg(FlowySvgs.delete_s);
+      case TableOptionAction.duplicate:
+        return const FlowySvg(FlowySvgs.copy_s);
+      case TableOptionAction.clear:
+        return const FlowySvg(FlowySvgs.close_s);
+      case TableOptionAction.bgColor:
+        return const FlowySvg(
+          FlowySvgs.color_format_m,
+          size: Size.square(12),
+        ).padding(all: 2.0);
+    }
+  }
+
+  String get description {
+    switch (this) {
+      case TableOptionAction.addAfter:
+        return LocaleKeys.document_plugins_table_addAfter.tr();
+      case TableOptionAction.addBefore:
+        return LocaleKeys.document_plugins_table_addBefore.tr();
+      case TableOptionAction.delete:
+        return LocaleKeys.document_plugins_table_delete.tr();
+      case TableOptionAction.duplicate:
+        return LocaleKeys.document_plugins_table_duplicate.tr();
+      case TableOptionAction.clear:
+        return LocaleKeys.document_plugins_table_clear.tr();
+      case TableOptionAction.bgColor:
+        return LocaleKeys.document_plugins_table_bgColor.tr();
+    }
+  }
+}
+
+class TableOptionActionWrapper extends ActionCell {
+  TableOptionActionWrapper(this.inner);
+
+  final TableOptionAction inner;
+
+  @override
+  Widget? leftIcon(Color iconColor) => inner.icon(iconColor);
+
+  @override
+  String get name => inner.description;
+}
+
+class TableColorOptionAction extends PopoverActionCell {
+  TableColorOptionAction({
+    required this.node,
+    required this.editorState,
+    required this.position,
+    required this.dir,
+  });
+
+  final Node node;
+  final EditorState editorState;
+  final int position;
+  final TableDirection dir;
+
+  @override
+  Widget? leftIcon(Color iconColor) =>
+      TableOptionAction.bgColor.icon(iconColor);
+
+  @override
+  String get name => TableOptionAction.bgColor.description;
+
+  @override
+  Widget Function(
+    BuildContext context,
+    PopoverController parentController,
+    PopoverController controller,
+  ) get builder => (context, parentController, controller) {
+        int row = 0, col = position;
+        if (dir == TableDirection.row) {
+          col = 0;
+          row = position;
+        }
+
+        final cell = node.children.firstWhereOrNull(
+          (n) =>
+              n.attributes[TableCellBlockKeys.colPosition] == col &&
+              n.attributes[TableCellBlockKeys.rowPosition] == row,
+        );
+        final key = dir == TableDirection.col
+            ? TableCellBlockKeys.colBackgroundColor
+            : TableCellBlockKeys.rowBackgroundColor;
+        final bgColor = cell?.attributes[key] as String?;
+        final selectedColor = bgColor?.toColor();
+        // get default background color from themeExtension
+        final defaultColor = AFThemeExtension.of(context).tableCellBGColor;
+        final colors = [
+          // reset to default background color
+          FlowyColorOption(
+            color: defaultColor,
+            name: LocaleKeys.document_plugins_optionAction_defaultColor.tr(),
+          ),
+          ...FlowyTint.values.map(
+            (e) => FlowyColorOption(
+              color: e.color(context),
+              name: e.tintName(AppFlowyEditorLocalizations.current),
+            ),
+          ),
+        ];
+
+        return FlowyColorPicker(
+          colors: colors,
+          selected: selectedColor,
+          border: Border.all(
+            color: Theme.of(context).colorScheme.onBackground,
+            width: 1,
+          ),
+          onTap: (color, index) async {
+            final backgroundColor = selectedColor != color ? color.toHex() : "";
+            TableActions.setBgColor(
+              node,
+              position,
+              editorState,
+              backgroundColor,
+              dir,
+            );
+
+            controller.close();
+            parentController.close();
+          },
+        );
+      };
+}

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart

@@ -195,7 +195,7 @@ class _ToggleListBlockComponentWidgetState
       child: child,
     );
 
-    if (widget.actionBuilder != null) {
+    if (widget.showActions && widget.actionBuilder != null) {
       child = BlockComponentActionWrapper(
         node: node,
         actionBuilder: widget.actionBuilder!,

+ 1 - 0
frontend/appflowy_flutter/lib/workspace/application/appearance.dart

@@ -362,6 +362,7 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
             fontColor: theme.shader3,
           ),
           calloutBGColor: theme.hoverBG3,
+          tableCellBGColor: theme.surface,
           caption: _getFontStyle(
             fontFamily: fontFamily,
             fontSize: FontSizes.s11,

+ 6 - 0
frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension.dart

@@ -23,6 +23,7 @@ class AFThemeExtension extends ThemeExtension<AFThemeExtension> {
   final Color progressBarBGColor;
   final Color toggleButtonBGColor;
   final Color calloutBGColor;
+  final Color tableCellBGColor;
 
   final TextStyle code;
   final TextStyle callout;
@@ -46,6 +47,7 @@ class AFThemeExtension extends ThemeExtension<AFThemeExtension> {
     required this.toggleOffFill,
     required this.textColor,
     required this.calloutBGColor,
+    required this.tableCellBGColor,
     required this.code,
     required this.callout,
     required this.caption,
@@ -72,6 +74,7 @@ class AFThemeExtension extends ThemeExtension<AFThemeExtension> {
     Color? tint9,
     Color? textColor,
     Color? calloutBGColor,
+    Color? tableCellBGColor,
     Color? greyHover,
     Color? greySelect,
     Color? lightGreyHover,
@@ -96,6 +99,7 @@ class AFThemeExtension extends ThemeExtension<AFThemeExtension> {
       tint9: tint9 ?? this.tint9,
       textColor: textColor ?? this.textColor,
       calloutBGColor: calloutBGColor ?? this.calloutBGColor,
+      tableCellBGColor: tableCellBGColor ?? this.tableCellBGColor,
       greyHover: greyHover ?? this.greyHover,
       greySelect: greySelect ?? this.greySelect,
       lightGreyHover: lightGreyHover ?? this.lightGreyHover,
@@ -128,6 +132,8 @@ class AFThemeExtension extends ThemeExtension<AFThemeExtension> {
       tint9: Color.lerp(tint9, other.tint9, t)!,
       textColor: Color.lerp(textColor, other.textColor, t)!,
       calloutBGColor: Color.lerp(calloutBGColor, other.calloutBGColor, t)!,
+      tableCellBGColor:
+          Color.lerp(tableCellBGColor, other.tableCellBGColor, t)!,
       greyHover: Color.lerp(greyHover, other.greyHover, t)!,
       greySelect: Color.lerp(greySelect, other.greySelect, t)!,
       lightGreyHover: Color.lerp(lightGreyHover, other.lightGreyHover, t)!,

+ 3 - 3
frontend/appflowy_flutter/pubspec.lock

@@ -54,11 +54,11 @@ packages:
     dependency: "direct main"
     description:
       path: "."
-      ref: a912c1c
-      resolved-ref: a912c1c96532ec561ea68d5138aee415fdecede2
+      ref: "0e55cce"
+      resolved-ref: "0e55cce14f2ead916a8942a123d08b818934e2fd"
       url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
     source: git
-    version: "1.2.4"
+    version: "1.3.0"
   appflowy_popover:
     dependency: "direct main"
     description:

+ 1 - 1
frontend/appflowy_flutter/pubspec.yaml

@@ -48,7 +48,7 @@ dependencies:
   appflowy_editor:
     git:
       url: https://github.com/AppFlowy-IO/appflowy-editor.git
-      ref: a912c1c
+      ref: 0e55cce
   appflowy_popover:
     path: packages/appflowy_popover
 

+ 152 - 0
frontend/appflowy_flutter/test/unit_test/editor/transaction_adapter_test.dart

@@ -0,0 +1,152 @@
+import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:appflowy/plugins/document/application/editor_transaction_adapter.dart';
+
+void main() {
+  group('TransactionAdapter', () {
+    test('toBlockAction insert node with children operation', () {
+      final editorState = EditorState.blank();
+
+      final transaction = editorState.transaction;
+      transaction.insertNode(
+        [0],
+        paragraphNode(
+          children: [
+            paragraphNode(text: '1', children: [paragraphNode(text: '1.1')]),
+            paragraphNode(text: '2'),
+            paragraphNode(text: '3', children: [paragraphNode(text: '3.1')]),
+            paragraphNode(text: '4'),
+          ],
+        ),
+      );
+
+      expect(transaction.operations.length, 1);
+      expect(transaction.operations[0] is InsertOperation, true);
+
+      final actions = transaction.operations[0].toBlockAction(editorState);
+
+      expect(actions.length, 7);
+      for (final action in actions) {
+        expect(action.action, BlockActionTypePB.Insert);
+      }
+
+      expect(
+        actions[0].payload.parentId,
+        editorState.document.root.id,
+        reason: '0 - parent id',
+      );
+      expect(
+        actions[0].payload.prevId,
+        editorState.document.root.children.first.id,
+        reason: '0 - prev id',
+      );
+      expect(
+        actions[1].payload.parentId,
+        actions[0].payload.block.id,
+        reason: '1 - parent id',
+      );
+      expect(
+        actions[1].payload.prevId,
+        '',
+        reason: '1 - prev id',
+      );
+      expect(
+        actions[2].payload.parentId,
+        actions[1].payload.block.id,
+        reason: '2 - parent id',
+      );
+      expect(
+        actions[2].payload.prevId,
+        '',
+        reason: '2 - prev id',
+      );
+      expect(
+        actions[3].payload.parentId,
+        actions[0].payload.block.id,
+        reason: '3 - parent id',
+      );
+      expect(
+        actions[3].payload.prevId,
+        actions[1].payload.block.id,
+        reason: '3 - prev id',
+      );
+      expect(
+        actions[4].payload.parentId,
+        actions[0].payload.block.id,
+        reason: '4 - parent id',
+      );
+      expect(
+        actions[4].payload.prevId,
+        actions[3].payload.block.id,
+        reason: '4 - prev id',
+      );
+      expect(
+        actions[5].payload.parentId,
+        actions[4].payload.block.id,
+        reason: '5 - parent id',
+      );
+      expect(
+        actions[5].payload.prevId,
+        '',
+        reason: '5 - prev id',
+      );
+      expect(
+        actions[6].payload.parentId,
+        actions[0].payload.block.id,
+        reason: '6 - parent id',
+      );
+      expect(
+        actions[6].payload.prevId,
+        actions[4].payload.block.id,
+        reason: '6 - prev id',
+      );
+    });
+
+    test('toBlockAction insert node before all children nodes', () {
+      final document = Document(
+        root: Node(
+          type: 'page',
+          children: [
+            paragraphNode(children: [paragraphNode(text: '1')])
+          ],
+        ),
+      );
+      final editorState = EditorState(document: document);
+
+      final transaction = editorState.transaction;
+      transaction.insertNodes([0, 0], [paragraphNode(), paragraphNode()]);
+
+      expect(transaction.operations.length, 1);
+      expect(transaction.operations[0] is InsertOperation, true);
+
+      final actions = transaction.operations[0].toBlockAction(editorState);
+
+      expect(actions.length, 2);
+      for (final action in actions) {
+        expect(action.action, BlockActionTypePB.Insert);
+      }
+
+      expect(
+        actions[0].payload.parentId,
+        editorState.document.root.children.first.id,
+        reason: '0 - parent id',
+      );
+      expect(
+        actions[0].payload.prevId,
+        '',
+        reason: '0 - prev id',
+      );
+      expect(
+        actions[1].payload.parentId,
+        editorState.document.root.children.first.id,
+        reason: '1 - parent id',
+      );
+      expect(
+        actions[1].payload.prevId,
+        actions[0].payload.block.id,
+        reason: '1 - prev id',
+      );
+    });
+  });
+}

+ 8 - 0
frontend/resources/translations/en.json

@@ -557,6 +557,14 @@
       "outline": {
         "addHeadingToCreateOutline": "Add headings to create a table of contents."
       },
+      "table": {
+        "addAfter": "Add after",
+        "addBefore": "Add before",
+        "delete": "Delete",
+        "clear": "Clear content",
+        "duplicate": "Duplicate",
+        "bgColor": "Background color"
+      },
       "contextMenu": {
         "copy": "Copy",
         "cut": "Cut",