Explorar o código

Merge remote-tracking branch 'origin/main' into feature/theme

Lucas.Xu %!s(int64=2) %!d(string=hai) anos
pai
achega
5c13b324ec
Modificáronse 87 ficheiros con 1820 adicións e 897 borrados
  1. 22 0
      CHANGELOG.md
  2. 1 1
      frontend/Makefile.toml
  3. 2 1
      frontend/app_flowy/assets/translations/en.json
  4. 7 2
      frontend/app_flowy/lib/plugins/board/presentation/board_page.dart
  5. 26 26
      frontend/app_flowy/lib/plugins/board/presentation/toolbar/board_setting.dart
  6. 20 3
      frontend/app_flowy/lib/plugins/board/presentation/toolbar/board_toolbar.dart
  7. 7 11
      frontend/app_flowy/lib/plugins/doc/application/doc_bloc.dart
  8. 13 7
      frontend/app_flowy/lib/plugins/doc/application/doc_service.dart
  9. 2 1
      frontend/app_flowy/lib/plugins/doc/document.dart
  10. 1 1
      frontend/app_flowy/lib/plugins/doc/presentation/toolbar/link_button.dart
  11. 26 2
      frontend/app_flowy/lib/plugins/grid/application/field/field_editor_bloc.dart
  12. 9 0
      frontend/app_flowy/lib/plugins/grid/application/row/row_detail_bloc.dart
  13. 1 3
      frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart
  14. 3 1
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_cell.dart
  15. 8 13
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_editor.dart
  16. 12 14
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart
  17. 16 18
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart
  18. 6 10
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/cell_editor.dart
  19. 5 2
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/url_cell.dart
  20. 11 4
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell_action_sheet.dart
  21. 72 3
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_editor.dart
  22. 2 1
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_list.dart
  23. 4 8
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_option_editor.dart
  24. 7 9
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/grid_header.dart
  25. 4 3
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart
  26. 20 23
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/date.dart
  27. 1 1
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/multi_select.dart
  28. 11 12
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/number.dart
  29. 18 20
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/select_option.dart
  30. 1 1
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/single_select.dart
  31. 10 3
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_action_sheet.dart
  32. 53 15
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart
  33. 15 23
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_group.dart
  34. 7 9
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_property.dart
  35. 12 17
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_toolbar.dart
  36. 1 1
      frontend/app_flowy/lib/workspace/presentation/home/menu/app/create_button.dart
  37. 1 1
      frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/header.dart
  38. 1 1
      frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/item.dart
  39. 77 27
      frontend/app_flowy/lib/workspace/presentation/widgets/dialogs.dart
  40. 3 0
      frontend/app_flowy/macos/Runner.xcodeproj/project.pbxproj
  41. 33 1
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart
  42. 6 6
      frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/arrow_keys_handler_test.dart
  43. 8 8
      frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/format_style_handler_test.dart
  44. 2 2
      frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/redo_undo_handler_test.dart
  45. 1 1
      frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/select_all_handler_test.dart
  46. 137 2
      frontend/app_flowy/packages/appflowy_editor/test/service/shortcut_event/shortcut_event_test.dart
  47. 11 11
      frontend/app_flowy/packages/appflowy_popover/lib/popover.dart
  48. 1 1
      frontend/app_flowy/packages/flowy_infra_ui/lib/flowy_infra_ui.dart
  49. 48 0
      frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_stype_popover.dart
  50. 0 59
      frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_popover.dart
  51. 18 8
      frontend/app_flowy/packages/flowy_infra_ui/lib/widget/buttons/primary_button.dart
  52. 17 8
      frontend/app_flowy/packages/flowy_infra_ui/lib/widget/buttons/secondary_button.dart
  53. 1 1
      frontend/app_flowy/packages/flowy_infra_ui/lib/widget/dialog/dialog_size.dart
  54. 4 2
      frontend/app_flowy/packages/flowy_infra_ui/lib/widget/dialog/styled_dialogs.dart
  55. 2 0
      frontend/app_flowy/packages/flowy_infra_ui/pubspec.yaml
  56. 5 5
      frontend/rust-lib/flowy-net/src/http_server/document.rs
  57. 5 5
      frontend/rust-lib/flowy-net/src/local_server/server.rs
  58. 9 9
      frontend/rust-lib/flowy-sdk/src/deps_resolve/folder_deps.rs
  59. 6 6
      frontend/rust-lib/flowy-sdk/src/deps_resolve/text_block_deps.rs
  60. 2 2
      frontend/rust-lib/flowy-sdk/src/lib.rs
  61. 3 3
      frontend/rust-lib/flowy-sdk/src/module.rs
  62. 3 4
      frontend/rust-lib/flowy-text-block/src/editor.rs
  63. 46 0
      frontend/rust-lib/flowy-text-block/src/entities.rs
  64. 20 19
      frontend/rust-lib/flowy-text-block/src/event_handler.rs
  65. 8 8
      frontend/rust-lib/flowy-text-block/src/event_map.rs
  66. 4 4
      frontend/rust-lib/flowy-text-block/src/lib.rs
  67. 63 56
      frontend/rust-lib/flowy-text-block/src/manager.rs
  68. 3 3
      frontend/rust-lib/flowy-text-block/src/queue.rs
  69. 1 1
      frontend/rust-lib/flowy-text-block/tests/document/script.rs
  70. 1 1
      shared-lib/flowy-sync/src/entities/text_block.rs
  71. 1 1
      shared-lib/lib-ot/Cargo.toml
  72. 0 143
      shared-lib/lib-ot/src/core/document/operation.rs
  73. 0 127
      shared-lib/lib-ot/src/core/document/path.rs
  74. 2 2
      shared-lib/lib-ot/src/core/mod.rs
  75. 2 2
      shared-lib/lib-ot/src/core/node_tree/mod.rs
  76. 1 1
      shared-lib/lib-ot/src/core/node_tree/node.rs
  77. 0 0
      shared-lib/lib-ot/src/core/node_tree/node_serde.rs
  78. 174 0
      shared-lib/lib-ot/src/core/node_tree/operation.rs
  79. 0 0
      shared-lib/lib-ot/src/core/node_tree/operation_serde.rs
  80. 190 0
      shared-lib/lib-ot/src/core/node_tree/path.rs
  81. 46 15
      shared-lib/lib-ot/src/core/node_tree/transaction.rs
  82. 28 15
      shared-lib/lib-ot/src/core/node_tree/tree.rs
  83. 0 1
      shared-lib/lib-ot/src/text_delta/delta.rs
  84. 4 3
      shared-lib/lib-ot/tests/node/editor_test.rs
  85. 169 5
      shared-lib/lib-ot/tests/node/operation_test.rs
  86. 79 18
      shared-lib/lib-ot/tests/node/script.rs
  87. 138 30
      shared-lib/lib-ot/tests/node/tree_test.rs

+ 22 - 0
CHANGELOG.md

@@ -1,5 +1,27 @@
 # Release Notes
 
+## Version 0.0.5.1 - 09/14/2022
+
+New features
+- Enable deleting a field in board 
+- Fix some bugs
+
+
+## Version 0.0.5 - 09/08/2022
+New Features - Kanban Board like Notion and Trello beta
+Boards are the best way to manage projects & tasks. Use them to group your databases by select, multiselect, and checkbox.
+
+<p align="left"><img src="https://user-images.githubusercontent.com/12026239/190055984-6efa2d7a-ee38-4551-859e-ee56388e1859.gif" width="1000px" /></p>
+
+- Set up columns that represent a specific phase of the project cycle and use cards to represent each project / task
+- Drag and drop a card from one phase / column to another phase / column
+- Update database properties in the Board view by clicking on a property and making edits on the card
+
+### Other Features & Improvements
+- Settings allow users to change avatars
+- Click and drag the right edge to resize your sidebar
+- And many user interface improvements (link)
+
 ## Version 0.0.5 - beta.2 - beta.1 - 09/01/2022
 
 New features

+ 1 - 1
frontend/Makefile.toml

@@ -22,7 +22,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
 CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
 CARGO_MAKE_CRATE_NAME = "dart-ffi"
 LIB_NAME = "dart_ffi"
-CURRENT_APP_VERSION = "0.0.5"
+CURRENT_APP_VERSION = "0.0.5.1"
 FEATURES = "flutter"
 PRODUCT_NAME = "AppFlowy"
 # CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html

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

@@ -191,7 +191,8 @@
       "optionTitle": "Options",
       "addOption": "Add option",
       "editProperty": "Edit property",
-      "newColumn": "New column"
+      "newColumn": "New column",
+      "deleteFieldPromptMessage": "Are you sure? This property will be deleted"
     },
     "row": {
       "duplicate": "Duplicate",

+ 7 - 2
frontend/app_flowy/lib/plugins/board/presentation/board_page.dart

@@ -287,8 +287,13 @@ class _BoardContentState extends State<BoardContent> {
     );
   }
 
