Bläddra i källkod

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

Lucas.Xu 2 år sedan
förälder
incheckning
9e40b7f992
53 ändrade filer med 970 tillägg och 321 borttagningar
  1. 0 2
      .github/workflows/appflowy_editor_test.yml
  2. 1 1
      .github/workflows/dart_test.yml
  3. 1 1
      .github/workflows/release.yml
  4. 54 7
      frontend/Makefile.toml
  5. 19 14
      frontend/app_flowy/assets/translations/es-VE.json
  6. 1 1
      frontend/app_flowy/lib/plugins/board/board.dart
  7. 16 0
      frontend/app_flowy/lib/plugins/board/tests/integrate_test/card_test.dart
  8. 6 1
      frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_data_controller.dart
  9. 25 4
      frontend/app_flowy/lib/plugins/grid/application/grid_bloc.dart
  10. 1 0
      frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart
  11. 1 1
      frontend/app_flowy/lib/plugins/grid/grid.dart
  12. 6 3
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_container.dart
  13. 11 7
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/checkbox_cell.dart
  14. 5 1
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_cell.dart
  15. 16 12
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/number_cell.dart
  16. 5 1
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart
  17. 16 4
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/text_field.dart
  18. 16 12
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/text_cell.dart
  19. 12 8
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/url_cell.dart
  20. 17 13
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_editor.dart
  21. 1 5
      frontend/app_flowy/lib/startup/deps_resolver.dart
  22. 1 1
      frontend/app_flowy/lib/startup/plugin/plugin.dart
  23. 2 2
      frontend/app_flowy/lib/startup/tasks/rust_sdk.dart
  24. 35 19
      frontend/app_flowy/lib/workspace/application/app/app_bloc.dart
  25. 9 4
      frontend/app_flowy/lib/workspace/application/app/app_service.dart
  26. 6 4
      frontend/app_flowy/lib/workspace/application/view/view_bloc.dart
  27. 2 2
      frontend/app_flowy/lib/workspace/application/workspace/workspace_service.dart
  28. 6 7
      frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/header.dart
  29. 0 1
      frontend/app_flowy/lib/workspace/presentation/home/menu/app/menu_app.dart
  30. 60 39
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart
  31. 2 3
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart
  32. 12 11
      frontend/app_flowy/packages/flowy_sdk/lib/ffi.dart
  33. 34 5
      frontend/app_flowy/pubspec.lock
  34. 2 0
      frontend/app_flowy/pubspec.yaml
  35. 44 0
      frontend/app_flowy/test/bloc_test/grid_test/grid_bloc_test.dart
  36. 36 0
      frontend/app_flowy/test/bloc_test/grid_test/select_option_bloc_test.dart
  37. 124 0
      frontend/app_flowy/test/bloc_test/grid_test/util.dart
  38. 104 0
      frontend/app_flowy/test/bloc_test/menu_test/app_bloc_test.dart
  39. 109 0
      frontend/app_flowy/test/util.dart
  40. 0 41
      frontend/app_flowy/test/util/test_env.dart
  41. 0 29
      frontend/app_flowy/test/workspace_bloc_test.dart
  42. 1 2
      frontend/rust-lib/flowy-grid/src/manager.rs
  43. 1 0
      frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/mod.rs
  44. 2 1
      frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/multi_select_type_option.rs
  45. 23 21
      frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_type_option.rs
  46. 2 1
      frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/single_select_type_option.rs
  47. 63 0
      frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/type_option_transform.rs
  48. 3 1
      frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs
  49. 3 3
      frontend/rust-lib/flowy-grid/src/services/grid_editor.rs
  50. 27 17
      frontend/scripts/makefile/desktop.toml
  51. 19 1
      frontend/scripts/makefile/tool.toml
  52. 7 6
      shared-lib/flowy-ast/src/ast.rs
  53. 1 2
      shared-lib/flowy-ast/src/ty_ext.rs

+ 0 - 2
.github/workflows/appflowy_editor_test.yml

@@ -4,8 +4,6 @@ on:
   push:
   push:
     branches:
     branches:
       - "main"
       - "main"
-    paths:
-      - "frontend/app_flowy/packages/appflowy_editor/**"
 
 
   pull_request:
   pull_request:
     branches:
     branches:

+ 1 - 1
.github/workflows/dart_test.yml

@@ -67,7 +67,7 @@ jobs:
       - name: Build FlowySDK
       - name: Build FlowySDK
         working-directory: frontend
         working-directory: frontend
         run: |
         run: |
-          cargo make --profile development-linux-x86_64 flowy-sdk-dev
+          cargo make --profile test-linux test-lib-build
 
 
       - name: Code Generation
       - name: Code Generation
         working-directory: frontend/app_flowy
         working-directory: frontend/app_flowy

+ 1 - 1
.github/workflows/release.yml

@@ -118,7 +118,7 @@ jobs:
 
 
       - name: Archive macOS app
       - name: Archive macOS app
         working-directory: ${{ env.MACOS_APP_RELEASE_PATH }}
         working-directory: ${{ env.MACOS_APP_RELEASE_PATH }}
-        run: zip -qr ${{ env.MACOS_X86_ZIP_NAME }} AppFlowy.app
+        run: zip --symlinks -qr ${{ env.MACOS_X86_ZIP_NAME }} AppFlowy.app
 
 
       - name: Upload Release Asset
       - name: Upload Release Asset
         uses: actions/upload-release-asset@v1
         uses: actions/upload-release-asset@v1

+ 54 - 7
frontend/Makefile.toml

@@ -40,10 +40,15 @@ PRODUCT_NAME = "AppFlowy"
 #   for cdylib:
 #   for cdylib:
 #       if (Platform.isMacOS) return DynamicLibrary.open('${prefix}/libdart_ffi.dylib');
 #       if (Platform.isMacOS) return DynamicLibrary.open('${prefix}/libdart_ffi.dylib');
 CRATE_TYPE = "staticlib"
 CRATE_TYPE = "staticlib"
-SDK_EXT = "a"
+LIB_EXT = "a"
 APP_ENVIRONMENT = "local"
 APP_ENVIRONMENT = "local"
 FLUTTER_FLOWY_SDK_PATH = "app_flowy/packages/flowy_sdk"
 FLUTTER_FLOWY_SDK_PATH = "app_flowy/packages/flowy_sdk"
 PROTOBUF_DERIVE_CACHE = "../shared-lib/flowy-derive/src/derive_cache/derive_cache.rs"
 PROTOBUF_DERIVE_CACHE = "../shared-lib/flowy-derive/src/derive_cache/derive_cache.rs"
+# Test default config
+TEST_CRATE_TYPE = "cdylib"
+TEST_LIB_EXT = "dylib"
+TEST_BUILD_FLAG = "debug"
+TEST_COMPILE_TARGET = "x86_64-apple-darwin"
 
 
 [env.development-mac-arm64]
 [env.development-mac-arm64]
 RUST_LOG = "info"
 RUST_LOG = "info"
@@ -88,7 +93,7 @@ BUILD_FLAG = "debug"
 FLUTTER_OUTPUT_DIR = "Debug"
 FLUTTER_OUTPUT_DIR = "Debug"
 PRODUCT_EXT = "exe"
 PRODUCT_EXT = "exe"
 CRATE_TYPE = "cdylib"
 CRATE_TYPE = "cdylib"
-SDK_EXT = "dll"
+LIB_EXT = "dll"
 
 
 [env.production-windows-x86]
 [env.production-windows-x86]
 BUILD_FLAG = "release"
 BUILD_FLAG = "release"
@@ -97,7 +102,7 @@ RUST_COMPILE_TARGET = "x86_64-pc-windows-msvc"
 FLUTTER_OUTPUT_DIR = "Release"
 FLUTTER_OUTPUT_DIR = "Release"
 PRODUCT_EXT = "exe"
 PRODUCT_EXT = "exe"
 CRATE_TYPE = "cdylib"
 CRATE_TYPE = "cdylib"
-SDK_EXT = "dll"
+LIB_EXT = "dll"
 APP_ENVIRONMENT = "production"
 APP_ENVIRONMENT = "production"
 
 
 [env.development-linux-x86_64]
 [env.development-linux-x86_64]
@@ -106,7 +111,7 @@ RUST_COMPILE_TARGET = "x86_64-unknown-linux-gnu"
 BUILD_FLAG = "debug"
 BUILD_FLAG = "debug"
 CRATE_TYPE = "cdylib"
 CRATE_TYPE = "cdylib"
 FLUTTER_OUTPUT_DIR = "Debug"
 FLUTTER_OUTPUT_DIR = "Debug"
-SDK_EXT = "so"
+LIB_EXT = "so"
 LINUX_ARCH = "x64"
 LINUX_ARCH = "x64"
 
 
 [env.production-linux-x86_64]
 [env.production-linux-x86_64]
@@ -115,7 +120,7 @@ TARGET_OS = "linux"
 RUST_COMPILE_TARGET = "x86_64-unknown-linux-gnu"
 RUST_COMPILE_TARGET = "x86_64-unknown-linux-gnu"
 CRATE_TYPE = "cdylib"
 CRATE_TYPE = "cdylib"
 FLUTTER_OUTPUT_DIR = "Release"
 FLUTTER_OUTPUT_DIR = "Release"
-SDK_EXT = "so"
+LIB_EXT = "so"
 LINUX_ARCH = "x64"
 LINUX_ARCH = "x64"
 APP_ENVIRONMENT = "production"
 APP_ENVIRONMENT = "production"
 
 
@@ -125,7 +130,7 @@ RUST_COMPILE_TARGET = "aarch64-unknown-linux-gnu"
 BUILD_FLAG = "debug"
 BUILD_FLAG = "debug"
 CRATE_TYPE = "cdylib"
 CRATE_TYPE = "cdylib"
 FLUTTER_OUTPUT_DIR = "Debug"
 FLUTTER_OUTPUT_DIR = "Debug"
-SDK_EXT = "so"
+LIB_EXT = "so"
 LINUX_ARCH = "arm64"
 LINUX_ARCH = "arm64"
 
 
 [env.production-linux-aarch64]
 [env.production-linux-aarch64]
@@ -134,7 +139,7 @@ TARGET_OS = "linux"
 RUST_COMPILE_TARGET = "aarch64-unknown-linux-gnu"
 RUST_COMPILE_TARGET = "aarch64-unknown-linux-gnu"
 CRATE_TYPE = "cdylib"
 CRATE_TYPE = "cdylib"
 FLUTTER_OUTPUT_DIR = "Release"
 FLUTTER_OUTPUT_DIR = "Release"
-SDK_EXT = "so"
+LIB_EXT = "so"
 LINUX_ARCH = "arm64"
 LINUX_ARCH = "arm64"
 APP_ENVIRONMENT = "production"
 APP_ENVIRONMENT = "production"
 
 
@@ -197,6 +202,46 @@ script = [
 ]
 ]
 script_runner = "@duckscript"
 script_runner = "@duckscript"
 
 
+[env.test-macos]
+TEST_CRATE_TYPE = "cdylib"
+TEST_LIB_EXT = "dylib"
+# For the moment, the DynamicLibrary only supports open x86_64 architectures binary.
+TEST_COMPILE_TARGET = "x86_64-apple-darwin"
+
+[env.test-linux]
+TEST_CRATE_TYPE = "cdylib"
+TEST_LIB_EXT = "so"
+TEST_COMPILE_TARGET = "x86_64-unknown-linux-gnu"
+
+[env.test-windows]
+TEST_CRATE_TYPE = "cdylib"
+TEST_LIB_EXT = "dll"
+TEST_COMPILE_TARGET = "x86_64-pc-windows-msvc"
+
+[tasks.setup-test-crate-type]
+private = true
+script = [
+    """
+      toml = readfile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml
+      val = replace ${toml} "staticlib" ${TEST_CRATE_TYPE}
+      result = writefile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml ${val}
+      assert ${result}
+      """,
+]
+script_runner = "@duckscript"
+
+[tasks.restore-test-crate-type]
+private = true
+script = [
+    """
+      toml = readfile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml
+      val = replace ${toml} ${TEST_CRATE_TYPE} "staticlib"
+      result = writefile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml ${val}
+      assert ${result}
+      """,
+]
+script_runner = "@duckscript"
+
 [tasks.test-build]
 [tasks.test-build]
 condition = { env_set = ["FLUTTER_FLOWY_SDK_PATH"] }
 condition = { env_set = ["FLUTTER_FLOWY_SDK_PATH"] }
 script = ["""
 script = ["""
@@ -204,3 +249,5 @@ script = ["""
       cargo build -vv --features=dart
       cargo build -vv --features=dart
       """]
       """]
 script_runner = "@shell"
 script_runner = "@shell"
+
+

+ 19 - 14
frontend/app_flowy/assets/translations/es-VE.json

@@ -7,7 +7,7 @@
   "letsGoButtonText": "Vamos",
   "letsGoButtonText": "Vamos",
   "title": "Título",
   "title": "Título",
   "signUp": {
   "signUp": {
-    "buttonText": "Registar",
+    "buttonText": "Registrar",
     "title": "Registrar en @:appName",
     "title": "Registrar en @:appName",
     "getStartedText": "Empezar",
     "getStartedText": "Empezar",
     "emptyPasswordError": "La contraseña no puede estar en blanco",
     "emptyPasswordError": "La contraseña no puede estar en blanco",
@@ -16,7 +16,7 @@
     "alreadyHaveAnAccount": "¿Posee credenciales?",
     "alreadyHaveAnAccount": "¿Posee credenciales?",
     "emailHint": "Correo",
     "emailHint": "Correo",
     "passwordHint": "Contraseña",
     "passwordHint": "Contraseña",
-    "repeatPasswordHint": "Repite la contraseña"
+    "repeatPasswordHint": "Repetir contraseña"
   },
   },
   "signIn": {
   "signIn": {
     "loginTitle": "Ingresa a @:appName",
     "loginTitle": "Ingresa a @:appName",
@@ -58,7 +58,7 @@
     }
     }
   },
   },
   "deletePagePrompt": {
   "deletePagePrompt": {
-    "text": "Esta paágina esta en la Papelera",
+    "text": "Esta página está en la Papelera",
     "restore": "Recuperar página",
     "restore": "Recuperar página",
     "deletePermanent": "Eliminar permanentemente"
     "deletePermanent": "Eliminar permanentemente"
   },
   },
@@ -69,7 +69,7 @@
     "debug": {
     "debug": {
       "name": "Información de depuración",
       "name": "Información de depuración",
       "success": "¡Información copiada!",
       "success": "¡Información copiada!",
-      "fail": "No fué posible copiar la información"
+      "fail": "No fue posible copiar la información"
     }
     }
   },
   },
   "menuAppHeader": {
   "menuAppHeader": {
@@ -167,15 +167,15 @@
       "singleSelectFieldName": "Seleccionar",
       "singleSelectFieldName": "Seleccionar",
       "multiSelectFieldName": "Selección múltiple",
       "multiSelectFieldName": "Selección múltiple",
       "urlFieldName": "URL",
       "urlFieldName": "URL",
-      "numberFormat": " Formato numérico",
-      "dateFormat": " Formato de fecha",
-      "includeTime": " Incluir tiempo",
+      "numberFormat": "Formato numérico",
+      "dateFormat": "Formato de fecha",
+      "includeTime": "Incluir tiempo",
       "dateFormatFriendly": "Mes Día, Año",
       "dateFormatFriendly": "Mes Día, Año",
       "dateFormatISO": "Año-Mes-Día",
       "dateFormatISO": "Año-Mes-Día",
       "dateFormatLocal": "Mes/Día/Año",
       "dateFormatLocal": "Mes/Día/Año",
       "dateFormatUS": "Año/Mes/Día",
       "dateFormatUS": "Año/Mes/Día",
-      "timeFormat": " Time format",
-      "invalidTimeFormat": "Formato de tiempo",
+      "timeFormat": "Formato de tiempo",
+      "invalidTimeFormat": "Formato de tiempo inválido",
       "timeFormatTwelveHour": "12 horas",
       "timeFormatTwelveHour": "12 horas",
       "timeFormatTwentyFourHour": "24 horas",
       "timeFormatTwentyFourHour": "24 horas",
       "addSelectOption": "Añadir una opción",
       "addSelectOption": "Añadir una opción",
@@ -205,17 +205,22 @@
       "panelTitle": "Selecciona una opción o crea una",
       "panelTitle": "Selecciona una opción o crea una",
       "searchOption": "Buscar una opción"
       "searchOption": "Buscar una opción"
     },
     },
-    "menuName": "Grid"
+    "menuName": "Cuadrícula"
   },
   },
   "document": {
   "document": {
-    "menuName": "Doc",
+    "menuName": "Documento",
     "date": {
     "date": {
       "timeHintTextInTwelveHour": "01:00 PM",
       "timeHintTextInTwelveHour": "01:00 PM",
       "timeHintTextInTwentyFourHour": "13:00"
       "timeHintTextInTwentyFourHour": "13:00"
     }
     }
   },
   },
   "sideBar": {
   "sideBar": {
-    "openSidebar": "Open sidebar",
-    "closeSidebar": "Close sidebar"
+    "openSidebar": "Abrir panel lateral",
+    "closeSidebar": "Cerrar panel lateral"
+  },
+    "board": {
+    "column": {
+      "create_new_card": "Nuevo"
+    }
   }
   }
-}
+}

+ 1 - 1
frontend/app_flowy/lib/plugins/board/board.dart

@@ -27,7 +27,7 @@ class BoardPluginBuilder implements PluginBuilder {
   ViewDataTypePB get dataType => ViewDataTypePB.Database;
   ViewDataTypePB get dataType => ViewDataTypePB.Database;
 
 
   @override
   @override
-  ViewLayoutTypePB? get subDataType => ViewLayoutTypePB.Board;
+  ViewLayoutTypePB? get layoutType => ViewLayoutTypePB.Board;
 }
 }
 
 
 class BoardPluginConfig implements PluginConfig {
 class BoardPluginConfig implements PluginConfig {

+ 16 - 0
frontend/app_flowy/lib/plugins/board/tests/integrate_test/card_test.dart

@@ -0,0 +1,16 @@
+// import 'package:flutter_test/flutter_test.dart';
+// import 'package:integration_test/integration_test.dart';
+// import 'package:app_flowy/main.dart' as app;
+
+// void main() {
+//   IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+//   group('end-to-end test', () {
+//     testWidgets('tap on the floating action button, verify counter',
+//         (tester) async {
+//       app.main();
+
+//       await tester.pumpAndSettle();
+//     });
+//   });
+// }

+ 6 - 1
frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_data_controller.dart

@@ -80,7 +80,12 @@ class TypeOptionDataController {
   Future<void> switchToField(FieldType newFieldType) {
   Future<void> switchToField(FieldType newFieldType) {
     return loader.switchToField(field.id, newFieldType).then((result) {
     return loader.switchToField(field.id, newFieldType).then((result) {
       return result.fold(
       return result.fold(
-        (_) {},
+        (_) {
+          // Should load the type-option data after switching to a new field.
+          // After loading the type-option data, the editor widget that uses
+          // the type-option data will be rebuild.
+          loadTypeOptionData();
+        },
         (err) => Log.error(err),
         (err) => Log.error(err),
       );
       );
     });
     });

+ 25 - 4
frontend/app_flowy/lib/plugins/grid/application/grid_bloc.dart

@@ -12,10 +12,12 @@ import 'grid_data_controller.dart';
 import 'row/row_cache.dart';
 import 'row/row_cache.dart';
 import 'dart:collection';
 import 'dart:collection';
 
 
+import 'row/row_service.dart';
 part 'grid_bloc.freezed.dart';
 part 'grid_bloc.freezed.dart';
 
 
 class GridBloc extends Bloc<GridEvent, GridState> {
 class GridBloc extends Bloc<GridEvent, GridState> {
   final GridDataController dataController;
   final GridDataController dataController;
+  void Function()? _createRowOperation;
 
 
   GridBloc({required ViewPB view})
   GridBloc({required ViewPB view})
       : dataController = GridDataController(view: view),
       : dataController = GridDataController(view: view),
@@ -28,7 +30,19 @@ class GridBloc extends Bloc<GridEvent, GridState> {
             await _loadGrid(emit);
             await _loadGrid(emit);
           },
           },
           createRow: () {
           createRow: () {
-            dataController.createRow();
+            state.loadingState.when(
+              loading: () {
+                _createRowOperation = () => dataController.createRow();
+              },
+              finish: (_) => dataController.createRow(),
+            );
+          },
+          deleteRow: (rowInfo) async {
+            final rowService = RowFFIService(
+              blockId: rowInfo.rowPB.blockId,
+              gridId: rowInfo.gridId,
+            );
+            await rowService.deleteRow(rowInfo.rowPB.id);
           },
           },
           didReceiveGridUpdate: (grid) {
           didReceiveGridUpdate: (grid) {
             emit(state.copyWith(grid: Some(grid)));
             emit(state.copyWith(grid: Some(grid)));
@@ -84,9 +98,15 @@ class GridBloc extends Bloc<GridEvent, GridState> {
   Future<void> _loadGrid(Emitter<GridState> emit) async {
   Future<void> _loadGrid(Emitter<GridState> emit) async {
     final result = await dataController.loadData();
     final result = await dataController.loadData();
     result.fold(
     result.fold(
-      (grid) => emit(
-        state.copyWith(loadingState: GridLoadingState.finish(left(unit))),
-      ),
+      (grid) {
+        if (_createRowOperation != null) {
+          _createRowOperation?.call();
+          _createRowOperation = null;
+        }
+        emit(
+          state.copyWith(loadingState: GridLoadingState.finish(left(unit))),
+        );
+      },
       (err) => emit(
       (err) => emit(
         state.copyWith(loadingState: GridLoadingState.finish(right(err))),
         state.copyWith(loadingState: GridLoadingState.finish(right(err))),
       ),
       ),
@@ -98,6 +118,7 @@ class GridBloc extends Bloc<GridEvent, GridState> {
 class GridEvent with _$GridEvent {
 class GridEvent with _$GridEvent {
   const factory GridEvent.initial() = InitialGrid;
   const factory GridEvent.initial() = InitialGrid;
   const factory GridEvent.createRow() = _CreateRow;
   const factory GridEvent.createRow() = _CreateRow;
+  const factory GridEvent.deleteRow(RowInfo rowInfo) = _DeleteRow;
   const factory GridEvent.didReceiveRowUpdate(
   const factory GridEvent.didReceiveRowUpdate(
     List<RowInfo> rows,
     List<RowInfo> rows,
     RowsChangedReason listState,
     RowsChangedReason listState,

+ 1 - 0
frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart

@@ -64,6 +64,7 @@ class GridDataController {
     });
     });
   }
   }
 
 
+  // Loads the rows from each block
   Future<Either<Unit, FlowyError>> loadData() async {
   Future<Either<Unit, FlowyError>> loadData() async {
     final result = await _gridFFIService.loadGrid();
     final result = await _gridFFIService.loadGrid();
     return Future(
     return Future(

+ 1 - 1
frontend/app_flowy/lib/plugins/grid/grid.dart

@@ -29,7 +29,7 @@ class GridPluginBuilder implements PluginBuilder {
   ViewDataTypePB get dataType => ViewDataTypePB.Database;
   ViewDataTypePB get dataType => ViewDataTypePB.Database;
 
 
   @override
   @override
-  ViewLayoutTypePB? get subDataType => ViewLayoutTypePB.Grid;
+  ViewLayoutTypePB? get layoutType => ViewLayoutTypePB.Grid;
 }
 }
 
 
 class GridPluginConfig implements PluginConfig {
 class GridPluginConfig implements PluginConfig {

+ 6 - 3
frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_container.dart

@@ -1,4 +1,5 @@
 import 'package:flowy_infra/theme.dart';
 import 'package:flowy_infra/theme.dart';
+import 'package:flutter/material.dart';
 import 'package:flutter/widgets.dart';
 import 'package:flutter/widgets.dart';
 import 'package:provider/provider.dart';
 import 'package:provider/provider.dart';
 import 'package:styled_widget/styled_widget.dart';
 import 'package:styled_widget/styled_widget.dart';
@@ -56,7 +57,6 @@ class CellContainer extends StatelessWidget {
             child: Container(
             child: Container(
               constraints: BoxConstraints(maxWidth: width, minHeight: 46),
               constraints: BoxConstraints(maxWidth: width, minHeight: 46),
               decoration: _makeBoxDecoration(context, isFocus),
               decoration: _makeBoxDecoration(context, isFocus),
-              padding: GridSize.cellContentInsets,
               child: container,
               child: container,
             ),
             ),
           );
           );
@@ -92,8 +92,11 @@ class _GridCellEnterRegion extends StatelessWidget {
       builder: (context, onEnter, _) {
       builder: (context, onEnter, _) {
         List<Widget> children = [child];
         List<Widget> children = [child];
         if (onEnter) {
         if (onEnter) {
-          children.add(CellAccessoryContainer(accessories: accessories)
-              .positioned(right: 0));
+          children.add(
+            CellAccessoryContainer(accessories: accessories).positioned(
+              right: GridSize.cellContentInsets.right,
+            ),
+          );
         }
         }
 
 
         return MouseRegion(
         return MouseRegion(

+ 11 - 7
frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/checkbox_cell.dart

@@ -4,6 +4,7 @@ import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra_ui/style_widget/icon_button.dart';
 import 'package:flowy_infra_ui/style_widget/icon_button.dart';
 import 'package:flutter/widgets.dart';
 import 'package:flutter/widgets.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
+import '../../layout/sizes.dart';
 import 'cell_builder.dart';
 import 'cell_builder.dart';
 
 
 class GridCheckboxCell extends GridCellWidget {
 class GridCheckboxCell extends GridCellWidget {
@@ -40,13 +41,16 @@ class _CheckboxCellState extends GridCellState<GridCheckboxCell> {
               : svgWidget('editor/editor_uncheck');
               : svgWidget('editor/editor_uncheck');
           return Align(
           return Align(
             alignment: Alignment.centerLeft,
             alignment: Alignment.centerLeft,
-            child: FlowyIconButton(
-              onPressed: () => context
-                  .read<CheckboxCellBloc>()
-                  .add(const CheckboxCellEvent.select()),
-              iconPadding: EdgeInsets.zero,
-              icon: icon,
-              width: 20,
+            child: Padding(
+              padding: GridSize.cellContentInsets,
+              child: FlowyIconButton(
+                onPressed: () => context
+                    .read<CheckboxCellBloc>()
+                    .add(const CheckboxCellEvent.select()),
+                iconPadding: EdgeInsets.zero,
+                icon: icon,
+                width: 20,
+              ),
             ),
             ),
           );
           );
         },
         },

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

@@ -6,6 +6,7 @@ import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/plugins/grid/application/prelude.dart';
 import 'package:app_flowy/plugins/grid/application/prelude.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
 
 
+import '../../../layout/sizes.dart';
 import '../cell_builder.dart';
 import '../cell_builder.dart';
 import 'date_editor.dart';
 import 'date_editor.dart';
 
 
@@ -75,7 +76,10 @@ class _DateCellState extends GridCellState<GridDateCell> {
                 onTap: () => _popover.show(),
                 onTap: () => _popover.show(),
                 child: Align(
                 child: Align(
                   alignment: alignment,
                   alignment: alignment,
-                  child: FlowyText.medium(state.dateStr, fontSize: 12),
+                  child: Padding(
+                    padding: GridSize.cellContentInsets,
+                    child: FlowyText.medium(state.dateStr, fontSize: 12),
+                  ),
                 ),
                 ),
               ),
               ),
             ),
             ),

+ 16 - 12
frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/number_cell.dart

@@ -4,6 +4,7 @@ import 'package:app_flowy/plugins/grid/application/prelude.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
 
+import '../../layout/sizes.dart';
 import 'cell_builder.dart';
 import 'cell_builder.dart';
 
 
 class GridNumberCell extends GridCellWidget {
 class GridNumberCell extends GridCellWidget {
@@ -45,18 +46,21 @@ class _NumberCellState extends GridFocusNodeCellState<GridNumberCell> {
                 _controller.text = contentFromState(state),
                 _controller.text = contentFromState(state),
           ),
           ),
         ],
         ],
-        child: TextField(
-          controller: _controller,
-          focusNode: focusNode,
-          onEditingComplete: () => focusNode.unfocus(),
-          onSubmitted: (_) => focusNode.unfocus(),
-          maxLines: 1,
-          style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
-          textInputAction: TextInputAction.done,
-          decoration: const InputDecoration(
-            contentPadding: EdgeInsets.zero,
-            border: InputBorder.none,
-            isDense: true,
+        child: Padding(
+          padding: GridSize.cellContentInsets,
+          child: TextField(
+            controller: _controller,
+            focusNode: focusNode,
+            onEditingComplete: () => focusNode.unfocus(),
+            onSubmitted: (_) => focusNode.unfocus(),
+            maxLines: 1,
+            style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
+            textInputAction: TextInputAction.done,
+            decoration: const InputDecoration(
+              contentPadding: EdgeInsets.zero,
+              border: InputBorder.none,
+              isDense: true,
+            ),
           ),
           ),
         ),
         ),
       ),
       ),

+ 5 - 1
frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart

@@ -11,6 +11,7 @@ import 'package:flowy_sdk/protobuf/flowy-grid/select_type_option.pb.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
 
+import '../../../layout/sizes.dart';
 import '../cell_builder.dart';
 import '../cell_builder.dart';
 import 'extension.dart';
 import 'extension.dart';
 import 'select_option_editor.dart';
 import 'select_option_editor.dart';
@@ -170,7 +171,10 @@ class _SelectOptionWrapState extends State<SelectOptionWrap> {
       alignment: AlignmentDirectional.center,
       alignment: AlignmentDirectional.center,
       fit: StackFit.expand,
       fit: StackFit.expand,
       children: [
       children: [
-        _wrapPopover(child),
+        Padding(
+          padding: GridSize.cellContentInsets,
+          child: _wrapPopover(child),
+        ),
         InkWell(onTap: () => _popover.show()),
         InkWell(onTap: () => _popover.show()),
       ],
       ],
     );
     );

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

@@ -3,6 +3,7 @@ import 'dart:collection';
 import 'package:flowy_infra/size.dart';
 import 'package:flowy_infra/size.dart';
 import 'package:flowy_infra/theme.dart';
 import 'package:flowy_infra/theme.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/select_type_option.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/select_type_option.pb.dart';
+import 'package:flutter/gestures.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:app_flowy/generated/locale_keys.g.dart';
 import 'package:app_flowy/generated/locale_keys.g.dart';
@@ -170,10 +171,21 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
         .toList();
         .toList();
     return Padding(
     return Padding(
       padding: const EdgeInsets.all(8.0),
       padding: const EdgeInsets.all(8.0),
-      child: SingleChildScrollView(
-        controller: sc,
-        scrollDirection: Axis.horizontal,
-        child: Wrap(spacing: 4, children: children),
+      child: ScrollConfiguration(
+        behavior: ScrollConfiguration.of(context).copyWith(
+          dragDevices: {
+            PointerDeviceKind.mouse,
+            PointerDeviceKind.touch,
+            PointerDeviceKind.trackpad,
+            PointerDeviceKind.stylus,
+            PointerDeviceKind.invertedStylus,
+          },
+        ),
+        child: SingleChildScrollView(
+          controller: sc,
+          scrollDirection: Axis.horizontal,
+          child: Wrap(spacing: 4, children: children),
+        ),
       ),
       ),
     );
     );
   }
   }

+ 16 - 12
frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/text_cell.dart

@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/plugins/grid/application/prelude.dart';
 import 'package:app_flowy/plugins/grid/application/prelude.dart';
+import '../../layout/sizes.dart';
 import 'cell_builder.dart';
 import 'cell_builder.dart';
 
 
 class GridTextCellStyle extends GridCellStyle {
 class GridTextCellStyle extends GridCellStyle {
@@ -56,18 +57,21 @@ class _GridTextCellState extends GridFocusNodeCellState<GridTextCell> {
             _controller.text = state.content;
             _controller.text = state.content;
           }
           }
         },
         },
-        child: TextField(
-          controller: _controller,
-          focusNode: focusNode,
-          onChanged: (value) => focusChanged(),
-          onEditingComplete: () => focusNode.unfocus(),
-          maxLines: null,
-          style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
-          decoration: InputDecoration(
-            contentPadding: EdgeInsets.zero,
-            border: InputBorder.none,
-            hintText: widget.cellStyle?.placeholder,
-            isDense: true,
+        child: Padding(
+          padding: GridSize.cellContentInsets,
+          child: TextField(
+            controller: _controller,
+            focusNode: focusNode,
+            onChanged: (value) => focusChanged(),
+            onEditingComplete: () => focusNode.unfocus(),
+            maxLines: null,
+            style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
+            decoration: InputDecoration(
+              contentPadding: EdgeInsets.zero,
+              border: InputBorder.none,
+              hintText: widget.cellStyle?.placeholder,
+              isDense: true,
+            ),
           ),
           ),
         ),
         ),
       ),
       ),

+ 12 - 8
frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/url_cell.dart

@@ -12,6 +12,7 @@ import 'package:flutter/services.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:app_flowy/plugins/grid/application/prelude.dart';
 import 'package:app_flowy/plugins/grid/application/prelude.dart';
 import 'package:url_launcher/url_launcher.dart';
 import 'package:url_launcher/url_launcher.dart';
+import '../../../layout/sizes.dart';
 import '../cell_accessory.dart';
 import '../cell_accessory.dart';
 import '../cell_builder.dart';
 import '../cell_builder.dart';
 import 'cell_editor.dart';
 import 'cell_editor.dart';
@@ -115,14 +116,17 @@ class _GridURLCellState extends GridCellState<GridURLCell> {
       value: _cellBloc,
       value: _cellBloc,
       child: BlocBuilder<URLCellBloc, URLCellState>(
       child: BlocBuilder<URLCellBloc, URLCellState>(
         builder: (context, state) {
         builder: (context, state) {
-          final richText = RichText(
-            textAlign: TextAlign.left,
-            text: TextSpan(
-              text: state.content,
-              style: TextStyle(
-                color: theme.main2,
-                fontSize: 14,
-                decoration: TextDecoration.underline,
+          final richText = Padding(
+            padding: GridSize.cellContentInsets,
+            child: RichText(
+              textAlign: TextAlign.left,
+              text: TextSpan(
+                text: state.content,
+                style: TextStyle(
+                  color: theme.main2,
+                  fontSize: 14,
+                  decoration: TextDecoration.underline,
+                ),
               ),
               ),
             ),
             ),
           );
           );

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

@@ -1,5 +1,6 @@
 import 'package:app_flowy/plugins/grid/application/field/field_editor_bloc.dart';
 import 'package:app_flowy/plugins/grid/application/field/field_editor_bloc.dart';
 import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart';
 import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart';
+import 'package:app_flowy/plugins/grid/presentation/layout/sizes.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:dartz/dartz.dart' show none;
 import 'package:dartz/dartz.dart' show none;
 import 'package:easy_localization/easy_localization.dart';
 import 'package:easy_localization/easy_localization.dart';
@@ -58,19 +59,22 @@ class _FieldEditorState extends State<FieldEditor> {
         isGroupField: widget.isGroupField,
         isGroupField: widget.isGroupField,
         loader: widget.typeOptionLoader,
         loader: widget.typeOptionLoader,
       )..add(const FieldEditorEvent.initial()),
       )..add(const FieldEditorEvent.initial()),
-      child: ListView(
-        shrinkWrap: true,
-        children: [
-          FlowyText.medium(
-            LocaleKeys.grid_field_editProperty.tr(),
-            fontSize: 12,
-          ),
-          const VSpace(10),
-          _FieldNameTextField(popoverMutex: popoverMutex),
-          const VSpace(10),
-          ..._addDeleteFieldButton(),
-          _FieldTypeOptionCell(popoverMutex: popoverMutex),
-        ],
+      child: Padding(
+        padding: GridSize.typeOptionContentInsets,
+        child: ListView(
+          shrinkWrap: true,
+          children: [
+            FlowyText.medium(
+              LocaleKeys.grid_field_editProperty.tr(),
+              fontSize: 12,
+            ),
+            const VSpace(10),
+            _FieldNameTextField(popoverMutex: popoverMutex),
+            const VSpace(10),
+            ..._addDeleteFieldButton(),
+            _FieldTypeOptionCell(popoverMutex: popoverMutex),
+          ],
+        ),
       ),
       ),
     );
     );
   }
   }

+ 1 - 5
frontend/app_flowy/lib/startup/deps_resolver.dart

@@ -118,11 +118,7 @@ void _resolveFolderDeps(GetIt getIt) {
 
 
   // AppPB
   // AppPB
   getIt.registerFactoryParam<AppBloc, AppPB, void>(
   getIt.registerFactoryParam<AppBloc, AppPB, void>(
-    (app, _) => AppBloc(
-      app: app,
-      appService: AppService(),
-      appListener: AppListener(appId: app.id),
-    ),
+    (app, _) => AppBloc(app: app),
   );
   );
 
 
   // trash
   // trash

+ 1 - 1
frontend/app_flowy/lib/startup/plugin/plugin.dart

@@ -51,7 +51,7 @@ abstract class PluginBuilder {
 
 
   ViewDataTypePB get dataType => ViewDataTypePB.Text;
   ViewDataTypePB get dataType => ViewDataTypePB.Text;
 
 
-  ViewLayoutTypePB? get subDataType => ViewLayoutTypePB.Document;
+  ViewLayoutTypePB? get layoutType => ViewLayoutTypePB.Document;
 }
 }
 
 
 abstract class PluginConfig {
 abstract class PluginConfig {

+ 2 - 2
frontend/app_flowy/lib/startup/tasks/rust_sdk.dart

@@ -16,12 +16,12 @@ class InitRustSDKTask extends LaunchTask {
 }
 }
 
 
 Future<Directory> appFlowyDocumentDirectory() async {
 Future<Directory> appFlowyDocumentDirectory() async {
-  Directory documentsDir = await getApplicationDocumentsDirectory();
-
   switch (integrationEnv()) {
   switch (integrationEnv()) {
     case IntegrationMode.develop:
     case IntegrationMode.develop:
+      Directory documentsDir = await getApplicationDocumentsDirectory();
       return Directory('${documentsDir.path}/flowy_dev').create();
       return Directory('${documentsDir.path}/flowy_dev').create();
     case IntegrationMode.release:
     case IntegrationMode.release:
+      Directory documentsDir = await getApplicationDocumentsDirectory();
       return Directory('${documentsDir.path}/flowy').create();
       return Directory('${documentsDir.path}/flowy').create();
     case IntegrationMode.test:
     case IntegrationMode.test:
       return Directory("${Directory.current.path}/.sandbox");
       return Directory("${Directory.current.path}/.sandbox");

+ 35 - 19
frontend/app_flowy/lib/workspace/application/app/app_bloc.dart

@@ -22,19 +22,24 @@ class AppBloc extends Bloc<AppEvent, AppState> {
   final AppService appService;
   final AppService appService;
   final AppListener appListener;
   final AppListener appListener;
 
 
-  AppBloc(
-      {required this.app, required this.appService, required this.appListener})
-      : super(AppState.initial(app)) {
+  AppBloc({required this.app})
+      : appService = AppService(),
+        appListener = AppListener(appId: app.id),
+        super(AppState.initial(app)) {
     on<AppEvent>((event, emit) async {
     on<AppEvent>((event, emit) async {
       await event.map(initial: (e) async {
       await event.map(initial: (e) async {
         _startListening();
         _startListening();
         await _loadViews(emit);
         await _loadViews(emit);
       }, createView: (CreateView value) async {
       }, createView: (CreateView value) async {
         await _createView(value, emit);
         await _createView(value, emit);
+      }, loadViews: (_) async {
+        await _loadViews(emit);
       }, didReceiveViewUpdated: (e) async {
       }, didReceiveViewUpdated: (e) async {
         await _didReceiveViewUpdated(e.views, emit);
         await _didReceiveViewUpdated(e.views, emit);
       }, delete: (e) async {
       }, delete: (e) async {
-        await _deleteView(emit);
+        await _deleteApp(emit);
+      }, deleteView: (deletedView) async {
+        await _deleteView(emit, deletedView.viewId);
       }, rename: (e) async {
       }, rename: (e) async {
         await _renameView(e, emit);
         await _renameView(e, emit);
       }, appDidUpdate: (e) async {
       }, appDidUpdate: (e) async {
@@ -71,7 +76,8 @@ class AppBloc extends Bloc<AppEvent, AppState> {
     );
     );
   }
   }
 
 
-  Future<void> _deleteView(Emitter<AppState> emit) async {
+// Delete the current app
+  Future<void> _deleteApp(Emitter<AppState> emit) async {
     final result = await appService.delete(appId: app.id);
     final result = await appService.delete(appId: app.id);
     result.fold(
     result.fold(
       (unit) => emit(state.copyWith(successOrFailure: left(unit))),
       (unit) => emit(state.copyWith(successOrFailure: left(unit))),
@@ -79,16 +85,24 @@ class AppBloc extends Bloc<AppEvent, AppState> {
     );
     );
   }
   }
 
 
+  Future<void> _deleteView(Emitter<AppState> emit, String viewId) async {
+    final result = await appService.deleteView(viewId: viewId);
+    result.fold(
+      (unit) => emit(state.copyWith(successOrFailure: left(unit))),
+      (error) => emit(state.copyWith(successOrFailure: right(error))),
+    );
+  }
+
   Future<void> _createView(CreateView value, Emitter<AppState> emit) async {
   Future<void> _createView(CreateView value, Emitter<AppState> emit) async {
-    final viewOrFailed = await appService.createView(
+    final result = await appService.createView(
       appId: app.id,
       appId: app.id,
       name: value.name,
       name: value.name,
-      desc: value.desc,
-      dataType: value.dataType,
-      pluginType: value.pluginType,
-      layout: value.layout,
+      desc: value.desc ?? "",
+      dataType: value.pluginBuilder.dataType,
+      pluginType: value.pluginBuilder.pluginType,
+      layoutType: value.pluginBuilder.layoutType!,
     );
     );
-    viewOrFailed.fold(
+    result.fold(
       (view) => emit(state.copyWith(
       (view) => emit(state.copyWith(
         latestCreatedView: view,
         latestCreatedView: view,
         successOrFailure: left(unit),
         successOrFailure: left(unit),
@@ -107,7 +121,9 @@ class AppBloc extends Bloc<AppEvent, AppState> {
   }
   }
 
 
   Future<void> _didReceiveViewUpdated(
   Future<void> _didReceiveViewUpdated(
-      List<ViewPB> views, Emitter<AppState> emit) async {
+    List<ViewPB> views,
+    Emitter<AppState> emit,
+  ) async {
     final latestCreatedView = state.latestCreatedView;
     final latestCreatedView = state.latestCreatedView;
     AppState newState = state.copyWith(views: views);
     AppState newState = state.copyWith(views: views);
     if (latestCreatedView != null) {
     if (latestCreatedView != null) {
@@ -138,12 +154,12 @@ class AppEvent with _$AppEvent {
   const factory AppEvent.initial() = Initial;
   const factory AppEvent.initial() = Initial;
   const factory AppEvent.createView(
   const factory AppEvent.createView(
     String name,
     String name,
-    String desc,
-    ViewDataTypePB dataType,
-    ViewLayoutTypePB layout,
-    PluginType pluginType,
-  ) = CreateView;
-  const factory AppEvent.delete() = Delete;
+    PluginBuilder pluginBuilder, {
+    String? desc,
+  }) = CreateView;
+  const factory AppEvent.loadViews() = LoadApp;
+  const factory AppEvent.delete() = DeleteApp;
+  const factory AppEvent.deleteView(String viewId) = DeleteView;
   const factory AppEvent.rename(String newName) = Rename;
   const factory AppEvent.rename(String newName) = Rename;
   const factory AppEvent.didReceiveViewUpdated(List<ViewPB> views) =
   const factory AppEvent.didReceiveViewUpdated(List<ViewPB> views) =
       ReceiveViews;
       ReceiveViews;
@@ -161,7 +177,7 @@ class AppState with _$AppState {
 
 
   factory AppState.initial(AppPB app) => AppState(
   factory AppState.initial(AppPB app) => AppState(
         app: app,
         app: app,
-        views: [],
+        views: app.belongings.items,
         successOrFailure: left(unit),
         successOrFailure: left(unit),
       );
       );
 }
 }

+ 9 - 4
frontend/app_flowy/lib/workspace/application/app/app_service.dart

@@ -18,17 +18,17 @@ class AppService {
   Future<Either<ViewPB, FlowyError>> createView({
   Future<Either<ViewPB, FlowyError>> createView({
     required String appId,
     required String appId,
     required String name,
     required String name,
-    required String desc,
+    String? desc,
     required ViewDataTypePB dataType,
     required ViewDataTypePB dataType,
     required PluginType pluginType,
     required PluginType pluginType,
-    required ViewLayoutTypePB layout,
+    required ViewLayoutTypePB layoutType,
   }) {
   }) {
     var payload = CreateViewPayloadPB.create()
     var payload = CreateViewPayloadPB.create()
       ..belongToId = appId
       ..belongToId = appId
       ..name = name
       ..name = name
-      ..desc = desc
+      ..desc = desc ?? ""
       ..dataType = dataType
       ..dataType = dataType
-      ..layout = layout;
+      ..layout = layoutType;
 
 
     return FolderEventCreateView(payload).send();
     return FolderEventCreateView(payload).send();
   }
   }
@@ -49,6 +49,11 @@ class AppService {
     return FolderEventDeleteApp(request).send();
     return FolderEventDeleteApp(request).send();
   }
   }
 
 
+  Future<Either<Unit, FlowyError>> deleteView({required String viewId}) {
+    final request = RepeatedViewIdPB.create()..items.add(viewId);
+    return FolderEventDeleteView(request).send();
+  }
+
   Future<Either<Unit, FlowyError>> updateApp(
   Future<Either<Unit, FlowyError>> updateApp(
       {required String appId, String? name}) {
       {required String appId, String? name}) {
     UpdateAppPayloadPB payload = UpdateAppPayloadPB.create()..appId = appId;
     UpdateAppPayloadPB payload = UpdateAppPayloadPB.create()..appId = appId;

+ 6 - 4
frontend/app_flowy/lib/workspace/application/view/view_bloc.dart

@@ -31,12 +31,14 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
         },
         },
         viewDidUpdate: (e) {
         viewDidUpdate: (e) {
           e.result.fold(
           e.result.fold(
-            (view) => emit(state.copyWith(view: view, successOrFailure: left(unit))),
+            (view) =>
+                emit(state.copyWith(view: view, successOrFailure: left(unit))),
             (error) => emit(state.copyWith(successOrFailure: right(error))),
             (error) => emit(state.copyWith(successOrFailure: right(error))),
           );
           );
         },
         },
         rename: (e) async {
         rename: (e) async {
-          final result = await service.updateView(viewId: view.id, name: e.newName);
+          final result =
+              await service.updateView(viewId: view.id, name: e.newName);
           emit(
           emit(
             result.fold(
             result.fold(
               (l) => state.copyWith(successOrFailure: left(unit)),
               (l) => state.copyWith(successOrFailure: left(unit)),
@@ -46,7 +48,6 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
         },
         },
         delete: (e) async {
         delete: (e) async {
           final result = await service.delete(viewId: view.id);
           final result = await service.delete(viewId: view.id);
-          await service.updateView(viewId: view.id);
           emit(
           emit(
             result.fold(
             result.fold(
               (l) => state.copyWith(successOrFailure: left(unit)),
               (l) => state.copyWith(successOrFailure: left(unit)),
@@ -81,7 +82,8 @@ class ViewEvent with _$ViewEvent {
   const factory ViewEvent.rename(String newName) = Rename;
   const factory ViewEvent.rename(String newName) = Rename;
   const factory ViewEvent.delete() = Delete;
   const factory ViewEvent.delete() = Delete;
   const factory ViewEvent.duplicate() = Duplicate;
   const factory ViewEvent.duplicate() = Duplicate;
-  const factory ViewEvent.viewDidUpdate(Either<ViewPB, FlowyError> result) = ViewDidUpdate;
+  const factory ViewEvent.viewDidUpdate(Either<ViewPB, FlowyError> result) =
+      ViewDidUpdate;
 }
 }
 
 
 @freezed
 @freezed

+ 2 - 2
frontend/app_flowy/lib/workspace/application/workspace/workspace_service.dart

@@ -17,11 +17,11 @@ class WorkspaceService {
     required this.workspaceId,
     required this.workspaceId,
   });
   });
   Future<Either<AppPB, FlowyError>> createApp(
   Future<Either<AppPB, FlowyError>> createApp(
-      {required String name, required String desc}) {
+      {required String name, String? desc}) {
     final payload = CreateAppPayloadPB.create()
     final payload = CreateAppPayloadPB.create()
       ..name = name
       ..name = name
       ..workspaceId = workspaceId
       ..workspaceId = workspaceId
-      ..desc = desc;
+      ..desc = desc ?? "";
     return FolderEventCreateApp(payload).send();
     return FolderEventCreateApp(payload).send();
   }
   }
 
 

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

@@ -104,13 +104,12 @@ class MenuAppHeader extends StatelessWidget {
       message: LocaleKeys.menuAppHeader_addPageTooltip.tr(),
       message: LocaleKeys.menuAppHeader_addPageTooltip.tr(),
       child: AddButton(
       child: AddButton(
         onSelected: (pluginBuilder) {
         onSelected: (pluginBuilder) {
-          context.read<AppBloc>().add(AppEvent.createView(
-                LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
-                "",
-                pluginBuilder.dataType,
-                pluginBuilder.subDataType!,
-                pluginBuilder.pluginType,
-              ));
+          context.read<AppBloc>().add(
+                AppEvent.createView(
+                  LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
+                  pluginBuilder,
+                ),
+              );
         },
         },
       ).padding(right: MenuAppSizes.headerPadding),
       ).padding(right: MenuAppSizes.headerPadding),
     );
     );

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

@@ -50,7 +50,6 @@ class _MenuAppState extends State<MenuApp> {
             },
             },
           ),
           ),
           BlocListener<AppBloc, AppState>(
           BlocListener<AppBloc, AppState>(
-            listenWhen: (p, c) => p.views != c.views,
             listener: (context, state) => viewDataContext.views = state.views,
             listener: (context, state) => viewDataContext.views = state.views,
           ),
           ),
         ],
         ],

+ 60 - 39
frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart

@@ -188,9 +188,7 @@ ShortcutEventHandler doubleTildeToStrikethrough = (editorState, event) {
   return KeyEventResult.handled;
   return KeyEventResult.handled;
 };
 };
 
 
-/// To create a link, enclose the link text in brackets (e.g., [link text]).
-/// Then, immediately follow it with the URL in parentheses (e.g., (https://example.com)).
-ShortcutEventHandler markdownLinkToLinkHandler = (editorState, event) {
+ShortcutEventHandler markdownLinkOrImageHandler = (editorState, event) {
   final selectionService = editorState.service.selectionService;
   final selectionService = editorState.service.selectionService;
   final selection = selectionService.currentSelection.value;
   final selection = selectionService.currentSelection.value;
   final textNodes = selectionService.currentSelectedNodes.whereType<TextNode>();
   final textNodes = selectionService.currentSelectedNodes.whereType<TextNode>();
@@ -198,48 +196,72 @@ ShortcutEventHandler markdownLinkToLinkHandler = (editorState, event) {
     return KeyEventResult.ignored;
     return KeyEventResult.ignored;
   }
   }
 
 
-  // find all of the indexs for important characters
+  // Find all of the indexes of the relevant characters
   final textNode = textNodes.first;
   final textNode = textNodes.first;
   final text = textNode.toPlainText();
   final text = textNode.toPlainText();
+  final firstExclamation = text.indexOf('!');
   final firstOpeningBracket = text.indexOf('[');
   final firstOpeningBracket = text.indexOf('[');
   final firstClosingBracket = text.indexOf(']');
   final firstClosingBracket = text.indexOf(']');
 
 
-  // use regex to validate the format of the link
-  // note: this enforces that the link has http or https
-  final regexp = RegExp(r'\[([\w\s\d]+)\]\(((?:\/|https?:\/\/)[\w\d./?=#]+)$');
-  final match = regexp.firstMatch(text);
-  if (match == null) {
+  // Use RegEx to determine whether it's an image or a link
+  // Difference between image and link syntax is that image
+  // has an exclamation point at the beginning.
+  // Note: The RegEx enforces that the URL has http or https
+  final imgRegEx =
+      RegExp(r'\!\[([\w\s\d]+)\]\(((?:\/|https?:\/\/)[\w\d-./?=#%&]+)$');
+  final lnkRegEx =
+      RegExp(r'\[([\w\s\d]+)\]\(((?:\/|https?:\/\/)[\w\d-./?=#%&]+)$');
+
+  if (imgRegEx.firstMatch(text) != null) {
+    // Extract the alt text and the URL of the image
+    final match = lnkRegEx.firstMatch(text);
+    final imgUrl = match?.group(2);
+
+    // Delete the text and replace it with the image pointed to by the URL
+    final transaction = editorState.transaction
+      ..deleteText(textNode, firstExclamation, text.length)
+      ..insertNode(
+          textNode.path,
+          Node.fromJson({
+            'type': 'image',
+            'attributes': {
+              'image_src': imgUrl,
+              'align': 'center',
+            }
+          }));
+    editorState.apply(transaction);
+  } else if (lnkRegEx.firstMatch(text) != null) {
+    // Extract the text and the URL of the link
+    final match = lnkRegEx.firstMatch(text);
+    final linkText = match?.group(1);
+    final linkUrl = match?.group(2);
+
+    // Delete the initial opening bracket,
+    // update the href attribute of the text surrounded by [ ] to the url,
+    // delete everything after the text,
+    // and update the cursor position.
+    final transaction = editorState.transaction
+      ..deleteText(textNode, firstOpeningBracket, 1)
+      ..formatText(
+        textNode,
+        firstOpeningBracket,
+        firstClosingBracket - firstOpeningBracket - 1,
+        {
+          BuiltInAttributeKey.href: linkUrl,
+        },
+      )
+      ..deleteText(textNode, firstClosingBracket - 1,
+          selection.end.offset - firstClosingBracket)
+      ..afterSelection = Selection.collapsed(
+        Position(
+          path: textNode.path,
+          offset: firstOpeningBracket + linkText!.length,
+        ),
+      );
+    editorState.apply(transaction);
+  } else {
     return KeyEventResult.ignored;
     return KeyEventResult.ignored;
   }
   }
-
-  // extract the text and the url of the link
-  final linkText = match.group(1);
-  final linkUrl = match.group(2);
-
-  // Delete the initial opening bracket,
-  // update the href attribute of the text surrounded by [ ] to the url,
-  // delete everything after the text,
-  // and update the cursor position.
-  final transaction = editorState.transaction
-    ..deleteText(textNode, firstOpeningBracket, 1)
-    ..formatText(
-      textNode,
-      firstOpeningBracket,
-      firstClosingBracket - firstOpeningBracket - 1,
-      {
-        BuiltInAttributeKey.href: linkUrl,
-      },
-    )
-    ..deleteText(textNode, firstClosingBracket - 1,
-        selection.end.offset - firstClosingBracket)
-    ..afterSelection = Selection.collapsed(
-      Position(
-        path: textNode.path,
-        offset: firstOpeningBracket + linkText!.length,
-      ),
-    );
-  editorState.apply(transaction);
-
   return KeyEventResult.handled;
   return KeyEventResult.handled;
 };
 };
 
 
@@ -369,6 +391,5 @@ ShortcutEventHandler doubleUnderscoresToBold = (editorState, event) {
       ),
       ),
     );
     );
   editorState.apply(transaction);
   editorState.apply(transaction);
-
   return KeyEventResult.handled;
   return KeyEventResult.handled;
 };
 };

+ 2 - 3
frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart

@@ -16,7 +16,6 @@ import 'package:appflowy_editor/src/service/internal_key_event_handlers/whitespa
 import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event.dart';
 import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/foundation.dart';
 
 
-//
 List<ShortcutEvent> builtInShortcutEvents = [
 List<ShortcutEvent> builtInShortcutEvents = [
   ShortcutEvent(
   ShortcutEvent(
     key: 'Move cursor up',
     key: 'Move cursor up',
@@ -276,9 +275,9 @@ List<ShortcutEvent> builtInShortcutEvents = [
     handler: doubleTildeToStrikethrough,
     handler: doubleTildeToStrikethrough,
   ),
   ),
   ShortcutEvent(
   ShortcutEvent(
-    key: 'Markdown link to link',
+    key: 'Markdown link or image',
     command: 'shift+parenthesis right',
     command: 'shift+parenthesis right',
-    handler: markdownLinkToLinkHandler,
+    handler: markdownLinkOrImageHandler,
   ),
   ),
   // https://github.com/flutter/flutter/issues/104944
   // https://github.com/flutter/flutter/issues/104944
   // Workaround: Using space editing on the web platform often results in errors,
   // Workaround: Using space editing on the web platform often results in errors,

+ 12 - 11
frontend/app_flowy/packages/flowy_sdk/lib/ffi.dart

@@ -7,10 +7,10 @@ import 'package:ffi/ffi.dart' as ffi;
 import 'package:flutter/foundation.dart' as Foundation;
 import 'package:flutter/foundation.dart' as Foundation;
 
 
 // ignore_for_file: unused_import, camel_case_types, non_constant_identifier_names
 // ignore_for_file: unused_import, camel_case_types, non_constant_identifier_names
-final DynamicLibrary _dl = _open();
+final DynamicLibrary _dart_ffi_lib = _open();
 
 
 /// Reference to the Dynamic Library, it should be only used for low-level access
 /// Reference to the Dynamic Library, it should be only used for low-level access
-final DynamicLibrary dl = _dl;
+final DynamicLibrary dl = _dart_ffi_lib;
 DynamicLibrary _open() {
 DynamicLibrary _open() {
   if (Platform.environment.containsKey('FLUTTER_TEST')) {
   if (Platform.environment.containsKey('FLUTTER_TEST')) {
     final prefix = "${Directory.current.path}/.sandbox";
     final prefix = "${Directory.current.path}/.sandbox";
@@ -18,7 +18,8 @@ DynamicLibrary _open() {
       return DynamicLibrary.open('${prefix}/libdart_ffi.so');
       return DynamicLibrary.open('${prefix}/libdart_ffi.so');
     if (Platform.isAndroid)
     if (Platform.isAndroid)
       return DynamicLibrary.open('${prefix}/libdart_ffi.so');
       return DynamicLibrary.open('${prefix}/libdart_ffi.so');
-    if (Platform.isMacOS) return DynamicLibrary.open('${prefix}/libdart_ffi.a');
+    if (Platform.isMacOS)
+      return DynamicLibrary.open('${prefix}/libdart_ffi.dylib');
     if (Platform.isIOS) return DynamicLibrary.open('${prefix}/libdart_ffi.a');
     if (Platform.isIOS) return DynamicLibrary.open('${prefix}/libdart_ffi.a');
     if (Platform.isWindows)
     if (Platform.isWindows)
       return DynamicLibrary.open('${prefix}/dart_ffi.dll');
       return DynamicLibrary.open('${prefix}/dart_ffi.dll');
@@ -42,8 +43,8 @@ void async_event(
   _invoke_async(port, input, len);
   _invoke_async(port, input, len);
 }
 }
 
 
-final _invoke_async_Dart _invoke_async =
-    _dl.lookupFunction<_invoke_async_C, _invoke_async_Dart>('async_event');
+final _invoke_async_Dart _invoke_async = _dart_ffi_lib
+    .lookupFunction<_invoke_async_C, _invoke_async_Dart>('async_event');
 typedef _invoke_async_C = Void Function(
 typedef _invoke_async_C = Void Function(
   Int64 port,
   Int64 port,
   Pointer<Uint8> input,
   Pointer<Uint8> input,
@@ -63,8 +64,8 @@ Pointer<Uint8> sync_event(
   return _invoke_sync(input, len);
   return _invoke_sync(input, len);
 }
 }
 
 
-final _invoke_sync_Dart _invoke_sync =
-    _dl.lookupFunction<_invoke_sync_C, _invoke_sync_Dart>('sync_event');
+final _invoke_sync_Dart _invoke_sync = _dart_ffi_lib
+    .lookupFunction<_invoke_sync_C, _invoke_sync_Dart>('sync_event');
 typedef _invoke_sync_C = Pointer<Uint8> Function(
 typedef _invoke_sync_C = Pointer<Uint8> Function(
   Pointer<Uint8> input,
   Pointer<Uint8> input,
   Uint64 len,
   Uint64 len,
@@ -82,7 +83,7 @@ int init_sdk(
 }
 }
 
 
 final _init_sdk_Dart _init_sdk =
 final _init_sdk_Dart _init_sdk =
-    _dl.lookupFunction<_init_sdk_C, _init_sdk_Dart>('init_sdk');
+    _dart_ffi_lib.lookupFunction<_init_sdk_C, _init_sdk_Dart>('init_sdk');
 typedef _init_sdk_C = Int64 Function(
 typedef _init_sdk_C = Int64 Function(
   Pointer<ffi.Utf8> path,
   Pointer<ffi.Utf8> path,
 );
 );
@@ -96,7 +97,7 @@ int set_stream_port(int port) {
 }
 }
 
 
 final _set_stream_port_Dart _set_stream_port =
 final _set_stream_port_Dart _set_stream_port =
-    _dl.lookupFunction<_set_stream_port_C, _set_stream_port_Dart>(
+    _dart_ffi_lib.lookupFunction<_set_stream_port_C, _set_stream_port_Dart>(
         'set_stream_port');
         'set_stream_port');
 
 
 typedef _set_stream_port_C = Int32 Function(
 typedef _set_stream_port_C = Int32 Function(
@@ -111,7 +112,7 @@ void link_me_please() {
   _link_me_please();
   _link_me_please();
 }
 }
 
 
-final _link_me_please_Dart _link_me_please = _dl
+final _link_me_please_Dart _link_me_please = _dart_ffi_lib
     .lookupFunction<_link_me_please_C, _link_me_please_Dart>('link_me_please');
     .lookupFunction<_link_me_please_C, _link_me_please_Dart>('link_me_please');
 typedef _link_me_please_C = Void Function();
 typedef _link_me_please_C = Void Function();
 typedef _link_me_please_Dart = void Function();
 typedef _link_me_please_Dart = void Function();
@@ -123,7 +124,7 @@ void store_dart_post_cobject(
   _store_dart_post_cobject(ptr);
   _store_dart_post_cobject(ptr);
 }
 }
 
 
-final _store_dart_post_cobject_Dart _store_dart_post_cobject = _dl
+final _store_dart_post_cobject_Dart _store_dart_post_cobject = _dart_ffi_lib
     .lookupFunction<_store_dart_post_cobject_C, _store_dart_post_cobject_Dart>(
     .lookupFunction<_store_dart_post_cobject_C, _store_dart_post_cobject_Dart>(
         'store_dart_post_cobject');
         'store_dart_post_cobject');
 typedef _store_dart_post_cobject_C = Void Function(
 typedef _store_dart_post_cobject_C = Void Function(

+ 34 - 5
frontend/app_flowy/pubspec.lock

@@ -49,7 +49,7 @@ packages:
       name: archive
       name: archive
       url: "https://pub.dartlang.org"
       url: "https://pub.dartlang.org"
     source: hosted
     source: hosted
-    version: "3.3.1"
+    version: "3.1.11"
   args:
   args:
     dependency: transitive
     dependency: transitive
     description:
     description:
@@ -245,7 +245,7 @@ packages:
       name: coverage
       name: coverage
       url: "https://pub.dartlang.org"
       url: "https://pub.dartlang.org"
     source: hosted
     source: hosted
-    version: "1.3.2"
+    version: "1.2.0"
   cross_file:
   cross_file:
     dependency: transitive
     dependency: transitive
     description:
     description:
@@ -259,7 +259,7 @@ packages:
       name: crypto
       name: crypto
       url: "https://pub.dartlang.org"
       url: "https://pub.dartlang.org"
     source: hosted
     source: hosted
-    version: "3.0.2"
+    version: "3.0.1"
   csslib:
   csslib:
     dependency: transitive
     dependency: transitive
     description:
     description:
@@ -454,6 +454,11 @@ packages:
       url: "https://pub.dartlang.org"
       url: "https://pub.dartlang.org"
     source: hosted
     source: hosted
     version: "0.6.1"
     version: "0.6.1"
+  flutter_driver:
+    dependency: transitive
+    description: flutter
+    source: sdk
+    version: "0.0.0"
   flutter_inappwebview:
   flutter_inappwebview:
     dependency: transitive
     dependency: transitive
     description:
     description:
@@ -555,6 +560,11 @@ packages:
       url: "https://pub.dartlang.org"
       url: "https://pub.dartlang.org"
     source: hosted
     source: hosted
     version: "2.1.3"
     version: "2.1.3"
+  fuchsia_remote_debug_protocol:
+    dependency: transitive
+    description: flutter
+    source: sdk
+    version: "0.0.0"
   get_it:
   get_it:
     dependency: "direct main"
     dependency: "direct main"
     description:
     description:
@@ -660,6 +670,11 @@ packages:
       url: "https://pub.dartlang.org"
       url: "https://pub.dartlang.org"
     source: hosted
     source: hosted
     version: "2.5.0"
     version: "2.5.0"
+  integration_test:
+    dependency: "direct dev"
+    description: flutter
+    source: sdk
+    version: "0.0.0"
   intl:
   intl:
     dependency: "direct main"
     dependency: "direct main"
     description:
     description:
@@ -1246,6 +1261,13 @@ packages:
       url: "https://pub.dartlang.org"
       url: "https://pub.dartlang.org"
     source: hosted
     source: hosted
     version: "0.3.1+2"
     version: "0.3.1+2"
+  sync_http:
+    dependency: transitive
+    description:
+      name: sync_http
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.3.0"
   table_calendar:
   table_calendar:
     dependency: "direct main"
     dependency: "direct main"
     description:
     description:
@@ -1322,7 +1344,7 @@ packages:
       name: typed_data
       name: typed_data
       url: "https://pub.dartlang.org"
       url: "https://pub.dartlang.org"
     source: hosted
     source: hosted
-    version: "1.3.1"
+    version: "1.3.0"
   universal_platform:
   universal_platform:
     dependency: transitive
     dependency: transitive
     description:
     description:
@@ -1441,7 +1463,7 @@ packages:
       name: vm_service
       name: vm_service
       url: "https://pub.dartlang.org"
       url: "https://pub.dartlang.org"
     source: hosted
     source: hosted
-    version: "8.3.0"
+    version: "8.2.2"
   watcher:
   watcher:
     dependency: transitive
     dependency: transitive
     description:
     description:
@@ -1456,6 +1478,13 @@ packages:
       url: "https://pub.dartlang.org"
       url: "https://pub.dartlang.org"
     source: hosted
     source: hosted
     version: "2.2.0"
     version: "2.2.0"
+  webdriver:
+    dependency: transitive
+    description:
+      name: webdriver
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.0"
   webkit_inspection_protocol:
   webkit_inspection_protocol:
     dependency: transitive
     dependency: transitive
     description:
     description:

+ 2 - 0
frontend/app_flowy/pubspec.yaml

@@ -97,6 +97,8 @@ dev_dependencies:
 
 
   flutter_test:
   flutter_test:
     sdk: flutter
     sdk: flutter
+  integration_test:
+    sdk: flutter
   build_runner: ^2.2.0
   build_runner: ^2.2.0
   freezed: ^2.1.0+1
   freezed: ^2.1.0+1
   bloc_test: ^9.0.2
   bloc_test: ^9.0.2

+ 44 - 0
frontend/app_flowy/test/bloc_test/grid_test/grid_bloc_test.dart

@@ -0,0 +1,44 @@
+import 'package:app_flowy/plugins/grid/application/grid_bloc.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:bloc_test/bloc_test.dart';
+import 'util.dart';
+
+void main() {
+  late AppFlowyGridTest gridTest;
+  setUpAll(() async {
+    gridTest = await AppFlowyGridTest.ensureInitialized();
+  });
+
+  group('GridBloc', () {
+    blocTest<GridBloc, GridState>(
+      "Create row",
+      build: () =>
+          GridBloc(view: gridTest.gridView)..add(const GridEvent.initial()),
+      act: (bloc) => bloc.add(const GridEvent.createRow()),
+      wait: const Duration(milliseconds: 300),
+      verify: (bloc) {
+        assert(bloc.state.rowInfos.length == 4);
+      },
+    );
+  });
+
+  group('GridBloc', () {
+    late GridBloc gridBloc;
+    setUpAll(() async {
+      gridBloc = GridBloc(view: gridTest.gridView)
+        ..add(const GridEvent.initial());
+      await gridResponseFuture();
+    });
+
+    // The initial number of rows is three
+    test('', () async {
+      assert(gridBloc.state.rowInfos.length == 3);
+    });
+
+    test('delete row', () async {
+      gridBloc.add(GridEvent.deleteRow(gridBloc.state.rowInfos.last));
+      await gridResponseFuture();
+      assert(gridBloc.state.rowInfos.length == 2);
+    });
+  });
+}

+ 36 - 0
frontend/app_flowy/test/bloc_test/grid_test/select_option_bloc_test.dart

@@ -0,0 +1,36 @@
+import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
+import 'package:app_flowy/plugins/grid/application/cell/select_option_editor_bloc.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:bloc_test/bloc_test.dart';
+import 'util.dart';
+
+void main() {
+  late AppFlowyGridSelectOptionCellTest cellTest;
+  setUpAll(() async {
+    cellTest = await AppFlowyGridSelectOptionCellTest.ensureInitialized();
+  });
+
+  group('SingleSelectOptionBloc', () {
+    late GridSelectOptionCellController cellController;
+    setUp(() async {
+      cellController =
+          await cellTest.makeCellController(FieldType.SingleSelect);
+    });
+
+    blocTest<SelectOptionCellEditorBloc, SelectOptionEditorState>(
+      "create option",
+      build: () {
+        final bloc = SelectOptionCellEditorBloc(cellController: cellController);
+        bloc.add(const SelectOptionEditorEvent.initial());
+        return bloc;
+      },
+      act: (bloc) => bloc.add(const SelectOptionEditorEvent.newOption("A")),
+      wait: gridResponseDuration(),
+      verify: (bloc) {
+        assert(bloc.state.options.length == 1);
+        assert(bloc.state.options[0].name == "A");
+      },
+    );
+  });
+}

+ 124 - 0
frontend/app_flowy/test/bloc_test/grid_test/util.dart

@@ -0,0 +1,124 @@
+import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
+import 'package:app_flowy/plugins/grid/application/grid_data_controller.dart';
+import 'package:app_flowy/plugins/grid/application/row/row_bloc.dart';
+import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
+import 'package:app_flowy/plugins/grid/application/row/row_data_controller.dart';
+import 'package:app_flowy/plugins/grid/grid.dart';
+import 'package:app_flowy/workspace/application/app/app_service.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
+
+import '../../util.dart';
+
+/// Create a empty Grid for test
+class AppFlowyGridTest {
+  // ignore: unused_field
+  final AppFlowyUnitTest _inner;
+  late ViewPB gridView;
+  AppFlowyGridTest(AppFlowyUnitTest unitTest) : _inner = unitTest;
+
+  static Future<AppFlowyGridTest> ensureInitialized() async {
+    final inner = await AppFlowyUnitTest.ensureInitialized();
+    final test = AppFlowyGridTest(inner);
+    await test._createTestGrid();
+    return test;
+  }
+
+  Future<void> _createTestGrid() async {
+    final app = await _inner.createTestApp();
+    final builder = GridPluginBuilder();
+    final result = await AppService().createView(
+      appId: app.id,
+      name: "Test Grid",
+      dataType: builder.dataType,
+      pluginType: builder.pluginType,
+      layoutType: builder.layoutType!,
+    );
+    result.fold(
+      (view) => gridView = view,
+      (error) {},
+    );
+  }
+}
+
+class AppFlowyGridSelectOptionCellTest {
+  final AppFlowyGridCellTest _cellTest;
+
+  AppFlowyGridSelectOptionCellTest(AppFlowyGridCellTest cellTest)
+      : _cellTest = cellTest;
+
+  static Future<AppFlowyGridSelectOptionCellTest> ensureInitialized() async {
+    final cellTest = await AppFlowyGridCellTest.ensureInitialized();
+    final test = AppFlowyGridSelectOptionCellTest(cellTest);
+    return test;
+  }
+
+  /// For the moment, just edit the first row of the grid.
+  Future<GridSelectOptionCellController> makeCellController(
+      FieldType fieldType) async {
+    assert(fieldType == FieldType.SingleSelect ||
+        fieldType == FieldType.MultiSelect);
+
+    final fieldContexts =
+        _cellTest._dataController.fieldController.fieldContexts;
+    final field =
+        fieldContexts.firstWhere((element) => element.fieldType == fieldType);
+    final builder = await _cellTest.cellControllerBuilder(0, field.id);
+    final cellController = builder.build() as GridSelectOptionCellController;
+    return cellController;
+  }
+}
+
+class AppFlowyGridCellTest {
+  // ignore: unused_field
+  final AppFlowyGridTest _gridTest;
+  final GridDataController _dataController;
+  AppFlowyGridCellTest(AppFlowyGridTest gridTest)
+      : _gridTest = gridTest,
+        _dataController = GridDataController(view: gridTest.gridView);
+
+  static Future<AppFlowyGridCellTest> ensureInitialized() async {
+    final gridTest = await AppFlowyGridTest.ensureInitialized();
+    final test = AppFlowyGridCellTest(gridTest);
+    await test._loadGridData();
+    return test;
+  }
+
+  Future<void> _loadGridData() async {
+    final result = await _dataController.loadData();
+    result.fold((l) => null, (r) => throw Exception(r));
+  }
+
+  Future<GridCellControllerBuilder> cellControllerBuilder(
+      int rowIndex, String fieldId) async {
+    final RowInfo rowInfo = _dataController.rowInfos[rowIndex];
+    final blockCache = _dataController.blocks[rowInfo.rowPB.blockId];
+    final rowCache = blockCache?.rowCache;
+
+    final rowDataController = GridRowDataController(
+      rowInfo: rowInfo,
+      fieldController: _dataController.fieldController,
+      rowCache: rowCache!,
+    );
+
+    final rowBloc = RowBloc(
+      rowInfo: rowInfo,
+      dataController: rowDataController,
+    )..add(const RowEvent.initial());
+    await gridResponseFuture(milliseconds: 300);
+
+    return GridCellControllerBuilder(
+      cellId: rowBloc.state.gridCellMap[fieldId]!,
+      cellCache: rowCache.cellCache,
+      delegate: rowDataController,
+    );
+  }
+}
+
+Future<void> gridResponseFuture({int milliseconds = 200}) {
+  return Future.delayed(gridResponseDuration(milliseconds: milliseconds));
+}
+
+Duration gridResponseDuration({int milliseconds = 200}) {
+  return Duration(milliseconds: milliseconds);
+}

+ 104 - 0
frontend/app_flowy/test/bloc_test/menu_test/app_bloc_test.dart

@@ -0,0 +1,104 @@
+import 'package:app_flowy/plugins/board/board.dart';
+import 'package:app_flowy/plugins/doc/document.dart';
+import 'package:app_flowy/plugins/grid/grid.dart';
+import 'package:app_flowy/workspace/application/app/app_bloc.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder/app.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:bloc_test/bloc_test.dart';
+import '../../util.dart';
+
+void main() {
+  late AppFlowyUnitTest test;
+  setUpAll(() async {
+    test = await AppFlowyUnitTest.ensureInitialized();
+  });
+
+  group(
+    'AppBloc',
+    () {
+      late AppPB app;
+      setUp(() async {
+        app = await test.createTestApp();
+      });
+
+      blocTest<AppBloc, AppState>(
+        "Create a document",
+        build: () => AppBloc(app: app)..add(const AppEvent.initial()),
+        act: (bloc) {
+          bloc.add(
+              AppEvent.createView("Test document", DocumentPluginBuilder()));
+        },
+        wait: blocResponseDuration(),
+        verify: (bloc) {
+          assert(bloc.state.views.length == 1);
+          assert(bloc.state.views.last.name == "Test document");
+          assert(bloc.state.views.last.layout == ViewLayoutTypePB.Document);
+        },
+      );
+
+      blocTest<AppBloc, AppState>(
+        "Create a grid",
+        build: () => AppBloc(app: app)..add(const AppEvent.initial()),
+        act: (bloc) {
+          bloc.add(AppEvent.createView("Test grid", GridPluginBuilder()));
+        },
+        wait: blocResponseDuration(),
+        verify: (bloc) {
+          assert(bloc.state.views.length == 1);
+          assert(bloc.state.views.last.name == "Test grid");
+          assert(bloc.state.views.last.layout == ViewLayoutTypePB.Grid);
+        },
+      );
+
+      blocTest<AppBloc, AppState>(
+        "Create a Kanban board",
+        build: () => AppBloc(app: app)..add(const AppEvent.initial()),
+        act: (bloc) {
+          bloc.add(AppEvent.createView("Test board", BoardPluginBuilder()));
+        },
+        wait: const Duration(milliseconds: 100),
+        verify: (bloc) {
+          assert(bloc.state.views.length == 1);
+          assert(bloc.state.views.last.name == "Test board");
+          assert(bloc.state.views.last.layout == ViewLayoutTypePB.Board);
+        },
+      );
+    },
+  );
+
+  group('AppBloc', () {
+    late ViewPB view;
+    late AppPB app;
+    setUpAll(() async {
+      app = await test.createTestApp();
+    });
+
+    blocTest<AppBloc, AppState>(
+      "create a document",
+      build: () => AppBloc(app: app)..add(const AppEvent.initial()),
+      act: (bloc) {
+        bloc.add(AppEvent.createView("Test document", DocumentPluginBuilder()));
+      },
+      wait: blocResponseDuration(),
+      verify: (bloc) {
+        assert(bloc.state.views.length == 1);
+        view = bloc.state.views.last;
+      },
+    );
+    blocTest<AppBloc, AppState>(
+      "delete the document",
+      build: () => AppBloc(app: app)..add(const AppEvent.initial()),
+      act: (bloc) => bloc.add(AppEvent.deleteView(view.id)),
+    );
+    blocTest<AppBloc, AppState>(
+      "verify the document is exist",
+      build: () => AppBloc(app: app)..add(const AppEvent.initial()),
+      act: (bloc) => bloc.add(const AppEvent.loadViews()),
+      wait: blocResponseDuration(),
+      verify: (bloc) {
+        assert(bloc.state.views.isEmpty);
+      },
+    );
+  });
+}

+ 109 - 0
frontend/app_flowy/test/util.dart

@@ -0,0 +1,109 @@
+import 'package:app_flowy/startup/startup.dart';
+import 'package:app_flowy/user/application/auth_service.dart';
+import 'package:app_flowy/user/application/user_service.dart';
+import 'package:app_flowy/workspace/application/workspace/workspace_service.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra/uuid.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder/app.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder/workspace.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+import 'package:app_flowy/main.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+class AppFlowyIntegrateTest {
+  static Future<AppFlowyIntegrateTest> ensureInitialized() async {
+    IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+    SharedPreferences.setMockInitialValues({});
+    main();
+    return AppFlowyIntegrateTest();
+  }
+}
+
+class AppFlowyUnitTest {
+  late UserProfilePB userProfile;
+  late UserService userService;
+  late WorkspaceService workspaceService;
+  late List<WorkspacePB> workspaces;
+
+  static Future<AppFlowyUnitTest> ensureInitialized() async {
+    TestWidgetsFlutterBinding.ensureInitialized();
+    SharedPreferences.setMockInitialValues({});
+    _pathProviderInitialized();
+
+    await EasyLocalization.ensureInitialized();
+    await FlowyRunner.run(FlowyTestApp());
+
+    final test = AppFlowyUnitTest();
+    await test._signIn();
+    await test._loadWorkspace();
+
+    await test._initialServices();
+    return test;
+  }
+
+  Future<void> _signIn() async {
+    final authService = getIt<AuthService>();
+    const password = "AppFlowy123@";
+    final uid = uuid();
+    final userEmail = "[email protected]";
+    final result = await authService.signUp(
+      name: "TestUser",
+      password: password,
+      email: userEmail,
+    );
+    return result.fold(
+      (user) {
+        userProfile = user;
+        userService = UserService(userId: userProfile.id);
+      },
+      (error) {},
+    );
+  }
+
+  WorkspacePB get currentWorkspace => workspaces[0];
+
+  Future<void> _loadWorkspace() async {
+    final result = await userService.getWorkspaces();
+    result.fold(
+      (value) => workspaces = value,
+      (error) {
+        throw Exception(error);
+      },
+    );
+  }
+
+  Future<void> _initialServices() async {
+    workspaceService = WorkspaceService(workspaceId: currentWorkspace.id);
+  }
+
+  Future<AppPB> createTestApp() async {
+    final result = await workspaceService.createApp(name: "Test App");
+    return result.fold(
+      (app) => app,
+      (error) => throw Exception(error),
+    );
+  }
+}
+
+void _pathProviderInitialized() {
+  const MethodChannel channel =
+      MethodChannel('plugins.flutter.io/path_provider');
+  channel.setMockMethodCallHandler((MethodCall methodCall) async {
+    return ".";
+  });
+}
+
+class FlowyTestApp implements EntryPoint {
+  @override
+  Widget create() {
+    return Container();
+  }
+}
+
+Duration blocResponseDuration({int millseconds = 100}) {
+  return Duration(milliseconds: millseconds);
+}

+ 0 - 41
frontend/app_flowy/test/util/test_env.dart

@@ -1,41 +0,0 @@
-import 'package:app_flowy/startup/startup.dart';
-import 'package:app_flowy/user/application/auth_service.dart';
-import 'package:flowy_infra/uuid.dart';
-import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-
-class FlowyTest {
-  static Future<FlowyTest> setup() async {
-    TestWidgetsFlutterBinding.ensureInitialized();
-    // await EasyLocalization.ensureInitialized();
-
-    await FlowyRunner.run(FlowyTestApp());
-    return FlowyTest();
-  }
-
-  Future<UserProfilePB> signIn() async {
-    final authService = getIt<AuthService>();
-    const password = "AppFlowy123@";
-    final uid = uuid();
-    final userEmail = "[email protected]";
-    final result = await authService.signUp(
-      name: "FlowyTestUser",
-      password: password,
-      email: userEmail,
-    );
-    return result.fold(
-      (user) => user,
-      (error) {
-        throw StateError("$error");
-      },
-    );
-  }
-}
-
-class FlowyTestApp implements EntryPoint {
-  @override
-  Widget create() {
-    return Container();
-  }
-}

+ 0 - 29
frontend/app_flowy/test/workspace_bloc_test.dart

@@ -1,29 +0,0 @@
-import 'package:app_flowy/startup/startup.dart';
-import 'package:app_flowy/workspace/application/workspace/welcome_bloc.dart';
-import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:bloc_test/bloc_test.dart';
-
-import 'util/test_env.dart';
-
-void main() {
-  UserProfilePB? userInfo;
-  setUpAll(() async {
-    final flowyTest = await FlowyTest.setup();
-    userInfo = await flowyTest.signIn();
-  });
-
-  group('WelcomeBloc', () {
-    blocTest<WelcomeBloc, WelcomeState>(
-      "welcome screen init",
-      build: () => getIt<WelcomeBloc>(param1: userInfo),
-      act: (bloc) {
-        bloc.add(const WelcomeEvent.initial());
-      },
-      wait: const Duration(seconds: 3),
-      verify: (bloc) {
-        assert(bloc.state.isLoading == false);
-      },
-    );
-  });
-}

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

@@ -93,10 +93,9 @@ impl GridManager {
         Ok(())
         Ok(())
     }
     }
 
 
-    #[tracing::instrument(level = "debug", skip_all, fields(grid_id), err)]
+    #[tracing::instrument(level = "debug", skip_all, err)]
     pub async fn open_grid<T: AsRef<str>>(&self, grid_id: T) -> FlowyResult<Arc<GridRevisionEditor>> {
     pub async fn open_grid<T: AsRef<str>>(&self, grid_id: T) -> FlowyResult<Arc<GridRevisionEditor>> {
         let grid_id = grid_id.as_ref();
         let grid_id = grid_id.as_ref();
-        tracing::Span::current().record("grid_id", &grid_id);
         let _ = self.migration.run_v1_migration(grid_id).await;
         let _ = self.migration.run_v1_migration(grid_id).await;
         self.get_or_create_grid_editor(grid_id).await
         self.get_or_create_grid_editor(grid_id).await
     }
     }

+ 1 - 0
frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/mod.rs

@@ -1,6 +1,7 @@
 mod multi_select_type_option;
 mod multi_select_type_option;
 mod select_type_option;
 mod select_type_option;
 mod single_select_type_option;
 mod single_select_type_option;
+mod type_option_transform;
 
 
 pub use multi_select_type_option::*;
 pub use multi_select_type_option::*;
 pub use select_type_option::*;
 pub use select_type_option::*;

+ 2 - 1
frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/multi_select_type_option.rs

@@ -1,6 +1,7 @@
 use crate::entities::FieldType;
 use crate::entities::FieldType;
 use crate::impl_type_option;
 use crate::impl_type_option;
 use crate::services::cell::{CellBytes, CellData, CellDataChangeset, CellDataOperation, CellDisplayable};
 use crate::services::cell::{CellBytes, CellData, CellDataChangeset, CellDataOperation, CellDisplayable};
+use crate::services::field::selection_type_option::type_option_transform::SelectOptionTypeOptionTransformer;
 use crate::services::field::type_options::util::get_cell_data;
 use crate::services::field::type_options::util::get_cell_data;
 use crate::services::field::{
 use crate::services::field::{
     BoxTypeOptionBuilder, SelectOptionCellChangeset, SelectOptionIds, SelectOptionPB, SelectTypeOptionSharedAction,
     BoxTypeOptionBuilder, SelectOptionCellChangeset, SelectOptionIds, SelectOptionPB, SelectTypeOptionSharedAction,
@@ -110,7 +111,7 @@ impl TypeOptionBuilder for MultiSelectTypeOptionBuilder {
     }
     }
 
 
     fn transform(&mut self, field_type: &FieldType, type_option_data: String) {
     fn transform(&mut self, field_type: &FieldType, type_option_data: String) {
-        self.0.transform_type_option(field_type, type_option_data);
+        SelectOptionTypeOptionTransformer::transform_type_option(&mut self.0, field_type, type_option_data)
     }
     }
 }
 }
 #[cfg(test)]
 #[cfg(test)]

+ 23 - 21
frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_type_option.rs

@@ -2,7 +2,8 @@ use crate::entities::{CellChangesetPB, FieldType, GridCellIdPB, GridCellIdParams
 use crate::services::cell::{
 use crate::services::cell::{
     CellBytes, CellBytesParser, CellData, CellDataIsEmpty, CellDisplayable, FromCellChangeset, FromCellString,
     CellBytes, CellBytesParser, CellData, CellDataIsEmpty, CellDisplayable, FromCellChangeset, FromCellString,
 };
 };
-use crate::services::field::{MultiSelectTypeOptionPB, SingleSelectTypeOptionPB, CHECK, UNCHECK};
+use crate::services::field::selection_type_option::type_option_transform::SelectOptionTypeOptionTransformer;
+use crate::services::field::{MultiSelectTypeOptionPB, SingleSelectTypeOptionPB};
 use bytes::Bytes;
 use bytes::Bytes;
 use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
 use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
 use flowy_error::{internal_error, ErrorCode, FlowyResult};
 use flowy_error::{internal_error, ErrorCode, FlowyResult};
@@ -119,25 +120,6 @@ pub trait SelectTypeOptionSharedAction: TypeOptionDataSerializer + Send + Sync {
         }
         }
     }
     }
 
 
-    fn transform_type_option(&mut self, field_type: &FieldType, _type_option_data: String) {
-        match field_type {
-            FieldType::Checkbox => {
-                //add Yes and No options if it does not exist.
-                if !self.options().iter().any(|option| option.name == CHECK) {
-                    let check_option = SelectOptionPB::with_color(CHECK, SelectOptionColorPB::Green);
-                    self.mut_options().push(check_option);
-                }
-
-                if !self.options().iter().any(|option| option.name == UNCHECK) {
-                    let uncheck_option = SelectOptionPB::with_color(UNCHECK, SelectOptionColorPB::Yellow);
-                    self.mut_options().push(uncheck_option);
-                }
-            }
-            FieldType::MultiSelect => {}
-            _ => {}
-        }
-    }
-
     fn transform_cell_data(
     fn transform_cell_data(
         &self,
         &self,
         cell_data: CellData<SelectOptionIds>,
         cell_data: CellData<SelectOptionIds>,
@@ -150,6 +132,21 @@ pub trait SelectTypeOptionSharedAction: TypeOptionDataSerializer + Send + Sync {
             }
             }
             FieldType::Checkbox => {
             FieldType::Checkbox => {
                 // transform the cell data to the option id
                 // transform the cell data to the option id
+                let mut transformed_ids = Vec::new();
+                let options = self.options();
+                cell_data.0.iter().for_each(|ids| {
+                    ids.0.iter().for_each(|name| {
+                        let id = options
+                            .iter()
+                            .find(|option| option.name == name.clone())
+                            .unwrap()
+                            .id
+                            .clone();
+                        transformed_ids.push(id);
+                    })
+                });
+
+                return CellBytes::from(self.get_selected_options(CellData(Some(SelectOptionIds(transformed_ids)))));
             }
             }
             _ => {
             _ => {
                 return Ok(CellBytes::default());
                 return Ok(CellBytes::default());
@@ -174,7 +171,12 @@ where
         decoded_field_type: &FieldType,
         decoded_field_type: &FieldType,
         field_rev: &FieldRevision,
         field_rev: &FieldRevision,
     ) -> FlowyResult<CellBytes> {
     ) -> FlowyResult<CellBytes> {
-        self.transform_cell_data(cell_data, decoded_field_type, field_rev)
+        SelectOptionTypeOptionTransformer::transform_type_option_cell_data(
+            self,
+            cell_data,
+            decoded_field_type,
+            field_rev,
+        )
     }
     }
 
 
     fn displayed_cell_string(
     fn displayed_cell_string(

+ 2 - 1
frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/single_select_type_option.rs

@@ -1,6 +1,7 @@
 use crate::entities::FieldType;
 use crate::entities::FieldType;
 use crate::impl_type_option;
 use crate::impl_type_option;
 use crate::services::cell::{CellBytes, CellData, CellDataChangeset, CellDataOperation, CellDisplayable};
 use crate::services::cell::{CellBytes, CellData, CellDataChangeset, CellDataOperation, CellDisplayable};
+use crate::services::field::selection_type_option::type_option_transform::SelectOptionTypeOptionTransformer;
 use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder};
 use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder};
 use crate::services::field::{
 use crate::services::field::{
     SelectOptionCellChangeset, SelectOptionIds, SelectOptionPB, SelectTypeOptionSharedAction,
     SelectOptionCellChangeset, SelectOptionIds, SelectOptionPB, SelectTypeOptionSharedAction,
@@ -96,7 +97,7 @@ impl TypeOptionBuilder for SingleSelectTypeOptionBuilder {
     }
     }
 
 
     fn transform(&mut self, field_type: &FieldType, type_option_data: String) {
     fn transform(&mut self, field_type: &FieldType, type_option_data: String) {
-        self.0.transform_type_option(field_type, type_option_data);
+        SelectOptionTypeOptionTransformer::transform_type_option(&mut self.0, field_type, type_option_data)
     }
     }
 }
 }
 
 

+ 63 - 0
frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/type_option_transform.rs

@@ -0,0 +1,63 @@
+use crate::entities::FieldType;
+use crate::services::cell::{CellBytes, CellData};
+use crate::services::field::{
+    SelectOptionColorPB, SelectOptionIds, SelectOptionPB, SelectTypeOptionSharedAction, CHECK, UNCHECK,
+};
+use flowy_error::FlowyResult;
+use flowy_grid_data_model::revision::FieldRevision;
+
+/// Handles how to transform the cell data when switching between different field types
+pub struct SelectOptionTypeOptionTransformer();
+impl SelectOptionTypeOptionTransformer {
+    pub fn transform_type_option<T>(shared: &mut T, field_type: &FieldType, _type_option_data: String)
+    where
+        T: SelectTypeOptionSharedAction,
+    {
+        match field_type {
+            FieldType::Checkbox => {
+                //add Yes and No options if it does not exist.
+                if !shared.options().iter().any(|option| option.name == CHECK) {
+                    let check_option = SelectOptionPB::with_color(CHECK, SelectOptionColorPB::Green);
+                    shared.mut_options().push(check_option);
+                }
+
+                if !shared.options().iter().any(|option| option.name == UNCHECK) {
+                    let uncheck_option = SelectOptionPB::with_color(UNCHECK, SelectOptionColorPB::Yellow);
+                    shared.mut_options().push(uncheck_option);
+                }
+            }
+            FieldType::MultiSelect => {}
+            _ => {}
+        }
+    }
+
+    pub fn transform_type_option_cell_data<T>(
+        shared: &T,
+        cell_data: CellData<SelectOptionIds>,
+        decoded_field_type: &FieldType,
+        _field_rev: &FieldRevision,
+    ) -> FlowyResult<CellBytes>
+    where
+        T: SelectTypeOptionSharedAction,
+    {
+        match decoded_field_type {
+            FieldType::SingleSelect | FieldType::MultiSelect => {
+                //
+                CellBytes::from(shared.get_selected_options(cell_data))
+            }
+            FieldType::Checkbox => {
+                // transform the cell data to the option id
+                let mut transformed_ids = Vec::new();
+                let options = shared.options();
+                cell_data.try_into_inner()?.iter().for_each(|name| {
+                    if let Some(option) = options.iter().find(|option| &option.name == name) {
+                        transformed_ids.push(option.id.clone());
+                    }
+                });
+                let transformed_cell_data = CellData::from(SelectOptionIds::from(transformed_ids));
+                CellBytes::from(shared.get_selected_options(transformed_cell_data))
+            }
+            _ => Ok(CellBytes::default()),
+        }
+    }
+}

+ 3 - 1
frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs

@@ -31,11 +31,13 @@ impl TypeOptionBuilder for RichTextTypeOptionBuilder {
     }
     }
 }
 }
 
 
+/// For the moment, the `RichTextTypeOptionPB` is empty. The `data` property is not
+/// used yet.
 #[derive(Debug, Clone, Default, Serialize, Deserialize, ProtoBuf)]
 #[derive(Debug, Clone, Default, Serialize, Deserialize, ProtoBuf)]
 pub struct RichTextTypeOptionPB {
 pub struct RichTextTypeOptionPB {
     #[pb(index = 1)]
     #[pb(index = 1)]
     #[serde(default)]
     #[serde(default)]
-    data: String, //It's not used yet
+    data: String,
 }
 }
 impl_type_option!(RichTextTypeOptionPB, FieldType::RichText);
 impl_type_option!(RichTextTypeOptionPB, FieldType::RichText);
 
 

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

@@ -100,15 +100,15 @@ impl GridRevisionEditor {
     ///
     ///
     /// * `grid_id`: the id of the grid
     /// * `grid_id`: the id of the grid
     /// * `field_id`: the id of the field
     /// * `field_id`: the id of the field
-    /// * `type_option_data`: the updated type-option data.
-    ///
+    /// * `type_option_data`: the updated type-option data. The `type-option` data might be empty
+    /// if there is no type-option config for that field. For example, the `RichTextTypeOptionPB`.
+    ///  
     pub async fn update_field_type_option(
     pub async fn update_field_type_option(
         &self,
         &self,
         grid_id: &str,
         grid_id: &str,
         field_id: &str,
         field_id: &str,
         type_option_data: Vec<u8>,
         type_option_data: Vec<u8>,
     ) -> FlowyResult<()> {
     ) -> FlowyResult<()> {
-        debug_assert!(!type_option_data.is_empty());
         if type_option_data.is_empty() {
         if type_option_data.is_empty() {
             return Ok(());
             return Ok(());
         }
         }

+ 27 - 17
frontend/scripts/makefile/desktop.toml

@@ -21,17 +21,18 @@ run_task = { name = ["setup-crate-type","sdk-build-android", "restore-crate-type
 [tasks.flowy-sdk-dev-macos]
 [tasks.flowy-sdk-dev-macos]
 category = "Build"
 category = "Build"
 dependencies = ["env_check"]
 dependencies = ["env_check"]
-run_task = { name = ["setup-crate-type","sdk-build", "post-desktop", "restore-crate-type", "copy-to-sys-tmpdir"] }
+run_task = { name = ["setup-crate-type","sdk-build", "post-desktop", "restore-crate-type"] }
 
 
 [tasks.flowy-sdk-dev-windows]
 [tasks.flowy-sdk-dev-windows]
 category = "Build"
 category = "Build"
 dependencies = ["env_check"]
 dependencies = ["env_check"]
-run_task = { name = ["setup-crate-type","sdk-build", "post-desktop", "restore-crate-type", "copy-to-sys-tmpdir"] }
+run_task = { name = ["setup-crate-type","sdk-build", "post-desktop", "restore-crate-type"] }
 
 
 [tasks.flowy-sdk-dev-linux]
 [tasks.flowy-sdk-dev-linux]
 category = "Build"
 category = "Build"
 dependencies = ["env_check"]
 dependencies = ["env_check"]
-run_task = { name = ["setup-crate-type","sdk-build", "post-desktop", "restore-crate-type", "copy-to-sys-tmpdir"] }
+run_task = { name = ["setup-crate-type","sdk-build", "post-desktop", "restore-crate-type"] }
+
 
 
 #
 #
 [tasks.sdk-build]
 [tasks.sdk-build]
@@ -114,7 +115,7 @@ script = [
   """
   """
     echo "🚀 🚀 🚀  Flowy-SDK(macOS) build success"
     echo "🚀 🚀 🚀  Flowy-SDK(macOS) build success"
     dart_ffi_dir= set ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/app_flowy/packages/flowy_sdk/${TARGET_OS}
     dart_ffi_dir= set ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/app_flowy/packages/flowy_sdk/${TARGET_OS}
-    lib = set lib${LIB_NAME}.${SDK_EXT}
+    lib = set lib${LIB_NAME}.${LIB_EXT}
 
 
     cp ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/target/${RUST_COMPILE_TARGET}/${BUILD_FLAG}/${lib} \
     cp ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/target/${RUST_COMPILE_TARGET}/${BUILD_FLAG}/${lib} \
     ${dart_ffi_dir}/${lib}
     ${dart_ffi_dir}/${lib}
@@ -131,7 +132,7 @@ script = [
   """
   """
     echo "🚀 🚀 🚀  Flowy-SDK(windows) build success"
     echo "🚀 🚀 🚀  Flowy-SDK(windows) build success"
     dart_ffi_dir= set ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/app_flowy/windows/flutter/dart_ffi
     dart_ffi_dir= set ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/app_flowy/windows/flutter/dart_ffi
-    lib = set ${LIB_NAME}.${SDK_EXT}
+    lib = set ${LIB_NAME}.${LIB_EXT}
 
 
     # copy dll
     # copy dll
     cp ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/target/${RUST_COMPILE_TARGET}/${BUILD_FLAG}/${lib} \
     cp ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/target/${RUST_COMPILE_TARGET}/${BUILD_FLAG}/${lib} \
@@ -150,7 +151,7 @@ script = [
   """
   """
     echo "🚀 🚀 🚀  Flowy-SDK(linux) build success"
     echo "🚀 🚀 🚀  Flowy-SDK(linux) build success"
     dart_ffi_dir= set ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/app_flowy/linux/flutter/dart_ffi
     dart_ffi_dir= set ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/app_flowy/linux/flutter/dart_ffi
-    lib = set lib${LIB_NAME}.${SDK_EXT}
+    lib = set lib${LIB_NAME}.${LIB_EXT}
 
 
     # copy dll
     # copy dll
     cp ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/target/${RUST_COMPILE_TARGET}/${BUILD_FLAG}/${lib} \
     cp ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/target/${RUST_COMPILE_TARGET}/${BUILD_FLAG}/${lib} \
@@ -163,25 +164,34 @@ script = [
 ]
 ]
 script_runner = "@duckscript"
 script_runner = "@duckscript"
 
 
-[tasks.copy-to-sys-tmpdir]
+[tasks.test-lib-build]
+category = "Build"
+dependencies = ["env_check"]
+run_task = { name = ["setup-test-crate-type","test-sdk-build", "copy-to-sandbox-folder", "restore-test-crate-type"] }
+
+[tasks.test-sdk-build]
 private = true
 private = true
 script = [
 script = [
   """
   """
-    # Copy the flowy_sdk lib to system temp directory for flutter unit test.
-    lib = set lib${LIB_NAME}.${SDK_EXT}
-    dest = set ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/app_flowy/.sandbox/${lib}
-    rm ${dest}
-    cp ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/target/${RUST_COMPILE_TARGET}/${BUILD_FLAG}/${lib} \
-    ${dest}
+    cd rust-lib/
+    rustup show
+    echo cargo build --package=dart-ffi --target ${TEST_COMPILE_TARGET} --features "${FEATURES}"
+    cargo build --package=dart-ffi --target ${TEST_COMPILE_TARGET} --features "${FEATURES}"
+    cd ../
   """,
   """,
 ]
 ]
-script_runner = "@duckscript"
+script_runner = "@shell"
 
 
-[tasks.copy-to-sys-tmpdir.windows]
+[tasks.copy-to-sandbox-folder]
 private = true
 private = true
 script = [
 script = [
   """
   """
-    # Doesn't work on windows
+    # Copy the flowy_sdk lib to system temp directory for flutter unit test.
+    lib = set lib${LIB_NAME}.${TEST_LIB_EXT}
+    dest = set ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/app_flowy/.sandbox/${lib}
+    rm ${dest}
+    cp ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/target/${TEST_COMPILE_TARGET}/${TEST_BUILD_FLAG}/${lib} \
+    ${dest}
   """,
   """,
 ]
 ]
-script_runner = "@duckscript"
+script_runner = "@duckscript"

+ 19 - 1
frontend/scripts/makefile/tool.toml

@@ -43,7 +43,7 @@ run_task = { name = "remove_files_with_pattern" }
 #Dart Clean
 #Dart Clean
 [tasks.rm_dart_generated_files]
 [tasks.rm_dart_generated_files]
 env = { "dart_flowy_sdk_path" = "./app_flowy/packages/flowy_sdk/" }
 env = { "dart_flowy_sdk_path" = "./app_flowy/packages/flowy_sdk/" }
-run_task = { name = ["rm_dart_generated_protobuf_files"] }
+run_task = { name = ["rm_dart_generated_protobuf_files", "rm_dart_generated_event_files"] }
 
 
 [tasks.rm_dart_generated_protobuf_files]
 [tasks.rm_dart_generated_protobuf_files]
 private = true
 private = true
@@ -63,6 +63,24 @@ script = [
 script_runner = "@duckscript"
 script_runner = "@duckscript"
 
 
 
 
+[tasks.rm_dart_generated_event_files]
+private = true
+script = [
+  """
+  dart_event_folder = glob_array ${dart_flowy_sdk_path}/lib/dispatch/dart_event
+
+  if not array_is_empty ${dart_event_folder}
+    echo Remove generated dart event files:
+    for path in ${dart_event_folder}
+        echo remove ${path}
+        rm -rf ${path}
+    end
+  end
+  """,
+]
+script_runner = "@duckscript"
+
+
 [tasks.remove_files_with_pattern]
 [tasks.remove_files_with_pattern]
 private = true
 private = true
 script = [
 script = [

+ 7 - 6
shared-lib/flowy-ast/src/ast.rs

@@ -112,7 +112,7 @@ pub struct ASTField<'a> {
 }
 }
 
 
 impl<'a> ASTField<'a> {
 impl<'a> ASTField<'a> {
-    pub fn new(cx: &Ctxt, field: &'a syn::Field, index: usize) -> Self {
+    pub fn new(cx: &Ctxt, field: &'a syn::Field, index: usize) -> Result<Self, String> {
         let mut bracket_inner_ty = None;
         let mut bracket_inner_ty = None;
         let mut bracket_ty = None;
         let mut bracket_ty = None;
         let mut bracket_category = Some(BracketCategory::Other);
         let mut bracket_category = Some(BracketCategory::Other);
@@ -144,15 +144,16 @@ impl<'a> ASTField<'a> {
                 }
                 }
             }
             }
             Ok(None) => {
             Ok(None) => {
-                cx.error_spanned_by(&field.ty, "fail to get the ty inner type");
+                let msg = format!("Fail to get the ty inner type: {:?}", field);
+                return Err(msg);
             }
             }
             Err(e) => {
             Err(e) => {
                 eprintln!("ASTField parser failed: {:?} with error: {}", field, e);
                 eprintln!("ASTField parser failed: {:?} with error: {}", field, e);
-                panic!()
+                return Err(e);
             }
             }
         }
         }
 
 
-        ASTField {
+        Ok(ASTField {
             member: match &field.ident {
             member: match &field.ident {
                 Some(ident) => syn::Member::Named(ident.clone()),
                 Some(ident) => syn::Member::Named(ident.clone()),
                 None => syn::Member::Unnamed(index.into()),
                 None => syn::Member::Unnamed(index.into()),
@@ -163,7 +164,7 @@ impl<'a> ASTField<'a> {
             bracket_ty,
             bracket_ty,
             bracket_inner_ty,
             bracket_inner_ty,
             bracket_category,
             bracket_category,
-        }
+        })
     }
     }
 
 
     pub fn ty_as_str(&self) -> String {
     pub fn ty_as_str(&self) -> String {
@@ -235,6 +236,6 @@ fn fields_from_ast<'a>(cx: &Ctxt, fields: &'a Punctuated<syn::Field, Token![,]>)
     fields
     fields
         .iter()
         .iter()
         .enumerate()
         .enumerate()
-        .map(|(index, field)| ASTField::new(cx, field, index))
+        .flat_map(|(index, field)| ASTField::new(cx, field, index).ok())
         .collect()
         .collect()
 }
 }

+ 1 - 2
shared-lib/flowy-ast/src/ty_ext.rs

@@ -74,8 +74,7 @@ pub fn parse_ty<'a>(ctxt: &Ctxt, ty: &'a syn::Type) -> Result<Option<TyInfo<'a>>
             }));
             }));
         };
         };
     }
     }
-    ctxt.error_spanned_by(ty, "Unsupported inner type, get inner type fail".to_string());
-    Ok(None)
+    Err("Unsupported inner type, get inner type fail".to_string())
 }
 }
 
 
 fn parse_bracketed(bracketed: &AngleBracketedGenericArguments) -> Vec<&syn::Type> {
 fn parse_bracketed(bracketed: &AngleBracketedGenericArguments) -> Vec<&syn::Type> {