Ver código fonte

Merge remote-tracking branch 'origin/main' into fix/click-selection-menu-item-delete-text

Lucas.Xu 2 anos atrás
pai
commit
d1d5b37c14
100 arquivos alterados com 1651 adições e 885 exclusões
  1. 1 1
      frontend/app_flowy/assets/translations/en.json
  2. 1 1
      frontend/app_flowy/assets/translations/es-VE.json
  3. 1 1
      frontend/app_flowy/assets/translations/fr-FR.json
  4. 1 1
      frontend/app_flowy/assets/translations/id-ID.json
  5. 1 1
      frontend/app_flowy/assets/translations/ja-JP.json
  6. 7 7
      frontend/app_flowy/assets/translations/pt-BR.json
  7. 4 4
      frontend/app_flowy/assets/translations/ru-RU.json
  8. 1 1
      frontend/app_flowy/assets/translations/zh-CN.json
  9. 1 1
      frontend/app_flowy/assets/translations/zh-TW.json
  10. 4 1
      frontend/app_flowy/lib/plugins/board/presentation/card/card.dart
  11. 1 4
      frontend/app_flowy/lib/plugins/doc/document.dart
  12. 16 15
      frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_controller.dart
  13. 35 0
      frontend/app_flowy/lib/plugins/grid/application/cell/select_option_editor_bloc.dart
  14. 8 18
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_cell.dart
  15. 104 53
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_editor.dart
  16. 2 2
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart
  17. 3 3
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/text_field.dart
  18. 5 6
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/url_cell.dart
  19. 1 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell.dart
  20. 20 14
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell_action_sheet.dart
  21. 9 17
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_editor.dart
  22. 1 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/grid_header.dart
  23. 1 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart
  24. 1 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart
  25. 1 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_toolbar.dart
  26. 1 2
      frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/add_button.dart
  27. 5 4
      frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/header.dart
  28. 8 4
      frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/right_click_action.dart
  29. 2 2
      frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/disclosure_action.dart
  30. 4 4
      frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/item.dart
  31. 15 10
      frontend/app_flowy/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart
  32. 26 25
      frontend/app_flowy/lib/workspace/presentation/widgets/pop_up_action.dart
  33. 5 0
      frontend/app_flowy/packages/appflowy_editor/example/.firebaserc
  34. 23 0
      frontend/app_flowy/packages/appflowy_editor/example/firebase.json
  35. 51 22
      frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart
  36. 0 165
      frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/image_node_widget.dart
  37. 0 100
      frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/youtube_link_node_widget.dart
  38. 0 2
      frontend/app_flowy/packages/appflowy_editor/example/macos/Flutter/GeneratedPluginRegistrant.swift
  39. 0 6
      frontend/app_flowy/packages/appflowy_editor/example/macos/Podfile.lock
  40. 2 2
      frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml
  41. 36 31
      frontend/app_flowy/packages/appflowy_editor/lib/src/document/node.dart
  42. 2 4
      frontend/app_flowy/packages/appflowy_editor/lib/src/document/selection.dart
  43. 30 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/path_extensions.dart
  44. 46 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/infra/infra.dart
  45. 2 2
      frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart
  46. 56 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/built_in_text_widget.dart
  47. 6 2
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart
  48. 6 46
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart
  49. 6 2
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart
  50. 6 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart
  51. 22 6
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart
  52. 41 26
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart
  53. 25 22
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart
  54. 34 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/tab_handler.dart
  55. 8 3
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/keyboard_service.dart
  56. 24 9
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart
  57. 6 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart
  58. 5 1
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/shortcut_event.dart
  59. 153 0
      frontend/app_flowy/packages/appflowy_editor/test/document/node_test.dart
  60. 38 0
      frontend/app_flowy/packages/appflowy_editor/test/extensions/path_extensions_test.dart
  61. 51 0
      frontend/app_flowy/packages/appflowy_editor/test/infra/infra_test.dart
  62. 1 0
      frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart
  63. 0 49
      frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_item_widget_test.dart
  64. 0 1
      frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart
  65. 137 1
      frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart
  66. 151 0
      frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/tab_handler_test.dart
  67. 2 3
      frontend/app_flowy/packages/appflowy_popover/lib/src/popover.dart
  68. 13 7
      frontend/app_flowy/packages/flowy_infra/lib/theme.dart
  69. 2 2
      frontend/app_flowy/packages/flowy_infra_ui/example/lib/overlay/overlay_screen.dart
  70. 1 1
      frontend/app_flowy/packages/flowy_infra_ui/lib/flowy_infra_ui.dart
  71. 40 4
      frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart
  72. 0 1
      frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_overlay.dart
  73. 72 28
      frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/list_overlay.dart
  74. 0 34
      frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/overlay_container.dart
  75. 7 2
      frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/button.dart
  76. 11 3
      frontend/rust-lib/flowy-folder/src/manager.rs
  77. 21 6
      frontend/rust-lib/flowy-folder/src/services/view/controller.rs
  78. 0 5
      frontend/rust-lib/flowy-grid/src/dart_notification.rs
  79. 1 1
      frontend/rust-lib/flowy-grid/src/event_handler.rs
  80. 6 1
      frontend/rust-lib/flowy-grid/src/macros.rs
  81. 16 3
      frontend/rust-lib/flowy-grid/src/manager.rs
  82. 4 1
      frontend/rust-lib/flowy-grid/src/services/cell/any_cell_data.rs
  83. 21 12
      frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs
  84. 1 1
      frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_tests.rs
  85. 1 1
      frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_type_option_entities.rs
  86. 12 7
      frontend/rust-lib/flowy-grid/src/services/grid_editor.rs
  87. 14 7
      frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs
  88. 14 2
      frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs
  89. 1 0
      frontend/rust-lib/flowy-grid/src/services/group/action.rs
  90. 10 4
      frontend/rust-lib/flowy-grid/src/services/group/controller.rs
  91. 2 2
      frontend/rust-lib/flowy-grid/src/services/group/controller_impls/default_controller.rs
  92. 38 10
      frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/util.rs
  93. 12 10
      frontend/rust-lib/flowy-grid/src/services/group/group_util.rs
  94. 1 0
      frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs
  95. 2 5
      frontend/rust-lib/flowy-grid/tests/grid/group_test/test.rs
  96. 40 9
      frontend/rust-lib/flowy-sdk/src/deps_resolve/folder_deps.rs
  97. 0 2
      frontend/scripts/install_dev_env/install_macos.sh
  98. 10 3
      shared-lib/flowy-grid-data-model/src/revision/grid_rev.rs
  99. 6 2
      shared-lib/flowy-grid-data-model/src/revision/grid_view.rs
  100. 6 2
      shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs

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

@@ -181,7 +181,7 @@
       "includeTime": " Include time",
       "dateFormatFriendly": "Month Day,Year",
       "dateFormatISO": "Year-Month-Day",
-      "dateFormatLocal": "Year/Month/Day",
+      "dateFormatLocal": "Month/Day/Year",
       "dateFormatUS": "Year/Month/Day",
       "timeFormat": " Time format",
       "invalidTimeFormat": "Invalid format",

+ 1 - 1
frontend/app_flowy/assets/translations/es-VE.json

@@ -172,7 +172,7 @@
       "includeTime": " Incluir tiempo",
       "dateFormatFriendly": "Mes Día, Año",
       "dateFormatISO": "Año-Mes-Día",
-      "dateFormatLocal": "Año/Mes/Día",
+      "dateFormatLocal": "Mes/Día/Año",
       "dateFormatUS": "Año/Mes/Día",
       "timeFormat": " Time format",
       "invalidTimeFormat": "Formato de tiempo",

+ 1 - 1
frontend/app_flowy/assets/translations/fr-FR.json

@@ -170,7 +170,7 @@
       "includeTime": " Inclure l'heure",
       "dateFormatFriendly": "Mois Jour, Année",
       "dateFormatISO": "Année-Mois-Jour",
-      "dateFormatLocal": "Année/Mois/Jour",
+      "dateFormatLocal": "Mois/Jour/Année",
       "dateFormatUS": "Année/Mois/Jour",
       "timeFormat": " Format du temps",
       "invalidTimeFormat": "Format invalide",

+ 1 - 1
frontend/app_flowy/assets/translations/id-ID.json

@@ -173,7 +173,7 @@
       "includeTime": " Sertakan waktu",
       "dateFormatFriendly": "Bulan Hari,Tahun",
       "dateFormatISO": "Tahun-Bulan-Hari",
-      "dateFormatLocal": "Tahun/Bulan/Hari",
+      "dateFormatLocal": "Bulan/Hari/Tahun",
       "dateFormatUS": "Tahun/Bulan/Hari",
       "timeFormat": " Format waktu",
       "invalidTimeFormat": "Format yang tidak valid",

+ 1 - 1
frontend/app_flowy/assets/translations/ja-JP.json

@@ -165,7 +165,7 @@
       "includeTime": " 時刻を含める",
       "dateFormatFriendly": "月 日,年",
       "dateFormatISO": "年-月-日",
-      "dateFormatLocal": "年/月/日",
+      "dateFormatLocal": "月/日/年",
       "dateFormatUS": "年/月/日",
       "timeFormat": " 時刻書式",
       "timeFormatTwelveHour": "12 時間表記",

+ 7 - 7
frontend/app_flowy/assets/translations/pt-BR.json

@@ -63,13 +63,13 @@
     "deletePermanent": "Apagar permanentemente"
   },
   "dialogCreatePageNameHint": "Nome da página",
-    "questionBubble": {
+  "questionBubble": {
     "whatsNew": "O que há de novo?",
     "help": "Ajuda e Suporte",
     "debug": {
-    "name": "Informação de depuração",
-    "success": "Informação de depuração copiada para a área de transferência!",
-    "fail": "Falha ao copiar a informação de depuração para a área de transferência"
+      "name": "Informação de depuração",
+      "success": "Informação de depuração copiada para a área de transferência!",
+      "fail": "Falha ao copiar a informação de depuração para a área de transferência"
     }
   },
   "menuAppHeader": {
@@ -148,7 +148,7 @@
     "menu": {
       "appearance": "Aparência",
       "language": "Idioma",
-      "user":"Usuário",
+      "user": "Usuário",
       "open": "Abrir as Configurações"
     },
     "appearance": {
@@ -181,7 +181,7 @@
       "includeTime": "Incluir horário",
       "dateFormatFriendly": "Mês/Dia/Ano",
       "dateFormatISO": "Ano/Mês/Dia",
-      "dateFormatLocal": "Ano/Mês/Dia",
+      "dateFormatLocal": "Mês/Dia/Ano",
       "dateFormatUS": "Ano/Mês/Dia",
       "timeFormat": "Formato de hora",
       "invalidTimeFormat": "Formato Inválido",
@@ -231,4 +231,4 @@
       "create_new_card": "Novo"
     }
   }
-}
+}

+ 4 - 4
frontend/app_flowy/assets/translations/ru-RU.json