-  void _openCard(String gridId, GridFieldController fieldController,
-      RowPB rowPB, GridRowCache rowCache, BuildContext context) {
+  void _openCard(
+    String gridId,
+    GridFieldController fieldController,
+    RowPB rowPB,
+    GridRowCache rowCache,
+    BuildContext context,
+  ) {
     final rowInfo = RowInfo(
       gridId: gridId,
       fields: UnmodifiableListView(fieldController.fieldContexts),

+ 26 - 26
frontend/app_flowy/lib/plugins/board/presentation/toolbar/board_setting.dart

@@ -2,11 +2,12 @@ import 'package:app_flowy/generated/locale_keys.g.dart';
 import 'package:app_flowy/plugins/board/application/toolbar/board_setting_bloc.dart';
 import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
 import 'package:app_flowy/plugins/grid/presentation/layout/sizes.dart';
+import 'package:app_flowy/plugins/grid/presentation/widgets/toolbar/grid_group.dart';
 import 'package:app_flowy/plugins/grid/presentation/widgets/toolbar/grid_property.dart';
+import 'package:appflowy_popover/popover.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra/theme.dart';
-import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flowy_infra_ui/style_widget/button.dart';
 import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
@@ -141,10 +142,12 @@ extension _GridSettingExtension on BoardSettingAction {
 }
 
 class BoardSettingListPopover extends StatefulWidget {
+  final PopoverController popoverController;
   final BoardSettingContext settingContext;
 
   const BoardSettingListPopover({
     Key? key,
+    required this.popoverController,
     required this.settingContext,
   }) : super(key: key);
 
@@ -153,36 +156,33 @@ class BoardSettingListPopover extends StatefulWidget {
 }
 
 class _BoardSettingListPopoverState extends State<BoardSettingListPopover> {
-  bool _showGridPropertyList = false;
+  BoardSettingAction? _action;
 
   @override
   Widget build(BuildContext context) {
-    if (_showGridPropertyList) {
-      return OverlayContainer(
-        constraints: BoxConstraints.loose(const Size(260, 400)),
-        child: GridPropertyList(
-          gridId: widget.settingContext.viewId,
-          fieldController: widget.settingContext.fieldController,
-        ),
-      );
+    if (_action != null) {
+      switch (_action!) {
+        case BoardSettingAction.groups:
+          return GridGroupList(
+            viewId: widget.settingContext.viewId,
+            fieldController: widget.settingContext.fieldController,
+            onDismissed: () {
+              widget.popoverController.close();
+            },
+          );
+        case BoardSettingAction.properties:
+          return GridPropertyList(
+            gridId: widget.settingContext.viewId,
+            fieldController: widget.settingContext.fieldController,
+          );
+      }
     }
 
-    return OverlayContainer(
-      constraints: BoxConstraints.loose(const Size(140, 400)),
-      child: BoardSettingList(
-        settingContext: widget.settingContext,
-        onAction: (action, settingContext) {
-          switch (action) {
-            case BoardSettingAction.groups:
-              break;
-            case BoardSettingAction.properties:
-              setState(() {
-                _showGridPropertyList = true;
-              });
-              break;
-          }
-        },
-      ),
+    return BoardSettingList(
+      settingContext: widget.settingContext,
+      onAction: (action, settingContext) {
+        setState(() => _action = action);
+      },
     );
   }
 }

+ 20 - 3
frontend/app_flowy/lib/plugins/board/presentation/toolbar/board_toolbar.dart

@@ -2,6 +2,7 @@ import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
 import 'package:appflowy_popover/popover.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra/theme.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flowy_infra_ui/style_widget/icon_button.dart';
 import 'package:flutter/widgets.dart';
 import 'package:provider/provider.dart';
@@ -40,15 +41,30 @@ class BoardToolbar extends StatelessWidget {
   }
 }
 
-class _SettingButton extends StatelessWidget {
+class _SettingButton extends StatefulWidget {
   final BoardSettingContext settingContext;
   const _SettingButton({required this.settingContext, Key? key})
       : super(key: key);
 
+  @override
+  State<_SettingButton> createState() => _SettingButtonState();
+}
+
+class _SettingButtonState extends State<_SettingButton> {
+  late PopoverController popoverController;
+
+  @override
+  void initState() {
+    popoverController = PopoverController();
+    super.initState();
+  }
+
   @override
   Widget build(BuildContext context) {
     final theme = context.read<AppTheme>();
-    return Popover(
+    return AppFlowyStylePopover(
+      controller: popoverController,
+      constraints: BoxConstraints.loose(const Size(260, 400)),
       triggerActions: PopoverTriggerActionFlags.click,
       child: FlowyIconButton(
         hoverColor: theme.hover,
@@ -61,7 +77,8 @@ class _SettingButton extends StatelessWidget {
       ),
       popupBuilder: (BuildContext popoverContext) {
         return BoardSettingListPopover(
-          settingContext: settingContext,
+          settingContext: widget.settingContext,
+          popoverController: popoverController,
         );
       },
     );

+ 7 - 11
frontend/app_flowy/lib/plugins/doc/application/doc_bloc.dart

@@ -90,7 +90,7 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
     final result = await service.openDocument(docId: view.id);
     result.fold(
       (block) {
-        document = _decodeJsonToDocument(block.deltaStr);
+        document = _decodeJsonToDocument(block.snapshot);
         _subscription = document.changes.listen((event) {
           final delta = event.item2;
           final documentDelta = document.toDelta();
@@ -115,16 +115,12 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
   void _composeDelta(Delta composedDelta, Delta documentDelta) async {
     final json = jsonEncode(composedDelta.toJson());
     Log.debug("doc_id: $view.id - Send json: $json");
-    final result = await service.composeDelta(docId: view.id, data: json);
-
-    result.fold((rustDoc) {
-      // final json = utf8.decode(doc.data);
-      final rustDelta = Delta.fromJson(jsonDecode(rustDoc.deltaStr));
-      if (documentDelta != rustDelta) {
-        Log.error("Receive : $rustDelta");
-        Log.error("Expected : $documentDelta");
-      }
-    }, (r) => null);
+    final result = await service.applyEdit(docId: view.id, data: json);
+
+    result.fold(
+      (_) {},
+      (r) => Log.error(r),
+    );
   }
 
   Document _decodeJsonToDocument(String data) {

+ 13 - 7
frontend/app_flowy/lib/plugins/doc/application/doc_service.dart

@@ -4,22 +4,28 @@ import 'package:flowy_sdk/dispatch/dispatch.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-sync/text_block.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-text-block/entities.pb.dart';
 
 class DocumentService {
-  Future<Either<TextBlockDeltaPB, FlowyError>> openDocument({
+  Future<Either<TextBlockPB, FlowyError>> openDocument({
     required String docId,
   }) async {
     await FolderEventSetLatestView(ViewIdPB(value: docId)).send();
 
     final payload = TextBlockIdPB(value: docId);
-    return TextBlockEventGetBlockData(payload).send();
+    return TextBlockEventGetTextBlock(payload).send();
   }
 
-  Future<Either<TextBlockDeltaPB, FlowyError>> composeDelta({required String docId, required String data}) {
-    final payload = TextBlockDeltaPB.create()
-      ..blockId = docId
-      ..deltaStr = data;
-    return TextBlockEventApplyDelta(payload).send();
+  Future<Either<Unit, FlowyError>> applyEdit({
+    required String docId,
+    required String data,
+    String operations = "",
+  }) {
+    final payload = EditPayloadPB.create()
+      ..textBlockId = docId
+      ..operations = operations
+      ..delta = data;
+    return TextBlockEventApplyEdit(payload).send();
   }
 
   Future<Either<Unit, FlowyError>> closeDocument({required String docId}) {

+ 2 - 1
frontend/app_flowy/lib/plugins/doc/document.dart

@@ -186,7 +186,8 @@ class DocumentShareButton extends StatelessWidget {
                 'Exported to: ${LocaleKeys.notifications_export_path.tr()}');
             break;
           case ShareAction.copyLink:
-            FlowyAlertDialog(title: LocaleKeys.shareAction_workInProgress.tr())
+            NavigatorAlertDialog(
+                    title: LocaleKeys.shareAction_workInProgress.tr())
                 .show(context);
             break;
         }

+ 1 - 1
frontend/app_flowy/lib/plugins/doc/presentation/toolbar/link_button.dart

@@ -84,7 +84,7 @@ class FlowyLinkStyleButtonState extends State<FlowyLinkStyleButton> {
       value = values.first;
     }
 
-    TextFieldDialog(
+    NavigatorTextFieldDialog(
       title: 'URL',
       value: value,
       confirm: (newValue) {

+ 26 - 2
frontend/app_flowy/lib/plugins/grid/application/field/field_editor_bloc.dart

@@ -2,6 +2,7 @@ import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'dart:async';
 import 'package:dartz/dartz.dart';
+import 'field_service.dart';
 import 'type_option/type_option_context.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 
@@ -15,10 +16,11 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
   FieldEditorBloc({
     required String gridId,
     required String fieldName,
+    required bool isGroupField,
     required IFieldTypeOptionLoader loader,
   })  : dataController =
             TypeOptionDataController(gridId: gridId, loader: loader),
-        super(FieldEditorState.initial(gridId, fieldName)) {
+        super(FieldEditorState.initial(gridId, fieldName, isGroupField)) {
     on<FieldEditorEvent>(
       (event, emit) async {
         await event.when(
@@ -35,7 +37,23 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
             emit(state.copyWith(name: name));
           },
           didReceiveFieldChanged: (FieldPB field) {
-            emit(state.copyWith(field: Some(field), name: field.name));
+            emit(state.copyWith(
+              field: Some(field),
+              name: field.name,
+              canDelete: field.isPrimary,
+            ));
+          },
+          deleteField: () {
+            state.field.fold(
+              () => null,
+              (field) {
+                final fieldService = FieldService(
+                  gridId: gridId,
+                  fieldId: field.id,
+                );
+                fieldService.deleteField();
+              },
+            );
           },
         );
       },
@@ -52,6 +70,7 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
 class FieldEditorEvent with _$FieldEditorEvent {
   const factory FieldEditorEvent.initial() = _InitialField;
   const factory FieldEditorEvent.updateName(String name) = _UpdateName;
+  const factory FieldEditorEvent.deleteField() = _DeleteField;
   const factory FieldEditorEvent.didReceiveFieldChanged(FieldPB field) =
       _DidReceiveFieldChanged;
 }
@@ -63,16 +82,21 @@ class FieldEditorState with _$FieldEditorState {
     required String errorText,
     required String name,
     required Option<FieldPB> field,
+    required bool canDelete,
+    required bool isGroupField,
   }) = _FieldEditorState;
 
   factory FieldEditorState.initial(
     String gridId,
     String fieldName,
+    bool isGroupField,
   ) =>
       FieldEditorState(
         gridId: gridId,
         errorText: '',
         field: none(),
+        canDelete: false,
         name: fieldName,
+        isGroupField: isGroupField,
       );
 }

+ 9 - 0
frontend/app_flowy/lib/plugins/grid/application/row/row_detail_bloc.dart

@@ -1,4 +1,5 @@
 import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
+import 'package:app_flowy/plugins/grid/application/field/field_service.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
@@ -24,6 +25,13 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
           didReceiveCellDatas: (_DidReceiveCellDatas value) {
             emit(state.copyWith(gridCells: value.gridCells));
           },
+          deleteField: (_DeleteField value) {
+            final fieldService = FieldService(
+              gridId: dataController.rowInfo.gridId,
+              fieldId: value.fieldId,
+            );
+            fieldService.deleteField();
+          },
         );
       },
     );
@@ -49,6 +57,7 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
 @freezed
 class RowDetailEvent with _$RowDetailEvent {
   const factory RowDetailEvent.initial() = _Initial;
+  const factory RowDetailEvent.deleteField(String fieldId) = _DeleteField;
   const factory RowDetailEvent.didReceiveCellDatas(
       List<GridCellIdentifier> gridCells) = _DidReceiveCellDatas;
 }

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

@@ -317,9 +317,7 @@ class _GridFooter extends StatelessWidget {
           height: GridSize.footerHeight,
           child: Padding(
             padding: GridSize.footerContentInsets,
-            child: const Expanded(
-              child: SizedBox(height: 40, child: GridAddRowButton()),
-            ),
+            child: const SizedBox(height: 40, child: GridAddRowButton()),
           ),
         ),
       ),

+ 3 - 1
frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_cell.dart

@@ -1,3 +1,4 @@
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flutter/widgets.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
@@ -62,10 +63,11 @@ class _DateCellState extends GridCellState<GridDateCell> {
       value: _cellBloc,
       child: BlocBuilder<DateCellBloc, DateCellState>(
         builder: (context, state) {
-          return Popover(
+          return AppFlowyStylePopover(
             controller: _popover,
             offset: const Offset(0, 20),
             direction: PopoverDirection.bottomWithLeftAligned,
+            constraints: BoxConstraints.loose(const Size(320, 500)),
             child: SizedBox.expand(
               child: GestureDetector(
                 behavior: HitTestBehavior.opaque,

+ 8 - 13
frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_editor.dart

@@ -64,12 +64,9 @@ class _DateCellEditor extends State<DateCellEditor> {
       return Container();
     }
 
-    return OverlayContainer(
-      constraints: BoxConstraints.loose(const Size(320, 500)),
-      child: _CellCalendarWidget(
-        cellContext: widget.cellController,
-        dateTypeOptionPB: _dateTypeOptionPB!,
-      ),
+    return _CellCalendarWidget(
+      cellContext: widget.cellController,
+      dateTypeOptionPB: _dateTypeOptionPB!,
     );
   }
 }
@@ -302,10 +299,11 @@ class _DateTypeOptionButton extends StatelessWidget {
     return BlocSelector<DateCalBloc, DateCalState, DateTypeOptionPB>(
       selector: (state) => state.dateTypeOptionPB,
       builder: (context, dateTypeOptionPB) {
-        return Popover(
+        return AppFlowyStylePopover(
           triggerActions:
               PopoverTriggerActionFlags.hover | PopoverTriggerActionFlags.click,
           offset: const Offset(20, 0),
+          constraints: BoxConstraints.loose(const Size(140, 100)),
           child: FlowyButton(
             text: FlowyText.medium(title, fontSize: 12),
             hoverColor: theme.hover,
@@ -313,12 +311,9 @@ class _DateTypeOptionButton extends StatelessWidget {
             rightIcon: svgWidget("grid/more", color: theme.iconColor),
           ),
           popupBuilder: (BuildContext popContext) {
-            return OverlayContainer(
-              constraints: BoxConstraints.loose(const Size(140, 100)),
-              child: _CalDateTimeSetting(
-                dateTypeOptionPB: dateTypeOptionPB,
-                onEvent: (event) => context.read<DateCalBloc>().add(event),
-              ),
+            return _CalDateTimeSetting(
+              dateTypeOptionPB: dateTypeOptionPB,
+              onEvent: (event) => context.read<DateCalBloc>().add(event),
             );
           },
         );

+ 12 - 14
frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart

@@ -3,7 +3,7 @@ import 'package:app_flowy/plugins/grid/application/prelude.dart';
 import 'package:appflowy_popover/popover.dart';
 
 import 'package:flowy_infra/theme.dart';
-import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
 // ignore: unused_import
 import 'package:flowy_sdk/log.dart';
@@ -194,8 +194,10 @@ class _SelectOptionWrapState extends State<SelectOptionWrap> {
       alignment: AlignmentDirectional.center,
       fit: StackFit.expand,
       children: [
-        Popover(
+        AppFlowyStylePopover(
           controller: _popover,
+          constraints: BoxConstraints.loose(
+              Size(SelectOptionCellEditor.editorPanelWidth, 300)),
           offset: const Offset(0, 20),
           direction: PopoverDirection.bottomWithLeftAligned,
           // triggerActions: PopoverTriggerActionFlags.c,
@@ -203,18 +205,14 @@ class _SelectOptionWrapState extends State<SelectOptionWrap> {
             WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
               widget.onFocus?.call(true);
             });
-            return OverlayContainer(
-              constraints: BoxConstraints.loose(
-                  Size(SelectOptionCellEditor.editorPanelWidth, 300)),
-              child: SizedBox(
-                width: SelectOptionCellEditor.editorPanelWidth,
-                child: SelectOptionCellEditor(
-                  cellController: widget.cellControllerBuilder.build()
-                      as GridSelectOptionCellController,
-                  onDismissed: () {
-                    widget.onFocus?.call(false);
-                  },
-                ),
+            return SizedBox(
+              width: SelectOptionCellEditor.editorPanelWidth,
+              child: SelectOptionCellEditor(
+                cellController: widget.cellControllerBuilder.build()
+                    as GridSelectOptionCellController,
+                onDismissed: () {
+                  widget.onFocus?.call(false);
+                },
               ),
             );
           },

+ 16 - 18
frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart

@@ -251,9 +251,10 @@ class _SelectOptionCellState extends State<_SelectOptionCell> {
   @override
   Widget build(BuildContext context) {
     final theme = context.watch<AppTheme>();
-    return Popover(
+    return AppFlowyStylePopover(
       controller: _popoverController,
       offset: const Offset(20, 0),
+      constraints: BoxConstraints.loose(const Size(200, 300)),
       child: SizedBox(
         height: GridSize.typeOptionItemHeight,
         child: Row(
@@ -286,23 +287,20 @@ class _SelectOptionCellState extends State<_SelectOptionCell> {
         ),
       ),
       popupBuilder: (BuildContext popoverContext) {
-        return OverlayContainer(
-          constraints: BoxConstraints.loose(const Size(200, 300)),
-          child: SelectOptionTypeOptionEditor(
-            option: widget.option,
-            onDeleted: () {
-              context
-                  .read<SelectOptionCellEditorBloc>()
-                  .add(SelectOptionEditorEvent.deleteOption(widget.option));
-            },
-            onUpdated: (updatedOption) {
-              context
-                  .read<SelectOptionCellEditorBloc>()
-                  .add(SelectOptionEditorEvent.updateOption(updatedOption));
-            },
-            key: ValueKey(widget.option
-                .id), // Use ValueKey to refresh the UI, otherwise, it will remain the old value.
-          ),
+        return SelectOptionTypeOptionEditor(
+          option: widget.option,
+          onDeleted: () {
+            context
+                .read<SelectOptionCellEditorBloc>()
+                .add(SelectOptionEditorEvent.deleteOption(widget.option));
+          },
+          onUpdated: (updatedOption) {
+            context
+                .read<SelectOptionCellEditorBloc>()
+                .add(SelectOptionEditorEvent.updateOption(updatedOption));
+          },
+          key: ValueKey(widget.option
+              .id), // Use ValueKey to refresh the UI, otherwise, it will remain the old value.
         );
       },
     );

+ 6 - 10
frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/cell_editor.dart

@@ -1,6 +1,5 @@
 import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
 import 'package:app_flowy/plugins/grid/application/cell/url_cell_editor_bloc.dart';
-import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flutter/material.dart';
 import 'dart:async';
 
@@ -79,15 +78,12 @@ class URLEditorPopover extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    return OverlayContainer(
-      constraints: BoxConstraints.loose(const Size(300, 160)),
-      child: SizedBox(
-        width: 200,
-        child: Padding(
-          padding: const EdgeInsets.all(6),
-          child: URLCellEditor(
-            cellController: cellController,
-          ),
+    return SizedBox(
+      width: 200,
+      child: Padding(
+        padding: const EdgeInsets.all(6),
+        child: URLCellEditor(
+          cellController: cellController,
         ),
       ),
     );

+ 5 - 2
frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/url_cell.dart

@@ -6,6 +6,7 @@ import 'package:appflowy_popover/popover.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra/theme.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
@@ -129,8 +130,9 @@ class _GridURLCellState extends GridCellState<GridURLCell> {
             ),
           );
 
-          return Popover(
+          return AppFlowyStylePopover(
             controller: _popoverController,
+            constraints: BoxConstraints.loose(const Size(300, 160)),
             direction: PopoverDirection.bottomWithLeftAligned,
             offset: const Offset(0, 20),
             child: SizedBox.expand(
@@ -214,7 +216,8 @@ class _EditURLAccessoryState extends State<_EditURLAccessory>
   @override
   Widget build(BuildContext context) {
     final theme = context.watch<AppTheme>();
-    return Popover(
+    return AppFlowyStylePopover(
+      constraints: BoxConstraints.loose(const Size(300, 160)),
       controller: _popoverController,
       direction: PopoverDirection.bottomWithLeftAligned,
       triggerActions: PopoverTriggerActionFlags.click,

+ 11 - 4
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell_action_sheet.dart

@@ -2,6 +2,7 @@ import 'package:app_flowy/plugins/grid/application/field/type_option/type_option
 import 'package:app_flowy/plugins/grid/presentation/widgets/header/field_editor.dart';
 import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/plugins/grid/application/prelude.dart';
+import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra/theme.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -168,7 +169,7 @@ class FieldActionCell extends StatelessWidget {
         }
       },
       leftIcon: svgWidget(action.iconName(),
-        color: enable ? theme.iconColor : theme.disableIconColor),
+          color: enable ? theme.iconColor : theme.disableIconColor),
     );
   }
 }
@@ -215,9 +216,15 @@ extension _FieldActionExtension on FieldAction {
             .add(const FieldActionSheetEvent.duplicateField());
         break;
       case FieldAction.delete:
-        context
-            .read<FieldActionSheetBloc>()
-            .add(const FieldActionSheetEvent.deleteField());
+        NavigatorAlertDialog(
+          title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
+          confirm: () {
+            context
+                .read<FieldActionSheetBloc>()
+                .add(const FieldActionSheetEvent.deleteField());
+          },
+        ).show(context);
+
         break;
     }
   }

+ 72 - 3
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_editor.dart

@@ -2,8 +2,13 @@ import 'package:app_flowy/plugins/grid/application/field/field_editor_bloc.dart'
 import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart';
 import 'package:appflowy_popover/popover.dart';
 import 'package:easy_localization/easy_localization.dart';
+import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart';
+import 'package:flowy_infra/theme.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flowy_infra_ui/style_widget/button.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flowy_infra_ui/widget/spacing.dart';
+import 'package:flowy_sdk/log.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:app_flowy/generated/locale_keys.g.dart';
@@ -13,14 +18,16 @@ import 'field_type_option_editor.dart';
 class FieldEditor extends StatefulWidget {
   final String gridId;
   final String fieldName;
-  final VoidCallback? onRemoved;
+  final bool isGroupField;
+  final Function(String)? onDeleted;
 
   final IFieldTypeOptionLoader typeOptionLoader;
   const FieldEditor({
     required this.gridId,
     this.fieldName = "",
     required this.typeOptionLoader,
-    this.onRemoved,
+    this.isGroupField = false,
+    this.onDeleted,
     Key? key,
   }) : super(key: key);
 
@@ -43,10 +50,10 @@ class _FieldEditorState extends State<FieldEditor> {
       create: (context) => FieldEditorBloc(
         gridId: widget.gridId,
         fieldName: widget.fieldName,
+        isGroupField: widget.isGroupField,
         loader: widget.typeOptionLoader,
       )..add(const FieldEditorEvent.initial()),
       child: BlocBuilder<FieldEditorBloc, FieldEditorState>(
-        buildWhen: (p, c) => false,
         builder: (context, state) {
           return ListView(
             shrinkWrap: true,
@@ -56,6 +63,16 @@ class _FieldEditorState extends State<FieldEditor> {
               const VSpace(10),
               const _FieldNameCell(),
               const VSpace(10),
+              _DeleteFieldButton(
+                popoverMutex: popoverMutex,
+                onDeleted: () {
+                  state.field.fold(
+                    () => Log.error('Can not delete the field'),
+                    (field) => widget.onDeleted?.call(field.id),
+                  );
+                },
+              ),
+              const VSpace(10),
               _FieldTypeOptionCell(popoverMutex: popoverMutex),
             ],
           );
@@ -114,3 +131,55 @@ class _FieldNameCell extends StatelessWidget {
     );
   }
 }
+
+class _DeleteFieldButton extends StatelessWidget {
+  final PopoverMutex popoverMutex;
+  final VoidCallback? onDeleted;
+
+  const _DeleteFieldButton({
+    required this.popoverMutex,
+    required this.onDeleted,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = context.watch<AppTheme>();
+    return BlocBuilder<FieldEditorBloc, FieldEditorState>(
+      buildWhen: (previous, current) => previous != current,
+      builder: (context, state) {
+        final enable = !state.canDelete && !state.isGroupField;
+        Widget button = FlowyButton(
+          text: FlowyText.medium(
+            LocaleKeys.grid_field_delete.tr(),
+            fontSize: 12,
+            color: enable ? null : theme.shader4,
+          ),
+        );
+        if (enable) button = _wrapPopover(button);
+        return button;
+      },
+    );
+  }
+
+  Widget _wrapPopover(Widget widget) {
+    return AppFlowyStylePopover(
+      triggerActions: PopoverTriggerActionFlags.click,
+      constraints: BoxConstraints.loose(const Size(400, 240)),
+      mutex: popoverMutex,
+      direction: PopoverDirection.center,
+      popupBuilder: (popupContext) {
+        return PopoverAlertView(
+          title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
+          cancel: () => popoverMutex.state?.close(),
+          confirm: () {
+            onDeleted?.call();
+            popoverMutex.state?.close();
+          },
+          popoverMutex: popoverMutex,
+        );
+      },
+      child: widget,
+    );
+  }
+}

+ 2 - 1
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_list.dart

@@ -1,3 +1,4 @@
+import 'package:appflowy_popover/popover.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra/theme.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -25,7 +26,7 @@ class FieldTypeList extends StatelessWidget with FlowyOverlayDelegate {
         fieldType: fieldType,
         onSelectField: (fieldType) {
           onSelectField(fieldType);
-          FlowyOverlay.of(context).remove(FieldTypeList.identifier());
+          PopoverContainer.of(context).closeAll();
         },
       );
     }).toList();

+ 4 - 8
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_option_editor.dart

@@ -64,19 +64,15 @@ class FieldTypeOptionEditor extends StatelessWidget {
     final theme = context.watch<AppTheme>();
     return SizedBox(
       height: GridSize.typeOptionItemHeight,
-      child: Popover(
-        triggerActions:
-            PopoverTriggerActionFlags.hover | PopoverTriggerActionFlags.click,
+      child: AppFlowyStylePopover(
+        constraints: BoxConstraints.loose(const Size(460, 440)),
+        triggerActions: PopoverTriggerActionFlags.click,
         mutex: popoverMutex,
         offset: const Offset(20, 0),
         popupBuilder: (context) {
-          final list = FieldTypeList(onSelectField: (newFieldType) {
+          return FieldTypeList(onSelectField: (newFieldType) {
             dataController.switchToField(newFieldType);
           });
-          return OverlayContainer(
-            constraints: BoxConstraints.loose(const Size(460, 440)),
-            child: list,
-          );
         },
         child: FlowyButton(
           text: FlowyText.medium(field.fieldType.title(), fontSize: 12),

+ 7 - 9
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/grid_header.dart

@@ -7,7 +7,7 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:appflowy_popover/popover.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra/theme.dart';
-import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flowy_infra_ui/style_widget/button.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
@@ -176,9 +176,10 @@ class CreateFieldButton extends StatelessWidget {
   Widget build(BuildContext context) {
     final theme = context.watch<AppTheme>();
 
-    return Popover(
+    return AppFlowyStylePopover(
       triggerActions: PopoverTriggerActionFlags.click,
       direction: PopoverDirection.bottomWithRightAligned,
+      constraints: BoxConstraints.loose(const Size(240, 200)),
       child: FlowyButton(
         text: FlowyText.medium(
           LocaleKeys.grid_field_newColumn.tr(),
@@ -192,13 +193,10 @@ class CreateFieldButton extends StatelessWidget {
         ),
       ),
       popupBuilder: (BuildContext popover) {
-        return OverlayContainer(
-          constraints: BoxConstraints.loose(const Size(240, 200)),
-          child: FieldEditor(
-            gridId: gridId,
-            fieldName: "",
-            typeOptionLoader: NewFieldTypeOptionLoader(gridId: gridId),
-          ),
+        return FieldEditor(
+          gridId: gridId,
+          fieldName: "",
+          typeOptionLoader: NewFieldTypeOptionLoader(gridId: gridId),
         );
       },
     );

+ 4 - 3
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart

@@ -54,9 +54,10 @@ Widget? makeTypeOptionWidget({
   return builder.build(context);
 }
 
-TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder(
-    {required TypeOptionDataController dataController,
-    required PopoverMutex popoverMutex}) {
+TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder({
+  required TypeOptionDataController dataController,
+  required PopoverMutex popoverMutex,
+}) {
   final gridId = dataController.gridId;
   final fieldType = dataController.field.fieldType;
 

+ 20 - 23
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/date.dart

@@ -62,23 +62,21 @@ class DateTypeOptionWidget extends TypeOptionWidget {
   }
 
   Widget _renderDateFormatButton(BuildContext context, DateFormat dataFormat) {
-    return Popover(
+    return AppFlowyStylePopover(
       mutex: popoverMutex,
       triggerActions:
           PopoverTriggerActionFlags.hover | PopoverTriggerActionFlags.click,
       offset: const Offset(20, 0),
+      constraints: BoxConstraints.loose(const Size(460, 440)),
       popupBuilder: (popoverContext) {
-        return OverlayContainer(
-          constraints: BoxConstraints.loose(const Size(460, 440)),
-          child: DateFormatList(
-            selectedFormat: dataFormat,
-            onSelected: (format) {
-              context
-                  .read<DateTypeOptionBloc>()
-                  .add(DateTypeOptionEvent.didSelectDateFormat(format));
-              PopoverContainerState.of(popoverContext).closeAll();
-            },
-          ),
+        return DateFormatList(
+          selectedFormat: dataFormat,
+          onSelected: (format) {
+            context
+                .read<DateTypeOptionBloc>()
+                .add(DateTypeOptionEvent.didSelectDateFormat(format));
+            PopoverContainer.of(popoverContext).closeAll();
+          },
         );
       },
       child: const DateFormatButton(),
@@ -86,22 +84,21 @@ class DateTypeOptionWidget extends TypeOptionWidget {
   }
 
   Widget _renderTimeFormatButton(BuildContext context, TimeFormat timeFormat) {
-    return Popover(
+    return AppFlowyStylePopover(
       mutex: popoverMutex,
       triggerActions:
           PopoverTriggerActionFlags.hover | PopoverTriggerActionFlags.click,
       offset: const Offset(20, 0),
+      constraints: BoxConstraints.loose(const Size(460, 440)),
       popupBuilder: (BuildContext popoverContext) {
-        return OverlayContainer(
-          constraints: BoxConstraints.loose(const Size(460, 440)),
-          child: TimeFormatList(
-              selectedFormat: timeFormat,
-              onSelected: (format) {
-                context
-                    .read<DateTypeOptionBloc>()
-                    .add(DateTypeOptionEvent.didSelectTimeFormat(format));
-                PopoverContainerState.of(popoverContext).closeAll();
-              }),
+        return TimeFormatList(
+          selectedFormat: timeFormat,
+          onSelected: (format) {
+            context
+                .read<DateTypeOptionBloc>()
+                .add(DateTypeOptionEvent.didSelectTimeFormat(format));
+            PopoverContainer.of(popoverContext).closeAll();
+          },
         );
       },
       child: TimeFormatButton(timeFormat: timeFormat),

+ 1 - 1
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/multi_select.dart

@@ -41,7 +41,7 @@ class MultiSelectTypeOptionWidget extends TypeOptionWidget {
     return SelectOptionTypeOptionWidget(
       options: selectOptionAction.typeOption.options,
       beginEdit: () {
-        PopoverContainerState.of(context).closeAll();
+        PopoverContainer.of(context).closeAll();
       },
       popoverMutex: popoverMutex,
       typeOptionAction: selectOptionAction,

+ 11 - 12
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/number.dart

@@ -55,11 +55,12 @@ class NumberTypeOptionWidget extends TypeOptionWidget {
           listener: (context, state) =>
               typeOptionContext.typeOption = state.typeOption,
           builder: (context, state) {
-            return Popover(
+            return AppFlowyStylePopover(
               mutex: popoverMutex,
               triggerActions: PopoverTriggerActionFlags.hover |
                   PopoverTriggerActionFlags.click,
               offset: const Offset(20, 0),
+              constraints: BoxConstraints.loose(const Size(460, 440)),
               child: FlowyButton(
                 margin: GridSize.typeOptionContentInsets,
                 hoverColor: theme.hover,
@@ -76,17 +77,14 @@ class NumberTypeOptionWidget extends TypeOptionWidget {
                 ),
               ),
               popupBuilder: (BuildContext popoverContext) {
-                return OverlayContainer(
-                  constraints: BoxConstraints.loose(const Size(460, 440)),
-                  child: NumberFormatList(
-                    onSelected: (format) {
-                      context
-                          .read<NumberTypeOptionBloc>()
-                          .add(NumberTypeOptionEvent.didSelectFormat(format));
-                      PopoverContainerState.of(popoverContext).closeAll();
-                    },
-                    selectedFormat: state.typeOption.format,
-                  ),
+                return NumberFormatList(
+                  onSelected: (format) {
+                    context
+                        .read<NumberTypeOptionBloc>()
+                        .add(NumberTypeOptionEvent.didSelectFormat(format));
+                    PopoverContainer.of(popoverContext).closeAll();
+                  },
+                  selectedFormat: state.typeOption.format,
                 );
               },
             );
@@ -116,6 +114,7 @@ class NumberFormatList extends StatelessWidget {
           crossAxisAlignment: CrossAxisAlignment.start,
           children: [
             const _FilterTextField(),
+            const VSpace(10),
             BlocBuilder<NumberFormatBloc, NumberFormatState>(
               builder: (context, state) {
                 final cells = state.formats.map((format) {

+ 18 - 20
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/select_option.dart

@@ -2,7 +2,7 @@ import 'package:app_flowy/plugins/grid/application/field/type_option/select_opti
 import 'package:appflowy_popover/popover.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra/theme.dart';
-import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flowy_infra_ui/style_widget/button.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flowy_infra_ui/widget/spacing.dart';
@@ -180,10 +180,11 @@ class _OptionCellState extends State<_OptionCell> {
   Widget build(BuildContext context) {
     final theme = context.watch<AppTheme>();
 
-    return Popover(
+    return AppFlowyStylePopover(
       controller: _popoverController,
       mutex: widget.popoverMutex,
       offset: const Offset(20, 0),
+      constraints: BoxConstraints.loose(const Size(460, 440)),
       child: SizedBox(
         height: GridSize.typeOptionItemHeight,
         child: SelectOptionTagCell(
@@ -200,24 +201,21 @@ class _OptionCellState extends State<_OptionCell> {
         ),
       ),
       popupBuilder: (BuildContext popoverContext) {
-        return OverlayContainer(
-          constraints: BoxConstraints.loose(const Size(460, 440)),
-          child: SelectOptionTypeOptionEditor(
-            option: widget.option,
-            onDeleted: () {
-              context
-                  .read<SelectOptionTypeOptionBloc>()
-                  .add(SelectOptionTypeOptionEvent.deleteOption(widget.option));
-              PopoverContainerState.of(popoverContext).closeAll();
-            },
-            onUpdated: (updatedOption) {
-              context
-                  .read<SelectOptionTypeOptionBloc>()
-                  .add(SelectOptionTypeOptionEvent.updateOption(updatedOption));
-              PopoverContainerState.of(popoverContext).closeAll();
-            },
-            key: ValueKey(widget.option.id),
-          ),
+        return SelectOptionTypeOptionEditor(
+          option: widget.option,
+          onDeleted: () {
+            context
+                .read<SelectOptionTypeOptionBloc>()
+                .add(SelectOptionTypeOptionEvent.deleteOption(widget.option));
+            PopoverContainer.of(popoverContext).closeAll();
+          },
+          onUpdated: (updatedOption) {
+            context
+                .read<SelectOptionTypeOptionBloc>()
+                .add(SelectOptionTypeOptionEvent.updateOption(updatedOption));
+            PopoverContainer.of(popoverContext).closeAll();
+          },
+          key: ValueKey(widget.option.id),
         );
       },
     );

+ 1 - 1
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/single_select.dart

@@ -40,7 +40,7 @@ class SingleSelectTypeOptionWidget extends TypeOptionWidget {
     return SelectOptionTypeOptionWidget(
       options: selectOptionAction.typeOption.options,
       beginEdit: () {
-        PopoverContainerState.of(context).closeAll();
+        PopoverContainer.of(context).closeAll();
       },
       popoverMutex: popoverMutex,
       typeOptionAction: selectOptionAction,

+ 10 - 3
frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_action_sheet.dart

@@ -1,4 +1,5 @@
 import 'package:app_flowy/plugins/grid/application/row/row_action_sheet_bloc.dart';
+import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:app_flowy/generated/locale_keys.g.dart';
 import 'package:flowy_infra/image.dart';
@@ -150,9 +151,15 @@ extension _RowActionExtension on _RowAction {
             .add(const RowActionSheetEvent.duplicateRow());
         break;
       case _RowAction.delete:
-        context
-            .read<RowActionSheetBloc>()
-            .add(const RowActionSheetEvent.deleteRow());
+        NavigatorAlertDialog(
+          title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
+          confirm: () {
+            context
+                .read<RowActionSheetBloc>()
+                .add(const RowActionSheetEvent.deleteRow());
+          },
+        ).show(context);
+
         break;
     }
   }

+ 53 - 15
frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart

@@ -133,6 +133,7 @@ class _PropertyList extends StatelessWidget {
                 ),
               ),
             ),
+            const VSpace(10),
             _CreateFieldButton(
               viewId: viewId,
               onClosed: () {
@@ -144,13 +145,16 @@ class _PropertyList extends StatelessWidget {
                   );
                 });
               },
-              onOpened: () {
-                return OverlayContainer(
-                  constraints: BoxConstraints.loose(const Size(240, 200)),
-                  child: FieldEditor(
-                    gridId: viewId,
-                    typeOptionLoader: NewFieldTypeOptionLoader(gridId: viewId),
-                  ),
+              onOpened: (controller) {
+                return FieldEditor(
+                  gridId: viewId,
+                  typeOptionLoader: NewFieldTypeOptionLoader(gridId: viewId),
+                  onDeleted: (fieldId) {
+                    controller.close();
+                    context
+                        .read<RowDetailBloc>()
+                        .add(RowDetailEvent.deleteField(fieldId));
+                  },
                 );
               },
             ),
@@ -161,10 +165,11 @@ class _PropertyList extends StatelessWidget {
   }
 }
 
-class _CreateFieldButton extends StatelessWidget {
+class _CreateFieldButton extends StatefulWidget {
   final String viewId;
-  final Widget Function() onOpened;
+  final Widget Function(PopoverController) onOpened;
   final VoidCallback onClosed;
+
   const _CreateFieldButton({
     required this.viewId,
     required this.onOpened,
@@ -172,16 +177,32 @@ class _CreateFieldButton extends StatelessWidget {
     Key? key,
   }) : super(key: key);
 
+  @override
+  State<_CreateFieldButton> createState() => _CreateFieldButtonState();
+}
+
+class _CreateFieldButtonState extends State<_CreateFieldButton> {
+  late PopoverController popoverController;
+
+  @override
+  void initState() {
+    popoverController = PopoverController();
+    super.initState();
+  }
+
   @override
   Widget build(BuildContext context) {
     final theme = context.read<AppTheme>();
 
-    return Popover(
+    return AppFlowyStylePopover(
+      constraints: BoxConstraints.loose(const Size(240, 200)),
+      controller: popoverController,
       triggerActions: PopoverTriggerActionFlags.click,
-      direction: PopoverDirection.bottomWithLeftAligned,
-      onClose: onClosed,
-      child: SizedBox(
+      direction: PopoverDirection.topWithLeftAligned,
+      onClose: widget.onClosed,
+      child: Container(
         height: 40,
+        decoration: _makeBoxDecoration(context),
         child: FlowyButton(
           text: FlowyText.medium(
             LocaleKeys.grid_field_newColumn.tr(),
@@ -192,7 +213,17 @@ class _CreateFieldButton extends StatelessWidget {
           leftIcon: svgWidget("home/add"),
         ),
       ),
-      popupBuilder: (BuildContext context) => onOpened(),
+      popupBuilder: (BuildContext context) =>
+          widget.onOpened(popoverController),
+    );
+  }
+
+  BoxDecoration _makeBoxDecoration(BuildContext context) {
+    final theme = context.read<AppTheme>();
+    final borderSide = BorderSide(color: theme.shader6, width: 1.0);
+    return BoxDecoration(
+      color: theme.surface,
+      border: Border(top: borderSide),
     );
   }
 }
@@ -241,16 +272,23 @@ class _RowDetailCellState extends State<_RowDetailCell> {
               child: Popover(
                 controller: popover,
                 offset: const Offset(20, 0),
-                popupBuilder: (context) {
+                popupBuilder: (popoverContext) {
                   return OverlayContainer(
                     constraints: BoxConstraints.loose(const Size(240, 200)),
                     child: FieldEditor(
                       gridId: widget.cellId.gridId,
                       fieldName: widget.cellId.fieldContext.field.name,
+                      isGroupField: widget.cellId.fieldContext.isGroupField,
                       typeOptionLoader: FieldTypeOptionLoader(
                         gridId: widget.cellId.gridId,
                         field: widget.cellId.fieldContext.field,
                       ),
+                      onDeleted: (fieldId) {
+                        popover.close();
+                        context
+                            .read<RowDetailBloc>()
+                            .add(RowDetailEvent.deleteField(fieldId));
+                      },
                     ),
                   );
                 },

+ 15 - 23
frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_group.dart

@@ -3,7 +3,6 @@ import 'package:app_flowy/plugins/grid/presentation/layout/sizes.dart';
 import 'package:app_flowy/plugins/grid/presentation/widgets/header/field_type_extension.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra/theme.dart';
-import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flowy_infra_ui/style_widget/button.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flowy_infra_ui/widget/spacing.dart';
@@ -15,9 +14,11 @@ import 'package:flutter_bloc/flutter_bloc.dart';
 class GridGroupList extends StatelessWidget {
   final String viewId;
   final GridFieldController fieldController;
+  final VoidCallback onDismissed;
   const GridGroupList({
     required this.viewId,
     required this.fieldController,
+    required this.onDismissed,
     Key? key,
   }) : super(key: key);
 
@@ -33,6 +34,7 @@ class GridGroupList extends StatelessWidget {
           final cells = state.fieldContexts.map((fieldContext) {
             Widget cell = _GridGroupCell(
               fieldContext: fieldContext,
+              onSelected: () => onDismissed(),
               key: ValueKey(fieldContext.id),
             );
 
@@ -56,29 +58,16 @@ class GridGroupList extends StatelessWidget {
       ),
     );
   }
-
-  void show(BuildContext context) {
-    FlowyOverlay.of(context).insertWithAnchor(
-      widget: OverlayContainer(
-        constraints: BoxConstraints.loose(const Size(260, 400)),
-        child: this,
-      ),
-      identifier: identifier(),
-      anchorContext: context,
-      anchorDirection: AnchorDirection.bottomRight,
-      style: FlowyOverlayStyle(blur: false),
-    );
-  }
-
-  static String identifier() {
-    return (GridGroupList).toString();
-  }
 }
 
 class _GridGroupCell extends StatelessWidget {
+  final VoidCallback onSelected;
   final GridFieldContext fieldContext;
-  const _GridGroupCell({required this.fieldContext, Key? key})
-      : super(key: key);
+  const _GridGroupCell({
+    required this.fieldContext,
+    required this.onSelected,
+    Key? key,
+  }) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
@@ -97,8 +86,10 @@ class _GridGroupCell extends StatelessWidget {
       child: FlowyButton(
         text: FlowyText.medium(fieldContext.name, fontSize: 12),
         hoverColor: theme.hover,
-        leftIcon: svgWidget(fieldContext.fieldType.iconName(),
-            color: theme.iconColor),
+        leftIcon: svgWidget(
+          fieldContext.fieldType.iconName(),
+          color: theme.iconColor,
+        ),
         rightIcon: rightIcon,
         onTap: () {
           context.read<GridGroupBloc>().add(
@@ -107,7 +98,8 @@ class _GridGroupCell extends StatelessWidget {
                   fieldContext.fieldType,
                 ),
               );
-          FlowyOverlay.of(context).remove(GridGroupList.identifier());
+          onSelected();
+          // FlowyOverlay.of(context).remove(GridGroupList.identifier());
         },
       ),
     );

+ 7 - 9
frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_property.dart

@@ -116,10 +116,11 @@ class _GridPropertyCell extends StatelessWidget {
   }
 
   Widget _editFieldButton(AppTheme theme, BuildContext context) {
-    return Popover(
+    return AppFlowyStylePopover(
       mutex: popoverMutex,
       triggerActions: PopoverTriggerActionFlags.click,
       offset: const Offset(20, 0),
+      constraints: BoxConstraints.loose(const Size(240, 200)),
       child: FlowyButton(
         text: FlowyText.medium(fieldContext.name, fontSize: 12),
         hoverColor: theme.hover,
@@ -127,14 +128,11 @@ class _GridPropertyCell extends StatelessWidget {
             color: theme.iconColor),
       ),
       popupBuilder: (BuildContext context) {
-        return OverlayContainer(
-          constraints: BoxConstraints.loose(const Size(240, 200)),
-          child: FieldEditor(
-            gridId: gridId,
-            fieldName: fieldContext.name,
-            typeOptionLoader: FieldTypeOptionLoader(
-                gridId: gridId, field: fieldContext.field),
-          ),
+        return FieldEditor(
+          gridId: gridId,
+          fieldName: fieldContext.name,
+          typeOptionLoader:
+              FieldTypeOptionLoader(gridId: gridId, field: fieldContext.field),
         );
       },
     );

+ 12 - 17
frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_toolbar.dart

@@ -53,7 +53,8 @@ class _SettingButton extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     final theme = context.watch<AppTheme>();
-    return Popover(
+    return AppFlowyStylePopover(
+      constraints: BoxConstraints.loose(const Size(260, 400)),
       triggerActions: PopoverTriggerActionFlags.click,
       offset: const Offset(0, 10),
       child: FlowyIconButton(
@@ -87,25 +88,19 @@ class _GridSettingListPopoverState extends State<_GridSettingListPopover> {
   @override
   Widget build(BuildContext context) {
     if (_action == GridSettingAction.properties) {
-      return OverlayContainer(
-        constraints: BoxConstraints.loose(const Size(260, 400)),
-        child: GridPropertyList(
-          gridId: widget.settingContext.gridId,
-          fieldController: widget.settingContext.fieldController,
-        ),
+      return GridPropertyList(
+        gridId: widget.settingContext.gridId,
+        fieldController: widget.settingContext.fieldController,
       );
     }
 
-    return OverlayContainer(
-      constraints: BoxConstraints.loose(const Size(140, 400)),
-      child: GridSettingList(
-        settingContext: widget.settingContext,
-        onAction: (action, settingContext) {
-          setState(() {
-            _action = action;
-          });
-        },
-      ),
+    return GridSettingList(
+      settingContext: widget.settingContext,
+      onAction: (action, settingContext) {
+        setState(() {
+          _action = action;
+        });
+      },
     );
   }
 }

+ 1 - 1
frontend/app_flowy/lib/workspace/presentation/home/menu/app/create_button.dart

@@ -30,7 +30,7 @@ class NewAppButton extends StatelessWidget {
   }
 
   Future<void> _showCreateAppDialog(BuildContext context) async {
-    return TextFieldDialog(
+    return NavigatorTextFieldDialog(
       title: LocaleKeys.newPageText.tr(),
       value: "",
       confirm: (newValue) {

+ 1 - 1
frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/header.dart

@@ -126,7 +126,7 @@ class MenuAppHeader extends StatelessWidget {
     action.fold(() {}, (action) {
       switch (action) {
         case AppDisclosureAction.rename:
-          TextFieldDialog(
+          NavigatorTextFieldDialog(
             title: LocaleKeys.menuAppHeader_renameDialog.tr(),
             value: context.read<AppBloc>().state.app.name,
             confirm: (newValue) {

+ 1 - 1
frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/item.dart

@@ -109,7 +109,7 @@ class ViewSectionItem extends StatelessWidget {
     action.foldRight({}, (action, previous) {
       switch (action) {
         case ViewDisclosureAction.rename:
-          TextFieldDialog(
+          NavigatorTextFieldDialog(
             title: LocaleKeys.disclosureAction_rename.tr(),
             value: context.read<ViewBloc>().state.view.name,
             confirm: (newValue) {

+ 77 - 27
frontend/app_flowy/lib/workspace/presentation/widgets/dialogs.dart

@@ -1,3 +1,4 @@
+import 'package:appflowy_popover/popover.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra/text_style.dart';
 import 'package:flowy_infra/theme.dart';
@@ -15,13 +16,13 @@ import 'package:textstyle_extensions/textstyle_extensions.dart';
 export 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart';
 import 'package:app_flowy/generated/locale_keys.g.dart';
 
-class TextFieldDialog extends StatefulWidget {
+class NavigatorTextFieldDialog extends StatefulWidget {
   final String value;
   final String title;
   final void Function()? cancel;
   final void Function(String) confirm;
 
-  const TextFieldDialog({
+  const NavigatorTextFieldDialog({
     required this.title,
     required this.value,
     required this.confirm,
@@ -30,10 +31,10 @@ class TextFieldDialog extends StatefulWidget {
   }) : super(key: key);
 
   @override
-  State<TextFieldDialog> createState() => _CreateTextFieldDialog();
+  State<NavigatorTextFieldDialog> createState() => _CreateTextFieldDialog();
 }
 
-class _CreateTextFieldDialog extends State<TextFieldDialog> {
+class _CreateTextFieldDialog extends State<NavigatorTextFieldDialog> {
   String newValue = "";
 
   @override
@@ -56,7 +57,8 @@ class _CreateTextFieldDialog extends State<TextFieldDialog> {
           FlowyFormTextInput(
             hintText: LocaleKeys.dialogCreatePageNameHint.tr(),
             initialValue: widget.value,
-            textStyle: const TextStyle(fontSize: 24, fontWeight: FontWeight.w400),
+            textStyle:
+                const TextStyle(fontSize: 24, fontWeight: FontWeight.w400),
             autoFocus: true,
             onChanged: (text) {
               newValue = text;
@@ -70,11 +72,13 @@ class _CreateTextFieldDialog extends State<TextFieldDialog> {
           OkCancelButton(
             onOkPressed: () {
               widget.confirm(newValue);
+              Navigator.of(context).pop();
             },
             onCancelPressed: () {
               if (widget.cancel != null) {
                 widget.cancel!();
               }
+              Navigator.of(context).pop();
             },
           )
         ],
@@ -83,12 +87,14 @@ class _CreateTextFieldDialog extends State<TextFieldDialog> {
   }
 }
 
-class FlowyAlertDialog extends StatefulWidget {
+class PopoverAlertView extends StatelessWidget {
+  final PopoverMutex popoverMutex;
   final String title;
   final void Function()? cancel;
   final void Function()? confirm;
 
-  const FlowyAlertDialog({
+  const PopoverAlertView({
+    required this.popoverMutex,
     required this.title,
     this.confirm,
     this.cancel,
@@ -96,10 +102,46 @@ class FlowyAlertDialog extends StatefulWidget {
   }) : super(key: key);
 
   @override
-  State<FlowyAlertDialog> createState() => _CreateFlowyAlertDialog();
+  Widget build(BuildContext context) {
+    final theme = context.watch<AppTheme>();
+    return StyledDialog(
+      child: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        mainAxisAlignment: MainAxisAlignment.center,
+        children: <Widget>[
+          ...[
+            FlowyText.medium(title, color: theme.shader4),
+          ],
+          if (confirm != null) ...[
+            const VSpace(20),
+            OkCancelButton(
+              onOkPressed: confirm,
+              onCancelPressed: cancel,
+            )
+          ]
+        ],
+      ),
+    );
+  }
 }
 
-class _CreateFlowyAlertDialog extends State<FlowyAlertDialog> {
+class NavigatorAlertDialog extends StatefulWidget {
+  final String title;
+  final void Function()? cancel;
+  final void Function()? confirm;
+
+  const NavigatorAlertDialog({
+    required this.title,
+    this.confirm,
+    this.cancel,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  State<NavigatorAlertDialog> createState() => _CreateFlowyAlertDialog();
+}
+
+class _CreateFlowyAlertDialog extends State<NavigatorAlertDialog> {
   @override
   void initState() {
     super.initState();
@@ -118,10 +160,13 @@ class _CreateFlowyAlertDialog extends State<FlowyAlertDialog> {
           ],
           if (widget.confirm != null) ...[
             const VSpace(20),
-            OkCancelButton(
-              onOkPressed: widget.confirm!,
-              onCancelPressed: widget.confirm,
-            )
+            OkCancelButton(onOkPressed: () {
+              widget.confirm?.call();
+              Navigator.of(context).pop();
+            }, onCancelPressed: () {
+              widget.cancel?.call();
+              Navigator.of(context).pop();
+            })
           ]
         ],
       ),
@@ -129,7 +174,7 @@ class _CreateFlowyAlertDialog extends State<FlowyAlertDialog> {
   }
 }
 
-class OkCancelDialog extends StatelessWidget {
+class NavigatorOkCancelDialog extends StatelessWidget {
   final VoidCallback? onOkPressed;
   final VoidCallback? onCancelPressed;
   final String? okTitle;
@@ -138,7 +183,7 @@ class OkCancelDialog extends StatelessWidget {
   final String message;
   final double? maxWidth;
 
-  const OkCancelDialog(
+  const NavigatorOkCancelDialog(
       {Key? key,
       this.onOkPressed,
       this.onCancelPressed,
@@ -158,7 +203,7 @@ class OkCancelDialog extends StatelessWidget {
         crossAxisAlignment: CrossAxisAlignment.start,
         children: <Widget>[
           if (title != null) ...[
-            Text(title!.toUpperCase(), style: TextStyles.T1.textColor(theme.shader1)),
+            FlowyText.medium(title!.toUpperCase(), color: theme.shader1),
             VSpace(Insets.sm * 1.5),
             Container(color: theme.bg1, height: 1),
             VSpace(Insets.m * 1.5),
@@ -166,8 +211,14 @@ class OkCancelDialog extends StatelessWidget {
           Text(message, style: TextStyles.Body1.textHeight(1.5)),
           SizedBox(height: Insets.l),
           OkCancelButton(
-            onOkPressed: onOkPressed,
-            onCancelPressed: onCancelPressed,
+            onOkPressed: () {
+              onOkPressed?.call();
+              Navigator.of(context).pop();
+            },
+            onCancelPressed: () {
+              onCancelPressed?.call();
+              Navigator.of(context).pop();
+            },
             okTitle: okTitle?.toUpperCase(),
             cancelTitle: cancelTitle?.toUpperCase(),
           )
@@ -185,7 +236,12 @@ class OkCancelButton extends StatelessWidget {
   final double? minHeight;
 
   const OkCancelButton(
-      {Key? key, this.onOkPressed, this.onCancelPressed, this.okTitle, this.cancelTitle, this.minHeight})
+      {Key? key,
+      this.onOkPressed,
+      this.onCancelPressed,
+      this.okTitle,
+      this.cancelTitle,
+      this.minHeight})
       : super(key: key);
 
   @override
@@ -198,20 +254,14 @@ class OkCancelButton extends StatelessWidget {
           if (onCancelPressed != null)
             SecondaryTextButton(
               cancelTitle ?? LocaleKeys.button_Cancel.tr(),
-              onPressed: () {
-                onCancelPressed!();
-                AppGlobals.nav.pop();
-              },
+              onPressed: onCancelPressed,
               bigMode: true,
             ),
           HSpace(Insets.m),
           if (onOkPressed != null)
             PrimaryTextButton(
               okTitle ?? LocaleKeys.button_OK.tr(),
-              onPressed: () {
-                onOkPressed!();
-                AppGlobals.nav.pop();
-              },
+              onPressed: onOkPressed,
               bigMode: true,
             ),
         ],

+ 3 - 0
frontend/app_flowy/macos/Runner.xcodeproj/project.pbxproj

@@ -416,6 +416,7 @@
 			isa = XCBuildConfiguration;
 			baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
 			buildSettings = {
+				ARCHS = arm64;
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				CLANG_ENABLE_MODULES = YES;
 				CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
@@ -549,6 +550,7 @@
 			isa = XCBuildConfiguration;
 			baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
 			buildSettings = {
+				ARCHS = arm64;
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				CLANG_ENABLE_MODULES = YES;
 				CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
@@ -573,6 +575,7 @@
 			isa = XCBuildConfiguration;
 			baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
 			buildSettings = {
+				ARCHS = arm64;
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				CLANG_ENABLE_MODULES = YES;
 				CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;

+ 33 - 1
frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart

@@ -58,122 +58,153 @@ List<ShortcutEvent> builtInShortcutEvents = [
     key: 'Move cursor top',
     command: 'meta+arrow up',
     windowsCommand: 'ctrl+arrow up',
+    linuxCommand: 'ctrl+arrow up',
     handler: cursorTop,
   ),
   ShortcutEvent(
     key: 'Move cursor bottom',
     command: 'meta+arrow down',
     windowsCommand: 'ctrl+arrow down',
+    linuxCommand: 'ctrl+arrow down',
     handler: cursorBottom,
   ),
   ShortcutEvent(
     key: 'Move cursor begin',
     command: 'meta+arrow left',
     windowsCommand: 'ctrl+arrow left',
+    linuxCommand: 'ctrl+arrow left',
     handler: cursorBegin,
   ),
   ShortcutEvent(
     key: 'Move cursor end',
     command: 'meta+arrow right',
     windowsCommand: 'ctrl+arrow right',
+    linuxCommand: 'ctrl+arrow right',
     handler: cursorEnd,
   ),
   ShortcutEvent(
     key: 'Cursor top select',
     command: 'meta+shift+arrow up',
     windowsCommand: 'ctrl+shift+arrow up',
+    linuxCommand: 'ctrl+shift+arrow up',
     handler: cursorTopSelect,
   ),
   ShortcutEvent(
     key: 'Cursor bottom select',
     command: 'meta+shift+arrow down',
     windowsCommand: 'ctrl+shift+arrow down',
+    linuxCommand: 'ctrl+shift+arrow down',
     handler: cursorBottomSelect,
   ),
   ShortcutEvent(
     key: 'Cursor begin select',
     command: 'meta+shift+arrow left',
     windowsCommand: 'ctrl+shift+arrow left',
+    linuxCommand: 'ctrl+shift+arrow left',
     handler: cursorBeginSelect,
   ),
   ShortcutEvent(
     key: 'Cursor end select',
     command: 'meta+shift+arrow right',
     windowsCommand: 'ctrl+shift+arrow right',
+    linuxCommand: 'ctrl+shift+arrow right',
     handler: cursorEndSelect,
   ),
   ShortcutEvent(
     key: 'Redo',
     command: 'meta+shift+z',
     windowsCommand: 'ctrl+shift+z',
+    linuxCommand: 'ctrl+shift+z',
     handler: redoEventHandler,
   ),
   ShortcutEvent(
     key: 'Undo',
     command: 'meta+z',
     windowsCommand: 'ctrl+z',
+    linuxCommand: 'ctrl+z',
     handler: undoEventHandler,
   ),
   ShortcutEvent(
     key: 'Format bold',
     command: 'meta+b',
     windowsCommand: 'ctrl+b',
+    linuxCommand: 'ctrl+b',
     handler: formatBoldEventHandler,
   ),
   ShortcutEvent(
     key: 'Format italic',
     command: 'meta+i',
     windowsCommand: 'ctrl+i',
+    linuxCommand: 'ctrl+i',
     handler: formatItalicEventHandler,
   ),
   ShortcutEvent(
     key: 'Format underline',
     command: 'meta+u',
     windowsCommand: 'ctrl+u',
+    linuxCommand: 'ctrl+u',
     handler: formatUnderlineEventHandler,
   ),
   ShortcutEvent(
     key: 'Format strikethrough',
     command: 'meta+shift+s',
     windowsCommand: 'ctrl+shift+s',
+    linuxCommand: 'ctrl+shift+s',
     handler: formatStrikethroughEventHandler,
   ),
   ShortcutEvent(
     key: 'Format highlight',
     command: 'meta+shift+h',
     windowsCommand: 'ctrl+shift+h',
+    linuxCommand: 'ctrl+shift+h',
     handler: formatHighlightEventHandler,
   ),
   ShortcutEvent(
     key: 'Format embed code',
     command: 'meta+e',
     windowsCommand: 'ctrl+e',
+    linuxCommand: 'ctrl+e',
     handler: formatEmbedCodeEventHandler,
   ),
   ShortcutEvent(
     key: 'Format link',
     command: 'meta+k',
     windowsCommand: 'ctrl+k',
+    linuxCommand: 'ctrl+k',
     handler: formatLinkEventHandler,
   ),
   ShortcutEvent(
     key: 'Copy',
     command: 'meta+c',
     windowsCommand: 'ctrl+c',
+    linuxCommand: 'ctrl+c',
     handler: copyEventHandler,
   ),
   ShortcutEvent(
     key: 'Paste',
     command: 'meta+v',
     windowsCommand: 'ctrl+v',
+    linuxCommand: 'ctrl+v',
     handler: pasteEventHandler,
   ),
   ShortcutEvent(
-    key: 'Paste',
+    key: 'Cut',
     command: 'meta+x',
     windowsCommand: 'ctrl+x',
+    linuxCommand: 'ctrl+x',
     handler: cutEventHandler,
   ),
+  ShortcutEvent(
+    key: 'Home',
+    command: 'home',
+    handler: cursorBegin,
+  ),
+  ShortcutEvent(
+    key: 'End',
+    command: 'end',
+    handler: cursorEnd,
+  ),
+
   // TODO: split the keys.
   ShortcutEvent(
     key: 'Delete Text',
@@ -199,6 +230,7 @@ List<ShortcutEvent> builtInShortcutEvents = [
     key: 'select all',
     command: 'meta+a',
     windowsCommand: 'ctrl+a',
+    linuxCommand: 'ctrl+a',
     handler: selectAllHandler,
   ),
   ShortcutEvent(

+ 6 - 6
frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/arrow_keys_handler_test.dart

@@ -287,7 +287,7 @@ void main() async {
       LogicalKeyboardKey.arrowDown,
       isShiftPressed: true,
     );
-    if (Platform.isWindows) {
+    if (Platform.isWindows || Platform.isLinux) {
       await editor.pressLogicKey(
         LogicalKeyboardKey.arrowRight,
         isShiftPressed: true,
@@ -321,7 +321,7 @@ void main() async {
       LogicalKeyboardKey.arrowUp,
       isShiftPressed: true,
     );
-    if (Platform.isWindows) {
+    if (Platform.isWindows || Platform.isLinux) {
       await editor.pressLogicKey(
         LogicalKeyboardKey.arrowLeft,
         isShiftPressed: true,
@@ -398,7 +398,7 @@ Future<void> _testPressArrowKeyWithMetaInSelection(
     }
   }
   await editor.updateSelection(selection);
-  if (Platform.isWindows) {
+  if (Platform.isWindows || Platform.isLinux) {
     await editor.pressLogicKey(
       LogicalKeyboardKey.arrowLeft,
       isControlPressed: true,
@@ -415,7 +415,7 @@ Future<void> _testPressArrowKeyWithMetaInSelection(
     Selection.single(path: [0], startOffset: 0),
   );
 
-  if (Platform.isWindows) {
+  if (Platform.isWindows || Platform.isLinux) {
     await editor.pressLogicKey(
       LogicalKeyboardKey.arrowRight,
       isControlPressed: true,
@@ -432,7 +432,7 @@ Future<void> _testPressArrowKeyWithMetaInSelection(
     Selection.single(path: [0], startOffset: text.length),
   );
 
-  if (Platform.isWindows) {
+  if (Platform.isWindows || Platform.isLinux) {
     await editor.pressLogicKey(
       LogicalKeyboardKey.arrowUp,
       isControlPressed: true,
@@ -449,7 +449,7 @@ Future<void> _testPressArrowKeyWithMetaInSelection(
     Selection.single(path: [0], startOffset: 0),
   );
 
-  if (Platform.isWindows) {
+  if (Platform.isWindows || Platform.isLinux) {
     await editor.pressLogicKey(
       LogicalKeyboardKey.arrowDown,
       isControlPressed: true,

+ 8 - 8
frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/format_style_handler_test.dart

@@ -94,7 +94,7 @@ Future<void> _testUpdateTextStyleByCommandX(
   var selection =
       Selection.single(path: [1], startOffset: 2, endOffset: text.length - 2);
   await editor.updateSelection(selection);
-  if (Platform.isWindows) {
+  if (Platform.isWindows || Platform.isLinux) {
     await editor.pressLogicKey(
       key,
       isShiftPressed: isShiftPressed,
@@ -121,7 +121,7 @@ Future<void> _testUpdateTextStyleByCommandX(
   selection =
       Selection.single(path: [1], startOffset: 0, endOffset: text.length);
   await editor.updateSelection(selection);
-  if (Platform.isWindows) {
+  if (Platform.isWindows || Platform.isLinux) {
     await editor.pressLogicKey(
       key,
       isShiftPressed: isShiftPressed,
@@ -146,7 +146,7 @@ Future<void> _testUpdateTextStyleByCommandX(
       true);
 
   await editor.updateSelection(selection);
-  if (Platform.isWindows) {
+  if (Platform.isWindows || Platform.isLinux) {
     await editor.pressLogicKey(
       key,
       isShiftPressed: isShiftPressed,
@@ -168,7 +168,7 @@ Future<void> _testUpdateTextStyleByCommandX(
     end: Position(path: [2], offset: text.length),
   );
   await editor.updateSelection(selection);
-  if (Platform.isWindows) {
+  if (Platform.isWindows || Platform.isLinux) {
     await editor.pressLogicKey(
       key,
       isShiftPressed: isShiftPressed,
@@ -203,7 +203,7 @@ Future<void> _testUpdateTextStyleByCommandX(
 
   await editor.updateSelection(selection);
 
-  if (Platform.isWindows) {
+  if (Platform.isWindows || Platform.isLinux) {
     await editor.pressLogicKey(
       key,
       isShiftPressed: isShiftPressed,
@@ -249,7 +249,7 @@ Future<void> _testLinkMenuInSingleTextSelection(WidgetTester tester) async {
   expect(find.byType(ToolbarWidget), findsOneWidget);
 
   // trigger the link menu
-  if (Platform.isWindows) {
+  if (Platform.isWindows || Platform.isLinux) {
     await editor.pressLogicKey(LogicalKeyboardKey.keyK, isControlPressed: true);
   } else {
     await editor.pressLogicKey(LogicalKeyboardKey.keyK, isMetaPressed: true);
@@ -272,7 +272,7 @@ Future<void> _testLinkMenuInSingleTextSelection(WidgetTester tester) async {
       true);
 
   await editor.updateSelection(selection);
-  if (Platform.isWindows) {
+  if (Platform.isWindows || Platform.isLinux) {
     await editor.pressLogicKey(LogicalKeyboardKey.keyK, isControlPressed: true);
   } else {
     await editor.pressLogicKey(LogicalKeyboardKey.keyK, isMetaPressed: true);
@@ -289,7 +289,7 @@ Future<void> _testLinkMenuInSingleTextSelection(WidgetTester tester) async {
   expect(find.byType(LinkMenu), findsNothing);
 
   // Remove link
-  if (Platform.isWindows) {
+  if (Platform.isWindows || Platform.isLinux) {
     await editor.pressLogicKey(LogicalKeyboardKey.keyK, isControlPressed: true);
   } else {
     await editor.pressLogicKey(LogicalKeyboardKey.keyK, isMetaPressed: true);

+ 2 - 2
frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/redo_undo_handler_test.dart

@@ -43,7 +43,7 @@ Future<void> _testBackspaceUndoRedo(
   await editor.pressLogicKey(LogicalKeyboardKey.backspace);
   expect(editor.documentLength, 2);
 
-  if (Platform.isWindows) {
+  if (Platform.isWindows || Platform.isLinux) {
     await editor.pressLogicKey(
       LogicalKeyboardKey.keyZ,
       isControlPressed: true,
@@ -59,7 +59,7 @@ Future<void> _testBackspaceUndoRedo(
   expect((editor.nodeAtPath([1]) as TextNode).toRawString(), text);
   expect(editor.documentSelection, selection);
 
-  if (Platform.isWindows) {
+  if (Platform.isWindows || Platform.isLinux) {
     await editor.pressLogicKey(
       LogicalKeyboardKey.keyZ,
       isControlPressed: true,

+ 1 - 1
frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/select_all_handler_test.dart

@@ -28,7 +28,7 @@ Future<void> _testSelectAllHandler(WidgetTester tester, int lines) async {
     editor.insertTextNode(text);
   }
   await editor.startTesting();
-  if (Platform.isWindows) {
+  if (Platform.isWindows || Platform.isLinux) {
     await editor.pressLogicKey(LogicalKeyboardKey.keyA, isControlPressed: true);
   } else {
     await editor.pressLogicKey(LogicalKeyboardKey.keyA, isMetaPressed: true);

+ 137 - 2
frontend/app_flowy/packages/appflowy_editor/test/service/shortcut_event/shortcut_event_test.dart

@@ -39,7 +39,7 @@ void main() async {
       await editor.updateSelection(
         Selection.single(path: [1], startOffset: text.length),
       );
-      if (Platform.isWindows) {
+      if (Platform.isWindows || Platform.isLinux) {
         await editor.pressLogicKey(
           LogicalKeyboardKey.arrowLeft,
           isControlPressed: true,
@@ -62,11 +62,12 @@ void main() async {
         if (event.key == 'Move cursor begin') {
           event.updateCommand(
             windowsCommand: 'alt+arrow left',
+            linuxCommand: 'alt+arrow left',
             macOSCommand: 'alt+arrow left',
           );
         }
       }
-      if (Platform.isWindows || Platform.isMacOS) {
+      if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) {
         await editor.pressLogicKey(
           LogicalKeyboardKey.arrowLeft,
           isAltPressed: true,
@@ -84,5 +85,139 @@ void main() async {
 
       tester.pumpAndSettle();
     });
+
+    testWidgets('redefine move cursor end command', (tester) async {
+      const text = 'Welcome to Appflowy 😁';
+      final editor = tester.editor
+        ..insertTextNode(text)
+        ..insertTextNode(text);
+      await editor.startTesting();
+      await editor.updateSelection(
+        Selection.single(path: [1], startOffset: 0),
+      );
+      if (Platform.isWindows || Platform.isLinux) {
+        await editor.pressLogicKey(
+          LogicalKeyboardKey.arrowRight,
+          isControlPressed: true,
+        );
+      } else {
+        await editor.pressLogicKey(
+          LogicalKeyboardKey.arrowRight,
+          isMetaPressed: true,
+        );
+      }
+      expect(
+        editor.documentSelection,
+        Selection.single(path: [1], startOffset: text.length),
+      );
+      await editor.updateSelection(
+        Selection.single(path: [1], startOffset: 0),
+      );
+
+      for (final event in builtInShortcutEvents) {
+        if (event.key == 'Move cursor end') {
+          event.updateCommand(
+            windowsCommand: 'alt+arrow right',
+            linuxCommand: 'alt+arrow right',
+            macOSCommand: 'alt+arrow right',
+          );
+        }
+      }
+      await editor.pressLogicKey(
+        LogicalKeyboardKey.arrowRight,
+        isAltPressed: true,
+      );
+      expect(
+        editor.documentSelection,
+        Selection.single(path: [1], startOffset: text.length),
+      );
+    });
+
+    testWidgets('Test Home Key to move to start of current text',
+        (tester) async {
+      const text = 'Welcome to Appflowy 😁';
+      final editor = tester.editor
+        ..insertTextNode(text)
+        ..insertTextNode(text);
+      await editor.startTesting();
+      await editor.updateSelection(
+        Selection.single(path: [1], startOffset: text.length),
+      );
+      if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
+        await editor.pressLogicKey(
+          LogicalKeyboardKey.home,
+        );
+      }
+
+      expect(
+        editor.documentSelection,
+        Selection.single(path: [1], startOffset: 0),
+      );
+      await editor.updateSelection(
+        Selection.single(path: [1], startOffset: text.length),
+      );
+
+      for (final event in builtInShortcutEvents) {
+        if (event.key == 'Move cursor begin') {
+          event.updateCommand(
+            windowsCommand: 'home',
+            linuxCommand: 'home',
+            macOSCommand: 'home',
+          );
+        }
+      }
+      if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) {
+        await editor.pressLogicKey(
+          LogicalKeyboardKey.home,
+        );
+      }
+      expect(
+        editor.documentSelection,
+        Selection.single(path: [1], startOffset: 0),
+      );
+    });
+
+    testWidgets('Test End Key to move to end of current text', (tester) async {
+      const text = 'Welcome to Appflowy 😁';
+      final editor = tester.editor
+        ..insertTextNode(text)
+        ..insertTextNode(text);
+      await editor.startTesting();
+      await editor.updateSelection(
+        Selection.single(path: [1], startOffset: text.length),
+      );
+      if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
+        await editor.pressLogicKey(
+          LogicalKeyboardKey.end,
+        );
+      }
+
+      expect(
+        editor.documentSelection,
+        Selection.single(path: [1], startOffset: text.length),
+      );
+      await editor.updateSelection(
+        Selection.single(path: [1], startOffset: 0),
+      );
+
+      for (final event in builtInShortcutEvents) {
+        if (event.key == 'Move cursor end') {
+          event.updateCommand(
+            windowsCommand: 'end',
+            linuxCommand: 'end',
+            macOSCommand: 'end',
+          );
+        }
+      }
+      if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) {
+        await editor.pressLogicKey(
+          LogicalKeyboardKey.end,
+        );
+      }
+      expect(
+        editor.documentSelection,
+        Selection.single(path: [1], startOffset: text.length),
+      );
+    });
   });
 }

+ 11 - 11
frontend/app_flowy/packages/appflowy_popover/lib/popover.dart

@@ -59,7 +59,7 @@ class Popover extends StatefulWidget {
   final Decoration? maskDecoration;
 
   /// The function used to build the popover.
-  final Widget Function(BuildContext context) popupBuilder;
+  final Widget? Function(BuildContext context) popupBuilder;
 
   final int triggerActions;
 
@@ -265,7 +265,7 @@ class _PopoverMaskState extends State<_PopoverMask> {
 }
 
 class PopoverContainer extends StatefulWidget {
-  final Widget Function(BuildContext context) popupBuilder;
+  final Widget? Function(BuildContext context) popupBuilder;
   final PopoverDirection direction;
   final PopoverLink popoverLink;
   final Offset offset;
@@ -284,6 +284,15 @@ class PopoverContainer extends StatefulWidget {
 
   @override
   State<StatefulWidget> createState() => PopoverContainerState();
+
+  static PopoverContainerState of(BuildContext context) {
+    if (context is StatefulElement && context.state is PopoverContainerState) {
+      return context.state as PopoverContainerState;
+    }
+    final PopoverContainerState? result =
+        context.findAncestorStateOfType<PopoverContainerState>();
+    return result!;
+  }
 }
 
 class PopoverContainerState extends State<PopoverContainer> {
@@ -302,13 +311,4 @@ class PopoverContainerState extends State<PopoverContainer> {
   close() => widget.onClose();
 
   closeAll() => widget.onCloseAll();
-
-  static PopoverContainerState of(BuildContext context) {
-    if (context is StatefulElement && context.state is PopoverContainerState) {
-      return context.state as PopoverContainerState;
-    }
-    final PopoverContainerState? result =
-        context.findAncestorStateOfType<PopoverContainerState>();
-    return result!;
-  }
 }

+ 1 - 1
frontend/app_flowy/packages/flowy_infra_ui/lib/flowy_infra_ui.dart

@@ -9,4 +9,4 @@ export 'src/flowy_overlay/flowy_overlay.dart';
 export 'src/flowy_overlay/list_overlay.dart';
 export 'src/flowy_overlay/option_overlay.dart';
 export 'src/flowy_overlay/flowy_dialog.dart';
-export 'src/flowy_overlay/flowy_popover.dart';
+export 'src/flowy_overlay/appflowy_stype_popover.dart';

+ 48 - 0
frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_stype_popover.dart

@@ -0,0 +1,48 @@
+import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
+import 'package:appflowy_popover/popover.dart';
+import 'package:flutter/material.dart';
+
+class AppFlowyStylePopover extends StatelessWidget {
+  final Widget child;
+  final PopoverController? controller;
+  final Widget Function(BuildContext context) popupBuilder;
+  final PopoverDirection direction;
+  final int triggerActions;
+  final BoxConstraints? constraints;
+  final void Function()? onClose;
+  final PopoverMutex? mutex;
+  final Offset? offset;
+
+  const AppFlowyStylePopover({
+    Key? key,
+    required this.child,
+    required this.popupBuilder,
+    this.direction = PopoverDirection.rightWithTopAligned,
+    this.onClose,
+    this.constraints,
+    this.mutex,
+    this.triggerActions = 0,
+    this.offset,
+    this.controller,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Popover(
+      controller: controller,
+      onClose: onClose,
+      direction: direction,
+      mutex: mutex,
+      triggerActions: triggerActions,
+      popupBuilder: (context) {
+        final child = popupBuilder(context);
+        debugPrint('$child popover');
+        return OverlayContainer(
+          constraints: constraints,
+          child: popupBuilder(context),
+        );
+      },
+      child: child,
+    );
+  }
+}

+ 0 - 59
frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_popover.dart

@@ -1,59 +0,0 @@
-import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
-import 'package:flowy_infra_ui/style_widget/decoration.dart';
-import 'package:flowy_infra/theme.dart';
-import 'package:provider/provider.dart';
-import 'package:flutter/material.dart';
-import './flowy_popover_layout.dart';
-
-const _overlayContainerPadding = EdgeInsets.all(12);
-
-class FlowyPopover extends StatefulWidget {
-  final Widget Function(BuildContext context) builder;
-  final ShapeBorder? shape;
-  final Rect anchorRect;
-  final AnchorDirection? anchorDirection;
-  final EdgeInsets padding;
-  final BoxConstraints? constraints;
-
-  const FlowyPopover({
-    Key? key,
-    required this.builder,
-    required this.anchorRect,
-    this.shape,
-    this.padding = _overlayContainerPadding,
-    this.anchorDirection,
-    this.constraints,
-  }) : super(key: key);
-
-  @override
-  State<FlowyPopover> createState() => _FlowyPopoverState();
-}
-
-class _FlowyPopoverState extends State<FlowyPopover> {
-  final preRenderKey = GlobalKey();
-  Size? size;
-
-  @override
-  Widget build(BuildContext context) {
-    final theme =
-        context.watch<AppTheme?>() ?? AppTheme.fromType(ThemeType.light);
-    return Material(
-        type: MaterialType.transparency,
-        child: CustomSingleChildLayout(
-            delegate: PopoverLayoutDelegate(
-              anchorRect: widget.anchorRect,
-              anchorDirection:
-                  widget.anchorDirection ?? AnchorDirection.rightWithTopAligned,
-              overlapBehaviour: OverlapBehaviour.stretch,
-            ),
-            child: Container(
-              padding: widget.padding,
-              constraints: widget.constraints ??
-                  BoxConstraints.loose(const Size(280, 400)),
-              decoration: FlowyDecoration.decoration(
-                  theme.surface, theme.shadowColor.withOpacity(0.15)),
-              key: preRenderKey,
-              child: widget.builder(context),
-            )));
-  }
-}

+ 18 - 8
frontend/app_flowy/packages/flowy_infra_ui/lib/widget/buttons/primary_button.dart

@@ -1,22 +1,30 @@
+import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flutter/material.dart';
 import 'package:provider/provider.dart';
 import 'package:flowy_infra/size.dart';
-import 'package:flowy_infra/text_style.dart';
 import 'package:flowy_infra/theme.dart';
 import 'base_styled_button.dart';
-import 'package:textstyle_extensions/textstyle_extensions.dart';
 
 class PrimaryTextButton extends StatelessWidget {
   final String label;
   final VoidCallback? onPressed;
   final bool bigMode;
 
-  const PrimaryTextButton(this.label, {Key? key, this.onPressed, this.bigMode = false}) : super(key: key);
+  const PrimaryTextButton(this.label,
+      {Key? key, this.onPressed, this.bigMode = false})
+      : super(key: key);
 
   @override
   Widget build(BuildContext context) {
-    TextStyle txtStyle = TextStyles.Btn.textColor(Colors.white);
-    return PrimaryButton(bigMode: bigMode, onPressed: onPressed, child: Text(label, style: txtStyle));
+    final theme = context.watch<AppTheme>();
+    return PrimaryButton(
+      bigMode: bigMode,
+      onPressed: onPressed,
+      child: FlowyText.regular(
+        label,
+        color: theme.surface,
+      ),
+    );
   }
 }
 
@@ -25,14 +33,16 @@ class PrimaryButton extends StatelessWidget {
   final VoidCallback? onPressed;
   final bool bigMode;
 
-  const PrimaryButton({Key? key, required this.child, this.onPressed, this.bigMode = false}) : super(key: key);
+  const PrimaryButton(
+      {Key? key, required this.child, this.onPressed, this.bigMode = false})
+      : super(key: key);
 
   @override
   Widget build(BuildContext context) {
     final theme = context.watch<AppTheme>();
     return BaseStyledButton(
-      minWidth: bigMode ? 170 : 78,
-      minHeight: bigMode ? 48 : 28,
+      minWidth: bigMode ? 100 : 80,
+      minHeight: bigMode ? 40 : 38,
       contentPadding: EdgeInsets.zero,
       bgColor: theme.main1,
       hoverColor: theme.main1,

+ 17 - 8
frontend/app_flowy/packages/flowy_infra_ui/lib/widget/buttons/secondary_button.dart

@@ -1,9 +1,8 @@
+import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flutter/material.dart';
 import 'package:provider/provider.dart';
 // ignore: import_of_legacy_library_into_null_safe
-import 'package:textstyle_extensions/textstyle_extensions.dart';
 import 'package:flowy_infra/size.dart';
-import 'package:flowy_infra/text_style.dart';
 import 'package:flowy_infra/theme.dart';
 import 'base_styled_button.dart';
 
@@ -12,13 +11,21 @@ class SecondaryTextButton extends StatelessWidget {
   final VoidCallback? onPressed;
   final bool bigMode;
 
-  const SecondaryTextButton(this.label, {Key? key, this.onPressed, this.bigMode = false}) : super(key: key);
+  const SecondaryTextButton(this.label,
+      {Key? key, this.onPressed, this.bigMode = false})
+      : super(key: key);
 
   @override
   Widget build(BuildContext context) {
     final theme = context.watch<AppTheme>();
-    TextStyle txtStyle = TextStyles.Btn.textColor(theme.main1);
-    return SecondaryButton(bigMode: bigMode, onPressed: onPressed, child: Text(label, style: txtStyle));
+    return SecondaryButton(
+      bigMode: bigMode,
+      onPressed: onPressed,
+      child: FlowyText.regular(
+        label,
+        color: theme.main1,
+      ),
+    );
   }
 }
 
@@ -27,14 +34,16 @@ class SecondaryButton extends StatelessWidget {
   final VoidCallback? onPressed;
   final bool bigMode;
 
-  const SecondaryButton({Key? key, required this.child, this.onPressed, this.bigMode = false}) : super(key: key);
+  const SecondaryButton(
+      {Key? key, required this.child, this.onPressed, this.bigMode = false})
+      : super(key: key);
 
   @override
   Widget build(BuildContext context) {
     final theme = context.watch<AppTheme>();
     return BaseStyledButton(
-      minWidth: bigMode ? 170 : 78,
-      minHeight: bigMode ? 48 : 28,
+      minWidth: bigMode ? 100 : 80,
+      minHeight: bigMode ? 40 : 38,
       contentPadding: EdgeInsets.zero,
       bgColor: theme.shader7,
       hoverColor: theme.hover,

+ 1 - 1
frontend/app_flowy/packages/flowy_infra_ui/lib/widget/dialog/dialog_size.dart

@@ -1,3 +1,3 @@
 class DialogSize {
-  static double get minDialogWidth => 480;
+  static double get minDialogWidth => 400;
 }

+ 4 - 2
frontend/app_flowy/packages/flowy_infra_ui/lib/widget/dialog/styled_dialogs.dart

@@ -65,7 +65,7 @@ class StyledDialog extends StatelessWidget {
 
     return FocusTraversalGroup(
       child: Container(
-        margin: margin ?? EdgeInsets.all(Insets.lGutter * 2),
+        margin: margin ?? EdgeInsets.all(Insets.sm * 2),
         alignment: Alignment.center,
         child: Container(
           constraints: BoxConstraints(
@@ -133,7 +133,9 @@ class StyledDialogRoute<T> extends PopupRoute<T> {
         super(settings: settings, filter: barrier.filter);
 
   @override
-  bool get barrierDismissible => barrier.dismissible;
+  bool get barrierDismissible {
+    return barrier.dismissible;
+  }
 
   @override
   String get barrierLabel => barrier.label;

+ 2 - 0
frontend/app_flowy/packages/flowy_infra_ui/pubspec.yaml

@@ -27,6 +27,8 @@ dependencies:
     path: flowy_infra_ui_platform_interface
   flowy_infra_ui_web:
     path: flowy_infra_ui_web
+  appflowy_popover:
+    path: ../appflowy_popover
 
   # Flowy packages
   flowy_infra:

+ 5 - 5
frontend/rust-lib/flowy-net/src/http_server/document.rs

@@ -4,7 +4,7 @@ use crate::{
 };
 use flowy_error::FlowyError;
 use flowy_sync::entities::text_block::{CreateTextBlockParams, DocumentPB, ResetTextBlockParams, TextBlockIdPB};
-use flowy_text_block::BlockCloudService;
+use flowy_text_block::TextEditorCloudService;
 use http_flowy::response::FlowyResponse;
 use lazy_static::lazy_static;
 use lib_infra::future::FutureResult;
@@ -20,20 +20,20 @@ impl BlockHttpCloudService {
     }
 }
 
-impl BlockCloudService for BlockHttpCloudService {
-    fn create_block(&self, token: &str, params: CreateTextBlockParams) -> FutureResult<(), FlowyError> {
+impl TextEditorCloudService for BlockHttpCloudService {
+    fn create_text_block(&self, token: &str, params: CreateTextBlockParams) -> FutureResult<(), FlowyError> {
         let token = token.to_owned();
         let url = self.config.doc_url();
         FutureResult::new(async move { create_document_request(&token, params, &url).await })
     }
 
-    fn read_block(&self, token: &str, params: TextBlockIdPB) -> FutureResult<Option<DocumentPB>, FlowyError> {
+    fn read_text_block(&self, token: &str, params: TextBlockIdPB) -> FutureResult<Option<DocumentPB>, FlowyError> {
         let token = token.to_owned();
         let url = self.config.doc_url();
         FutureResult::new(async move { read_document_request(&token, params, &url).await })
     }
 
-    fn update_block(&self, token: &str, params: ResetTextBlockParams) -> FutureResult<(), FlowyError> {
+    fn update_text_block(&self, token: &str, params: ResetTextBlockParams) -> FutureResult<(), FlowyError> {
         let token = token.to_owned();
         let url = self.config.doc_url();
         FutureResult::new(async move { reset_doc_request(&token, params, &url).await })

+ 5 - 5
frontend/rust-lib/flowy-net/src/local_server/server.rs

@@ -261,7 +261,7 @@ use flowy_folder::entities::{
 use flowy_folder_data_model::revision::{
     gen_app_id, gen_workspace_id, AppRevision, TrashRevision, ViewRevision, WorkspaceRevision,
 };
-use flowy_text_block::BlockCloudService;
+use flowy_text_block::TextEditorCloudService;
 use flowy_user::entities::{
     SignInParams, SignInResponse, SignUpParams, SignUpResponse, UpdateUserProfileParams, UserProfilePB,
 };
@@ -414,12 +414,12 @@ impl UserCloudService for LocalServer {
     }
 }
 
-impl BlockCloudService for LocalServer {
-    fn create_block(&self, _token: &str, _params: CreateTextBlockParams) -> FutureResult<(), FlowyError> {
+impl TextEditorCloudService for LocalServer {
+    fn create_text_block(&self, _token: &str, _params: CreateTextBlockParams) -> FutureResult<(), FlowyError> {
         FutureResult::new(async { Ok(()) })
     }
 
-    fn read_block(&self, _token: &str, params: TextBlockIdPB) -> FutureResult<Option<DocumentPB>, FlowyError> {
+    fn read_text_block(&self, _token: &str, params: TextBlockIdPB) -> FutureResult<Option<DocumentPB>, FlowyError> {
         let doc = DocumentPB {
             block_id: params.value,
             text: initial_quill_delta_string(),
@@ -429,7 +429,7 @@ impl BlockCloudService for LocalServer {
         FutureResult::new(async { Ok(Some(doc)) })
     }
 
-    fn update_block(&self, _token: &str, _params: ResetTextBlockParams) -> FutureResult<(), FlowyError> {
+    fn update_text_block(&self, _token: &str, _params: ResetTextBlockParams) -> FutureResult<(), FlowyError> {
         FutureResult::new(async { Ok(()) })
     }
 }

+ 9 - 9
frontend/rust-lib/flowy-sdk/src/deps_resolve/folder_deps.rs

@@ -18,7 +18,7 @@ use flowy_revision::{RevisionWebSocket, WSStateReceiver};
 use flowy_sync::client_document::default::initial_quill_delta_string;
 use flowy_sync::entities::revision::{RepeatedRevision, Revision};
 use flowy_sync::entities::ws_data::ClientRevisionWSData;
-use flowy_text_block::TextBlockManager;
+use flowy_text_block::TextEditorManager;
 use flowy_user::services::UserSession;
 use futures_core::future::BoxFuture;
 use lib_infra::future::{BoxResultFuture, FutureResult};
@@ -34,7 +34,7 @@ impl FolderDepsResolver {
         user_session: Arc<UserSession>,
         server_config: &ClientServerConfiguration,
         ws_conn: &Arc<FlowyWebSocketConnect>,
-        text_block_manager: &Arc<TextBlockManager>,
+        text_block_manager: &Arc<TextEditorManager>,
         grid_manager: &Arc<GridManager>,
     ) -> Arc<FolderManager> {
         let user: Arc<dyn WorkspaceUser> = Arc::new(WorkspaceUserImpl(user_session.clone()));
@@ -63,7 +63,7 @@ impl FolderDepsResolver {
 }
 
 fn make_view_data_processor(
-    text_block_manager: Arc<TextBlockManager>,
+    text_block_manager: Arc<TextEditorManager>,
     grid_manager: Arc<GridManager>,
 ) -> ViewDataProcessorMap {
     let mut map: HashMap<ViewDataTypePB, Arc<dyn ViewDataProcessor + Send + Sync>> = HashMap::new();
@@ -135,7 +135,7 @@ impl WSMessageReceiver for FolderWSMessageReceiverImpl {
     }
 }
 
-struct TextBlockViewDataProcessor(Arc<TextBlockManager>);
+struct TextBlockViewDataProcessor(Arc<TextEditorManager>);
 impl ViewDataProcessor for TextBlockViewDataProcessor {
     fn initialize(&self) -> FutureResult<(), FlowyError> {
         let manager = self.0.clone();
@@ -147,7 +147,7 @@ impl ViewDataProcessor for TextBlockViewDataProcessor {
         let view_id = view_id.to_string();
         let manager = self.0.clone();
         FutureResult::new(async move {
-            let _ = manager.create_block(view_id, repeated_revision).await?;
+            let _ = manager.create_text_block(view_id, repeated_revision).await?;
             Ok(())
         })
     }
@@ -156,7 +156,7 @@ impl ViewDataProcessor for TextBlockViewDataProcessor {
         let manager = self.0.clone();
         let view_id = view_id.to_string();
         FutureResult::new(async move {
-            let _ = manager.delete_block(view_id)?;
+            let _ = manager.close_text_editor(view_id)?;
             Ok(())
         })
     }
@@ -165,7 +165,7 @@ impl ViewDataProcessor for TextBlockViewDataProcessor {
         let manager = self.0.clone();
         let view_id = view_id.to_string();
         FutureResult::new(async move {
-            let _ = manager.close_block(view_id)?;
+            let _ = manager.close_text_editor(view_id)?;
             Ok(())
         })
     }
@@ -174,7 +174,7 @@ impl ViewDataProcessor for TextBlockViewDataProcessor {
         let view_id = view_id.to_string();
         let manager = self.0.clone();
         FutureResult::new(async move {
-            let editor = manager.open_block(view_id).await?;
+            let editor = manager.open_text_editor(view_id).await?;
             let delta_bytes = Bytes::from(editor.delta_str().await?);
             Ok(delta_bytes)
         })
@@ -195,7 +195,7 @@ impl ViewDataProcessor for TextBlockViewDataProcessor {
             let delta_data = Bytes::from(view_data);
             let repeated_revision: RepeatedRevision =
                 Revision::initial_revision(&user_id, &view_id, delta_data.clone()).into();
-            let _ = manager.create_block(view_id, repeated_revision).await?;
+            let _ = manager.create_text_block(view_id, repeated_revision).await?;
             Ok(delta_data)
         })
     }

+ 6 - 6
frontend/rust-lib/flowy-sdk/src/deps_resolve/text_block_deps.rs

@@ -8,7 +8,7 @@ use flowy_revision::{RevisionWebSocket, WSStateReceiver};
 use flowy_sync::entities::ws_data::ClientRevisionWSData;
 use flowy_text_block::{
     errors::{internal_error, FlowyError},
-    BlockCloudService, TextBlockManager, TextBlockUser,
+    TextEditorCloudService, TextEditorManager, TextEditorUser,
 };
 use flowy_user::services::UserSession;
 use futures_core::future::BoxFuture;
@@ -23,15 +23,15 @@ impl TextBlockDepsResolver {
         ws_conn: Arc<FlowyWebSocketConnect>,
         user_session: Arc<UserSession>,
         server_config: &ClientServerConfiguration,
-    ) -> Arc<TextBlockManager> {
+    ) -> Arc<TextEditorManager> {
         let user = Arc::new(BlockUserImpl(user_session));
         let rev_web_socket = Arc::new(TextBlockWebSocket(ws_conn.clone()));
-        let cloud_service: Arc<dyn BlockCloudService> = match local_server {
+        let cloud_service: Arc<dyn TextEditorCloudService> = match local_server {
             None => Arc::new(BlockHttpCloudService::new(server_config.clone())),
             Some(local_server) => local_server,
         };
 
-        let manager = Arc::new(TextBlockManager::new(cloud_service, user, rev_web_socket));
+        let manager = Arc::new(TextEditorManager::new(cloud_service, user, rev_web_socket));
         let receiver = Arc::new(DocumentWSMessageReceiverImpl(manager.clone()));
         ws_conn.add_ws_message_receiver(receiver).unwrap();
 
@@ -40,7 +40,7 @@ impl TextBlockDepsResolver {
 }
 
 struct BlockUserImpl(Arc<UserSession>);
-impl TextBlockUser for BlockUserImpl {
+impl TextEditorUser for BlockUserImpl {
     fn user_dir(&self) -> Result<String, FlowyError> {
         let dir = self.0.user_dir().map_err(|e| FlowyError::unauthorized().context(e))?;
 
@@ -90,7 +90,7 @@ impl RevisionWebSocket for TextBlockWebSocket {
     }
 }
 
-struct DocumentWSMessageReceiverImpl(Arc<TextBlockManager>);
+struct DocumentWSMessageReceiverImpl(Arc<TextEditorManager>);
 impl WSMessageReceiver for DocumentWSMessageReceiverImpl {
     fn source(&self) -> WSChannel {
         WSChannel::Document

+ 2 - 2
frontend/rust-lib/flowy-sdk/src/lib.rs

@@ -11,7 +11,7 @@ use flowy_net::{
     local_server::LocalServer,
     ws::connection::{listen_on_websocket, FlowyWebSocketConnect},
 };
-use flowy_text_block::TextBlockManager;
+use flowy_text_block::TextEditorManager;
 use flowy_user::services::{notifier::UserStatus, UserSession, UserSessionConfig};
 use lib_dispatch::prelude::*;
 use lib_dispatch::runtime::tokio_default_runtime;
@@ -89,7 +89,7 @@ pub struct FlowySDK {
     #[allow(dead_code)]
     config: FlowySDKConfig,
     pub user_session: Arc<UserSession>,
-    pub text_block_manager: Arc<TextBlockManager>,
+    pub text_block_manager: Arc<TextEditorManager>,
     pub folder_manager: Arc<FolderManager>,
     pub grid_manager: Arc<GridManager>,
     pub dispatcher: Arc<EventDispatcher>,

+ 3 - 3
frontend/rust-lib/flowy-sdk/src/module.rs

@@ -1,7 +1,7 @@
 use flowy_folder::manager::FolderManager;
 use flowy_grid::manager::GridManager;
 use flowy_net::ws::connection::FlowyWebSocketConnect;
-use flowy_text_block::TextBlockManager;
+use flowy_text_block::TextEditorManager;
 use flowy_user::services::UserSession;
 use lib_dispatch::prelude::Module;
 use std::sync::Arc;
@@ -11,7 +11,7 @@ pub fn mk_modules(
     folder_manager: &Arc<FolderManager>,
     grid_manager: &Arc<GridManager>,
     user_session: &Arc<UserSession>,
-    text_block_manager: &Arc<TextBlockManager>,
+    text_block_manager: &Arc<TextEditorManager>,
 ) -> Vec<Module> {
     let user_module = mk_user_module(user_session.clone());
     let folder_module = mk_folder_module(folder_manager.clone());
@@ -43,6 +43,6 @@ fn mk_grid_module(grid_manager: Arc<GridManager>) -> Module {
     flowy_grid::event_map::create(grid_manager)
 }
 
-fn mk_text_block_module(text_block_manager: Arc<TextBlockManager>) -> Module {
+fn mk_text_block_module(text_block_manager: Arc<TextEditorManager>) -> Module {
     flowy_text_block::event_map::create(text_block_manager)
 }

+ 3 - 4
frontend/rust-lib/flowy-text-block/src/editor.rs

@@ -2,7 +2,7 @@ use crate::web_socket::EditorCommandSender;
 use crate::{
     errors::FlowyError,
     queue::{EditBlockQueue, EditorCommand},
-    TextBlockUser,
+    TextEditorUser,
 };
 use bytes::Bytes;
 use flowy_error::{internal_error, FlowyResult};
@@ -24,7 +24,6 @@ use tokio::sync::{mpsc, oneshot};
 
 pub struct TextBlockEditor {
     pub doc_id: String,
-    #[allow(dead_code)]
     rev_manager: Arc<RevisionManager>,
     #[cfg(feature = "sync")]
     ws_manager: Arc<flowy_revision::RevisionWebSocketManager>,
@@ -35,7 +34,7 @@ impl TextBlockEditor {
     #[allow(unused_variables)]
     pub(crate) async fn new(
         doc_id: &str,
-        user: Arc<dyn TextBlockUser>,
+        user: Arc<dyn TextEditorUser>,
         mut rev_manager: RevisionManager,
         rev_web_socket: Arc<dyn RevisionWebSocket>,
         cloud_service: Arc<dyn RevisionCloudService>,
@@ -194,7 +193,7 @@ impl std::ops::Drop for TextBlockEditor {
 
 // The edit queue will exit after the EditorCommandSender was dropped.
 fn spawn_edit_queue(
-    user: Arc<dyn TextBlockUser>,
+    user: Arc<dyn TextEditorUser>,
     rev_manager: Arc<RevisionManager>,
     delta: TextDelta,
 ) -> EditorCommandSender {

+ 46 - 0
frontend/rust-lib/flowy-text-block/src/entities.rs

@@ -29,6 +29,52 @@ impl std::convert::From<i32> for ExportType {
     }
 }
 
+#[derive(Default, ProtoBuf)]
+pub struct EditPayloadPB {
+    #[pb(index = 1)]
+    pub text_block_id: String,
+
+    // Encode in JSON format
+    #[pb(index = 2)]
+    pub operations: String,
+
+    // Encode in JSON format
+    #[pb(index = 3)]
+    pub delta: String,
+}
+
+#[derive(Default)]
+pub struct EditParams {
+    pub text_block_id: String,
+
+    // Encode in JSON format
+    pub operations: String,
+
+    // Encode in JSON format
+    pub delta: String,
+}
+
+impl TryInto<EditParams> for EditPayloadPB {
+    type Error = ErrorCode;
+    fn try_into(self) -> Result<EditParams, Self::Error> {
+        Ok(EditParams {
+            text_block_id: self.text_block_id,
+            operations: self.operations,
+            delta: self.delta,
+        })
+    }
+}
+
+#[derive(Default, ProtoBuf)]
+pub struct TextBlockPB {
+    #[pb(index = 1)]
+    pub text_block_id: String,
+
+    /// Encode in JSON format
+    #[pb(index = 2)]
+    pub snapshot: String,
+}
+
 #[derive(Default, ProtoBuf)]
 pub struct ExportPayloadPB {
     #[pb(index = 1)]

+ 20 - 19
frontend/rust-lib/flowy-text-block/src/event_handler.rs

@@ -1,39 +1,40 @@
-use crate::entities::{ExportDataPB, ExportParams, ExportPayloadPB};
-use crate::TextBlockManager;
+use crate::entities::{EditParams, EditPayloadPB, ExportDataPB, ExportParams, ExportPayloadPB, TextBlockPB};
+use crate::TextEditorManager;
 use flowy_error::FlowyError;
-use flowy_sync::entities::text_block::{TextBlockDeltaPB, TextBlockIdPB};
+use flowy_sync::entities::text_block::TextBlockIdPB;
 use lib_dispatch::prelude::{data_result, AppData, Data, DataResult};
 use std::convert::TryInto;
 use std::sync::Arc;
 
-pub(crate) async fn get_block_data_handler(
+pub(crate) async fn get_text_block_handler(
     data: Data<TextBlockIdPB>,
-    manager: AppData<Arc<TextBlockManager>>,
-) -> DataResult<TextBlockDeltaPB, FlowyError> {
-    let block_id: TextBlockIdPB = data.into_inner();
-    let editor = manager.open_block(&block_id).await?;
+    manager: AppData<Arc<TextEditorManager>>,
+) -> DataResult<TextBlockPB, FlowyError> {
+    let text_block_id: TextBlockIdPB = data.into_inner();
+    let editor = manager.open_text_editor(&text_block_id).await?;
     let delta_str = editor.delta_str().await?;
-    data_result(TextBlockDeltaPB {
-        block_id: block_id.into(),
-        delta_str,
+    data_result(TextBlockPB {
+        text_block_id: text_block_id.into(),
+        snapshot: delta_str,
     })
 }
 
-pub(crate) async fn apply_delta_handler(
-    data: Data<TextBlockDeltaPB>,
-    manager: AppData<Arc<TextBlockManager>>,
-) -> DataResult<TextBlockDeltaPB, FlowyError> {
-    let block_delta = manager.receive_local_delta(data.into_inner()).await?;
-    data_result(block_delta)
+pub(crate) async fn apply_edit_handler(
+    data: Data<EditPayloadPB>,
+    manager: AppData<Arc<TextEditorManager>>,
+) -> Result<(), FlowyError> {
+    let params: EditParams = data.into_inner().try_into()?;
+    let _ = manager.apply_edit(params).await?;
+    Ok(())
 }
 
 #[tracing::instrument(level = "debug", skip(data, manager), err)]
 pub(crate) async fn export_handler(
     data: Data<ExportPayloadPB>,
-    manager: AppData<Arc<TextBlockManager>>,
+    manager: AppData<Arc<TextEditorManager>>,
 ) -> DataResult<ExportDataPB, FlowyError> {
     let params: ExportParams = data.into_inner().try_into()?;
-    let editor = manager.open_block(&params.view_id).await?;
+    let editor = manager.open_text_editor(&params.view_id).await?;
     let delta_json = editor.delta_str().await?;
     data_result(ExportDataPB {
         data: delta_json,

+ 8 - 8
frontend/rust-lib/flowy-text-block/src/event_map.rs

@@ -1,16 +1,16 @@
 use crate::event_handler::*;
-use crate::TextBlockManager;
+use crate::TextEditorManager;
 use flowy_derive::{Flowy_Event, ProtoBuf_Enum};
 use lib_dispatch::prelude::Module;
 use std::sync::Arc;
 use strum_macros::Display;
 
-pub fn create(block_manager: Arc<TextBlockManager>) -> Module {
+pub fn create(block_manager: Arc<TextEditorManager>) -> Module {
     let mut module = Module::new().name(env!("CARGO_PKG_NAME")).data(block_manager);
 
     module = module
-        .event(TextBlockEvent::GetBlockData, get_block_data_handler)
-        .event(TextBlockEvent::ApplyDelta, apply_delta_handler)
+        .event(TextBlockEvent::GetTextBlock, get_text_block_handler)
+        .event(TextBlockEvent::ApplyEdit, apply_edit_handler)
         .event(TextBlockEvent::ExportDocument, export_handler);
 
     module
@@ -19,11 +19,11 @@ pub fn create(block_manager: Arc<TextBlockManager>) -> Module {
 #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)]
 #[event_err = "FlowyError"]
 pub enum TextBlockEvent {
-    #[event(input = "TextBlockIdPB", output = "TextBlockDeltaPB")]
-    GetBlockData = 0,
+    #[event(input = "TextBlockIdPB", output = "TextBlockPB")]
+    GetTextBlock = 0,
 
-    #[event(input = "TextBlockDeltaPB", output = "TextBlockDeltaPB")]
-    ApplyDelta = 1,
+    #[event(input = "EditPayloadPB")]
+    ApplyEdit = 1,
 
     #[event(input = "ExportPayloadPB", output = "ExportDataPB")]
     ExportDocument = 2,

+ 4 - 4
frontend/rust-lib/flowy-text-block/src/lib.rs

@@ -18,10 +18,10 @@ use crate::errors::FlowyError;
 use flowy_sync::entities::text_block::{CreateTextBlockParams, DocumentPB, ResetTextBlockParams, TextBlockIdPB};
 use lib_infra::future::FutureResult;
 
-pub trait BlockCloudService: Send + Sync {
-    fn create_block(&self, token: &str, params: CreateTextBlockParams) -> FutureResult<(), FlowyError>;
+pub trait TextEditorCloudService: Send + Sync {
+    fn create_text_block(&self, token: &str, params: CreateTextBlockParams) -> FutureResult<(), FlowyError>;
 
-    fn read_block(&self, token: &str, params: TextBlockIdPB) -> FutureResult<Option<DocumentPB>, FlowyError>;
+    fn read_text_block(&self, token: &str, params: TextBlockIdPB) -> FutureResult<Option<DocumentPB>, FlowyError>;
 
-    fn update_block(&self, token: &str, params: ResetTextBlockParams) -> FutureResult<(), FlowyError>;
+    fn update_text_block(&self, token: &str, params: ResetTextBlockParams) -> FutureResult<(), FlowyError>;
 }

+ 63 - 56
frontend/rust-lib/flowy-text-block/src/manager.rs

@@ -1,5 +1,6 @@
+use crate::entities::EditParams;
 use crate::queue::TextBlockRevisionCompactor;
-use crate::{editor::TextBlockEditor, errors::FlowyError, BlockCloudService};
+use crate::{editor::TextBlockEditor, errors::FlowyError, TextEditorCloudService};
 use bytes::Bytes;
 use dashmap::DashMap;
 use flowy_database::ConnectionPool;
@@ -16,30 +17,30 @@ use flowy_sync::entities::{
 use lib_infra::future::FutureResult;
 use std::{convert::TryInto, sync::Arc};
 
-pub trait TextBlockUser: Send + Sync {
+pub trait TextEditorUser: Send + Sync {
     fn user_dir(&self) -> Result<String, FlowyError>;
     fn user_id(&self) -> Result<String, FlowyError>;
     fn token(&self) -> Result<String, FlowyError>;
     fn db_pool(&self) -> Result<Arc<ConnectionPool>, FlowyError>;
 }
 
-pub struct TextBlockManager {
-    cloud_service: Arc<dyn BlockCloudService>,
+pub struct TextEditorManager {
+    cloud_service: Arc<dyn TextEditorCloudService>,
     rev_web_socket: Arc<dyn RevisionWebSocket>,
-    editor_map: Arc<TextBlockEditorMap>,
-    user: Arc<dyn TextBlockUser>,
+    editor_map: Arc<TextEditorMap>,
+    user: Arc<dyn TextEditorUser>,
 }
 
-impl TextBlockManager {
+impl TextEditorManager {
     pub fn new(
-        cloud_service: Arc<dyn BlockCloudService>,
-        text_block_user: Arc<dyn TextBlockUser>,
+        cloud_service: Arc<dyn TextEditorCloudService>,
+        text_block_user: Arc<dyn TextEditorUser>,
         rev_web_socket: Arc<dyn RevisionWebSocket>,
     ) -> Self {
         Self {
             cloud_service,
             rev_web_socket,
-            editor_map: Arc::new(TextBlockEditorMap::new()),
+            editor_map: Arc::new(TextEditorMap::new()),
             user: text_block_user,
         }
     }
@@ -50,45 +51,47 @@ impl TextBlockManager {
         Ok(())
     }
 
-    #[tracing::instrument(level = "trace", skip(self, block_id), fields(block_id), err)]
-    pub async fn open_block<T: AsRef<str>>(&self, block_id: T) -> Result<Arc<TextBlockEditor>, FlowyError> {
-        let block_id = block_id.as_ref();
-        tracing::Span::current().record("block_id", &block_id);
-        self.get_block_editor(block_id).await
+    #[tracing::instrument(level = "trace", skip(self, editor_id), fields(editor_id), err)]
+    pub async fn open_text_editor<T: AsRef<str>>(&self, editor_id: T) -> Result<Arc<TextBlockEditor>, FlowyError> {
+        let editor_id = editor_id.as_ref();
+        tracing::Span::current().record("editor_id", &editor_id);
+        self.get_text_editor(editor_id).await
     }
 
-    #[tracing::instrument(level = "trace", skip(self, block_id), fields(block_id), err)]
-    pub fn close_block<T: AsRef<str>>(&self, block_id: T) -> Result<(), FlowyError> {
-        let block_id = block_id.as_ref();
-        tracing::Span::current().record("block_id", &block_id);
-        self.editor_map.remove(block_id);
+    #[tracing::instrument(level = "trace", skip(self, editor_id), fields(editor_id), err)]
+    pub fn close_text_editor<T: AsRef<str>>(&self, editor_id: T) -> Result<(), FlowyError> {
+        let editor_id = editor_id.as_ref();
+        tracing::Span::current().record("editor_id", &editor_id);
+        self.editor_map.remove(editor_id);
         Ok(())
     }
 
-    #[tracing::instrument(level = "debug", skip(self, doc_id), fields(doc_id), err)]
-    pub fn delete_block<T: AsRef<str>>(&self, doc_id: T) -> Result<(), FlowyError> {
-        let doc_id = doc_id.as_ref();
-        tracing::Span::current().record("doc_id", &doc_id);
-        self.editor_map.remove(doc_id);
-        Ok(())
-    }
-
-    #[tracing::instrument(level = "debug", skip(self, delta), fields(doc_id = %delta.block_id), err)]
+    #[tracing::instrument(level = "debug", skip(self, delta), err)]
     pub async fn receive_local_delta(&self, delta: TextBlockDeltaPB) -> Result<TextBlockDeltaPB, FlowyError> {
-        let editor = self.get_block_editor(&delta.block_id).await?;
+        let editor = self.get_text_editor(&delta.text_block_id).await?;
         let _ = editor.compose_local_delta(Bytes::from(delta.delta_str)).await?;
-        let document_json = editor.delta_str().await?;
+        let delta_str = editor.delta_str().await?;
         Ok(TextBlockDeltaPB {
-            block_id: delta.block_id.clone(),
-            delta_str: document_json,
+            text_block_id: delta.text_block_id.clone(),
+            delta_str,
         })
     }
 
-    pub async fn create_block<T: AsRef<str>>(&self, doc_id: T, revisions: RepeatedRevision) -> FlowyResult<()> {
-        let doc_id = doc_id.as_ref().to_owned();
+    pub async fn apply_edit(&self, params: EditParams) -> FlowyResult<()> {
+        let editor = self.get_text_editor(&params.text_block_id).await?;
+        let _ = editor.compose_local_delta(Bytes::from(params.delta)).await?;
+        Ok(())
+    }
+
+    pub async fn create_text_block<T: AsRef<str>>(
+        &self,
+        text_block_id: T,
+        revisions: RepeatedRevision,
+    ) -> FlowyResult<()> {
+        let doc_id = text_block_id.as_ref().to_owned();
         let db_pool = self.user.db_pool()?;
         // Maybe we could save the block to disk without creating the RevisionManager
-        let rev_manager = self.make_rev_manager(&doc_id, db_pool)?;
+        let rev_manager = self.make_text_block_rev_manager(&doc_id, db_pool)?;
         let _ = rev_manager.reset_object(revisions).await?;
         Ok(())
     }
@@ -110,26 +113,26 @@ impl TextBlockManager {
     }
 }
 
-impl TextBlockManager {
-    async fn get_block_editor(&self, block_id: &str) -> FlowyResult<Arc<TextBlockEditor>> {
+impl TextEditorManager {
+    async fn get_text_editor(&self, block_id: &str) -> FlowyResult<Arc<TextBlockEditor>> {
         match self.editor_map.get(block_id) {
             None => {
                 let db_pool = self.user.db_pool()?;
-                self.make_text_block_editor(block_id, db_pool).await
+                self.make_text_editor(block_id, db_pool).await
             }
             Some(editor) => Ok(editor),
         }
     }
 
     #[tracing::instrument(level = "trace", skip(self, pool), err)]
-    async fn make_text_block_editor(
+    async fn make_text_editor(
         &self,
         block_id: &str,
         pool: Arc<ConnectionPool>,
     ) -> Result<Arc<TextBlockEditor>, FlowyError> {
         let user = self.user.clone();
         let token = self.user.token()?;
-        let rev_manager = self.make_rev_manager(block_id, pool.clone())?;
+        let rev_manager = self.make_text_block_rev_manager(block_id, pool.clone())?;
         let cloud_service = Arc::new(TextBlockRevisionCloudService {
             token,
             server: self.cloud_service.clone(),
@@ -140,7 +143,11 @@ impl TextBlockManager {
         Ok(doc_editor)
     }
 
-    fn make_rev_manager(&self, doc_id: &str, pool: Arc<ConnectionPool>) -> Result<RevisionManager, FlowyError> {
+    fn make_text_block_rev_manager(
+        &self,
+        doc_id: &str,
+        pool: Arc<ConnectionPool>,
+    ) -> Result<RevisionManager, FlowyError> {
         let user_id = self.user.user_id()?;
         let disk_cache = SQLiteTextBlockRevisionPersistence::new(&user_id, pool.clone());
         let rev_persistence = RevisionPersistence::new(&user_id, doc_id, disk_cache);
@@ -161,7 +168,7 @@ impl TextBlockManager {
 
 struct TextBlockRevisionCloudService {
     token: String,
-    server: Arc<dyn BlockCloudService>,
+    server: Arc<dyn TextEditorCloudService>,
 }
 
 impl RevisionCloudService for TextBlockRevisionCloudService {
@@ -173,7 +180,7 @@ impl RevisionCloudService for TextBlockRevisionCloudService {
         let user_id = user_id.to_string();
 
         FutureResult::new(async move {
-            match server.read_block(&token, params).await? {
+            match server.read_text_block(&token, params).await? {
                 None => Err(FlowyError::record_not_found().context("Remote doesn't have this document")),
                 Some(doc) => {
                     let delta_data = Bytes::from(doc.text.clone());
@@ -193,36 +200,36 @@ impl RevisionCloudService for TextBlockRevisionCloudService {
     }
 }
 
-pub struct TextBlockEditorMap {
+pub struct TextEditorMap {
     inner: DashMap<String, Arc<TextBlockEditor>>,
 }
 
-impl TextBlockEditorMap {
+impl TextEditorMap {
     fn new() -> Self {
         Self { inner: DashMap::new() }
     }
 
-    pub(crate) fn insert(&self, block_id: &str, doc: &Arc<TextBlockEditor>) {
-        if self.inner.contains_key(block_id) {
-            log::warn!("Doc:{} already exists in cache", block_id);
+    pub(crate) fn insert(&self, editor_id: &str, doc: &Arc<TextBlockEditor>) {
+        if self.inner.contains_key(editor_id) {
+            log::warn!("Doc:{} already exists in cache", editor_id);
         }
-        self.inner.insert(block_id.to_string(), doc.clone());
+        self.inner.insert(editor_id.to_string(), doc.clone());
     }
 
-    pub(crate) fn get(&self, block_id: &str) -> Option<Arc<TextBlockEditor>> {
-        Some(self.inner.get(block_id)?.clone())
+    pub(crate) fn get(&self, editor_id: &str) -> Option<Arc<TextBlockEditor>> {
+        Some(self.inner.get(editor_id)?.clone())
     }
 
-    pub(crate) fn remove(&self, block_id: &str) {
-        if let Some(editor) = self.get(block_id) {
+    pub(crate) fn remove(&self, editor_id: &str) {
+        if let Some(editor) = self.get(editor_id) {
             editor.stop()
         }
-        self.inner.remove(block_id);
+        self.inner.remove(editor_id);
     }
 }
 
 #[tracing::instrument(level = "trace", skip(web_socket, handlers))]
-fn listen_ws_state_changed(web_socket: Arc<dyn RevisionWebSocket>, handlers: Arc<TextBlockEditorMap>) {
+fn listen_ws_state_changed(web_socket: Arc<dyn RevisionWebSocket>, handlers: Arc<TextEditorMap>) {
     tokio::spawn(async move {
         let mut notify = web_socket.subscribe_state_changed().await;
         while let Ok(state) = notify.recv().await {

+ 3 - 3
frontend/rust-lib/flowy-text-block/src/queue.rs

@@ -1,5 +1,5 @@
 use crate::web_socket::EditorCommandReceiver;
-use crate::TextBlockUser;
+use crate::TextEditorUser;
 use async_stream::stream;
 use bytes::Bytes;
 use flowy_error::{FlowyError, FlowyResult};
@@ -23,14 +23,14 @@ use tokio::sync::{oneshot, RwLock};
 // serial.
 pub(crate) struct EditBlockQueue {
     document: Arc<RwLock<ClientDocument>>,
-    user: Arc<dyn TextBlockUser>,
+    user: Arc<dyn TextEditorUser>,
     rev_manager: Arc<RevisionManager>,
     receiver: Option<EditorCommandReceiver>,
 }
 
 impl EditBlockQueue {
     pub(crate) fn new(
-        user: Arc<dyn TextBlockUser>,
+        user: Arc<dyn TextEditorUser>,
         rev_manager: Arc<RevisionManager>,
         delta: TextDelta,
         receiver: EditorCommandReceiver,

+ 1 - 1
frontend/rust-lib/flowy-text-block/tests/document/script.rs

@@ -27,7 +27,7 @@ impl TextBlockEditorTest {
         let sdk = FlowySDKTest::default();
         let _ = sdk.init_user().await;
         let test = ViewTest::new_text_block_view(&sdk).await;
-        let editor = sdk.text_block_manager.open_block(&test.view.id).await.unwrap();
+        let editor = sdk.text_block_manager.open_text_editor(&test.view.id).await.unwrap();
         Self { sdk, editor }
     }
 

+ 1 - 1
shared-lib/flowy-sync/src/entities/text_block.rs

@@ -69,7 +69,7 @@ pub struct ResetTextBlockParams {
 #[derive(ProtoBuf, Default, Debug, Clone)]
 pub struct TextBlockDeltaPB {
     #[pb(index = 1)]
-    pub block_id: String,
+    pub text_block_id: String,
 
     #[pb(index = 2)]
     pub delta_str: String,

+ 1 - 1
shared-lib/lib-ot/Cargo.toml

@@ -7,7 +7,7 @@ edition = "2018"
 
 [dependencies]
 bytecount = "0.6.0"
-serde = { version = "1.0", features = ["derive"] }
+serde = { version = "1.0", features = ["derive", "rc"] }
 #protobuf = {version = "2.18.0"}
 #flowy-derive = { path = "../flowy-derive" }
 tokio = { version = "1", features = ["sync"] }

+ 0 - 143
shared-lib/lib-ot/src/core/document/operation.rs

@@ -1,143 +0,0 @@
-use crate::core::attributes::Attributes;
-use crate::core::document::path::Path;
-use crate::core::{NodeBodyChangeset, NodeData};
-use crate::errors::OTError;
-use serde::{Deserialize, Serialize};
-
-#[derive(Clone, Serialize, Deserialize)]
-#[serde(tag = "op")]
-pub enum NodeOperation {
-    #[serde(rename = "insert")]
-    Insert { path: Path, nodes: Vec<NodeData> },
-
-    #[serde(rename = "update")]
-    UpdateAttributes {
-        path: Path,
-        attributes: Attributes,
-        #[serde(rename = "oldAttributes")]
-        old_attributes: Attributes,
-    },
-
-    #[serde(rename = "update-body")]
-    // #[serde(serialize_with = "serialize_edit_body")]
-    // #[serde(deserialize_with = "deserialize_edit_body")]
-    UpdateBody { path: Path, changeset: NodeBodyChangeset },
-
-    #[serde(rename = "delete")]
-    Delete { path: Path, nodes: Vec<NodeData> },
-}
-
-impl NodeOperation {
-    pub fn path(&self) -> &Path {
-        match self {
-            NodeOperation::Insert { path, .. } => path,
-            NodeOperation::UpdateAttributes { path, .. } => path,
-            NodeOperation::Delete { path, .. } => path,
-            NodeOperation::UpdateBody { path, .. } => path,
-        }
-    }
-    pub fn invert(&self) -> NodeOperation {
-        match self {
-            NodeOperation::Insert { path, nodes } => NodeOperation::Delete {
-                path: path.clone(),
-                nodes: nodes.clone(),
-            },
-            NodeOperation::UpdateAttributes {
-                path,
-                attributes,
-                old_attributes,
-            } => NodeOperation::UpdateAttributes {
-                path: path.clone(),
-                attributes: old_attributes.clone(),
-                old_attributes: attributes.clone(),
-            },
-            NodeOperation::Delete { path, nodes } => NodeOperation::Insert {
-                path: path.clone(),
-                nodes: nodes.clone(),
-            },
-            NodeOperation::UpdateBody { path, changeset: body } => NodeOperation::UpdateBody {
-                path: path.clone(),
-                changeset: body.inverted(),
-            },
-        }
-    }
-    pub fn clone_with_new_path(&self, path: Path) -> NodeOperation {
-        match self {
-            NodeOperation::Insert { nodes, .. } => NodeOperation::Insert {
-                path,
-                nodes: nodes.clone(),
-            },
-            NodeOperation::UpdateAttributes {
-                attributes,
-                old_attributes,
-                ..
-            } => NodeOperation::UpdateAttributes {
-                path,
-                attributes: attributes.clone(),
-                old_attributes: old_attributes.clone(),
-            },
-            NodeOperation::Delete { nodes, .. } => NodeOperation::Delete {
-                path,
-                nodes: nodes.clone(),
-            },
-            NodeOperation::UpdateBody { path, changeset } => NodeOperation::UpdateBody {
-                path: path.clone(),
-                changeset: changeset.clone(),
-            },
-        }
-    }
-    pub fn transform(a: &NodeOperation, b: &NodeOperation) -> NodeOperation {
-        match a {
-            NodeOperation::Insert { path: a_path, nodes } => {
-                let new_path = Path::transform(a_path, b.path(), nodes.len() as i64);
-                b.clone_with_new_path(new_path)
-            }
-            NodeOperation::Delete { path: a_path, nodes } => {
-                let new_path = Path::transform(a_path, b.path(), nodes.len() as i64);
-                b.clone_with_new_path(new_path)
-            }
-            _ => b.clone(),
-        }
-    }
-}
-
-#[derive(Serialize, Deserialize, Default)]
-pub struct NodeOperationList {
-    operations: Vec<NodeOperation>,
-}
-
-impl NodeOperationList {
-    pub fn into_inner(self) -> Vec<NodeOperation> {
-        self.operations
-    }
-}
-
-impl std::ops::Deref for NodeOperationList {
-    type Target = Vec<NodeOperation>;
-
-    fn deref(&self) -> &Self::Target {
-        &self.operations
-    }
-}
-
-impl std::ops::DerefMut for NodeOperationList {
-    fn deref_mut(&mut self) -> &mut Self::Target {
-        &mut self.operations
-    }
-}
-
-impl NodeOperationList {
-    pub fn new(operations: Vec<NodeOperation>) -> Self {
-        Self { operations }
-    }
-
-    pub fn from_bytes(bytes: Vec<u8>) -> Result<Self, OTError> {
-        let operation_list = serde_json::from_slice(&bytes).map_err(|err| OTError::serde().context(err))?;
-        Ok(operation_list)
-    }
-
-    pub fn to_bytes(&self) -> Result<Vec<u8>, OTError> {
-        let bytes = serde_json::to_vec(self).map_err(|err| OTError::serde().context(err))?;
-        Ok(bytes)
-    }
-}

+ 0 - 127
shared-lib/lib-ot/src/core/document/path.rs

@@ -1,127 +0,0 @@
-use serde::{Deserialize, Serialize};
-
-#[derive(Clone, Serialize, Deserialize, Eq, PartialEq, Debug)]
-pub struct Path(pub Vec<usize>);
-
-impl std::ops::Deref for Path {
-    type Target = Vec<usize>;
-
-    fn deref(&self) -> &Self::Target {
-        &self.0
-    }
-}
-
-impl std::convert::From<usize> for Path {
-    fn from(val: usize) -> Self {
-        Path(vec![val])
-    }
-}
-
-impl std::convert::From<&usize> for Path {
-    fn from(val: &usize) -> Self {
-        Path(vec![*val])
-    }
-}
-
-impl std::convert::From<&Path> for Path {
-    fn from(path: &Path) -> Self {
-        path.clone()
-    }
-}
-
-impl From<Vec<usize>> for Path {
-    fn from(v: Vec<usize>) -> Self {
-        Path(v)
-    }
-}
-
-impl From<&Vec<usize>> for Path {
-    fn from(values: &Vec<usize>) -> Self {
-        Path(values.clone())
-    }
-}
-
-impl From<&[usize]> for Path {
-    fn from(values: &[usize]) -> Self {
-        Path(values.to_vec())
-    }
-}
-
-impl Path {
-    // delta is default to be 1
-    pub fn transform(pre_insert_path: &Path, b: &Path, offset: i64) -> Path {
-        if pre_insert_path.len() > b.len() {
-            return b.clone();
-        }
-        if pre_insert_path.is_empty() || b.is_empty() {
-            return b.clone();
-        }
-        // check the prefix
-        for i in 0..(pre_insert_path.len() - 1) {
-            if pre_insert_path.0[i] != b.0[i] {
-                return b.clone();
-            }
-        }
-        let mut prefix: Vec<usize> = pre_insert_path.0[0..(pre_insert_path.len() - 1)].into();
-        let mut suffix: Vec<usize> = b.0[pre_insert_path.0.len()..].into();
-        let prev_insert_last: usize = *pre_insert_path.0.last().unwrap();
-        let b_at_index = b.0[pre_insert_path.0.len() - 1];
-        if prev_insert_last <= b_at_index {
-            prefix.push(((b_at_index as i64) + offset) as usize);
-        } else {
-            prefix.push(b_at_index);
-        }
-        prefix.append(&mut suffix);
-
-        Path(prefix)
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use crate::core::Path;
-    #[test]
-    fn path_transform_test_1() {
-        assert_eq!(
-            { Path::transform(&Path(vec![0, 1]), &Path(vec![0, 1]), 1) }.0,
-            vec![0, 2]
-        );
-
-        assert_eq!(
-            { Path::transform(&Path(vec![0, 1]), &Path(vec![0, 1]), 5) }.0,
-            vec![0, 6]
-        );
-    }
-
-    #[test]
-    fn path_transform_test_2() {
-        assert_eq!(
-            { Path::transform(&Path(vec![0, 1]), &Path(vec![0, 2]), 1) }.0,
-            vec![0, 3]
-        );
-    }
-
-    #[test]
-    fn path_transform_test_3() {
-        assert_eq!(
-            { Path::transform(&Path(vec![0, 1]), &Path(vec![0, 2, 7, 8, 9]), 1) }.0,
-            vec![0, 3, 7, 8, 9]
-        );
-    }
-
-    #[test]
-    fn path_transform_no_changed_test() {
-        assert_eq!(
-            { Path::transform(&Path(vec![0, 1, 2]), &Path(vec![0, 0, 7, 8, 9]), 1) }.0,
-            vec![0, 0, 7, 8, 9]
-        );
-        assert_eq!(
-            { Path::transform(&Path(vec![0, 1, 2]), &Path(vec![0, 1]), 1) }.0,
-            vec![0, 1]
-        );
-        assert_eq!(
-            { Path::transform(&Path(vec![1, 1]), &Path(vec![1, 0]), 1) }.0,
-            vec![1, 0]
-        );
-    }
-}

+ 2 - 2
shared-lib/lib-ot/src/core/mod.rs

@@ -1,12 +1,12 @@
 pub mod attributes;
 mod delta;
-mod document;
 mod interval;
+mod node_tree;
 mod ot_str;
 
 pub use attributes::*;
 pub use delta::operation::*;
 pub use delta::*;
-pub use document::*;
 pub use interval::*;
+pub use node_tree::*;
 pub use ot_str::*;

+ 2 - 2
shared-lib/lib-ot/src/core/document/mod.rs → shared-lib/lib-ot/src/core/node_tree/mod.rs

@@ -2,14 +2,14 @@
 
 mod node;
 mod node_serde;
-mod node_tree;
 mod operation;
 mod operation_serde;
 mod path;
 mod transaction;
+mod tree;
 
 pub use node::*;
-pub use node_tree::*;
 pub use operation::*;
 pub use path::*;
 pub use transaction::*;
+pub use tree::*;

+ 1 - 1
shared-lib/lib-ot/src/core/document/node.rs → shared-lib/lib-ot/src/core/node_tree/node.rs

@@ -6,7 +6,7 @@ use crate::errors::OTError;
 use crate::text_delta::TextDelta;
 use serde::{Deserialize, Serialize};
 
-#[derive(Default, Clone, Serialize, Deserialize, Eq, PartialEq)]
+#[derive(Default, Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
 pub struct NodeData {
     #[serde(rename = "type")]
     pub node_type: String,

+ 0 - 0
shared-lib/lib-ot/src/core/document/node_serde.rs → shared-lib/lib-ot/src/core/node_tree/node_serde.rs


+ 174 - 0
shared-lib/lib-ot/src/core/node_tree/operation.rs

@@ -0,0 +1,174 @@
+use crate::core::attributes::Attributes;
+use crate::core::{NodeBodyChangeset, NodeData, Path};
+use crate::errors::OTError;
+use serde::{Deserialize, Serialize};
+use std::rc::Rc;
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(tag = "op")]
+pub enum NodeOperation {
+    #[serde(rename = "insert")]
+    Insert { path: Path, nodes: Vec<NodeData> },
+
+    #[serde(rename = "update-attribute")]
+    UpdateAttributes {
+        path: Path,
+        new: Attributes,
+        old: Attributes,
+    },
+
+    #[serde(rename = "update-body")]
+    // #[serde(serialize_with = "serialize_edit_body")]
+    // #[serde(deserialize_with = "deserialize_edit_body")]
+    UpdateBody { path: Path, changeset: NodeBodyChangeset },
+
+    #[serde(rename = "delete")]
+    Delete { path: Path, nodes: Vec<NodeData> },
+}
+
+impl NodeOperation {
+    pub fn get_path(&self) -> &Path {
+        match self {
+            NodeOperation::Insert { path, .. } => path,
+            NodeOperation::UpdateAttributes { path, .. } => path,
+            NodeOperation::Delete { path, .. } => path,
+            NodeOperation::UpdateBody { path, .. } => path,
+        }
+    }
+
+    pub fn get_mut_path(&mut self) -> &mut Path {
+        match self {
+            NodeOperation::Insert { path, .. } => path,
+            NodeOperation::UpdateAttributes { path, .. } => path,
+            NodeOperation::Delete { path, .. } => path,
+            NodeOperation::UpdateBody { path, .. } => path,
+        }
+    }
+
+    pub fn invert(&self) -> NodeOperation {
+        match self {
+            NodeOperation::Insert { path, nodes } => NodeOperation::Delete {
+                path: path.clone(),
+                nodes: nodes.clone(),
+            },
+            NodeOperation::UpdateAttributes {
+                path,
+                new: attributes,
+                old: old_attributes,
+            } => NodeOperation::UpdateAttributes {
+                path: path.clone(),
+                new: old_attributes.clone(),
+                old: attributes.clone(),
+            },
+            NodeOperation::Delete { path, nodes } => NodeOperation::Insert {
+                path: path.clone(),
+                nodes: nodes.clone(),
+            },
+            NodeOperation::UpdateBody { path, changeset: body } => NodeOperation::UpdateBody {
+                path: path.clone(),
+                changeset: body.inverted(),
+            },
+        }
+    }
+
+    /// Make the `other` operation can be applied to the version after applying the `self` operation.
+    /// The semantics of transform is used when editing conflicts occur, which is often determined by the version id。
+    /// For example, if the inserted position has been acquired by others, then it's needed to do the transform to
+    /// make sure the inserted position is right.
+    ///
+    /// # Arguments
+    ///
+    /// * `other`: The operation that is going to be transformed
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use lib_ot::core::{NodeDataBuilder, NodeOperation, Path};
+    /// let node_1 = NodeDataBuilder::new("text_1").build();
+    /// let node_2 = NodeDataBuilder::new("text_2").build();
+    ///
+    /// let op_1 = NodeOperation::Insert {
+    ///     path: Path(vec![0, 1]),
+    ///     nodes: vec![node_1],
+    /// };
+    ///
+    /// let mut op_2 = NodeOperation::Insert {
+    ///     path: Path(vec![0, 1]),
+    ///     nodes: vec![node_2],
+    /// };
+    ///
+    /// assert_eq!(serde_json::to_string(&op_2).unwrap(), r#"{"op":"insert","path":[0,1],"nodes":[{"type":"text_2"}]}"#);
+    ///
+    /// op_1.transform(&mut op_2);
+    /// assert_eq!(serde_json::to_string(&op_2).unwrap(), r#"{"op":"insert","path":[0,2],"nodes":[{"type":"text_2"}]}"#);
+    ///
+    /// ```
+    pub fn transform(&self, other: &mut NodeOperation) {
+        match self {
+            NodeOperation::Insert { path, nodes } => {
+                let new_path = path.transform(other.get_path(), nodes.len());
+                *other.get_mut_path() = new_path;
+            }
+            NodeOperation::Delete { path, nodes } => {
+                let new_path = path.transform(other.get_path(), nodes.len());
+                *other.get_mut_path() = new_path;
+            }
+            _ => {
+                // Only insert/delete will change the path.
+            }
+        }
+    }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+pub struct NodeOperationList {
+    operations: Vec<Rc<NodeOperation>>,
+}
+
+impl NodeOperationList {
+    pub fn into_inner(self) -> Vec<Rc<NodeOperation>> {
+        self.operations
+    }
+
+    pub fn add_op(&mut self, operation: NodeOperation) {
+        self.operations.push(Rc::new(operation));
+    }
+}
+
+impl std::ops::Deref for NodeOperationList {
+    type Target = Vec<Rc<NodeOperation>>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.operations
+    }
+}
+
+impl std::ops::DerefMut for NodeOperationList {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.operations
+    }
+}
+
+impl std::convert::From<Vec<NodeOperation>> for NodeOperationList {
+    fn from(operations: Vec<NodeOperation>) -> Self {
+        Self::new(operations)
+    }
+}
+
+impl NodeOperationList {
+    pub fn new(operations: Vec<NodeOperation>) -> Self {
+        Self {
+            operations: operations.into_iter().map(Rc::new).collect(),
+        }
+    }
+
+    pub fn from_bytes(bytes: Vec<u8>) -> Result<Self, OTError> {
+        let operation_list = serde_json::from_slice(&bytes).map_err(|err| OTError::serde().context(err))?;
+        Ok(operation_list)
+    }
+
+    pub fn to_bytes(&self) -> Result<Vec<u8>, OTError> {
+        let bytes = serde_json::to_vec(self).map_err(|err| OTError::serde().context(err))?;
+        Ok(bytes)
+    }
+}

+ 0 - 0
shared-lib/lib-ot/src/core/document/operation_serde.rs → shared-lib/lib-ot/src/core/node_tree/operation_serde.rs


+ 190 - 0
shared-lib/lib-ot/src/core/node_tree/path.rs

@@ -0,0 +1,190 @@
+use serde::{Deserialize, Serialize};
+
+/// The `Path` represents as a path to reference to the node in the `NodeTree`.
+/// ┌─────────┐
+/// │  Root   │
+/// └─────────┼──────────┐
+///           │0: Node A │
+///           └──────────┼────────────┐
+///                      │0: Node A-1 │  
+///                      ├────────────┤
+///                      │1: Node A-2 │
+///           ┌──────────┼────────────┘
+///           │1: Node B │
+///           └──────────┼────────────┐
+///                      │0: Node B-1 │
+///                      ├────────────┤
+///                      │1: Node B-2 │
+///           ┌──────────┼────────────┘
+///           │2: Node C │
+///           └──────────┘
+///
+/// The path of  Node A will be [0]
+/// The path of  Node A-1 will be [0,0]
+/// The path of  Node A-2 will be [0,1]
+/// The path of  Node B-2 will be [1,1]
+#[derive(Clone, Serialize, Deserialize, Eq, PartialEq, Debug)]
+pub struct Path(pub Vec<usize>);
+
+impl std::ops::Deref for Path {
+    type Target = Vec<usize>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl std::convert::From<usize> for Path {
+    fn from(val: usize) -> Self {
+        Path(vec![val])
+    }
+}
+
+impl std::convert::From<&usize> for Path {
+    fn from(val: &usize) -> Self {
+        Path(vec![*val])
+    }
+}
+
+impl std::convert::From<&Path> for Path {
+    fn from(path: &Path) -> Self {
+        path.clone()
+    }
+}
+
+impl From<Vec<usize>> for Path {
+    fn from(v: Vec<usize>) -> Self {
+        Path(v)
+    }
+}
+
+impl From<&Vec<usize>> for Path {
+    fn from(values: &Vec<usize>) -> Self {
+        Path(values.clone())
+    }
+}
+
+impl From<&[usize]> for Path {
+    fn from(values: &[usize]) -> Self {
+        Path(values.to_vec())
+    }
+}
+
+impl Path {
+    /// Calling this function if there are two changes want to modify the same path.
+    ///
+    /// # Arguments
+    ///
+    /// * `other`: the path that need to be transformed  
+    /// * `offset`: represents the len of nodes referenced by the current path
+    ///
+    /// If two changes modify the same path or the path was shared by them. Then it needs to do the
+    /// transformation to make sure the changes are applied to the right path.
+    ///
+    /// returns: the path represents the position that the other path reference to.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use lib_ot::core::Path;
+    /// let path = Path(vec![0, 1]);
+    /// for (old_path, len_of_nodes, expected_path) in vec![
+    ///     // Try to modify the path [0, 1], but someone has inserted  one element before the
+    ///     // current path [0,1] in advance. That causes the modified path [0,1] to no longer
+    ///     // valid. It needs to do the transformation to get the right path.
+    ///     //
+    ///     // [0,2] is the path you want to modify.
+    ///     (Path(vec![0, 1]), 1, Path(vec![0, 2])),
+    ///     (Path(vec![0, 1]), 5, Path(vec![0, 6])),
+    ///     (Path(vec![0, 2]), 1, Path(vec![0, 3])),
+    ///     // Try to modify the path [0, 2,3,4], but someone has inserted one element before the
+    ///     // current path [0,1] in advance. That cause the prefix path [0,2] of [0,2,3,4]
+    ///     // no longer valid.
+    ///     // It needs to do the transformation to get the right path. So [0,2] is transformed to [0,3]
+    ///     // and the suffix [3,4] of the [0,2,3,4] remains the same. So the transformed result is
+    ///     //
+    ///     // [0,3,3,4]
+    ///     (Path(vec![0, 2, 3, 4]), 1, Path(vec![0, 3, 3, 4])),
+    /// ] {
+    ///     assert_eq!(path.transform(&old_path, len_of_nodes), expected_path);
+    /// }
+    /// // The path remains the same in the following test. Because the shared path is not changed.
+    /// let path = Path(vec![0, 1, 2]);
+    /// for (old_path, len_of_nodes, expected_path) in vec![
+    ///     // Try to modify the path [0,0,0,1,2], but someone has inserted one element
+    ///     // before [0,1,2]. [0,0,0,1,2] and [0,1,2] share the same path [0,x], because
+    ///     // the element was inserted at [0,1,2] that didn't affect the shared path [0, x].
+    ///     // So, after the transformation, the path is not changed.
+    ///     (Path(vec![0, 0, 0, 1, 2]), 1, Path(vec![0, 0, 0, 1, 2])),
+    ///     (Path(vec![0, 1]), 1, Path(vec![0, 1])),
+    /// ] {
+    ///     assert_eq!(path.transform(&old_path, len_of_nodes), expected_path);
+    /// }
+    ///
+    /// let path = Path(vec![1, 1]);
+    /// for (old_path, len_of_nodes, expected_path) in vec![(Path(vec![1, 0]), 1, Path(vec![1, 0]))] {
+    ///     assert_eq!(path.transform(&old_path, len_of_nodes), expected_path);
+    /// }
+    /// ```
+    /// For example, client A and client B want to insert a node at the same index, the server applies
+    /// the changes made by client B. But, before applying the client A's changes, server transforms
+    /// the changes first in order to make sure that client A modify the right position. After that,
+    /// the changes can be applied to the server.
+    ///
+    /// ┌──────────┐            ┌──────────┐               ┌──────────┐
+    /// │ Client A │            │  Server  │               │ Client B │
+    /// └─────┬────┘            └─────┬────┘               └────┬─────┘
+    ///       │                       │   ┌ ─ ─ ─ ─ ─ ─ ─ ┐     │
+    ///       │                       │    Root                 │
+    ///       │                       │   │    0:A        │     │
+    ///       │                       │    ─ ─ ─ ─ ─ ─ ─ ─      │
+    ///       │                       │ ◀───────────────────────│
+    ///       │                       │    Insert B at index 1  │
+    ///       │                       │                         │
+    ///       │                       │   ┌ ─ ─ ─ ─ ─ ─ ─ ┐     │
+    ///       │                       │    Root                 │
+    ///       │                       │   │    0:A        │     │
+    ///       ├──────────────────────▶│        1:B              │
+    ///       │ Insert C at index 1   │   └ ─ ─ ─ ─ ─ ─ ─ ┘     │
+    ///       │                       │                         │
+    ///       │                       │ transform index 1 to 2  │
+    ///       │                       │                         │
+    ///       │                       │  ┌ ─ ─ ─ ─ ─ ─ ─ ─      │
+    ///       │                       │   Root            │     │
+    ///       │                       │  │    0:A               │
+    ///       ▼                       ▼       1:B         │     ▼
+    ///                                  │    2:C
+    ///                                   ─ ─ ─ ─ ─ ─ ─ ─ ┘
+    pub fn transform(&self, other: &Path, offset: usize) -> Path {
+        if self.len() > other.len() {
+            return other.clone();
+        }
+        if self.is_empty() || other.is_empty() {
+            return other.clone();
+        }
+        for i in 0..(self.len() - 1) {
+            if self.0[i] != other.0[i] {
+                return other.clone();
+            }
+        }
+
+        // Splits the `Path` into two part. The suffix will contain the last element of the `Path`.
+        let second_last_index = self.0.len() - 1;
+        let mut prefix: Vec<usize> = self.0[0..second_last_index].into();
+        let mut suffix: Vec<usize> = other.0[self.0.len()..].into();
+        let last_value = *self.0.last().unwrap();
+
+        let other_second_last_value = other.0[second_last_index];
+
+        //
+        if last_value <= other_second_last_value {
+            prefix.push(other_second_last_value + offset);
+        } else {
+            prefix.push(other_second_last_value);
+        }
+
+        // concat the prefix and suffix into a new path
+        prefix.append(&mut suffix);
+        Path(prefix)
+    }
+}

+ 46 - 15
shared-lib/lib-ot/src/core/document/transaction.rs → shared-lib/lib-ot/src/core/node_tree/transaction.rs

@@ -1,26 +1,57 @@
 use crate::core::attributes::Attributes;
-use crate::core::document::path::Path;
-use crate::core::{NodeData, NodeOperation, NodeTree};
+use crate::core::{NodeData, NodeOperation, NodeTree, Path};
+use crate::errors::OTError;
 use indextree::NodeId;
+use std::rc::Rc;
 
 use super::{NodeBodyChangeset, NodeOperationList};
 
+#[derive(Debug, Clone, Default)]
 pub struct Transaction {
     operations: NodeOperationList,
 }
 
 impl Transaction {
-    pub fn new(operations: NodeOperationList) -> Transaction {
-        Transaction { operations }
+    pub fn new() -> Self {
+        Self::default()
     }
 
-    pub fn into_operations(self) -> Vec<NodeOperation> {
+    pub fn from_operations<T: Into<NodeOperationList>>(operations: T) -> Self {
+        Self {
+            operations: operations.into(),
+        }
+    }
+
+    pub fn into_operations(self) -> Vec<Rc<NodeOperation>> {
         self.operations.into_inner()
     }
+
+    /// Make the `other` can be applied to the version after applying the `self` transaction.
+    ///
+    /// The semantics of transform is used when editing conflicts occur, which is often determined by the version id。
+    /// the operations of the transaction will be transformed into the conflict operations.
+    pub fn transform(&self, other: &Transaction) -> Result<Transaction, OTError> {
+        let mut new_transaction = other.clone();
+        for other_operation in new_transaction.iter_mut() {
+            let other_operation = Rc::make_mut(other_operation);
+            for operation in self.operations.iter() {
+                operation.transform(other_operation);
+            }
+        }
+        Ok(new_transaction)
+    }
+
+    pub fn compose(&mut self, other: &Transaction) -> Result<(), OTError> {
+        // For the moment, just append `other` operations to the end of `self`.
+        for operation in other.operations.iter() {
+            self.operations.push(operation.clone());
+        }
+        Ok(())
+    }
 }
 
 impl std::ops::Deref for Transaction {
-    type Target = NodeOperationList;
+    type Target = Vec<Rc<NodeOperation>>;
 
     fn deref(&self) -> &Self::Target {
         &self.operations
@@ -64,7 +95,7 @@ impl<'a> TransactionBuilder<'a> {
     /// let transaction = TransactionBuilder::new(&node_tree)
     ///     .insert_nodes_at_path(0,vec![ NodeData::new("text_1"),  NodeData::new("text_2")])
     ///     .finalize();
-    ///  node_tree.apply(transaction).unwrap();
+    ///  node_tree.apply_transaction(transaction).unwrap();
     ///
     ///  node_tree.node_id_at_path(vec![0, 0]);
     /// ```
@@ -94,7 +125,7 @@ impl<'a> TransactionBuilder<'a> {
     /// let transaction = TransactionBuilder::new(&node_tree)
     ///     .insert_node_at_path(0, NodeData::new("text"))
     ///     .finalize();
-    ///  node_tree.apply(transaction).unwrap();
+    ///  node_tree.apply_transaction(transaction).unwrap();
     /// ```
     ///
     pub fn insert_node_at_path<T: Into<Path>>(self, path: T, node: NodeData) -> Self {
@@ -112,10 +143,10 @@ impl<'a> TransactionBuilder<'a> {
                     }
                 }
 
-                self.operations.push(NodeOperation::UpdateAttributes {
+                self.operations.add_op(NodeOperation::UpdateAttributes {
                     path: path.clone(),
-                    attributes,
-                    old_attributes,
+                    new: attributes,
+                    old: old_attributes,
                 });
             }
             None => tracing::warn!("Update attributes at path: {:?} failed. Node is not exist", path),
@@ -126,7 +157,7 @@ impl<'a> TransactionBuilder<'a> {
     pub fn update_body_at_path(mut self, path: &Path, changeset: NodeBodyChangeset) -> Self {
         match self.node_tree.node_id_at_path(path) {
             Some(_) => {
-                self.operations.push(NodeOperation::UpdateBody {
+                self.operations.add_op(NodeOperation::UpdateBody {
                     path: path.clone(),
                     changeset,
                 });
@@ -148,7 +179,7 @@ impl<'a> TransactionBuilder<'a> {
             node = self.node_tree.following_siblings(node).next().unwrap();
         }
 
-        self.operations.push(NodeOperation::Delete {
+        self.operations.add_op(NodeOperation::Delete {
             path: path.clone(),
             nodes: deleted_nodes,
         });
@@ -172,11 +203,11 @@ impl<'a> TransactionBuilder<'a> {
     }
 
     pub fn push(mut self, op: NodeOperation) -> Self {
-        self.operations.push(op);
+        self.operations.add_op(op);
         self
     }
 
     pub fn finalize(self) -> Transaction {
-        Transaction::new(self.operations)
+        Transaction::from_operations(self.operations)
     }
 }

+ 28 - 15
shared-lib/lib-ot/src/core/document/node_tree.rs → shared-lib/lib-ot/src/core/node_tree/tree.rs

@@ -1,8 +1,8 @@
 use crate::core::attributes::Attributes;
-use crate::core::document::path::Path;
-use crate::core::{Node, NodeBodyChangeset, NodeData, NodeOperation, OperationTransform, Transaction};
+use crate::core::{Node, NodeBodyChangeset, NodeData, NodeOperation, OperationTransform, Path, Transaction};
 use crate::errors::{ErrorBuilder, OTError, OTErrorCode};
 use indextree::{Arena, Children, FollowingSiblings, NodeId};
+use std::rc::Rc;
 
 use super::NodeOperationList;
 
@@ -26,14 +26,13 @@ impl NodeTree {
     }
 
     pub fn from_bytes(root_name: &str, bytes: Vec<u8>) -> Result<Self, OTError> {
-        let operations = NodeOperationList::from_bytes(bytes)?.into_inner();
+        let operations = NodeOperationList::from_bytes(bytes)?;
         Self::from_operations(root_name, operations)
     }
 
-    pub fn from_operations(root_name: &str, operations: Vec<NodeOperation>) -> Result<Self, OTError> {
+    pub fn from_operations(root_name: &str, operations: NodeOperationList) -> Result<Self, OTError> {
         let mut node_tree = NodeTree::new(root_name);
-
-        for operation in operations {
+        for operation in operations.into_inner().into_iter() {
             let _ = node_tree.apply_op(operation)?;
         }
         Ok(node_tree)
@@ -54,13 +53,14 @@ impl NodeTree {
     /// # Examples
     ///
     /// ```
+    /// use std::rc::Rc;
     /// use lib_ot::core::{NodeOperation, NodeTree, NodeData, Path};
     /// let nodes = vec![NodeData::new("text".to_string())];
     /// let root_path: Path = vec![0].into();
     /// let op = NodeOperation::Insert {path: root_path.clone(),nodes };
     ///
     /// let mut node_tree = NodeTree::new("root");
-    /// node_tree.apply_op(op).unwrap();
+    /// node_tree.apply_op(Rc::new(op)).unwrap();
     /// let node_id = node_tree.node_id_at_path(&root_path).unwrap();
     /// let node_path = node_tree.path_from_node_id(node_id);
     /// debug_assert_eq!(node_path, root_path);
@@ -105,23 +105,25 @@ impl NodeTree {
         counter
     }
 
-    ///
+    /// Returns the note_id at the position of the tree with id note_id
     /// # Arguments
     ///
-    /// * `node_id`:
-    /// * `index`:
+    /// * `node_id`: the node id of the child's parent
+    /// * `index`: index of the node in parent children list
     ///
     /// returns: Option<NodeId>
     ///
     /// # Examples
     ///
     /// ```
+    /// use std::rc::Rc;
     /// use lib_ot::core::{NodeOperation, NodeTree, NodeData, Path};
     /// let node_1 = NodeData::new("text".to_string());
     /// let inserted_path: Path = vec![0].into();
     ///
     /// let mut node_tree = NodeTree::new("root");
-    /// node_tree.apply_op(NodeOperation::Insert {path: inserted_path.clone(),nodes: vec![node_1.clone()] }).unwrap();
+    /// let op = NodeOperation::Insert {path: inserted_path.clone(),nodes: vec![node_1.clone()] };
+    /// node_tree.apply_op(Rc::new(op)).unwrap();
     ///
     /// let node_2 = node_tree.get_node_at_path(&inserted_path).unwrap();
     /// assert_eq!(node_2.node_type, node_1.node_type);
@@ -137,6 +139,10 @@ impl NodeTree {
         None
     }
 
+    /// Returns all children whose parent node id is node_id
+    ///
+    /// * `node_id`: the children's parent node id
+    ///
     pub fn children_from_node(&self, node_id: NodeId) -> Children<'_, Node> {
         node_id.children(&self.arena)
     }
@@ -159,7 +165,7 @@ impl NodeTree {
         node_id.following_siblings(&self.arena)
     }
 
-    pub fn apply(&mut self, transaction: Transaction) -> Result<(), OTError> {
+    pub fn apply_transaction(&mut self, transaction: Transaction) -> Result<(), OTError> {
         let operations = transaction.into_operations();
         for operation in operations {
             self.apply_op(operation)?;
@@ -167,10 +173,15 @@ impl NodeTree {
         Ok(())
     }
 
-    pub fn apply_op(&mut self, op: NodeOperation) -> Result<(), OTError> {
+    pub fn apply_op(&mut self, op: Rc<NodeOperation>) -> Result<(), OTError> {
+        let op = match Rc::try_unwrap(op) {
+            Ok(op) => op,
+            Err(op) => op.as_ref().clone(),
+        };
+
         match op {
             NodeOperation::Insert { path, nodes } => self.insert_nodes(&path, nodes),
-            NodeOperation::UpdateAttributes { path, attributes, .. } => self.update_attributes(&path, attributes),
+            NodeOperation::UpdateAttributes { path, new, .. } => self.update_attributes(&path, new),
             NodeOperation::UpdateBody { path, changeset } => self.update_body(&path, changeset),
             NodeOperation::Delete { path, nodes } => self.delete_node(&path, nodes),
         }
@@ -216,7 +227,9 @@ impl NodeTree {
             return Ok(());
         }
 
-        if index == parent.children(&self.arena).count() {
+        // Append the node to the end of the children list if index greater or equal to the
+        // length of the children.
+        if index >= parent.children(&self.arena).count() {
             self.append_nodes(&parent, nodes);
             return Ok(());
         }

+ 0 - 1
shared-lib/lib-ot/src/text_delta/delta.rs

@@ -2,5 +2,4 @@ use crate::core::{Attributes, Operation, OperationBuilder, Operations};
 
 pub type TextDelta = Operations<Attributes>;
 pub type TextDeltaBuilder = OperationBuilder<Attributes>;
-
 pub type TextOperation = Operation<Attributes>;

+ 4 - 3
shared-lib/lib-ot/tests/node/editor_test.rs

@@ -26,7 +26,8 @@ fn editor_deserialize_node_test() {
     test.run_scripts(vec![
         InsertNode {
             path,
-            node: node.clone(),
+            node_data: node.clone(),
+            rev_id: 1,
         },
         AssertNumberOfNodesAtPath { path: None, len: 1 },
         AssertNumberOfNodesAtPath {
@@ -41,11 +42,11 @@ fn editor_deserialize_node_test() {
             path: vec![0, 1].into(),
             expected: expected_delta,
         },
-        AssertNode {
+        AssertNodeData {
             path: vec![0, 0].into(),
             expected: Some(node.children[0].clone()),
         },
-        AssertNode {
+        AssertNodeData {
             path: vec![0, 3].into(),
             expected: Some(node.children[3].clone()),
         },

+ 169 - 5
shared-lib/lib-ot/tests/node/operation_test.rs

@@ -1,4 +1,6 @@
-use lib_ot::core::AttributeBuilder;
+use crate::node::script::NodeScript::*;
+use crate::node::script::NodeTest;
+use lib_ot::core::{AttributeBuilder, Node};
 use lib_ot::{
     core::{NodeBodyChangeset, NodeData, NodeDataBuilder, NodeOperation, Path},
     text_delta::TextDeltaBuilder,
@@ -33,15 +35,14 @@ fn operation_insert_node_with_children_serde_test() {
 fn operation_update_node_attributes_serde_test() {
     let operation = NodeOperation::UpdateAttributes {
         path: Path(vec![0, 1]),
-        attributes: AttributeBuilder::new().insert("bold", true).build(),
-        old_attributes: AttributeBuilder::new().insert("bold", false).build(),
+        new: AttributeBuilder::new().insert("bold", true).build(),
+        old: AttributeBuilder::new().insert("bold", false).build(),
     };
 
     let result = serde_json::to_string(&operation).unwrap();
-
     assert_eq!(
         result,
-        r#"{"op":"update","path":[0,1],"attributes":{"bold":true},"oldAttributes":{"bold":null}}"#
+        r#"{"op":"update-attribute","path":[0,1],"new":{"bold":true},"old":{"bold":null}}"#
     );
 }
 
@@ -69,3 +70,166 @@ fn operation_update_node_body_deserialize_test() {
     let json_2 = serde_json::to_string(&operation).unwrap();
     assert_eq!(json_1, json_2);
 }
+
+#[test]
+fn operation_insert_op_transform_test() {
+    let node_1 = NodeDataBuilder::new("text_1").build();
+    let node_2 = NodeDataBuilder::new("text_2").build();
+    let op_1 = NodeOperation::Insert {
+        path: Path(vec![0, 1]),
+        nodes: vec![node_1],
+    };
+
+    let mut insert_2 = NodeOperation::Insert {
+        path: Path(vec![0, 1]),
+        nodes: vec![node_2],
+    };
+
+    // let mut node_tree = NodeTree::new("root");
+    // node_tree.apply_op(insert_1.clone()).unwrap();
+
+    op_1.transform(&mut insert_2);
+    let json = serde_json::to_string(&insert_2).unwrap();
+    assert_eq!(json, r#"{"op":"insert","path":[0,2],"nodes":[{"type":"text_2"}]}"#);
+}
+
+#[test]
+fn operation_insert_transform_one_level_path_test() {
+    let mut test = NodeTest::new();
+    let node_data_1 = NodeDataBuilder::new("text_1").build();
+    let node_data_2 = NodeDataBuilder::new("text_2").build();
+    let node_data_3 = NodeDataBuilder::new("text_3").build();
+    let node_3: Node = node_data_3.clone().into();
+    //  0: text_1
+    //  1: text_2
+    //
+    //  Insert a new operation with rev_id 1,but the rev_id:1 is already exist, so
+    //  it needs to be transformed.
+    //  1:text_3 => 2:text_3
+    //
+    //  0: text_1
+    //  1: text_2
+    //  2: text_3
+    //
+    //  If the rev_id of the insert operation is 3. then the tree will be:
+    //  0: text_1
+    //  1: text_3
+    //  2: text_2
+    let scripts = vec![
+        InsertNode {
+            path: 0.into(),
+            node_data: node_data_1,
+            rev_id: 1,
+        },
+        InsertNode {
+            path: 1.into(),
+            node_data: node_data_2,
+            rev_id: 2,
+        },
+        InsertNode {
+            path: 1.into(),
+            node_data: node_data_3,
+            rev_id: 1,
+        },
+        AssertNode {
+            path: 2.into(),
+            expected: Some(node_3),
+        },
+    ];
+    test.run_scripts(scripts);
+}
+
+#[test]
+fn operation_insert_transform_multiple_level_path_test() {
+    let mut test = NodeTest::new();
+    let node_data_1 = NodeDataBuilder::new("text_1")
+        .add_node(NodeDataBuilder::new("text_1_1").build())
+        .add_node(NodeDataBuilder::new("text_1_2").build())
+        .build();
+
+    let node_data_2 = NodeDataBuilder::new("text_2")
+        .add_node(NodeDataBuilder::new("text_2_1").build())
+        .add_node(NodeDataBuilder::new("text_2_2").build())
+        .build();
+
+    let node_data_3 = NodeDataBuilder::new("text_3").build();
+    let scripts = vec![
+        InsertNode {
+            path: 0.into(),
+            node_data: node_data_1,
+            rev_id: 1,
+        },
+        InsertNode {
+            path: 1.into(),
+            node_data: node_data_2,
+            rev_id: 2,
+        },
+        InsertNode {
+            path: 1.into(),
+            node_data: node_data_3.clone(),
+            rev_id: 1,
+        },
+        AssertNode {
+            path: 2.into(),
+            expected: Some(node_data_3.into()),
+        },
+    ];
+    test.run_scripts(scripts);
+}
+
+#[test]
+fn operation_delete_transform_path_test() {
+    let mut test = NodeTest::new();
+    let node_data_1 = NodeDataBuilder::new("text_1").build();
+    let node_data_2 = NodeDataBuilder::new("text_2").build();
+    let node_data_3 = NodeDataBuilder::new("text_3").build();
+    let node_3: Node = node_data_3.clone().into();
+
+    let scripts = vec![
+        InsertNode {
+            path: 0.into(),
+            node_data: node_data_1,
+            rev_id: 1,
+        },
+        InsertNode {
+            path: 1.into(),
+            node_data: node_data_2,
+            rev_id: 2,
+        },
+        // The node's in the tree will be:
+        // 0: text_1
+        // 2: text_2
+        //
+        // The insert action is happened concurrently with the delete action, because they
+        // share the same rev_id. aka, 3. The delete action is want to delete the node at index 1,
+        // but it was moved to index 2.
+        InsertNode {
+            path: 1.into(),
+            node_data: node_data_3,
+            rev_id: 3,
+        },
+        // 0: text_1
+        // 1: text_3
+        // 2: text_2
+        //
+        // The path of the delete action will be transformed to a new path that point to the text_2.
+        // 1 -> 2
+        DeleteNode {
+            path: 1.into(),
+            rev_id: 3,
+        },
+        // After perform the delete action, the tree will be:
+        // 0: text_1
+        // 1: text_3
+        AssertNumberOfNodesAtPath { path: None, len: 2 },
+        AssertNode {
+            path: 1.into(),
+            expected: Some(node_3),
+        },
+        AssertNode {
+            path: 2.into(),
+            expected: None,
+        },
+    ];
+    test.run_scripts(scripts);
+}

+ 79 - 18
shared-lib/lib-ot/tests/node/script.rs

@@ -1,26 +1,58 @@
+use lib_ot::core::{Node, Transaction};
 use lib_ot::{
     core::attributes::Attributes,
     core::{NodeBody, NodeBodyChangeset, NodeData, NodeTree, Path, TransactionBuilder},
     text_delta::TextDelta,
 };
+use std::collections::HashMap;
 
 pub enum NodeScript {
-    InsertNode { path: Path, node: NodeData },
-    UpdateAttributes { path: Path, attributes: Attributes },
-    UpdateBody { path: Path, changeset: NodeBodyChangeset },
-    DeleteNode { path: Path },
-    AssertNumberOfNodesAtPath { path: Option<Path>, len: usize },
-    AssertNode { path: Path, expected: Option<NodeData> },
-    AssertNodeDelta { path: Path, expected: TextDelta },
+    InsertNode {
+        path: Path,
+        node_data: NodeData,
+        rev_id: usize,
+    },
+    UpdateAttributes {
+        path: Path,
+        attributes: Attributes,
+    },
+    UpdateBody {
+        path: Path,
+        changeset: NodeBodyChangeset,
+    },
+    DeleteNode {
+        path: Path,
+        rev_id: usize,
+    },
+    AssertNumberOfNodesAtPath {
+        path: Option<Path>,
+        len: usize,
+    },
+    AssertNodeData {
+        path: Path,
+        expected: Option<NodeData>,
+    },
+    AssertNode {
+        path: Path,
+        expected: Option<Node>,
+    },
+    AssertNodeDelta {
+        path: Path,
+        expected: TextDelta,
+    },
 }
 
 pub struct NodeTest {
+    rev_id: usize,
+    rev_operations: HashMap<usize, Transaction>,
     node_tree: NodeTree,
 }
 
 impl NodeTest {
     pub fn new() -> Self {
         Self {
+            rev_id: 0,
+            rev_operations: HashMap::new(),
             node_tree: NodeTree::new("root"),
         }
     }
@@ -33,40 +65,54 @@ impl NodeTest {
 
     pub fn run_script(&mut self, script: NodeScript) {
         match script {
-            NodeScript::InsertNode { path, node } => {
-                let transaction = TransactionBuilder::new(&self.node_tree)
+            NodeScript::InsertNode {
+                path,
+                node_data: node,
+                rev_id,
+            } => {
+                let mut transaction = TransactionBuilder::new(&self.node_tree)
                     .insert_node_at_path(path, node)
                     .finalize();
-
-                self.node_tree.apply(transaction).unwrap();
+                self.transform_transaction_if_need(&mut transaction, rev_id);
+                self.apply_transaction(transaction);
             }
             NodeScript::UpdateAttributes { path, attributes } => {
                 let transaction = TransactionBuilder::new(&self.node_tree)
                     .update_attributes_at_path(&path, attributes)
                     .finalize();
-                self.node_tree.apply(transaction).unwrap();
+                self.apply_transaction(transaction);
             }
             NodeScript::UpdateBody { path, changeset } => {
                 //
                 let transaction = TransactionBuilder::new(&self.node_tree)
                     .update_body_at_path(&path, changeset)
                     .finalize();
-                self.node_tree.apply(transaction).unwrap();
+                self.apply_transaction(transaction);
             }
-            NodeScript::DeleteNode { path } => {
-                let transaction = TransactionBuilder::new(&self.node_tree)
+            NodeScript::DeleteNode { path, rev_id } => {
+                let mut transaction = TransactionBuilder::new(&self.node_tree)
                     .delete_node_at_path(&path)
                     .finalize();
-                self.node_tree.apply(transaction).unwrap();
+                self.transform_transaction_if_need(&mut transaction, rev_id);
+                self.apply_transaction(transaction);
             }
             NodeScript::AssertNode { path, expected } => {
                 let node_id = self.node_tree.node_id_at_path(path);
+                if expected.is_none() && node_id.is_none() {
+                    return;
+                }
+
+                let node = self.node_tree.get_node(node_id.unwrap()).cloned();
+                assert_eq!(node, expected);
+            }
+            NodeScript::AssertNodeData { path, expected } => {
+                let node_id = self.node_tree.node_id_at_path(path);
 
                 match node_id {
                     None => assert!(node_id.is_none()),
                     Some(node_id) => {
-                        let node_data = self.node_tree.get_node(node_id).cloned();
-                        assert_eq!(node_data, expected.map(|e| e.into()));
+                        let node = self.node_tree.get_node(node_id).cloned();
+                        assert_eq!(node, expected.map(|e| e.into()));
                     }
                 }
             }
@@ -94,4 +140,19 @@ impl NodeTest {
             }
         }
     }
+
+    fn apply_transaction(&mut self, transaction: Transaction) {
+        self.rev_id += 1;
+        self.rev_operations.insert(self.rev_id, transaction.clone());
+        self.node_tree.apply_transaction(transaction).unwrap();
+    }
+
+    fn transform_transaction_if_need(&mut self, transaction: &mut Transaction, rev_id: usize) {
+        if self.rev_id >= rev_id {
+            for rev_id in rev_id..=self.rev_id {
+                let old_transaction = self.rev_operations.get(&rev_id).unwrap();
+                *transaction = old_transaction.transform(transaction).unwrap();
+            }
+        }
+    }
 }

+ 138 - 30
shared-lib/lib-ot/tests/node/tree_test.rs

@@ -14,9 +14,10 @@ fn node_insert_test() {
     let scripts = vec![
         InsertNode {
             path: path.clone(),
-            node: inserted_node.clone(),
+            node_data: inserted_node.clone(),
+            rev_id: 1,
         },
-        AssertNode {
+        AssertNodeData {
             path,
             expected: Some(inserted_node),
         },
@@ -32,9 +33,10 @@ fn node_insert_node_with_children_test() {
     let scripts = vec![
         InsertNode {
             path: path.clone(),
-            node: inserted_node.clone(),
+            node_data: inserted_node.clone(),
+            rev_id: 1,
         },
-        AssertNode {
+        AssertNodeData {
             path,
             expected: Some(inserted_node),
         },
@@ -57,25 +59,28 @@ fn node_insert_multi_nodes_test() {
     let scripts = vec![
         InsertNode {
             path: path_1.clone(),
-            node: node_1.clone(),
+            node_data: node_1.clone(),
+            rev_id: 1,
         },
         InsertNode {
             path: path_2.clone(),
-            node: node_2.clone(),
+            node_data: node_2.clone(),
+            rev_id: 2,
         },
         InsertNode {
             path: path_3.clone(),
-            node: node_3.clone(),
+            node_data: node_3.clone(),
+            rev_id: 3,
         },
-        AssertNode {
+        AssertNodeData {
             path: path_1,
             expected: Some(node_1),
         },
-        AssertNode {
+        AssertNodeData {
             path: path_2,
             expected: Some(node_2),
         },
-        AssertNode {
+        AssertNodeData {
             path: path_3,
             expected: Some(node_3),
         },
@@ -96,48 +101,145 @@ fn node_insert_node_in_ordered_nodes_test() {
     let path_3: Path = 2.into();
     let node_3 = NodeData::new("text_3");
 
-    let path_4: Path = 3.into();
-
     let scripts = vec![
         InsertNode {
             path: path_1.clone(),
-            node: node_1.clone(),
+            node_data: node_1.clone(),
+            rev_id: 1,
         },
         InsertNode {
             path: path_2.clone(),
-            node: node_2_1.clone(),
+            node_data: node_2_1.clone(),
+            rev_id: 2,
         },
         InsertNode {
             path: path_3.clone(),
-            node: node_3.clone(),
+            node_data: node_3,
+            rev_id: 3,
         },
-        // 0:note_1 , 1: note_2_1, 2: note_3
+        // 0:text_1
+        // 1:text_2_1
+        // 2:text_3
         InsertNode {
             path: path_2.clone(),
-            node: node_2_2.clone(),
+            node_data: node_2_2.clone(),
+            rev_id: 4,
         },
-        // 0:note_1 , 1:note_2_2,  2: note_2_1, 3: note_3
-        AssertNode {
+        // 0:text_1
+        // 1:text_2_2
+        // 2:text_2_1
+        // 3:text_3
+        AssertNodeData {
             path: path_1,
             expected: Some(node_1),
         },
-        AssertNode {
+        AssertNodeData {
             path: path_2,
             expected: Some(node_2_2),
         },
-        AssertNode {
+        AssertNodeData {
             path: path_3,
             expected: Some(node_2_1),
         },
+        AssertNumberOfNodesAtPath { path: None, len: 4 },
+    ];
+    test.run_scripts(scripts);
+}
+
+#[test]
+fn node_insert_nested_nodes_test() {
+    let mut test = NodeTest::new();
+    let node_data_1_1 = NodeDataBuilder::new("text_1_1").build();
+    let node_data_1_2 = NodeDataBuilder::new("text_1_2").build();
+    let node_data_1 = NodeDataBuilder::new("text_1")
+        .add_node(node_data_1_1.clone())
+        .add_node(node_data_1_2.clone())
+        .build();
+
+    let node_data_2_1 = NodeDataBuilder::new("text_2_1").build();
+    let node_data_2_2 = NodeDataBuilder::new("text_2_2").build();
+    let node_data_2 = NodeDataBuilder::new("text_2")
+        .add_node(node_data_2_1.clone())
+        .add_node(node_data_2_2.clone())
+        .build();
+
+    let scripts = vec![
+        InsertNode {
+            path: 0.into(),
+            node_data: node_data_1,
+            rev_id: 1,
+        },
+        InsertNode {
+            path: 1.into(),
+            node_data: node_data_2,
+            rev_id: 2,
+        },
+        // the tree will be:
+        // 0:text_1
+        //      0:text_1_1
+        //      1:text_1_2
+        // 1:text_2
+        //      0:text_2_1
+        //      1:text_2_2
         AssertNode {
-            path: path_4,
-            expected: Some(node_3),
+            path: vec![0, 0].into(),
+            expected: Some(node_data_1_1.into()),
+        },
+        AssertNode {
+            path: vec![0, 1].into(),
+            expected: Some(node_data_1_2.into()),
+        },
+        AssertNode {
+            path: vec![1, 0].into(),
+            expected: Some(node_data_2_1.into()),
+        },
+        AssertNode {
+            path: vec![1, 1].into(),
+            expected: Some(node_data_2_2.into()),
         },
-        AssertNumberOfNodesAtPath { path: None, len: 4 },
     ];
     test.run_scripts(scripts);
 }
 
+#[test]
+fn node_insert_node_before_existing_nested_nodes_test() {
+    let mut test = NodeTest::new();
+    let node_data_1_1 = NodeDataBuilder::new("text_1_1").build();
+    let node_data_1_2 = NodeDataBuilder::new("text_1_2").build();
+    let node_data_1 = NodeDataBuilder::new("text_1")
+        .add_node(node_data_1_1.clone())
+        .add_node(node_data_1_2.clone())
+        .build();
+
+    let scripts = vec![
+        InsertNode {
+            path: 0.into(),
+            node_data: node_data_1,
+            rev_id: 1,
+        },
+        // 0:text_1
+        //      0:text_1_1
+        //      1:text_1_2
+        InsertNode {
+            path: 0.into(),
+            node_data: NodeDataBuilder::new("text_0").build(),
+            rev_id: 2,
+        },
+        // 0:text_0
+        // 1:text_1
+        //      0:text_1_1
+        //      1:text_1_2
+        AssertNode {
+            path: vec![1, 0].into(),
+            expected: Some(node_data_1_1.into()),
+        },
+        AssertNode {
+            path: vec![1, 1].into(),
+            expected: Some(node_data_1_2.into()),
+        },
+    ];
+    test.run_scripts(scripts);
+}
 #[test]
 fn node_insert_with_attributes_test() {
     let mut test = NodeTest::new();
@@ -149,13 +251,14 @@ fn node_insert_with_attributes_test() {
     let scripts = vec![
         InsertNode {
             path: path.clone(),
-            node: inserted_node.clone(),
+            node_data: inserted_node.clone(),
+            rev_id: 1,
         },
         UpdateAttributes {
             path: path.clone(),
             attributes: inserted_node.attributes.clone(),
         },
-        AssertNode {
+        AssertNodeData {
             path,
             expected: Some(inserted_node),
         },
@@ -172,10 +275,14 @@ fn node_delete_test() {
     let scripts = vec![
         InsertNode {
             path: path.clone(),
-            node: inserted_node,
+            node_data: inserted_node,
+            rev_id: 1,
+        },
+        DeleteNode {
+            path: path.clone(),
+            rev_id: 2,
         },
-        DeleteNode { path: path.clone() },
-        AssertNode { path, expected: None },
+        AssertNodeData { path, expected: None },
     ];
     test.run_scripts(scripts);
 }
@@ -198,7 +305,8 @@ fn node_update_body_test() {
     let scripts = vec![
         InsertNode {
             path: path.clone(),
-            node,
+            node_data: node,
+            rev_id: 1,
         },
         UpdateBody {
             path: path.clone(),