瀏覽代碼

Merge branch 'main' into feat/history

appflowy 2 年之前
父節點
當前提交
1e3c69f15b
共有 100 個文件被更改,包括 2093 次插入1468 次删除
  1. 26 0
      .githooks/commit-msg
  2. 3 1
      .githooks/pre-commit
  3. 5 0
      .githooks/pre-push
  4. 2 2
      .github/workflows/ci.yaml
  5. 18 1
      .github/workflows/dart_lint.yml
  6. 7 9
      .github/workflows/dart_test.yml
  7. 1 1
      .github/workflows/release.yml
  8. 21 15
      .github/workflows/rust_lint.yml
  9. 15 1
      .gitignore
  10. 0 4
      .husky/commit-msg
  11. 0 35
      Makefile.toml
  12. 2 1
      README.md
  13. 40 13
      frontend/.vscode/launch.json
  14. 1 0
      frontend/.vscode/settings.json
  15. 93 26
      frontend/.vscode/tasks.json
  16. 0 2
      frontend/Brewfile
  17. 0 14
      frontend/Makefile
  18. 8 2
      frontend/Makefile.toml
  19. 0 48
      frontend/app_flowy/.vscode/launch.json
  20. 0 26
      frontend/app_flowy/.vscode/settings.json
  21. 0 129
      frontend/app_flowy/.vscode/tasks.json
  22. 1 2
      frontend/app_flowy/analysis_options.yaml
  23. 10 0
      frontend/app_flowy/assets/translations/en.json
  24. 6 0
      frontend/app_flowy/assets/translations/it-IT.json
  25. 199 0
      frontend/app_flowy/assets/translations/ja-JP.json
  26. 10 9
      frontend/app_flowy/assets/translations/pt-BR.json
  27. 146 0
      frontend/app_flowy/assets/translations/pt-PT.json
  28. 62 0
      frontend/app_flowy/assets/translations/ru-RU.json
  29. 39 0
      frontend/app_flowy/lib/core/folder_notification.dart
  30. 39 0
      frontend/app_flowy/lib/core/grid_notification.dart
  31. 0 62
      frontend/app_flowy/lib/core/notification_helper.dart
  32. 39 0
      frontend/app_flowy/lib/core/user_notification.dart
  33. 4 1
      frontend/app_flowy/lib/plugin/plugin.dart
  34. 27 31
      frontend/app_flowy/lib/startup/deps_resolver.dart
  35. 1 0
      frontend/app_flowy/lib/startup/tasks/app_widget.dart
  36. 2 0
      frontend/app_flowy/lib/startup/tasks/load_plugin.dart
  37. 5 5
      frontend/app_flowy/lib/user/application/auth_service.dart
  38. 3 2
      frontend/app_flowy/lib/user/application/sign_in_bloc.dart
  39. 3 2
      frontend/app_flowy/lib/user/application/sign_up_bloc.dart
  40. 94 61
      frontend/app_flowy/lib/user/application/user_listener.dart
  41. 36 9
      frontend/app_flowy/lib/user/application/user_service.dart
  42. 4 4
      frontend/app_flowy/lib/user/application/user_settings_service.dart
  43. 2 2
      frontend/app_flowy/lib/user/domain/auth_state.dart
  44. 6 6
      frontend/app_flowy/lib/user/presentation/router.dart
  45. 2 2
      frontend/app_flowy/lib/user/presentation/sign_in_screen.dart
  46. 2 2
      frontend/app_flowy/lib/user/presentation/sign_up_screen.dart
  47. 4 7
      frontend/app_flowy/lib/user/presentation/skip_log_in_screen.dart
  48. 1 1
      frontend/app_flowy/lib/user/presentation/splash_screen.dart
  49. 7 7
      frontend/app_flowy/lib/user/presentation/welcome_screen.dart
  50. 18 18
      frontend/app_flowy/lib/workspace/application/app/app_bloc.dart
  51. 7 7
      frontend/app_flowy/lib/workspace/application/app/app_listener.dart
  52. 11 11
      frontend/app_flowy/lib/workspace/application/app/app_service.dart
  53. 2 2
      frontend/app_flowy/lib/workspace/application/appearance.dart
  54. 0 0
      frontend/app_flowy/lib/workspace/application/board/board_bloc.dart
  55. 3 3
      frontend/app_flowy/lib/workspace/application/doc/doc_bloc.dart
  56. 8 8
      frontend/app_flowy/lib/workspace/application/doc/doc_service.dart
  57. 29 4
      frontend/app_flowy/lib/workspace/application/doc/share_bloc.dart
  58. 5 5
      frontend/app_flowy/lib/workspace/application/doc/share_service.dart
  59. 56 0
      frontend/app_flowy/lib/workspace/application/grid/block/block_cache.dart
  60. 51 0
      frontend/app_flowy/lib/workspace/application/grid/block/block_listener.dart
  61. 1 1
      frontend/app_flowy/lib/workspace/application/grid/cell/cell_listener.dart
  62. 70 0
      frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_cache.dart
  63. 79 0
      frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_data_loader.dart
  64. 16 19
      frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_data_persistence.dart
  65. 60 0
      frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_field_notifier.dart
  66. 36 32
      frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_service.dart
  67. 177 94
      frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/context_builder.dart
  68. 0 111
      frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/data_cache.dart
  69. 0 134
      frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/data_loader.dart
  70. 2 2
      frontend/app_flowy/lib/workspace/application/grid/cell/checkbox_cell_bloc.dart
  71. 10 9
      frontend/app_flowy/lib/workspace/application/grid/cell/date_cal_bloc.dart
  72. 11 11
      frontend/app_flowy/lib/workspace/application/grid/cell/date_cell_bloc.dart
  73. 2 2
      frontend/app_flowy/lib/workspace/application/grid/cell/number_cell_bloc.dart
  74. 5 5
      frontend/app_flowy/lib/workspace/application/grid/cell/select_option_cell_bloc.dart
  75. 23 40
      frontend/app_flowy/lib/workspace/application/grid/cell/select_option_editor_bloc.dart
  76. 18 18
      frontend/app_flowy/lib/workspace/application/grid/cell/select_option_service.dart
  77. 2 2
      frontend/app_flowy/lib/workspace/application/grid/cell/text_cell_bloc.dart
  78. 4 4
      frontend/app_flowy/lib/workspace/application/grid/cell/url_cell_bloc.dart
  79. 4 4
      frontend/app_flowy/lib/workspace/application/grid/cell/url_cell_editor_bloc.dart
  80. 5 5
      frontend/app_flowy/lib/workspace/application/grid/field/field_action_sheet_bloc.dart
  81. 3 3
      frontend/app_flowy/lib/workspace/application/grid/field/field_cell_bloc.dart
  82. 23 12
      frontend/app_flowy/lib/workspace/application/grid/field/field_editor_bloc.dart
  83. 0 57
      frontend/app_flowy/lib/workspace/application/grid/field/field_editor_pannel_bloc.dart
  84. 4 4
      frontend/app_flowy/lib/workspace/application/grid/field/field_listener.dart
  85. 44 47
      frontend/app_flowy/lib/workspace/application/grid/field/field_service.dart
  86. 57 0
      frontend/app_flowy/lib/workspace/application/grid/field/field_type_option_edit_bloc.dart
  87. 4 4
      frontend/app_flowy/lib/workspace/application/grid/field/grid_listenr.dart
  88. 3 2
      frontend/app_flowy/lib/workspace/application/grid/field/type_option/date_bloc.dart
  89. 7 7
      frontend/app_flowy/lib/workspace/application/grid/field/type_option/edit_select_option_bloc.dart
  90. 15 13
      frontend/app_flowy/lib/workspace/application/grid/field/type_option/multi_select_type_option.dart
  91. 2 2
      frontend/app_flowy/lib/workspace/application/grid/field/type_option/number_bloc.dart
  92. 1 1
      frontend/app_flowy/lib/workspace/application/grid/field/type_option/number_format_bloc.dart
  93. 12 12
      frontend/app_flowy/lib/workspace/application/grid/field/type_option/select_option_type_option_bloc.dart
  94. 14 13
      frontend/app_flowy/lib/workspace/application/grid/field/type_option/single_select_type_option.dart
  95. 18 19
      frontend/app_flowy/lib/workspace/application/grid/field/type_option/type_option_service.dart
  96. 73 44
      frontend/app_flowy/lib/workspace/application/grid/grid_bloc.dart
  97. 8 8
      frontend/app_flowy/lib/workspace/application/grid/grid_header_bloc.dart
  98. 0 42
      frontend/app_flowy/lib/workspace/application/grid/grid_listener.dart
  99. 90 78
      frontend/app_flowy/lib/workspace/application/grid/grid_service.dart
  100. 4 4
      frontend/app_flowy/lib/workspace/application/grid/prelude.dart

+ 26 - 0
.githooks/commit-msg

@@ -0,0 +1,26 @@
+#!/bin/sh
+#
+# An example hook script to check the commit log message.
+# Called by "git commit" with one argument, the name of the file
+# that has the commit message.  The hook should exit with non-zero
+# status after issuing an appropriate message if it wants to stop the
+# commit.  The hook is allowed to edit the commit message file.
+
+echo "Running the AppFlowy commit-msg hook."
+
+# This example catches duplicate Signed-off-by lines.
+
+test "" = "$(grep '^Signed-off-by: ' "$1" |
+	 sort | uniq -c | sed -e '/^[ 	]*1[ 	]/d')" || {
+	echo >&2 Duplicate Signed-off-by lines.
+	exit 1
+}
+
+npx --no -- commitlint --edit $1
+
+if [ $? -ne 0 ]
+then
+    echo "Please fix your commit message to match AppFlowy coding standards"
+    exit 1
+fi
+

+ 3 - 1
.githooks/pre-commit

@@ -1,5 +1,7 @@
 #!/usr/bin/env bash
 
+echo "Running local AppFlowy pre-commit hook."
+
 #flutter format .
 ##https://gist.github.com/benmccallum/28e4f216d9d72f5965133e6c43aaff6e
 limit=$(( 1 * 2**20 )) # 1MB
@@ -31,4 +33,4 @@ for file in $( git diff-index --cached --name-only $against ); do
 		file_too_large $filename $file_size
 		exit 1;
 	fi
-done
+done

+ 5 - 0
.githooks/pre-push

@@ -1,15 +1,20 @@
 #!/usr/bin/env bash
 
+echo "Running local AppFlowy pre-push hook."
+
 if [[ `git status --porcelain` ]]; then
   printf "\e[31;1m%s\e[0m\n" 'This script needs to run against committed code only. Please commit or stash you changes.'
   exit 1
 fi
+
 printf "\e[33;1m%s\e[0m\n" 'Running the Flutter analyzer'
 flutter analyze
+
 if [ $? -ne 0 ]; then
   printf "\e[31;1m%s\e[0m\n" 'Flutter analyzer error'
   exit 1
 fi
+
 printf "\e[33;1m%s\e[0m\n" 'Finished running the Flutter analyzer'
 printf "\e[33;1m%s\e[0m\n" 'Running unit tests'
 

+ 2 - 2
.github/workflows/ci.yaml

@@ -16,7 +16,7 @@ jobs:
         os: [ubuntu-latest, macos-latest]
         include:
           - os: ubuntu-latest
-            flutter_profile: development-linux-x86
+            flutter_profile: development-linux-x86_64
           - os: macos-latest
             flutter_profile: development-mac-x86_64
     runs-on: ${{ matrix.os }}
@@ -82,4 +82,4 @@ jobs:
       - name: Build
         working-directory: frontend
         run: |
-          cargo make --profile ${{ matrix.flutter_profile }} appflowy-dev
+          cargo make --profile ${{ matrix.flutter_profile }} appflowy-dev

+ 18 - 1
.github/workflows/dart_lint.yml

@@ -25,14 +25,31 @@ jobs:
         with:
           flutter-version: '3.0.0'
           channel: "stable"
-      - name: Deps Flutter
+      - uses: actions-rs/toolchain@v1
+        with:
+          toolchain: 'stable-2022-01-20'
+
+      - name: Rust Deps
+        working-directory: frontend
+        run: |
+          cargo install cargo-make
+          cargo make flowy_dev
+
+      - name: Flutter Deps
         run: flutter packages pub get
         working-directory: frontend/app_flowy
+
+      - name: Build FlowySDK
+        working-directory: frontend
+        run: |
+          cargo make --profile development-linux-x86_64 flowy-sdk-dev
+
       - name: Code Generation
         working-directory: frontend/app_flowy
         run: |
           flutter packages pub run easy_localization:generate -f keys -o locale_keys.g.dart -S assets/translations -s en.json
           flutter packages pub run build_runner build --delete-conflicting-outputs
+
       - name: Run Flutter Analyzer
         working-directory: frontend/app_flowy
         run: flutter analyze

+ 7 - 9
.github/workflows/dart_test.yml

@@ -43,18 +43,21 @@ jobs:
             shared-lib/target
           key: ${{ runner.os }}-rust-rust-lib-share-lib-${{ steps.rust_toolchain.outputs.rustc_hash }}-${{ hashFiles('./frontend/rust-lib/Cargo.toml') }}    
 
-      - name: Deps Flutter
+      - name: Flutter Deps
         working-directory: frontend/app_flowy
         run: |
           flutter config --enable-linux-desktop
         
-      - name: Deps Rust
+      - name: Rust Deps
         working-directory: frontend
         run: |
           cargo install cargo-make
-          cargo install duckscript_cli
           cargo make flowy_dev
-      
+      - name: Build FlowySDK
+        working-directory: frontend
+        run: |
+          cargo make --profile development-linux-x86_64 flowy-sdk-dev
+
       - name: Code Generation
         working-directory: frontend/app_flowy
         run: |
@@ -62,11 +65,6 @@ jobs:
           flutter packages pub run easy_localization:generate -f keys -o locale_keys.g.dart -S assets/translations -s en.json
           flutter packages pub run build_runner build --delete-conflicting-outputs
       
-      - name: Build FlowySDK
-        working-directory: frontend
-        run: |
-          cargo make --profile development-linux-x86 flowy-sdk-dev
-      
       - name: Run bloc tests
         working-directory: frontend/app_flowy
         run: |

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

@@ -67,7 +67,7 @@ jobs:
         working-directory: frontend
         run: |
           flutter config --enable-linux-desktop
-          cargo make --env APP_VERSION=${{ github.ref_name }} --profile production-linux-x86 appflowy
+          cargo make --env APP_VERSION=${{ github.ref_name }} --profile production-linux-x86_64 appflowy
 
       - name: Upload Release Asset
         id: upload-release-asset

+ 21 - 15
.github/workflows/rust_lint.yml

@@ -17,25 +17,31 @@ jobs:
     steps:
       - uses: actions/checkout@v2
       - uses: actions-rs/toolchain@v1
-        with:
-          toolchain: 'stable-2022-01-20'
-          override: true
+      with:
+        toolchain: 'stable-2022-01-20'
+        override: true
+
+      - name: Rust Deps
+        working-directory: frontend
+        run: |
+          cargo install cargo-make
+          cargo make flowy_dev
+
+      - name: Build FlowySDK
+        working-directory: frontend
+        run: |
+          cargo make --profile development-linux-x86_64 flowy-sdk-dev
+
       - run: rustup component add rustfmt
         working-directory: frontend/rust-lib
-      - run: cargo fmt --all -- --check
+      - name: rustfmt
+        run: cargo fmt --all -- --check
         working-directory: frontend/rust-lib/
 
 
-  rust-clippy:
-    runs-on: ubuntu-latest
-    name: Clippy
-    steps:
-      - uses: actions/checkout@v2
-      - uses: actions-rs/toolchain@v1
-        with:
-          toolchain: 'stable-2022-01-20'
-          override: true
       - run: rustup component add clippy
+          working-directory: frontend/rust-lib
+      - name: clippy
+        run: cargo clippy --no-default-features
         working-directory: frontend/rust-lib
-      - run: cargo clippy --no-default-features
-        working-directory: frontend/rust-lib
+

+ 15 - 1
.gitignore

@@ -17,4 +17,18 @@ yarn.lock
 node_modules
 **/.proto_cache
 **/.cache
-**/.DS_Store
+**/.DS_Store
+
+**/src/protobuf
+**/resources/proto
+
+frontend/.vscode/*
+!frontend/.vscode/settings.json
+!frontend/.vscode/tasks.json
+!frontend/.vscode/launch.json
+!frontend/.vscode/extensions.json
+!frontend/.vscode/*.code-snippets
+
+# Commit the highest level pubspec.lock, but ignore the others
+pubspec.lock
+!frontend/app_flowy/pubspec.lock

+ 0 - 4
.husky/commit-msg

@@ -1,4 +0,0 @@
-#!/bin/sh
-. "$(dirname "$0")/_/husky.sh"
-
-npx --no -- commitlint --edit 

+ 0 - 35
Makefile.toml

@@ -1,35 +0,0 @@
-[tasks.install-commitlint.mac]
-script = [
-    """
-    brew install npm
-  	yarn install
-  	yarn husky install
-    """,
-]
-script_runner = "@shell"
-
-[tasks.install-commitlint.windows]
-script = [
-    """
-    echo "WIP"
-    """,
-]
-script_runner = "@duckscript"
-
-[tasks.install-commitlint.linux]
-script = [
-    """
-    if command -v apt &> /dev/null
-    then
-      echo "Installing node.js and yarn (sudo apt install nodejs yarn)"
-      sudo apt install nodejs yarn
-    else
-    echo "Installing node.js and yarn (sudo pacman -S nodejs yarn)"
-      sudo pacman -S nodejs yarn
-    fi
-
-    yarn install
-    yarn husky install
-    """,
-]
-script_runner = "@shell"

+ 2 - 1
README.md

@@ -23,7 +23,8 @@ You are in charge of your data and customizations.
     <a href="https://twitter.com/appflowy"><b>Twitter</b></a>
 </p>
 
-<p align="center"><img src="https://github.com/AppFlowy-IO/appflowy/blob/main/doc/imgs/welcome.png" alt="The Open Source Alternative To Notion." width="1000px" /></p>
+<p align="center"><img src="https://user-images.githubusercontent.com/12026239/174754661-682980e4-e386-4685-bb6f-2da357390b61.png" alt="The Open Source Alternative To Notion." width="1000px" /></p>
+<p align="center"><img src="https://user-images.githubusercontent.com/12026239/174753177-98e4c899-2356-4137-bb42-374bba2b127b.png" alt="The Open Source Alternative To Notion." width="1000px" /></p>
 
 ## User Installation
 

+ 40 - 13
frontend/.vscode/launch.json

@@ -5,40 +5,67 @@
     "version": "0.2.0",
     "configurations": [
         {
-            "name": "app_flowy",
+            // This task builds the Rust and Dart code of AppFlowy.
+            "name": "AF: Build All",
             "request": "launch",
             "program": "./lib/main.dart",
             "type": "dart",
-            "preLaunchTask": "build_flowy_sdk",
-            "env":{
-                "RUST_LOG":"info"
+            "preLaunchTask": "AF: build_flowy_sdk",
+            "env": {
+                "RUST_LOG": "info"
             },
             "cwd": "${workspaceRoot}/app_flowy"
         },
         {
-            "name": "app_flowy(trace)",
+            "name": "AF: Debug Rust",
+            "request": "attach",
+            "type": "lldb",
+            "pid": "${command:pickMyProcess}"
+        },
+        {
+            //  This task only builds the Dart code of AppFlowy.
+            "name": "AF: Build Dart Only",
             "request": "launch",
             "program": "./lib/main.dart",
             "type": "dart",
-            "preLaunchTask": "build_flowy_sdk",
-            "env":{
-                "RUST_LOG":"trace"
+            "env": {
+                "RUST_LOG": "debug"
             },
             "cwd": "${workspaceRoot}/app_flowy"
         },
         {
-            "name": "app_flowy (profile mode)",
+            // This task builds will:
+            // - call the clean task,
+            // - rebuild all the generated Files (including freeze and language files)
+            // - rebuild the the Rust and Dart code of AppFlowy.
+            "name": "AF: Clean + Rebuild All",
             "request": "launch",
+            "program": "./lib/main.dart",
             "type": "dart",
-            "flutterMode": "profile"
+            "preLaunchTask": "AF: Clean + Rebuild All",
+            "env": {
+                "RUST_LOG": "info"
+            },
+            "cwd": "${workspaceRoot}/app_flowy"
         },
         {
-            "name": "Generate Language Files",
+            "name": "AF: Build All (rustlog: trace)",
             "request": "launch",
             "program": "./lib/main.dart",
             "type": "dart",
-            "preLaunchTask": "Generate Language Files",
-            "cwd": "${workspaceRoot}/app_flowy/"
+            "preLaunchTask": "AF: build_flowy_sdk",
+            "env": {
+                "RUST_LOG": "trace"
+            },
+            "cwd": "${workspaceRoot}/app_flowy"
+        },
+        {
+            "name": "AF: app_flowy (profile mode)",
+            "request": "launch",
+            "program": "./lib/main.dart",
+            "type": "dart",
+            "flutterMode": "profile",
+            "cwd": "${workspaceRoot}/app_flowy"
         },
     ]
 }

+ 1 - 0
frontend/.vscode/settings.json

@@ -23,4 +23,5 @@
         "*.log.*": "log"
     },
     "editor.formatOnSave": true,
+    "files.eol": "\n",
 }

+ 93 - 26
frontend/.vscode/tasks.json

@@ -10,13 +10,31 @@
 	// ${cwd}: the current working directory of the spawned process
 	"tasks": [
 		{
-			"label": "build_flowy_sdk",
+			"label": "AF: Clean + Rebuild All",
+			"type": "shell",
+			"dependsOrder": "sequence",
+			"dependsOn": [
+				"AF: Rust Clean",
+				"AF: Flutter Clean",
+				"AF: build_flowy_sdk",
+				"AF: Flutter Pub Get",
+				"AF: Flutter Package Get",
+				"AF: Generate Language Files",
+				"AF: Generate Freezed Files",
+			],
+			"presentation": {
+				"reveal": "always",
+				"panel": "new"
+			}
+		},
+		{
+			"label": "AF: build_flowy_sdk",
 			"type": "shell",
 			"command": "sh ./scripts/build_sdk.sh",
 			"windows": {
 				"options": {
 					"env": {
-						"FLOWY_DEV_ENV": "Windows",
+						"FLOWY_DEV_ENV": "Windows"
 					},
 					"shell": {
 						"executable": "cmd.exe",
@@ -31,27 +49,76 @@
 			"linux": {
 				"options": {
 					"env": {
-						"FLOWY_DEV_ENV": "Linux-x86",
+						"FLOWY_DEV_ENV": "Linux"
 					}
-				},
+				}
 			},
 			"osx": {
 				"options": {
 					"env": {
-						"FLOWY_DEV_ENV": "macOS",
+						"FLOWY_DEV_ENV": "macOS"
 					}
-				},
+				}
 			},
 			"group": "build",
 			"options": {
 				"cwd": "${workspaceFolder}"
+			}
+		},
+		{
+			"label": "AF: Code Gen",
+			"type": "shell",
+			"dependsOrder": "sequence",
+			"dependsOn": [
+				"AF: Flutter Clean",
+				"AF: Flutter Pub Get",
+				"AF: Flutter Package Get",
+				"AF: Generate Language Files",
+				"AF: Generate Freezed Files"
+			],
+			"group": {
+				"kind": "build",
+				"isDefault": true
 			},
-			// "problemMatcher": [
-			//     "$rustc"
-			// ],
+			"presentation": {
+				"reveal": "always",
+				"panel": "new"
+			}
+		},
+		{
+			"label": "AF: Flutter Clean",
+			"type": "shell",
+			"command": "flutter clean",
+			"options": {
+				"cwd": "${workspaceFolder}/app_flowy"
+			}
+		},
+		{
+			"label": "AF: Flutter Pub Get",
+			"type": "shell",
+			"command": "flutter pub get",
+			"options": {
+				"cwd": "${workspaceFolder}/app_flowy"
+			}
+		},
+		{
+			"label": "AF: Flutter Package Get",
+			"type": "shell",
+			"command": "flutter packages pub get",
+			"options": {
+				"cwd": "${workspaceFolder}/app_flowy"
+			}
 		},
 		{
-			"label": "Generate Language Files",
+			"label": "AF: Generate Freezed Files",
+			"type": "shell",
+			"command": "flutter pub run build_runner build --delete-conflicting-outputs",
+			"options": {
+				"cwd": "${workspaceFolder}/app_flowy"
+			}
+		},
+		{
+			"label": "AF: Generate Language Files",
 			"type": "shell",
 			"command": "sh ./scripts/generate_language_files.sh",
 			"windows": {
@@ -69,28 +136,28 @@
 			"group": "build",
 			"options": {
 				"cwd": "${workspaceFolder}"
-			},
+			}
 		},
 		{
-			"label": "Clean",
+			"label": "AF: Rust Clean",
 			"type": "shell",
-			"command": "sh ./scripts/clean.sh",
-			"windows": {
-				"options": {
-					"shell": {
-						"executable": "cmd.exe",
-						"args": [
-							"/d",
-							"/c",
-							".\\scripts\\clean.cmd"
-						]
-					}
-				}
-			},
+			"command": "cargo make flowy_clean",
 			"group": "build",
 			"options": {
 				"cwd": "${workspaceFolder}"
-			},
+			}
+		},
+		{
+			"label": "AF: flutter build aar",
+			"type": "flutter",
+			"command": "flutter",
+			"args": [
+				"build",
+				"aar"
+			],
+			"group": "build",
+			"problemMatcher": [],
+			"detail": "app_flowy"
 		}
 	]
 }

+ 0 - 2
frontend/Brewfile

@@ -1,2 +0,0 @@
-brew 'sqlite3'
-brew 'rustup-init'

+ 0 - 14
frontend/Makefile

@@ -1,14 +0,0 @@
-.PHONY: flowy_dev_install flowy_clean
-
-flowy_dev_install:
-	brew bundle
-	rustup-init -y --default-toolchain=stable
-	cargo install --force cargo-make
-	cargo install --force duckscript_cli
-	cargo make flowy_dev
-
-
-flowy_clean:
-	sh ./scripts/clean.sh
-
-

+ 8 - 2
frontend/Makefile.toml

@@ -8,6 +8,7 @@ extend = [
     { path = "scripts/makefile/env.toml" },
     { path = "scripts/makefile/flutter.toml" },
     { path = "scripts/makefile/tool.toml" },
+    { path = "scripts/makefile/githooks.toml" },
 ]
 
 [config]
@@ -52,6 +53,7 @@ RUST_COMPILE_TARGET = "aarch64-apple-darwin"
 BUILD_FLAG = "debug"
 FLUTTER_OUTPUT_DIR = "Debug"
 PRODUCT_EXT = "app"
+BUILD_ARCHS = "arm64"
 
 [env.development-mac-x86_64]
 RUST_LOG = "info"
@@ -60,6 +62,7 @@ RUST_COMPILE_TARGET = "x86_64-apple-darwin"
 BUILD_FLAG = "debug"
 FLUTTER_OUTPUT_DIR = "Debug"
 PRODUCT_EXT = "app"
+BUILD_ARCHS = "x86_64"
 
 [env.production-mac-arm64]
 BUILD_FLAG = "release"
@@ -68,6 +71,7 @@ RUST_COMPILE_TARGET = "aarch64-apple-darwin"
 FLUTTER_OUTPUT_DIR = "Release"
 PRODUCT_EXT = "app"
 APP_ENVIRONMENT = "production"
+BUILD_ARCHS = "arm64"
 
 [env.production-mac-x86_64]
 BUILD_FLAG = "release"
@@ -76,6 +80,7 @@ RUST_COMPILE_TARGET = "x86_64-apple-darwin"
 FLUTTER_OUTPUT_DIR = "Release"
 PRODUCT_EXT = "app"
 APP_ENVIRONMENT = "production"
+BUILD_ARCHS = "x86_64"
 
 [env.development-windows-x86]
 TARGET_OS = "windows"
@@ -96,7 +101,7 @@ CRATE_TYPE = "cdylib"
 SDK_EXT = "dll"
 APP_ENVIRONMENT = "production"
 
-[env.development-linux-x86]
+[env.development-linux-x86_64]
 TARGET_OS = "linux"
 RUST_COMPILE_TARGET = "x86_64-unknown-linux-gnu"
 BUILD_FLAG = "debug"
@@ -105,7 +110,7 @@ FLUTTER_OUTPUT_DIR = "Debug"
 SDK_EXT = "so"
 LINUX_ARCH = "x64"
 
-[env.production-linux-x86]
+[env.production-linux-x86_64]
 BUILD_FLAG = "release"
 TARGET_OS = "linux"
 RUST_COMPILE_TARGET = "x86_64-unknown-linux-gnu"
@@ -146,6 +151,7 @@ script = [
     echo PRODUCT_EXT: ${PRODUCT_EXT}
     echo APP_ENVIRONMENT: ${APP_ENVIRONMENT}
     echo ${platforms}
+    echo ${BUILD_ARCHS}
     '''
 ]
 script_runner = "@shell"

+ 0 - 48
frontend/app_flowy/.vscode/launch.json

@@ -1,48 +0,0 @@
-{
-    // Use IntelliSense to learn about possible attributes.
-    // Hover to view descriptions of existing attributes.
-    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
-    "version": "0.2.0",
-    "configurations": [
-        {
-            // This task builds the Rust and Dart code of AppFlowy.
-            "name": "Build",
-            "request": "launch",
-            "program": "${workspaceRoot}/lib/main.dart",
-            "preLaunchTask": "build_flowy_sdk",
-            "type": "dart",
-            "env": {
-                "RUST_LOG": "debug"
-            },
-            "cwd": "${workspaceRoot}"
-        },
-        {
-            //  This task only build the Dart code of AppFlowy.
-            "name": "Build (Dart)",
-            "request": "launch",
-            "program": "${workspaceRoot}/lib/main.dart",
-            "type": "dart",
-            "env": {
-                "RUST_LOG": "debug"
-            },
-            "cwd": "${workspaceRoot}"
-        },
-        {
-            "name": "Build (trace log)",
-            "request": "launch",
-            "program": "${workspaceRoot}/lib/main.dart",
-            "type": "dart",
-            "preLaunchTask": "build_flowy_sdk",
-            "env": {
-                "RUST_LOG": "trace"
-            },
-            "cwd": "${workspaceRoot}"
-        },
-        {
-            "name": "Build (profile mode)",
-            "request": "launch",
-            "type": "dart",
-            "flutterMode": "profile"
-        },
-    ]
-}

+ 0 - 26
frontend/app_flowy/.vscode/settings.json

@@ -1,26 +0,0 @@
-{
-    "[dart]": {
-        "editor.formatOnSave": true,
-        "editor.formatOnType": true,
-        "editor.rulers": [
-            120
-        ],
-        "editor.selectionHighlight": false,
-        "editor.suggest.snippetsPreventQuickSuggestions": false,
-        "editor.suggestSelection": "first",
-        "editor.tabCompletion": "onlySnippets",
-        "editor.wordBasedSuggestions": false
-    },
-    "svgviewer.enableautopreview": true,
-    "svgviewer.previewcolumn": "Active",
-    "svgviewer.showzoominout": true,
-    "editor.wordWrapColumn": 120,
-    "editor.minimap.maxColumn": 140,
-    "prettier.printWidth": 140,
-    "editor.wordWrap": "wordWrapColumn",
-    "dart.lineLength": 120,
-    "files.associations": {
-        "*.log.*": "log"
-    },
-    "editor.formatOnSave": true,
-}

+ 0 - 129
frontend/app_flowy/.vscode/tasks.json

@@ -1,129 +0,0 @@
-{
-	"version": "2.0.0",
-	// https://code.visualstudio.com/docs/editor/tasks
-	// https://gist.github.com/deadalusai/9e13e36d61ec7fb72148
-	// ${workspaceRoot}: the root folder of the team
-	// ${file}: the current opened file
-	// ${fileBasename}: the current opened file's basename
-	// ${fileDirname}: the current opened file's dirname
-	// ${fileExtname}: the current opened file's extension
-	// ${cwd}: the current working directory of the spawned process
-	"tasks": [
-		{
-			"label": "build_flowy_sdk",
-			"type": "shell",
-			"command": "sh ./scripts/build_sdk.sh",
-			"windows": {
-				"options": {
-					"env": {
-						"FLOWY_DEV_ENV": "Windows",
-					},
-					"shell": {
-						"executable": "cmd.exe",
-						"args": [
-							"/d",
-							"/c",
-							".\\scripts\\build_sdk.cmd"
-						]
-					}
-				}
-			},
-			"linux": {
-				"options": {
-					"env": {
-						"FLOWY_DEV_ENV": "Linux-x86",
-					}
-				},
-			},
-			"osx": {
-				"options": {
-					"env": {
-						"FLOWY_DEV_ENV": "macOS",
-					}
-				},
-			},
-			"group": "build",
-			"options": {
-				"cwd": "${workspaceFolder}/../"
-			},
-			// "problemMatcher": [
-			//     "$rustc"
-			// ],
-		},
-		{
-			"label": "Code Gen",
-			"type": "shell",
-			"dependsOn": [
-				"Flutter Pub",
-				"Flutter Package Get",
-				"Generate Language Files",
-				"Generate Freezed Files"
-			],
-			"group": {
-				"kind": "build",
-				"isDefault": true,
-			},
-			"dependsOrder": "sequence",
-			"presentation": {
-				"reveal": "always",
-				"panel": "new"
-			},
-		},
-		{
-			"label": "Flutter Pub",
-			"type": "shell",
-			"command": "flutter pub get",
-		},
-		{
-			"label": "Flutter Package Get",
-			"type": "shell",
-			"command": "flutter packages pub get",
-		},
-		{
-			"label": "Generate Freezed Files",
-			"type": "shell",
-			"command": "flutter pub run build_runner build --delete-conflicting-outputs",
-		},
-		{
-			"label": "Generate Language Files",
-			"type": "shell",
-			"command": "sh ./scripts/generate_language_files.sh",
-			"windows": {
-				"options": {
-					"shell": {
-						"executable": "cmd.exe",
-						"args": [
-							"/d",
-							"/c",
-							".\\scripts\\generate_language_files.cmd"
-						]
-					}
-				}
-			},
-			"group": "build",
-			"options": {
-				"cwd": "${workspaceFolder}/../"
-			},
-		},
-		{
-			"label": "Clean",
-			"type": "shell",
-			"command": "sh ./scripts/clean.sh",
-			"windows": {
-				"options": {
-					"shell": {
-						"executable": "cmd.exe",
-						"args": [
-							"/d",
-							"/c",
-							".\\scripts\\clean.cmd"
-						]
-					}
-				}
-			},
-			"options": {
-				"cwd": "${workspaceFolder}/../"
-			},
-		}
-	]
-}

+ 1 - 2
frontend/app_flowy/analysis_options.yaml

@@ -17,7 +17,6 @@ analyzer:
     - "packages/flowy_editor/**"
     - "packages/editor/**"
     # - "packages/flowy_infra_ui/**"
-    
 linter:
   # The lint rules applied to this project can be customized in the
   # section below to disable rules from the `package:flutter_lints/flutter.yaml`
@@ -38,4 +37,4 @@ linter:
 # https://dart.dev/guides/language/analysis-options
 
 errors:
-  invalid_annotation_target: ignore
+  invalid_annotation_target: ignore

+ 10 - 0
frontend/app_flowy/assets/translations/en.json

@@ -96,6 +96,12 @@
     "lightMode": "Switch to Light mode",
     "darkMode": "Switch to Dark mode"
   },
+  "notifications": {
+    "export": {
+      "markdown": "Exported Note To Markdown",
+      "path": "Documents/flowy"
+    }
+  },
   "contactsPage": {
     "title": "Contacts",
     "whatsHappening": "What's happening this week?",
@@ -199,6 +205,10 @@
       "pannelTitle": "Select an option or create one",
       "searchOption": "Search for an option"
     },
+    "menuName": "Grid"
+  },
+  "document": {
+    "menuName": "Doc",
     "date": {
       "timeHintTextInTwelveHour": "12:00 AM",
       "timeHintTextInTwentyFourHour": "12:00"

+ 6 - 0
frontend/app_flowy/assets/translations/it-IT.json

@@ -141,5 +141,11 @@
       "lightLabel": "Modalità Chiara",
       "darkLabel": "Modalità Scura"
     }
+  },
+  "grid": {
+    "menuName":"Griglia"
+  },
+  "document":{
+    "menuName":"Documento"
   }
 }

+ 199 - 0
frontend/app_flowy/assets/translations/ja-JP.json

@@ -0,0 +1,199 @@
+{
+  "appName": "AppFlowy",
+  "defaultUsername": "ユーザー",
+  "welcomeText": "Welcome to @:appName",
+  "githubStarText": "Star on GitHub",
+  "subscribeNewsletterText": "新着情報を受け取る",
+  "letsGoButtonText": "Let's Go",
+  "title": "タイトル",
+  "signUp": {
+    "buttonText": "新規登録",
+    "title": "@:appNameに新規登録",
+    "getStartedText": "はじめる",
+    "emptyPasswordError": "パスワードを空にはできません",
+    "repeatPasswordEmptyError": "パスワード(確認用)を空にはできません",
+    "unmatchedPasswordError": "パスワード(確認用)が一致しません",
+    "alreadyHaveAnAccount": "すでにアカウントを登録済ですか?",
+    "emailHint": "メールアドレス",
+    "passwordHint": "パスワード",
+    "repeatPasswordHint": "パスワード(確認用)"
+  },
+  "signIn": {
+    "loginTitle": "@:appName にログイン",
+    "loginButtonText": "ログイン",
+    "buttonText": "サインイン",
+    "forgotPassword": "パスワードをお忘れですか?",
+    "emailHint": "メールアドレス",
+    "passwordHint": "パスワード",
+    "dontHaveAnAccount": "まだアカウントをお持ちではないですか?",
+    "repeatPasswordEmptyError": "パスワード(確認用)を空にはできません",
+    "unmatchedPasswordError": "パスワード(確認用)が一致しません"
+  },
+  "workspace": {
+    "create": "ワークスペースを作成する",
+    "hint": "ワークスペース",
+    "notFoundError": "ワークスペースがみつかりません"
+  },
+  "shareAction": {
+    "buttonText": "共有する",
+    "workInProgress": "Coming soon",
+    "markdown": "Markdown",
+    "copyLink": "Copy Link"
+  },
+  "disclosureAction": {
+    "rename": "名前を変更",
+    "delete": "削除",
+    "duplicate": "コピーを作成"
+  },
+  "blankPageTitle": "空のページ",
+  "newPageText": "新しいページ",
+  "trash": {
+    "text": "ごみ箱",
+    "restoreAll": "全て復元",
+    "deleteAll": "全て削除",
+    "pageHeader": {
+      "fileName": "ファイル名",
+      "lastModified": "最終更新日時",
+      "created": "作成日時"
+    }
+  },
+  "deletePagePrompt": {
+    "text": "このページはごみ箱にあります",
+    "restore": "ページを元に戻す",
+    "deletePermanent": "削除する"
+  },
+  "dialogCreatePageNameHint": "ページ名",
+  "questionBubble": {
+    "whatsNew": "What's new?",
+    "help": "ヘルプとサポート",
+    "debug": {
+      "name": "デバッグ情報",
+      "success": "デバッグ情報をクリップボードにコピーしました!",
+      "fail": "デバッグ情報をクリップボードにコピーできませんでした"
+    }
+  },
+  "menuAppHeader": {
+    "addPageTooltip": "内部ページを追加",
+    "defaultNewPageName": "Untitled",
+    "renameDialog": "名前を変更"
+  },
+  "toolbar": {
+    "undo": "元に戻す",
+    "redo": "やり直し",
+    "bold": "太字",
+    "italic": "斜体",
+    "underline": "下線",
+    "strike": "取り消し線",
+    "numList": "番号付きリスト",
+    "bulletList": "箇条書き",
+    "checkList": "チェックボックス",
+    "inlineCode": "インラインコード",
+    "quote": "引用文",
+    "header": "見出し",
+    "highlight": "文字の背景色"
+  },
+  "tooltip": {
+    "lightMode": "ライトモードに切り替える",
+    "darkMode": "ダークモードに切り替える"
+  },
+  "contactsPage": {
+    "title": "連絡先",
+    "whatsHappening": "今週はどんなことがありましたか?",
+    "addContact": "連絡先を追加する",
+    "editContact": "連絡先を編集する"
+  },
+  "button": {
+    "OK": "OK",
+    "Cancel": "キャンセル",
+    "signIn": "サインイン",
+    "signOut": "サインアウト",
+    "complete": "完了",
+    "save": "保存"
+  },
+  "label": {
+    "welcome": "ようこそ!",
+    "firstName": "名",
+    "middleName": "ミドルネーム",
+    "lastName": "姓",
+    "stepX": "Step {X}"
+  },
+  "oAuth": {
+    "err": {
+      "failedTitle": "アカウントに接続できません",
+      "failedMsg": "サインインが完了したことをブラウザーで確認してください"
+    },
+    "google": {
+      "title": "GOOGLEでサインイン",
+      "instruction1": "GOOGLEでのサインインを有効にするためには、Webブラウザーを使ってこのアプリケーションを認証する必要があります。",
+      "instruction2": "アイコンをクリックするか、以下のテキストを選択して、このコードをクリップボードにコピーします。",
+      "instruction3": "以下のリンク先をブラウザーで開いて、次のコードを入力します。",
+      "instruction4": "登録が完了したら以下のボタンを押してください。"
+    }
+  },
+  "settings": {
+    "title": "設定",
+    "menu": {
+      "appearance": "外観",
+      "language": "言語",
+      "open": "設定"
+    },
+    "appearance": {
+      "lightLabel": "ライトモード",
+      "darkLabel": "ダークモード"
+    }
+  },
+  "grid": {
+    "settings": {
+      "filter": "絞り込み",
+      "sortBy": "並び替え",
+      "Properties": "プロパティ"
+    },
+    "field": {
+      "hide": "隠す",
+      "insertLeft": "左に挿入",
+      "insertRight": "右に挿入",
+      "duplicate": "コピーを作成",
+      "delete": "削除",
+      "textFieldName": "テキスト",
+      "checkboxFieldName": "チェックボックス",
+      "dateFieldName": "日付",
+      "numberFieldName": "数値",
+      "singleSelectFieldName": "単一選択",
+      "multiSelectFieldName": "複数選択",
+      "numberFormat": " 数値書式",
+      "dateFormat": " 日付書式",
+      "includeTime": " 時刻を含める",
+      "dateFormatFriendly": "月 日,年",
+      "dateFormatISO": "年-月-日",
+      "dateFormatLocal": "年/月/日",
+      "dateFormatUS": "年/月/日",
+      "timeFormat": " 時刻書式",
+      "timeFormatTwelveHour": "12 時間表記",
+      "timeFormatTwentyFourHour": "24 時間表記",
+      "addSelectOption": "選択候補追加",
+      "optionTitle": "選択候補",
+      "addOption": "選択候補追加",
+      "editProperty": "プロパティの編集"
+    },
+    "row": {
+      "duplicate": "コピーを作成",
+      "delete": "削除",
+      "textPlaceholder": "空白"
+    },
+    "selectOption": {
+      "purpleColor": "紫",
+      "pinkColor": "ピンク",
+      "lightPinkColor": "ライトピンク",
+      "orangeColor": "オレンジ",
+      "yellowColor": "黄色",
+      "limeColor": "ライム",
+      "greenColor": "緑",
+      "aquaColor": "水色",
+      "blueColor": "青",
+      "deleteTag": "選択候補を削除",
+      "colorPannelTitle": "色",
+      "pannelTitle": "選択候補を検索 または 作成する",
+      "searchOption": "選択候補を検索"
+    }
+  }
+}

+ 10 - 9
frontend/app_flowy/assets/translations/pt-BR.json

@@ -7,11 +7,11 @@
     "letsGoButtonText": "Vamos lá",
     "title": "Título",
     "signUp": {
-      "buttonText": "Inscreve-se",
-      "title": "Inscrever-se @:appName",
+      "buttonText": "Se inscreva",
+      "title": "Se inscreva no @:appName",
       "getStartedText": "Começar",
-      "emptyPasswordError": "Senha não pode ser em branco.",
-      "repeatPasswordEmptyError": "Confirmar a senha não pode ser em branco.",
+      "emptyPasswordError": "Senha não pode estar em branco.",
+      "repeatPasswordEmptyError": "Confirmar a senha não pode estar em branco.",
       "unmatchedPasswordError": "As senhas não conferem.",
       "alreadyHaveAnAccount": "Já possui uma conta?",
       "emailHint": "Email",
@@ -19,14 +19,14 @@
       "repeatPasswordHint": "Confirme a senha"
     },
     "signIn": {
-      "loginTitle": "Login to @:appName",
+      "loginTitle": "Entre no @:appName",
       "loginButtonText": "Login",
       "buttonText": "Entre",
       "forgotPassword": "Esqueceu a senha?",
       "emailHint": "Email",
       "passwordHint": "Senha",
       "dontHaveAnAccount": "Não possui uma conta?",
-      "repeatPasswordEmptyError": "Confirmar a senha não pode ser em branco.",
+      "repeatPasswordEmptyError": "Confirmar a senha não pode estar em branco.",
       "unmatchedPasswordError": "As senhas não conferem."
     },
     "workspace": {
@@ -67,7 +67,7 @@
       "whatsNew": "O que há de novo?",
       "help": "Ajuda & Suporte",
       "debug": {
-        "name": "Informação de debug",
+        "name": "Informação de depuração",
         "success": "Copiar informação de debug para o clipboard!",
         "fail": "Falha em copiar a informação de debug para o clipboard"
       }
@@ -104,7 +104,7 @@
     },
     "button": {
       "OK": "OK",
-      "Cancel": "Canelar",
+      "Cancel": "Cancelar",
       "signIn": "Entrar",
       "signOut": "Sair",
       "complete": "Completar",
@@ -143,4 +143,5 @@
       }
     }
   }
-  
+ 
+

+ 146 - 0
frontend/app_flowy/assets/translations/pt-PT.json

@@ -0,0 +1,146 @@
+{
+    "appName": "AppFlowy",
+    "defaultUsername": "Me",
+    "welcomeText": "Bem vindo ao @:appName",
+    "githubStarText": "Star on GitHub",
+    "subscribeNewsletterText": "Inscreve-te ao Newsletter",
+    "letsGoButtonText": "Bora",
+    "title": "Título",
+    "signUp": {
+      "buttonText": "Inscreve-te",
+      "title": "Inscreve-te ao @:appName",
+      "getStartedText": "Começar",
+      "emptyPasswordError": "A palavra-passe não pode estar em branco.",
+      "repeatPasswordEmptyError": "Confirmar a palavra-passe não pode estar em branco.",
+      "unmatchedPasswordError": "As palavras-passes não coincidem.",
+      "alreadyHaveAnAccount": "Já possuis uma conta?",
+      "emailHint": "Email",
+      "passwordHint": "Password",
+      "repeatPasswordHint": "Confirma a tua password"
+    },
+    "signIn": {
+      "loginTitle": "Entre no @:appName",
+      "loginButtonText": "Login",
+      "buttonText": "Entre",
+      "forgotPassword": "Esqueceste-te da tua palavra-passe?",
+      "emailHint": "Email",
+      "passwordHint": "Palavra-passe",
+      "dontHaveAnAccount": "Não possuis uma conta?",
+      "repeatPasswordEmptyError": "Confirmar a palavra-passe não pode estar em branco.",
+      "unmatchedPasswordError": "As palavras-passes não conferem."
+    },
+    "workspace": {
+      "create": "Cria um ambiente de trabalho",
+      "hint": "ambiente de trabalho",
+      "notFoundError": "Ambiente de trabalho não encontrada"
+    },
+    "shareAction": {
+      "buttonText": "Partilhar",
+      "workInProgress": "Em breve",
+      "markdown": "Markdown",
+      "copyLink": "Copiar o link"
+    },
+    "disclosureAction": {
+      "rename": "Renomear",
+      "delete": "Apagar",
+      "duplicate": "Duplicar"
+    },
+    "blankPageTitle": "Página em branco",
+    "newPageText": "Nova página",
+    "trash": {
+      "text": "Lixo",
+      "restoreAll": "Restaurar todos",
+      "deleteAll": "Apagar todos",
+      "pageHeader": {
+        "fileName": "Nome do ficheiro",
+        "lastModified": "Última modificação",
+        "created": "Criado"
+      }
+    },
+    "deletePagePrompt": {
+      "text": "Esta página está no lixo",
+      "restore": "Restaurar a página",
+      "deletePermanent": "Apagar permanentemente"
+    },
+    "dialogCreatePageNameHint": "Nome da página",
+    "questionBubble": {
+      "whatsNew": "O que há de novo?",
+      "help": "Ajuda & Suporte",
+      "debug": {
+        "name": "Informação de depuração",
+        "success": "Copiar informação de depuração para o clipboard!",
+        "fail": "Falha em copiar a informação de depuração para o clipboard"
+      }
+    },
+    "menuAppHeader": {
+      "addPageTooltip": "Adiciona uma nova página.",
+      "defaultNewPageName": "Sem título",
+      "renameDialog": "Renomear"
+    },
+    "toolbar": {
+      "undo": "Desfazer",
+      "redo": "Refazer",
+      "bold": "Negrito",
+      "italic": "Itálico",
+      "underline": "Sublinhado",
+      "strike": "Riscado",
+      "numList": "Lista numerada",
+      "bulletList": "Lista com marcadores",
+      "checkList": "Lista de verificação",
+      "inlineCode": "Embutir código",
+      "quote": "Citação em bloco",
+      "header": "Cabeçalho",
+      "highlight": "Realçar"
+    },
+    "tooltip": {
+      "lightMode": "Mudar para o modo Claro.",
+      "darkMode": "Mudar para o modo Escuro."
+    },
+    "contactsPage": {
+      "title": "Conctatos",
+      "whatsHappening": "O que está a acontecer nesta semana?",
+      "addContact": "Adicionar um conctato",
+      "editContact": "Editar um conctato"
+    },
+    "button": {
+      "OK": "OK",
+      "Cancel": "Cancelar",
+      "signIn": "Entrar",
+      "signOut": "Sair",
+      "complete": "Completar",
+      "save": "Guardar"
+    },
+    "label": {
+      "welcome": "Bem vindo!",
+      "firstName": "Nome",
+      "middleName": "Nome do Meio",
+      "lastName": "Apelido",
+      "stepX": "Passo {X}"
+    },
+    "oAuth": {
+      "err": {
+        "failedTitle": "Erro ao conectar à sua conta.",
+        "failedMsg": "Verifica se concluiste o processo de login no teu navegador."
+      },
+      "google": {
+        "title": "GOOGLE SIGN-IN",
+        "instruction1": "Para importar os teus Conctatos do Google, tens de autorizar esta aplicação usando o teu navegador web.",
+        "instruction2": "Copia este código para a tua área de transferências clicando no ícone ou selecionando o texto:",
+        "instruction3": "Navega até o link a seguir no seu navegador e digite o código acima:",
+        "instruction4": "Clica no botão abaixo ao concluir a inscrição:"
+      }
+    },
+    "settings": {
+      "title": "Definições",
+      "menu": {
+        "appearance": "Aparência",
+        "language": "Idioma",
+        "open": "Abrir as Definições"
+      },
+      "appearance": {
+        "lightLabel": "Modo Claro",
+        "darkLabel": "Modo Escuro"
+      }
+    }
+  }
+

+ 62 - 0
frontend/app_flowy/assets/translations/ru-RU.json

@@ -141,6 +141,68 @@
         "lightLabel": "Светлая тема",
         "darkLabel": "Тёмная тема"
       }
+    },
+    "grid": {
+      "settings": {
+        "filter": "Фильтр",
+        "sortBy": "Сортировать",
+        "Properties": "Свойства"
+      },
+      "field": {
+        "hide": "Скрыть",
+        "insertLeft": "Вставить слева",
+        "insertRight": "Вставить справа",
+        "duplicate": "Дублировать",
+        "delete": "Удалить",
+        "textFieldName": "Текст",
+        "checkboxFieldName": "Checkbox",
+        "dateFieldName": "Дата",
+        "numberFieldName": "Число",
+        "singleSelectFieldName": "Выбор",
+        "multiSelectFieldName": "Выбор многих",
+        "urlFieldName": "URL",
+        "numberFormat": " Формат числа",
+        "dateFormat": " Формат даты",
+        "includeTime": " Время",
+        "dateFormatFriendly": "День Месяц, Год",
+        "dateFormatISO": "Год-Месяц-День",
+        "dateFormatLocal": "Год/Месяц/День",
+        "dateFormatUS": "Год/Месяц/День",
+        "timeFormat": " Форматировать время",
+        "invalidTimeFormat": "Неверный формат",
+        "timeFormatTwelveHour": "12 часов",
+        "timeFormatTwentyFourHour": "24 часа",
+        "addSelectOption": "Добавить вариант",
+        "optionTitle": "Варианты",
+        "addOption": "Добавить",
+        "editProperty": "Редактировать свойство"
+      },
+      "row": {
+        "duplicate": "Дублировать",
+        "delete": "Удалить",
+        "textPlaceholder": "Пусто",
+        "copyProperty": "Свойство скопировано"
+      },
+      "selectOption": {
+        "create": "Создать",
+        "purpleColor": "Фиолетовый",
+        "pinkColor": "Розовый",
+        "lightPinkColor": "Светло-розовый",
+        "orangeColor": "Оранжевый",
+        "yellowColor": "Желтый",
+        "limeColor": "Ярко-зелёный",
+        "greenColor": "Зелёный",
+        "aquaColor": "Морской волны",
+        "blueColor": "Синий",
+        "deleteTag": "Удалить вариант",
+        "colorPannelTitle": "Цвета",
+        "pannelTitle": "Выберите или создайте вариант",
+        "searchOption": "Поиск"
+      },
+      "date": {
+        "timeHintTextInTwelveHour": "12:00 AM",
+        "timeHintTextInTwentyFourHour": "12:00"
+      }
     }
   }
   

+ 39 - 0
frontend/app_flowy/lib/core/folder_notification.dart

@@ -0,0 +1,39 @@
+import 'dart:async';
+import 'dart:typed_data';
+import 'package:flowy_sdk/protobuf/dart-notify/protobuf.dart';
+import 'package:dartz/dartz.dart';
+import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder/dart_notification.pb.dart';
+import 'package:flowy_sdk/rust_stream.dart';
+
+import 'notification_helper.dart';
+
+// Folder
+typedef FolderNotificationCallback = void Function(FolderNotification, Either<Uint8List, FlowyError>);
+
+class FolderNotificationParser extends NotificationParser<FolderNotification, FlowyError> {
+  FolderNotificationParser({String? id, required FolderNotificationCallback callback})
+      : super(
+          id: id,
+          callback: callback,
+          tyParser: (ty) => FolderNotification.valueOf(ty),
+          errorParser: (bytes) => FlowyError.fromBuffer(bytes),
+        );
+}
+
+typedef FolderNotificationHandler = Function(FolderNotification ty, Either<Uint8List, FlowyError> result);
+
+class FolderNotificationListener {
+  StreamSubscription<SubscribeObject>? _subscription;
+  FolderNotificationParser? _parser;
+
+  FolderNotificationListener({required String objectId, required FolderNotificationHandler handler})
+      : _parser = FolderNotificationParser(id: objectId, callback: handler) {
+    _subscription = RustStreamReceiver.listen((observable) => _parser?.parse(observable));
+  }
+
+  Future<void> stop() async {
+    _parser = null;
+    await _subscription?.cancel();
+  }
+}

+ 39 - 0
frontend/app_flowy/lib/core/grid_notification.dart

@@ -0,0 +1,39 @@
+import 'dart:async';
+import 'dart:typed_data';
+import 'package:flowy_sdk/protobuf/dart-notify/protobuf.dart';
+import 'package:dartz/dartz.dart';
+import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/dart_notification.pb.dart';
+import 'package:flowy_sdk/rust_stream.dart';
+
+import 'notification_helper.dart';
+
+// GridPB
+typedef GridNotificationCallback = void Function(GridNotification, Either<Uint8List, FlowyError>);
+
+class GridNotificationParser extends NotificationParser<GridNotification, FlowyError> {
+  GridNotificationParser({String? id, required GridNotificationCallback callback})
+      : super(
+          id: id,
+          callback: callback,
+          tyParser: (ty) => GridNotification.valueOf(ty),
+          errorParser: (bytes) => FlowyError.fromBuffer(bytes),
+        );
+}
+
+typedef GridNotificationHandler = Function(GridNotification ty, Either<Uint8List, FlowyError> result);
+
+class GridNotificationListener {
+  StreamSubscription<SubscribeObject>? _subscription;
+  GridNotificationParser? _parser;
+
+  GridNotificationListener({required String objectId, required GridNotificationHandler handler})
+      : _parser = GridNotificationParser(id: objectId, callback: handler) {
+    _subscription = RustStreamReceiver.listen((observable) => _parser?.parse(observable));
+  }
+
+  Future<void> stop() async {
+    _parser = null;
+    await _subscription?.cancel();
+  }
+}

+ 0 - 62
frontend/app_flowy/lib/core/notification_helper.dart

@@ -1,68 +1,6 @@
-import 'dart:async';
 import 'dart:typed_data';
 import 'package:flowy_sdk/protobuf/dart-notify/protobuf.dart';
-import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart';
 import 'package:dartz/dartz.dart';
-import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-folder/dart_notification.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid/dart_notification.pb.dart';
-import 'package:flowy_sdk/rust_stream.dart';
-
-// User
-typedef UserNotificationCallback = void Function(UserNotification, Either<Uint8List, FlowyError>);
-
-class UserNotificationParser extends NotificationParser<UserNotification, FlowyError> {
-  UserNotificationParser({required String id, required UserNotificationCallback callback})
-      : super(
-          id: id,
-          callback: callback,
-          tyParser: (ty) => UserNotification.valueOf(ty),
-          errorParser: (bytes) => FlowyError.fromBuffer(bytes),
-        );
-}
-
-// Folder
-typedef FolderNotificationCallback = void Function(FolderNotification, Either<Uint8List, FlowyError>);
-
-class FolderNotificationParser extends NotificationParser<FolderNotification, FlowyError> {
-  FolderNotificationParser({String? id, required FolderNotificationCallback callback})
-      : super(
-          id: id,
-          callback: callback,
-          tyParser: (ty) => FolderNotification.valueOf(ty),
-          errorParser: (bytes) => FlowyError.fromBuffer(bytes),
-        );
-}
-
-// Grid
-typedef GridNotificationCallback = void Function(GridNotification, Either<Uint8List, FlowyError>);
-
-class GridNotificationParser extends NotificationParser<GridNotification, FlowyError> {
-  GridNotificationParser({String? id, required GridNotificationCallback callback})
-      : super(
-          id: id,
-          callback: callback,
-          tyParser: (ty) => GridNotification.valueOf(ty),
-          errorParser: (bytes) => FlowyError.fromBuffer(bytes),
-        );
-}
-
-typedef GridNotificationHandler = Function(GridNotification ty, Either<Uint8List, FlowyError> result);
-
-class GridNotificationListener {
-  StreamSubscription<SubscribeObject>? _subscription;
-  GridNotificationParser? _parser;
-
-  GridNotificationListener({required String objectId, required GridNotificationHandler handler})
-      : _parser = GridNotificationParser(id: objectId, callback: handler) {
-    _subscription = RustStreamReceiver.listen((observable) => _parser?.parse(observable));
-  }
-
-  Future<void> stop() async {
-    _parser = null;
-    await _subscription?.cancel();
-  }
-}
 
 class NotificationParser<T, E> {
   String? id;

+ 39 - 0
frontend/app_flowy/lib/core/user_notification.dart

@@ -0,0 +1,39 @@
+import 'dart:async';
+import 'dart:typed_data';
+import 'package:flowy_sdk/protobuf/dart-notify/protobuf.dart';
+import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart';
+import 'package:dartz/dartz.dart';
+import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
+import 'package:flowy_sdk/rust_stream.dart';
+
+import 'notification_helper.dart';
+
+// User
+typedef UserNotificationCallback = void Function(UserNotification, Either<Uint8List, FlowyError>);
+
+class UserNotificationParser extends NotificationParser<UserNotification, FlowyError> {
+  UserNotificationParser({required String id, required UserNotificationCallback callback})
+      : super(
+          id: id,
+          callback: callback,
+          tyParser: (ty) => UserNotification.valueOf(ty),
+          errorParser: (bytes) => FlowyError.fromBuffer(bytes),
+        );
+}
+
+typedef UserNotificationHandler = Function(UserNotification ty, Either<Uint8List, FlowyError> result);
+
+class UserNotificationListener {
+  StreamSubscription<SubscribeObject>? _subscription;
+  UserNotificationParser? _parser;
+
+  UserNotificationListener({required String objectId, required UserNotificationHandler handler})
+      : _parser = UserNotificationParser(id: objectId, callback: handler) {
+    _subscription = RustStreamReceiver.listen((observable) => _parser?.parse(observable));
+  }
+
+  Future<void> stop() async {
+    _parser = null;
+    await _subscription?.cancel();
+  }
+}

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

@@ -4,7 +4,7 @@ import 'package:app_flowy/plugin/plugin.dart';
 import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
 import 'package:flowy_infra/notifier.dart';
-import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
 import 'package:flutter/widgets.dart';
 
 export "./src/sandbox.dart";
@@ -14,6 +14,7 @@ enum DefaultPlugin {
   blank,
   trash,
   grid,
+  board,
 }
 
 extension FlowyDefaultPluginExt on DefaultPlugin {
@@ -27,6 +28,8 @@ extension FlowyDefaultPluginExt on DefaultPlugin {
         return 2;
       case DefaultPlugin.grid:
         return 3;
+      case DefaultPlugin.board:
+        return 4;
     }
   }
 }

+ 27 - 31
frontend/app_flowy/lib/startup/deps_resolver.dart

@@ -13,9 +13,9 @@ import 'package:app_flowy/user/application/prelude.dart';
 import 'package:app_flowy/user/presentation/router.dart';
 import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
 import 'package:app_flowy/workspace/presentation/home/menu/menu.dart';
-import 'package:flowy_sdk/protobuf/flowy-folder-data-model/app.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-user-data-model/user_profile.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder/app.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-user/user_profile.pb.dart';
 import 'package:fluttertoast/fluttertoast.dart';
 import 'package:get_it/get_it.dart';
 
@@ -51,37 +51,37 @@ void _resolveHomeDeps(GetIt getIt) {
 
   getIt.registerSingleton(MenuSharedState());
 
-  getIt.registerFactoryParam<UserListener, UserProfile, void>(
-    (user, _) => UserListener(user: user),
+  getIt.registerFactoryParam<UserListener, UserProfilePB, void>(
+    (user, _) => UserListener(userProfile: user),
   );
 
   //
   getIt.registerLazySingleton<HomeStackManager>(() => HomeStackManager());
 
-  getIt.registerFactoryParam<WelcomeBloc, UserProfile, void>(
+  getIt.registerFactoryParam<WelcomeBloc, UserProfilePB, void>(
     (user, _) => WelcomeBloc(
-      userService: UserService(),
-      userListener: getIt<UserListener>(param1: user),
+      userService: UserService(userId: user.id),
+      userWorkspaceListener: UserWorkspaceListener(userProfile: user),
     ),
   );
 
   // share
   getIt.registerLazySingleton<ShareService>(() => ShareService());
-  getIt.registerFactoryParam<DocShareBloc, View, void>(
+  getIt.registerFactoryParam<DocShareBloc, ViewPB, void>(
       (view, _) => DocShareBloc(view: view, service: getIt<ShareService>()));
 }
 
 void _resolveFolderDeps(GetIt getIt) {
   //workspace
-  getIt.registerFactoryParam<WorkspaceListener, UserProfile, String>((user, workspaceId) =>
-      WorkspaceListener(service: WorkspaceListenerService(user: user, workspaceId: workspaceId)));
+  getIt.registerFactoryParam<WorkspaceListener, UserProfilePB, String>(
+      (user, workspaceId) => WorkspaceListener(user: user, workspaceId: workspaceId));
 
-  // View
-  getIt.registerFactoryParam<ViewListener, View, void>(
+  // ViewPB
+  getIt.registerFactoryParam<ViewListener, ViewPB, void>(
     (view, _) => ViewListener(view: view),
   );
 
-  getIt.registerFactoryParam<ViewBloc, View, void>(
+  getIt.registerFactoryParam<ViewBloc, ViewPB, void>(
     (view, _) => ViewBloc(
       view: view,
       service: ViewService(),
@@ -90,23 +90,19 @@ void _resolveFolderDeps(GetIt getIt) {
   );
 
   //Menu
-  getIt.registerFactoryParam<MenuBloc, UserProfile, String>(
+  getIt.registerFactoryParam<MenuBloc, UserProfilePB, String>(
     (user, workspaceId) => MenuBloc(
       workspaceId: workspaceId,
       listener: getIt<WorkspaceListener>(param1: user, param2: workspaceId),
     ),
   );
 
-  getIt.registerFactoryParam<MenuUserBloc, UserProfile, void>(
-    (user, _) => MenuUserBloc(
-      user,
-      UserService(),
-      getIt<UserListener>(param1: user),
-    ),
+  getIt.registerFactoryParam<MenuUserBloc, UserProfilePB, void>(
+    (user, _) => MenuUserBloc(user),
   );
 
-  // App
-  getIt.registerFactoryParam<AppBloc, App, void>(
+  // AppPB
+  getIt.registerFactoryParam<AppBloc, AppPB, void>(
     (app, _) => AppBloc(
       app: app,
       appService: AppService(appId: app.id),
@@ -127,7 +123,7 @@ void _resolveFolderDeps(GetIt getIt) {
 
 void _resolveDocDeps(GetIt getIt) {
 // Doc
-  getIt.registerFactoryParam<DocumentBloc, View, void>(
+  getIt.registerFactoryParam<DocumentBloc, ViewPB, void>(
     (view, _) => DocumentBloc(
       view: view,
       service: DocumentService(),
@@ -138,8 +134,8 @@ void _resolveDocDeps(GetIt getIt) {
 }
 
 void _resolveGridDeps(GetIt getIt) {
-  // Grid
-  getIt.registerFactoryParam<GridBloc, View, void>(
+  // GridPB
+  getIt.registerFactoryParam<GridBloc, ViewPB, void>(
     (view, _) => GridBloc(view: view),
   );
 
@@ -157,31 +153,31 @@ void _resolveGridDeps(GetIt getIt) {
     ),
   );
 
-  getIt.registerFactoryParam<TextCellBloc, GridCellContext, void>(
+  getIt.registerFactoryParam<TextCellBloc, GridCellController, void>(
     (context, _) => TextCellBloc(
       cellContext: context,
     ),
   );
 
-  getIt.registerFactoryParam<SelectOptionCellBloc, GridSelectOptionCellContext, void>(
+  getIt.registerFactoryParam<SelectOptionCellBloc, GridSelectOptionCellController, void>(
     (context, _) => SelectOptionCellBloc(
       cellContext: context,
     ),
   );
 
-  getIt.registerFactoryParam<NumberCellBloc, GridCellContext, void>(
+  getIt.registerFactoryParam<NumberCellBloc, GridCellController, void>(
     (context, _) => NumberCellBloc(
       cellContext: context,
     ),
   );
 
-  getIt.registerFactoryParam<DateCellBloc, GridDateCellContext, void>(
+  getIt.registerFactoryParam<DateCellBloc, GridDateCellController, void>(
     (context, _) => DateCellBloc(
       cellContext: context,
     ),
   );
 
-  getIt.registerFactoryParam<CheckboxCellBloc, GridCellContext, void>(
+  getIt.registerFactoryParam<CheckboxCellBloc, GridCellController, void>(
     (cellData, _) => CheckboxCellBloc(
       service: CellService(),
       cellContext: cellData,

+ 1 - 0
frontend/app_flowy/lib/startup/tasks/app_widget.dart

@@ -37,6 +37,7 @@ class InitAppWidgetTask extends LaunchTask {
               Locale('fr', 'CA'),
               Locale('hu', 'HU'),
               Locale('it', 'IT'),
+              Locale('ja', 'JP'),
               Locale('pt', 'BR'),
               Locale('ru', 'RU'),
               Locale('tr', 'TR'),

+ 2 - 0
frontend/app_flowy/lib/startup/tasks/load_plugin.dart

@@ -1,6 +1,7 @@
 import 'package:app_flowy/plugin/plugin.dart';
 import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/workspace/presentation/plugins/blank/blank.dart';
+import 'package:app_flowy/workspace/presentation/plugins/board/board.dart';
 import 'package:app_flowy/workspace/presentation/plugins/doc/document.dart';
 import 'package:app_flowy/workspace/presentation/plugins/grid/grid.dart';
 import 'package:app_flowy/workspace/presentation/plugins/trash/trash.dart';
@@ -15,5 +16,6 @@ class PluginLoadTask extends LaunchTask {
     registerPlugin(builder: TrashPluginBuilder(), config: TrashPluginConfig());
     registerPlugin(builder: DocumentPluginBuilder());
     registerPlugin(builder: GridPluginBuilder(), config: GridPluginConfig());
+    registerPlugin(builder: BoardPluginBuilder(), config: BoardPluginConfig());
   }
 }

+ 5 - 5
frontend/app_flowy/lib/user/application/auth_service.dart

@@ -1,21 +1,21 @@
 import 'package:dartz/dartz.dart';
 import 'package:flowy_sdk/dispatch/dispatch.dart';
-import 'package:flowy_sdk/protobuf/flowy-user-data-model/protobuf.dart' show SignInPayload, SignUpPayload, UserProfile;
+import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart' show SignInPayloadPB, SignUpPayloadPB, UserProfilePB;
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 
 class AuthService {
-  Future<Either<UserProfile, FlowyError>> signIn({required String? email, required String? password}) {
+  Future<Either<UserProfilePB, FlowyError>> signIn({required String? email, required String? password}) {
     //
-    final request = SignInPayload.create()
+    final request = SignInPayloadPB.create()
       ..email = email ?? ''
       ..password = password ?? '';
 
     return UserEventSignIn(request).send();
   }
 
-  Future<Either<UserProfile, FlowyError>> signUp(
+  Future<Either<UserProfilePB, FlowyError>> signUp(
       {required String? name, required String? password, required String? email}) {
-    final request = SignUpPayload.create()
+    final request = SignUpPayloadPB.create()
       ..email = email ?? ''
       ..name = name ?? ''
       ..password = password ?? '';

+ 3 - 2
frontend/app_flowy/lib/user/application/sign_in_bloc.dart

@@ -1,7 +1,8 @@
 import 'package:app_flowy/user/application/auth_service.dart';
 import 'package:dartz/dartz.dart';
+import 'package:flowy_sdk/protobuf/flowy-error-code/code.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-user-data-model/protobuf.dart' show UserProfile, ErrorCode;
+import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart' show UserProfilePB;
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
@@ -68,7 +69,7 @@ class SignInState with _$SignInState {
     required bool isSubmitting,
     required Option<String> passwordError,
     required Option<String> emailError,
-    required Option<Either<UserProfile, FlowyError>> successOrFail,
+    required Option<Either<UserProfilePB, FlowyError>> successOrFail,
   }) = _SignInState;
 
   factory SignInState.initial() => SignInState(

+ 3 - 2
frontend/app_flowy/lib/user/application/sign_up_bloc.dart

@@ -1,7 +1,8 @@
 import 'package:app_flowy/user/application/auth_service.dart';
 import 'package:dartz/dartz.dart';
 import 'package:easy_localization/easy_localization.dart';
-import 'package:flowy_sdk/protobuf/flowy-user-data-model/protobuf.dart' show UserProfile, ErrorCode;
+import 'package:flowy_sdk/protobuf/flowy-error-code/code.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart' show UserProfilePB;
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
@@ -119,7 +120,7 @@ class SignUpState with _$SignUpState {
     required Option<String> passwordError,
     required Option<String> repeatPasswordError,
     required Option<String> emailError,
-    required Option<Either<UserProfile, FlowyError>> successOrFail,
+    required Option<Either<UserProfilePB, FlowyError>> successOrFail,
   }) = _SignUpState;
 
   factory SignUpState.initial() => SignUpState(

+ 94 - 61
frontend/app_flowy/lib/user/application/user_listener.dart

@@ -1,121 +1,154 @@
 import 'dart:async';
+import 'package:app_flowy/core/folder_notification.dart';
+import 'package:app_flowy/core/user_notification.dart';
 import 'package:dartz/dartz.dart';
-import 'package:flowy_sdk/protobuf/flowy-folder-data-model/workspace.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-error-code/code.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder/workspace.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 import 'dart:typed_data';
-import 'package:app_flowy/core/notification_helper.dart';
 import 'package:flowy_infra/notifier.dart';
 import 'package:flowy_sdk/protobuf/dart-notify/protobuf.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder/dart_notification.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-user-data-model/errors.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-user-data-model/user_profile.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-user/user_profile.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-user/dart_notification.pb.dart' as user;
 import 'package:flowy_sdk/rust_stream.dart';
 
-typedef UserProfileNotifyValue = Either<UserProfile, FlowyError>;
+typedef UserProfileNotifyValue = Either<UserProfilePB, FlowyError>;
 typedef AuthNotifyValue = Either<Unit, FlowyError>;
-typedef WorkspaceListNotifyValue = Either<List<Workspace>, FlowyError>;
-typedef WorkspaceSettingNotifyValue = Either<CurrentWorkspaceSetting, FlowyError>;
 
 class UserListener {
   StreamSubscription<SubscribeObject>? _subscription;
-  final _profileNotifier = PublishNotifier<UserProfileNotifyValue>();
-  final _authNotifier = PublishNotifier<AuthNotifyValue>();
-  final _workspaceListNotifier = PublishNotifier<WorkspaceListNotifyValue>();
-  final _workSettingNotifier = PublishNotifier<WorkspaceSettingNotifyValue>();
+  PublishNotifier<AuthNotifyValue>? _authNotifier = PublishNotifier();
+  PublishNotifier<UserProfileNotifyValue>? _profileNotifier = PublishNotifier();
 
-  FolderNotificationParser? _workspaceParser;
   UserNotificationParser? _userParser;
-  final UserProfile _user;
+  final UserProfilePB _userProfile;
   UserListener({
-    required UserProfile user,
-  }) : _user = user;
+    required UserProfilePB userProfile,
+  }) : _userProfile = userProfile;
 
   void start({
     void Function(AuthNotifyValue)? onAuthChanged,
     void Function(UserProfileNotifyValue)? onProfileUpdated,
-    void Function(WorkspaceListNotifyValue)? onWorkspaceListUpdated,
-    void Function(WorkspaceSettingNotifyValue)? onWorkspaceSettingUpdated,
   }) {
-    if (onAuthChanged != null) {
-      _authNotifier.addListener(() {
-        onAuthChanged(_authNotifier.currentValue!);
-      });
-    }
-
     if (onProfileUpdated != null) {
-      _profileNotifier.addListener(() {
-        onProfileUpdated(_profileNotifier.currentValue!);
-      });
+      _profileNotifier?.addPublishListener(onProfileUpdated);
     }
 
-    if (onWorkspaceListUpdated != null) {
-      _workspaceListNotifier.addListener(() {
-        onWorkspaceListUpdated(_workspaceListNotifier.currentValue!);
-      });
-    }
-
-    if (onWorkspaceSettingUpdated != null) {
-      _workSettingNotifier.addListener(() {
-        onWorkspaceSettingUpdated(_workSettingNotifier.currentValue!);
-      });
+    if (onAuthChanged != null) {
+      _authNotifier?.addPublishListener(onAuthChanged);
     }
 
-    _workspaceParser = FolderNotificationParser(id: _user.token, callback: _notificationCallback);
-    _userParser = UserNotificationParser(id: _user.token, callback: _userNotificationCallback);
+    _userParser = UserNotificationParser(id: _userProfile.token, callback: _userNotificationCallback);
     _subscription = RustStreamReceiver.listen((observable) {
-      _workspaceParser?.parse(observable);
       _userParser?.parse(observable);
     });
   }
 
   Future<void> stop() async {
-    _workspaceParser = null;
     _userParser = null;
     await _subscription?.cancel();
-    _profileNotifier.dispose();
-    _authNotifier.dispose();
-    _workspaceListNotifier.dispose();
+    _profileNotifier?.dispose();
+    _profileNotifier = null;
+
+    _authNotifier?.dispose();
+    _authNotifier = null;
+  }
+
+  void _userNotificationCallback(user.UserNotification ty, Either<Uint8List, FlowyError> result) {
+    switch (ty) {
+      case user.UserNotification.UserUnauthorized:
+        result.fold(
+          (_) {},
+          (error) => _authNotifier?.value = right(error),
+        );
+        break;
+      case user.UserNotification.UserProfileUpdated:
+        result.fold(
+          (payload) => _profileNotifier?.value = left(UserProfilePB.fromBuffer(payload)),
+          (error) => _profileNotifier?.value = right(error),
+        );
+        break;
+      default:
+        break;
+    }
+  }
+}
+
+typedef WorkspaceListNotifyValue = Either<List<WorkspacePB>, FlowyError>;
+typedef WorkspaceSettingNotifyValue = Either<CurrentWorkspaceSettingPB, FlowyError>;
+
+class UserWorkspaceListener {
+  PublishNotifier<AuthNotifyValue>? _authNotifier = PublishNotifier();
+  PublishNotifier<WorkspaceListNotifyValue>? _workspacesChangedNotifier = PublishNotifier();
+  PublishNotifier<WorkspaceSettingNotifyValue>? _settingChangedNotifier = PublishNotifier();
+
+  FolderNotificationListener? _listener;
+  final UserProfilePB _userProfile;
+
+  UserWorkspaceListener({
+    required UserProfilePB userProfile,
+  }) : _userProfile = userProfile;
+
+  void start({
+    void Function(AuthNotifyValue)? onAuthChanged,
+    void Function(WorkspaceListNotifyValue)? onWorkspacesUpdated,
+    void Function(WorkspaceSettingNotifyValue)? onSettingUpdated,
+  }) {
+    if (onAuthChanged != null) {
+      _authNotifier?.addPublishListener(onAuthChanged);
+    }
+
+    if (onWorkspacesUpdated != null) {
+      _workspacesChangedNotifier?.addPublishListener(onWorkspacesUpdated);
+    }
+
+    if (onSettingUpdated != null) {
+      _settingChangedNotifier?.addPublishListener(onSettingUpdated);
+    }
+
+    _listener = FolderNotificationListener(
+      objectId: _userProfile.token,
+      handler: _handleObservableType,
+    );
   }
 
-  void _notificationCallback(FolderNotification ty, Either<Uint8List, FlowyError> result) {
+  void _handleObservableType(FolderNotification ty, Either<Uint8List, FlowyError> result) {
     switch (ty) {
       case FolderNotification.UserCreateWorkspace:
       case FolderNotification.UserDeleteWorkspace:
       case FolderNotification.WorkspaceListUpdated:
         result.fold(
-          (payload) => _workspaceListNotifier.value = left(RepeatedWorkspace.fromBuffer(payload).items),
-          (error) => _workspaceListNotifier.value = right(error),
+          (payload) => _workspacesChangedNotifier?.value = left(RepeatedWorkspacePB.fromBuffer(payload).items),
+          (error) => _workspacesChangedNotifier?.value = right(error),
         );
         break;
       case FolderNotification.WorkspaceSetting:
         result.fold(
-          (payload) => _workSettingNotifier.value = left(CurrentWorkspaceSetting.fromBuffer(payload)),
-          (error) => _workSettingNotifier.value = right(error),
+          (payload) => _settingChangedNotifier?.value = left(CurrentWorkspaceSettingPB.fromBuffer(payload)),
+          (error) => _settingChangedNotifier?.value = right(error),
         );
         break;
       case FolderNotification.UserUnauthorized:
         result.fold(
           (_) {},
-          (error) => _authNotifier.value = right(FlowyError.create()..code = ErrorCode.UserUnauthorized.value),
+          (error) => _authNotifier?.value = right(FlowyError.create()..code = ErrorCode.UserUnauthorized.value),
         );
         break;
-
       default:
         break;
     }
   }
 
-  void _userNotificationCallback(user.UserNotification ty, Either<Uint8List, FlowyError> result) {
-    switch (ty) {
-      case user.UserNotification.UserUnauthorized:
-        result.fold(
-          (payload) => _profileNotifier.value = left(UserProfile.fromBuffer(payload)),
-          (error) => _profileNotifier.value = right(error),
-        );
-        break;
-      default:
-        break;
-    }
+  Future<void> stop() async {
+    await _listener?.stop();
+    _workspacesChangedNotifier?.dispose();
+    _workspacesChangedNotifier = null;
+
+    _settingChangedNotifier?.dispose();
+    _settingChangedNotifier = null;
+
+    _authNotifier?.dispose();
+    _authNotifier = null;
   }
 }

+ 36 - 9
frontend/app_flowy/lib/user/application/user_service.dart

@@ -1,15 +1,42 @@
 import 'dart:async';
+
 import 'package:dartz/dartz.dart';
 import 'package:flowy_sdk/dispatch/dispatch.dart';
-import 'package:flowy_sdk/protobuf/flowy-folder-data-model/workspace.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-user-data-model/user_profile.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder/workspace.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-user/user_profile.pb.dart';
 
 class UserService {
-  Future<Either<UserProfile, FlowyError>> fetchUserProfile({required String userId}) {
+  final String userId;
+  UserService({
+    required this.userId,
+  });
+  Future<Either<UserProfilePB, FlowyError>> getUserProfile({required String userId}) {
     return UserEventGetUserProfile().send();
   }
 
+  Future<Either<Unit, FlowyError>> updateUserProfile({
+    String? name,
+    String? password,
+    String? email,
+  }) {
+    var payload = UpdateUserProfilePayloadPB.create()..id = userId;
+
+    if (name != null) {
+      payload.name = name;
+    }
+
+    if (password != null) {
+      payload.password = password;
+    }
+
+    if (email != null) {
+      payload.email = email;
+    }
+
+    return UserEventUpdateUserProfile(payload).send();
+  }
+
   Future<Either<Unit, FlowyError>> deleteWorkspace({required String workspaceId}) {
     throw UnimplementedError();
   }
@@ -22,8 +49,8 @@ class UserService {
     return UserEventInitUser().send();
   }
 
-  Future<Either<List<Workspace>, FlowyError>> getWorkspaces() {
-    final request = WorkspaceId.create();
+  Future<Either<List<WorkspacePB>, FlowyError>> getWorkspaces() {
+    final request = WorkspaceIdPB.create();
 
     return FolderEventReadWorkspaces(request).send().then((result) {
       return result.fold(
@@ -33,8 +60,8 @@ class UserService {
     });
   }
 
-  Future<Either<Workspace, FlowyError>> openWorkspace(String workspaceId) {
-    final request = WorkspaceId.create()..value = workspaceId;
+  Future<Either<WorkspacePB, FlowyError>> openWorkspace(String workspaceId) {
+    final request = WorkspaceIdPB.create()..value = workspaceId;
     return FolderEventOpenWorkspace(request).send().then((result) {
       return result.fold(
         (workspace) => left(workspace),
@@ -43,8 +70,8 @@ class UserService {
     });
   }
 
-  Future<Either<Workspace, FlowyError>> createWorkspace(String name, String desc) {
-    final request = CreateWorkspacePayload.create()
+  Future<Either<WorkspacePB, FlowyError>> createWorkspace(String name, String desc) {
+    final request = CreateWorkspacePayloadPB.create()
       ..name = name
       ..desc = desc;
     return FolderEventCreateWorkspace(request).send().then((result) {

+ 4 - 4
frontend/app_flowy/lib/user/application/user_settings_service.dart

@@ -2,14 +2,14 @@ import 'package:dartz/dartz.dart';
 import 'package:flowy_sdk/dispatch/dispatch.dart';
 import 'package:flowy_sdk/flowy_sdk.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-user-data-model/user_setting.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-user/user_setting.pb.dart';
 
 class UserSettingsService {
-  Future<AppearanceSettings> getAppearanceSettings() async {
+  Future<AppearanceSettingsPB> getAppearanceSettings() async {
     final result = await UserEventGetAppearanceSetting().send();
 
     return result.fold(
-      (AppearanceSettings setting) {
+      (AppearanceSettingsPB setting) {
         return setting;
       },
       (error) {
@@ -18,7 +18,7 @@ class UserSettingsService {
     );
   }
 
-  Future<Either<Unit, FlowyError>> setAppearanceSettings(AppearanceSettings settings) {
+  Future<Either<Unit, FlowyError>> setAppearanceSettings(AppearanceSettingsPB settings) {
     return UserEventSetAppearanceSetting(settings).send();
   }
 }

+ 2 - 2
frontend/app_flowy/lib/user/domain/auth_state.dart

@@ -1,11 +1,11 @@
-import 'package:flowy_sdk/protobuf/flowy-user-data-model/protobuf.dart' show UserProfile;
+import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart' show UserProfilePB;
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 part 'auth_state.freezed.dart';
 
 @freezed
 class AuthState with _$AuthState {
-  const factory AuthState.authenticated(UserProfile userProfile) = Authenticated;
+  const factory AuthState.authenticated(UserProfilePB userProfile) = Authenticated;
   const factory AuthState.unauthenticated(FlowyError error) = Unauthenticated;
   const factory AuthState.initial() = _Initial;
 }

+ 6 - 6
frontend/app_flowy/lib/user/presentation/router.dart

@@ -7,8 +7,8 @@ import 'package:app_flowy/user/presentation/welcome_screen.dart';
 import 'package:app_flowy/workspace/presentation/home/home_screen.dart';
 import 'package:flowy_infra/time/duration.dart';
 import 'package:flowy_infra_ui/widget/route/animation.dart';
-import 'package:flowy_sdk/protobuf/flowy-user-data-model/protobuf.dart' show UserProfile;
-import 'package:flowy_sdk/protobuf/flowy-folder-data-model/protobuf.dart';
+import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart' show UserProfilePB;
+import 'package:flowy_sdk/protobuf/flowy-folder/protobuf.dart';
 import 'package:flutter/material.dart';
 
 class AuthRouter {
@@ -16,7 +16,7 @@ class AuthRouter {
     // TODO: implement showForgetPasswordScreen
   }
 
-  void pushWelcomeScreen(BuildContext context, UserProfile userProfile) {
+  void pushWelcomeScreen(BuildContext context, UserProfilePB userProfile) {
     getIt<SplashRoute>().pushWelcomeScreen(context, userProfile);
   }
 
@@ -28,7 +28,7 @@ class AuthRouter {
     );
   }
 
-  void pushHomeScreen(BuildContext context, UserProfile profile, CurrentWorkspaceSetting workspaceSetting) {
+  void pushHomeScreen(BuildContext context, UserProfilePB profile, CurrentWorkspaceSettingPB workspaceSetting) {
     Navigator.push(
       context,
       PageRoutes.fade(() => HomeScreen(profile, workspaceSetting), RouteDurations.slow.inMilliseconds * .001),
@@ -37,7 +37,7 @@ class AuthRouter {
 }
 
 class SplashRoute {
-  Future<void> pushWelcomeScreen(BuildContext context, UserProfile userProfile) async {
+  Future<void> pushWelcomeScreen(BuildContext context, UserProfilePB userProfile) async {
     final screen = WelcomeScreen(userProfile: userProfile);
     final workspaceId = await Navigator.of(context).push(
       PageRoutes.fade(
@@ -49,7 +49,7 @@ class SplashRoute {
     pushHomeScreen(context, userProfile, workspaceId);
   }
 
-  void pushHomeScreen(BuildContext context, UserProfile userProfile, CurrentWorkspaceSetting workspaceSetting) {
+  void pushHomeScreen(BuildContext context, UserProfilePB userProfile, CurrentWorkspaceSettingPB workspaceSetting) {
     Navigator.push(
       context,
       PageRoutes.fade(() => HomeScreen(userProfile, workspaceSetting), RouteDurations.slow.inMilliseconds * .001),

+ 2 - 2
frontend/app_flowy/lib/user/presentation/sign_in_screen.dart

@@ -10,7 +10,7 @@ import 'package:flowy_infra_ui/widget/rounded_input_field.dart';
 import 'package:flowy_infra_ui/widget/spacing.dart';
 import 'package:flowy_infra_ui/style_widget/snap_bar.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-user-data-model/protobuf.dart' show UserProfile;
+import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart' show UserProfilePB;
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:dartz/dartz.dart';
@@ -39,7 +39,7 @@ class SignInScreen extends StatelessWidget {
     );
   }
 
-  void _handleSuccessOrFail(Either<UserProfile, FlowyError> result, BuildContext context) {
+  void _handleSuccessOrFail(Either<UserProfilePB, FlowyError> result, BuildContext context) {
     result.fold(
       (user) => router.pushWelcomeScreen(context, user),
       (error) => showSnapBar(context, error.msg),

+ 2 - 2
frontend/app_flowy/lib/user/presentation/sign_up_screen.dart

@@ -8,7 +8,7 @@ import 'package:flowy_infra_ui/widget/rounded_button.dart';
 import 'package:flowy_infra_ui/widget/rounded_input_field.dart';
 import 'package:flowy_infra_ui/widget/spacing.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-user-data-model/protobuf.dart' show UserProfile;
+import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart' show UserProfilePB;
 import 'package:flowy_infra_ui/style_widget/snap_bar.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
@@ -36,7 +36,7 @@ class SignUpScreen extends StatelessWidget {
     );
   }
 
-  void _handleSuccessOrFail(BuildContext context, Either<UserProfile, FlowyError> result) {
+  void _handleSuccessOrFail(BuildContext context, Either<UserProfilePB, FlowyError> result) {
     result.fold(
       (user) => router.pushWelcomeScreen(context, user),
       (error) => showSnapBar(context, error.msg),

+ 4 - 7
frontend/app_flowy/lib/user/presentation/skip_log_in_screen.dart

@@ -1,5 +1,4 @@
 import 'package:app_flowy/user/application/auth_service.dart';
-import 'package:app_flowy/user/application/user_listener.dart';
 import 'package:app_flowy/user/presentation/router.dart';
 import 'package:app_flowy/user/presentation/widgets/background.dart';
 import 'package:easy_localization/easy_localization.dart';
@@ -10,9 +9,9 @@ import 'package:flowy_infra_ui/widget/rounded_button.dart';
 import 'package:flowy_infra_ui/widget/spacing.dart';
 import 'package:flowy_sdk/log.dart';
 import 'package:flowy_sdk/dispatch/dispatch.dart';
-import 'package:flowy_sdk/protobuf/flowy-folder-data-model/protobuf.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder/protobuf.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-user-data-model/user_profile.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-user/user_profile.pb.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:url_launcher/url_launcher.dart';
@@ -34,8 +33,6 @@ class SkipLogInScreen extends StatefulWidget {
 }
 
 class _SkipLogInScreenState extends State<SkipLogInScreen> {
-  UserListener? userListener;
-
   @override
   Widget build(BuildContext context) {
     return Scaffold(
@@ -119,8 +116,8 @@ class _SkipLogInScreenState extends State<SkipLogInScreen> {
 
   void _openCurrentWorkspace(
     BuildContext context,
-    UserProfile user,
-    dartz.Either<CurrentWorkspaceSetting, FlowyError> workspacesOrError,
+    UserProfilePB user,
+    dartz.Either<CurrentWorkspaceSettingPB, FlowyError> workspacesOrError,
   ) {
     workspacesOrError.fold(
       (workspaceSetting) {

+ 1 - 1
frontend/app_flowy/lib/user/presentation/splash_screen.dart

@@ -4,7 +4,7 @@ import 'package:app_flowy/user/domain/auth_state.dart';
 import 'package:app_flowy/user/presentation/router.dart';
 import 'package:flowy_sdk/log.dart';
 import 'package:flowy_sdk/dispatch/dispatch.dart';
-import 'package:flowy_sdk/protobuf/error-code/error_code.pbenum.dart';
+import 'package:flowy_sdk/protobuf/flowy-error-code/code.pb.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 

+ 7 - 7
frontend/app_flowy/lib/user/presentation/welcome_screen.dart

@@ -5,14 +5,14 @@ import 'package:flowy_infra/theme.dart';
 import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
 import 'package:flowy_infra_ui/style_widget/button.dart';
 import 'package:flowy_infra_ui/widget/error_page.dart';
-import 'package:flowy_sdk/protobuf/flowy-folder-data-model/workspace.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-user-data-model/user_profile.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder/workspace.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-user/user_profile.pb.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:app_flowy/generated/locale_keys.g.dart';
 
 class WelcomeScreen extends StatelessWidget {
-  final UserProfile userProfile;
+  final UserProfilePB userProfile;
   const WelcomeScreen({
     Key? key,
     required this.userProfile,
@@ -65,7 +65,7 @@ class WelcomeScreen extends StatelessWidget {
     );
   }
 
-  Widget _renderList(List<Workspace> workspaces) {
+  Widget _renderList(List<WorkspacePB> workspaces) {
     return Expanded(
       child: StyledListView(
         itemBuilder: (BuildContext context, int index) {
@@ -80,7 +80,7 @@ class WelcomeScreen extends StatelessWidget {
     );
   }
 
-  void _handleOnPress(BuildContext context, Workspace workspace) {
+  void _handleOnPress(BuildContext context, WorkspacePB workspace) {
     context.read<WelcomeBloc>().add(WelcomeEvent.openWorkspace(workspace));
 
     Navigator.of(context).pop(workspace.id);
@@ -88,8 +88,8 @@ class WelcomeScreen extends StatelessWidget {
 }
 
 class WorkspaceItem extends StatelessWidget {
-  final Workspace workspace;
-  final void Function(Workspace workspace) onPressed;
+  final WorkspacePB workspace;
+  final void Function(WorkspacePB workspace) onPressed;
   const WorkspaceItem({Key? key, required this.workspace, required this.onPressed}) : super(key: key);
 
   @override

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

@@ -7,8 +7,8 @@ import 'package:app_flowy/workspace/application/app/app_service.dart';
 import 'package:app_flowy/workspace/presentation/home/menu/menu.dart';
 import 'package:expandable/expandable.dart';
 import 'package:flowy_sdk/log.dart';
-import 'package:flowy_sdk/protobuf/flowy-folder-data-model/app.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder/app.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 import 'package:flutter/foundation.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
@@ -18,7 +18,7 @@ import 'package:dartz/dartz.dart';
 part 'app_bloc.freezed.dart';
 
 class AppBloc extends Bloc<AppEvent, AppState> {
-  final App app;
+  final AppPB app;
   final AppService appService;
   final AppListener appListener;
 
@@ -103,7 +103,7 @@ class AppBloc extends Bloc<AppEvent, AppState> {
     return super.close();
   }
 
-  Future<void> _didReceiveViewUpdated(List<View> views, Emitter<AppState> emit) async {
+  Future<void> _didReceiveViewUpdated(List<ViewPB> views, Emitter<AppState> emit) async {
     final latestCreatedView = state.latestCreatedView;
     AppState newState = state.copyWith(views: views);
     if (latestCreatedView != null) {
@@ -139,20 +139,20 @@ class AppEvent with _$AppEvent {
   ) = CreateView;
   const factory AppEvent.delete() = Delete;
   const factory AppEvent.rename(String newName) = Rename;
-  const factory AppEvent.didReceiveViewUpdated(List<View> views) = ReceiveViews;
-  const factory AppEvent.appDidUpdate(App app) = AppDidUpdate;
+  const factory AppEvent.didReceiveViewUpdated(List<ViewPB> views) = ReceiveViews;
+  const factory AppEvent.appDidUpdate(AppPB app) = AppDidUpdate;
 }
 
 @freezed
 class AppState with _$AppState {
   const factory AppState({
-    required App app,
-    required List<View> views,
-    View? latestCreatedView,
+    required AppPB app,
+    required List<ViewPB> views,
+    ViewPB? latestCreatedView,
     required Either<Unit, FlowyError> successOrFailure,
   }) = _AppState;
 
-  factory AppState.initial(App app) => AppState(
+  factory AppState.initial(AppPB app) => AppState(
         app: app,
         views: [],
         successOrFailure: left(unit),
@@ -161,8 +161,8 @@ class AppState with _$AppState {
 
 class AppViewDataContext extends ChangeNotifier {
   final String appId;
-  final ValueNotifier<List<View>> _viewsNotifier = ValueNotifier([]);
-  final ValueNotifier<View?> _selectedViewNotifier = ValueNotifier(null);
+  final ValueNotifier<List<ViewPB>> _viewsNotifier = ValueNotifier([]);
+  final ValueNotifier<ViewPB?> _selectedViewNotifier = ValueNotifier(null);
   VoidCallback? _menuSharedStateListener;
   ExpandableController expandController = ExpandableController(initialExpanded: false);
 
@@ -173,7 +173,7 @@ class AppViewDataContext extends ChangeNotifier {
     });
   }
 
-  VoidCallback addSelectedViewChangeListener(void Function(View?) callback) {
+  VoidCallback addSelectedViewChangeListener(void Function(ViewPB?) callback) {
     listener() {
       callback(_selectedViewNotifier.value);
     }
@@ -186,7 +186,7 @@ class AppViewDataContext extends ChangeNotifier {
     _selectedViewNotifier.removeListener(listener);
   }
 
-  void _setLatestView(View? view) {
+  void _setLatestView(ViewPB? view) {
     view?.freeze();
 
     if (_selectedViewNotifier.value != view) {
@@ -196,9 +196,9 @@ class AppViewDataContext extends ChangeNotifier {
     }
   }
 
-  View? get selectedView => _selectedViewNotifier.value;
+  ViewPB? get selectedView => _selectedViewNotifier.value;
 
-  set views(List<View> views) {
+  set views(List<ViewPB> views) {
     if (_viewsNotifier.value != views) {
       _viewsNotifier.value = views;
       _expandIfNeed();
@@ -206,9 +206,9 @@ class AppViewDataContext extends ChangeNotifier {
     }
   }
 
-  UnmodifiableListView<View> get views => UnmodifiableListView(_viewsNotifier.value);
+  UnmodifiableListView<ViewPB> get views => UnmodifiableListView(_viewsNotifier.value);
 
-  VoidCallback addViewsChangeListener(void Function(UnmodifiableListView<View>) callback) {
+  VoidCallback addViewsChangeListener(void Function(UnmodifiableListView<ViewPB>) callback) {
     listener() {
       callback(views);
     }

+ 7 - 7
frontend/app_flowy/lib/workspace/application/app/app_listener.dart

@@ -1,17 +1,17 @@
 import 'dart:async';
 import 'dart:typed_data';
+import 'package:app_flowy/core/folder_notification.dart';
 import 'package:dartz/dartz.dart';
-import 'package:app_flowy/core/notification_helper.dart';
 import 'package:flowy_sdk/log.dart';
 import 'package:flowy_sdk/protobuf/dart-notify/subject.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-folder-data-model/app.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder/app.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder/dart_notification.pb.dart';
 import 'package:flowy_sdk/rust_stream.dart';
 
-typedef AppDidUpdateCallback = void Function(App app);
-typedef ViewsDidChangeCallback = void Function(Either<List<View>, FlowyError> viewsOrFailed);
+typedef AppDidUpdateCallback = void Function(AppPB app);
+typedef ViewsDidChangeCallback = void Function(Either<List<ViewPB>, FlowyError> viewsOrFailed);
 
 class AppListener {
   StreamSubscription<SubscribeObject>? _subscription;
@@ -37,7 +37,7 @@ class AppListener {
         if (_viewsChanged != null) {
           result.fold(
             (payload) {
-              final repeatedView = RepeatedView.fromBuffer(payload);
+              final repeatedView = RepeatedViewPB.fromBuffer(payload);
               _viewsChanged!(left(repeatedView.items));
             },
             (error) => _viewsChanged!(right(error)),
@@ -48,7 +48,7 @@ class AppListener {
         if (_updated != null) {
           result.fold(
             (payload) {
-              final app = App.fromBuffer(payload);
+              final app = AppPB.fromBuffer(payload);
               _updated!(app);
             },
             (error) => Log.error(error),

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

@@ -3,8 +3,8 @@ import 'dart:async';
 import 'package:dartz/dartz.dart';
 import 'package:flowy_sdk/dispatch/dispatch.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-folder-data-model/app.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder/app.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
 
 import 'package:app_flowy/plugin/plugin.dart';
 
@@ -14,20 +14,20 @@ class AppService {
     required this.appId,
   });
 
-  Future<Either<App, FlowyError>> getAppDesc({required String appId}) {
-    final payload = AppId.create()..value = appId;
+  Future<Either<AppPB, FlowyError>> getAppDesc({required String appId}) {
+    final payload = AppIdPB.create()..value = appId;
 
     return FolderEventReadApp(payload).send();
   }
 
-  Future<Either<View, FlowyError>> createView({
+  Future<Either<ViewPB, FlowyError>> createView({
     required String appId,
     required String name,
     required String desc,
     required PluginDataType dataType,
     required PluginType pluginType,
   }) {
-    final payload = CreateViewPayload.create()
+    final payload = CreateViewPayloadPB.create()
       ..belongToId = appId
       ..name = name
       ..desc = desc
@@ -37,8 +37,8 @@ class AppService {
     return FolderEventCreateView(payload).send();
   }
 
-  Future<Either<List<View>, FlowyError>> getViews({required String appId}) {
-    final payload = AppId.create()..value = appId;
+  Future<Either<List<ViewPB>, FlowyError>> getViews({required String appId}) {
+    final payload = AppIdPB.create()..value = appId;
 
     return FolderEventReadApp(payload).send().then((result) {
       return result.fold(
@@ -49,12 +49,12 @@ class AppService {
   }
 
   Future<Either<Unit, FlowyError>> delete({required String appId}) {
-    final request = AppId.create()..value = appId;
+    final request = AppIdPB.create()..value = appId;
     return FolderEventDeleteApp(request).send();
   }
 
   Future<Either<Unit, FlowyError>> updateApp({required String appId, String? name}) {
-    UpdateAppPayload payload = UpdateAppPayload.create()..appId = appId;
+    UpdateAppPayloadPB payload = UpdateAppPayloadPB.create()..appId = appId;
 
     if (name != null) {
       payload.name = name;
@@ -67,7 +67,7 @@ class AppService {
     required int fromIndex,
     required int toIndex,
   }) {
-    final payload = MoveFolderItemPayload.create()
+    final payload = MoveFolderItemPayloadPB.create()
       ..itemId = viewId
       ..from = fromIndex
       ..to = toIndex

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

@@ -4,12 +4,12 @@ import 'package:app_flowy/user/application/user_settings_service.dart';
 import 'package:equatable/equatable.dart';
 import 'package:flowy_infra/theme.dart';
 import 'package:flowy_sdk/log.dart';
-import 'package:flowy_sdk/protobuf/flowy-user-data-model/user_setting.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-user/user_setting.pb.dart';
 import 'package:flutter/material.dart';
 import 'package:easy_localization/easy_localization.dart';
 
 class AppearanceSettingModel extends ChangeNotifier with EquatableMixin {
-  AppearanceSettings setting;
+  AppearanceSettingsPB setting;
   AppTheme _theme;
   Locale _locale;
   Timer? _saveOperation;

+ 0 - 0
frontend/app_flowy/lib/workspace/application/board/board_bloc.dart


+ 3 - 3
frontend/app_flowy/lib/workspace/application/doc/doc_bloc.dart

@@ -2,9 +2,9 @@ import 'dart:convert';
 import 'package:app_flowy/workspace/application/doc/doc_service.dart';
 import 'package:app_flowy/workspace/application/trash/trash_service.dart';
 import 'package:app_flowy/workspace/application/view/view_listener.dart';
-import 'package:flowy_sdk/protobuf/flowy-folder-data-model/trash.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder/trash.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
 import 'package:flutter_quill/flutter_quill.dart' show Document, Delta;
 import 'package:flowy_sdk/log.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
@@ -17,7 +17,7 @@ part 'doc_bloc.freezed.dart';
 typedef FlutterQuillDocument = Document;
 
 class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
-  final View view;
+  final ViewPB view;
   final DocumentService service;
 
   final ViewListener listener;

+ 8 - 8
frontend/app_flowy/lib/workspace/application/doc/doc_service.dart

@@ -1,29 +1,29 @@
 import 'package:dartz/dartz.dart';
 import 'package:flowy_sdk/dispatch/dispatch.dart';
 
-import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-sync/text_block_info.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-sync/text_block.pb.dart';
 
 class DocumentService {
-  Future<Either<TextBlockDelta, FlowyError>> openDocument({
+  Future<Either<TextBlockDeltaPB, FlowyError>> openDocument({
     required String docId,
   }) async {
-    await FolderEventSetLatestView(ViewId(value: docId)).send();
+    await FolderEventSetLatestView(ViewIdPB(value: docId)).send();
 
-    final payload = TextBlockId(value: docId);
+    final payload = TextBlockIdPB(value: docId);
     return TextBlockEventGetBlockData(payload).send();
   }
 
-  Future<Either<TextBlockDelta, FlowyError>> composeDelta({required String docId, required String data}) {
-    final payload = TextBlockDelta.create()
+  Future<Either<TextBlockDeltaPB, FlowyError>> composeDelta({required String docId, required String data}) {
+    final payload = TextBlockDeltaPB.create()
       ..blockId = docId
       ..deltaStr = data;
     return TextBlockEventApplyDelta(payload).send();
   }
 
   Future<Either<Unit, FlowyError>> closeDocument({required String docId}) {
-    final request = ViewId(value: docId);
+    final request = ViewIdPB(value: docId);
     return FolderEventCloseView(request).send();
   }
 }

+ 29 - 4
frontend/app_flowy/lib/workspace/application/doc/share_bloc.dart

@@ -1,7 +1,10 @@
+import 'dart:async';
+import 'dart:io';
+import 'package:app_flowy/startup/tasks/rust_sdk.dart';
 import 'package:app_flowy/workspace/application/doc/share_service.dart';
 import 'package:app_flowy/workspace/application/markdown/delta_markdown.dart';
 import 'package:flowy_sdk/protobuf/flowy-text-block/entities.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
@@ -10,7 +13,7 @@ part 'share_bloc.freezed.dart';
 
 class DocShareBloc extends Bloc<DocShareEvent, DocShareState> {
   ShareService service;
-  View view;
+  ViewPB view;
   DocShareBloc({required this.view, required this.service}) : super(const DocShareState.initial()) {
     on<DocShareEvent>((event, emit) async {
       await event.map(
@@ -30,11 +33,33 @@ class DocShareBloc extends Bloc<DocShareEvent, DocShareState> {
     });
   }
 
-  ExportData _convertDeltaToMarkdown(ExportData value) {
+  ExportDataPB _convertDeltaToMarkdown(ExportDataPB value) {
     final result = deltaToMarkdown(value.data);
     value.data = result;
+    writeFile(result);
     return value;
   }
+
+  Future<Directory> get _exportDir async {
+    Directory documentsDir = await appFlowyDocumentDirectory();
+
+    return documentsDir;
+  }
+
+  Future<String> get _localPath async {
+    final dir = await _exportDir;
+    return dir.path;
+  }
+
+  Future<File> get _localFile async {
+    final path = await _localPath;
+    return File('$path/${view.name}.md');
+  }
+
+  Future<File> writeFile(String md) async {
+    final file = await _localFile;
+    return file.writeAsString(md);
+  }
 }
 
 @freezed
@@ -48,5 +73,5 @@ class DocShareEvent with _$DocShareEvent {
 class DocShareState with _$DocShareState {
   const factory DocShareState.initial() = _Initial;
   const factory DocShareState.loading() = _Loading;
-  const factory DocShareState.finish(Either<ExportData, FlowyError> successOrFail) = _Finish;
+  const factory DocShareState.finish(Either<ExportDataPB, FlowyError> successOrFail) = _Finish;
 }

+ 5 - 5
frontend/app_flowy/lib/workspace/application/doc/share_service.dart

@@ -5,23 +5,23 @@ import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-text-block/protobuf.dart';
 
 class ShareService {
-  Future<Either<ExportData, FlowyError>> export(String docId, ExportType type) {
-    final request = ExportPayload.create()
+  Future<Either<ExportDataPB, FlowyError>> export(String docId, ExportType type) {
+    final request = ExportPayloadPB.create()
       ..viewId = docId
       ..exportType = type;
 
     return TextBlockEventExportDocument(request).send();
   }
 
-  Future<Either<ExportData, FlowyError>> exportText(String docId) {
+  Future<Either<ExportDataPB, FlowyError>> exportText(String docId) {
     return export(docId, ExportType.Text);
   }
 
-  Future<Either<ExportData, FlowyError>> exportMarkdown(String docId) {
+  Future<Either<ExportDataPB, FlowyError>> exportMarkdown(String docId) {
     return export(docId, ExportType.Markdown);
   }
 
-  Future<Either<ExportData, FlowyError>> exportURL(String docId) {
+  Future<Either<ExportDataPB, FlowyError>> exportURL(String docId) {
     return export(docId, ExportType.Link);
   }
 }

+ 56 - 0
frontend/app_flowy/lib/workspace/application/grid/block/block_cache.dart

@@ -0,0 +1,56 @@
+import 'dart:async';
+import 'package:app_flowy/workspace/application/grid/grid_service.dart';
+import 'package:app_flowy/workspace/application/grid/row/row_service.dart';
+import 'package:flowy_sdk/log.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
+
+import 'block_listener.dart';
+
+/// Read https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid for more information
+class GridBlockCache {
+  final String gridId;
+  final GridBlockPB block;
+  late GridRowCache _rowCache;
+  late GridBlockListener _listener;
+
+  List<GridRowInfo> get rows => _rowCache.rows;
+  GridRowCache get rowCache => _rowCache;
+
+  GridBlockCache({
+    required this.gridId,
+    required this.block,
+    required GridFieldCache fieldCache,
+  }) {
+    _rowCache = GridRowCache(
+      gridId: gridId,
+      block: block,
+      notifier: GridRowCacheFieldNotifierImpl(fieldCache),
+    );
+
+    _listener = GridBlockListener(blockId: block.id);
+    _listener.start((result) {
+      result.fold(
+        (changesets) => _rowCache.applyChangesets(changesets),
+        (err) => Log.error(err),
+      );
+    });
+  }
+
+  Future<void> dispose() async {
+    await _listener.stop();
+    await _rowCache.dispose();
+  }
+
+  void addListener({
+    required void Function(GridRowChangeReason) onChangeReason,
+    bool Function()? listenWhen,
+  }) {
+    _rowCache.onRowsChanged((reason) {
+      if (listenWhen != null && listenWhen() == false) {
+        return;
+      }
+
+      onChangeReason(reason);
+    });
+  }
+}

+ 51 - 0
frontend/app_flowy/lib/workspace/application/grid/block/block_listener.dart

@@ -0,0 +1,51 @@
+import 'dart:async';
+import 'dart:typed_data';
+import 'package:app_flowy/core/grid_notification.dart';
+import 'package:dartz/dartz.dart';
+import 'package:flowy_infra/notifier.dart';
+import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/dart_notification.pb.dart';
+
+typedef GridBlockUpdateNotifierValue = Either<List<GridBlockChangesetPB>, FlowyError>;
+
+class GridBlockListener {
+  final String blockId;
+  PublishNotifier<GridBlockUpdateNotifierValue>? _rowsUpdateNotifier = PublishNotifier();
+  GridNotificationListener? _listener;
+
+  GridBlockListener({required this.blockId});
+
+  void start(void Function(GridBlockUpdateNotifierValue) onBlockChanged) {
+    if (_listener != null) {
+      _listener?.stop();
+    }
+
+    _listener = GridNotificationListener(
+      objectId: blockId,
+      handler: _handler,
+    );
+
+    _rowsUpdateNotifier?.addPublishListener(onBlockChanged);
+  }
+
+  void _handler(GridNotification ty, Either<Uint8List, FlowyError> result) {
+    switch (ty) {
+      case GridNotification.DidUpdateGridBlock:
+        result.fold(
+          (payload) => _rowsUpdateNotifier?.value = left([GridBlockChangesetPB.fromBuffer(payload)]),
+          (error) => _rowsUpdateNotifier?.value = right(error),
+        );
+        break;
+
+      default:
+        break;
+    }
+  }
+
+  Future<void> stop() async {
+    await _listener?.stop();
+    _rowsUpdateNotifier?.dispose();
+    _rowsUpdateNotifier = null;
+  }
+}

+ 1 - 1
frontend/app_flowy/lib/workspace/application/grid/cell/cell_listener.dart

@@ -1,10 +1,10 @@
+import 'package:app_flowy/core/grid_notification.dart';
 import 'package:dartz/dartz.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/dart_notification.pb.dart';
 import 'package:flowy_infra/notifier.dart';
 import 'dart:async';
 import 'dart:typed_data';
-import 'package:app_flowy/core/notification_helper.dart';
 
 typedef UpdateFieldNotifiedValue = Either<Unit, FlowyError>;
 

+ 70 - 0
frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_cache.dart

@@ -0,0 +1,70 @@
+part of 'cell_service.dart';
+
+typedef GridCellMap = LinkedHashMap<String, GridCellIdentifier>;
+
+class GridCell {
+  dynamic object;
+  GridCell({
+    required this.object,
+  });
+}
+
+/// Use to index the cell in the grid.
+/// We use [fieldId + rowId] to identify the cell.
+class GridCellCacheKey {
+  final String fieldId;
+  final String rowId;
+  GridCellCacheKey({
+    required this.fieldId,
+    required this.rowId,
+  });
+}
+
+/// GridCellCache is used to cache cell data of each block.
+/// We use GridCellCacheKey to index the cell in the cache.
+/// Read https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid
+/// for more information
+class GridCellCache {
+  final String gridId;
+
+  /// fieldId: {cacheKey: GridCell}
+  final Map<String, Map<String, dynamic>> _cellDataByFieldId = {};
+  GridCellCache({
+    required this.gridId,
+  });
+
+  void remove(String fieldId) {
+    _cellDataByFieldId.remove(fieldId);
+  }
+
+  void insert<T extends GridCell>(GridCellCacheKey key, T value) {
+    var map = _cellDataByFieldId[key.fieldId];
+    if (map == null) {
+      _cellDataByFieldId[key.fieldId] = {};
+      map = _cellDataByFieldId[key.fieldId];
+    }
+
+    map![key.rowId] = value.object;
+  }
+
+  T? get<T>(GridCellCacheKey key) {
+    final map = _cellDataByFieldId[key.fieldId];
+    if (map == null) {
+      return null;
+    } else {
+      final value = map[key.rowId];
+      if (value is T) {
+        return value;
+      } else {
+        if (value != null) {
+          Log.error("Expected value type: $T, but receive $value");
+        }
+        return null;
+      }
+    }
+  }
+
+  Future<void> dispose() async {
+    _cellDataByFieldId.clear();
+  }
+}

+ 79 - 0
frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_data_loader.dart

@@ -0,0 +1,79 @@
+part of 'cell_service.dart';
+
+abstract class IGridCellDataConfig {
+  // The cell data will reload if it receives the field's change notification.
+  bool get reloadOnFieldChanged;
+}
+
+abstract class IGridCellDataParser<T> {
+  T? parserData(List<int> data);
+}
+
+class GridCellDataLoader<T> {
+  final CellService service = CellService();
+  final GridCellIdentifier cellId;
+  final IGridCellDataParser<T> parser;
+  final bool reloadOnFieldChanged;
+
+  GridCellDataLoader({
+    required this.cellId,
+    required this.parser,
+    this.reloadOnFieldChanged = false,
+  });
+
+  Future<T?> loadData() {
+    final fut = service.getCell(cellId: cellId);
+    return fut.then(
+      (result) => result.fold((GridCellPB cell) {
+        try {
+          return parser.parserData(cell.data);
+        } catch (e, s) {
+          Log.error('$parser parser cellData failed, $e');
+          Log.error('Stack trace \n $s');
+          return null;
+        }
+      }, (err) {
+        Log.error(err);
+        return null;
+      }),
+    );
+  }
+}
+
+class StringCellDataParser implements IGridCellDataParser<String> {
+  @override
+  String? parserData(List<int> data) {
+    final s = utf8.decode(data);
+    return s;
+  }
+}
+
+class DateCellDataParser implements IGridCellDataParser<DateCellDataPB> {
+  @override
+  DateCellDataPB? parserData(List<int> data) {
+    if (data.isEmpty) {
+      return null;
+    }
+    return DateCellDataPB.fromBuffer(data);
+  }
+}
+
+class SelectOptionCellDataParser implements IGridCellDataParser<SelectOptionCellDataPB> {
+  @override
+  SelectOptionCellDataPB? parserData(List<int> data) {
+    if (data.isEmpty) {
+      return null;
+    }
+    return SelectOptionCellDataPB.fromBuffer(data);
+  }
+}
+
+class URLCellDataParser implements IGridCellDataParser<URLCellDataPB> {
+  @override
+  URLCellDataPB? parserData(List<int> data) {
+    if (data.isEmpty) {
+      return null;
+    }
+    return URLCellDataPB.fromBuffer(data);
+  }
+}

+ 16 - 19
frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/data_persistence.dart → frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_data_persistence.dart

@@ -1,25 +1,22 @@
 part of 'cell_service.dart';
 
-abstract class _GridCellDataPersistence<D> {
+/// Save the cell data to disk
+/// You can extend this class to do custom operations. For example, the DateCellDataPersistence.
+abstract class IGridCellDataPersistence<D> {
   Future<Option<FlowyError>> save(D data);
 }
 
-class CellDataPersistence implements _GridCellDataPersistence<String> {
-  final GridCell gridCell;
+class CellDataPersistence implements IGridCellDataPersistence<String> {
+  final GridCellIdentifier cellId;
 
   CellDataPersistence({
-    required this.gridCell,
+    required this.cellId,
   });
   final CellService _cellService = CellService();
 
   @override
   Future<Option<FlowyError>> save(String data) async {
-    final fut = _cellService.updateCell(
-      gridId: gridCell.gridId,
-      fieldId: gridCell.field.id,
-      rowId: gridCell.rowId,
-      data: data,
-    );
+    final fut = _cellService.updateCell(cellId: cellId, data: data);
 
     return fut.then((result) {
       return result.fold(
@@ -35,15 +32,15 @@ class CalendarData with _$CalendarData {
   const factory CalendarData({required DateTime date, String? time}) = _CalendarData;
 }
 
-class DateCellDataPersistence implements _GridCellDataPersistence<CalendarData> {
-  final GridCell gridCell;
+class DateCellDataPersistence implements IGridCellDataPersistence<CalendarData> {
+  final GridCellIdentifier cellId;
   DateCellDataPersistence({
-    required this.gridCell,
+    required this.cellId,
   });
 
   @override
   Future<Option<FlowyError>> save(CalendarData data) {
-    var payload = DateChangesetPayload.create()..cellIdentifier = _cellIdentifier(gridCell);
+    var payload = DateChangesetPayloadPB.create()..cellIdentifier = _makeCellIdPayload(cellId);
 
     final date = (data.date.millisecondsSinceEpoch ~/ 1000).toString();
     payload.date = date;
@@ -61,9 +58,9 @@ class DateCellDataPersistence implements _GridCellDataPersistence<CalendarData>
   }
 }
 
-CellIdentifierPayload _cellIdentifier(GridCell gridCell) {
-  return CellIdentifierPayload.create()
-    ..gridId = gridCell.gridId
-    ..fieldId = gridCell.field.id
-    ..rowId = gridCell.rowId;
+GridCellIdentifierPayloadPB _makeCellIdPayload(GridCellIdentifier cellId) {
+  return GridCellIdentifierPayloadPB.create()
+    ..gridId = cellId.gridId
+    ..fieldId = cellId.fieldId
+    ..rowId = cellId.rowId;
 }

+ 60 - 0
frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_field_notifier.dart

@@ -0,0 +1,60 @@
+import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
+import 'package:flutter/foundation.dart';
+
+import 'cell_service.dart';
+
+abstract class GridFieldChangedNotifier {
+  void onFieldChanged(void Function(GridFieldPB) callback);
+  void dispose();
+}
+
+/// GridPB's cell helper wrapper that enables each cell will get notified when the corresponding field was changed.
+/// You Register an onFieldChanged callback to listen to the cell changes, and unregister if you don't want to listen.
+class GridCellFieldNotifier {
+  /// fieldId: {objectId: callback}
+  final Map<String, Map<String, List<VoidCallback>>> _fieldListenerByFieldId = {};
+
+  GridCellFieldNotifier({required GridFieldChangedNotifier notifier}) {
+    notifier.onFieldChanged(
+      (field) {
+        final map = _fieldListenerByFieldId[field.id];
+        if (map != null) {
+          for (final callbacks in map.values) {
+            for (final callback in callbacks) {
+              callback();
+            }
+          }
+        }
+      },
+    );
+  }
+
+  ///
+  void register(GridCellCacheKey cacheKey, VoidCallback onFieldChanged) {
+    var map = _fieldListenerByFieldId[cacheKey.fieldId];
+    if (map == null) {
+      _fieldListenerByFieldId[cacheKey.fieldId] = {};
+      map = _fieldListenerByFieldId[cacheKey.fieldId];
+      map![cacheKey.rowId] = [onFieldChanged];
+    } else {
+      var objects = map[cacheKey.rowId];
+      if (objects == null) {
+        map[cacheKey.rowId] = [onFieldChanged];
+      } else {
+        objects.add(onFieldChanged);
+      }
+    }
+  }
+
+  void unregister(GridCellCacheKey cacheKey, VoidCallback fn) {
+    var callbacks = _fieldListenerByFieldId[cacheKey.fieldId]?[cacheKey.rowId];
+    final index = callbacks?.indexWhere((callback) => callback == fn);
+    if (index != null && index != -1) {
+      callbacks?.removeAt(index);
+    }
+  }
+
+  Future<void> dispose() async {
+    _fieldListenerByFieldId.clear();
+  }
+}

+ 36 - 32
frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_service.dart

@@ -1,27 +1,30 @@
 import 'dart:async';
 import 'dart:collection';
 
+import 'package:app_flowy/workspace/application/grid/grid_service.dart';
 import 'package:dartz/dartz.dart';
 import 'package:equatable/equatable.dart';
 import 'package:flowy_sdk/dispatch/dispatch.dart';
 import 'package:flowy_sdk/log.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/cell_entities.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option_entities.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option_entities.pb.dart';
 import 'package:flutter/foundation.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'package:app_flowy/workspace/application/grid/cell/cell_listener.dart';
-import 'package:app_flowy/workspace/application/grid/cell/select_option_service.dart';
 import 'package:app_flowy/workspace/application/grid/field/field_service.dart';
 import 'dart:convert' show utf8;
+
+import '../../field/type_option/type_option_service.dart';
+import 'cell_field_notifier.dart';
 part 'cell_service.freezed.dart';
-part 'data_loader.dart';
+part 'cell_data_loader.dart';
 part 'context_builder.dart';
-part 'data_cache.dart';
-part 'data_persistence.dart';
+part 'cell_cache.dart';
+part 'cell_data_persistence.dart';
 
 // key: rowId
 
@@ -29,45 +32,46 @@ class CellService {
   CellService();
 
   Future<Either<void, FlowyError>> updateCell({
-    required String gridId,
-    required String fieldId,
-    required String rowId,
+    required GridCellIdentifier cellId,
     required String data,
   }) {
-    final payload = CellChangeset.create()
-      ..gridId = gridId
-      ..fieldId = fieldId
-      ..rowId = rowId
-      ..cellContentChangeset = data;
+    final payload = CellChangesetPB.create()
+      ..gridId = cellId.gridId
+      ..fieldId = cellId.fieldId
+      ..rowId = cellId.rowId
+      ..content = data;
     return GridEventUpdateCell(payload).send();
   }
 
-  Future<Either<Cell, FlowyError>> getCell({
-    required String gridId,
-    required String fieldId,
-    required String rowId,
+  Future<Either<GridCellPB, FlowyError>> getCell({
+    required GridCellIdentifier cellId,
   }) {
-    final payload = CellIdentifierPayload.create()
-      ..gridId = gridId
-      ..fieldId = fieldId
-      ..rowId = rowId;
+    final payload = GridCellIdentifierPayloadPB.create()
+      ..gridId = cellId.gridId
+      ..fieldId = cellId.fieldId
+      ..rowId = cellId.rowId;
     return GridEventGetCell(payload).send();
   }
 }
 
+/// Id of the cell
+/// We can locate the cell by using gridId + rowId + field.id.
 @freezed
-class GridCell with _$GridCell {
-  const factory GridCell({
+class GridCellIdentifier with _$GridCellIdentifier {
+  const factory GridCellIdentifier({
     required String gridId,
     required String rowId,
-    required Field field,
-    Cell? cell,
-  }) = _GridCell;
+    required GridFieldPB field,
+  }) = _GridCellIdentifier;
 
   // ignore: unused_element
-  const GridCell._();
+  const GridCellIdentifier._();
+
+  String get fieldId => field.id;
+
+  FieldType get fieldType => field.fieldType;
 
-  String cellId() {
-    return rowId + field.id + "${field.fieldType}";
+  ValueKey key() {
+    return ValueKey(rowId + fieldId + "${field.fieldType}");
   }
 }

+ 177 - 94
frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/context_builder.dart

@@ -1,154 +1,183 @@
 part of 'cell_service.dart';
 
-typedef GridCellContext = _GridCellContext<String, String>;
-typedef GridSelectOptionCellContext = _GridCellContext<SelectOptionCellData, String>;
-typedef GridDateCellContext = _GridCellContext<DateCellData, CalendarData>;
-typedef GridURLCellContext = _GridCellContext<URLCellData, String>;
+typedef GridCellController = IGridCellController<String, String>;
+typedef GridSelectOptionCellController = IGridCellController<SelectOptionCellDataPB, String>;
+typedef GridDateCellController = IGridCellController<DateCellDataPB, CalendarData>;
+typedef GridURLCellController = IGridCellController<URLCellDataPB, String>;
 
-class GridCellContextBuilder {
+class GridCellControllerBuilder {
+  final GridCellIdentifier _cellId;
   final GridCellCache _cellCache;
-  final GridCell _gridCell;
-  GridCellContextBuilder({
+  final GridFieldCache _fieldCache;
+
+  GridCellControllerBuilder({
+    required GridCellIdentifier cellId,
     required GridCellCache cellCache,
-    required GridCell gridCell,
+    required GridFieldCache fieldCache,
   })  : _cellCache = cellCache,
-        _gridCell = gridCell;
+        _fieldCache = fieldCache,
+        _cellId = cellId;
+
+  IGridCellController build() {
+    final cellFieldNotifier = GridCellFieldNotifier(notifier: _GridFieldChangedNotifierImpl(_fieldCache));
 
-  _GridCellContext build() {
-    switch (_gridCell.field.fieldType) {
+    switch (_cellId.fieldType) {
       case FieldType.Checkbox:
         final cellDataLoader = GridCellDataLoader(
-          gridCell: _gridCell,
+          cellId: _cellId,
           parser: StringCellDataParser(),
         );
-        return GridCellContext(
-          gridCell: _gridCell,
+        return GridCellController(
+          cellId: _cellId,
           cellCache: _cellCache,
           cellDataLoader: cellDataLoader,
-          cellDataPersistence: CellDataPersistence(gridCell: _gridCell),
+          fieldNotifier: cellFieldNotifier,
+          cellDataPersistence: CellDataPersistence(cellId: _cellId),
         );
       case FieldType.DateTime:
         final cellDataLoader = GridCellDataLoader(
-          gridCell: _gridCell,
+          cellId: _cellId,
           parser: DateCellDataParser(),
-          config: const GridCellDataConfig(reloadOnFieldChanged: true),
+          reloadOnFieldChanged: true,
         );
 
-        return GridDateCellContext(
-          gridCell: _gridCell,
+        return GridDateCellController(
+          cellId: _cellId,
           cellCache: _cellCache,
           cellDataLoader: cellDataLoader,
-          cellDataPersistence: DateCellDataPersistence(gridCell: _gridCell),
+          fieldNotifier: cellFieldNotifier,
+          cellDataPersistence: DateCellDataPersistence(cellId: _cellId),
         );
       case FieldType.Number:
         final cellDataLoader = GridCellDataLoader(
-          gridCell: _gridCell,
+          cellId: _cellId,
           parser: StringCellDataParser(),
-          config: const GridCellDataConfig(reloadOnCellChanged: true, reloadOnFieldChanged: true),
+          reloadOnFieldChanged: true,
         );
-        return GridCellContext(
-          gridCell: _gridCell,
+        return GridCellController(
+          cellId: _cellId,
           cellCache: _cellCache,
           cellDataLoader: cellDataLoader,
-          cellDataPersistence: CellDataPersistence(gridCell: _gridCell),
+          fieldNotifier: cellFieldNotifier,
+          cellDataPersistence: CellDataPersistence(cellId: _cellId),
         );
       case FieldType.RichText:
         final cellDataLoader = GridCellDataLoader(
-          gridCell: _gridCell,
+          cellId: _cellId,
           parser: StringCellDataParser(),
         );
-        return GridCellContext(
-          gridCell: _gridCell,
+        return GridCellController(
+          cellId: _cellId,
           cellCache: _cellCache,
           cellDataLoader: cellDataLoader,
-          cellDataPersistence: CellDataPersistence(gridCell: _gridCell),
+          fieldNotifier: cellFieldNotifier,
+          cellDataPersistence: CellDataPersistence(cellId: _cellId),
         );
       case FieldType.MultiSelect:
       case FieldType.SingleSelect:
         final cellDataLoader = GridCellDataLoader(
-          gridCell: _gridCell,
+          cellId: _cellId,
           parser: SelectOptionCellDataParser(),
-          config: const GridCellDataConfig(reloadOnFieldChanged: true),
+          reloadOnFieldChanged: true,
         );
 
-        return GridSelectOptionCellContext(
-          gridCell: _gridCell,
+        return GridSelectOptionCellController(
+          cellId: _cellId,
           cellCache: _cellCache,
           cellDataLoader: cellDataLoader,
-          cellDataPersistence: CellDataPersistence(gridCell: _gridCell),
+          fieldNotifier: cellFieldNotifier,
+          cellDataPersistence: CellDataPersistence(cellId: _cellId),
         );
 
       case FieldType.URL:
         final cellDataLoader = GridCellDataLoader(
-          gridCell: _gridCell,
+          cellId: _cellId,
           parser: URLCellDataParser(),
         );
-        return GridURLCellContext(
-          gridCell: _gridCell,
+        return GridURLCellController(
+          cellId: _cellId,
           cellCache: _cellCache,
           cellDataLoader: cellDataLoader,
-          cellDataPersistence: CellDataPersistence(gridCell: _gridCell),
+          fieldNotifier: cellFieldNotifier,
+          cellDataPersistence: CellDataPersistence(cellId: _cellId),
         );
     }
     throw UnimplementedError;
   }
 }
 
-// T: the type of the CellData
-// D: the type of the data that will be save to disk
+/// IGridCellController is used to manipulate the cell and receive notifications.
+/// * Read/Write cell data
+/// * Listen on field/cell notifications.
+///
+/// Generic T represents the type of the cell data.
+/// Generic D represents the type of data that will be saved to the disk
+///
 // ignore: must_be_immutable
-class _GridCellContext<T, D> extends Equatable {
-  final GridCell gridCell;
-  final GridCellCache cellCache;
+class IGridCellController<T, D> extends Equatable {
+  final GridCellIdentifier cellId;
+  final GridCellCache _cellsCache;
   final GridCellCacheKey _cacheKey;
-  final IGridCellDataLoader<T> cellDataLoader;
-  final _GridCellDataPersistence<D> cellDataPersistence;
   final FieldService _fieldService;
+  final GridCellFieldNotifier _fieldNotifier;
+  final GridCellDataLoader<T> _cellDataLoader;
+  final IGridCellDataPersistence<D> _cellDataPersistence;
 
   late final CellListener _cellListener;
-  late final ValueNotifier<T?>? _cellDataNotifier;
+  ValueNotifier<T?>? _cellDataNotifier;
+
   bool isListening = false;
   VoidCallback? _onFieldChangedFn;
   Timer? _loadDataOperation;
   Timer? _saveDataOperation;
+  bool _isDispose = false;
 
-  _GridCellContext({
-    required this.gridCell,
-    required this.cellCache,
-    required this.cellDataLoader,
-    required this.cellDataPersistence,
-  })  : _fieldService = FieldService(gridId: gridCell.gridId, fieldId: gridCell.field.id),
-        _cacheKey = GridCellCacheKey(objectId: gridCell.rowId, fieldId: gridCell.field.id);
-
-  _GridCellContext<T, D> clone() {
-    return _GridCellContext(
-        gridCell: gridCell,
-        cellDataLoader: cellDataLoader,
-        cellCache: cellCache,
-        cellDataPersistence: cellDataPersistence);
-  }
+  IGridCellController({
+    required this.cellId,
+    required GridCellCache cellCache,
+    required GridCellFieldNotifier fieldNotifier,
+    required GridCellDataLoader<T> cellDataLoader,
+    required IGridCellDataPersistence<D> cellDataPersistence,
+  })  : _cellsCache = cellCache,
+        _cellDataLoader = cellDataLoader,
+        _cellDataPersistence = cellDataPersistence,
+        _fieldNotifier = fieldNotifier,
+        _fieldService = FieldService(gridId: cellId.gridId, fieldId: cellId.field.id),
+        _cacheKey = GridCellCacheKey(rowId: cellId.rowId, fieldId: cellId.field.id);
 
-  String get gridId => gridCell.gridId;
+  IGridCellController<T, D> clone() {
+    return IGridCellController(
+        cellId: cellId,
+        cellDataLoader: _cellDataLoader,
+        cellCache: _cellsCache,
+        fieldNotifier: _fieldNotifier,
+        cellDataPersistence: _cellDataPersistence);
+  }
 
-  String get rowId => gridCell.rowId;
+  String get gridId => cellId.gridId;
 
-  String get cellId => gridCell.rowId + gridCell.field.id;
+  String get rowId => cellId.rowId;
 
-  String get fieldId => gridCell.field.id;
+  String get fieldId => cellId.field.id;
 
-  Field get field => gridCell.field;
+  GridFieldPB get field => cellId.field;
 
-  FieldType get fieldType => gridCell.field.fieldType;
+  FieldType get fieldType => cellId.field.fieldType;
 
-  VoidCallback? startListening({required void Function(T?) onCellChanged}) {
+  VoidCallback? startListening({required void Function(T?) onCellChanged, VoidCallback? onCellFieldChanged}) {
     if (isListening) {
       Log.error("Already started. It seems like you should call clone first");
       return null;
     }
-
     isListening = true;
-    _cellDataNotifier = ValueNotifier(cellCache.get(_cacheKey));
-    _cellListener = CellListener(rowId: gridCell.rowId, fieldId: gridCell.field.id);
+
+    _cellDataNotifier = ValueNotifier(_cellsCache.get(_cacheKey));
+    _cellListener = CellListener(rowId: cellId.rowId, fieldId: cellId.field.id);
+
+    /// 1.Listen on user edit event and load the new cell data if needed.
+    /// For example:
+    ///  user input: 12
+    ///  cell display: $12
     _cellListener.start(onCellChanged: (result) {
       result.fold(
         (_) => _loadData(),
@@ -156,22 +185,27 @@ class _GridCellContext<T, D> extends Equatable {
       );
     });
 
-    if (cellDataLoader.config.reloadOnFieldChanged) {
-      _onFieldChangedFn = () {
-        _loadData();
-      };
-      cellCache.addFieldListener(_cacheKey, _onFieldChangedFn!);
-    }
-
-    onCellChangedFn() {
-      onCellChanged(_cellDataNotifier?.value);
+    /// 2.Listen on the field event and load the cell data if needed.
+    _onFieldChangedFn = () {
+      if (onCellFieldChanged != null) {
+        onCellFieldChanged();
+      }
 
-      if (cellDataLoader.config.reloadOnCellChanged) {
+      /// reloadOnFieldChanged should be true if you need to load the data when the corresponding field is changed
+      /// For example:
+      ///   ¥12 -> $12
+      if (_cellDataLoader.reloadOnFieldChanged) {
         _loadData();
       }
-    }
+    };
+
+    _fieldNotifier.register(_cacheKey, _onFieldChangedFn!);
 
+    /// Notify the listener, the cell data was changed.
+    onCellChangedFn() => onCellChanged(_cellDataNotifier?.value);
     _cellDataNotifier?.addListener(onCellChangedFn);
+
+    // Return the function pointer that can be used when calling removeListener.
     return onCellChangedFn;
   }
 
@@ -179,29 +213,45 @@ class _GridCellContext<T, D> extends Equatable {
     _cellDataNotifier?.removeListener(fn);
   }
 
-  T? getCellData({bool loadIfNoCache = true}) {
-    final data = cellCache.get(_cacheKey);
-    if (data == null && loadIfNoCache) {
+  /// Return the cell data.
+  /// The cell data will be read from the Cache first, and load from disk if it does not exist.
+  /// You can set [loadIfNotExist] to false (default is true) to disable loading the cell data.
+  T? getCellData({bool loadIfNotExist = true}) {
+    final data = _cellsCache.get(_cacheKey);
+    if (data == null && loadIfNotExist) {
       _loadData();
     }
     return data;
   }
 
-  Future<Either<FieldTypeOptionData, FlowyError>> getTypeOptionData() {
-    return _fieldService.getFieldTypeOptionData(fieldType: fieldType);
+  /// Return the FieldTypeOptionDataPB that can be parsed into corresponding class using the [parser].
+  /// [PD] is the type that the parser return.
+  Future<Either<PD, FlowyError>> getFieldTypeOption<PD, P extends TypeOptionDataParser>(P parser) {
+    return _fieldService.getFieldTypeOptionData(fieldType: fieldType).then((result) {
+      return result.fold(
+        (data) => parser.fromBuffer(data.typeOptionData),
+        (err) => right(err),
+      );
+    });
   }
 
+  /// Save the cell data to disk
+  /// You can set [dedeplicate] to true (default is false) to reduce the save operation.
+  /// It's useful when you call this method when user editing the [TextField].
+  /// The default debounce interval is 300 milliseconds.
   void saveCellData(D data, {bool deduplicate = false, void Function(Option<FlowyError>)? resultCallback}) async {
     if (deduplicate) {
       _loadDataOperation?.cancel();
-      _loadDataOperation = Timer(const Duration(milliseconds: 300), () async {
-        final result = await cellDataPersistence.save(data);
+
+      _saveDataOperation?.cancel();
+      _saveDataOperation = Timer(const Duration(milliseconds: 300), () async {
+        final result = await _cellDataPersistence.save(data);
         if (resultCallback != null) {
           resultCallback(result);
         }
       });
     } else {
-      final result = await cellDataPersistence.save(data);
+      final result = await _cellDataPersistence.save(data);
       if (resultCallback != null) {
         resultCallback(result);
       }
@@ -209,26 +259,59 @@ class _GridCellContext<T, D> extends Equatable {
   }
 
   void _loadData() {
+    _saveDataOperation?.cancel();
+
     _loadDataOperation?.cancel();
     _loadDataOperation = Timer(const Duration(milliseconds: 10), () {
-      cellDataLoader.loadData().then((data) {
+      _cellDataLoader.loadData().then((data) {
         _cellDataNotifier?.value = data;
-        cellCache.insert(GridCellCacheData(key: _cacheKey, object: data));
+        _cellsCache.insert(_cacheKey, GridCell(object: data));
       });
     });
   }
 
   void dispose() {
+    if (_isDispose) {
+      Log.error("$this should only dispose once");
+      return;
+    }
+    _isDispose = true;
     _cellListener.stop();
     _loadDataOperation?.cancel();
     _saveDataOperation?.cancel();
+    _cellDataNotifier = null;
 
     if (_onFieldChangedFn != null) {
-      cellCache.removeFieldListener(_cacheKey, _onFieldChangedFn!);
+      _fieldNotifier.unregister(_cacheKey, _onFieldChangedFn!);
       _onFieldChangedFn = null;
     }
   }
 
   @override
-  List<Object> get props => [cellCache.get(_cacheKey) ?? "", cellId];
+  List<Object> get props => [_cellsCache.get(_cacheKey) ?? "", cellId.rowId + cellId.field.id];
+}
+
+class _GridFieldChangedNotifierImpl extends GridFieldChangedNotifier {
+  final GridFieldCache _cache;
+  FieldChangesetCallback? _onChangesetFn;
+
+  _GridFieldChangedNotifierImpl(GridFieldCache cache) : _cache = cache;
+
+  @override
+  void dispose() {
+    if (_onChangesetFn != null) {
+      _cache.removeListener(onChangsetListener: _onChangesetFn!);
+      _onChangesetFn = null;
+    }
+  }
+
+  @override
+  void onFieldChanged(void Function(GridFieldPB p1) callback) {
+    _onChangesetFn = (GridFieldChangesetPB changeset) {
+      for (final updatedField in changeset.updatedFields) {
+        callback(updatedField);
+      }
+    };
+    _cache.addListener(onChangeset: _onChangesetFn);
+  }
 }

+ 0 - 111
frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/data_cache.dart

@@ -1,111 +0,0 @@
-part of 'cell_service.dart';
-
-typedef GridCellMap = LinkedHashMap<String, GridCell>;
-
-class GridCellCacheData {
-  GridCellCacheKey key;
-  dynamic object;
-  GridCellCacheData({
-    required this.key,
-    required this.object,
-  });
-}
-
-class GridCellCacheKey {
-  final String fieldId;
-  final String objectId;
-  GridCellCacheKey({
-    required this.fieldId,
-    required this.objectId,
-  });
-}
-
-abstract class GridCellFieldDelegate {
-  void onFieldChanged(void Function(String) callback);
-  void dispose();
-}
-
-class GridCellCache {
-  final String gridId;
-  final GridCellFieldDelegate fieldDelegate;
-
-  /// fieldId: {objectId: callback}
-  final Map<String, Map<String, List<VoidCallback>>> _fieldListenerByFieldId = {};
-
-  /// fieldId: {cacheKey: cacheData}
-  final Map<String, Map<String, dynamic>> _cellDataByFieldId = {};
-  GridCellCache({
-    required this.gridId,
-    required this.fieldDelegate,
-  }) {
-    fieldDelegate.onFieldChanged((fieldId) {
-      _cellDataByFieldId.remove(fieldId);
-      final map = _fieldListenerByFieldId[fieldId];
-      if (map != null) {
-        for (final callbacks in map.values) {
-          for (final callback in callbacks) {
-            callback();
-          }
-        }
-      }
-    });
-  }
-
-  void addFieldListener(GridCellCacheKey cacheKey, VoidCallback onFieldChanged) {
-    var map = _fieldListenerByFieldId[cacheKey.fieldId];
-    if (map == null) {
-      _fieldListenerByFieldId[cacheKey.fieldId] = {};
-      map = _fieldListenerByFieldId[cacheKey.fieldId];
-      map![cacheKey.objectId] = [onFieldChanged];
-    } else {
-      var objects = map[cacheKey.objectId];
-      if (objects == null) {
-        map[cacheKey.objectId] = [onFieldChanged];
-      } else {
-        objects.add(onFieldChanged);
-      }
-    }
-  }
-
-  void removeFieldListener(GridCellCacheKey cacheKey, VoidCallback fn) {
-    var callbacks = _fieldListenerByFieldId[cacheKey.fieldId]?[cacheKey.objectId];
-    final index = callbacks?.indexWhere((callback) => callback == fn);
-    if (index != null && index != -1) {
-      callbacks?.removeAt(index);
-    }
-  }
-
-  void insert<T extends GridCellCacheData>(T item) {
-    var map = _cellDataByFieldId[item.key.fieldId];
-    if (map == null) {
-      _cellDataByFieldId[item.key.fieldId] = {};
-      map = _cellDataByFieldId[item.key.fieldId];
-    }
-
-    map![item.key.objectId] = item.object;
-  }
-
-  T? get<T>(GridCellCacheKey key) {
-    final map = _cellDataByFieldId[key.fieldId];
-    if (map == null) {
-      return null;
-    } else {
-      final object = map[key.objectId];
-      if (object is T) {
-        return object;
-      } else {
-        if (object != null) {
-          Log.error("Cache data type does not match the cache data type");
-        }
-
-        return null;
-      }
-    }
-  }
-
-  Future<void> dispose() async {
-    _fieldListenerByFieldId.clear();
-    _cellDataByFieldId.clear();
-    fieldDelegate.dispose();
-  }
-}

+ 0 - 134
frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/data_loader.dart

@@ -1,134 +0,0 @@
-part of 'cell_service.dart';
-
-abstract class IGridCellDataConfig {
-  // The cell data will reload if it receives the field's change notification.
-  bool get reloadOnFieldChanged;
-
-  // When the reloadOnCellChanged is true, it will load the cell data after user input.
-  // For example: The number cell reload the cell data that carries the format
-  // user input: 12
-  // cell display: $12
-  bool get reloadOnCellChanged;
-}
-
-class GridCellDataConfig implements IGridCellDataConfig {
-  @override
-  final bool reloadOnCellChanged;
-
-  @override
-  final bool reloadOnFieldChanged;
-
-  const GridCellDataConfig({
-    this.reloadOnCellChanged = false,
-    this.reloadOnFieldChanged = false,
-  });
-}
-
-abstract class IGridCellDataLoader<T> {
-  Future<T?> loadData();
-
-  IGridCellDataConfig get config;
-}
-
-abstract class ICellDataParser<T> {
-  T? parserData(List<int> data);
-}
-
-class GridCellDataLoader<T> extends IGridCellDataLoader<T> {
-  final CellService service = CellService();
-  final GridCell gridCell;
-  final ICellDataParser<T> parser;
-
-  @override
-  final IGridCellDataConfig config;
-
-  GridCellDataLoader({
-    required this.gridCell,
-    required this.parser,
-    this.config = const GridCellDataConfig(),
-  });
-
-  @override
-  Future<T?> loadData() {
-    final fut = service.getCell(
-      gridId: gridCell.gridId,
-      fieldId: gridCell.field.id,
-      rowId: gridCell.rowId,
-    );
-    return fut.then(
-      (result) => result.fold((Cell cell) {
-        try {
-          return parser.parserData(cell.data);
-        } catch (e, s) {
-          Log.error('$parser parser cellData failed, $e');
-          Log.error('Stack trace \n $s');
-          return null;
-        }
-      }, (err) {
-        Log.error(err);
-        return null;
-      }),
-    );
-  }
-}
-
-class SelectOptionCellDataLoader extends IGridCellDataLoader<SelectOptionCellData> {
-  final SelectOptionService service;
-  final GridCell gridCell;
-  SelectOptionCellDataLoader({
-    required this.gridCell,
-  }) : service = SelectOptionService(gridCell: gridCell);
-  @override
-  Future<SelectOptionCellData?> loadData() async {
-    return service.getOpitonContext().then((result) {
-      return result.fold(
-        (data) => data,
-        (err) {
-          Log.error(err);
-          return null;
-        },
-      );
-    });
-  }
-
-  @override
-  IGridCellDataConfig get config => const GridCellDataConfig(reloadOnFieldChanged: true);
-}
-
-class StringCellDataParser implements ICellDataParser<String> {
-  @override
-  String? parserData(List<int> data) {
-    final s = utf8.decode(data);
-    return s;
-  }
-}
-
-class DateCellDataParser implements ICellDataParser<DateCellData> {
-  @override
-  DateCellData? parserData(List<int> data) {
-    if (data.isEmpty) {
-      return null;
-    }
-    return DateCellData.fromBuffer(data);
-  }
-}
-
-class SelectOptionCellDataParser implements ICellDataParser<SelectOptionCellData> {
-  @override
-  SelectOptionCellData? parserData(List<int> data) {
-    if (data.isEmpty) {
-      return null;
-    }
-    return SelectOptionCellData.fromBuffer(data);
-  }
-}
-
-class URLCellDataParser implements ICellDataParser<URLCellData> {
-  @override
-  URLCellData? parserData(List<int> data) {
-    if (data.isEmpty) {
-      return null;
-    }
-    return URLCellData.fromBuffer(data);
-  }
-}

+ 2 - 2
frontend/app_flowy/lib/workspace/application/grid/cell/checkbox_cell_bloc.dart

@@ -6,7 +6,7 @@ import 'cell_service/cell_service.dart';
 part 'checkbox_cell_bloc.freezed.dart';
 
 class CheckboxCellBloc extends Bloc<CheckboxCellEvent, CheckboxCellState> {
-  final GridCellContext cellContext;
+  final GridCellController cellContext;
   void Function()? _onCellChangedFn;
 
   CheckboxCellBloc({
@@ -67,7 +67,7 @@ class CheckboxCellState with _$CheckboxCellState {
     required bool isSelected,
   }) = _CheckboxCellState;
 
-  factory CheckboxCellState.initial(GridCellContext context) {
+  factory CheckboxCellState.initial(GridCellController context) {
     return CheckboxCellState(isSelected: _isSelected(context.getCellData()));
   }
 }

+ 10 - 9
frontend/app_flowy/lib/workspace/application/grid/cell/date_cal_bloc.dart

@@ -5,6 +5,7 @@ import 'package:flowy_sdk/log.dart';
 import 'package:flowy_sdk/protobuf/flowy-error-code/code.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option_entities.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'package:table_calendar/table_calendar.dart';
@@ -16,12 +17,12 @@ import 'package:fixnum/fixnum.dart' as $fixnum;
 part 'date_cal_bloc.freezed.dart';
 
 class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
-  final GridDateCellContext cellContext;
+  final GridDateCellController cellContext;
   void Function()? _onCellChangedFn;
 
   DateCalBloc({
     required DateTypeOption dateTypeOption,
-    required DateCellData? cellData,
+    required DateCellDataPB? cellData,
     required this.cellContext,
   }) : super(DateCalState.initial(dateTypeOption, cellData)) {
     on<DateCalEvent>(
@@ -37,7 +38,7 @@ class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
           setFocusedDay: (focusedDay) {
             emit(state.copyWith(focusedDay: focusedDay));
           },
-          didReceiveCellUpdate: (DateCellData? cellData) {
+          didReceiveCellUpdate: (DateCellDataPB? cellData) {
             final calData = calDataFromCellData(cellData);
             final time = calData.foldRight("", (dateData, previous) => dateData.time);
             emit(state.copyWith(calData: calData, time: time));
@@ -187,7 +188,7 @@ class DateCalEvent with _$DateCalEvent {
   const factory DateCalEvent.setDateFormat(DateFormat dateFormat) = _DateFormat;
   const factory DateCalEvent.setIncludeTime(bool includeTime) = _IncludeTime;
   const factory DateCalEvent.setTime(String time) = _Time;
-  const factory DateCalEvent.didReceiveCellUpdate(DateCellData? data) = _DidReceiveCellUpdate;
+  const factory DateCalEvent.didReceiveCellUpdate(DateCellDataPB? data) = _DidReceiveCellUpdate;
   const factory DateCalEvent.didUpdateCalData(Option<CalendarData> data, Option<String> timeFormatError) =
       _DidUpdateCalData;
 }
@@ -206,7 +207,7 @@ class DateCalState with _$DateCalState {
 
   factory DateCalState.initial(
     DateTypeOption dateTypeOption,
-    DateCellData? cellData,
+    DateCellDataPB? cellData,
   ) {
     Option<CalendarData> calData = calDataFromCellData(cellData);
     final time = calData.foldRight("", (dateData, previous) => dateData.time);
@@ -225,14 +226,14 @@ class DateCalState with _$DateCalState {
 String _timeHintText(DateTypeOption typeOption) {
   switch (typeOption.timeFormat) {
     case TimeFormat.TwelveHour:
-      return LocaleKeys.grid_date_timeHintTextInTwelveHour.tr();
+      return LocaleKeys.document_date_timeHintTextInTwelveHour.tr();
     case TimeFormat.TwentyFourHour:
-      return LocaleKeys.grid_date_timeHintTextInTwentyFourHour.tr();
+      return LocaleKeys.document_date_timeHintTextInTwentyFourHour.tr();
   }
   return "";
 }
 
-Option<CalendarData> calDataFromCellData(DateCellData? cellData) {
+Option<CalendarData> calDataFromCellData(DateCellDataPB? cellData) {
   String? time = timeFromCellData(cellData);
   Option<CalendarData> calData = none();
   if (cellData != null) {
@@ -248,7 +249,7 @@ $fixnum.Int64 timestampFromDateTime(DateTime dateTime) {
   return $fixnum.Int64(timestamp);
 }
 
-String? timeFromCellData(DateCellData? cellData) {
+String? timeFromCellData(DateCellDataPB? cellData) {
   String? time;
   if (cellData?.hasTime() ?? false) {
     time = cellData?.time;

+ 11 - 11
frontend/app_flowy/lib/workspace/application/grid/cell/date_cell_bloc.dart

@@ -1,5 +1,5 @@
-import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart' show Field;
-import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option_entities.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
@@ -7,7 +7,7 @@ import 'cell_service/cell_service.dart';
 part 'date_cell_bloc.freezed.dart';
 
 class DateCellBloc extends Bloc<DateCellEvent, DateCellState> {
-  final GridDateCellContext cellContext;
+  final GridDateCellController cellContext;
   void Function()? _onCellChangedFn;
 
   DateCellBloc({required this.cellContext}) : super(DateCellState.initial(cellContext)) {
@@ -15,10 +15,10 @@ class DateCellBloc extends Bloc<DateCellEvent, DateCellState> {
       (event, emit) async {
         event.when(
           initial: () => _startListening(),
-          didReceiveCellUpdate: (DateCellData? cellData) {
+          didReceiveCellUpdate: (DateCellDataPB? cellData) {
             emit(state.copyWith(data: cellData, dateStr: _dateStrFromCellData(cellData)));
           },
-          didReceiveFieldUpdate: (Field value) => emit(state.copyWith(field: value)),
+          didReceiveFieldUpdate: (GridFieldPB value) => emit(state.copyWith(field: value)),
         );
       },
     );
@@ -48,19 +48,19 @@ class DateCellBloc extends Bloc<DateCellEvent, DateCellState> {
 @freezed
 class DateCellEvent with _$DateCellEvent {
   const factory DateCellEvent.initial() = _InitialCell;
-  const factory DateCellEvent.didReceiveCellUpdate(DateCellData? data) = _DidReceiveCellUpdate;
-  const factory DateCellEvent.didReceiveFieldUpdate(Field field) = _DidReceiveFieldUpdate;
+  const factory DateCellEvent.didReceiveCellUpdate(DateCellDataPB? data) = _DidReceiveCellUpdate;
+  const factory DateCellEvent.didReceiveFieldUpdate(GridFieldPB field) = _DidReceiveFieldUpdate;
 }
 
 @freezed
 class DateCellState with _$DateCellState {
   const factory DateCellState({
-    required DateCellData? data,
+    required DateCellDataPB? data,
     required String dateStr,
-    required Field field,
+    required GridFieldPB field,
   }) = _DateCellState;
 
-  factory DateCellState.initial(GridDateCellContext context) {
+  factory DateCellState.initial(GridDateCellController context) {
     final cellData = context.getCellData();
 
     return DateCellState(
@@ -71,7 +71,7 @@ class DateCellState with _$DateCellState {
   }
 }
 
-String _dateStrFromCellData(DateCellData? cellData) {
+String _dateStrFromCellData(DateCellDataPB? cellData) {
   String dateStr = "";
   if (cellData != null) {
     dateStr = cellData.date + " " + cellData.time;

+ 2 - 2
frontend/app_flowy/lib/workspace/application/grid/cell/number_cell_bloc.dart

@@ -8,7 +8,7 @@ import 'cell_service/cell_service.dart';
 part 'number_cell_bloc.freezed.dart';
 
 class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
-  final GridCellContext cellContext;
+  final GridCellController cellContext;
   void Function()? _onCellChangedFn;
 
   NumberCellBloc({
@@ -72,7 +72,7 @@ class NumberCellState with _$NumberCellState {
     required Either<String, FlowyError> content,
   }) = _NumberCellState;
 
-  factory NumberCellState.initial(GridCellContext context) {
+  factory NumberCellState.initial(GridCellController context) {
     final cellContent = context.getCellData() ?? "";
     return NumberCellState(
       content: left(cellContent),

+ 5 - 5
frontend/app_flowy/lib/workspace/application/grid/cell/select_option_cell_bloc.dart

@@ -1,5 +1,5 @@
 import 'dart:async';
-import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'package:app_flowy/workspace/application/grid/cell/cell_service/cell_service.dart';
@@ -7,7 +7,7 @@ import 'package:app_flowy/workspace/application/grid/cell/cell_service/cell_serv
 part 'select_option_cell_bloc.freezed.dart';
 
 class SelectOptionCellBloc extends Bloc<SelectOptionCellEvent, SelectOptionCellState> {
-  final GridSelectOptionCellContext cellContext;
+  final GridSelectOptionCellController cellContext;
   void Function()? _onCellChangedFn;
 
   SelectOptionCellBloc({
@@ -56,17 +56,17 @@ class SelectOptionCellBloc extends Bloc<SelectOptionCellEvent, SelectOptionCellS
 class SelectOptionCellEvent with _$SelectOptionCellEvent {
   const factory SelectOptionCellEvent.initial() = _InitialCell;
   const factory SelectOptionCellEvent.didReceiveOptions(
-    List<SelectOption> selectedOptions,
+    List<SelectOptionPB> selectedOptions,
   ) = _DidReceiveOptions;
 }
 
 @freezed
 class SelectOptionCellState with _$SelectOptionCellState {
   const factory SelectOptionCellState({
-    required List<SelectOption> selectedOptions,
+    required List<SelectOptionPB> selectedOptions,
   }) = _SelectOptionCellState;
 
-  factory SelectOptionCellState.initial(GridSelectOptionCellContext context) {
+  factory SelectOptionCellState.initial(GridSelectOptionCellController context) {
     final data = context.getCellData();
 
     return SelectOptionCellState(

+ 23 - 40
frontend/app_flowy/lib/workspace/application/grid/cell/select_option_editor_bloc.dart

@@ -1,8 +1,7 @@
 import 'dart:async';
-import 'package:app_flowy/workspace/application/grid/field/grid_listenr.dart';
 import 'package:dartz/dartz.dart';
 import 'package:flowy_sdk/log.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'package:app_flowy/workspace/application/grid/cell/cell_service/cell_service.dart';
@@ -13,16 +12,13 @@ part 'select_option_editor_bloc.freezed.dart';
 
 class SelectOptionCellEditorBloc extends Bloc<SelectOptionEditorEvent, SelectOptionEditorState> {
   final SelectOptionService _selectOptionService;
-  final GridSelectOptionCellContext cellContext;
-  late final GridFieldsListener _fieldListener;
-  void Function()? _onCellChangedFn;
+  final GridSelectOptionCellController cellController;
   Timer? _delayOperation;
 
   SelectOptionCellEditorBloc({
-    required this.cellContext,
-  })  : _selectOptionService = SelectOptionService(gridCell: cellContext.gridCell),
-        _fieldListener = GridFieldsListener(gridId: cellContext.gridId),
-        super(SelectOptionEditorState.initial(cellContext)) {
+    required this.cellController,
+  })  : _selectOptionService = SelectOptionService(cellId: cellController.cellId),
+        super(SelectOptionEditorState.initial(cellController)) {
     on<SelectOptionEditorEvent>(
       (event, emit) async {
         await event.map(
@@ -64,13 +60,8 @@ class SelectOptionCellEditorBloc extends Bloc<SelectOptionEditorEvent, SelectOpt
 
   @override
   Future<void> close() async {
-    if (_onCellChangedFn != null) {
-      cellContext.removeListener(_onCellChangedFn!);
-      _onCellChangedFn = null;
-    }
     _delayOperation?.cancel();
-    await _fieldListener.stop();
-    cellContext.dispose();
+    cellController.dispose();
     return super.close();
   }
 
@@ -79,7 +70,7 @@ class SelectOptionCellEditorBloc extends Bloc<SelectOptionEditorEvent, SelectOpt
     result.fold((l) => {}, (err) => Log.error(err));
   }
 
-  void _deleteOption(SelectOption option) async {
+  void _deleteOption(SelectOptionPB option) async {
     final result = await _selectOptionService.delete(
       option: option,
     );
@@ -87,7 +78,7 @@ class SelectOptionCellEditorBloc extends Bloc<SelectOptionEditorEvent, SelectOpt
     result.fold((l) => null, (err) => Log.error(err));
   }
 
-  void _updateOption(SelectOption option) async {
+  void _updateOption(SelectOptionPB option) async {
     final result = await _selectOptionService.update(
       option: option,
     );
@@ -131,8 +122,8 @@ class SelectOptionCellEditorBloc extends Bloc<SelectOptionEditorEvent, SelectOpt
     });
   }
 
-  _MakeOptionResult _makeOptions(Option<String> filter, List<SelectOption> allOptions) {
-    final List<SelectOption> options = List.from(allOptions);
+  _MakeOptionResult _makeOptions(Option<String> filter, List<SelectOptionPB> allOptions) {
+    final List<SelectOptionPB> options = List.from(allOptions);
     Option<String> createOption = filter;
 
     filter.foldRight(null, (filter, previous) {
@@ -157,24 +148,16 @@ class SelectOptionCellEditorBloc extends Bloc<SelectOptionEditorEvent, SelectOpt
   }
 
   void _startListening() {
-    _onCellChangedFn = cellContext.startListening(
+    cellController.startListening(
       onCellChanged: ((selectOptionContext) {
         if (!isClosed) {
           _loadOptions();
         }
       }),
+      onCellFieldChanged: () {
+        _loadOptions();
+      },
     );
-
-    _fieldListener.start(onFieldsChanged: (result) {
-      result.fold(
-        (changeset) {
-          if (changeset.updatedFields.isNotEmpty) {
-            _loadOptions();
-          }
-        },
-        (err) => Log.error(err),
-      );
-    });
   }
 }
 
@@ -182,26 +165,26 @@ class SelectOptionCellEditorBloc extends Bloc<SelectOptionEditorEvent, SelectOpt
 class SelectOptionEditorEvent with _$SelectOptionEditorEvent {
   const factory SelectOptionEditorEvent.initial() = _Initial;
   const factory SelectOptionEditorEvent.didReceiveOptions(
-      List<SelectOption> options, List<SelectOption> selectedOptions) = _DidReceiveOptions;
+      List<SelectOptionPB> options, List<SelectOptionPB> selectedOptions) = _DidReceiveOptions;
   const factory SelectOptionEditorEvent.newOption(String optionName) = _NewOption;
   const factory SelectOptionEditorEvent.selectOption(String optionId) = _SelectOption;
-  const factory SelectOptionEditorEvent.updateOption(SelectOption option) = _UpdateOption;
-  const factory SelectOptionEditorEvent.deleteOption(SelectOption option) = _DeleteOption;
+  const factory SelectOptionEditorEvent.updateOption(SelectOptionPB option) = _UpdateOption;
+  const factory SelectOptionEditorEvent.deleteOption(SelectOptionPB option) = _DeleteOption;
   const factory SelectOptionEditorEvent.filterOption(String optionName) = _SelectOptionFilter;
 }
 
 @freezed
 class SelectOptionEditorState with _$SelectOptionEditorState {
   const factory SelectOptionEditorState({
-    required List<SelectOption> options,
-    required List<SelectOption> allOptions,
-    required List<SelectOption> selectedOptions,
+    required List<SelectOptionPB> options,
+    required List<SelectOptionPB> allOptions,
+    required List<SelectOptionPB> selectedOptions,
     required Option<String> createOption,
     required Option<String> filter,
   }) = _SelectOptionEditorState;
 
-  factory SelectOptionEditorState.initial(GridSelectOptionCellContext context) {
-    final data = context.getCellData(loadIfNoCache: false);
+  factory SelectOptionEditorState.initial(GridSelectOptionCellController context) {
+    final data = context.getCellData(loadIfNotExist: false);
     return SelectOptionEditorState(
       options: data?.options ?? [],
       allOptions: data?.options ?? [],
@@ -213,7 +196,7 @@ class SelectOptionEditorState with _$SelectOptionEditorState {
 }
 
 class _MakeOptionResult {
-  List<SelectOption> options;
+  List<SelectOptionPB> options;
   Option<String> createOption;
 
   _MakeOptionResult({

+ 18 - 18
frontend/app_flowy/lib/workspace/application/grid/cell/select_option_service.dart

@@ -2,28 +2,28 @@ import 'package:dartz/dartz.dart';
 import 'package:flowy_sdk/dispatch/dispatch.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/cell_entities.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
 import 'package:app_flowy/workspace/application/grid/field/type_option/type_option_service.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart';
 import 'cell_service/cell_service.dart';
 
 class SelectOptionService {
-  final GridCell gridCell;
-  SelectOptionService({required this.gridCell});
+  final GridCellIdentifier cellId;
+  SelectOptionService({required this.cellId});
 
-  String get gridId => gridCell.gridId;
-  String get fieldId => gridCell.field.id;
-  String get rowId => gridCell.rowId;
+  String get gridId => cellId.gridId;
+  String get fieldId => cellId.field.id;
+  String get rowId => cellId.rowId;
 
   Future<Either<Unit, FlowyError>> create({required String name}) {
     return TypeOptionService(gridId: gridId, fieldId: fieldId).newOption(name: name).then(
       (result) {
         return result.fold(
           (option) {
-            final cellIdentifier = CellIdentifierPayload.create()
+            final cellIdentifier = GridCellIdentifierPayloadPB.create()
               ..gridId = gridId
               ..fieldId = fieldId
               ..rowId = rowId;
-            final payload = SelectOptionChangesetPayload.create()
+            final payload = SelectOptionChangesetPayloadPB.create()
               ..insertOption = option
               ..cellIdentifier = cellIdentifier;
             return GridEventUpdateSelectOption(payload).send();
@@ -35,26 +35,26 @@ class SelectOptionService {
   }
 
   Future<Either<Unit, FlowyError>> update({
-    required SelectOption option,
+    required SelectOptionPB option,
   }) {
-    final payload = SelectOptionChangesetPayload.create()
+    final payload = SelectOptionChangesetPayloadPB.create()
       ..updateOption = option
       ..cellIdentifier = _cellIdentifier();
     return GridEventUpdateSelectOption(payload).send();
   }
 
   Future<Either<Unit, FlowyError>> delete({
-    required SelectOption option,
+    required SelectOptionPB option,
   }) {
-    final payload = SelectOptionChangesetPayload.create()
+    final payload = SelectOptionChangesetPayloadPB.create()
       ..deleteOption = option
       ..cellIdentifier = _cellIdentifier();
 
     return GridEventUpdateSelectOption(payload).send();
   }
 
-  Future<Either<SelectOptionCellData, FlowyError>> getOpitonContext() {
-    final payload = CellIdentifierPayload.create()
+  Future<Either<SelectOptionCellDataPB, FlowyError>> getOpitonContext() {
+    final payload = GridCellIdentifierPayloadPB.create()
       ..gridId = gridId
       ..fieldId = fieldId
       ..rowId = rowId;
@@ -63,21 +63,21 @@ class SelectOptionService {
   }
 
   Future<Either<void, FlowyError>> select({required String optionId}) {
-    final payload = SelectOptionCellChangesetPayload.create()
+    final payload = SelectOptionCellChangesetPayloadPB.create()
       ..cellIdentifier = _cellIdentifier()
       ..insertOptionId = optionId;
     return GridEventUpdateSelectOptionCell(payload).send();
   }
 
   Future<Either<void, FlowyError>> unSelect({required String optionId}) {
-    final payload = SelectOptionCellChangesetPayload.create()
+    final payload = SelectOptionCellChangesetPayloadPB.create()
       ..cellIdentifier = _cellIdentifier()
       ..deleteOptionId = optionId;
     return GridEventUpdateSelectOptionCell(payload).send();
   }
 
-  CellIdentifierPayload _cellIdentifier() {
-    return CellIdentifierPayload.create()
+  GridCellIdentifierPayloadPB _cellIdentifier() {
+    return GridCellIdentifierPayloadPB.create()
       ..gridId = gridId
       ..fieldId = fieldId
       ..rowId = rowId;

+ 2 - 2
frontend/app_flowy/lib/workspace/application/grid/cell/text_cell_bloc.dart

@@ -6,7 +6,7 @@ import 'cell_service/cell_service.dart';
 part 'text_cell_bloc.freezed.dart';
 
 class TextCellBloc extends Bloc<TextCellEvent, TextCellState> {
-  final GridCellContext cellContext;
+  final GridCellController cellContext;
   void Function()? _onCellChangedFn;
   TextCellBloc({
     required this.cellContext,
@@ -63,7 +63,7 @@ class TextCellState with _$TextCellState {
     required String content,
   }) = _TextCellState;
 
-  factory TextCellState.initial(GridCellContext context) => TextCellState(
+  factory TextCellState.initial(GridCellController context) => TextCellState(
         content: context.getCellData() ?? "",
       );
 }

+ 4 - 4
frontend/app_flowy/lib/workspace/application/grid/cell/url_cell_bloc.dart

@@ -1,4 +1,4 @@
-import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option_entities.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
@@ -7,7 +7,7 @@ import 'cell_service/cell_service.dart';
 part 'url_cell_bloc.freezed.dart';
 
 class URLCellBloc extends Bloc<URLCellEvent, URLCellState> {
-  final GridURLCellContext cellContext;
+  final GridURLCellController cellContext;
   void Function()? _onCellChangedFn;
   URLCellBloc({
     required this.cellContext,
@@ -57,7 +57,7 @@ class URLCellBloc extends Bloc<URLCellEvent, URLCellState> {
 class URLCellEvent with _$URLCellEvent {
   const factory URLCellEvent.initial() = _InitialCell;
   const factory URLCellEvent.updateURL(String url) = _UpdateURL;
-  const factory URLCellEvent.didReceiveCellUpdate(URLCellData? cell) = _DidReceiveCellUpdate;
+  const factory URLCellEvent.didReceiveCellUpdate(URLCellDataPB? cell) = _DidReceiveCellUpdate;
 }
 
 @freezed
@@ -67,7 +67,7 @@ class URLCellState with _$URLCellState {
     required String url,
   }) = _URLCellState;
 
-  factory URLCellState.initial(GridURLCellContext context) {
+  factory URLCellState.initial(GridURLCellController context) {
     final cellData = context.getCellData();
     return URLCellState(
       content: cellData?.content ?? "",

+ 4 - 4
frontend/app_flowy/lib/workspace/application/grid/cell/url_cell_editor_bloc.dart

@@ -1,4 +1,4 @@
-import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option_entities.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
@@ -7,7 +7,7 @@ import 'cell_service/cell_service.dart';
 part 'url_cell_editor_bloc.freezed.dart';
 
 class URLCellEditorBloc extends Bloc<URLCellEditorEvent, URLCellEditorState> {
-  final GridURLCellContext cellContext;
+  final GridURLCellController cellContext;
   void Function()? _onCellChangedFn;
   URLCellEditorBloc({
     required this.cellContext,
@@ -54,7 +54,7 @@ class URLCellEditorBloc extends Bloc<URLCellEditorEvent, URLCellEditorState> {
 @freezed
 class URLCellEditorEvent with _$URLCellEditorEvent {
   const factory URLCellEditorEvent.initial() = _InitialCell;
-  const factory URLCellEditorEvent.didReceiveCellUpdate(URLCellData? cell) = _DidReceiveCellUpdate;
+  const factory URLCellEditorEvent.didReceiveCellUpdate(URLCellDataPB? cell) = _DidReceiveCellUpdate;
   const factory URLCellEditorEvent.updateText(String text) = _UpdateText;
 }
 
@@ -64,7 +64,7 @@ class URLCellEditorState with _$URLCellEditorState {
     required String content,
   }) = _URLCellEditorState;
 
-  factory URLCellEditorState.initial(GridURLCellContext context) {
+  factory URLCellEditorState.initial(GridURLCellController context) {
     final cellData = context.getCellData();
     return URLCellEditorState(
       content: cellData?.content ?? "",

+ 5 - 5
frontend/app_flowy/lib/workspace/application/grid/field/field_action_sheet_bloc.dart

@@ -1,5 +1,5 @@
 import 'package:flowy_sdk/log.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
@@ -10,8 +10,8 @@ part 'field_action_sheet_bloc.freezed.dart';
 class FieldActionSheetBloc extends Bloc<FieldActionSheetEvent, FieldActionSheetState> {
   final FieldService fieldService;
 
-  FieldActionSheetBloc({required Field field, required this.fieldService})
-      : super(FieldActionSheetState.initial(FieldTypeOptionData.create()..field_2 = field)) {
+  FieldActionSheetBloc({required GridFieldPB field, required this.fieldService})
+      : super(FieldActionSheetState.initial(FieldTypeOptionDataPB.create()..field_2 = field)) {
     on<FieldActionSheetEvent>(
       (event, emit) async {
         await event.map(
@@ -67,12 +67,12 @@ class FieldActionSheetEvent with _$FieldActionSheetEvent {
 @freezed
 class FieldActionSheetState with _$FieldActionSheetState {
   const factory FieldActionSheetState({
-    required FieldTypeOptionData fieldTypeOptionData,
+    required FieldTypeOptionDataPB fieldTypeOptionData,
     required String errorText,
     required String fieldName,
   }) = _FieldActionSheetState;
 
-  factory FieldActionSheetState.initial(FieldTypeOptionData data) => FieldActionSheetState(
+  factory FieldActionSheetState.initial(FieldTypeOptionDataPB data) => FieldActionSheetState(
         fieldTypeOptionData: data,
         errorText: '',
         fieldName: data.field_2.name,

+ 3 - 3
frontend/app_flowy/lib/workspace/application/grid/field/field_cell_bloc.dart

@@ -1,7 +1,7 @@
 import 'package:app_flowy/workspace/application/grid/field/field_listener.dart';
 import 'package:app_flowy/workspace/application/grid/field/field_service.dart';
 import 'package:flowy_sdk/log.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart' show Field;
+import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
@@ -62,7 +62,7 @@ class FieldCellBloc extends Bloc<FieldCellEvent, FieldCellState> {
 @freezed
 class FieldCellEvent with _$FieldCellEvent {
   const factory FieldCellEvent.initial() = _InitialCell;
-  const factory FieldCellEvent.didReceiveFieldUpdate(Field field) = _DidReceiveFieldUpdate;
+  const factory FieldCellEvent.didReceiveFieldUpdate(GridFieldPB field) = _DidReceiveFieldUpdate;
   const factory FieldCellEvent.startUpdateWidth(double offset) = _StartUpdateWidth;
   const factory FieldCellEvent.endUpdateWidth() = _EndUpdateWidth;
 }
@@ -71,7 +71,7 @@ class FieldCellEvent with _$FieldCellEvent {
 class FieldCellState with _$FieldCellState {
   const factory FieldCellState({
     required String gridId,
-    required Field field,
+    required GridFieldPB field,
     required double width,
   }) = _FieldCellState;
 

+ 23 - 12
frontend/app_flowy/lib/workspace/application/grid/field/field_editor_bloc.dart

@@ -1,3 +1,4 @@
+import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
@@ -6,27 +7,32 @@ import 'package:dartz/dartz.dart';
 part 'field_editor_bloc.freezed.dart';
 
 class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
+  final TypeOptionDataController dataController;
+
   FieldEditorBloc({
     required String gridId,
     required String fieldName,
-    required IFieldContextLoader fieldContextLoader,
-  }) : super(FieldEditorState.initial(gridId, fieldName, fieldContextLoader)) {
+    required IFieldTypeOptionLoader loader,
+  })  : dataController = TypeOptionDataController(gridId: gridId, loader: loader),
+        super(FieldEditorState.initial(gridId, fieldName)) {
     on<FieldEditorEvent>(
       (event, emit) async {
         await event.when(
           initial: () async {
-            final fieldContext = GridFieldContext(gridId: gridId, loader: fieldContextLoader);
-            await fieldContext.loadData().then((result) {
-              result.fold(
-                (l) => emit(state.copyWith(fieldContext: Some(fieldContext), name: fieldContext.field.name)),
-                (r) => null,
-              );
+            dataController.addFieldListener((field) {
+              if (!isClosed) {
+                add(FieldEditorEvent.didReceiveFieldChanged(field));
+              }
             });
+            await dataController.loadData();
           },
           updateName: (name) {
-            state.fieldContext.fold(() => null, (fieldContext) => fieldContext.fieldName = name);
+            dataController.fieldName = name;
             emit(state.copyWith(name: name));
           },
+          didReceiveFieldChanged: (GridFieldPB field) {
+            emit(state.copyWith(field: Some(field)));
+          },
         );
       },
     );
@@ -42,6 +48,7 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
 class FieldEditorEvent with _$FieldEditorEvent {
   const factory FieldEditorEvent.initial() = _InitialField;
   const factory FieldEditorEvent.updateName(String name) = _UpdateName;
+  const factory FieldEditorEvent.didReceiveFieldChanged(GridFieldPB field) = _DidReceiveFieldChanged;
 }
 
 @freezed
@@ -50,13 +57,17 @@ class FieldEditorState with _$FieldEditorState {
     required String gridId,
     required String errorText,
     required String name,
-    required Option<GridFieldContext> fieldContext,
+    required Option<GridFieldPB> field,
   }) = _FieldEditorState;
 
-  factory FieldEditorState.initial(String gridId, String fieldName, IFieldContextLoader loader) => FieldEditorState(
+  factory FieldEditorState.initial(
+    String gridId,
+    String fieldName,
+  ) =>
+      FieldEditorState(
         gridId: gridId,
-        fieldContext: none(),
         errorText: '',
+        field: none(),
         name: fieldName,
       );
 }

+ 0 - 57
frontend/app_flowy/lib/workspace/application/grid/field/field_editor_pannel_bloc.dart

@@ -1,57 +0,0 @@
-import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
-import 'package:flutter_bloc/flutter_bloc.dart';
-import 'package:freezed_annotation/freezed_annotation.dart';
-import 'dart:async';
-
-import 'field_service.dart';
-
-part 'field_editor_pannel_bloc.freezed.dart';
-
-class FieldEditorPannelBloc extends Bloc<FieldEditorPannelEvent, FieldEditorPannelState> {
-  final GridFieldContext _fieldContext;
-  void Function()? _fieldListenFn;
-
-  FieldEditorPannelBloc(GridFieldContext fieldContext)
-      : _fieldContext = fieldContext,
-        super(FieldEditorPannelState.initial(fieldContext)) {
-    on<FieldEditorPannelEvent>(
-      (event, emit) async {
-        event.when(
-          initial: () {
-            _fieldListenFn = fieldContext.addFieldListener((field) {
-              add(FieldEditorPannelEvent.didReceiveFieldUpdated(field));
-            });
-          },
-          didReceiveFieldUpdated: (field) {
-            emit(state.copyWith(field: field));
-          },
-        );
-      },
-    );
-  }
-
-  @override
-  Future<void> close() async {
-    if (_fieldListenFn != null) {
-      _fieldContext.removeFieldListener(_fieldListenFn!);
-    }
-    return super.close();
-  }
-}
-
-@freezed
-class FieldEditorPannelEvent with _$FieldEditorPannelEvent {
-  const factory FieldEditorPannelEvent.initial() = _Initial;
-  const factory FieldEditorPannelEvent.didReceiveFieldUpdated(Field field) = _DidReceiveFieldUpdated;
-}
-
-@freezed
-class FieldEditorPannelState with _$FieldEditorPannelState {
-  const factory FieldEditorPannelState({
-    required Field field,
-  }) = _FieldEditorPannelState;
-
-  factory FieldEditorPannelState.initial(GridFieldContext fieldContext) => FieldEditorPannelState(
-        field: fieldContext.field,
-      );
-}

+ 4 - 4
frontend/app_flowy/lib/workspace/application/grid/field/field_listener.dart

@@ -1,13 +1,13 @@
+import 'package:app_flowy/core/grid_notification.dart';
 import 'package:dartz/dartz.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/dart_notification.pb.dart';
 import 'package:flowy_infra/notifier.dart';
 import 'dart:async';
 import 'dart:typed_data';
-import 'package:app_flowy/core/notification_helper.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
 
-typedef UpdateFieldNotifiedValue = Either<Field, FlowyError>;
+typedef UpdateFieldNotifiedValue = Either<GridFieldPB, FlowyError>;
 
 class SingleFieldListener {
   final String fieldId;
@@ -31,7 +31,7 @@ class SingleFieldListener {
     switch (ty) {
       case GridNotification.DidUpdateField:
         result.fold(
-          (payload) => _updateFieldNotifier?.value = left(Field.fromBuffer(payload)),
+          (payload) => _updateFieldNotifier?.value = left(GridFieldPB.fromBuffer(payload)),
           (error) => _updateFieldNotifier?.value = right(error),
         );
         break;

+ 44 - 47
frontend/app_flowy/lib/workspace/application/grid/field/field_service.dart

@@ -1,14 +1,19 @@
 import 'package:dartz/dartz.dart';
+import 'package:flowy_infra/notifier.dart';
 import 'package:flowy_sdk/dispatch/dispatch.dart';
 import 'package:flowy_sdk/log.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/grid_entities.pb.dart';
 import 'package:flutter/foundation.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'package:protobuf/protobuf.dart';
 part 'field_service.freezed.dart';
 
+/// FieldService consists of lots of event functions. We define the events in the backend(Rust),
+/// you can find the corresponding event implementation in event_map.rs of the corresponding crate.
+///
+/// You could check out the rust-lib/flowy-grid/event_map.rs for more information.
 class FieldService {
   final String gridId;
   final String fieldId;
@@ -16,10 +21,10 @@ class FieldService {
   FieldService({required this.gridId, required this.fieldId});
 
   Future<Either<Unit, FlowyError>> moveField(int fromIndex, int toIndex) {
-    final payload = MoveItemPayload.create()
+    final payload = MoveItemPayloadPB.create()
       ..gridId = gridId
       ..itemId = fieldId
-      ..ty = MoveItemType.MoveField
+      ..ty = MoveItemTypePB.MoveField
       ..fromIndex = fromIndex
       ..toIndex = toIndex;
 
@@ -34,7 +39,7 @@ class FieldService {
     double? width,
     List<int>? typeOptionData,
   }) {
-    var payload = FieldChangesetPayload.create()
+    var payload = FieldChangesetPayloadPB.create()
       ..gridId = gridId
       ..fieldId = fieldId;
 
@@ -68,11 +73,11 @@ class FieldService {
   // Create the field if it does not exist. Otherwise, update the field.
   static Future<Either<Unit, FlowyError>> insertField({
     required String gridId,
-    required Field field,
+    required GridFieldPB field,
     List<int>? typeOptionData,
     String? startFieldId,
   }) {
-    var payload = InsertFieldPayload.create()
+    var payload = InsertFieldPayloadPB.create()
       ..gridId = gridId
       ..field_2 = field
       ..typeOptionData = typeOptionData ?? [];
@@ -89,7 +94,7 @@ class FieldService {
     required String fieldId,
     required List<int> typeOptionData,
   }) {
-    var payload = UpdateFieldTypeOptionPayload.create()
+    var payload = UpdateFieldTypeOptionPayloadPB.create()
       ..gridId = gridId
       ..fieldId = fieldId
       ..typeOptionData = typeOptionData;
@@ -98,7 +103,7 @@ class FieldService {
   }
 
   Future<Either<Unit, FlowyError>> deleteField() {
-    final payload = FieldIdentifierPayload.create()
+    final payload = GridFieldIdentifierPayloadPB.create()
       ..gridId = gridId
       ..fieldId = fieldId;
 
@@ -106,17 +111,17 @@ class FieldService {
   }
 
   Future<Either<Unit, FlowyError>> duplicateField() {
-    final payload = FieldIdentifierPayload.create()
+    final payload = GridFieldIdentifierPayloadPB.create()
       ..gridId = gridId
       ..fieldId = fieldId;
 
     return GridEventDuplicateField(payload).send();
   }
 
-  Future<Either<FieldTypeOptionData, FlowyError>> getFieldTypeOptionData({
+  Future<Either<FieldTypeOptionDataPB, FlowyError>> getFieldTypeOptionData({
     required FieldType fieldType,
   }) {
-    final payload = EditFieldPayload.create()
+    final payload = EditFieldPayloadPB.create()
       ..gridId = gridId
       ..fieldId = fieldId
       ..fieldType = fieldType;
@@ -133,16 +138,16 @@ class FieldService {
 class GridFieldCellContext with _$GridFieldCellContext {
   const factory GridFieldCellContext({
     required String gridId,
-    required Field field,
+    required GridFieldPB field,
   }) = _GridFieldCellContext;
 }
 
-abstract class IFieldContextLoader {
+abstract class IFieldTypeOptionLoader {
   String get gridId;
-  Future<Either<FieldTypeOptionData, FlowyError>> load();
+  Future<Either<FieldTypeOptionDataPB, FlowyError>> load();
 
-  Future<Either<FieldTypeOptionData, FlowyError>> switchToField(String fieldId, FieldType fieldType) {
-    final payload = EditFieldPayload.create()
+  Future<Either<FieldTypeOptionDataPB, FlowyError>> switchToField(String fieldId, FieldType fieldType) {
+    final payload = EditFieldPayloadPB.create()
       ..gridId = gridId
       ..fieldId = fieldId
       ..fieldType = fieldType;
@@ -151,16 +156,16 @@ abstract class IFieldContextLoader {
   }
 }
 
-class NewFieldContextLoader extends IFieldContextLoader {
+class NewFieldTypeOptionLoader extends IFieldTypeOptionLoader {
   @override
   final String gridId;
-  NewFieldContextLoader({
+  NewFieldTypeOptionLoader({
     required this.gridId,
   });
 
   @override
-  Future<Either<FieldTypeOptionData, FlowyError>> load() {
-    final payload = EditFieldPayload.create()
+  Future<Either<FieldTypeOptionDataPB, FlowyError>> load() {
+    final payload = EditFieldPayloadPB.create()
       ..gridId = gridId
       ..fieldType = FieldType.RichText;
 
@@ -168,19 +173,19 @@ class NewFieldContextLoader extends IFieldContextLoader {
   }
 }
 
-class FieldContextLoader extends IFieldContextLoader {
+class FieldTypeOptionLoader extends IFieldTypeOptionLoader {
   @override
   final String gridId;
-  final Field field;
+  final GridFieldPB field;
 
-  FieldContextLoader({
+  FieldTypeOptionLoader({
     required this.gridId,
     required this.field,
   });
 
   @override
-  Future<Either<FieldTypeOptionData, FlowyError>> load() {
-    final payload = EditFieldPayload.create()
+  Future<Either<FieldTypeOptionDataPB, FlowyError>> load() {
+    final payload = EditFieldPayloadPB.create()
       ..gridId = gridId
       ..fieldId = field.id
       ..fieldType = field.fieldType;
@@ -189,16 +194,16 @@ class FieldContextLoader extends IFieldContextLoader {
   }
 }
 
-class GridFieldContext {
+class TypeOptionDataController {
   final String gridId;
-  final IFieldContextLoader _loader;
+  final IFieldTypeOptionLoader _loader;
 
-  late FieldTypeOptionData _data;
-  ValueNotifier<Field>? _fieldNotifier;
+  late FieldTypeOptionDataPB _data;
+  final PublishNotifier<GridFieldPB> _fieldNotifier = PublishNotifier();
 
-  GridFieldContext({
+  TypeOptionDataController({
     required this.gridId,
-    required IFieldContextLoader loader,
+    required IFieldTypeOptionLoader loader,
   }) : _loader = loader;
 
   Future<Either<Unit, FlowyError>> loadData() async {
@@ -207,13 +212,7 @@ class GridFieldContext {
       (data) {
         data.freeze();
         _data = data;
-
-        if (_fieldNotifier == null) {
-          _fieldNotifier = ValueNotifier(data.field_2);
-        } else {
-          _fieldNotifier?.value = data.field_2;
-        }
-
+        _fieldNotifier.value = data.field_2;
         return left(unit);
       },
       (err) {
@@ -223,9 +222,9 @@ class GridFieldContext {
     );
   }
 
-  Field get field => _data.field_2;
+  GridFieldPB get field => _data.field_2;
 
-  set field(Field field) {
+  set field(GridFieldPB field) {
     _updateData(newField: field);
   }
 
@@ -239,7 +238,7 @@ class GridFieldContext {
     _updateData(newTypeOptionData: typeOptionData);
   }
 
-  void _updateData({String? newName, Field? newField, List<int>? newTypeOptionData}) {
+  void _updateData({String? newName, GridFieldPB? newField, List<int>? newTypeOptionData}) {
     _data = _data.rebuild((rebuildData) {
       if (newName != null) {
         rebuildData.field_2 = rebuildData.field_2.rebuild((rebuildField) {
@@ -256,9 +255,7 @@ class GridFieldContext {
       }
     });
 
-    if (_data.field_2 != _fieldNotifier?.value) {
-      _fieldNotifier?.value = _data.field_2;
-    }
+    _fieldNotifier.value = _data.field_2;
 
     FieldService.insertField(
       gridId: gridId,
@@ -283,16 +280,16 @@ class GridFieldContext {
     });
   }
 
-  void Function() addFieldListener(void Function(Field) callback) {
+  void Function() addFieldListener(void Function(GridFieldPB) callback) {
     listener() {
       callback(field);
     }
 
-    _fieldNotifier?.addListener(listener);
+    _fieldNotifier.addListener(listener);
     return listener;
   }
 
   void removeFieldListener(void Function() listener) {
-    _fieldNotifier?.removeListener(listener);
+    _fieldNotifier.removeListener(listener);
   }
 }

+ 57 - 0
frontend/app_flowy/lib/workspace/application/grid/field/field_type_option_edit_bloc.dart

@@ -0,0 +1,57 @@
+import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+import 'dart:async';
+
+import 'field_service.dart';
+
+part 'field_type_option_edit_bloc.freezed.dart';
+
+class FieldTypeOptionEditBloc extends Bloc<FieldTypeOptionEditEvent, FieldTypeOptionEditState> {
+  final TypeOptionDataController _dataController;
+  void Function()? _fieldListenFn;
+
+  FieldTypeOptionEditBloc(TypeOptionDataController dataController)
+      : _dataController = dataController,
+        super(FieldTypeOptionEditState.initial(dataController)) {
+    on<FieldTypeOptionEditEvent>(
+      (event, emit) async {
+        event.when(
+          initial: () {
+            _fieldListenFn = dataController.addFieldListener((field) {
+              add(FieldTypeOptionEditEvent.didReceiveFieldUpdated(field));
+            });
+          },
+          didReceiveFieldUpdated: (field) {
+            emit(state.copyWith(field: field));
+          },
+        );
+      },
+    );
+  }
+
+  @override
+  Future<void> close() async {
+    if (_fieldListenFn != null) {
+      _dataController.removeFieldListener(_fieldListenFn!);
+    }
+    return super.close();
+  }
+}
+
+@freezed
+class FieldTypeOptionEditEvent with _$FieldTypeOptionEditEvent {
+  const factory FieldTypeOptionEditEvent.initial() = _Initial;
+  const factory FieldTypeOptionEditEvent.didReceiveFieldUpdated(GridFieldPB field) = _DidReceiveFieldUpdated;
+}
+
+@freezed
+class FieldTypeOptionEditState with _$FieldTypeOptionEditState {
+  const factory FieldTypeOptionEditState({
+    required GridFieldPB field,
+  }) = _FieldTypeOptionEditState;
+
+  factory FieldTypeOptionEditState.initial(TypeOptionDataController fieldContext) => FieldTypeOptionEditState(
+        field: fieldContext.field,
+      );
+}

+ 4 - 4
frontend/app_flowy/lib/workspace/application/grid/field/grid_listenr.dart

@@ -1,13 +1,13 @@
+import 'package:app_flowy/core/grid_notification.dart';
 import 'package:dartz/dartz.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/dart_notification.pb.dart';
 import 'package:flowy_infra/notifier.dart';
 import 'dart:async';
 import 'dart:typed_data';
-import 'package:app_flowy/core/notification_helper.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
 
-typedef UpdateFieldNotifiedValue = Either<GridFieldChangeset, FlowyError>;
+typedef UpdateFieldNotifiedValue = Either<GridFieldChangesetPB, FlowyError>;
 
 class GridFieldsListener {
   final String gridId;
@@ -27,7 +27,7 @@ class GridFieldsListener {
     switch (ty) {
       case GridNotification.DidUpdateGridField:
         result.fold(
-          (payload) => updateFieldsNotifier?.value = left(GridFieldChangeset.fromBuffer(payload)),
+          (payload) => updateFieldsNotifier?.value = left(GridFieldChangesetPB.fromBuffer(payload)),
           (error) => updateFieldsNotifier?.value = right(error),
         );
         break;

+ 3 - 2
frontend/app_flowy/lib/workspace/application/grid/field/type_option/date_bloc.dart

@@ -1,14 +1,15 @@
 import 'package:app_flowy/workspace/application/grid/field/type_option/type_option_service.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option_entities.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
 import 'package:protobuf/protobuf.dart';
 part 'date_bloc.freezed.dart';
 
-typedef DateTypeOptionContext = TypeOptionContext<DateTypeOption>;
+typedef DateTypeOptionContext = TypeOptionWidgetContext<DateTypeOption>;
 
-class DateTypeOptionDataBuilder extends TypeOptionDataBuilder<DateTypeOption> {
+class DateTypeOptionDataParser extends TypeOptionDataParser<DateTypeOption> {
   @override
   DateTypeOption fromBuffer(List<int> buffer) {
     return DateTypeOption.fromBuffer(buffer);

+ 7 - 7
frontend/app_flowy/lib/workspace/application/grid/field/type_option/edit_select_option_bloc.dart

@@ -1,4 +1,4 @@
-import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
@@ -7,7 +7,7 @@ import 'package:dartz/dartz.dart';
 part 'edit_select_option_bloc.freezed.dart';
 
 class EditSelectOptionBloc extends Bloc<EditSelectOptionEvent, EditSelectOptionState> {
-  EditSelectOptionBloc({required SelectOption option}) : super(EditSelectOptionState.initial(option)) {
+  EditSelectOptionBloc({required SelectOptionPB option}) : super(EditSelectOptionState.initial(option)) {
     on<EditSelectOptionEvent>(
       (event, emit) async {
         event.map(
@@ -30,14 +30,14 @@ class EditSelectOptionBloc extends Bloc<EditSelectOptionEvent, EditSelectOptionS
     return super.close();
   }
 
-  SelectOption _updateColor(SelectOptionColor color) {
+  SelectOptionPB _updateColor(SelectOptionColorPB color) {
     state.option.freeze();
     return state.option.rebuild((option) {
       option.color = color;
     });
   }
 
-  SelectOption _updateName(String name) {
+  SelectOptionPB _updateName(String name) {
     state.option.freeze();
     return state.option.rebuild((option) {
       option.name = name;
@@ -48,18 +48,18 @@ class EditSelectOptionBloc extends Bloc<EditSelectOptionEvent, EditSelectOptionS
 @freezed
 class EditSelectOptionEvent with _$EditSelectOptionEvent {
   const factory EditSelectOptionEvent.updateName(String name) = _UpdateName;
-  const factory EditSelectOptionEvent.updateColor(SelectOptionColor color) = _UpdateColor;
+  const factory EditSelectOptionEvent.updateColor(SelectOptionColorPB color) = _UpdateColor;
   const factory EditSelectOptionEvent.delete() = _Delete;
 }
 
 @freezed
 class EditSelectOptionState with _$EditSelectOptionState {
   const factory EditSelectOptionState({
-    required SelectOption option,
+    required SelectOptionPB option,
     required Option<bool> deleted,
   }) = _EditSelectOptionState;
 
-  factory EditSelectOptionState.initial(SelectOption option) => EditSelectOptionState(
+  factory EditSelectOptionState.initial(SelectOptionPB option) => EditSelectOptionState(
         option: option,
         deleted: none(),
       );

+ 15 - 13
frontend/app_flowy/lib/workspace/application/grid/field/type_option/multi_select_type_option.dart

@@ -1,26 +1,28 @@
 import 'package:app_flowy/workspace/application/grid/field/field_service.dart';
 import 'package:flowy_sdk/log.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/multi_select_type_option.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart';
 import 'dart:async';
 import 'package:protobuf/protobuf.dart';
 import 'select_option_type_option_bloc.dart';
 import 'type_option_service.dart';
 
-class MultiSelectTypeOptionContext extends TypeOptionContext<MultiSelectTypeOption> with SelectOptionTypeOptionAction {
+class MultiSelectTypeOptionContext extends TypeOptionWidgetContext<MultiSelectTypeOption>
+    with SelectOptionTypeOptionAction {
   final TypeOptionService service;
 
   MultiSelectTypeOptionContext({
-    required MultiSelectTypeOptionDataBuilder dataBuilder,
-    required GridFieldContext fieldContext,
+    required MultiSelectTypeOptionWidgetDataParser dataBuilder,
+    required TypeOptionDataController dataController,
   })  : service = TypeOptionService(
-          gridId: fieldContext.gridId,
-          fieldId: fieldContext.field.id,
+          gridId: dataController.gridId,
+          fieldId: dataController.field.id,
         ),
-        super(dataBuilder: dataBuilder, fieldContext: fieldContext);
+        super(dataParser: dataBuilder, dataController: dataController);
 
   @override
-  List<SelectOption> Function(SelectOption) get deleteOption {
-    return (SelectOption option) {
+  List<SelectOptionPB> Function(SelectOptionPB) get deleteOption {
+    return (SelectOptionPB option) {
       typeOption.freeze();
       typeOption = typeOption.rebuild((typeOption) {
         final index = typeOption.options.indexWhere((element) => element.id == option.id);
@@ -33,7 +35,7 @@ class MultiSelectTypeOptionContext extends TypeOptionContext<MultiSelectTypeOpti
   }
 
   @override
-  Future<List<SelectOption>> Function(String) get insertOption {
+  Future<List<SelectOptionPB>> Function(String) get insertOption {
     return (String optionName) {
       return service.newOption(name: optionName).then((result) {
         return result.fold(
@@ -55,8 +57,8 @@ class MultiSelectTypeOptionContext extends TypeOptionContext<MultiSelectTypeOpti
   }
 
   @override
-  List<SelectOption> Function(SelectOption) get udpateOption {
-    return (SelectOption option) {
+  List<SelectOptionPB> Function(SelectOptionPB) get udpateOption {
+    return (SelectOptionPB option) {
       typeOption.freeze();
       typeOption = typeOption.rebuild((typeOption) {
         final index = typeOption.options.indexWhere((element) => element.id == option.id);
@@ -69,7 +71,7 @@ class MultiSelectTypeOptionContext extends TypeOptionContext<MultiSelectTypeOpti
   }
 }
 
-class MultiSelectTypeOptionDataBuilder extends TypeOptionDataBuilder<MultiSelectTypeOption> {
+class MultiSelectTypeOptionWidgetDataParser extends TypeOptionDataParser<MultiSelectTypeOption> {
   @override
   MultiSelectTypeOption fromBuffer(List<int> buffer) {
     return MultiSelectTypeOption.fromBuffer(buffer);

+ 2 - 2
frontend/app_flowy/lib/workspace/application/grid/field/type_option/number_bloc.dart

@@ -8,9 +8,9 @@ import 'package:protobuf/protobuf.dart';
 
 part 'number_bloc.freezed.dart';
 
-typedef NumberTypeOptionContext = TypeOptionContext<NumberTypeOption>;
+typedef NumberTypeOptionContext = TypeOptionWidgetContext<NumberTypeOption>;
 
-class NumberTypeOptionDataBuilder extends TypeOptionDataBuilder<NumberTypeOption> {
+class NumberTypeOptionWidgetDataParser extends TypeOptionDataParser<NumberTypeOption> {
   @override
   NumberTypeOption fromBuffer(List<int> buffer) {
     return NumberTypeOption.fromBuffer(buffer);

+ 1 - 1
frontend/app_flowy/lib/workspace/application/grid/field/type_option/number_format_bloc.dart

@@ -86,7 +86,7 @@ extension NumberFormatExtension on NumberFormat {
         return "New Zealand dollar";
       case NumberFormat.NorwegianKrone:
         return "Norwegian krone";
-      case NumberFormat.Number:
+      case NumberFormat.Num:
         return "Number";
       case NumberFormat.Percent:
         return "Percent";

+ 12 - 12
frontend/app_flowy/lib/workspace/application/grid/field/type_option/select_option_type_option_bloc.dart

@@ -1,4 +1,4 @@
-import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
@@ -6,25 +6,25 @@ import 'package:dartz/dartz.dart';
 part 'select_option_type_option_bloc.freezed.dart';
 
 abstract class SelectOptionTypeOptionAction {
-  Future<List<SelectOption>> Function(String) get insertOption;
+  Future<List<SelectOptionPB>> Function(String) get insertOption;
 
-  List<SelectOption> Function(SelectOption) get deleteOption;
+  List<SelectOptionPB> Function(SelectOptionPB) get deleteOption;
 
-  List<SelectOption> Function(SelectOption) get udpateOption;
+  List<SelectOptionPB> Function(SelectOptionPB) get udpateOption;
 }
 
 class SelectOptionTypeOptionBloc extends Bloc<SelectOptionTypeOptionEvent, SelectOptionTypeOptionState> {
   final SelectOptionTypeOptionAction typeOptionAction;
 
   SelectOptionTypeOptionBloc({
-    required List<SelectOption> options,
+    required List<SelectOptionPB> options,
     required this.typeOptionAction,
   }) : super(SelectOptionTypeOptionState.initial(options)) {
     on<SelectOptionTypeOptionEvent>(
       (event, emit) async {
         await event.when(
           createOption: (optionName) async {
-            final List<SelectOption> options = await typeOptionAction.insertOption(optionName);
+            final List<SelectOptionPB> options = await typeOptionAction.insertOption(optionName);
             emit(state.copyWith(options: options));
           },
           addingOption: () {
@@ -34,11 +34,11 @@ class SelectOptionTypeOptionBloc extends Bloc<SelectOptionTypeOptionEvent, Selec
             emit(state.copyWith(isEditingOption: false, newOptionName: none()));
           },
           updateOption: (option) {
-            final List<SelectOption> options = typeOptionAction.udpateOption(option);
+            final List<SelectOptionPB> options = typeOptionAction.udpateOption(option);
             emit(state.copyWith(options: options));
           },
           deleteOption: (option) {
-            final List<SelectOption> options = typeOptionAction.deleteOption(option);
+            final List<SelectOptionPB> options = typeOptionAction.deleteOption(option);
             emit(state.copyWith(options: options));
           },
         );
@@ -57,19 +57,19 @@ class SelectOptionTypeOptionEvent with _$SelectOptionTypeOptionEvent {
   const factory SelectOptionTypeOptionEvent.createOption(String optionName) = _CreateOption;
   const factory SelectOptionTypeOptionEvent.addingOption() = _AddingOption;
   const factory SelectOptionTypeOptionEvent.endAddingOption() = _EndAddingOption;
-  const factory SelectOptionTypeOptionEvent.updateOption(SelectOption option) = _UpdateOption;
-  const factory SelectOptionTypeOptionEvent.deleteOption(SelectOption option) = _DeleteOption;
+  const factory SelectOptionTypeOptionEvent.updateOption(SelectOptionPB option) = _UpdateOption;
+  const factory SelectOptionTypeOptionEvent.deleteOption(SelectOptionPB option) = _DeleteOption;
 }
 
 @freezed
 class SelectOptionTypeOptionState with _$SelectOptionTypeOptionState {
   const factory SelectOptionTypeOptionState({
-    required List<SelectOption> options,
+    required List<SelectOptionPB> options,
     required bool isEditingOption,
     required Option<String> newOptionName,
   }) = _SelectOptionTyepOptionState;
 
-  factory SelectOptionTypeOptionState.initial(List<SelectOption> options) => SelectOptionTypeOptionState(
+  factory SelectOptionTypeOptionState.initial(List<SelectOptionPB> options) => SelectOptionTypeOptionState(
         options: options,
         isEditingOption: false,
         newOptionName: none(),

+ 14 - 13
frontend/app_flowy/lib/workspace/application/grid/field/type_option/single_select_type_option.dart

@@ -1,27 +1,28 @@
 import 'package:app_flowy/workspace/application/grid/field/field_service.dart';
 import 'package:flowy_sdk/log.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/single_select_type_option.pb.dart';
 import 'dart:async';
 import 'package:protobuf/protobuf.dart';
 import 'select_option_type_option_bloc.dart';
 import 'type_option_service.dart';
 
-class SingleSelectTypeOptionContext extends TypeOptionContext<SingleSelectTypeOption>
+class SingleSelectTypeOptionContext extends TypeOptionWidgetContext<SingleSelectTypeOptionPB>
     with SelectOptionTypeOptionAction {
   final TypeOptionService service;
 
   SingleSelectTypeOptionContext({
-    required SingleSelectTypeOptionDataBuilder dataBuilder,
-    required GridFieldContext fieldContext,
+    required SingleSelectTypeOptionWidgetDataParser dataBuilder,
+    required TypeOptionDataController fieldContext,
   })  : service = TypeOptionService(
           gridId: fieldContext.gridId,
           fieldId: fieldContext.field.id,
         ),
-        super(dataBuilder: dataBuilder, fieldContext: fieldContext);
+        super(dataParser: dataBuilder, dataController: fieldContext);
 
   @override
-  List<SelectOption> Function(SelectOption) get deleteOption {
-    return (SelectOption option) {
+  List<SelectOptionPB> Function(SelectOptionPB) get deleteOption {
+    return (SelectOptionPB option) {
       typeOption.freeze();
       typeOption = typeOption.rebuild((typeOption) {
         final index = typeOption.options.indexWhere((element) => element.id == option.id);
@@ -34,7 +35,7 @@ class SingleSelectTypeOptionContext extends TypeOptionContext<SingleSelectTypeOp
   }
 
   @override
-  Future<List<SelectOption>> Function(String) get insertOption {
+  Future<List<SelectOptionPB>> Function(String) get insertOption {
     return (String optionName) {
       return service.newOption(name: optionName).then((result) {
         return result.fold(
@@ -56,8 +57,8 @@ class SingleSelectTypeOptionContext extends TypeOptionContext<SingleSelectTypeOp
   }
 
   @override
-  List<SelectOption> Function(SelectOption) get udpateOption {
-    return (SelectOption option) {
+  List<SelectOptionPB> Function(SelectOptionPB) get udpateOption {
+    return (SelectOptionPB option) {
       typeOption.freeze();
       typeOption = typeOption.rebuild((typeOption) {
         final index = typeOption.options.indexWhere((element) => element.id == option.id);
@@ -70,9 +71,9 @@ class SingleSelectTypeOptionContext extends TypeOptionContext<SingleSelectTypeOp
   }
 }
 
-class SingleSelectTypeOptionDataBuilder extends TypeOptionDataBuilder<SingleSelectTypeOption> {
+class SingleSelectTypeOptionWidgetDataParser extends TypeOptionDataParser<SingleSelectTypeOptionPB> {
   @override
-  SingleSelectTypeOption fromBuffer(List<int> buffer) {
-    return SingleSelectTypeOption.fromBuffer(buffer);
+  SingleSelectTypeOptionPB fromBuffer(List<int> buffer) {
+    return SingleSelectTypeOptionPB.fromBuffer(buffer);
   }
 }

+ 18 - 19
frontend/app_flowy/lib/workspace/application/grid/field/type_option/type_option_service.dart

@@ -4,10 +4,9 @@ import 'package:app_flowy/workspace/application/grid/field/field_service.dart';
 import 'package:dartz/dartz.dart';
 import 'package:flowy_sdk/dispatch/dispatch.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/cell_entities.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart';
 import 'package:protobuf/protobuf.dart';
 
 class TypeOptionService {
@@ -19,14 +18,14 @@ class TypeOptionService {
     required this.fieldId,
   });
 
-  Future<Either<SelectOption, FlowyError>> newOption({
+  Future<Either<SelectOptionPB, FlowyError>> newOption({
     required String name,
   }) {
-    final fieldIdentifier = FieldIdentifierPayload.create()
+    final fieldIdentifier = GridFieldIdentifierPayloadPB.create()
       ..gridId = gridId
       ..fieldId = fieldId;
 
-    final payload = CreateSelectOptionPayload.create()
+    final payload = CreateSelectOptionPayloadPB.create()
       ..optionName = name
       ..fieldIdentifier = fieldIdentifier;
 
@@ -34,36 +33,36 @@ class TypeOptionService {
   }
 }
 
-abstract class TypeOptionDataBuilder<T> {
+abstract class TypeOptionDataParser<T> {
   T fromBuffer(List<int> buffer);
 }
 
-class TypeOptionContext<T extends GeneratedMessage> {
+class TypeOptionWidgetContext<T extends GeneratedMessage> {
   T? _typeOptionObject;
-  final GridFieldContext _fieldContext;
-  final TypeOptionDataBuilder<T> dataBuilder;
+  final TypeOptionDataController _dataController;
+  final TypeOptionDataParser<T> dataParser;
 
-  TypeOptionContext({
-    required this.dataBuilder,
-    required GridFieldContext fieldContext,
-  }) : _fieldContext = fieldContext;
+  TypeOptionWidgetContext({
+    required this.dataParser,
+    required TypeOptionDataController dataController,
+  }) : _dataController = dataController;
 
-  String get gridId => _fieldContext.gridId;
+  String get gridId => _dataController.gridId;
 
-  Field get field => _fieldContext.field;
+  GridFieldPB get field => _dataController.field;
 
   T get typeOption {
     if (_typeOptionObject != null) {
       return _typeOptionObject!;
     }
 
-    final T object = dataBuilder.fromBuffer(_fieldContext.typeOptionData);
+    final T object = dataParser.fromBuffer(_dataController.typeOptionData);
     _typeOptionObject = object;
     return object;
   }
 
   set typeOption(T typeOption) {
-    _fieldContext.typeOptionData = typeOption.writeToBuffer();
+    _dataController.typeOptionData = typeOption.writeToBuffer();
     _typeOptionObject = typeOption;
   }
 }
@@ -75,10 +74,10 @@ abstract class TypeOptionFieldDelegate {
 
 class TypeOptionContext2<T> {
   final String gridId;
-  final Field field;
+  final GridFieldPB field;
   final FieldService _fieldService;
   T? _data;
-  final TypeOptionDataBuilder dataBuilder;
+  final TypeOptionDataParser dataBuilder;
 
   TypeOptionContext2({
     required this.gridId,

+ 73 - 44
frontend/app_flowy/lib/workspace/application/grid/grid_bloc.dart

@@ -1,12 +1,13 @@
 import 'dart:async';
 import 'package:dartz/dartz.dart';
 import 'package:equatable/equatable.dart';
+import 'package:flowy_sdk/log.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid-data-model/protobuf.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
-import 'cell/cell_service/cell_service.dart';
+import 'block/block_cache.dart';
 import 'grid_service.dart';
 import 'row/row_service.dart';
 import 'dart:collection';
@@ -14,25 +15,27 @@ import 'dart:collection';
 part 'grid_bloc.freezed.dart';
 
 class GridBloc extends Bloc<GridEvent, GridState> {
+  final String gridId;
   final GridService _gridService;
   final GridFieldCache fieldCache;
-  late final GridRowCache rowCache;
-  late final GridCellCache cellCache;
 
-  GridBloc({required View view})
-      : _gridService = GridService(gridId: view.id),
-        fieldCache = GridFieldCache(gridId: view.id),
-        super(GridState.initial(view.id)) {
-    rowCache = GridRowCache(
-      gridId: view.id,
-      fieldDelegate: GridRowCacheDelegateImpl(fieldCache),
-    );
+  // key: the block id
+  final LinkedHashMap<String, GridBlockCache> _blocks;
 
-    cellCache = GridCellCache(
-      gridId: view.id,
-      fieldDelegate: GridCellCacheDelegateImpl(fieldCache),
-    );
+  List<GridRowInfo> get rowInfos {
+    final List<GridRowInfo> rows = [];
+    for (var block in _blocks.values) {
+      rows.addAll(block.rows);
+    }
+    return rows;
+  }
 
+  GridBloc({required ViewPB view})
+      : gridId = view.id,
+        _blocks = LinkedHashMap.identity(),
+        _gridService = GridService(gridId: view.id),
+        fieldCache = GridFieldCache(gridId: view.id),
+        super(GridState.initial(view.id)) {
     on<GridEvent>(
       (event, emit) async {
         await event.when(
@@ -43,11 +46,11 @@ class GridBloc extends Bloc<GridEvent, GridState> {
           createRow: () {
             _gridService.createRow();
           },
-          didReceiveRowUpdate: (rows, listState) {
-            emit(state.copyWith(rows: rows, listState: listState));
+          didReceiveRowUpdate: (newRowInfos, reason) {
+            emit(state.copyWith(rowInfos: newRowInfos, reason: reason));
           },
           didReceiveFieldUpdate: (fields) {
-            emit(state.copyWith(rows: rowCache.clonedRows, fields: GridFieldEquatable(fields)));
+            emit(state.copyWith(rowInfos: rowInfos, fields: GridFieldEquatable(fields)));
           },
         );
       },
@@ -57,21 +60,23 @@ class GridBloc extends Bloc<GridEvent, GridState> {
   @override
   Future<void> close() async {
     await _gridService.closeGrid();
-    await cellCache.dispose();
-    await rowCache.dispose();
     await fieldCache.dispose();
+
+    for (final blockCache in _blocks.values) {
+      blockCache.dispose();
+    }
     return super.close();
   }
 
+  GridRowCache? getRowCache(String blockId, String rowId) {
+    final GridBlockCache? blockCache = _blocks[blockId];
+    return blockCache?.rowCache;
+  }
+
   void _startListening() {
     fieldCache.addListener(
       listenWhen: () => !isClosed,
-      onChanged: (fields) => add(GridEvent.didReceiveFieldUpdate(fields)),
-    );
-
-    rowCache.addListener(
-      listenWhen: () => !isClosed,
-      onChanged: (rows, listState) => add(GridEvent.didReceiveRowUpdate(rowCache.clonedRows, listState)),
+      onFields: (fields) => add(GridEvent.didReceiveFieldUpdate(fields)),
     );
   }
 
@@ -79,24 +84,26 @@ class GridBloc extends Bloc<GridEvent, GridState> {
     final result = await _gridService.loadGrid();
     return Future(
       () => result.fold(
-        (grid) async => await _loadFields(grid, emit),
+        (grid) async {
+          _initialBlocks(grid.blocks);
+          await _loadFields(grid, emit);
+        },
         (err) => emit(state.copyWith(loadingState: GridLoadingState.finish(right(err)))),
       ),
     );
   }
 
-  Future<void> _loadFields(Grid grid, Emitter<GridState> emit) async {
-    final result = await _gridService.getFields(fieldOrders: grid.fieldOrders);
+  Future<void> _loadFields(GridPB grid, Emitter<GridState> emit) async {
+    final result = await _gridService.getFields(fieldIds: grid.fields);
     return Future(
       () => result.fold(
         (fields) {
           fieldCache.fields = fields.items;
-          rowCache.resetRows(grid.blockOrders);
 
           emit(state.copyWith(
             grid: Some(grid),
             fields: GridFieldEquatable(fieldCache.fields),
-            rows: rowCache.clonedRows,
+            rowInfos: rowInfos,
             loadingState: GridLoadingState.finish(left(unit)),
           ));
         },
@@ -104,34 +111,57 @@ class GridBloc extends Bloc<GridEvent, GridState> {
       ),
     );
   }
+
+  void _initialBlocks(List<GridBlockPB> blocks) {
+    for (final block in blocks) {
+      if (_blocks[block.id] != null) {
+        Log.warn("Intial duplicate block's cache: ${block.id}");
+        return;
+      }
+
+      final cache = GridBlockCache(
+        gridId: gridId,
+        block: block,
+        fieldCache: fieldCache,
+      );
+
+      cache.addListener(
+        listenWhen: () => !isClosed,
+        onChangeReason: (reason) => add(GridEvent.didReceiveRowUpdate(rowInfos, reason)),
+      );
+
+      _blocks[block.id] = cache;
+    }
+  }
 }
 
 @freezed
 class GridEvent with _$GridEvent {
   const factory GridEvent.initial() = InitialGrid;
   const factory GridEvent.createRow() = _CreateRow;
-  const factory GridEvent.didReceiveRowUpdate(List<GridRow> rows, GridRowChangeReason listState) = _DidReceiveRowUpdate;
-  const factory GridEvent.didReceiveFieldUpdate(List<Field> fields) = _DidReceiveFieldUpdate;
+  const factory GridEvent.didReceiveRowUpdate(List<GridRowInfo> rows, GridRowChangeReason listState) =
+      _DidReceiveRowUpdate;
+  const factory GridEvent.didReceiveFieldUpdate(List<GridFieldPB> fields) = _DidReceiveFieldUpdate;
 }
 
 @freezed
 class GridState with _$GridState {
   const factory GridState({
     required String gridId,
-    required Option<Grid> grid,
+    required Option<GridPB> grid,
     required GridFieldEquatable fields,
-    required List<GridRow> rows,
+    required List<GridRowInfo> rowInfos,
     required GridLoadingState loadingState,
-    required GridRowChangeReason listState,
+    required GridRowChangeReason reason,
   }) = _GridState;
 
   factory GridState.initial(String gridId) => GridState(
         fields: const GridFieldEquatable([]),
-        rows: [],
+        rowInfos: [],
         grid: none(),
         gridId: gridId,
         loadingState: const _Loading(),
-        listState: const InitialListState(),
+        reason: const InitialListState(),
       );
 }
 
@@ -142,9 +172,8 @@ class GridLoadingState with _$GridLoadingState {
 }
 
 class GridFieldEquatable extends Equatable {
-  final List<Field> _fields;
-
-  const GridFieldEquatable(List<Field> fields) : _fields = fields;
+  final List<GridFieldPB> _fields;
+  const GridFieldEquatable(List<GridFieldPB> fields) : _fields = fields;
 
   @override
   List<Object?> get props {
@@ -154,5 +183,5 @@ class GridFieldEquatable extends Equatable {
     ];
   }
 
-  UnmodifiableListView<Field> get value => UnmodifiableListView(_fields);
+  UnmodifiableListView<GridFieldPB> get value => UnmodifiableListView(_fields);
 }

+ 8 - 8
frontend/app_flowy/lib/workspace/application/grid/grid_header_bloc.dart

@@ -1,6 +1,6 @@
 import 'package:app_flowy/workspace/application/grid/field/field_service.dart';
 import 'package:flowy_sdk/log.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
@@ -34,7 +34,7 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
   }
 
   Future<void> _moveField(_MoveField value, Emitter<GridHeaderState> emit) async {
-    final fields = List<Field>.from(state.fields);
+    final fields = List<GridFieldPB>.from(state.fields);
     fields.insert(value.toIndex, fields.removeAt(value.fromIndex));
     emit(state.copyWith(fields: fields));
 
@@ -48,7 +48,7 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
 
   Future<void> _startListening() async {
     fieldCache.addListener(
-      onChanged: (fields) => add(GridHeaderEvent.didReceiveFieldUpdate(fields)),
+      onFields: (fields) => add(GridHeaderEvent.didReceiveFieldUpdate(fields)),
       listenWhen: () => !isClosed,
     );
   }
@@ -62,16 +62,16 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
 @freezed
 class GridHeaderEvent with _$GridHeaderEvent {
   const factory GridHeaderEvent.initial() = _InitialHeader;
-  const factory GridHeaderEvent.didReceiveFieldUpdate(List<Field> fields) = _DidReceiveFieldUpdate;
-  const factory GridHeaderEvent.moveField(Field field, int fromIndex, int toIndex) = _MoveField;
+  const factory GridHeaderEvent.didReceiveFieldUpdate(List<GridFieldPB> fields) = _DidReceiveFieldUpdate;
+  const factory GridHeaderEvent.moveField(GridFieldPB field, int fromIndex, int toIndex) = _MoveField;
 }
 
 @freezed
 class GridHeaderState with _$GridHeaderState {
-  const factory GridHeaderState({required List<Field> fields}) = _GridHeaderState;
+  const factory GridHeaderState({required List<GridFieldPB> fields}) = _GridHeaderState;
 
-  factory GridHeaderState.initial(List<Field> fields) {
-    // final List<Field> newFields = List.from(fields);
+  factory GridHeaderState.initial(List<GridFieldPB> fields) {
+    // final List<GridFieldPB> newFields = List.from(fields);
     // newFields.retainWhere((field) => field.visibility);
     return GridHeaderState(fields: fields);
   }

+ 0 - 42
frontend/app_flowy/lib/workspace/application/grid/grid_listener.dart

@@ -1,42 +0,0 @@
-import 'package:dartz/dartz.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid/dart_notification.pb.dart';
-import 'package:flowy_infra/notifier.dart';
-import 'dart:async';
-import 'dart:typed_data';
-import 'package:app_flowy/core/notification_helper.dart';
-
-class GridRowListener {
-  final String gridId;
-  PublishNotifier<Either<List<GridRowsChangeset>, FlowyError>> rowsUpdateNotifier = PublishNotifier(comparable: null);
-  GridNotificationListener? _listener;
-
-  GridRowListener({required this.gridId});
-
-  void start() {
-    _listener = GridNotificationListener(
-      objectId: gridId,
-      handler: _handler,
-    );
-  }
-
-  void _handler(GridNotification ty, Either<Uint8List, FlowyError> result) {
-    switch (ty) {
-      case GridNotification.DidUpdateGridRow:
-        result.fold(
-          (payload) => rowsUpdateNotifier.value = left([GridRowsChangeset.fromBuffer(payload)]),
-          (error) => rowsUpdateNotifier.value = right(error),
-        );
-        break;
-
-      default:
-        break;
-    }
-  }
-
-  Future<void> stop() async {
-    await _listener?.stop();
-    rowsUpdateNotifier.dispose();
-  }
-}

+ 90 - 78
frontend/app_flowy/lib/workspace/application/grid/grid_service.dart

@@ -5,10 +5,12 @@ import 'package:dartz/dartz.dart';
 import 'package:flowy_sdk/dispatch/dispatch.dart';
 import 'package:flowy_sdk/log.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/grid_entities.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/row_entities.pb.dart';
 import 'package:flutter/foundation.dart';
-import 'cell/cell_service/cell_service.dart';
 import 'row/row_service.dart';
 
 class GridService {
@@ -17,60 +19,61 @@ class GridService {
     required this.gridId,
   });
 
-  Future<Either<Grid, FlowyError>> loadGrid() async {
-    await FolderEventSetLatestView(ViewId(value: gridId)).send();
+  Future<Either<GridPB, FlowyError>> loadGrid() async {
+    await FolderEventSetLatestView(ViewIdPB(value: gridId)).send();
 
-    final payload = GridId(value: gridId);
-    return GridEventGetGridData(payload).send();
+    final payload = GridIdPB(value: gridId);
+    return GridEventGetGrid(payload).send();
   }
 
-  Future<Either<Row, FlowyError>> createRow({Option<String>? startRowId}) {
-    CreateRowPayload payload = CreateRowPayload.create()..gridId = gridId;
+  Future<Either<GridRowPB, FlowyError>> createRow({Option<String>? startRowId}) {
+    CreateRowPayloadPB payload = CreateRowPayloadPB.create()..gridId = gridId;
     startRowId?.fold(() => null, (id) => payload.startRowId = id);
     return GridEventCreateRow(payload).send();
   }
 
-  Future<Either<RepeatedField, FlowyError>> getFields({required List<FieldOrder> fieldOrders}) {
-    final payload = QueryFieldPayload.create()
+  Future<Either<RepeatedGridFieldPB, FlowyError>> getFields({required List<GridFieldIdPB> fieldIds}) {
+    final payload = QueryFieldPayloadPB.create()
       ..gridId = gridId
-      ..fieldOrders = RepeatedFieldOrder(items: fieldOrders);
+      ..fieldIds = RepeatedGridFieldIdPB(items: fieldIds);
     return GridEventGetFields(payload).send();
   }
 
   Future<Either<Unit, FlowyError>> closeGrid() {
-    final request = ViewId(value: gridId);
+    final request = ViewIdPB(value: gridId);
     return FolderEventCloseView(request).send();
   }
 }
 
 class FieldsNotifier extends ChangeNotifier {
-  List<Field> _fields = [];
+  List<GridFieldPB> _fields = [];
 
-  set fields(List<Field> fields) {
+  set fields(List<GridFieldPB> fields) {
     _fields = fields;
     notifyListeners();
   }
 
-  List<Field> get fields => _fields;
+  List<GridFieldPB> get fields => _fields;
 }
 
-typedef ChangesetListener = void Function(GridFieldChangeset);
+typedef FieldChangesetCallback = void Function(GridFieldChangesetPB);
+typedef FieldsCallback = void Function(List<GridFieldPB>);
 
 class GridFieldCache {
   final String gridId;
-  late final GridFieldsListener _fieldListener;
+  final GridFieldsListener _fieldListener;
   FieldsNotifier? _fieldNotifier = FieldsNotifier();
-  final List<ChangesetListener> _changesetListener = [];
+  final Map<FieldsCallback, VoidCallback> _fieldsCallbackMap = {};
+  final Map<FieldChangesetCallback, FieldChangesetCallback> _changesetCallbackMap = {};
 
-  GridFieldCache({required this.gridId}) {
-    _fieldListener = GridFieldsListener(gridId: gridId);
+  GridFieldCache({required this.gridId}) : _fieldListener = GridFieldsListener(gridId: gridId) {
     _fieldListener.start(onFieldsChanged: (result) {
       result.fold(
         (changeset) {
           _deleteFields(changeset.deletedFields);
           _insertFields(changeset.insertedFields);
           _updateFields(changeset.updatedFields);
-          for (final listener in _changesetListener) {
+          for (final listener in _changesetCallbackMap.values) {
             listener(changeset);
           }
         },
@@ -85,55 +88,65 @@ class GridFieldCache {
     _fieldNotifier = null;
   }
 
-  UnmodifiableListView<Field> get unmodifiableFields => UnmodifiableListView(_fieldNotifier?.fields ?? []);
+  UnmodifiableListView<GridFieldPB> get unmodifiableFields => UnmodifiableListView(_fieldNotifier?.fields ?? []);
 
-  List<Field> get fields => [..._fieldNotifier?.fields ?? []];
+  List<GridFieldPB> get fields => [..._fieldNotifier?.fields ?? []];
 
-  set fields(List<Field> fields) {
+  set fields(List<GridFieldPB> fields) {
     _fieldNotifier?.fields = [...fields];
   }
 
-  VoidCallback addListener(
-      {VoidCallback? listener, void Function(List<Field>)? onChanged, bool Function()? listenWhen}) {
-    f() {
-      if (listenWhen != null && listenWhen() == false) {
-        return;
+  void addListener({
+    FieldsCallback? onFields,
+    FieldChangesetCallback? onChangeset,
+    bool Function()? listenWhen,
+  }) {
+    if (onChangeset != null) {
+      fn(c) {
+        if (listenWhen != null && listenWhen() == false) {
+          return;
+        }
+        onChangeset(c);
       }
 
-      if (onChanged != null) {
-        onChanged(fields);
-      }
-
-      if (listener != null) {
-        listener();
-      }
+      _changesetCallbackMap[onChangeset] = fn;
     }
 
-    _fieldNotifier?.addListener(f);
-    return f;
-  }
+    if (onFields != null) {
+      fn() {
+        if (listenWhen != null && listenWhen() == false) {
+          return;
+        }
+        onFields(fields);
+      }
 
-  void removeListener(VoidCallback f) {
-    _fieldNotifier?.removeListener(f);
+      _fieldsCallbackMap[onFields] = fn;
+      _fieldNotifier?.addListener(fn);
+    }
   }
 
-  void addChangesetListener(ChangesetListener listener) {
-    _changesetListener.add(listener);
-  }
+  void removeListener({
+    FieldsCallback? onFieldsListener,
+    FieldChangesetCallback? onChangsetListener,
+  }) {
+    if (onFieldsListener != null) {
+      final fn = _fieldsCallbackMap.remove(onFieldsListener);
+      if (fn != null) {
+        _fieldNotifier?.removeListener(fn);
+      }
+    }
 
-  void removeChangesetListener(ChangesetListener listener) {
-    final index = _changesetListener.indexWhere((element) => element == listener);
-    if (index != -1) {
-      _changesetListener.removeAt(index);
+    if (onChangsetListener != null) {
+      _changesetCallbackMap.remove(onChangsetListener);
     }
   }
 
-  void _deleteFields(List<FieldOrder> deletedFields) {
+  void _deleteFields(List<GridFieldIdPB> deletedFields) {
     if (deletedFields.isEmpty) {
       return;
     }
-    final List<Field> newFields = fields;
-    final Map<String, FieldOrder> deletedFieldMap = {
+    final List<GridFieldPB> newFields = fields;
+    final Map<String, GridFieldIdPB> deletedFieldMap = {
       for (var fieldOrder in deletedFields) fieldOrder.fieldId: fieldOrder
     };
 
@@ -141,11 +154,11 @@ class GridFieldCache {
     _fieldNotifier?.fields = newFields;
   }
 
-  void _insertFields(List<IndexField> insertedFields) {
+  void _insertFields(List<IndexFieldPB> insertedFields) {
     if (insertedFields.isEmpty) {
       return;
     }
-    final List<Field> newFields = fields;
+    final List<GridFieldPB> newFields = fields;
     for (final indexField in insertedFields) {
       if (newFields.length > indexField.index) {
         newFields.insert(indexField.index, indexField.field_1);
@@ -156,11 +169,11 @@ class GridFieldCache {
     _fieldNotifier?.fields = newFields;
   }
 
-  void _updateFields(List<Field> updatedFields) {
+  void _updateFields(List<GridFieldPB> updatedFields) {
     if (updatedFields.isEmpty) {
       return;
     }
-    final List<Field> newFields = fields;
+    final List<GridFieldPB> newFields = fields;
     for (final updatedField in updatedFields) {
       final index = newFields.indexWhere((field) => field.id == updatedField.id);
       if (index != -1) {
@@ -172,43 +185,42 @@ class GridFieldCache {
   }
 }
 
-class GridRowCacheDelegateImpl extends GridRowFieldDelegate {
+class GridRowCacheFieldNotifierImpl extends GridRowCacheFieldNotifier {
   final GridFieldCache _cache;
-  GridRowCacheDelegateImpl(GridFieldCache cache) : _cache = cache;
+  FieldChangesetCallback? _onChangesetFn;
+  FieldsCallback? _onFieldFn;
+  GridRowCacheFieldNotifierImpl(GridFieldCache cache) : _cache = cache;
 
   @override
-  UnmodifiableListView<Field> get fields => _cache.unmodifiableFields;
+  UnmodifiableListView<GridFieldPB> get fields => _cache.unmodifiableFields;
 
   @override
-  void onFieldChanged(FieldDidUpdateCallback callback) {
-    _cache.addListener(listener: () {
-      callback();
-    });
+  void onFieldsChanged(VoidCallback callback) {
+    _onFieldFn = (_) => callback();
+    _cache.addListener(onFields: _onFieldFn);
   }
-}
-
-class GridCellCacheDelegateImpl extends GridCellFieldDelegate {
-  final GridFieldCache _cache;
-  ChangesetListener? _changesetFn;
-  GridCellCacheDelegateImpl(GridFieldCache cache) : _cache = cache;
 
   @override
-  void onFieldChanged(void Function(String) callback) {
-    changesetFn(GridFieldChangeset changeset) {
+  void onFieldChanged(void Function(GridFieldPB) callback) {
+    _onChangesetFn = (GridFieldChangesetPB changeset) {
       for (final updatedField in changeset.updatedFields) {
-        callback(updatedField.id);
+        callback(updatedField);
       }
-    }
+    };
 
-    _cache.addChangesetListener(changesetFn);
-    _changesetFn = changesetFn;
+    _cache.addListener(onChangeset: _onChangesetFn);
   }
 
   @override
   void dispose() {
-    if (_changesetFn != null) {
-      _cache.removeChangesetListener(_changesetFn!);
-      _changesetFn = null;
+    if (_onFieldFn != null) {
+      _cache.removeListener(onFieldsListener: _onFieldFn!);
+      _onFieldFn = null;
+    }
+
+    if (_onChangesetFn != null) {
+      _cache.removeListener(onChangsetListener: _onChangesetFn!);
+      _onChangesetFn = null;
     }
   }
 }

+ 4 - 4
frontend/app_flowy/lib/workspace/application/grid/prelude.dart

@@ -4,18 +4,18 @@ export 'row/row_service.dart';
 export 'grid_service.dart';
 export 'grid_header_bloc.dart';
 
-// Field
+// GridFieldPB
 export 'field/field_service.dart';
 export 'field/field_action_sheet_bloc.dart';
 export 'field/field_editor_bloc.dart';
-export 'field/field_editor_pannel_bloc.dart';
+export 'field/field_type_option_edit_bloc.dart';
 
-// Field Type Option
+// GridFieldPB Type Option
 export 'field/type_option/date_bloc.dart';
 export 'field/type_option/number_bloc.dart';
 export 'field/type_option/single_select_type_option.dart';
 
-// Cell
+// GridCellPB
 export 'cell/text_cell_bloc.dart';
 export 'cell/number_cell_bloc.dart';
 export 'cell/select_option_cell_bloc.dart';

部分文件因文件數量過多而無法顯示