@@ -94,9 +94,9 @@
   },
   "tooltip": {
     "darkMode": "Переключиться в тёмную тему",
-     "openAsPage": "Открыть как страницу",
-     "addNewRow": "Добавить новую строку",
-     "openMenu": "Открыть меню"
+    "openAsPage": "Открыть как страницу",
+    "addNewRow": "Добавить новую строку",
+    "openMenu": "Открыть меню"
   },
   "sideBar": {
     "closeSidebar": "Закрыть боковое меню",
@@ -180,7 +180,7 @@
       "includeTime": " Время",
       "dateFormatFriendly": "День Месяц, Год",
       "dateFormatISO": "Год-Месяц-День",
-      "dateFormatLocal": "Год/Месяц/День",
+      "dateFormatLocal": "Месяц/День/Год",
       "dateFormatUS": "Год/Месяц/День",
       "timeFormat": " Форматировать время",
       "invalidTimeFormat": "Неверный формат",

+ 1 - 1
frontend/app_flowy/assets/translations/zh-CN.json

@@ -177,7 +177,7 @@
       "includeTime": " 包含时间",
       "dateFormatFriendly": "月 日,年",
       "dateFormatISO": "年-月-日",
-      "dateFormatLocal": "年/月/日",
+      "dateFormatLocal": "月/日/年",
       "dateFormatUS": "年/月/日",
       "timeFormat": " 时间格式",
       "invalidTimeFormat": "时间格式错误",

+ 1 - 1
frontend/app_flowy/assets/translations/zh-TW.json

@@ -173,7 +173,7 @@
       "includeTime": " 包含時間",
       "dateFormatFriendly": "月 日,年",
       "dateFormatISO": "年-月-日",
-      "dateFormatLocal": "年/月/日",
+      "dateFormatLocal": "月/日/年",
       "dateFormatUS": "年/月/日",
       "timeFormat": " 時間格式",
       "invalidTimeFormat": "格式無效",

+ 4 - 1
frontend/app_flowy/lib/plugins/board/presentation/card/card.dart

@@ -83,7 +83,9 @@ class _BoardCardState extends State<BoardCard> {
         builder: (context, state) {
           return AppFlowyPopover(
             controller: popoverController,
+            triggerActions: PopoverTriggerFlags.none,
             constraints: BoxConstraints.loose(const Size(140, 200)),
+            margin: const EdgeInsets.all(6),
             direction: PopoverDirection.rightWithCenterAligned,
             popupBuilder: (popoverContext) => _handlePopoverBuilder(
               context,
@@ -132,7 +134,8 @@ class _BoardCardState extends State<BoardCard> {
         throw UnimplementedError();
       case AccessoryType.more:
         return GridRowActionSheet(
-            rowData: context.read<BoardCardBloc>().rowInfo());
+          rowData: context.read<BoardCardBloc>().rowInfo(),
+        );
     }
   }
 

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

@@ -195,9 +195,6 @@ class ShareActions with ActionList<ShareActionWrapper>, FlowyOverlayDelegate {
 
   ShareActions({required this.onSelected});
 
-  @override
-  double get maxWidth => 130;
-
   @override
   double get itemHeight => 22;
 
@@ -233,7 +230,7 @@ class ShareActionWrapper extends ActionItem {
   ShareActionWrapper(this.inner);
 
   @override
-  Widget? get icon => null;
+  Widget? icon(Color iconColor) => null;
 
   @override
   String get name => inner.name;

+ 16 - 15
frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_controller.dart

@@ -149,18 +149,13 @@ class IGridCellController<T, D> extends Equatable {
         _cellDataPersistence = cellDataPersistence,
         _fieldNotifier = fieldNotifier,
         _fieldService = FieldService(
-            gridId: cellId.gridId, fieldId: cellId.fieldContext.id),
+          gridId: cellId.gridId,
+          fieldId: cellId.fieldContext.id,
+        ),
         _cacheKey = GridCellCacheKey(
-            rowId: cellId.rowId, fieldId: cellId.fieldContext.id);
-
-  IGridCellController<T, D> clone() {
-    return IGridCellController(
-        cellId: cellId,
-        cellDataLoader: _cellDataLoader,
-        cellCache: _cellsCache,
-        fieldNotifier: _fieldNotifier,
-        cellDataPersistence: _cellDataPersistence);
-  }
+          rowId: cellId.rowId,
+          fieldId: cellId.fieldContext.id,
+        );
 
   String get gridId => cellId.gridId;
 
@@ -172,9 +167,10 @@ class IGridCellController<T, D> extends Equatable {
 
   FieldType get fieldType => cellId.fieldContext.fieldType;
 
-  VoidCallback? startListening(
-      {required void Function(T?) onCellChanged,
-      VoidCallback? onCellFieldChanged}) {
+  VoidCallback? startListening({
+    required void Function(T?) onCellChanged,
+    VoidCallback? onCellFieldChanged,
+  }) {
     if (isListening) {
       Log.error("Already started. It seems like you should call clone first");
       return null;
@@ -283,7 +279,12 @@ class IGridCellController<T, D> extends Equatable {
     _loadDataOperation?.cancel();
     _loadDataOperation = Timer(const Duration(milliseconds: 10), () {
       _cellDataLoader.loadData().then((data) {
-        _cellsCache.insert(_cacheKey, GridCell(object: data));
+        if (data != null) {
+          _cellsCache.insert(_cacheKey, GridCell(object: data));
+        } else {
+          _cellsCache.remove(_cacheKey);
+        }
+
         _cellDataNotifier?.value = data;
       });
     });

+ 35 - 0
frontend/app_flowy/lib/plugins/grid/application/cell/select_option_editor_bloc.dart

@@ -54,6 +54,9 @@ class SelectOptionCellEditorBloc
           selectOption: (_SelectOption value) {
             _onSelectOption(value.optionId);
           },
+          trySelectOption: (_TrySelectOption value) {
+            _trySelectOption(value.optionName, emit);
+          },
           filterOption: (_SelectOptionFilter value) {
             _filterOption(value.optionName, emit);
           },
@@ -100,6 +103,36 @@ class SelectOptionCellEditorBloc
     }
   }
 
+  void _trySelectOption(
+      String optionName, Emitter<SelectOptionEditorState> emit) async {
+    SelectOptionPB? matchingOption;
+    bool optionExistsButSelected = false;
+
+    for (final option in state.options) {
+      if (option.name.toLowerCase() == optionName.toLowerCase()) {
+        if (!state.selectedOptions.contains(option)) {
+          matchingOption = option;
+          break;
+        } else {
+          optionExistsButSelected = true;
+        }
+      }
+    }
+
+    // if there isn't a matching option at all, then create it
+    if (matchingOption == null && !optionExistsButSelected) {
+      _createOption(optionName);
+    }
+
+    // if there is an unselected matching option, select it
+    if (matchingOption != null) {
+      _selectOptionService.select(optionId: matchingOption.id);
+    }
+
+    // clear the filter
+    emit(state.copyWith(filter: none()));
+  }
+
   void _filterOption(String optionName, Emitter<SelectOptionEditorState> emit) {
     final _MakeOptionResult result =
         _makeOptions(Some(optionName), state.allOptions);
@@ -187,6 +220,8 @@ class SelectOptionEditorEvent with _$SelectOptionEditorEvent {
       _DeleteOption;
   const factory SelectOptionEditorEvent.filterOption(String optionName) =
       _SelectOptionFilter;
+  const factory SelectOptionEditorEvent.trySelectOption(String optionName) =
+      _TrySelectOption;
 }
 
 @freezed

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

@@ -65,30 +65,24 @@ class _DateCellState extends GridCellState<GridDateCell> {
         builder: (context, state) {
           return AppFlowyPopover(
             controller: _popover,
-            offset: const Offset(0, 20),
+            triggerActions: PopoverTriggerFlags.none,
             direction: PopoverDirection.bottomWithLeftAligned,
             constraints: BoxConstraints.loose(const Size(320, 500)),
+            margin: EdgeInsets.zero,
             child: SizedBox.expand(
               child: GestureDetector(
                 behavior: HitTestBehavior.opaque,
-                onTap: () => _showCalendar(context),
-                child: MouseRegion(
-                  opaque: false,
-                  cursor: SystemMouseCursors.click,
-                  child: Align(
-                    alignment: alignment,
-                    child: FlowyText.medium(
-                      state.dateStr,
-                      fontSize: 12,
-                    ),
-                  ),
+                onTap: () => _popover.show(),
+                child: Align(
+                  alignment: alignment,
+                  child: FlowyText.medium(state.dateStr, fontSize: 12),
                 ),
               ),
             ),
             popupBuilder: (BuildContext popoverContent) {
-              final bloc = context.read<DateCellBloc>();
               return DateCellEditor(
-                cellController: bloc.cellController.clone(),
+                cellController: widget.cellControllerBuilder.build()
+                    as GridDateCellController,
                 onDismissed: () => widget.onCellEditing.value = false,
               );
             },
@@ -101,10 +95,6 @@ class _DateCellState extends GridCellState<GridDateCell> {
     );
   }
 
-  void _showCalendar(BuildContext context) {
-    _popover.show();
-  }
-
   @override
   Future<void> dispose() async {
     _cellBloc.close();

+ 104 - 53
frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_editor.dart

@@ -2,6 +2,7 @@ import 'package:app_flowy/generated/locale_keys.g.dart';
 import 'package:app_flowy/plugins/grid/application/cell/date_cal_bloc.dart';
 import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
+import 'package:dartz/dartz.dart' show Either;
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra/theme.dart';
@@ -11,6 +12,7 @@ import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flowy_infra_ui/widget/rounded_input_field.dart';
 import 'package:flowy_infra_ui/widget/spacing.dart';
 import 'package:flowy_sdk/log.dart';
+import 'package:flowy_sdk/protobuf/flowy-error/errors.pbserver.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
@@ -39,39 +41,42 @@ class DateCellEditor extends StatefulWidget {
 }
 
 class _DateCellEditor extends State<DateCellEditor> {
-  DateTypeOptionPB? _dateTypeOptionPB;
-
-  @override
-  void initState() {
-    super.initState();
-    _fetchData();
-  }
-
-  _fetchData() async {
-    final result = await widget.cellController
-        .getFieldTypeOption(DateTypeOptionDataParser());
-
-    result.fold((dateTypeOptionPB) {
-      setState(() {
-        _dateTypeOptionPB = dateTypeOptionPB;
-      });
-    }, (err) => Log.error(err));
-  }
-
   @override
   Widget build(BuildContext context) {
-    if (_dateTypeOptionPB == null) {
-      return Container();
-    }
+    return FutureBuilder<Either<dynamic, FlowyError>>(
+      future: widget.cellController.getFieldTypeOption(
+        DateTypeOptionDataParser(),
+      ),
+      builder: (BuildContext context, snapshot) {
+        if (snapshot.hasData) {
+          return _buildWidget(snapshot);
+        } else {
+          return const SizedBox();
+        }
+      },
+    );
+  }
 
-    return _CellCalendarWidget(
-      cellContext: widget.cellController,
-      dateTypeOptionPB: _dateTypeOptionPB!,
+  Widget _buildWidget(AsyncSnapshot<Either<dynamic, FlowyError>> snapshot) {
+    return snapshot.data!.fold(
+      (dateTypeOptionPB) {
+        return Padding(
+          padding: const EdgeInsets.all(12),
+          child: _CellCalendarWidget(
+            cellContext: widget.cellController,
+            dateTypeOptionPB: dateTypeOptionPB,
+          ),
+        );
+      },
+      (err) {
+        Log.error(err);
+        return const SizedBox();
+      },
     );
   }
 }
 
-class _CellCalendarWidget extends StatelessWidget {
+class _CellCalendarWidget extends StatefulWidget {
   final GridDateCellController cellContext;
   final DateTypeOptionPB dateTypeOptionPB;
 
@@ -81,26 +86,43 @@ class _CellCalendarWidget extends StatelessWidget {
     Key? key,
   }) : super(key: key);
 
+  @override
+  State<_CellCalendarWidget> createState() => _CellCalendarWidgetState();
+}
+
+class _CellCalendarWidgetState extends State<_CellCalendarWidget> {
+  late PopoverMutex popoverMutex;
+  late DateCalBloc bloc;
+
+  @override
+  void initState() {
+    popoverMutex = PopoverMutex();
+
+    bloc = DateCalBloc(
+      dateTypeOptionPB: widget.dateTypeOptionPB,
+      cellData: widget.cellContext.getCellData(),
+      cellController: widget.cellContext,
+    )..add(const DateCalEvent.initial());
+    super.initState();
+  }
+
   @override
   Widget build(BuildContext context) {
     final theme = context.watch<AppTheme>();
-    return BlocProvider(
-      create: (context) {
-        return DateCalBloc(
-          dateTypeOptionPB: dateTypeOptionPB,
-          cellData: cellContext.getCellData(),
-          cellController: cellContext,
-        )..add(const DateCalEvent.initial());
-      },
+    return BlocProvider.value(
+      value: bloc,
       child: BlocBuilder<DateCalBloc, DateCalState>(
         buildWhen: (p, c) => false,
         builder: (context, state) {
           List<Widget> children = [
             _buildCalendar(theme, context),
-            _TimeTextField(bloc: context.read<DateCalBloc>()),
+            _TimeTextField(
+              bloc: context.read<DateCalBloc>(),
+              popoverMutex: popoverMutex,
+            ),
             Divider(height: 1, color: theme.shader5),
             const _IncludeTimeButton(),
-            const _DateTypeOptionButton()
+            _DateTypeOptionButton(popoverMutex: popoverMutex)
           ];
 
           return ListView.separated(
@@ -119,6 +141,13 @@ class _CellCalendarWidget extends StatelessWidget {
     );
   }
 
+  @override
+  void dispose() {
+    bloc.close();
+    popoverMutex.dispose();
+    super.dispose();
+  }
+
   Widget _buildCalendar(AppTheme theme, BuildContext context) {
     return BlocBuilder<DateCalBloc, DateCalState>(
       builder: (context, state) {
@@ -218,8 +247,10 @@ class _IncludeTimeButton extends StatelessWidget {
 
 class _TimeTextField extends StatefulWidget {
   final DateCalBloc bloc;
+  final PopoverMutex popoverMutex;
   const _TimeTextField({
     required this.bloc,
+    required this.popoverMutex,
     Key? key,
   }) : super(key: key);
 
@@ -240,9 +271,18 @@ class _TimeTextFieldState extends State<_TimeTextField> {
         if (mounted) {
           widget.bloc.add(DateCalEvent.setTime(_controller.text));
         }
+
+        if (_focusNode.hasFocus) {
+          widget.popoverMutex.close();
+        }
       });
-    }
 
+      widget.popoverMutex.listenOnPopoverChanged(() {
+        if (_focusNode.hasFocus) {
+          _focusNode.unfocus();
+        }
+      });
+    }
     super.initState();
   }
 
@@ -290,7 +330,11 @@ class _TimeTextFieldState extends State<_TimeTextField> {
 }
 
 class _DateTypeOptionButton extends StatelessWidget {
-  const _DateTypeOptionButton({Key? key}) : super(key: key);
+  final PopoverMutex popoverMutex;
+  const _DateTypeOptionButton({
+    required this.popoverMutex,
+    Key? key,
+  }) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
@@ -301,6 +345,7 @@ class _DateTypeOptionButton extends StatelessWidget {
       selector: (state) => state.dateTypeOptionPB,
       builder: (context, dateTypeOptionPB) {
         return AppFlowyPopover(
+          mutex: popoverMutex,
           triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
           offset: const Offset(20, 0),
           constraints: BoxConstraints.loose(const Size(140, 100)),
@@ -313,7 +358,10 @@ class _DateTypeOptionButton extends StatelessWidget {
           popupBuilder: (BuildContext popContext) {
             return _CalDateTimeSetting(
               dateTypeOptionPB: dateTypeOptionPB,
-              onEvent: (event) => context.read<DateCalBloc>().add(event),
+              onEvent: (event) {
+                context.read<DateCalBloc>().add(event);
+                popoverMutex.close();
+              },
             );
           },
         );
@@ -325,46 +373,49 @@ class _DateTypeOptionButton extends StatelessWidget {
 class _CalDateTimeSetting extends StatefulWidget {
   final DateTypeOptionPB dateTypeOptionPB;
   final Function(DateCalEvent) onEvent;
-  const _CalDateTimeSetting(
-      {required this.dateTypeOptionPB, required this.onEvent, Key? key})
-      : super(key: key);
+  const _CalDateTimeSetting({
+    required this.dateTypeOptionPB,
+    required this.onEvent,
+    Key? key,
+  }) : super(key: key);
 
   @override
   State<_CalDateTimeSetting> createState() => _CalDateTimeSettingState();
 }
 
 class _CalDateTimeSettingState extends State<_CalDateTimeSetting> {
+  final timeSettingPopoverMutex = PopoverMutex();
   String? overlayIdentifier;
-  final _popoverMutex = PopoverMutex();
 
   @override
   Widget build(BuildContext context) {
     List<Widget> children = [
       AppFlowyPopover(
-        mutex: _popoverMutex,
-        asBarrier: true,
+        mutex: timeSettingPopoverMutex,
         triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
         offset: const Offset(20, 0),
         popupBuilder: (BuildContext context) {
           return DateFormatList(
             selectedFormat: widget.dateTypeOptionPB.dateFormat,
-            onSelected: (format) =>
-                widget.onEvent(DateCalEvent.setDateFormat(format)),
+            onSelected: (format) {
+              widget.onEvent(DateCalEvent.setDateFormat(format));
+              timeSettingPopoverMutex.close();
+            },
           );
         },
         child: const DateFormatButton(),
       ),
       AppFlowyPopover(
-        mutex: _popoverMutex,
-        asBarrier: true,
+        mutex: timeSettingPopoverMutex,
         triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
         offset: const Offset(20, 0),
         popupBuilder: (BuildContext context) {
           return TimeFormatList(
-            selectedFormat: widget.dateTypeOptionPB.timeFormat,
-            onSelected: (format) =>
-                widget.onEvent(DateCalEvent.setTimeFormat(format)),
-          );
+              selectedFormat: widget.dateTypeOptionPB.timeFormat,
+              onSelected: (format) {
+                widget.onEvent(DateCalEvent.setTimeFormat(format));
+                timeSettingPopoverMutex.close();
+              });
         },
         child: TimeFormatButton(timeFormat: widget.dateTypeOptionPB.timeFormat),
       ),

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

@@ -154,10 +154,10 @@ class _TextField extends StatelessWidget {
                   .read<SelectOptionCellEditorBloc>()
                   .add(SelectOptionEditorEvent.filterOption(text));
             },
-            onNewTag: (tagName) {
+            onSubmitted: (tagName) {
               context
                   .read<SelectOptionCellEditorBloc>()
-                  .add(SelectOptionEditorEvent.newOption(tagName));
+                  .add(SelectOptionEditorEvent.trySelectOption(tagName));
             },
           ),
         );

+ 3 - 3
frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/text_field.dart

@@ -17,7 +17,7 @@ class SelectOptionTextField extends StatefulWidget {
   final LinkedHashMap<String, SelectOptionPB> selectedOptionMap;
   final double distanceToText;
 
-  final Function(String) onNewTag;
+  final Function(String) onSubmitted;
   final Function(String) newText;
   final VoidCallback? onClick;
 
@@ -26,7 +26,7 @@ class SelectOptionTextField extends StatefulWidget {
     required this.selectedOptionMap,
     required this.distanceToText,
     required this.tagController,
-    required this.onNewTag,
+    required this.onSubmitted,
     required this.newText,
     this.onClick,
     TextEditingController? textController,
@@ -88,7 +88,7 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
               }
 
               if (text.isNotEmpty) {
-                widget.onNewTag(text);
+                widget.onSubmitted(text);
                 focusNode.requestFocus();
               }
             },

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

@@ -54,13 +54,11 @@ class GridURLCell extends GridCellWidget {
       GridURLCellAccessoryType ty, GridCellAccessoryBuildContext buildContext) {
     switch (ty) {
       case GridURLCellAccessoryType.edit:
-        final cellController =
-            cellControllerBuilder.build() as GridURLCellController;
         return GridCellAccessoryBuilder(
           builder: (Key key) => _EditURLAccessory(
             key: key,
-            cellContext: cellController,
             anchorContext: buildContext.anchorContext,
+            cellControllerBuilder: cellControllerBuilder,
           ),
         );
 
@@ -191,10 +189,10 @@ class _GridURLCellState extends GridCellState<GridURLCell> {
 }
 
 class _EditURLAccessory extends StatefulWidget {
-  final GridURLCellController cellContext;
+  final GridCellControllerBuilder cellControllerBuilder;
   final BuildContext anchorContext;
   const _EditURLAccessory({
-    required this.cellContext,
+    required this.cellControllerBuilder,
     required this.anchorContext,
     Key? key,
   }) : super(key: key);
@@ -224,7 +222,8 @@ class _EditURLAccessoryState extends State<_EditURLAccessory>
       child: svgWidget("editor/edit", color: theme.iconColor),
       popupBuilder: (BuildContext popoverContext) {
         return URLEditorPopover(
-          cellController: widget.cellContext.clone(),
+          cellController:
+              widget.cellControllerBuilder.build() as GridURLCellController,
         );
       },
     );

+ 1 - 0
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell.dart

@@ -162,6 +162,7 @@ class FieldCellButton extends StatelessWidget {
   Widget build(BuildContext context) {
     final theme = context.watch<AppTheme>();
     return FlowyButton(
+      radius: BorderRadius.zero,
       hoverColor: theme.shader6,
       onTap: onTap,
       leftIcon: svgWidget(field.fieldType.iconName(), color: theme.iconColor),

+ 20 - 14
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell_action_sheet.dart

@@ -93,9 +93,9 @@ class _EditFieldButton extends StatelessWidget {
 }
 
 class _FieldOperationList extends StatelessWidget {
-  final GridFieldCellContext fieldData;
+  final GridFieldCellContext fieldContext;
   final VoidCallback onDismissed;
-  const _FieldOperationList(this.fieldData, this.onDismissed, {Key? key})
+  const _FieldOperationList(this.fieldContext, this.onDismissed, {Key? key})
       : super(key: key);
 
   @override
@@ -118,14 +118,14 @@ class _FieldOperationList extends StatelessWidget {
         bool enable = true;
         switch (action) {
           case FieldAction.delete:
-            enable = !fieldData.field.isPrimary;
+            enable = !fieldContext.field.isPrimary;
             break;
           default:
             break;
         }
 
         return FieldActionCell(
-          fieldId: fieldData.field.id,
+          fieldContext: fieldContext,
           action: action,
           onTap: onDismissed,
           enable: enable,
@@ -136,13 +136,13 @@ class _FieldOperationList extends StatelessWidget {
 }
 
 class FieldActionCell extends StatelessWidget {
-  final String fieldId;
+  final GridFieldCellContext fieldContext;
   final VoidCallback onTap;
   final FieldAction action;
   final bool enable;
 
   const FieldActionCell({
-    required this.fieldId,
+    required this.fieldContext,
     required this.action,
     required this.onTap,
     required this.enable,
@@ -161,7 +161,7 @@ class FieldActionCell extends StatelessWidget {
       hoverColor: theme.hover,
       onTap: () {
         if (enable) {
-          action.run(context);
+          action.run(context, fieldContext);
           onTap();
         }
       },
@@ -202,7 +202,7 @@ extension _FieldActionExtension on FieldAction {
     }
   }
 
-  void run(BuildContext context) {
+  void run(BuildContext context, GridFieldCellContext fieldContext) {
     switch (this) {
       case FieldAction.hide:
         context
@@ -210,18 +210,24 @@ extension _FieldActionExtension on FieldAction {
             .add(const FieldActionSheetEvent.hideField());
         break;
       case FieldAction.duplicate:
-        context
-            .read<FieldActionSheetBloc>()
-            .add(const FieldActionSheetEvent.duplicateField());
+        PopoverContainer.of(context).close();
+
+        FieldService(
+          gridId: fieldContext.gridId,
+          fieldId: fieldContext.field.id,
+        ).duplicateField();
+
         break;
       case FieldAction.delete:
         PopoverContainer.of(context).close();
+
         NavigatorAlertDialog(
           title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
           confirm: () {
-            context
-                .read<FieldActionSheetBloc>()
-                .add(const FieldActionSheetEvent.deleteField());
+            FieldService(
+              gridId: fieldContext.gridId,
+              fieldId: fieldContext.field.id,
+            ).deleteField();
           },
         ).show(context);
 

+ 9 - 17
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_editor.dart

@@ -139,7 +139,6 @@ class _FieldNameTextField extends StatefulWidget {
 
 class _FieldNameTextFieldState extends State<_FieldNameTextField> {
   FocusNode focusNode = FocusNode();
-  VoidCallback? _popoverCallback;
   late TextEditingController controller;
 
   @override
@@ -151,6 +150,12 @@ class _FieldNameTextFieldState extends State<_FieldNameTextField> {
       }
     });
 
+    widget.popoverMutex.listenOnPopoverChanged(() {
+      if (focusNode.hasFocus) {
+        focusNode.unfocus();
+      }
+    });
+
     super.initState();
   }
 
@@ -176,8 +181,6 @@ class _FieldNameTextFieldState extends State<_FieldNameTextField> {
         buildWhen: (previous, current) =>
             previous.errorText != current.errorText,
         builder: (context, state) {
-          listenOnPopoverChanged(context);
-
           return RoundedInputField(
             height: 36,
             focusNode: focusNode,
@@ -198,18 +201,6 @@ class _FieldNameTextFieldState extends State<_FieldNameTextField> {
       ),
     );
   }
-
-  void listenOnPopoverChanged(BuildContext context) {
-    if (_popoverCallback != null) {
-      widget.popoverMutex.removePopoverListener(_popoverCallback!);
-    }
-    _popoverCallback = widget.popoverMutex.listenOnPopoverChanged(() {
-      if (focusNode.hasFocus) {
-        final node = FocusScope.of(context);
-        node.unfocus();
-      }
-    });
-  }
 }
 
 class _DeleteFieldButton extends StatelessWidget {
@@ -236,9 +227,10 @@ class _DeleteFieldButton extends StatelessWidget {
             color: enable ? null : theme.shader4,
           ),
           onTap: () => onDeleted?.call(),
+          hoverColor: theme.hover,
+          onHover: (_) => popoverMutex.close(),
         );
-        // if (enable) button = button;
-        return button;
+        return SizedBox(height: 36, child: button);
       },
     );
   }

+ 1 - 0
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/grid_header.dart

@@ -180,6 +180,7 @@ class CreateFieldButton extends StatelessWidget {
       asBarrier: true,
       constraints: BoxConstraints.loose(const Size(240, 600)),
       child: FlowyButton(
+        radius: BorderRadius.zero,
         text: FlowyText.medium(
           LocaleKeys.grid_field_newColumn.tr(),
           fontSize: 12,

+ 1 - 0
frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart

@@ -109,6 +109,7 @@ class _RowLeadingState extends State<_RowLeading> {
       triggerActions: PopoverTriggerFlags.none,
       constraints: BoxConstraints.loose(const Size(140, 200)),
       direction: PopoverDirection.rightWithCenterAligned,
+      margin: const EdgeInsets.all(6),
       popupBuilder: (BuildContext popoverContext) {
         return GridRowActionSheet(
             rowData: context.read<RowBloc>().state.rowInfo);

+ 1 - 0
frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart

@@ -280,6 +280,7 @@ class _RowDetailCellState extends State<_RowDetailCell> {
             AppFlowyPopover(
               controller: popover,
               constraints: BoxConstraints.loose(const Size(240, 600)),
+              triggerActions: PopoverTriggerFlags.none,
               popupBuilder: (popoverContext) => buildFieldEditor(),
               child: SizedBox(
                 width: 150,

+ 1 - 0
frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_toolbar.dart

@@ -55,6 +55,7 @@ class _SettingButton extends StatelessWidget {
     return AppFlowyPopover(
       constraints: BoxConstraints.loose(const Size(260, 400)),
       offset: const Offset(0, 10),
+      margin: const EdgeInsets.all(6),
       child: FlowyIconButton(
         width: 22,
         hoverColor: theme.hover,

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

@@ -61,8 +61,7 @@ class ActionList {
       itemBuilder: (context, index) => items[index],
       anchorContext: anchorContext,
       anchorDirection: AnchorDirection.bottomRight,
-      width: 120,
-      height: 80,
+      constraints: BoxConstraints.tight(const Size(120, 80)),
     );
   }
 }

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

@@ -86,7 +86,8 @@ class MenuAppHeader extends StatelessWidget {
               ?.toggle(),
           onSecondaryTap: () {
             final actionList = AppDisclosureActionSheet(
-                onSelected: (action) => _handleAction(context, action));
+              onSelected: (action) => _handleAction(context, action),
+            );
             actionList.show(
               context,
               anchorDirection: AnchorDirection.bottomWithCenterAligned,
@@ -158,12 +159,12 @@ extension AppDisclosureExtension on AppDisclosureAction {
     }
   }
 
-  Widget get icon {
+  Widget icon(Color iconColor) {
     switch (this) {
       case AppDisclosureAction.rename:
-        return svgWidget('editor/edit', color: const Color(0xffe5e5e5));
+        return svgWidget('editor/edit', color: iconColor);
       case AppDisclosureAction.delete:
-        return svgWidget('editor/delete', color: const Color(0xffe5e5e5));
+        return svgWidget('editor/delete', color: iconColor);
     }
   }
 }

+ 8 - 4
frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/right_click_action.dart

@@ -5,9 +5,12 @@ import 'package:flutter/material.dart';
 
 import 'header.dart';
 
-class AppDisclosureActionSheet with ActionList<DisclosureActionWrapper>, FlowyOverlayDelegate {
+class AppDisclosureActionSheet
+    with ActionList<DisclosureActionWrapper>, FlowyOverlayDelegate {
   final Function(dartz.Option<AppDisclosureAction>) onSelected;
-  final _items = AppDisclosureAction.values.map((action) => DisclosureActionWrapper(action)).toList();
+  final _items = AppDisclosureAction.values
+      .map((action) => DisclosureActionWrapper(action))
+      .toList();
 
   AppDisclosureActionSheet({
     required this.onSelected,
@@ -17,7 +20,8 @@ class AppDisclosureActionSheet with ActionList<DisclosureActionWrapper>, FlowyOv
   List<DisclosureActionWrapper> get items => _items;
 
   @override
-  void Function(dartz.Option<DisclosureActionWrapper> p1) get selectCallback => (result) {
+  void Function(dartz.Option<DisclosureActionWrapper> p1) get selectCallback =>
+      (result) {
         result.fold(
           () => onSelected(dartz.none()),
           (wrapper) => onSelected(
@@ -40,7 +44,7 @@ class DisclosureActionWrapper extends ActionItem {
 
   DisclosureActionWrapper(this.inner);
   @override
-  Widget? get icon => inner.icon;
+  Widget? icon(Color iconColor) => inner.icon(iconColor);
 
   @override
   String get name => inner.name;

+ 2 - 2
frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/disclosure_action.dart

@@ -80,7 +80,7 @@ class ViewDisclosureRegion extends StatelessWidget
   @override
   Widget build(BuildContext context) {
     return Listener(
-      onPointerDown: (event) => {_handleClick(event, context)},
+      onPointerDown: (event) => _handleClick(event, context),
       child: child,
     );
   }
@@ -123,7 +123,7 @@ class ViewDisclosureActionWrapper extends ActionItem {
 
   ViewDisclosureActionWrapper(this.inner);
   @override
-  Widget? get icon => inner.icon;
+  Widget? icon(Color iconColor) => inner.icon(iconColor);
 
   @override
   String get name => inner.name;

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

@@ -147,14 +147,14 @@ extension ViewDisclosureExtension on ViewDisclosureAction {
     }
   }
 
-  Widget get icon {
+  Widget icon(Color iconColor) {
     switch (this) {
       case ViewDisclosureAction.rename:
-        return svgWidget('editor/edit', color: const Color(0xff999999));
+        return svgWidget('editor/edit', color: iconColor);
       case ViewDisclosureAction.delete:
-        return svgWidget('editor/delete', color: const Color(0xff999999));
+        return svgWidget('editor/delete', color: iconColor);
       case ViewDisclosureAction.duplicate:
-        return svgWidget('editor/copy', color: const Color(0xff999999));
+        return svgWidget('editor/copy', color: iconColor);
     }
   }
 }

+ 15 - 10
frontend/app_flowy/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart

@@ -101,17 +101,16 @@ class _DebugToast {
   }
 }
 
-class QuestionBubbleActionSheet with ActionList<BubbleActionWrapper>, FlowyOverlayDelegate {
+class QuestionBubbleActionSheet
+    with ActionList<BubbleActionWrapper>, FlowyOverlayDelegate {
   final Function(dartz.Option<BubbleAction>) onSelected;
-  final _items = BubbleAction.values.map((action) => BubbleActionWrapper(action)).toList();
+  final _items =
+      BubbleAction.values.map((action) => BubbleActionWrapper(action)).toList();
 
   QuestionBubbleActionSheet({
     required this.onSelected,
   });
 
-  @override
-  double get maxWidth => 170;
-
   @override
   double get itemHeight => 22;
 
@@ -119,7 +118,8 @@ class QuestionBubbleActionSheet with ActionList<BubbleActionWrapper>, FlowyOverl
   List<BubbleActionWrapper> get items => _items;
 
   @override
-  void Function(dartz.Option<BubbleActionWrapper> p1) get selectCallback => (result) {
+  void Function(dartz.Option<BubbleActionWrapper> p1) get selectCallback =>
+      (result) {
         result.fold(
           () => onSelected(dartz.none()),
           (wrapper) => onSelected(
@@ -139,7 +139,7 @@ class QuestionBubbleActionSheet with ActionList<BubbleActionWrapper>, FlowyOverl
   @override
   ListOverlayFooter? get footer => ListOverlayFooter(
         widget: const FlowyVersionDescription(),
-        height: 30,
+        height: 40,
         padding: const EdgeInsets.only(top: 6),
       );
 }
@@ -156,7 +156,8 @@ class FlowyVersionDescription extends StatelessWidget {
       builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
         if (snapshot.connectionState == ConnectionState.done) {
           if (snapshot.hasError) {
-            return FlowyText("Error: ${snapshot.error}", fontSize: 12, color: theme.shader4);
+            return FlowyText("Error: ${snapshot.error}",
+                fontSize: 12, color: theme.shader4);
           }
 
           PackageInfo packageInfo = snapshot.data;
@@ -170,7 +171,11 @@ class FlowyVersionDescription extends StatelessWidget {
             children: [
               Divider(height: 1, color: theme.shader6, thickness: 1.0),
               const VSpace(6),
-              FlowyText("$appName $version.$buildNumber", fontSize: 12, color: theme.shader4),
+              FlowyText(
+                "$appName $version.$buildNumber",
+                fontSize: 12,
+                color: theme.shader4,
+              ),
             ],
           ).padding(
             horizontal: ActionListSizes.itemHPadding + ActionListSizes.padding,
@@ -190,7 +195,7 @@ class BubbleActionWrapper extends ActionItem {
 
   BubbleActionWrapper(this.inner);
   @override
-  Widget? get icon => inner.emoji;
+  Widget? icon(Color iconColor) => inner.emoji;
 
   @override
   String get name => inner.name;

+ 26 - 25
frontend/app_flowy/lib/workspace/presentation/widgets/pop_up_action.dart

@@ -13,7 +13,9 @@ abstract class ActionList<T extends ActionItem> {
 
   String get identifier => toString();
 
-  double get maxWidth => 162;
+  double get maxWidth => 300;
+
+  double get minWidth => 120;
 
   double get itemHeight => ActionListSizes.itemHeight;
 
@@ -29,28 +31,29 @@ abstract class ActionList<T extends ActionItem> {
     AnchorDirection anchorDirection = AnchorDirection.bottomRight,
     Offset? anchorOffset,
   }) {
-    final widgets = items
-        .map(
-          (action) => ActionCell<T>(
-            action: action,
-            itemHeight: itemHeight,
-            onSelected: (action) {
-              FlowyOverlay.of(buildContext).remove(identifier);
-              selectCallback(dartz.some(action));
-            },
-          ),
-        )
-        .toList();
-
     ListOverlay.showWithAnchor(
       buildContext,
       identifier: identifier,
-      itemCount: widgets.length,
-      itemBuilder: (context, index) => widgets[index],
+      itemCount: items.length,
+      itemBuilder: (context, index) {
+        final action = items[index];
+        return ActionCell<T>(
+          action: action,
+          itemHeight: itemHeight,
+          onSelected: (action) {
+            FlowyOverlay.of(buildContext).remove(identifier);
+            selectCallback(dartz.some(action));
+          },
+        );
+      },
       anchorContext: anchorContext ?? buildContext,
       anchorDirection: anchorDirection,
-      width: maxWidth,
-      height: widgets.length * (itemHeight + ActionListSizes.padding * 2),
+      constraints: BoxConstraints(
+        minHeight: items.length * (itemHeight + ActionListSizes.padding * 2),
+        maxHeight: items.length * (itemHeight + ActionListSizes.padding * 2),
+        maxWidth: maxWidth,
+        minWidth: minWidth,
+      ),
       delegate: delegate,
       anchorOffset: anchorOffset,
       footer: footer,
@@ -59,7 +62,7 @@ abstract class ActionList<T extends ActionItem> {
 }
 
 abstract class ActionItem {
-  Widget? get icon;
+  Widget? icon(Color iconColor);
   String get name;
 }
 
@@ -83,6 +86,7 @@ class ActionCell<T extends ActionItem> extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     final theme = context.watch<AppTheme>();
+    final icon = action.icon(theme.iconColor);
 
     return FlowyHover(
       style: HoverStyle(hoverColor: theme.hover),
@@ -92,14 +96,11 @@ class ActionCell<T extends ActionItem> extends StatelessWidget {
         child: SizedBox(
           height: itemHeight,
           child: Row(
-            crossAxisAlignment: CrossAxisAlignment.center,
+            crossAxisAlignment: CrossAxisAlignment.start,
             children: [
-              if (action.icon != null) action.icon!,
+              if (icon != null) icon,
               HSpace(ActionListSizes.itemHPadding),
-              FlowyText.medium(
-                action.name,
-                fontSize: 12,
-              ),
+              FlowyText.medium(action.name, fontSize: 12),
             ],
           ),
         ).padding(

+ 5 - 0
frontend/app_flowy/packages/appflowy_editor/example/.firebaserc

@@ -0,0 +1,5 @@
+{
+  "projects": {
+    "default": "appflowy-editor"
+  }
+}

+ 23 - 0
frontend/app_flowy/packages/appflowy_editor/example/firebase.json

@@ -0,0 +1,23 @@
+{
+  "hosting": {
+    "public": "build/web",
+    "ignore": [
+      "firebase.json",
+      "**/.*",
+      "**/node_modules/**"
+    ],
+    "rewrites": [
+      {
+        "source": "**",
+        "destination": "/index.html"
+      }
+    ],
+    "headers": [ {
+      "source": "**/*.@(png|jpg|jpeg|gif)",
+      "headers": [ {
+        "key": "Access-Control-Allow-Origin",
+        "value": "*"
+      } ]
+    } ]
+  }
+}

+ 51 - 22
frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart

@@ -1,13 +1,16 @@
 import 'dart:convert';
 import 'dart:io';
 
-import 'package:example/plugin/underscore_to_italic.dart';
+import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
+
+import 'package:example/plugin/underscore_to_italic.dart';
+import 'package:file_picker/file_picker.dart';
 import 'package:flutter_localizations/flutter_localizations.dart';
 import 'package:google_fonts/google_fonts.dart';
-
 import 'package:path_provider/path_provider.dart';
+import 'package:universal_html/html.dart' as html;
 
 import 'package:appflowy_editor/appflowy_editor.dart';
 
@@ -112,6 +115,7 @@ class _MyHomePageState extends State<MyHomePage> {
             child: AppFlowyEditor(
               editorState: _editorState!,
               editorStyle: _editorStyle,
+              editable: true,
               shortcutEvents: [
                 underscoreToItalic,
               ],
@@ -148,7 +152,7 @@ class _MyHomePageState extends State<MyHomePage> {
         ),
         ActionButton(
           icon: const Icon(Icons.import_export),
-          onPressed: () => _importDocument(),
+          onPressed: () async => await _importDocument(),
         ),
         ActionButton(
           icon: const Icon(Icons.color_lens),
@@ -167,28 +171,53 @@ class _MyHomePageState extends State<MyHomePage> {
   void _exportDocument(EditorState editorState) async {
     final document = editorState.document.toJson();
     final json = jsonEncode(document);
-    final directory = await getTemporaryDirectory();
-    final path = directory.path;
-    final file = File('$path/editor.json');
-    await file.writeAsString(json);
-
-    if (mounted) {
-      ScaffoldMessenger.of(context).showSnackBar(
-        SnackBar(
-          content: Text('The document is saved to the ${file.path}'),
-        ),
-      );
+    if (kIsWeb) {
+      final blob = html.Blob([json], 'text/plain', 'native');
+      html.AnchorElement(
+        href: html.Url.createObjectUrlFromBlob(blob).toString(),
+      )
+        ..setAttribute('download', 'editor.json')
+        ..click();
+    } else {
+      final directory = await getTemporaryDirectory();
+      final path = directory.path;
+      final file = File('$path/editor.json');
+      await file.writeAsString(json);
+
+      if (mounted) {
+        ScaffoldMessenger.of(context).showSnackBar(
+          SnackBar(
+            content: Text('The document is saved to the ${file.path}'),
+          ),
+        );
+      }
     }
   }
 
-  void _importDocument() async {
-    final directory = await getTemporaryDirectory();
-    final path = directory.path;
-    final file = File('$path/editor.json');
-    setState(() {
-      _editorState = null;
-      _jsonString = file.readAsString();
-    });
+  Future<void> _importDocument() async {
+    if (kIsWeb) {
+      final result = await FilePicker.platform.pickFiles(
+        allowMultiple: false,
+        allowedExtensions: ['json'],
+        type: FileType.custom,
+      );
+      final bytes = result?.files.first.bytes;
+      if (bytes != null) {
+        final jsonString = const Utf8Decoder().convert(bytes);
+        setState(() {
+          _editorState = null;
+          _jsonString = Future.value(jsonString);
+        });
+      }
+    } else {
+      final directory = await getTemporaryDirectory();
+      final path = '${directory.path}/editor.json';
+      final file = File(path);
+      setState(() {
+        _editorState = null;
+        _jsonString = file.readAsString();
+      });
+    }
   }
 
   void _switchToPage(int pageIndex) {

+ 0 - 165
frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/image_node_widget.dart

@@ -1,165 +0,0 @@
-import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:flutter/material.dart';
-
-/// 1. define your custom type in example.json
-///   For example I need to define an image plugin, then I define type equals
-///   "image", and add "image_src" into "attributes".
-///   {
-///     "type": "image",
-///     "attributes", { "image_src": "https://s1.ax1x.com/2022/07/28/vCgz1x.png" }
-///   }
-/// 2. create a class extends [NodeWidgetBuilder]
-/// 3. override the function `Widget build(NodeWidgetContext<Node> context)`
-///     and return a widget to render. The returned widget should be
-///     a StatefulWidget and mixin with [SelectableMixin].
-///
-/// 4. override the getter `nodeValidator`
-///     to verify the data structure in [Node].
-/// 5. register the plugin with `type` to `AppFlowyEditor` in `main.dart`.
-/// 6. Congratulations!
-
-class ImageNodeBuilder extends NodeWidgetBuilder<Node> {
-  @override
-  Widget build(NodeWidgetContext<Node> context) {
-    return ImageNodeWidget(
-      key: context.node.key,
-      node: context.node,
-      editorState: context.editorState,
-    );
-  }
-
-  @override
-  NodeValidator<Node> get nodeValidator => ((node) {
-        return node.type == 'image';
-      });
-}
-
-const double placeholderHeight = 132;
-
-class ImageNodeWidget extends StatefulWidget {
-  final Node node;
-  final EditorState editorState;
-
-  const ImageNodeWidget({
-    Key? key,
-    required this.node,
-    required this.editorState,
-  }) : super(key: key);
-
-  @override
-  State<ImageNodeWidget> createState() => _ImageNodeWidgetState();
-}
-
-class _ImageNodeWidgetState extends State<ImageNodeWidget>
-    with SelectableMixin {
-  bool isHovered = false;
-  Node get node => widget.node;
-  EditorState get editorState => widget.editorState;
-  String get src => widget.node.attributes['image_src'] as String;
-
-  @override
-  Position end() {
-    return Position(path: node.path, offset: 0);
-  }
-
-  @override
-  Position start() {
-    return Position(path: node.path, offset: 0);
-  }
-
-  @override
-  List<Rect> getRectsInSelection(Selection selection) {
-    return [];
-  }
-
-  @override
-  Selection getSelectionInRange(Offset start, Offset end) {
-    return Selection.collapsed(Position(path: node.path, offset: 0));
-  }
-
-  @override
-  Offset localToGlobal(Offset offset) {
-    throw UnimplementedError();
-  }
-
-  @override
-  Position getPositionInOffset(Offset start) {
-    return Position(path: node.path, offset: 0);
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    return _build(context);
-  }
-
-  Widget _loadingBuilder(
-      BuildContext context, Widget widget, ImageChunkEvent? evt) {
-    if (evt == null) {
-      return widget;
-    }
-    return Container(
-      alignment: Alignment.center,
-      height: placeholderHeight,
-      child: const Text("Loading..."),
-    );
-  }
-
-  Widget _errorBuilder(
-      BuildContext context, Object obj, StackTrace? stackTrace) {
-    return Container(
-      alignment: Alignment.center,
-      height: placeholderHeight,
-      child: const Text("Error..."),
-    );
-  }
-
-  Widget _frameBuilder(
-    BuildContext context,
-    Widget child,
-    int? frame,
-    bool wasSynchronouslyLoaded,
-  ) {
-    if (frame == null) {
-      return Container(
-        alignment: Alignment.center,
-        height: placeholderHeight,
-        child: const Text("Loading..."),
-      );
-    }
-
-    return child;
-  }
-
-  Widget _build(BuildContext context) {
-    return Column(
-      children: [
-        MouseRegion(
-            onEnter: (event) {
-              setState(() {
-                isHovered = true;
-              });
-            },
-            onExit: (event) {
-              setState(() {
-                isHovered = false;
-              });
-            },
-            child: Container(
-              clipBehavior: Clip.antiAlias,
-              decoration: BoxDecoration(
-                  border: Border.all(
-                    color: isHovered ? Colors.blue : Colors.grey,
-                  ),
-                  borderRadius: const BorderRadius.all(Radius.circular(20))),
-              child: Image.network(
-                src,
-                width: MediaQuery.of(context).size.width,
-                frameBuilder: _frameBuilder,
-                loadingBuilder: _loadingBuilder,
-                errorBuilder: _errorBuilder,
-              ),
-            )),
-      ],
-    );
-  }
-}

+ 0 - 100
frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/youtube_link_node_widget.dart

@@ -1,100 +0,0 @@
-import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:flutter/material.dart';
-import 'package:pod_player/pod_player.dart';
-
-class YouTubeLinkNodeBuilder extends NodeWidgetBuilder<Node> {
-  @override
-  Widget build(NodeWidgetContext<Node> context) {
-    return LinkNodeWidget(
-      key: context.node.key,
-      node: context.node,
-      editorState: context.editorState,
-    );
-  }
-
-  @override
-  NodeValidator<Node> get nodeValidator => ((node) {
-        return node.type == 'youtube_link';
-      });
-}
-
-class LinkNodeWidget extends StatefulWidget {
-  final Node node;
-  final EditorState editorState;
-
-  const LinkNodeWidget({
-    Key? key,
-    required this.node,
-    required this.editorState,
-  }) : super(key: key);
-
-  @override
-  State<LinkNodeWidget> createState() => _YouTubeLinkNodeWidgetState();
-}
-
-class _YouTubeLinkNodeWidgetState extends State<LinkNodeWidget>
-    with SelectableMixin {
-  Node get node => widget.node;
-  EditorState get editorState => widget.editorState;
-  String get src => widget.node.attributes['youtube_link'] as String;
-
-  @override
-  Position end() {
-    // TODO: implement end
-    throw UnimplementedError();
-  }
-
-  @override
-  Position start() {
-    // TODO: implement start
-    throw UnimplementedError();
-  }
-
-  @override
-  List<Rect> getRectsInSelection(Selection selection) {
-    // TODO: implement getRectsInSelection
-    throw UnimplementedError();
-  }
-
-  @override
-  Selection getSelectionInRange(Offset start, Offset end) {
-    // TODO: implement getSelectionInRange
-    throw UnimplementedError();
-  }
-
-  @override
-  Offset localToGlobal(Offset offset) {
-    throw UnimplementedError();
-  }
-
-  @override
-  Position getPositionInOffset(Offset start) {
-    // TODO: implement getPositionInOffset
-    throw UnimplementedError();
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    return _build(context);
-  }
-
-  late final PodPlayerController controller;
-
-  @override
-  void initState() {
-    controller = PodPlayerController(
-      playVideoFrom: PlayVideoFrom.network(
-        src,
-      ),
-    )..initialise();
-    super.initState();
-  }
-
-  Widget _build(BuildContext context) {
-    return Column(
-      children: [
-        PodVideoPlayer(controller: controller),
-      ],
-    );
-  }
-}

+ 0 - 2
frontend/app_flowy/packages/appflowy_editor/example/macos/Flutter/GeneratedPluginRegistrant.swift

@@ -8,11 +8,9 @@ import Foundation
 import path_provider_macos
 import rich_clipboard_macos
 import url_launcher_macos
-import wakelock_macos
 
 func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
   PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
   RichClipboardPlugin.register(with: registry.registrar(forPlugin: "RichClipboardPlugin"))
   UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
-  WakelockMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockMacosPlugin"))
 }

+ 0 - 6
frontend/app_flowy/packages/appflowy_editor/example/macos/Podfile.lock

@@ -6,15 +6,12 @@ PODS:
     - FlutterMacOS
   - url_launcher_macos (0.0.1):
     - FlutterMacOS
-  - wakelock_macos (0.0.1):
-    - FlutterMacOS
 
 DEPENDENCIES:
   - FlutterMacOS (from `Flutter/ephemeral`)
   - path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`)
   - rich_clipboard_macos (from `Flutter/ephemeral/.symlinks/plugins/rich_clipboard_macos/macos`)
   - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
-  - wakelock_macos (from `Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos`)
 
 EXTERNAL SOURCES:
   FlutterMacOS:
@@ -25,15 +22,12 @@ EXTERNAL SOURCES:
     :path: Flutter/ephemeral/.symlinks/plugins/rich_clipboard_macos/macos
   url_launcher_macos:
     :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
-  wakelock_macos:
-    :path: Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos
 
 SPEC CHECKSUMS:
   FlutterMacOS: ae6af50a8ea7d6103d888583d46bd8328a7e9811
   path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19
   rich_clipboard_macos: 43364b66b9dc69d203eb8dd6d758e2d12e02723c
   url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3
-  wakelock_macos: bc3f2a9bd8d2e6c89fee1e1822e7ddac3bd004a9
 
 PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c
 

+ 2 - 2
frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml

@@ -37,12 +37,12 @@ dependencies:
     path: ../
   provider: ^6.0.3
   url_launcher: ^6.1.5
-  video_player: ^2.4.5
-  pod_player: 0.0.8
   path_provider: ^2.0.11
   google_fonts: ^3.0.1
   flutter_localizations:
     sdk: flutter
+  file_picker: ^5.0.1
+  universal_html: ^2.0.8
 
 dev_dependencies:
   flutter_test:

+ 36 - 31
frontend/app_flowy/packages/appflowy_editor/lib/src/document/node.dart

@@ -193,16 +193,24 @@ class Node extends ChangeNotifier with LinkedListEntry<Node> {
     return parent!._path([index, ...previous]);
   }
 
-  Node deepClone() {
-    final newNode = Node(
-        type: type, children: LinkedList<Node>(), attributes: {...attributes});
-
-    for (final node in children) {
-      final newNode = node.deepClone();
-      newNode.parent = this;
-      newNode.children.add(newNode);
+  Node copyWith({
+    String? type,
+    LinkedList<Node>? children,
+    Attributes? attributes,
+  }) {
+    final node = Node(
+      type: type ?? this.type,
+      attributes: attributes ?? {..._attributes},
+      children: children ?? LinkedList(),
+    );
+    if (children == null && this.children.isNotEmpty) {
+      for (final child in this.children) {
+        node.children.add(
+          child.copyWith()..parent = node,
+        );
+      }
     }
-    return newNode;
+    return node;
   }
 }
 
@@ -215,7 +223,10 @@ class TextNode extends Node {
     LinkedList<Node>? children,
     Attributes? attributes,
   })  : _delta = delta,
-        super(children: children ?? LinkedList(), attributes: attributes ?? {});
+        super(
+          children: children ?? LinkedList(),
+          attributes: attributes ?? {},
+        );
 
   TextNode.empty({Attributes? attributes})
       : _delta = Delta([TextInsert('')]),
@@ -241,33 +252,27 @@ class TextNode extends Node {
     return map;
   }
 
+  @override
   TextNode copyWith({
     String? type,
     LinkedList<Node>? children,
     Attributes? attributes,
     Delta? delta,
-  }) =>
-      TextNode(
-        type: type ?? this.type,
-        children: children ?? this.children,
-        attributes: attributes ?? _attributes,
-        delta: delta ?? this.delta,
-      );
-
-  @override
-  TextNode deepClone() {
-    final newNode = TextNode(
-        type: type,
-        children: LinkedList<Node>(),
-        delta: delta.slice(0),
-        attributes: {...attributes});
-
-    for (final node in children) {
-      final newNode = node.deepClone();
-      newNode.parent = this;
-      newNode.children.add(newNode);
+  }) {
+    final textNode = TextNode(
+      type: type ?? this.type,
+      children: children,
+      attributes: attributes ?? _attributes,
+      delta: delta ?? this.delta,
+    );
+    if (children == null && this.children.isNotEmpty) {
+      for (final child in this.children) {
+        textNode.children.add(
+          child.copyWith()..parent = textNode,
+        );
+      }
     }
-    return newNode;
+    return textNode;
   }
 
   String toRawString() => _delta.toRawString();

+ 2 - 4
frontend/app_flowy/packages/appflowy_editor/lib/src/document/selection.dart

@@ -40,11 +40,9 @@ class Selection {
   bool get isCollapsed => start == end;
   bool get isSingle => pathEquals(start.path, end.path);
   bool get isForward =>
-      (start.path >= end.path && !pathEquals(start.path, end.path)) ||
-      (isSingle && start.offset > end.offset);
+      (start.path > end.path) || (isSingle && start.offset > end.offset);
   bool get isBackward =>
-      (start.path <= end.path && !pathEquals(start.path, end.path)) ||
-      (isSingle && start.offset < end.offset);
+      (start.path < end.path) || (isSingle && start.offset < end.offset);
 
   Selection get normalize {
     if (isForward) {

+ 30 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/path_extensions.dart

@@ -4,22 +4,52 @@ import 'dart:math';
 
 extension PathExtensions on Path {
   bool operator >=(Path other) {
+    if (pathEquals(this, other)) {
+      return true;
+    }
+    return this > other;
+  }
+
+  bool operator >(Path other) {
+    if (pathEquals(this, other)) {
+      return false;
+    }
     final length = min(this.length, other.length);
     for (var i = 0; i < length; i++) {
       if (this[i] < other[i]) {
         return false;
+      } else if (this[i] > other[i]) {
+        return true;
       }
     }
+    if (this.length < other.length) {
+      return false;
+    }
     return true;
   }
 
   bool operator <=(Path other) {
+    if (pathEquals(this, other)) {
+      return true;
+    }
+    return this < other;
+  }
+
+  bool operator <(Path other) {
+    if (pathEquals(this, other)) {
+      return false;
+    }
     final length = min(this.length, other.length);
     for (var i = 0; i < length; i++) {
       if (this[i] > other[i]) {
         return false;
+      } else if (this[i] < other[i]) {
+        return true;
       }
     }
+    if (this.length > other.length) {
+      return false;
+    }
     return true;
   }
 

+ 46 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/infra/infra.dart

@@ -0,0 +1,46 @@
+import 'package:appflowy_editor/src/document/node.dart';
+
+class Infra {
+// find the forward nearest text node
+  static TextNode? forwardNearestTextNode(Node node) {
+    var previous = node.previous;
+    while (previous != null) {
+      final lastTextNode = findLastTextNode(previous);
+      if (lastTextNode != null) {
+        return lastTextNode;
+      }
+      if (previous is TextNode) {
+        return previous;
+      }
+      previous = previous.previous;
+    }
+    final parent = node.parent;
+    if (parent != null) {
+      if (parent is TextNode) {
+        return parent;
+      }
+      return forwardNearestTextNode(parent);
+    }
+    return null;
+  }
+
+  // find the last text node
+  static TextNode? findLastTextNode(Node node) {
+    final children = node.children.toList(growable: false).reversed;
+    for (final child in children) {
+      if (child.children.isNotEmpty) {
+        final result = findLastTextNode(child);
+        if (result != null) {
+          return result;
+        }
+      }
+      if (child is TextNode) {
+        return child;
+      }
+    }
+    if (node is TextNode) {
+      return node;
+    }
+    return null;
+  }
+}

+ 2 - 2
frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart

@@ -36,7 +36,7 @@ class TransactionBuilder {
   /// Inserts a sequence of nodes at the position of path.
   insertNodes(Path path, List<Node> nodes) {
     beforeSelection = state.cursorSelection;
-    add(InsertOperation(path, nodes.map((node) => node.deepClone()).toList()));
+    add(InsertOperation(path, nodes.map((node) => node.copyWith()).toList()));
   }
 
   /// Updates the attributes of nodes.
@@ -75,7 +75,7 @@ class TransactionBuilder {
       nodes.add(node);
     }
 
-    add(DeleteOperation(path, nodes.map((node) => node.deepClone()).toList()));
+    add(DeleteOperation(path, nodes.map((node) => node.copyWith()).toList()));
   }
 
   textEdit(TextNode node, Delta Function() f) {

+ 56 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/built_in_text_widget.dart

@@ -1,4 +1,5 @@
 import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart';
 import 'package:flutter/material.dart';
 
 abstract class BuiltInTextWidget extends StatefulWidget {
@@ -59,3 +60,58 @@ mixin BuiltInStyleMixin<T extends BuiltInTextWidget> on State<T> {
     return const EdgeInsets.all(0);
   }
 }
+
+mixin BuiltInTextWidgetMixin<T extends BuiltInTextWidget> on State<T>
+    implements DefaultSelectable {
+  @override
+  Widget build(BuildContext context) {
+    if (widget.textNode.children.isEmpty) {
+      return buildWithSingle(context);
+    } else {
+      return buildWithChildren(context);
+    }
+  }
+
+  Widget buildWithSingle(BuildContext context);
+
+  Widget buildWithChildren(BuildContext context) {
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        buildWithSingle(context),
+        Row(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            // TODO: customize
+            const SizedBox(
+              width: 20,
+            ),
+            Expanded(
+              child: Column(
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: widget.textNode.children
+                    .map(
+                      (child) => widget.editorState.service.renderPluginService
+                          .buildPluginWidget(
+                        child is TextNode
+                            ? NodeWidgetContext<TextNode>(
+                                context: context,
+                                node: child,
+                                editorState: widget.editorState,
+                              )
+                            : NodeWidgetContext<Node>(
+                                context: context,
+                                node: child,
+                                editorState: widget.editorState,
+                              ),
+                      ),
+                    )
+                    .toList(),
+              ),
+            )
+          ],
+        )
+      ],
+    );
+  }
+}

+ 6 - 2
frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart

@@ -45,7 +45,11 @@ class BulletedListTextNodeWidget extends BuiltInTextWidget {
 // customize
 
 class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget>
-    with SelectableMixin, DefaultSelectable, BuiltInStyleMixin {
+    with
+        SelectableMixin,
+        DefaultSelectable,
+        BuiltInStyleMixin,
+        BuiltInTextWidgetMixin {
   @override
   final iconKey = GlobalKey();
 
@@ -61,7 +65,7 @@ class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget>
   }
 
   @override
-  Widget build(BuildContext context) {
+  Widget buildWithSingle(BuildContext context) {
     return Padding(
       padding: padding,
       child: Row(

+ 6 - 46
frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart

@@ -46,7 +46,11 @@ class CheckboxNodeWidget extends BuiltInTextWidget {
 }
 
 class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
-    with SelectableMixin, DefaultSelectable, BuiltInStyleMixin {
+    with
+        SelectableMixin,
+        DefaultSelectable,
+        BuiltInStyleMixin,
+        BuiltInTextWidgetMixin {
   @override
   final iconKey = GlobalKey();
 
@@ -62,15 +66,7 @@ class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
   }
 
   @override
-  Widget build(BuildContext context) {
-    if (widget.textNode.children.isEmpty) {
-      return _buildWithSingle(context);
-    } else {
-      return _buildWithChildren(context);
-    }
-  }
-
-  Widget _buildWithSingle(BuildContext context) {
+  Widget buildWithSingle(BuildContext context) {
     final check = widget.textNode.attributes.check;
     return Padding(
       padding: padding,
@@ -106,40 +102,4 @@ class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
       ),
     );
   }
-
-  Widget _buildWithChildren(BuildContext context) {
-    return Column(
-      crossAxisAlignment: CrossAxisAlignment.start,
-      children: [
-        _buildWithSingle(context),
-        Row(
-          children: [
-            const SizedBox(
-              width: 20,
-            ),
-            Column(
-              children: widget.textNode.children
-                  .map(
-                    (child) => widget.editorState.service.renderPluginService
-                        .buildPluginWidget(
-                      child is TextNode
-                          ? NodeWidgetContext<TextNode>(
-                              context: context,
-                              node: child,
-                              editorState: widget.editorState,
-                            )
-                          : NodeWidgetContext<Node>(
-                              context: context,
-                              node: child,
-                              editorState: widget.editorState,
-                            ),
-                    ),
-                  )
-                  .toList(),
-            )
-          ],
-        )
-      ],
-    );
-  }
 }

+ 6 - 2
frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart

@@ -43,7 +43,11 @@ class RichTextNodeWidget extends BuiltInTextWidget {
 // customize
 
 class _RichTextNodeWidgetState extends State<RichTextNodeWidget>
-    with SelectableMixin, DefaultSelectable, BuiltInStyleMixin {
+    with
+        SelectableMixin,
+        DefaultSelectable,
+        BuiltInStyleMixin,
+        BuiltInTextWidgetMixin {
   @override
   GlobalKey? get iconKey => null;
 
@@ -59,7 +63,7 @@ class _RichTextNodeWidgetState extends State<RichTextNodeWidget>
   }
 
   @override
-  Widget build(BuildContext context) {
+  Widget buildWithSingle(BuildContext context) {
     return Padding(
       padding: padding,
       child: FlowyRichText(

+ 6 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart

@@ -38,6 +38,7 @@ class AppFlowyEditor extends StatefulWidget {
     this.customBuilders = const {},
     this.shortcutEvents = const [],
     this.selectionMenuItems = const [],
+    this.editable = true,
     required this.editorStyle,
   }) : super(key: key);
 
@@ -53,6 +54,8 @@ class AppFlowyEditor extends StatefulWidget {
 
   final EditorStyle editorStyle;
 
+  final bool editable;
+
   @override
   State<AppFlowyEditor> createState() => _AppFlowyEditorState();
 }
@@ -106,11 +109,14 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
           cursorColor: widget.editorStyle.cursorColor,
           selectionColor: widget.editorStyle.selectionColor,
           editorState: editorState,
+          editable: widget.editable,
           child: AppFlowyInput(
             key: editorState.service.inputServiceKey,
             editorState: editorState,
+            editable: widget.editable,
             child: AppFlowyKeyboard(
               key: editorState.service.keyboardServiceKey,
+              editable: widget.editable,
               shortcutEvents: [
                 ...builtInShortcutEvents,
                 ...widget.shortcutEvents,

+ 22 - 6
frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart

@@ -1,4 +1,5 @@
 import 'package:appflowy_editor/src/infra/log.dart';
+import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 
@@ -43,11 +44,13 @@ abstract class AppFlowyInputService {
 class AppFlowyInput extends StatefulWidget {
   const AppFlowyInput({
     Key? key,
+    this.editable = true,
     required this.editorState,
     required this.child,
   }) : super(key: key);
 
   final EditorState editorState;
+  final bool editable;
   final Widget child;
 
   @override
@@ -61,26 +64,39 @@ class _AppFlowyInputState extends State<AppFlowyInput>
 
   EditorState get _editorState => widget.editorState;
 
+  // Disable space shortcut on the Web platform.
+  final Map<ShortcutActivator, Intent> _shortcuts = kIsWeb
+      ? {
+          LogicalKeySet(LogicalKeyboardKey.space):
+              DoNothingAndStopPropagationIntent(),
+        }
+      : {};
+
   @override
   void initState() {
     super.initState();
 
-    _editorState.service.selectionService.currentSelection
-        .addListener(_onSelectionChange);
+    if (widget.editable) {
+      _editorState.service.selectionService.currentSelection
+          .addListener(_onSelectionChange);
+    }
   }
 
   @override
   void dispose() {
-    close();
-    _editorState.service.selectionService.currentSelection
-        .removeListener(_onSelectionChange);
+    if (widget.editable) {
+      close();
+      _editorState.service.selectionService.currentSelection
+          .removeListener(_onSelectionChange);
+    }
 
     super.dispose();
   }
 
   @override
   Widget build(BuildContext context) {
-    return Container(
+    return Shortcuts(
+      shortcuts: _shortcuts,
       child: widget.child,
     );
   }

+ 41 - 26
frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart

@@ -1,8 +1,9 @@
+import 'package:appflowy_editor/src/infra/infra.dart';
 import 'package:appflowy_editor/src/service/internal_key_event_handlers/number_list_helper.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
-import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:appflowy_editor/src/extensions/path_extensions.dart';
 
 // Handle delete text.
 ShortcutEventHandler deleteTextHandler = (editorState, event) {
@@ -121,32 +122,46 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
 }
 
 KeyEventResult _backDeleteToPreviousTextNode(
-    EditorState editorState,
-    TextNode textNode,
-    TransactionBuilder transactionBuilder,
-    List<Node> nonTextNodes,
-    Selection selection) {
-  var previous = textNode.previous;
+  EditorState editorState,
+  TextNode textNode,
+  TransactionBuilder transactionBuilder,
+  List<Node> nonTextNodes,
+  Selection selection,
+) {
+  if (textNode.next == null &&
+      textNode.children.isEmpty &&
+      textNode.parent?.parent != null) {
+    transactionBuilder
+      ..deleteNode(textNode)
+      ..insertNode(textNode.parent!.path.next, textNode)
+      ..afterSelection = Selection.collapsed(
+        Position(path: textNode.parent!.path.next, offset: 0),
+      )
+      ..commit();
+    return KeyEventResult.handled;
+  }
+
   bool prevIsNumberList = false;
-  while (previous != null) {
-    if (previous is TextNode) {
-      if (previous.subtype == BuiltInAttributeKey.numberList) {
-        prevIsNumberList = true;
-      }
+  final previousTextNode = Infra.forwardNearestTextNode(textNode);
+  if (previousTextNode != null) {
+    if (previousTextNode.subtype == BuiltInAttributeKey.numberList) {
+      prevIsNumberList = true;
+    }
 
-      transactionBuilder
-        ..mergeText(previous, textNode)
-        ..deleteNode(textNode)
-        ..afterSelection = Selection.collapsed(
-          Position(
-            path: previous.path,
-            offset: previous.toRawString().length,
-          ),
-        );
-      break;
-    } else {
-      previous = previous.previous;
+    transactionBuilder.mergeText(previousTextNode, textNode);
+    if (textNode.children.isNotEmpty) {
+      transactionBuilder.insertNodes(
+        previousTextNode.path.next,
+        textNode.children.toList(growable: false),
+      );
     }
+    transactionBuilder.deleteNode(textNode);
+    transactionBuilder.afterSelection = Selection.collapsed(
+      Position(
+        path: previousTextNode.path,
+        offset: previousTextNode.toRawString().length,
+      ),
+    );
   }
 
   if (transactionBuilder.operations.isNotEmpty) {
@@ -157,8 +172,8 @@ KeyEventResult _backDeleteToPreviousTextNode(
   }
 
   if (prevIsNumberList) {
-    makeFollowingNodesIncremental(
-        editorState, previous!.path, transactionBuilder.afterSelection!);
+    makeFollowingNodesIncremental(editorState, previousTextNode!.path,
+        transactionBuilder.afterSelection!);
   }
 
   return KeyEventResult.handled;

+ 25 - 22
frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart

@@ -1,9 +1,9 @@
+import 'dart:collection';
+
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
 
 import 'package:appflowy_editor/src/extensions/path_extensions.dart';
-import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
 import './number_list_helper.dart';
 
 /// Handle some cases where enter is pressed and shift is not pressed.
@@ -16,10 +16,6 @@ import './number_list_helper.dart';
 ///   2.2 or insert a empty text node before.
 ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
     (editorState, event) {
-  if (event.logicalKey != LogicalKeyboardKey.enter || event.isShiftPressed) {
-    return KeyEventResult.ignored;
-  }
-
   var selection = editorState.service.selectionService.currentSelection.value;
   var nodes = editorState.service.selectionService.currentSelectedNodes;
   if (selection == null) {
@@ -124,7 +120,10 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
         TransactionBuilder(editorState)
           ..insertNode(
             textNode.path,
-            TextNode.empty(),
+            textNode.copyWith(
+              children: LinkedList(),
+              delta: Delta(),
+            ),
           )
           ..afterSelection = afterSelection
           ..commit();
@@ -142,21 +141,25 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
     Position(path: nextPath, offset: 0),
   );
 
-  TransactionBuilder(editorState)
-    ..insertNode(
-      textNode.path.next,
-      textNode.copyWith(
-        attributes: attributes,
-        delta: textNode.delta.slice(selection.end.offset),
-      ),
-    )
-    ..deleteText(
-      textNode,
-      selection.start.offset,
-      textNode.toRawString().length - selection.start.offset,
-    )
-    ..afterSelection = afterSelection
-    ..commit();
+  final transactionBuilder = TransactionBuilder(editorState);
+  transactionBuilder.insertNode(
+    textNode.path.next,
+    textNode.copyWith(
+      attributes: attributes,
+      delta: textNode.delta.slice(selection.end.offset),
+    ),
+  );
+  transactionBuilder.deleteText(
+    textNode,
+    selection.start.offset,
+    textNode.toRawString().length - selection.start.offset,
+  );
+  if (textNode.children.isNotEmpty) {
+    final children = textNode.children.toList(growable: false);
+    transactionBuilder.deleteNodes(children);
+  }
+  transactionBuilder.afterSelection = afterSelection;
+  transactionBuilder.commit();
 
   // If the new type of a text node is number list,
   // the numbers of the following nodes should be incremental.

+ 34 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/tab_handler.dart

@@ -0,0 +1,34 @@
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flutter/material.dart';
+
+ShortcutEventHandler tabHandler = (editorState, event) {
+  // Only Supports BulletedList For Now.
+
+  final selection = editorState.service.selectionService.currentSelection.value;
+  final textNodes = editorState.service.selectionService.currentSelectedNodes
+      .whereType<TextNode>();
+  if (textNodes.length != 1 || selection == null || !selection.isSingle) {
+    return KeyEventResult.ignored;
+  }
+
+  final textNode = textNodes.first;
+  final previous = textNode.previous;
+  if (textNode.subtype != BuiltInAttributeKey.bulletedList ||
+      previous == null ||
+      previous.subtype != BuiltInAttributeKey.bulletedList) {
+    return KeyEventResult.handled;
+  }
+
+  final path = previous.path + [previous.children.length];
+  final afterSelection = Selection(
+    start: selection.start.copyWith(path: path),
+    end: selection.end.copyWith(path: path),
+  );
+  TransactionBuilder(editorState)
+    ..deleteNode(textNode)
+    ..insertNode(path, textNode)
+    ..setAfterSelection(afterSelection)
+    ..commit();
+
+  return KeyEventResult.handled;
+};

+ 8 - 3
frontend/app_flowy/packages/appflowy_editor/lib/src/service/keyboard_service.dart

@@ -42,6 +42,7 @@ abstract class AppFlowyKeyboardService {
 class AppFlowyKeyboard extends StatefulWidget {
   const AppFlowyKeyboard({
     Key? key,
+    this.editable = true,
     required this.shortcutEvents,
     required this.editorState,
     required this.child,
@@ -50,6 +51,7 @@ class AppFlowyKeyboard extends StatefulWidget {
   final EditorState editorState;
   final Widget child;
   final List<ShortcutEvent> shortcutEvents;
+  final bool editable;
 
   @override
   State<AppFlowyKeyboard> createState() => _AppFlowyKeyboardState();
@@ -62,7 +64,6 @@ class _AppFlowyKeyboardState extends State<AppFlowyKeyboard>
   bool isFocus = true;
 
   @override
-  // TODO: implement shortcutEvents
   List<ShortcutEvent> get shortcutEvents => widget.shortcutEvents;
 
   @override
@@ -91,8 +92,12 @@ class _AppFlowyKeyboardState extends State<AppFlowyKeyboard>
 
   @override
   void enable() {
-    isFocus = true;
-    _focusNode.requestFocus();
+    if (widget.editable) {
+      isFocus = true;
+      _focusNode.requestFocus();
+    } else {
+      disable();
+    }
   }
 
   @override

+ 24 - 9
frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart

@@ -84,6 +84,7 @@ class AppFlowySelection extends StatefulWidget {
     Key? key,
     this.cursorColor = const Color(0xFF00BCF0),
     this.selectionColor = const Color.fromARGB(53, 111, 201, 231),
+    this.editable = true,
     required this.editorState,
     required this.child,
   }) : super(key: key);
@@ -92,6 +93,7 @@ class AppFlowySelection extends StatefulWidget {
   final Widget child;
   final Color cursorColor;
   final Color selectionColor;
+  final bool editable;
 
   @override
   State<AppFlowySelection> createState() => _AppFlowySelectionState();
@@ -144,15 +146,21 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
 
   @override
   Widget build(BuildContext context) {
-    return SelectionGestureDetector(
-      onPanStart: _onPanStart,
-      onPanUpdate: _onPanUpdate,
-      onPanEnd: _onPanEnd,
-      onTapDown: _onTapDown,
-      onDoubleTapDown: _onDoubleTapDown,
-      onTripleTapDown: _onTripleTapDown,
-      child: widget.child,
-    );
+    if (!widget.editable) {
+      return Container(
+        child: widget.child,
+      );
+    } else {
+      return SelectionGestureDetector(
+        onPanStart: _onPanStart,
+        onPanUpdate: _onPanUpdate,
+        onPanEnd: _onPanEnd,
+        onTapDown: _onTapDown,
+        onDoubleTapDown: _onDoubleTapDown,
+        onTripleTapDown: _onTripleTapDown,
+        child: widget.child,
+      );
+    }
   }
 
   @override
@@ -184,6 +192,10 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
 
   @override
   void updateSelection(Selection? selection) {
+    if (!widget.editable) {
+      return;
+    }
+
     selectionRects.clear();
     clearSelection();
 
@@ -323,6 +335,7 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
 
     // compute the selection in range.
     if (first != null && last != null) {
+      Log.selection.debug('first = $first, last = $last');
       final start =
           first.getSelectionInRange(panStartOffset, panEndOffset).start;
       final end = last.getSelectionInRange(panStartOffset, panEndOffset).end;
@@ -353,6 +366,8 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
     final normalizedSelection = selection.normalize;
     assert(normalizedSelection.isBackward);
 
+    Log.selection.debug('update selection areas, $normalizedSelection');
+
     for (var i = 0; i < backwardNodes.length; i++) {
       final node = backwardNodes[i];
       final selectable = node.selectable;

+ 6 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart

@@ -9,6 +9,7 @@ import 'package:appflowy_editor/src/service/internal_key_event_handlers/redo_und
 import 'package:appflowy_editor/src/service/internal_key_event_handlers/select_all_handler.dart';
 import 'package:appflowy_editor/src/service/internal_key_event_handlers/slash_handler.dart';
 import 'package:appflowy_editor/src/service/internal_key_event_handlers/format_style_handler.dart';
+import 'package:appflowy_editor/src/service/internal_key_event_handlers/tab_handler.dart';
 import 'package:appflowy_editor/src/service/internal_key_event_handlers/whitespace_handler.dart';
 import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event.dart';
 
@@ -243,4 +244,9 @@ List<ShortcutEvent> builtInShortcutEvents = [
     command: 'page down',
     handler: pageDownHandler,
   ),
+  ShortcutEvent(
+    key: 'Tab',
+    command: 'tab',
+    handler: tabHandler,
+  ),
 ];

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

@@ -2,6 +2,7 @@ import 'dart:io';
 
 import 'package:appflowy_editor/src/service/shortcut_event/keybinding.dart';
 import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart';
+import 'package:flutter/foundation.dart';
 
 /// Defines the implementation of shortcut event.
 class ShortcutEvent {
@@ -56,7 +57,10 @@ class ShortcutEvent {
     String? linuxCommand,
   }) {
     var matched = false;
-    if (Platform.isWindows &&
+    if (kIsWeb && command != null && command.isNotEmpty) {
+      this.command = command;
+      matched = true;
+    } else if (Platform.isWindows &&
         windowsCommand != null &&
         windowsCommand.isNotEmpty) {
       this.command = windowsCommand;

+ 153 - 0
frontend/app_flowy/packages/appflowy_editor/test/document/node_test.dart

@@ -0,0 +1,153 @@
+import 'dart:collection';
+
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() async {
+  setUpAll(() {
+    TestWidgetsFlutterBinding.ensureInitialized();
+  });
+
+  group('node.dart', () {
+    test('test node copyWith', () {
+      final node = Node(
+        type: 'example',
+        children: LinkedList(),
+        attributes: {
+          'example': 'example',
+        },
+      );
+      expect(node.toJson(), {
+        'type': 'example',
+        'attributes': {
+          'example': 'example',
+        },
+      });
+      expect(
+        node.copyWith().toJson(),
+        node.toJson(),
+      );
+
+      final nodeWithChildren = Node(
+        type: 'example',
+        children: LinkedList()..add(node),
+        attributes: {
+          'example': 'example',
+        },
+      );
+      expect(nodeWithChildren.toJson(), {
+        'type': 'example',
+        'attributes': {
+          'example': 'example',
+        },
+        'children': [
+          {
+            'type': 'example',
+            'attributes': {
+              'example': 'example',
+            },
+          },
+        ],
+      });
+      expect(
+        nodeWithChildren.copyWith().toJson(),
+        nodeWithChildren.toJson(),
+      );
+    });
+
+    test('test textNode copyWith', () {
+      final textNode = TextNode(
+        type: 'example',
+        children: LinkedList(),
+        attributes: {
+          'example': 'example',
+        },
+        delta: Delta()..insert('AppFlowy'),
+      );
+      expect(textNode.toJson(), {
+        'type': 'example',
+        'attributes': {
+          'example': 'example',
+        },
+        'delta': [
+          {'insert': 'AppFlowy'},
+        ],
+      });
+      expect(
+        textNode.copyWith().toJson(),
+        textNode.toJson(),
+      );
+
+      final textNodeWithChildren = TextNode(
+        type: 'example',
+        children: LinkedList()..add(textNode),
+        attributes: {
+          'example': 'example',
+        },
+        delta: Delta()..insert('AppFlowy'),
+      );
+      expect(textNodeWithChildren.toJson(), {
+        'type': 'example',
+        'attributes': {
+          'example': 'example',
+        },
+        'delta': [
+          {'insert': 'AppFlowy'},
+        ],
+        'children': [
+          {
+            'type': 'example',
+            'attributes': {
+              'example': 'example',
+            },
+            'delta': [
+              {'insert': 'AppFlowy'},
+            ],
+          },
+        ],
+      });
+      expect(
+        textNodeWithChildren.copyWith().toJson(),
+        textNodeWithChildren.toJson(),
+      );
+    });
+
+    test('test node path', () {
+      Node previous = Node(
+        type: 'example',
+        attributes: {},
+        children: LinkedList(),
+      );
+      const len = 10;
+      for (var i = 0; i < len; i++) {
+        final node = Node(
+          type: 'example_$i',
+          attributes: {},
+          children: LinkedList(),
+        );
+        previous.children.add(node..parent = previous);
+        previous = node;
+      }
+      expect(previous.path, List.filled(len, 0));
+    });
+
+    test('test copy with', () {
+      final child = Node(
+        type: 'child',
+        attributes: {},
+        children: LinkedList(),
+      );
+      final base = Node(
+        type: 'base',
+        attributes: {},
+        children: LinkedList()..add(child),
+      );
+      final node = base.copyWith(
+        type: 'node',
+      );
+      expect(identical(node.attributes, base.attributes), false);
+      expect(identical(node.children, base.children), false);
+      expect(identical(node.children.first, base.children.first), false);
+    });
+  });
+}

+ 38 - 0
frontend/app_flowy/packages/appflowy_editor/test/extensions/path_extensions_test.dart

@@ -0,0 +1,38 @@
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:appflowy_editor/src/extensions/path_extensions.dart';
+
+void main() async {
+  setUpAll(() {
+    TestWidgetsFlutterBinding.ensureInitialized();
+  });
+
+  group('path_extensions.dart', () {
+    test('test path equality', () {
+      var p1 = [0, 0];
+      var p2 = [0];
+
+      expect(p1 > p2, true);
+      expect(p1 >= p2, true);
+      expect(p1 < p2, false);
+      expect(p1 <= p2, false);
+
+      p1 = [1, 1, 2];
+      p2 = [1, 1, 3];
+
+      expect(p2 > p1, true);
+      expect(p2 >= p1, true);
+      expect(p2 < p1, false);
+      expect(p2 <= p1, false);
+
+      p1 = [2, 0, 1];
+      p2 = [2, 0, 1];
+
+      expect(p2 > p1, false);
+      expect(p1 > p2, false);
+      expect(p2 >= p1, true);
+      expect(p2 <= p1, true);
+      expect(pathEquals(p1, p2), true);
+    });
+  });
+}

+ 51 - 0
frontend/app_flowy/packages/appflowy_editor/test/infra/infra_test.dart

@@ -0,0 +1,51 @@
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:appflowy_editor/src/infra/infra.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() async {
+  group('infra.dart', () {
+    test('find the last text node', () {
+      // * Welcome to Appflowy 😁
+      //  * Welcome to Appflowy 😁
+      //  * Welcome to Appflowy 😁
+      //    * Welcome to Appflowy 😁
+      //    * Welcome to Appflowy 😁
+      //      * Welcome to Appflowy 😁
+      //      * Welcome to Appflowy 😁
+      const text = 'Welcome to Appflowy 😁';
+      TextNode textNode() {
+        return TextNode(
+          type: 'text',
+          delta: Delta()..insert(text),
+        );
+      }
+
+      final node110 = textNode();
+      final node111 = textNode();
+      final node11 = textNode()
+        ..insert(node110)
+        ..insert(node111);
+      final node10 = textNode();
+      final node1 = textNode()
+        ..insert(node10)
+        ..insert(node11);
+      final node0 = textNode();
+      final node = textNode()
+        ..insert(node0)
+        ..insert(node1);
+
+      expect(Infra.findLastTextNode(node)?.path, [1, 1, 1]);
+      expect(Infra.findLastTextNode(node0)?.path, [0]);
+      expect(Infra.findLastTextNode(node1)?.path, [1, 1, 1]);
+      expect(Infra.findLastTextNode(node10)?.path, [1, 0]);
+      expect(Infra.findLastTextNode(node11)?.path, [1, 1, 1]);
+
+      expect(Infra.forwardNearestTextNode(node111)?.path, [1, 1, 0]);
+      expect(Infra.forwardNearestTextNode(node110)?.path, [1, 1]);
+      expect(Infra.forwardNearestTextNode(node11)?.path, [1, 0]);
+      expect(Infra.forwardNearestTextNode(node10)?.path, [1]);
+      expect(Infra.forwardNearestTextNode(node1)?.path, [0]);
+      expect(Infra.forwardNearestTextNode(node0)?.path, []);
+    });
+  });
+}

+ 1 - 0
frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart

@@ -19,6 +19,7 @@ class EditorWidgetTester {
   EditorState get editorState => _editorState;
   Node get root => _editorState.document.root;
 
+  StateTree get document => _editorState.document;
   int get documentLength => _editorState.document.root.children.length;
   Selection? get documentSelection =>
       _editorState.service.selectionService.currentSelection.value;

+ 0 - 49
frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_item_widget_test.dart

@@ -1,49 +0,0 @@
-import 'package:appflowy_editor/src/render/selection_menu/selection_menu_item_widget.dart';
-import 'package:appflowy_editor/src/render/selection_menu/selection_menu_service.dart';
-import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-import '../../infra/test_editor.dart';
-
-void main() async {
-  setUpAll(() {
-    TestWidgetsFlutterBinding.ensureInitialized();
-  });
-
-  group('selection_menu_item_widget.dart', () {
-    testWidgets('test selection menu item widget', (tester) async {
-      bool flag = false;
-      final editorState = tester.editor.editorState;
-      final menuService = _TestSelectionMenuService();
-      const icon = Icon(Icons.abc);
-      final item = SelectionMenuItem(
-        name: () => 'example',
-        icon: icon,
-        keywords: ['example A', 'example B'],
-        handler: (editorState, menuService, context) {
-          flag = true;
-        },
-      );
-      final widget = SelectionMenuItemWidget(
-        editorState: editorState,
-        menuService: menuService,
-        item: item,
-        isSelected: true,
-      );
-      await tester.pumpWidget(MaterialApp(home: widget));
-      await tester.tap(find.byType(SelectionMenuItemWidget));
-      expect(flag, true);
-    });
-  });
-}
-
-class _TestSelectionMenuService implements SelectionMenuService {
-  @override
-  void dismiss() {}
-
-  @override
-  void show() {}
-
-  @override
-  Offset get topLeft => throw UnimplementedError();
-}

+ 0 - 1
frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart

@@ -1,5 +1,4 @@
 import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
 import 'package:appflowy_editor/src/render/selection_menu/selection_menu_item_widget.dart';
 import 'package:appflowy_editor/src/render/selection_menu/selection_menu_service.dart';
 import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart';

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

@@ -1,10 +1,12 @@
+import 'dart:collection';
+
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:appflowy_editor/src/render/image/image_node_widget.dart';
+import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:network_image_mock/network_image_mock.dart';
 import '../../infra/test_editor.dart';
-import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
 
 void main() async {
   setUpAll(() {
@@ -267,6 +269,140 @@ void main() async {
       BuiltInAttributeKey.h1,
     );
   });
+
+  testWidgets('Delete the nested bulleted list', (tester) async {
+    // * Welcome to Appflowy 😁
+    //  * Welcome to Appflowy 😁
+    //    * Welcome to Appflowy 😁
+    const text = 'Welcome to Appflowy 😁';
+    final node = TextNode(
+      type: 'text',
+      delta: Delta()..insert(text),
+      attributes: {
+        BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList,
+      },
+    );
+    node.insert(
+      node.copyWith()
+        ..insert(
+          node.copyWith(),
+        ),
+    );
+
+    final editor = tester.editor..insert(node);
+    await editor.startTesting();
+
+    // * Welcome to Appflowy 😁
+    //  * Welcome to Appflowy 😁
+    // Welcome to Appflowy 😁
+    await editor.updateSelection(
+      Selection.single(path: [0, 0, 0], startOffset: 0),
+    );
+    await editor.pressLogicKey(LogicalKeyboardKey.backspace);
+    expect(editor.nodeAtPath([0, 0, 0])?.subtype, null);
+    await editor.updateSelection(
+      Selection.single(path: [0, 0, 0], startOffset: 0),
+    );
+    await editor.pressLogicKey(LogicalKeyboardKey.backspace);
+    expect(editor.nodeAtPath([0, 1]) != null, true);
+    await editor.updateSelection(
+      Selection.single(path: [0, 1], startOffset: 0),
+    );
+    await editor.pressLogicKey(LogicalKeyboardKey.backspace);
+    expect(editor.nodeAtPath([1]) != null, true);
+    await editor.updateSelection(
+      Selection.single(path: [1], startOffset: 0),
+    );
+
+    // * Welcome to Appflowy 😁
+    //  * Welcome to Appflowy 😁Welcome to Appflowy 😁
+    await editor.pressLogicKey(LogicalKeyboardKey.backspace);
+    expect(
+      editor.documentSelection,
+      Selection.single(path: [0, 0], startOffset: text.length),
+    );
+    expect((editor.nodeAtPath([0, 0]) as TextNode).toRawString(), text * 2);
+  });
+
+  testWidgets('Delete the complicated nested bulleted list', (tester) async {
+    // * Welcome to Appflowy 😁
+    //  * Welcome to Appflowy 😁
+    //  * Welcome to Appflowy 😁
+    //    * Welcome to Appflowy 😁
+    //    * Welcome to Appflowy 😁
+    const text = 'Welcome to Appflowy 😁';
+    final node = TextNode(
+      type: 'text',
+      delta: Delta()..insert(text),
+      attributes: {
+        BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList,
+      },
+    );
+
+    node
+      ..insert(
+        node.copyWith(children: LinkedList()),
+      )
+      ..insert(
+        node.copyWith(children: LinkedList())
+          ..insert(
+            node.copyWith(children: LinkedList()),
+          )
+          ..insert(
+            node.copyWith(children: LinkedList()),
+          ),
+      );
+
+    final editor = tester.editor..insert(node);
+    await editor.startTesting();
+
+    await editor.updateSelection(
+      Selection.single(path: [0, 1], startOffset: 0),
+    );
+    await editor.pressLogicKey(LogicalKeyboardKey.backspace);
+    expect(
+      editor.nodeAtPath([0, 1])!.subtype != BuiltInAttributeKey.bulletedList,
+      true,
+    );
+    expect(
+      editor.nodeAtPath([0, 1, 0])!.subtype,
+      BuiltInAttributeKey.bulletedList,
+    );
+    expect(
+      editor.nodeAtPath([0, 1, 1])!.subtype,
+      BuiltInAttributeKey.bulletedList,
+    );
+    expect(find.byType(FlowyRichText), findsNWidgets(5));
+
+    // Before
+    // * Welcome to Appflowy 😁
+    //  * Welcome to Appflowy 😁
+    //  Welcome to Appflowy 😁
+    //    * Welcome to Appflowy 😁
+    //    * Welcome to Appflowy 😁
+    // After
+    // * Welcome to Appflowy 😁
+    //  * Welcome to Appflowy 😁Welcome to Appflowy 😁
+    //  * Welcome to Appflowy 😁
+    //  * Welcome to Appflowy 😁
+    await editor.pressLogicKey(LogicalKeyboardKey.backspace);
+    expect(
+      editor.nodeAtPath([0, 0])!.subtype == BuiltInAttributeKey.bulletedList,
+      true,
+    );
+    expect(
+      (editor.nodeAtPath([0, 0]) as TextNode).toRawString() == text * 2,
+      true,
+    );
+    expect(
+      editor.nodeAtPath([0, 1])!.subtype == BuiltInAttributeKey.bulletedList,
+      true,
+    );
+    expect(
+      editor.nodeAtPath([0, 2])!.subtype == BuiltInAttributeKey.bulletedList,
+      true,
+    );
+  });
 }
 
 Future<void> _deleteFirstImage(WidgetTester tester, bool isBackward) async {

+ 151 - 0
frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/tab_handler_test.dart

@@ -0,0 +1,151 @@
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
+import '../../infra/test_editor.dart';
+
+void main() async {
+  setUpAll(() {
+    TestWidgetsFlutterBinding.ensureInitialized();
+  });
+
+  group('tab_handler.dart', () {
+    testWidgets('press tab in plain text', (tester) async {
+      const text = 'Welcome to Appflowy 😁';
+      final editor = tester.editor
+        ..insertTextNode(text)
+        ..insertTextNode(text);
+      await editor.startTesting();
+      final document = editor.document;
+
+      var selection = Selection.single(path: [0], startOffset: 0);
+      await editor.updateSelection(selection);
+      await editor.pressLogicKey(LogicalKeyboardKey.tab);
+
+      // nothing happens
+      expect(editor.documentSelection, selection);
+      expect(editor.document.toJson(), document.toJson());
+
+      selection = Selection.single(path: [1], startOffset: 0);
+      await editor.updateSelection(selection);
+      await editor.pressLogicKey(LogicalKeyboardKey.tab);
+
+      // nothing happens
+      expect(editor.documentSelection, selection);
+      expect(editor.document.toJson(), document.toJson());
+    });
+
+    testWidgets('press tab in bulleted list', (tester) async {
+      const text = 'Welcome to Appflowy 😁';
+      final editor = tester.editor
+        ..insertTextNode(
+          text,
+          attributes: {
+            BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList
+          },
+        )
+        ..insertTextNode(
+          text,
+          attributes: {
+            BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList
+          },
+        )
+        ..insertTextNode(
+          text,
+          attributes: {
+            BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList
+          },
+        );
+      await editor.startTesting();
+      var document = editor.document;
+
+      var selection = Selection.single(path: [0], startOffset: 0);
+      await editor.updateSelection(selection);
+      await editor.pressLogicKey(LogicalKeyboardKey.tab);
+
+      // nothing happens
+      expect(editor.documentSelection, selection);
+      expect(editor.document.toJson(), document.toJson());
+
+      // Before
+      // * Welcome to Appflowy 😁
+      // * Welcome to Appflowy 😁
+      // * Welcome to Appflowy 😁
+      // After
+      // * Welcome to Appflowy 😁
+      //  * Welcome to Appflowy 😁
+      //  * Welcome to Appflowy 😁
+
+      selection = Selection.single(path: [1], startOffset: 0);
+      await editor.updateSelection(selection);
+
+      await editor.pressLogicKey(LogicalKeyboardKey.tab);
+
+      expect(
+        editor.documentSelection,
+        Selection.single(path: [0, 0], startOffset: 0),
+      );
+      expect(editor.nodeAtPath([0])!.subtype, BuiltInAttributeKey.bulletedList);
+      expect(editor.nodeAtPath([1])!.subtype, BuiltInAttributeKey.bulletedList);
+      expect(editor.nodeAtPath([2]), null);
+      expect(
+          editor.nodeAtPath([0, 0])!.subtype, BuiltInAttributeKey.bulletedList);
+
+      selection = Selection.single(path: [1], startOffset: 0);
+      await editor.updateSelection(selection);
+      await editor.pressLogicKey(LogicalKeyboardKey.tab);
+
+      expect(
+        editor.documentSelection,
+        Selection.single(path: [0, 1], startOffset: 0),
+      );
+      expect(editor.nodeAtPath([0])!.subtype, BuiltInAttributeKey.bulletedList);
+      expect(editor.nodeAtPath([1]), null);
+      expect(editor.nodeAtPath([2]), null);
+      expect(
+          editor.nodeAtPath([0, 0])!.subtype, BuiltInAttributeKey.bulletedList);
+      expect(
+          editor.nodeAtPath([0, 1])!.subtype, BuiltInAttributeKey.bulletedList);
+
+      // Before
+      // * Welcome to Appflowy 😁
+      //  * Welcome to Appflowy 😁
+      //  * Welcome to Appflowy 😁
+      // After
+      // * Welcome to Appflowy 😁
+      //  * Welcome to Appflowy 😁
+      //    * Welcome to Appflowy 😁
+      document = editor.document;
+      selection = Selection.single(path: [0, 0], startOffset: 0);
+      await editor.updateSelection(selection);
+      await editor.pressLogicKey(LogicalKeyboardKey.tab);
+
+      expect(
+        editor.documentSelection,
+        Selection.single(path: [0, 0], startOffset: 0),
+      );
+      expect(editor.document.toJson(), document.toJson());
+
+      selection = Selection.single(path: [0, 1], startOffset: 0);
+      await editor.updateSelection(selection);
+      await editor.pressLogicKey(LogicalKeyboardKey.tab);
+
+      expect(
+        editor.documentSelection,
+        Selection.single(path: [0, 0, 0], startOffset: 0),
+      );
+      expect(
+        editor.nodeAtPath([0])!.subtype,
+        BuiltInAttributeKey.bulletedList,
+      );
+      expect(
+        editor.nodeAtPath([0, 0])!.subtype,
+        BuiltInAttributeKey.bulletedList,
+      );
+      expect(editor.nodeAtPath([0, 1]), null);
+      expect(
+        editor.nodeAtPath([0, 0, 0])!.subtype,
+        BuiltInAttributeKey.bulletedList,
+      );
+    });
+  });
+}

+ 2 - 3
frontend/app_flowy/packages/appflowy_popover/lib/src/popover.dart

@@ -136,7 +136,6 @@ class PopoverState extends State<Popover> {
 
       return Stack(children: children);
     });
-
     _rootEntry.addEntry(context, this, newEntry, widget.asBarrier);
   }
 
@@ -243,7 +242,7 @@ class PopoverContainerState extends State<PopoverContainer> {
     );
   }
 
-  close() => widget.onClose();
+  void close() => widget.onClose();
 
-  closeAll() => widget.onCloseAll();
+  void closeAll() => widget.onCloseAll();
 }

+ 13 - 7
frontend/app_flowy/packages/flowy_infra/lib/theme.dart

@@ -104,7 +104,7 @@ class AppTheme {
           ..tint6 = const Color(0xfff5ffdc)
           ..tint7 = const Color(0xffddffd6)
           ..tint8 = const Color(0xffdefff1)
-          ..tint9 = const Color(0xffdefff1)
+          ..tint9 = const Color(0xffe1fbff)
           ..main1 = const Color(0xff00bcf0)
           ..main2 = const Color(0xff00b7ea)
           ..textColor = _black
@@ -152,7 +152,8 @@ class AppTheme {
   ThemeData get themeData {
     var t = ThemeData(
       textTheme: TextTheme(bodyText2: TextStyle(color: textColor)),
-      textSelectionTheme: TextSelectionThemeData(cursorColor: main2, selectionHandleColor: main2),
+      textSelectionTheme: TextSelectionThemeData(
+          cursorColor: main2, selectionHandleColor: main2),
       primaryIconTheme: IconThemeData(color: hover),
       iconTheme: IconThemeData(color: shader1),
       canvasColor: shader6,
@@ -179,7 +180,8 @@ class AppTheme {
         toggleableActiveColor: main1);
   }
 
-  Color shift(Color c, double d) => ColorUtils.shiftHsl(c, d * (isDark ? -1 : 1));
+  Color shift(Color c, double d) =>
+      ColorUtils.shiftHsl(c, d * (isDark ? -1 : 1));
 }
 
 class ColorUtils {
@@ -188,14 +190,18 @@ class ColorUtils {
     return hslc.withLightness((hslc.lightness + amt).clamp(0.0, 1.0)).toColor();
   }
 
-  static Color parseHex(String value) => Color(int.parse(value.substring(1, 7), radix: 16) + 0xFF000000);
+  static Color parseHex(String value) =>
+      Color(int.parse(value.substring(1, 7), radix: 16) + 0xFF000000);
 
   static Color blend(Color dst, Color src, double opacity) {
     return Color.fromARGB(
       255,
-      (dst.red.toDouble() * (1.0 - opacity) + src.red.toDouble() * opacity).toInt(),
-      (dst.green.toDouble() * (1.0 - opacity) + src.green.toDouble() * opacity).toInt(),
-      (dst.blue.toDouble() * (1.0 - opacity) + src.blue.toDouble() * opacity).toInt(),
+      (dst.red.toDouble() * (1.0 - opacity) + src.red.toDouble() * opacity)
+          .toInt(),
+      (dst.green.toDouble() * (1.0 - opacity) + src.green.toDouble() * opacity)
+          .toInt(),
+      (dst.blue.toDouble() * (1.0 - opacity) + src.blue.toDouble() * opacity)
+          .toInt(),
     );
   }
 }

+ 2 - 2
frontend/app_flowy/packages/flowy_infra_ui/example/lib/overlay/overlay_screen.dart

@@ -218,8 +218,8 @@ class OverlayScreen extends StatelessWidget {
                           overlapBehaviour: providerContext
                               .read<OverlayDemoConfiguration>()
                               .overlapBehaviour,
-                          width: 200.0,
-                          height: 200.0,
+                          constraints:
+                              BoxConstraints.tight(const Size(200, 200)),
                         );
                       },
                       child: const Text('Show List Overlay'),

+ 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/appflowy_stype_popover.dart';
+export 'src/flowy_overlay/appflowy_popover.dart';

+ 40 - 4
frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_stype_popover.dart → frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart

@@ -1,7 +1,10 @@
-import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:flutter/material.dart';
 
+import 'package:flowy_infra/theme.dart';
+import 'package:flowy_infra_ui/style_widget/decoration.dart';
+import 'package:provider/provider.dart';
+
 class AppFlowyPopover extends StatelessWidget {
   final Widget child;
   final PopoverController? controller;
@@ -13,6 +16,7 @@ class AppFlowyPopover extends StatelessWidget {
   final PopoverMutex? mutex;
   final Offset? offset;
   final bool asBarrier;
+  final EdgeInsets margin;
 
   const AppFlowyPopover({
     Key? key,
@@ -26,6 +30,7 @@ class AppFlowyPopover extends StatelessWidget {
     this.offset,
     this.controller,
     this.asBarrier = false,
+    this.margin = const EdgeInsets.all(12),
   }) : super(key: key);
 
   @override
@@ -39,13 +44,44 @@ class AppFlowyPopover extends StatelessWidget {
       triggerActions: triggerActions,
       popupBuilder: (context) {
         final child = popupBuilder(context);
-        debugPrint('$child popover');
-        return OverlayContainer(
+        debugPrint('Show $child popover');
+        return _PopoverContainer(
           constraints: constraints,
-          child: popupBuilder(context),
+          margin: margin,
+          child: child,
         );
       },
       child: child,
     );
   }
 }
+
+class _PopoverContainer extends StatelessWidget {
+  final Widget child;
+  final BoxConstraints? constraints;
+  final EdgeInsets margin;
+  const _PopoverContainer({
+    required this.child,
+    required this.margin,
+    this.constraints,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = context.watch<AppTheme>();
+    final decoration = FlowyDecoration.decoration(
+      theme.surface,
+      theme.shadowColor.withOpacity(0.15),
+    );
+    return Material(
+      type: MaterialType.transparency,
+      child: Container(
+        padding: margin,
+        decoration: decoration,
+        constraints: constraints,
+        child: child,
+      ),
+    );
+  }
+}

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

@@ -4,7 +4,6 @@ import 'dart:ui';
 import 'package:flowy_infra_ui/src/flowy_overlay/layout.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
-export './overlay_container.dart';
 
 /// Specifies how overlay are anchored to the SourceWidget
 enum AnchorDirection {

+ 72 - 28
frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/list_overlay.dart

@@ -1,5 +1,10 @@
-import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
+import 'dart:math';
+
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flutter/material.dart';
+import 'package:flowy_infra/theme.dart';
+import 'package:flowy_infra_ui/style_widget/decoration.dart';
+import 'package:provider/provider.dart';
 
 class ListOverlayFooter {
   Widget widget;
@@ -16,46 +21,55 @@ class ListOverlay extends StatelessWidget {
   const ListOverlay({
     Key? key,
     required this.itemBuilder,
-    this.itemCount,
+    this.itemCount = 0,
     this.controller,
-    this.width = double.infinity,
-    this.height = double.infinity,
+    this.constraints = const BoxConstraints(),
     this.footer,
   }) : super(key: key);
 
   final IndexedWidgetBuilder itemBuilder;
-  final int? itemCount;
+  final int itemCount;
   final ScrollController? controller;
-  final double width;
-  final double height;
+  final BoxConstraints constraints;
   final ListOverlayFooter? footer;
 
   @override
   Widget build(BuildContext context) {
     const padding = EdgeInsets.symmetric(horizontal: 6, vertical: 6);
-    double totalHeight = height + padding.vertical;
+    double totalHeight = constraints.minHeight + padding.vertical;
     if (footer != null) {
       totalHeight = totalHeight + footer!.height + footer!.padding.vertical;
     }
 
+    final innerConstraints = BoxConstraints(
+      minHeight: totalHeight,
+      maxHeight: max(constraints.maxHeight, totalHeight),
+      minWidth: constraints.minWidth,
+      maxWidth: constraints.maxWidth,
+    );
+
+    List<Widget> children = [];
+    for (var i = 0; i < itemCount; i++) {
+      children.add(itemBuilder(context, i));
+    }
+
     return OverlayContainer(
-      constraints: BoxConstraints.tight(Size(width, totalHeight)),
+      constraints: innerConstraints,
       padding: padding,
       child: SingleChildScrollView(
-        child: Column(
-          children: [
-            ListView.builder(
-              shrinkWrap: true,
-              itemBuilder: itemBuilder,
-              itemCount: itemCount,
-              controller: controller,
-            ),
-            if (footer != null)
-              Padding(
-                padding: footer!.padding,
-                child: footer!.widget,
-              ),
-          ],
+        scrollDirection: Axis.horizontal,
+        child: IntrinsicWidth(
+          child: Column(
+            mainAxisSize: MainAxisSize.min,
+            children: [
+              ...children,
+              if (footer != null)
+                Padding(
+                  padding: footer!.padding,
+                  child: footer!.widget,
+                ),
+            ],
+          ),
         ),
       ),
     );
@@ -65,10 +79,9 @@ class ListOverlay extends StatelessWidget {
     BuildContext context, {
     required String identifier,
     required IndexedWidgetBuilder itemBuilder,
-    int? itemCount,
+    int itemCount = 0,
     ScrollController? controller,
-    double width = double.infinity,
-    double height = double.infinity,
+    BoxConstraints constraints = const BoxConstraints(),
     required BuildContext anchorContext,
     AnchorDirection? anchorDirection,
     FlowyOverlayDelegate? delegate,
@@ -82,8 +95,7 @@ class ListOverlay extends StatelessWidget {
         itemBuilder: itemBuilder,
         itemCount: itemCount,
         controller: controller,
-        width: width,
-        height: height,
+        constraints: constraints,
         footer: footer,
       ),
       identifier: identifier,
@@ -96,3 +108,35 @@ class ListOverlay extends StatelessWidget {
     );
   }
 }
+
+const overlayContainerPadding = EdgeInsets.all(12);
+
+class OverlayContainer extends StatelessWidget {
+  final Widget child;
+  final BoxConstraints? constraints;
+  final EdgeInsets padding;
+  const OverlayContainer({
+    required this.child,
+    this.constraints,
+    this.padding = overlayContainerPadding,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final theme =
+        context.watch<AppTheme?>() ?? AppTheme.fromType(ThemeType.light);
+    return Material(
+      type: MaterialType.transparency,
+      child: Container(
+        padding: padding,
+        decoration: FlowyDecoration.decoration(
+          theme.surface,
+          theme.shadowColor.withOpacity(0.15),
+        ),
+        constraints: constraints,
+        child: child,
+      ),
+    );
+  }
+}

+ 0 - 34
frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/overlay_container.dart

@@ -1,34 +0,0 @@
-import 'package:flowy_infra/theme.dart';
-import 'package:flowy_infra_ui/style_widget/decoration.dart';
-import 'package:flutter/material.dart';
-import 'package:provider/provider.dart';
-
-const overlayContainerPadding = EdgeInsets.all(12);
-
-class OverlayContainer extends StatelessWidget {
-  final Widget child;
-  final BoxConstraints? constraints;
-  final EdgeInsets padding;
-  const OverlayContainer({
-    required this.child,
-    this.constraints,
-    this.padding = overlayContainerPadding,
-    Key? key,
-  }) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    final theme =
-        context.watch<AppTheme?>() ?? AppTheme.fromType(ThemeType.light);
-    return Material(
-      type: MaterialType.transparency,
-      child: Container(
-        padding: padding,
-        decoration: FlowyDecoration.decoration(
-            theme.surface, theme.shadowColor.withOpacity(0.15)),
-        constraints: constraints,
-        child: child,
-      ),
-    );
-  }
-}

+ 7 - 2
frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/button.dart

@@ -12,6 +12,8 @@ class FlowyButton extends StatelessWidget {
   final Widget? rightIcon;
   final Color hoverColor;
   final bool isSelected;
+  final BorderRadius radius;
+
   const FlowyButton({
     Key? key,
     required this.text,
@@ -22,6 +24,7 @@ class FlowyButton extends StatelessWidget {
     this.rightIcon,
     this.hoverColor = Colors.transparent,
     this.isSelected = false,
+    this.radius = const BorderRadius.all(Radius.circular(6)),
   }) : super(key: key);
 
   @override
@@ -29,8 +32,10 @@ class FlowyButton extends StatelessWidget {
     return InkWell(
       onTap: onTap,
       child: FlowyHover(
-        style:
-            HoverStyle(borderRadius: BorderRadius.zero, hoverColor: hoverColor),
+        style: HoverStyle(
+          borderRadius: radius,
+          hoverColor: hoverColor,
+        ),
         onHover: onHover,
         setSelected: () => isSelected,
         builder: (context, onHover) => _render(),

+ 11 - 3
frontend/rust-lib/flowy-folder/src/manager.rs

@@ -221,8 +221,9 @@ impl DefaultFolderBuilder {
                     initial_quill_delta_string()
                 };
                 let _ = view_controller.set_latest_view(&view.id);
+                let layout_type = ViewLayoutTypePB::from(view.layout.clone());
                 let _ = view_controller
-                    .create_view(&view.id, ViewDataTypePB::Text, Bytes::from(view_data))
+                    .create_view(&view.id, ViewDataTypePB::Text, layout_type, Bytes::from(view_data))
                     .await?;
             }
         }
@@ -249,11 +250,17 @@ impl FolderManager {
 pub trait ViewDataProcessor {
     fn initialize(&self) -> FutureResult<(), FlowyError>;
 
-    fn create_container(&self, user_id: &str, view_id: &str, delta_data: Bytes) -> FutureResult<(), FlowyError>;
+    fn create_container(
+        &self,
+        user_id: &str,
+        view_id: &str,
+        layout: ViewLayoutTypePB,
+        delta_data: Bytes,
+    ) -> FutureResult<(), FlowyError>;
 
     fn close_container(&self, view_id: &str) -> FutureResult<(), FlowyError>;
 
-    fn get_delta_data(&self, view_id: &str) -> FutureResult<Bytes, FlowyError>;
+    fn get_view_data(&self, view_id: &str) -> FutureResult<Bytes, FlowyError>;
 
     fn create_default_view(
         &self,
@@ -267,6 +274,7 @@ pub trait ViewDataProcessor {
         user_id: &str,
         view_id: &str,
         data: Vec<u8>,
+        layout: ViewLayoutTypePB,
     ) -> FutureResult<Bytes, FlowyError>;
 
     fn data_type(&self) -> ViewDataTypePB;

+ 21 - 6
frontend/rust-lib/flowy-folder/src/services/view/controller.rs

@@ -1,5 +1,5 @@
 pub use crate::entities::view::ViewDataTypePB;
-use crate::entities::ViewInfoPB;
+use crate::entities::{ViewInfoPB, ViewLayoutTypePB};
 use crate::manager::{ViewDataProcessor, ViewDataProcessorMap};
 use crate::{
     dart_notification::{send_dart_notification, FolderNotification},
@@ -61,16 +61,28 @@ impl ViewController {
         let processor = self.get_data_processor(params.data_type.clone())?;
         let user_id = self.user.user_id()?;
         if params.view_content_data.is_empty() {
+            tracing::trace!("Create view with build-in data");
             let view_data = processor
                 .create_default_view(&user_id, &params.view_id, params.layout.clone())
                 .await?;
             params.view_content_data = view_data.to_vec();
         } else {
+            tracing::trace!("Create view with view data");
             let delta_data = processor
-                .create_view_from_delta_data(&user_id, &params.view_id, params.view_content_data.clone())
+                .create_view_from_delta_data(
+                    &user_id,
+                    &params.view_id,
+                    params.view_content_data.clone(),
+                    params.layout.clone(),
+                )
                 .await?;
             let _ = self
-                .create_view(&params.view_id, params.data_type.clone(), delta_data)
+                .create_view(
+                    &params.view_id,
+                    params.data_type.clone(),
+                    params.layout.clone(),
+                    delta_data,
+                )
                 .await?;
         };
 
@@ -84,6 +96,7 @@ impl ViewController {
         &self,
         view_id: &str,
         data_type: ViewDataTypePB,
+        layout_type: ViewLayoutTypePB,
         delta_data: Bytes,
     ) -> Result<(), FlowyError> {
         if delta_data.is_empty() {
@@ -91,7 +104,9 @@ impl ViewController {
         }
         let user_id = self.user.user_id()?;
         let processor = self.get_data_processor(data_type)?;
-        let _ = processor.create_container(&user_id, view_id, delta_data).await?;
+        let _ = processor
+            .create_container(&user_id, view_id, layout_type, delta_data)
+            .await?;
         Ok(())
     }
 
@@ -218,7 +233,7 @@ impl ViewController {
             .await?;
 
         let processor = self.get_data_processor(view_rev.data_type.clone())?;
-        let delta_bytes = processor.get_delta_data(view_id).await?;
+        let view_data = processor.get_view_data(view_id).await?;
         let duplicate_params = CreateViewParams {
             belong_to_id: view_rev.app_id.clone(),
             name: format!("{} (copy)", &view_rev.name),
@@ -226,7 +241,7 @@ impl ViewController {
             thumbnail: view_rev.thumbnail,
             data_type: view_rev.data_type.into(),
             layout: view_rev.layout.into(),
-            view_content_data: delta_bytes.to_vec(),
+            view_content_data: view_data.to_vec(),
             view_id: gen_view_id(),
         };
 

+ 0 - 5
frontend/rust-lib/flowy-grid/src/dart_notification.rs

@@ -33,8 +33,3 @@ impl std::convert::From<GridNotification> for i32 {
 pub fn send_dart_notification(id: &str, ty: GridNotification) -> DartNotifyBuilder {
     DartNotifyBuilder::new(id, ty, OBSERVABLE_CATEGORY)
 }
-
-#[tracing::instrument(level = "trace")]
-pub fn send_anonymous_dart_notification(ty: GridNotification) -> DartNotifyBuilder {
-    DartNotifyBuilder::new("", ty, OBSERVABLE_CATEGORY)
-}

+ 1 - 1
frontend/rust-lib/flowy-grid/src/event_handler.rs

@@ -44,7 +44,7 @@ pub(crate) async fn update_grid_setting_handler(
 
     let editor = manager.get_grid_editor(&params.grid_id)?;
     if let Some(insert_params) = params.insert_group {
-        let _ = editor.create_group(insert_params).await?;
+        let _ = editor.insert_group(insert_params).await?;
     }
 
     if let Some(delete_params) = params.delete_group {

+ 6 - 1
frontend/rust-lib/flowy-grid/src/macros.rs

@@ -73,7 +73,12 @@ macro_rules! impl_type_option {
                 match serde_json::from_str(s) {
                     Ok(obj) => obj,
                     Err(err) => {
-                        tracing::error!("{} convert from any data failed, {:?}", stringify!($target), err);
+                        tracing::error!(
+                            "{} type option deserialize from {} failed, {:?}",
+                            stringify!($target),
+                            s,
+                            err
+                        );
                         $target::default()
                     }
                 }

+ 16 - 3
frontend/rust-lib/flowy-grid/src/manager.rs

@@ -1,3 +1,4 @@
+use crate::entities::GridLayout;
 use crate::services::block_editor::GridBlockRevisionCompactor;
 use crate::services::grid_editor::{GridRevisionCompactor, GridRevisionEditor};
 use crate::services::grid_view_manager::make_grid_view_rev_manager;
@@ -178,10 +179,18 @@ impl GridManager {
 pub async fn make_grid_view_data(
     user_id: &str,
     view_id: &str,
+    layout: GridLayout,
     grid_manager: Arc<GridManager>,
     build_context: BuildGridContext,
 ) -> FlowyResult<Bytes> {
-    for block_meta_data in &build_context.blocks {
+    let BuildGridContext {
+        field_revs,
+        block_metas,
+        blocks,
+        grid_view_revision_data,
+    } = build_context;
+
+    for block_meta_data in &blocks {
         let block_id = &block_meta_data.block_id;
         // Indexing the block's rows
         block_meta_data.rows.iter().for_each(|row| {
@@ -198,7 +207,7 @@ pub async fn make_grid_view_data(
 
     // Will replace the grid_id with the value returned by the gen_grid_id()
     let grid_id = view_id.to_owned();
-    let grid_rev = GridRevision::from_build_context(&grid_id, build_context);
+    let grid_rev = GridRevision::from_build_context(&grid_id, field_revs, block_metas);
 
     // Create grid
     let grid_rev_delta = make_grid_delta(&grid_rev);
@@ -208,7 +217,11 @@ pub async fn make_grid_view_data(
     let _ = grid_manager.create_grid(&grid_id, repeated_revision).await?;
 
     // Create grid view
-    let grid_view = GridViewRevision::new(grid_id, view_id.to_owned());
+    let grid_view = if grid_view_revision_data.is_empty() {
+        GridViewRevision::new(grid_id, view_id.to_owned(), layout.into())
+    } else {
+        GridViewRevision::from_json(grid_view_revision_data)?
+    };
     let grid_view_delta = make_grid_view_delta(&grid_view);
     let grid_view_delta_bytes = grid_view_delta.json_bytes();
     let repeated_revision: RepeatedRevision =

+ 4 - 1
frontend/rust-lib/flowy-grid/src/services/cell/any_cell_data.rs

@@ -19,7 +19,10 @@ impl std::str::FromStr for AnyCellData {
     type Err = FlowyError;
 
     fn from_str(s: &str) -> Result<Self, Self::Err> {
-        let type_option_cell_data: AnyCellData = serde_json::from_str(s)?;
+        let type_option_cell_data: AnyCellData = serde_json::from_str(s).map_err(|err| {
+            let msg = format!("Deserialize {} to any cell data failed. Serde error: {}", s, err);
+            FlowyError::internal().context(msg)
+        })?;
         Ok(type_option_cell_data)
     }
 }

+ 21 - 12
frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs

@@ -1,6 +1,7 @@
 use crate::entities::FieldType;
 use crate::services::cell::{AnyCellData, CellBytes};
 use crate::services::field::*;
+use std::fmt::Debug;
 
 use flowy_error::{ErrorCode, FlowyError, FlowyResult};
 use flowy_grid_data_model::revision::{CellRevision, FieldRevision, FieldTypeRevision};
@@ -73,20 +74,28 @@ pub fn apply_cell_data_changeset<C: ToString, T: AsRef<FieldRevision>>(
     Ok(AnyCellData::new(s, field_type).json())
 }
 
-pub fn decode_any_cell_data<T: TryInto<AnyCellData>>(data: T, field_rev: &FieldRevision) -> CellBytes {
-    if let Ok(any_cell_data) = data.try_into() {
-        let AnyCellData { data, field_type } = any_cell_data;
-        let to_field_type = field_rev.ty.into();
-        match try_decode_cell_data(data.into(), field_rev, &field_type, &to_field_type) {
-            Ok(cell_bytes) => cell_bytes,
-            Err(e) => {
-                tracing::error!("Decode cell data failed, {:?}", e);
-                CellBytes::default()
+pub fn decode_any_cell_data<T: TryInto<AnyCellData, Error = FlowyError> + Debug>(
+    data: T,
+    field_rev: &FieldRevision,
+) -> CellBytes {
+    match data.try_into() {
+        Ok(any_cell_data) => {
+            let AnyCellData { data, field_type } = any_cell_data;
+            let to_field_type = field_rev.ty.into();
+            match try_decode_cell_data(data.into(), field_rev, &field_type, &to_field_type) {
+                Ok(cell_bytes) => cell_bytes,
+                Err(e) => {
+                    tracing::error!("Decode cell data failed, {:?}", e);
+                    CellBytes::default()
+                }
             }
         }
-    } else {
-        tracing::error!("Decode type option data failed");
-        CellBytes::default()
+        Err(_err) => {
+            // It's okay to ignore this error, because it's okay that the current cell can't
+            // display the existing cell data. For example, the UI of the text cell will be blank if
+            // the type of the data of cell is Number.
+            CellBytes::default()
+        }
     }
 }
 

+ 1 - 1
frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_tests.rs

@@ -24,7 +24,7 @@ mod tests {
                     assert_date(&type_option, 1647251762, None, "2022-03-14", &field_rev);
                 }
                 DateFormat::Local => {
-                    assert_date(&type_option, 1647251762, None, "2022/03/14", &field_rev);
+                    assert_date(&type_option, 1647251762, None, "03/14/2022", &field_rev);
                 }
             }
         }

+ 1 - 1
frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_type_option_entities.rs

@@ -153,7 +153,7 @@ impl DateFormat {
     // https://docs.rs/chrono/0.4.19/chrono/format/strftime/index.html
     pub fn format_str(&self) -> &'static str {
         match self {
-            DateFormat::Local => "%Y/%m/%d",
+            DateFormat::Local => "%m/%d/%Y",
             DateFormat::US => "%Y/%m/%d",
             DateFormat::ISO => "%Y-%m-%d",
             DateFormat::Friendly => "%b %d,%Y",

+ 12 - 7
frontend/rust-lib/flowy-grid/src/services/grid_editor.rs

@@ -179,23 +179,20 @@ impl GridRevisionEditor {
             None => Err(ErrorCode::FieldDoesNotExist.into()),
             Some(field_type) => {
                 let _ = self.update_field_rev(params, field_type).await?;
-                match self.view_manager.did_update_field(&field_id).await {
-                    Ok(_) => {}
-                    Err(e) => tracing::error!("View manager update field failed: {:?}", e),
-                }
                 let _ = self.notify_did_update_grid_field(&field_id).await?;
                 Ok(())
             }
         }
     }
 
+    // Replaces the field revision with new field revision.
     pub async fn replace_field(&self, field_rev: Arc<FieldRevision>) -> FlowyResult<()> {
         let field_id = field_rev.id.clone();
         let _ = self
             .modify(|grid_pad| Ok(grid_pad.replace_field_rev(field_rev.clone())?))
             .await?;
 
-        match self.view_manager.did_update_field(&field_rev.id).await {
+        match self.view_manager.did_update_field(&field_rev.id, false).await {
             Ok(_) => {}
             Err(e) => tracing::error!("View manager update field failed: {:?}", e),
         }
@@ -279,6 +276,7 @@ impl GridRevisionEditor {
     }
 
     async fn update_field_rev(&self, params: FieldChangesetParams, field_type: FieldType) -> FlowyResult<()> {
+        let mut is_type_option_changed = false;
         let _ = self
             .modify(|grid| {
                 let deserializer = TypeOptionJsonDeserializer(field_type);
@@ -319,6 +317,7 @@ impl GridRevisionEditor {
                             Ok(json_str) => {
                                 let field_type = field.ty;
                                 field.insert_type_option_str(&field_type, json_str);
+                                is_type_option_changed = true;
                                 is_changed = Some(())
                             }
                             Err(err) => {
@@ -333,7 +332,11 @@ impl GridRevisionEditor {
             })
             .await?;
 
-        match self.view_manager.did_update_field(&params.field_id).await {
+        match self
+            .view_manager
+            .did_update_field(&params.field_id, is_type_option_changed)
+            .await
+        {
             Ok(_) => {}
             Err(e) => tracing::error!("View manager update field failed: {:?}", e),
         }
@@ -537,7 +540,7 @@ impl GridRevisionEditor {
         self.view_manager.get_filters().await
     }
 
-    pub async fn create_group(&self, params: InsertGroupParams) -> FlowyResult<()> {
+    pub async fn insert_group(&self, params: InsertGroupParams) -> FlowyResult<()> {
         self.view_manager.insert_or_update_group(params).await
     }
 
@@ -673,6 +676,7 @@ impl GridRevisionEditor {
 
     pub async fn duplicate_grid(&self) -> FlowyResult<BuildGridContext> {
         let grid_pad = self.grid_pad.read().await;
+        let grid_view_revision_data = self.view_manager.duplicate_grid_view().await?;
         let original_blocks = grid_pad.get_block_meta_revs();
         let (duplicated_fields, duplicated_blocks) = grid_pad.duplicate_grid_block_meta().await;
 
@@ -698,6 +702,7 @@ impl GridRevisionEditor {
             field_revs: duplicated_fields.into_iter().map(Arc::new).collect(),
             block_metas: duplicated_blocks,
             blocks: blocks_meta_data,
+            grid_view_revision_data,
         })
     }
 

+ 14 - 7
frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs

@@ -74,6 +74,11 @@ impl GridViewRevisionEditor {
         })
     }
 
+    pub(crate) async fn duplicate_view_data(&self) -> FlowyResult<String> {
+        let json_str = self.pad.read().await.json_str()?;
+        Ok(json_str)
+    }
+
     pub(crate) async fn will_create_row(&self, row_rev: &mut RowRevision, params: &CreateRowParams) {
         if params.group_id.is_none() {
             return;
@@ -277,6 +282,7 @@ impl GridViewRevisionEditor {
         Ok(())
     }
 
+    #[tracing::instrument(level = "debug", skip_all, err)]
     pub(crate) async fn group_by_field(&self, field_id: &str) -> FlowyResult<()> {
         if let Some(field_rev) = self.field_delegate.get_field_rev(field_id).await {
             let new_group_controller = new_group_controller_with_field_rev(
@@ -374,13 +380,14 @@ impl GridViewRevisionEditor {
 async fn new_group_controller(
     user_id: String,
     view_id: String,
-    pad: Arc<RwLock<GridViewRevisionPad>>,
+    view_rev_pad: Arc<RwLock<GridViewRevisionPad>>,
     rev_manager: Arc<RevisionManager>,
     field_delegate: Arc<dyn GridViewFieldDelegate>,
     row_delegate: Arc<dyn GridViewRowDelegate>,
 ) -> FlowyResult<Box<dyn GroupController>> {
-    let configuration_reader = GroupConfigurationReaderImpl(pad.clone());
+    let configuration_reader = GroupConfigurationReaderImpl(view_rev_pad.clone());
     let field_revs = field_delegate.get_field_revs().await;
+    let layout = view_rev_pad.read().await.layout();
     // Read the group field or find a new group field
     let field_rev = configuration_reader
         .get_configuration()
@@ -391,24 +398,24 @@ async fn new_group_controller(
                 .find(|field_rev| field_rev.id == configuration.field_id)
                 .cloned()
         })
-        .unwrap_or_else(|| find_group_field(&field_revs).unwrap());
+        .unwrap_or_else(|| find_group_field(&field_revs, &layout).unwrap());
 
-    new_group_controller_with_field_rev(user_id, view_id, pad, rev_manager, field_rev, row_delegate).await
+    new_group_controller_with_field_rev(user_id, view_id, view_rev_pad, rev_manager, field_rev, row_delegate).await
 }
 
 async fn new_group_controller_with_field_rev(
     user_id: String,
     view_id: String,
-    pad: Arc<RwLock<GridViewRevisionPad>>,
+    view_rev_pad: Arc<RwLock<GridViewRevisionPad>>,
     rev_manager: Arc<RevisionManager>,
     field_rev: Arc<FieldRevision>,
     row_delegate: Arc<dyn GridViewRowDelegate>,
 ) -> FlowyResult<Box<dyn GroupController>> {
-    let configuration_reader = GroupConfigurationReaderImpl(pad.clone());
+    let configuration_reader = GroupConfigurationReaderImpl(view_rev_pad.clone());
     let configuration_writer = GroupConfigurationWriterImpl {
         user_id,
         rev_manager,
-        view_pad: pad,
+        view_pad: view_rev_pad,
     };
     let row_revs = row_delegate.gv_row_revs().await;
     make_group_controller(view_id, field_rev, row_revs, configuration_reader, configuration_writer).await

+ 14 - 2
frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs

@@ -56,6 +56,12 @@ impl GridViewManager {
         })
     }
 
+    pub(crate) async fn duplicate_grid_view(&self) -> FlowyResult<String> {
+        let editor = self.get_default_view_editor().await?;
+        let view_data = editor.duplicate_view_data().await?;
+        Ok(view_data)
+    }
+
     /// When the row was created, we may need to modify the [RowRevision] according to the [CreateRowParams].
     pub(crate) async fn will_create_row(&self, row_rev: &mut RowRevision, params: &CreateRowParams) {
         for view_editor in self.view_editors.iter() {
@@ -169,9 +175,15 @@ impl GridViewManager {
         Ok(())
     }
 
-    pub(crate) async fn did_update_field(&self, field_id: &str) -> FlowyResult<()> {
+    #[tracing::instrument(level = "trace", skip(self), err)]
+    pub(crate) async fn did_update_field(&self, field_id: &str, is_type_option_changed: bool) -> FlowyResult<()> {
         let view_editor = self.get_default_view_editor().await?;
-        let _ = view_editor.did_update_field(field_id).await?;
+        if is_type_option_changed {
+            let _ = view_editor.group_by_field(field_id).await?;
+        } else {
+            let _ = view_editor.did_update_field(field_id).await?;
+        }
+
         Ok(())
     }
 

+ 1 - 0
frontend/rust-lib/flowy-grid/src/services/group/action.rs

@@ -14,5 +14,6 @@ pub trait GroupAction: Send + Sync {
     fn can_group(&self, content: &str, cell_data: &Self::CellDataType) -> bool;
     fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec<GroupChangesetPB>;
     fn remove_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec<GroupChangesetPB>;
+    // Move row from one group to another
     fn move_row(&mut self, cell_data: &Self::CellDataType, context: MoveGroupRowContext) -> Vec<GroupChangesetPB>;
 }

+ 10 - 4
frontend/rust-lib/flowy-grid/src/services/group/controller.rs

@@ -86,8 +86,7 @@ where
     G: GroupGenerator<Context = GroupContext<C>, TypeOptionType = T>,
 {
     pub async fn new(field_rev: &Arc<FieldRevision>, mut configuration: GroupContext<C>) -> FlowyResult<Self> {
-        let field_type_rev = field_rev.ty;
-        let type_option = field_rev.get_type_option::<T>(field_type_rev);
+        let type_option = field_rev.get_type_option::<T>(field_rev.ty);
         let groups = G::generate_groups(&field_rev.id, &configuration, &type_option);
         let _ = configuration.init_groups(groups, true)?;
 
@@ -278,12 +277,19 @@ where
         }
     }
 
+    #[tracing::instrument(level = "trace", skip_all, err)]
     fn move_group_row(&mut self, context: MoveGroupRowContext) -> FlowyResult<Vec<GroupChangesetPB>> {
-        if let Some(cell_rev) = context.row_rev.cells.get(&self.field_id) {
-            let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), context.field_rev);
+        let cell_rev = match context.row_rev.cells.get(&self.field_id) {
+            Some(cell_rev) => Some(cell_rev.clone()),
+            None => self.default_cell_rev(),
+        };
+
+        if let Some(cell_rev) = cell_rev {
+            let cell_bytes = decode_any_cell_data(cell_rev.data, context.field_rev);
             let cell_data = cell_bytes.parser::<P>()?;
             Ok(self.move_row(&cell_data, context))
         } else {
+            tracing::warn!("Unexpected moving group row, changes should not be empty");
             Ok(vec![])
         }
     }

+ 2 - 2
frontend/rust-lib/flowy-grid/src/services/group/controller_impls/default_controller.rs

@@ -55,7 +55,7 @@ impl GroupControllerSharedOperation for DefaultGroupController {
         _row_rev: &RowRevision,
         _field_rev: &FieldRevision,
     ) -> FlowyResult<Vec<GroupChangesetPB>> {
-        todo!()
+        Ok(vec![])
     }
 
     fn did_delete_row(
@@ -63,7 +63,7 @@ impl GroupControllerSharedOperation for DefaultGroupController {
         _row_rev: &RowRevision,
         _field_rev: &FieldRevision,
     ) -> FlowyResult<Vec<GroupChangesetPB>> {
-        todo!()
+        Ok(vec![])
     }
 
     fn move_group_row(&mut self, _context: MoveGroupRowContext) -> FlowyResult<Vec<GroupChangesetPB>> {

+ 38 - 10
frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/util.rs

@@ -1,11 +1,12 @@
-use crate::entities::{GroupChangesetPB, InsertedRowPB, RowPB};
-use crate::services::cell::insert_select_option_cell;
-use crate::services::field::{SelectOptionCellDataPB, SelectOptionPB};
+use crate::entities::{FieldType, GroupChangesetPB, InsertedRowPB, RowPB};
+use crate::services::cell::{insert_checkbox_cell, insert_select_option_cell};
+use crate::services::field::{SelectOptionCellDataPB, SelectOptionPB, CHECK};
 use crate::services::group::configuration::GroupContext;
-use crate::services::group::{GeneratedGroup, Group};
-
 use crate::services::group::controller::MoveGroupRowContext;
-use flowy_grid_data_model::revision::{GroupRevision, RowRevision, SelectOptionGroupConfigurationRevision};
+use crate::services::group::{GeneratedGroup, Group};
+use flowy_grid_data_model::revision::{
+    CellRevision, FieldRevision, GroupRevision, RowRevision, SelectOptionGroupConfigurationRevision,
+};
 
 pub type SelectOptionGroupContext = GroupContext<SelectOptionGroupConfigurationRevision>;
 
@@ -109,10 +110,16 @@ pub fn move_group_row(group: &mut Group, context: &mut MoveGroupRowContext) -> O
 
         // Update the corresponding row's cell content.
         if from_index.is_none() {
-            tracing::debug!("Mark row:{} belong to group:{}", row_rev.id, group.id);
-            let cell_rev = insert_select_option_cell(group.id.clone(), field_rev);
-            row_changeset.cell_by_field_id.insert(field_rev.id.clone(), cell_rev);
-            changeset.updated_rows.push(RowPB::from(*row_rev));
+            let cell_rev = make_inserted_cell_rev(&group.id, field_rev);
+            if let Some(cell_rev) = cell_rev {
+                tracing::debug!(
+                    "Update content of the cell in the row:{} to group:{}",
+                    row_rev.id,
+                    group.id
+                );
+                row_changeset.cell_by_field_id.insert(field_rev.id.clone(), cell_rev);
+                changeset.updated_rows.push(RowPB::from(*row_rev));
+            }
         }
     }
     if changeset.is_empty() {
@@ -122,6 +129,27 @@ pub fn move_group_row(group: &mut Group, context: &mut MoveGroupRowContext) -> O
     }
 }
 
+pub fn make_inserted_cell_rev(group_id: &str, field_rev: &FieldRevision) -> Option<CellRevision> {
+    let field_type: FieldType = field_rev.ty.into();
+    match field_type {
+        FieldType::SingleSelect => {
+            let cell_rev = insert_select_option_cell(group_id.to_owned(), field_rev);
+            Some(cell_rev)
+        }
+        FieldType::MultiSelect => {
+            let cell_rev = insert_select_option_cell(group_id.to_owned(), field_rev);
+            Some(cell_rev)
+        }
+        FieldType::Checkbox => {
+            let cell_rev = insert_checkbox_cell(group_id == CHECK, field_rev);
+            Some(cell_rev)
+        }
+        _ => {
+            tracing::warn!("Unknown field type: {:?}", field_type);
+            None
+        }
+    }
+}
 pub fn generate_select_option_groups(
     _field_id: &str,
     _group_ctx: &SelectOptionGroupContext,

+ 12 - 10
frontend/rust-lib/flowy-grid/src/services/group/group_util.rs

@@ -8,7 +8,7 @@ use crate::services::group::{
 use flowy_error::FlowyResult;
 use flowy_grid_data_model::revision::{
     CheckboxGroupConfigurationRevision, DateGroupConfigurationRevision, FieldRevision, GroupConfigurationRevision,
-    NumberGroupConfigurationRevision, RowRevision, SelectOptionGroupConfigurationRevision,
+    LayoutRevision, NumberGroupConfigurationRevision, RowRevision, SelectOptionGroupConfigurationRevision,
     TextGroupConfigurationRevision, UrlGroupConfigurationRevision,
 };
 use std::sync::Arc;
@@ -62,15 +62,17 @@ where
     Ok(group_controller)
 }
 
-pub fn find_group_field(field_revs: &[Arc<FieldRevision>]) -> Option<Arc<FieldRevision>> {
-    let field_rev = field_revs
-        .iter()
-        .find(|field_rev| {
-            let field_type: FieldType = field_rev.ty.into();
-            field_type.can_be_group()
-        })
-        .cloned();
-    field_rev
+pub fn find_group_field(field_revs: &[Arc<FieldRevision>], layout: &LayoutRevision) -> Option<Arc<FieldRevision>> {
+    match layout {
+        LayoutRevision::Table => field_revs.iter().find(|field_rev| field_rev.is_primary).cloned(),
+        LayoutRevision::Board => field_revs
+            .iter()
+            .find(|field_rev| {
+                let field_type: FieldType = field_rev.ty.into();
+                field_type.can_be_group()
+            })
+            .cloned(),
+    }
 }
 
 pub fn default_group_configuration(field_rev: &FieldRevision) -> GroupConfigurationRevision {

+ 1 - 0
frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs

@@ -127,6 +127,7 @@ fn make_test_grid() -> BuildGridContext {
                 let text_field = FieldBuilder::new(RichTextTypeOptionBuilder::default())
                     .name("Name")
                     .visibility(true)
+                    .primary(true)
                     .build();
                 grid_builder.add_field(text_field);
             }

+ 2 - 5
frontend/rust-lib/flowy-grid/tests/grid/group_test/test.rs

@@ -382,11 +382,8 @@ async fn group_insert_single_select_option_test() {
         AssertGroupCount(5),
     ];
     test.run_scripts(scripts).await;
-
-    // the group at index 4 is the default_group, so the new insert group will be the
-    // index 3.
-    let group_3 = test.group_at_index(3).await;
-    assert_eq!(group_3.desc, new_option_name);
+    let new_group = test.group_at_index(0).await;
+    assert_eq!(new_group.desc, new_option_name);
 }
 
 #[tokio::test]

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

@@ -7,6 +7,7 @@ use flowy_folder::{
     event_map::{FolderCouldServiceV1, WorkspaceDatabase, WorkspaceUser},
     manager::FolderManager,
 };
+use flowy_grid::entities::GridLayout;
 use flowy_grid::manager::{make_grid_view_data, GridManager};
 use flowy_grid::util::{make_default_board, make_default_grid};
 use flowy_grid_data_model::revision::BuildGridContext;
@@ -142,7 +143,15 @@ impl ViewDataProcessor for TextBlockViewDataProcessor {
         FutureResult::new(async move { manager.init() })
     }
 
-    fn create_container(&self, user_id: &str, view_id: &str, delta_data: Bytes) -> FutureResult<(), FlowyError> {
+    fn create_container(
+        &self,
+        user_id: &str,
+        view_id: &str,
+        layout: ViewLayoutTypePB,
+        delta_data: Bytes,
+    ) -> FutureResult<(), FlowyError> {
+        // Only accept Document type
+        debug_assert_eq!(layout, ViewLayoutTypePB::Document);
         let repeated_revision: RepeatedRevision = Revision::initial_revision(user_id, view_id, delta_data).into();
         let view_id = view_id.to_string();
         let manager = self.0.clone();
@@ -161,7 +170,7 @@ impl ViewDataProcessor for TextBlockViewDataProcessor {
         })
     }
 
-    fn get_delta_data(&self, view_id: &str) -> FutureResult<Bytes, FlowyError> {
+    fn get_view_data(&self, view_id: &str) -> FutureResult<Bytes, FlowyError> {
         let view_id = view_id.to_string();
         let manager = self.0.clone();
         FutureResult::new(async move {
@@ -196,7 +205,9 @@ impl ViewDataProcessor for TextBlockViewDataProcessor {
         _user_id: &str,
         _view_id: &str,
         data: Vec<u8>,
+        layout: ViewLayoutTypePB,
     ) -> FutureResult<Bytes, FlowyError> {
+        debug_assert_eq!(layout, ViewLayoutTypePB::Document);
         FutureResult::new(async move { Ok(Bytes::from(data)) })
     }
 
@@ -211,7 +222,13 @@ impl ViewDataProcessor for GridViewDataProcessor {
         FutureResult::new(async { Ok(()) })
     }
 
-    fn create_container(&self, user_id: &str, view_id: &str, delta_data: Bytes) -> FutureResult<(), FlowyError> {
+    fn create_container(
+        &self,
+        user_id: &str,
+        view_id: &str,
+        _layout: ViewLayoutTypePB,
+        delta_data: Bytes,
+    ) -> FutureResult<(), FlowyError> {
         let repeated_revision: RepeatedRevision = Revision::initial_revision(user_id, view_id, delta_data).into();
         let view_id = view_id.to_string();
         let grid_manager = self.0.clone();
@@ -230,7 +247,7 @@ impl ViewDataProcessor for GridViewDataProcessor {
         })
     }
 
-    fn get_delta_data(&self, view_id: &str) -> FutureResult<Bytes, FlowyError> {
+    fn get_view_data(&self, view_id: &str) -> FutureResult<Bytes, FlowyError> {
         let view_id = view_id.to_string();
         let grid_manager = self.0.clone();
         FutureResult::new(async move {
@@ -246,19 +263,22 @@ impl ViewDataProcessor for GridViewDataProcessor {
         view_id: &str,
         layout: ViewLayoutTypePB,
     ) -> FutureResult<Bytes, FlowyError> {
-        let build_context = match layout {
-            ViewLayoutTypePB::Grid => make_default_grid(),
-            ViewLayoutTypePB::Board => make_default_board(),
+        let (build_context, layout) = match layout {
+            ViewLayoutTypePB::Grid => (make_default_grid(), GridLayout::Table),
+            ViewLayoutTypePB::Board => (make_default_board(), GridLayout::Board),
             ViewLayoutTypePB::Document => {
                 return FutureResult::new(async move {
                     Err(FlowyError::internal().context(format!("Can't handle {:?} layout type", layout)))
                 });
             }
         };
+
         let user_id = user_id.to_string();
         let view_id = view_id.to_string();
         let grid_manager = self.0.clone();
-        FutureResult::new(async move { make_grid_view_data(&user_id, &view_id, grid_manager, build_context).await })
+        FutureResult::new(
+            async move { make_grid_view_data(&user_id, &view_id, layout, grid_manager, build_context).await },
+        )
     }
 
     fn create_view_from_delta_data(
@@ -266,15 +286,26 @@ impl ViewDataProcessor for GridViewDataProcessor {
         user_id: &str,
         view_id: &str,
         data: Vec<u8>,
+        layout: ViewLayoutTypePB,
     ) -> FutureResult<Bytes, FlowyError> {
         let user_id = user_id.to_string();
         let view_id = view_id.to_string();
         let grid_manager = self.0.clone();
 
+        let layout = match layout {
+            ViewLayoutTypePB::Grid => GridLayout::Table,
+            ViewLayoutTypePB::Board => GridLayout::Board,
+            ViewLayoutTypePB::Document => {
+                return FutureResult::new(async move {
+                    Err(FlowyError::internal().context(format!("Can't handle {:?} layout type", layout)))
+                });
+            }
+        };
+
         FutureResult::new(async move {
             let bytes = Bytes::from(data);
             let build_context = BuildGridContext::try_from(bytes)?;
-            make_grid_view_data(&user_id, &view_id, grid_manager, build_context).await
+            make_grid_view_data(&user_id, &view_id, layout, grid_manager, build_context).await
         })
     }
 

+ 0 - 2
frontend/scripts/install_dev_env/install_macos.sh

@@ -34,8 +34,6 @@ else
    printMessage "Skipping Rust installation."
 fi
 
-abvc
-
 # Install sqllite
 printMessage "Installing sqlLite3."
 brew install sqlite3 

+ 10 - 3
shared-lib/flowy-grid-data-model/src/revision/grid_rev.rs

@@ -34,11 +34,15 @@ impl GridRevision {
         }
     }
 
-    pub fn from_build_context(grid_id: &str, context: BuildGridContext) -> Self {
+    pub fn from_build_context(
+        grid_id: &str,
+        field_revs: Vec<Arc<FieldRevision>>,
+        block_metas: Vec<GridBlockMetaRevision>,
+    ) -> Self {
         Self {
             grid_id: grid_id.to_owned(),
-            fields: context.field_revs,
-            blocks: context.block_metas.into_iter().map(Arc::new).collect(),
+            fields: field_revs,
+            blocks: block_metas.into_iter().map(Arc::new).collect(),
         }
     }
 }
@@ -188,6 +192,9 @@ pub struct BuildGridContext {
     pub field_revs: Vec<Arc<FieldRevision>>,
     pub block_metas: Vec<GridBlockMetaRevision>,
     pub blocks: Vec<GridBlockRevision>,
+
+    // String in JSON format. It can be deserialized into [GridViewRevision]
+    pub grid_view_revision_data: String,
 }
 
 impl BuildGridContext {

+ 6 - 2
shared-lib/flowy-grid-data-model/src/revision/grid_view.rs

@@ -48,16 +48,20 @@ pub struct GridViewRevision {
 }
 
 impl GridViewRevision {
-    pub fn new(grid_id: String, view_id: String) -> Self {
+    pub fn new(grid_id: String, view_id: String, layout: LayoutRevision) -> Self {
         GridViewRevision {
             view_id,
             grid_id,
-            layout: Default::default(),
+            layout,
             filters: Default::default(),
             groups: Default::default(),
             // row_orders: vec![],
         }
     }
+
+    pub fn from_json(json: String) -> Result<Self, serde_json::Error> {
+        serde_json::from_str(&json)
+    }
 }
 
 #[derive(Debug, Clone, Default, Serialize, Deserialize)]

+ 6 - 2
shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs

@@ -103,8 +103,12 @@ impl GridRevisionPad {
             |grid_meta| match grid_meta.fields.iter().position(|field| field.id == field_id) {
                 None => Ok(None),
                 Some(index) => {
-                    grid_meta.fields.remove(index);
-                    Ok(Some(()))
+                    if grid_meta.fields[index].is_primary {
+                        Err(CollaborateError::can_not_delete_primary_field())
+                    } else {
+                        grid_meta.fields.remove(index);
+                        Ok(Some(()))
+                    }
                 }
             },
         )

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff