Browse Source

Merge branch 'main' into #692

Naughtz 2 năm trước cách đây
mục cha
commit
6dca0fddf9
100 tập tin đã thay đổi với 4136 bổ sung1400 xóa
  1. 19 13
      .github/workflows/ci.yaml
  2. 28 7
      .github/workflows/dart_lint.yml
  3. 26 25
      .github/workflows/dart_test.yml
  4. 36 0
      .github/workflows/flowy_editor_test.yml
  5. 2 2
      frontend/.vscode/launch.json
  6. 1 1
      frontend/app_flowy/analysis_options.yaml
  7. 32 0
      frontend/app_flowy/assets/images/emoji/1F42F.svg
  8. 24 0
      frontend/app_flowy/assets/images/emoji/1F431.svg
  9. 28 0
      frontend/app_flowy/assets/images/emoji/1F435.svg
  10. 28 0
      frontend/app_flowy/assets/images/emoji/1F43A.svg
  11. 17 0
      frontend/app_flowy/assets/images/emoji/1F600.svg
  12. 21 0
      frontend/app_flowy/assets/images/emoji/1F984.svg
  13. 33 0
      frontend/app_flowy/assets/images/emoji/1F9CC.svg
  14. 26 0
      frontend/app_flowy/assets/images/emoji/1F9DB.svg
  15. 39 0
      frontend/app_flowy/assets/images/emoji/1F9DD-200D-2642-FE0F.svg
  16. 28 0
      frontend/app_flowy/assets/images/emoji/1F9DE-200D-2642-FE0F.svg
  17. 28 0
      frontend/app_flowy/assets/images/emoji/1F9DF.svg
  18. 1 1
      frontend/app_flowy/lib/plugins/blank/blank.dart
  19. 290 0
      frontend/app_flowy/lib/plugins/board/application/board_bloc.dart
  20. 142 0
      frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart
  21. 71 0
      frontend/app_flowy/lib/plugins/board/application/card/board_checkbox_cell_bloc.dart
  22. 85 0
      frontend/app_flowy/lib/plugins/board/application/card/board_date_cell_bloc.dart
  23. 67 0
      frontend/app_flowy/lib/plugins/board/application/card/board_number_cell_bloc.dart
  24. 76 0
      frontend/app_flowy/lib/plugins/board/application/card/board_select_option_cell_bloc.dart
  25. 66 0
      frontend/app_flowy/lib/plugins/board/application/card/board_text_cell_bloc.dart
  26. 78 0
      frontend/app_flowy/lib/plugins/board/application/card/board_url_cell_bloc.dart
  27. 116 0
      frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart
  28. 49 0
      frontend/app_flowy/lib/plugins/board/application/card/card_data_controller.dart
  29. 20 0
      frontend/app_flowy/lib/plugins/board/application/group.dart
  30. 63 0
      frontend/app_flowy/lib/plugins/board/application/group_controller.dart
  31. 51 0
      frontend/app_flowy/lib/plugins/board/application/group_listener.dart
  32. 6 3
      frontend/app_flowy/lib/plugins/board/board.dart
  33. 162 5
      frontend/app_flowy/lib/plugins/board/presentation/board_page.dart
  34. 59 0
      frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart
  35. 59 0
      frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart
  36. 59 0
      frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart
  37. 63 0
      frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart
  38. 61 0
      frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart
  39. 66 0
      frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart
  40. 98 0
      frontend/app_flowy/lib/plugins/board/presentation/card/card.dart
  41. 69 0
      frontend/app_flowy/lib/plugins/board/presentation/card/card_cell_builder.dart
  42. 142 0
      frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart
  43. 3 3
      frontend/app_flowy/lib/plugins/doc/document.dart
  44. 13 5
      frontend/app_flowy/lib/plugins/doc/presentation/banner.dart
  45. 7 7
      frontend/app_flowy/lib/plugins/grid/application/block/block_cache.dart
  46. 10 6
      frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_field_notifier.dart
  47. 3 4
      frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_service.dart
  48. 22 18
      frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart
  49. 10 8
      frontend/app_flowy/lib/plugins/grid/application/cell/checkbox_cell_bloc.dart
  50. 19 19
      frontend/app_flowy/lib/plugins/grid/application/cell/date_cal_bloc.dart
  51. 15 10
      frontend/app_flowy/lib/plugins/grid/application/cell/date_cell_bloc.dart
  52. 12 9
      frontend/app_flowy/lib/plugins/grid/application/cell/number_cell_bloc.dart
  53. 6 6
      frontend/app_flowy/lib/plugins/grid/application/cell/select_option_cell_bloc.dart
  54. 1 1
      frontend/app_flowy/lib/plugins/grid/application/cell/select_option_editor_bloc.dart
  55. 2 2
      frontend/app_flowy/lib/plugins/grid/application/cell/select_option_service.dart
  56. 9 8
      frontend/app_flowy/lib/plugins/grid/application/cell/text_cell_bloc.dart
  57. 9 8
      frontend/app_flowy/lib/plugins/grid/application/cell/url_cell_bloc.dart
  58. 9 8
      frontend/app_flowy/lib/plugins/grid/application/cell/url_cell_editor_bloc.dart
  59. 9 5
      frontend/app_flowy/lib/plugins/grid/application/field/field_action_sheet_bloc.dart
  60. 192 0
      frontend/app_flowy/lib/plugins/grid/application/field/field_cache.dart
  61. 2 2
      frontend/app_flowy/lib/plugins/grid/application/field/field_cell_bloc.dart
  62. 12 7
      frontend/app_flowy/lib/plugins/grid/application/field/field_editor_bloc.dart
  63. 7 4
      frontend/app_flowy/lib/plugins/grid/application/field/field_listener.dart
  64. 6 162
      frontend/app_flowy/lib/plugins/grid/application/field/field_service.dart
  65. 9 6
      frontend/app_flowy/lib/plugins/grid/application/field/field_type_option_edit_bloc.dart
  66. 7 4
      frontend/app_flowy/lib/plugins/grid/application/field/grid_listener.dart
  67. 5 13
      frontend/app_flowy/lib/plugins/grid/application/field/type_option/date_bloc.dart
  68. 22 23
      frontend/app_flowy/lib/plugins/grid/application/field/type_option/multi_select_type_option.dart
  69. 4 14
      frontend/app_flowy/lib/plugins/grid/application/field/type_option/number_bloc.dart
  70. 22 13
      frontend/app_flowy/lib/plugins/grid/application/field/type_option/select_option_type_option_bloc.dart
  71. 18 22
      frontend/app_flowy/lib/plugins/grid/application/field/type_option/single_select_type_option.dart
  72. 195 0
      frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_context.dart
  73. 123 0
      frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_data_controller.dart
  74. 2 83
      frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_service.dart
  75. 68 94
      frontend/app_flowy/lib/plugins/grid/application/grid_bloc.dart
  76. 130 0
      frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart
  77. 9 8
      frontend/app_flowy/lib/plugins/grid/application/grid_header_bloc.dart
  78. 20 197
      frontend/app_flowy/lib/plugins/grid/application/grid_service.dart
  79. 2 2
      frontend/app_flowy/lib/plugins/grid/application/prelude.dart
  80. 13 12
      frontend/app_flowy/lib/plugins/grid/application/row/row_action_sheet_bloc.dart
  81. 32 35
      frontend/app_flowy/lib/plugins/grid/application/row/row_bloc.dart
  82. 328 0
      frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart
  83. 49 0
      frontend/app_flowy/lib/plugins/grid/application/row/row_data_controller.dart
  84. 15 25
      frontend/app_flowy/lib/plugins/grid/application/row/row_detail_bloc.dart
  85. 6 4
      frontend/app_flowy/lib/plugins/grid/application/row/row_listener.dart
  86. 28 345
      frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart
  87. 7 6
      frontend/app_flowy/lib/plugins/grid/application/setting/property_bloc.dart
  88. 5 2
      frontend/app_flowy/lib/plugins/grid/grid.dart
  89. 6 5
      frontend/app_flowy/lib/plugins/grid/presentation/controller/grid_scroll.dart
  90. 62 22
      frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart
  91. 7 3
      frontend/app_flowy/lib/plugins/grid/presentation/layout/layout.dart
  92. 2 12
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_accessory.dart
  93. 55 31
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_builder.dart
  94. 20 16
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_container.dart
  95. 10 8
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_shortcuts.dart
  96. 3 2
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/checkbox_cell.dart
  97. 3 3
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_cell.dart
  98. 23 21
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_editor.dart
  99. 9 7
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/number_cell.dart
  100. 4 3
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart

+ 19 - 13
.github/workflows/ci.yaml

@@ -3,11 +3,11 @@ name: CI
 on:
   push:
     branches:
-      - 'main'
-      
+      - "main"
+
   pull_request:
     branches:
-      - 'main'
+      - "main"
 
 jobs:
   build:
@@ -23,36 +23,37 @@ jobs:
 
     steps:
       - uses: actions/checkout@v2
-      
+
       - id: rust_toolchain
         uses: actions-rs/toolchain@v1
         with:
-          toolchain: 'stable-2022-01-20'
-      
+          toolchain: "stable-2022-01-20"
+
       - id: flutter
         uses: subosito/flutter-action@v2
         with:
-          channel: 'stable'
+          channel: "stable"
           cache: true
-          flutter-version: '3.0.5'
+          flutter-version: "3.0.5"
 
       - name: Cache Cargo
+        id: cache-cargo
         uses: actions/cache@v2
-        with: 
+        with:
           path: |
             ~/.cargo
           key: ${{ runner.os }}-cargo-${{ steps.rust_toolchain.outputs.rustc_hash }}-${{ hashFiles('./frontend/rust-lib/Cargo.toml') }}
 
       - name: Cache Rust
         uses: actions/cache@v2
-        with: 
+        with:
           path: |
             frontend/rust-lib/target
             shared-lib/target
-          key: ${{ runner.os }}-rust-rust-lib-share-lib-${{ steps.rust_toolchain.outputs.rustc_hash }}-${{ hashFiles('./frontend/rust-lib/Cargo.toml') }}    
+          key: ${{ runner.os }}-rust-rust-lib-share-lib-${{ steps.rust_toolchain.outputs.rustc_hash }}-${{ hashFiles('./frontend/rust-lib/Cargo.toml') }}
 
       - name: Setup Environment
-        run:   |
+        run: |
           if [ "$RUNNER_OS" == "Linux" ]; then
             sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
             sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list
@@ -64,11 +65,16 @@ jobs:
           fi
         shell: bash
 
-      - name: Deps
+      - if: steps.cache-cargo.outputs.cache-hit != 'true'
+        name: Deps
         working-directory: frontend
         run: |
           cargo install cargo-make
           cargo install duckscript_cli
+
+      - name: Cargo make flowy_dev
+        working-directory: frontend
+        run: |
           cargo make flowy_dev
 
       - name: Config Flutter

+ 28 - 7
.github/workflows/dart_lint.yml

@@ -7,14 +7,14 @@ name: Flutter lint
 
 on:
   push:
-    branches: [ main ]
+    branches: [main]
   pull_request:
-    branches: [ main ]
+    branches: [main]
 
 env:
   CARGO_TERM_COLOR: always
 
-jobs:  
+jobs:
   flutter-analyze:
     name: flutter analyze
     runs-on: ubuntu-latest
@@ -23,16 +23,38 @@ jobs:
         uses: actions/checkout@v2
       - uses: subosito/flutter-action@v1
         with:
-          flutter-version: '3.0.5'
+          flutter-version: "3.0.5"
           channel: "stable"
       - uses: actions-rs/toolchain@v1
         with:
-          toolchain: 'stable-2022-01-20'
+          toolchain: "stable-2022-01-20"
 
-      - name: Rust Deps
+      - name: Cache Cargo
+        id: cache-cargo
+        uses: actions/cache@v2
+        with:
+          path: |
+            ~/.cargo
+          key: ${{ runner.os }}-cargo-${{ steps.rust_toolchain.outputs.rustc_hash }}-${{ hashFiles('./frontend/rust-lib/Cargo.toml') }}
+
+      - name: Cache Rust
+        id: cache-rust-target
+        uses: actions/cache@v2
+        with:
+          path: |
+            frontend/rust-lib/target
+            shared-lib/target
+          key: ${{ runner.os }}-rust-rust-lib-share-lib-${{ steps.rust_toolchain.outputs.rustc_hash }}-${{ hashFiles('./frontend/rust-lib/Cargo.toml') }}
+
+      - if: steps.cache-cargo.outputs.cache-hit != 'true'
+        name: Rust Deps
         working-directory: frontend
         run: |
           cargo install cargo-make
+
+      - name: Cargo make flowy dev
+        working-directory: frontend
+        run: |
           cargo make flowy_dev
 
       - name: Flutter Deps
@@ -53,4 +75,3 @@ jobs:
       - name: Run Flutter Analyzer
         working-directory: frontend/app_flowy
         run: flutter analyze
-

+ 26 - 25
.github/workflows/dart_test.yml

@@ -3,12 +3,12 @@ name: Unit test(Flutter)
 on:
   push:
     branches:
-      - 'main'
-      
+      - "main"
+
   pull_request:
     branches:
-      - 'main'
-      - 'feat/flowy_editor'
+      - "main"
+      - "feat/flowy_editor"
 
 env:
   CARGO_TERM_COLOR: always
@@ -18,42 +18,49 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v2
-      
+
       - uses: actions-rs/toolchain@v1
         with:
-          toolchain: 'stable-2022-01-20'
-      
+          toolchain: "stable-2022-01-20"
+
       - uses: subosito/flutter-action@v2
         with:
-          channel: 'stable'
-          flutter-version: '3.0.5'
+          channel: "stable"
+          flutter-version: "3.0.5"
           cache: true
 
       - name: Cache Cargo
         uses: actions/cache@v2
-        with: 
+        with:
           path: |
             ~/.cargo
           key: ${{ runner.os }}-cargo-${{ steps.rust_toolchain.outputs.rustc_hash }}-${{ hashFiles('./frontend/rust-lib/Cargo.toml') }}
 
       - name: Cache Rust
+        id: cache-rust-target
         uses: actions/cache@v2
-        with: 
+        with:
           path: |
             frontend/rust-lib/target
             shared-lib/target
-          key: ${{ runner.os }}-rust-rust-lib-share-lib-${{ steps.rust_toolchain.outputs.rustc_hash }}-${{ hashFiles('./frontend/rust-lib/Cargo.toml') }}    
+          key: ${{ runner.os }}-rust-rust-lib-share-lib-${{ steps.rust_toolchain.outputs.rustc_hash }}-${{ hashFiles('./frontend/rust-lib/Cargo.toml') }}
 
-      - name: Flutter Deps
-        working-directory: frontend/app_flowy
-        run: |
-          flutter config --enable-linux-desktop
-        
-      - name: Rust Deps
+      - if: steps.cache-cargo.outputs.cache-hit != 'true'
+        name: Rust Deps
         working-directory: frontend
         run: |
           cargo install cargo-make
+
+      - name: Cargo make flowy dev
+        working-directory: frontend
+        run: |
           cargo make flowy_dev
+
+      - name: Flutter Deps
+        working-directory: frontend/app_flowy
+        run: |
+          flutter config --enable-linux-desktop
+
       - name: Build FlowySDK
         working-directory: frontend
         run: |
@@ -65,15 +72,9 @@ jobs:
           flutter packages pub get
           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 bloc tests
         working-directory: frontend/app_flowy
         run: |
           flutter pub get
           flutter test
-
-      - name: Run FlowyEditor tests
-        working-directory: frontend/app_flowy/packages/flowy_editor
-        run: |
-          flutter pub get
-          flutter test

+ 36 - 0
.github/workflows/flowy_editor_test.yml

@@ -0,0 +1,36 @@
+name: FlowyEditor test
+
+on:
+  push:
+    branches:
+      - "main"
+
+  pull_request:
+    branches:
+      - "main"
+
+env:
+  CARGO_TERM_COLOR: always
+
+jobs:
+  tests:
+    strategy:
+      matrix:
+        os: [macos-latest, ubuntu-latest, windows-latest]
+
+    runs-on: ${{ matrix.os }}
+
+    steps:
+      - uses: actions/checkout@v2
+
+      - uses: subosito/flutter-action@v2
+        with:
+          channel: "stable"
+          flutter-version: "3.0.5"
+          cache: true
+
+      - name: Run FlowyEditor tests
+        working-directory: frontend/app_flowy/packages/appflowy_editor
+        run: |
+          flutter pub get
+          flutter test

+ 2 - 2
frontend/.vscode/launch.json

@@ -29,7 +29,7 @@
             "program": "./lib/main.dart",
             "type": "dart",
             "env": {
-                "RUST_LOG": "debug"
+                "RUST_LOG": "trace"
             },
             "cwd": "${workspaceRoot}/app_flowy"
         },
@@ -44,7 +44,7 @@
             "type": "dart",
             "preLaunchTask": "AF: Clean + Rebuild All",
             "env": {
-                "RUST_LOG": "info"
+                "RUST_LOG": "trace"
             },
             "cwd": "${workspaceRoot}/app_flowy"
         },

+ 1 - 1
frontend/app_flowy/analysis_options.yaml

@@ -14,7 +14,7 @@ analyzer:
   exclude:
     - "**/*.g.dart"
     - "**/*.freezed.dart"
-    - "packages/flowy_editor/**"
+    - "packages/appflowy_editor/**"
     - "packages/editor/**"
     # - "packages/flowy_infra_ui/**"
 linter:

+ 32 - 0
frontend/app_flowy/assets/images/emoji/1F42F.svg

@@ -0,0 +1,32 @@
+<svg id="emoji" viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
+  <g id="color">
+    <path fill="#E27022" stroke="none" d="M19.3685,9.2168c0,0-8.31-0.7683-4.7954,9.1741c0,0,2.4911,6.1326,4.7954,7.3748 c0,0-9.3677,6.4041-3.7409,18.5692l0.6476,3.8526c0,0,3.75,5.5625,8.5,5.6875l1.75,1.625c0,0,1.1875,3.5625,2.125,4 c0.9375,0.4375,5.4375,3.0289,5.4375,3.0289l4.3582-0.2763l5.4543-2.5651c0,0,2.4375-2.0625,2.4375-4.6875l1.0021-1.375 c0,0,6.1854-1.2168,8.6229-5.8834l1.9107-7.1166v-4.5268l-0.3757-2.8898l-2.3475-5.8333l-1.5625-1.75c0,0,1.3125-2.2252,2-3.3001 c0.6875-1.0749,2.2857-4.745,2.2857-4.745l1.4018-3.3299l-1.4018-3.5l-3.9951-1.547l-2.3531,0.8595l-5.4035,5.7083 c0,0-5.5911-3.0058-9.1563-2.7633c-3.5652,0.2425-11.0354,2.43-11.0354,2.43L19.3685,9.2168z"/>
+    <path fill="#FFFFFF" stroke="none" d="M38.3377,29.5055c0,0,4.1875,3.7873,4.5,8.1312c0,0,4,3.7631,5.9375-1.9828l2.8125-6.4229 c0,0-1.9316-3.5892-8.125-3.7797L38.3377,29.5055z"/>
+    <path fill="#FFFFFF" stroke="none" d="M33.8377,29.5695c0,0-3.0625,3.7797-3.375,8.1236c0,0-4,3.7631-5.9375-1.9828l-4.125-6.4793 c0,0,2.3691-4.0405,8.5625-4.231L33.8377,29.5695z"/>
+    <path fill="#FFFFFF" stroke="none" d="M52.2127,33.5c0,0.3125,0.125,12-11.0625,13.375c0,0-6.625-2.125-10.625,0.375 c0,0-11.4375-4.3125-10.9375-13.875l-0.1875,5.8125c-1.4367,1.1319-3.6761-4.0312-0.6458-13.0208 c-5.2301,3.4846-5.9625,16.141-2.4792,22.0208c0,0,3.5,5.75,9.3125,5.9375c0,0,3.5,4.4375,10.3125,1.1875 c0,0,5.625,3.625,10.5-1.375c0,0,7.1875-0.7669,9.5625-6.1959c0,0,5.2083-15.2416-2.2083-21.6582 c0,0,3.3333,13.5417-1.2292,13.1667L52.2127,33.5z"/>
+    <path fill="#D0CFCE" stroke="none" d="M26.5252,55.5c0,0,0.8125,4.25,3.5625,4.9167l5.6667,2.25l4.75-0.9167l3.3958-2.0625 c0,0,2.6875-1.5208,2.4375-4.6875c0,0-6.75,2.5-10.1667,0.1667C36.171,55.1667,30.3794,57.3333,26.5252,55.5z"/>
+    <path fill="#E27022" stroke="none" d="M31.5044,47.0833c0,0,3.5,2.75,9.0833,0.0833l-0.25,2.6667l-4.4167,1.8333l-3.9167-1L31.5044,47.0833z"/>
+  </g>
+  <g id="hair"/>
+  <g id="skin"/>
+  <g id="skin-shadow"/>
+  <g id="line">
+    <path fill="#000000" stroke="none" d="M47.3393,34.6182c0,0,0.0607,2.3422-1.2037,3.1596c-1.2644,0.8174-3.3752-0.1996-3.3752-0.1996 s-0.0607-2.3422,1.2037-3.1596S47.3393,34.6182,47.3393,34.6182z"/>
+    <path fill="#000000" stroke="none" d="M25.9305,34.6182c0,0-0.0607,2.3422,1.2037,3.1596c1.2644,0.8174,3.3752-0.1996,3.3752-0.1996s0.0607-2.3422-1.2037-3.1596 S25.9305,34.6182,25.9305,34.6182z"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M24.1981,47.3666c0,0-1.6667,12.7083,11.9583,7.5833v-3.3438"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M21.2398,28.4916c-0.1667,0.3333-4.6667,9.1667,0.9167,16.25"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M22.1549,23.2401c-4.6652,2.7745-12.117,9.7821-5.2485,24.1265"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M27.3231,48.9916c0,0-11.1667,0.75-12.75,6.8333"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M28.9064,52.1582c-0.25,0.1667-10.3333,5.75-8.5833,10.6667"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M29.5766,59.1521c1.9799,2.0495,7.4123,6.3778,13.517,0.1296"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M17.4898,22.3249c-7.9284-9.061-3.0759-11.6581-0.2234-12.8765c0.9417-0.4022,2.0253-0.2705,2.8642,0.3168l5.7994,6.0597 c0,0,9.3237-6.3941,20.14,0l5.7994-6.0597c0.8389-0.5872,1.9225-0.719,2.8642-0.3168c2.8525,1.2184,7.705,3.8155-0.2234,12.8765"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M40.7186,47.7416c0,0,1,2.875-3.4167,3.625h-2.6038c-4.4167-0.75-3.4167-3.625-3.4167-3.625"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M47.8019,47.3666c0,0,1.6667,12.7083-11.9583,7.5833v-3.5833"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M50.7602,28.4916c0.1667,0.3333,4.6667,9.1667-0.9167,16.25"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M49.8832,23.2628c4.6675,2.7899,12.0602,9.7983,5.2104,24.1037"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M44.6769,48.9916c0,0,11.1667,0.75,12.75,6.8333"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M43.0936,52.1582c0.25,0.1667,10.3333,5.75,8.5833,10.6667"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M29.5766,24.1069c0,0,7.0776-3.3333,13.1833,0.4167"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M31.5208,29.2864c0,0,4.9901-3.3333,9.2949,0.4167"/>
+  </g>
+</svg>

+ 24 - 0
frontend/app_flowy/assets/images/emoji/1F431.svg

@@ -0,0 +1,24 @@
+<svg id="emoji" viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
+  <g id="color">
+    <path fill="#F4AA41" stroke="none" d="M58.4953,11.21c0,0-10.4076,2.3754-15.5743,6.7088c0,0-9-2.5-13.8333,0.1667 c0,0-9.6549-6.7318-15.6549-6.7318c0,0-5.0326,3.75,0.3216,21.0651c0,0-2.6667,10.6667,1.6667,16.3333s9.8333,9.5,9.8333,9.5 l8.7917,4.0417l3.9583,0.1667l9.9167-4.375c0,0,6.6667-4.8333,8.8333-9.5c2.1667-4.6667,2.1667-13.8333,2.1667-13.8333l-1-3.3333 l2.8333-11.0208L58.4953,11.21z"/>
+    <path fill="#FFFFFF" stroke="none" d="M30.8377,47.3355c0,0-7.625,2.75-1.375,9.25c0,0-0.9739,4.625,3.8034,5.4849h5.1966 c2.0529,0.0031,4.5833-1.0683,3.7404-5.3628c0,0,7.5513-6.3722-1.3654-9.3722l-4.875,2L30.8377,47.3355z"/>
+  </g>
+  <g id="hair"/>
+  <g id="skin"/>
+  <g id="skin-shadow"/>
+  <g id="line">
+    <ellipse cx="45.0854" cy="38.1033" rx="1.6461" ry="2.8119" fill="#000000" stroke="none"/>
+    <ellipse cx="26.8427" cy="38.1033" rx="1.6461" ry="2.8119" fill="#000000" stroke="none"/>
+    <polyline fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" points="31.9328,47.2287 36.037,50.0204 39.8495,47.2287"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M36.037,50.0204v4.2708c0,0-1.1042,3.6875-5.5417,2.875"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M15.8717,48.4759c-4.8928-7.2535-2.0014-15.8722-2.0014-15.8722s-5.25-14.875-0.4375-21.25c0,0,9.1875,1.5,15.6875,7.375 c4.5946-1.9379,9.1575-2.0128,13.6875-0.1437c6.5-5.875,15.6875-7.375,15.6875-7.375c4.8125,6.375-0.4375,21.25-0.4375,21.25 s2.8914,8.6187-2.0014,15.8722"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M14.7453,15.1037c0,0,12.8125,6.1875,10.0625,11.8125"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M24.8491,50.8753c0,0-9.3615-0.458-13.6525,7.5243"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M35.8911,49.8767v4.2708c0,0,1.1042,3.6875,5.5417,2.875"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M57.1828,14.96c0,0-12.8125,6.1875-10.0625,11.8125"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M47.2048,54.6836c0,0,8.2116,2.2454,8.6795,11.2958"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M48.079,50.7316c0,0,9.3615-0.458,13.6525,7.5243"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M31.3859,60.7598c3.88,1.6845,5.6481,1.8093,9.3021,0"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M25.4446,54.6836c0,0-8.2116,2.2454-8.6795,11.2958"/>
+  </g>
+</svg>

+ 28 - 0
frontend/app_flowy/assets/images/emoji/1F435.svg

@@ -0,0 +1,28 @@
+<svg id="emoji" viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
+  <g id="color">
+    <path fill="#3F3F3F" stroke="none" d="M35.8377,9.4188c-0.8333,0.1667-9,0.1667-9,0.1667l-8.75,7.25l-4.9167,10.25l-3,12.4167l1.3333,8.75 l5.75,7.1667c0,0,4.75,4,11.1667,3.75c0,0,20,2.9167,26.5-3.5833l3.3333-3.5833l3.1667-6.3333l0.0833-7.6667l-1.5-7l-2.1667-6.9167 l-4.0833-7.5l-4.9167-4.1667L44.921,9.8355l-4.1667-0.8333L35.8377,9.4188z"/>
+    <path fill="#F4AA41" stroke="none" d="M55.5877,19.5021l2.5-1.8333l3.0833-0.5l1.9167,1.25l0.75,1c0,0,0.9167,3.6667,1,3.9167 c0.0833,0.25,0,6,0,6l-1.25,3.1667l-2.1667,1.9167l-2.3333-5.5l-3.1667-7.25L55.5877,19.5021z"/>
+    <polygon fill="#F4AA41" stroke="none" points="16.671,19.0021 14.0044,17.1688 11.2544,16.5855 8.3377,18.4188 6.671,23.3355 7.171,29.1688 8.3377,31.8355 10.8377,34.6688 12.2544,30.1688 14.421,24.2521"/>
+    <path fill="#F4AA41" stroke="none" d="M46.3377,44.5855l4.0833-2.4167l1.75-4.6667l1.25-7.25l-1.0833-4.75l-2.4167-2.8333l-4.3333-1.8333 l-5.4167,0.1667l-3.5833-0.0833l-7-0.3333l-4.6667,0.3333l-4.25,2.8333l-2,4.5833l0.0833,3.9167c0,0-0.0833,3.5,0.1667,3.9167 c0.25,0.4167,1.5833,4.4167,1.5833,4.4167l1.1667,1.6667l4,1.8333l-1,4c0,0-0.1667,6.5,1.25,8.4167 c1.4167,1.9167,5.75,5.4167,5.75,5.4167l6.4167,0.6667l4.5833-1.6667l3.8334-4.8333c0.8412-2.6884,1.1213-5.4142,1-8.1667 L46.3377,44.5855z"/>
+  </g>
+  <g id="hair"/>
+  <g id="skin"/>
+  <g id="skin-shadow"/>
+  <g id="line">
+    <circle cx="27.6338" cy="29.6771" r="2" fill="#000000" stroke="none"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M23.0393,27.9538c0,0,4.726-6.5434,9.4521,0.1023"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M30.8968,40.1701c0,0,0.6484,2.299,3.537,2.7117"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M27.989,51.7674l1.2915,0.8045c1.2002,0.7477,2.5573,1.1408,3.9382,1.1408h5.6222c1.3232,0,2.6255-0.361,3.7893-1.0503 l1.5111-0.895"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M31.0557,35.5878c-2.6158,1.5082-5.9686,4.8289-5.5798,11.8762"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M26.8178,56.0264l0.2809,0.6695c1.0574,2.5206,3.1675,4.4503,5.7723,5.279l0.7864,0.2502 c2.2541,0.7171,4.6965,0.5509,6.8326-0.4651l0,0c2.0135-0.9577,3.6173-2.6051,4.5206-4.6436l0.4855-1.0957"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M16.0795,18.8447l-1.7688-1.228c-1.6197-1.1245-3.7931-1.0191-5.2965,0.2567l0,0c-0.7659,0.65-1.2866,1.5421-1.4642,2.5308 c-0.6192,3.4457-1.5742,11.5715,2.6707,13.7114"/>
+    <circle cx="43.3662" cy="29.9126" r="2" fill="#000000" stroke="none"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M48.0175,28.1759c0,0-4.726-6.5434-9.4521,0.1023"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M49.9607,41.4195c2.5152-1.7505,2.6995-5.9685,3.3012-10.1803c0.4127-2.8885-0.4716-5.895-3.5959-8.4888 c-3.1321-2.6002-7.3146-2.1486-9.39-1.6993c-1.0224,0.2213-2.0608,0.3145-3.1062,0.2758l-2.7267-0.1011 c-0.842-0.0312-1.6765-0.1564-2.4973-0.3471c-1.9859-0.4613-6.3637-1.0469-9.6117,1.6496 c-3.1243,2.5938-4.0086,5.6002-3.5959,8.4888c0.6017,4.2118,0.786,8.4298,3.3012,10.1803"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M41.1032,40.1701c0,0-0.6484,2.299-3.537,2.7117"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M41.2534,35.9947c2.5625,1.588,5.6444,4.9189,5.2707,11.6914"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M41.2534,35.9947c-0.5708-0.3538-1.1928-0.6531-1.7718-0.8958c-1.2375-0.5188-2.5724-0.7524-3.9139-0.7201l-0.0521,0.0013 c-1.5439,0.0372-3.0699,0.4152-4.4116,1.18c-0.0161,0.0092-0.0322,0.0184-0.0483,0.0277"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M54.4627,54.7601c0,0,12.6763-6.0522,3.9518-28.4623S36.0651,9.7105,36.0651,9.7105S22.3101,3.6656,13.5856,26.0757 s3.9518,28.4623,3.9518,28.4623"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M55.9205,19.0668l1.7688-1.228c1.6197-1.1245,3.7931-1.0191,5.2965,0.2567l0,0c0.7659,0.65,1.2866,1.5421,1.4642,2.5308 c0.6192,3.4457,1.5742,11.5715-2.6707,13.7114"/>
+  </g>
+</svg>

+ 28 - 0
frontend/app_flowy/assets/images/emoji/1F43A.svg

@@ -0,0 +1,28 @@
+<svg id="emoji" viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
+  <g id="color">
+    <path fill="#9B9B9A" stroke="none" d="M53.2127,6.2105l-6.25,6c0,0-5.875-3.125-12-3.125s-10.75,3-10.75,3l-6.75-6l-3.25-0.375l-3,3.75l-1.5,7.5 c0,0-0.125,9.75,2.5,11.625s-2,13.25-2,13.25l3.375,6.75l8,5.625l2.75,1.875c0,0,3.3331,7.5833,10.9944,5.5959 c0,0,5.7556,3.4875,10.3806-5.4709l2.5-1.5l6.375-3.625l6-8.125c0,0-3.25-10.75-2.5-14.25s2.875-9.5,2.625-11.25s-2-8.625-2-8.625 S56.8377,4.0855,53.2127,6.2105z"/>
+    <path fill="#3F3F3F" stroke="none" d="M12.421,45.5021c0,0,4.0833-10.25,10.1667-10l7.5833-0.4167l2.25,3.1667c0,0,0.9857,5.682-0.6702,9.5016 c-0.3188,0.7354,0.2502,1.5299,1.0442,1.4201c1.1109-0.1535,2.5407-0.2071,4.1332,0.0797c0.7536,0.1357,1.3819-0.5736,1.13-1.2966 c-0.6854-1.9673-1.4463-5.4314-0.5539-9.7882l2.75-2.9167l3.75,0.1667l3.5,0.5833c0,0,9.5-2,9.9167,10.25l2.6667-3.9167l-2.5-12.25 l1-2.5833l2-7.5l-1.0833-8.9167l-2.75-5.1667L52.671,6.0021l-4.4167,5.1667l-1.6667,0.6667l-5.25-1.8333l-9.25-0.6667 l-8.4167,2.8333l-6-6.8333l-3.5833-0.1667l-3.5,5.6667l-0.8333,6.9167l1.25,6.9167l1.5833,3.6667l-0.75,6.8333l-2,6.5 L12.421,45.5021z"/>
+    <path fill="#3F3F3F" stroke="none" d="M29.489,53.0348c0,0,4.4375-2.5625,11.1875,0.125l0.0625,0.8125l-2.25,1.5l-0.9375,1.625 c0,0-1.3125,1.25-2,1c-0.6875-0.25-3-1.125-3-1.125l-0.75-1.5625l-2.625-1.4375l0.0625-0.875"/>
+    <path fill="#9B9B9A" stroke="none" d="M55.2752,8.7105c0,0,5.25,8.5625,0.75,17.5c0,0-4.4375-6.5-7.9375-6.375 C48.0877,19.8355,55.1502,15.5855,55.2752,8.7105z"/>
+    <path fill="#9B9B9A" stroke="none" d="M14.7227,8.7105c0,0-5.25,8.5625-0.75,17.5c0,0,4.4375-6.5,7.9375-6.375 C21.9102,19.8355,14.8477,15.5855,14.7227,8.7105z"/>
+    <path fill="#9B9B9A" stroke="none" d="M29.1765,62.0759c0.1166,2.4426,0.8751,4.2037,2.6761,4.858h6.5847 c2.1107-0.7139,2.5376-2.4764,2.3018-4.6519L29.1765,62.0759"/>
+  </g>
+  <g id="hair"/>
+  <g id="skin"/>
+  <g id="skin-shadow"/>
+  <g id="line">
+    <circle cx="26.2495" cy="40.2751" r="2" fill="#000000" stroke="none"/>
+    <circle cx="44.2944" cy="40.5106" r="2" fill="#000000" stroke="none"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M29.3116,53.9749l2.375,1.3959c0,0,0.4899,2.9667,3.2712,2.748v3.5625c0,0-3.4688,1.7604-6.3438-0.5312 c-2.2371-1.7832-3.5938-4.6875-3.5938-4.6875"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M21.739,53.3376c-3.8308-1.0774-6.3025-3.0804-7.8464-5.5039L9.871,42.3259c2.7739-7.0508,2.5763-14.1342,2.5763-14.1342 c-4.7083-8.6667-1.625-17.4583-1.625-17.4583c2.5417-8.2917,6.7917-5.0417,6.7917-5.0417c3.0417,5.1667,9.875,8.9167,9.875,8.9167"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M15.614,9.2126c0,0,0.375,7.5,7.3125,10.375c0,0-6.6983,1.679-7.7816,6.3457"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M54.8556,9.2126c0,0-0.375,7.5-7.3125,10.375c0,0,6.6983,1.679,7.7816,6.3457"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M30.9474,40.5106c0,0,2.711-4.3812-1.25-5.6689c-2.7917-0.9075-6.2083,1.0584-6.2083,1.0584"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M35.3321,61.6813c0,0,3.4688,1.7604,6.3438-0.5312c2.2371-1.7832,3.5938-4.6875,3.5938-4.6875"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M40.6873,53.9749l-2.7917,1.3959c0,0-0.1566,2.9667-2.9378,2.748"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M42.8008,14.9502c0,0,6.8333-3.75,9.875-8.9167c0,0,4.25-3.25,6.7917,5.0417c0,0,3.0833,8.7917-1.625,17.4583 c0,0-0.0675,7.3188,2.6299,13.7929l-4.6974,6.7357c-1.5598,2.0223-3.8715,3.6742-7.2242,4.6172"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M39.3425,40.8524c0,0-2.7109-4.3812,1.25-5.6689c2.7917-0.9075,6.2083,1.0584,6.2083,1.0584"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M24.6882,11.4515c0,0,7.9524-5.5773,21.3522,0"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M31.3533,65.8686c2.7191,1.4858,5.2903,1.3535,7.75,0"/>
+  </g>
+</svg>

+ 17 - 0
frontend/app_flowy/assets/images/emoji/1F600.svg

@@ -0,0 +1,17 @@
+<svg id="emoji" viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
+  <g id="color">
+    <circle cx="36" cy="36" r="23" fill="#FCEA2B"/>
+    <path fill="#FFFFFF" d="M50.595,41.64c0.012,1.5397-0.2838,3.0662-0.87,4.49c-12.49,3.03-25.43,0.34-27.49-0.13 c-0.5588-1.3852-0.8407-2.8664-0.83-4.36h0.11c0,0,14.8,3.59,28.89,0.07L50.595,41.64z"/>
+    <path fill="#EA5A47" d="M49.7251,46.13c-1.79,4.27-6.35,7.23-13.69,7.23c-7.41,0-12.03-3.03-13.8-7.36 C24.2951,46.47,37.235,49.16,49.7251,46.13z"/>
+  </g>
+  <g id="hair"/>
+  <g id="skin"/>
+  <g id="skin-shadow"/>
+  <g id="line">
+    <circle cx="36" cy="36" r="23" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M50.595,41.64 c0.012,1.5397-0.2838,3.0662-0.87,4.49c-12.49,3.03-25.43,0.34-27.49-0.13c-0.5588-1.3852-0.8407-2.8664-0.83-4.36h0.11 c0,0,14.8,3.59,28.89,0.07L50.595,41.64z"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M49.7251,46.13 c-1.79,4.27-6.35,7.23-13.69,7.23c-7.41,0-12.03-3.03-13.8-7.36C24.2951,46.47,37.235,49.16,49.7251,46.13z"/>
+    <path d="M30,31c0,1.6568-1.3448,3-3,3c-1.6553,0-3-1.3433-3-3c0-1.6552,1.3447-3,3-3C28.6552,28,30,29.3448,30,31"/>
+    <path d="M48,31c0,1.6568-1.3447,3-3,3s-3-1.3433-3-3c0-1.6552,1.3447-3,3-3S48,29.3448,48,31"/>
+  </g>
+</svg>

+ 21 - 0
frontend/app_flowy/assets/images/emoji/1F984.svg

@@ -0,0 +1,21 @@
+<svg id="emoji" viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
+  <g id="color">
+    <path fill="#FFFFFF" stroke="none" d="M23.7544,12.3618l1.6667,7.1667l-5.3333,5.3333l-8.3333,14.3333l1,4.6667l2.1667,1.3333l4-0.1667 l3.5-3.3333l6.8333-1.8333c0,0,1.3333,1.5,2.1667,3s3.6667,4.1667,3.6667,4.1667l0.5,6l-1.8333,6.1667l-2,2.8333 c0,0,22,9.5,33.1667-7l-0.5-6l-1.8333-5l-3.3333-5.1667l-1-1.5l-0.1667-5.1667l-2.8333-5.3333l-5-3l-2.6667-4.5l-5.1667-4.1667 l-6.5-1.5l-5.6667,1l-4.1667-2.1667L23.7544,12.3618z"/>
+    <path fill="#EA5A47" stroke="none" d="M50.671,23.155l5.2083,4.095c0,0,5.5638,8.2181-0.3258,17.8201c-7.0492,11.4924,0,0,0,0 c-1.6183,3.4754-2.3141,6.7423-1.738,9.7216l-5.3111-4.4167V34.2917L50.671,23.155z"/>
+    <polyline fill="#EA5A47" stroke="none" points="25.8985,19.2712 10.7847,12.0212 15.951,18.1399 21.1747,23.995 25.8985,19.2712"/>
+    <path fill="#92D3F5" stroke="none" d="M29.7367,13.6311l10.7677,0.1362c0,0,9.2377,4.0661,10.5355,11.8161l0.6874,8.9567 c-2.6337,6.5386-3.0562,14.1267,2.0883,20.8336l0,0c0,0-7.1444,1.3215-9.8944-7.1094L42.3377,43.5l0.3258-6.0341l1.4169-5.6426 l-0.2833-4.8929l-2.2761-4.3124l-3.5322-2.8413l-5.792-2.0801L29.7367,13.6311"/>
+    <path fill="#61B2E4" stroke="none" d="M58.4549,36.75c0,0,5.5192,6.4066,6.9982,15.1193c0.1838,1.0826,0.1251,2.193-0.1377,3.2591 c-0.4317,1.7512-0.8179,4.9979,0.1452,7.3825c0.4689,1.1611-0.5621,2.3655-1.7883,2.1115 c-3.7094-0.7686-9.2437-3.6474-10.2567-8.0876c-0.0239-0.1047-0.0368-0.2138-0.0417-0.321l-0.266-5.7459 c-0.0132-0.2857,0.0516-0.5695,0.1875-0.8211l3.692-6.8359c0.0666-0.1233,0.1164-0.2549,0.1482-0.3914L58.4549,36.75"/>
+  </g>
+  <g id="hair"/>
+  <g id="skin"/>
+  <g id="skin-shadow"/>
+  <g id="line">
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M58.4549,37.7826C60.2229,40.1443,65,44.4647,64.5,54.0208"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M32.5,41.8854c0,0,8.4783,6.7823,0,18.7647"/>
+    <polyline fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" points="24.809,19.1338 10.25,11.75 21.1747,23.995"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M35.1962,30.8696c0.5489,8.3555-9.3225,9.703-11.954,10.3347c-0.3325,0.0798-0.6318,0.25-0.8736,0.4919l-2.2227,2.2227 c-0.3494,0.3494-0.8234,0.5458-1.3176,0.5458h-3.5121c-1.203,0-2.2711-0.7698-2.6515-1.9111l-0.531-1.5931 c-0.258-0.774-0.1649-1.6222,0.2549-2.3218l8.7862-14.6436l4.7238-4.7238l-2.1151-6.9054c0,0,7.8026-0.6987,8.4135,5.3308 c0,0,16.9281,2.4418,10.5531,19.383c0,0-1.625,5.9489,2.375,11.1846"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M30.9167,14.0208c0,0,22.2444-4.0208,19.9583,19.9583"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M49.9187,23.155c0,0,14.7665,6.5865,5.4563,22.22c0,0-5.375,6.5625,0.625,13.6042"/>
+    <circle cx="24.4167" cy="28.9304" r="2" fill="#000000" stroke="none"/>
+  </g>
+</svg>

+ 33 - 0
frontend/app_flowy/assets/images/emoji/1F9CC.svg

@@ -0,0 +1,33 @@
+<svg id="emoji" viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
+  <g id="color">
+    <path fill="#e27022" d="M50.63,15.6a1.2,1.2,0,0,1-1.73,1.18c-1.08-.56-2.98-1.24-3.92-.35a.3039.3039,0,0,1-.07.05c-1.52-2.09-4.21-3.79-8.91-3.79a3.7944,3.7944,0,0,1,3.56-4.24,8.4848,8.4848,0,0,0,4.73-2.06,5.0935,5.0935,0,0,1,.65,3.96l.49.01C49.15,10.36,50.4,13.41,50.63,15.6Z"/>
+    <path fill="#b1cc33" d="M36.0022,42.0333c-15.2776,0-10.5548-6.0794-10.5548-12.7628,0-3.5808-.3769-3.4016-.3769-5.9038s.6328-10.681,10.9317-10.681,10.9317,8.1789,10.9317,10.681-.3769,2.323-.3769,5.9038C46.557,35.9539,51.28,42.0333,36.0022,42.0333Z"/>
+    <path fill="#b1cc33" d="M46.9339,23.3667c2.137-2.8369,5.3093-2.0623,8.8-4.36-.548,3.5228-1.6172,12.3056-8.9959,12.787Z"/>
+    <path fill="#b1cc33" d="M25.2622,31.7941c-7.3787-.4814-8.4479-9.2642-8.9959-12.787,3.49,2.2973,6.6628,1.5227,8.8,4.36Z"/>
+    <path fill="#b1cc33" d="M17.0625,57.8177s-2-13,10-13c3.1918,2.1279,5.9264,3.5984,9,3.5922h-.125c3.0736.0062,5.8081-1.4643,9-3.5922,12,0,10,13,10,13l.0076,1H17.0361Z"/>
+    <path fill="#5c9e31" d="M36,58.8164c8.1262-10.7061,8.3828-14.831,8.3828-14.831a1.0008,1.0008,0,0,1,.5547-.168c3.6563,0,6.4844,1.1357,8.4072,3.3769C56.8984,51.3369,55.9668,57.7,55.9258,57.97a.9991.9991,0,0,1-.9863.8467Z"/>
+    <path fill="#a57939" d="M44.9635,57.8182A47.8735,47.8735,0,0,1,51.43,50.9394c4.4058-3.4682,6.7662-3.56,10.948-8.9061,2.0629-2.6374,2.6374-7.2817,0-9.3447S56.024,31.4452,53.5,34.5164c-4.681,5.6962-3.8249,8.713-6.1991,13.1933A102.5608,102.5608,0,0,1,40.4972,57.816v1h4.4654Z"/>
+    <path fill="#fff" d="M31.0551,31.4981h0a2.6068,2.6068,0,0,1,2.6068,2.6068v.651a0,0,0,0,1,0,0H31.0551a0,0,0,0,1,0,0V31.4981A0,0,0,0,1,31.0551,31.4981Z"/>
+    <path transform="translate(79.3987 66.254) rotate(180)" fill="#fff" d="M38.3959,31.4981h2.6068a0,0,0,0,1,0,0v.651a2.6068,2.6068,0,0,1-2.6068,2.6068h0a0,0,0,0,1,0,0V31.4981A0,0,0,0,1,38.3959,31.4981Z"/>
+  </g>
+  <g id="line">
+    <path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M54.7288,52.4471a16.7057,16.7057,0,0,1,.2087,5.3706"/>
+    <path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.0625,57.8177s-2-13,10-13c3.1918,2.1279,5.9264,3.5984,9,3.5922h-.125c3.0736.0062,5.8081-1.4643,9-3.5922"/>
+    <path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M44.287,12.09a5.0267,5.0267,0,0,0,0-5.7006,8.4359,8.4359,0,0,1-4.7251,2.0561,3.7937,3.7937,0,0,0-3.56,4.24"/>
+    <path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M46.9339,23.3667c2.137-2.8369,5.3093-2.0623,8.8-4.36-.548,3.5228-1.6172,12.3056-8.9959,12.787"/>
+    <path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M25.2622,31.7941c-7.3787-.4814-8.4479-9.2642-8.9959-12.787,3.49,2.2973,6.6628,1.5227,8.8,4.36"/>
+    <path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M36.0022,42.0333c-15.2776,0-10.5548-6.0794-10.5548-12.7628,0-3.5808-.3769-3.4016-.3769-5.9038s.6328-10.681,10.9317-10.681,10.9317,8.1789,10.9317,10.681-.3769,2.323-.3769,5.9038C46.557,35.9539,51.28,42.0333,36.0022,42.0333Z"/>
+    <path d="M42.0023,26.4993a2,2,0,1,1-2-2,2,2,0,0,1,2,2"/>
+    <path d="M34.0023,26.4993a2,2,0,1,1-2-2,2,2,0,0,1,2,2"/>
+    <path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M38.039,23.0265a5.4257,5.4257,0,0,1,5.0488-1.2713"/>
+    <path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M28.97,21.7552a5.4256,5.4256,0,0,1,5.0488,1.2713"/>
+    <path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M44.9635,57.8182A47.8735,47.8735,0,0,1,51.43,50.9394c4.4058-3.4682,6.7662-3.56,10.948-8.9061,2.0629-2.6374,2.6374-7.2817,0-9.3447S56.024,31.4452,53.5,34.5164c-4.681,5.6962-3.8249,8.713-6.1991,13.1933A102.5608,102.5608,0,0,1,40.4972,57.816"/>
+    <line x1="50.3728" x2="53.5159" y1="36.0726" y2="37.974" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
+    <line x1="56.4724" x2="58.3992" y1="45.2117" y2="48.3395" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
+    <line x1="59.3606" x2="59.0523" y1="29.4325" y2="33.0931" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
+    <line x1="31.0551" x2="31.0551" y1="32.4956" y2="35.2715" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
+    <line x1="42.2494" x2="29.8085" y1="35.2715" y2="35.2715" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
+    <line x1="41.0027" x2="41.0027" y1="32.4956" y2="35.2715" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
+    <path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M47.2467,15.1081a9.3183,9.3183,0,0,1,2.754,1.1523s0-5.21-4.4936-5.21"/>
+  </g>
+</svg>

+ 26 - 0
frontend/app_flowy/assets/images/emoji/1F9DB.svg

@@ -0,0 +1,26 @@
+<svg id="emoji" viewBox="0 0 72 72" version="1.1" xmlns="http://www.w3.org/2000/svg">
+  <g id="hair">
+    <path fill="#3F3F3F" d="M26.1,39.2c-4,0-4-6-4-13s4-14,14-14s14,7,14,14s0,13-4,13"/>
+  </g>
+  <g id="skin">
+    <path fill="#FCEA2B" d="M24.9494,31.1c0,9,4.9,14,11,14c5.9,0,11.1-5,11.1-14c0.0246-1.7187-0.3163-3.423-1-5c-3-3-7-8-7-8 c-4,3-7,6-13,7C26.0494,25.1,24.9494,26.1,24.9494,31.1z"/>
+  </g>
+  <g id="color">
+    <path fill="#D22F27" d="M35.65,45.1c-3.4-0.1-8.6-5.7-8.6-5.7h-13.6c1.5916,2.7161,3.0272,5.5207,4.3,8.4 c1.281,3.1916,2.2196,6.5102,2.8,9.9h30.9c0.5804-3.3898,1.519-6.7084,2.8-9.9c1.2728-2.8793,2.7084-5.6839,4.3-8.4h-13.9 C44.75,39.4,39.15,45.1,35.65,45.1z"/>
+    <path fill="#3F3F3F" d="M17,61v-3.8c0-5,5-9,10-9c6,5,12,5,18,0c5,0,10,4,10,9V61H17z"/>
+    <path d="M31.45,61h9c3.6-2.9,5.6-13.5,5.6-13.5c-5.7672,5-14.3328,5-20.1,0C25.95,47.4,27.95,58,31.45,61z"/>
+  </g>
+  <g id="line">
+    <path fill="none" stroke="#000000" stroke-linejoin="round" stroke-width="2" d="M26.5,39.4H13.4 c1.6694,2.6727,3.1079,5.4829,4.3,8.4c0.5072,1.2402,0.9413,2.509,1.3,3.8"/>
+    <path fill="none" stroke="#000000" stroke-linejoin="round" stroke-width="2" d="M53,51.4c0.4-1.2,0.8-2.4,1.3-3.6 c1.1713-2.9266,2.6108-5.7385,4.3-8.4H44.7"/>
+    <path d="M42,30.1c0,1.1046-0.8954,2-2,2s-2-0.8954-2-2s0.8954-2,2-2C41.1032,28.1032,41.9968,28.9968,42,30.1"/>
+    <path d="M34,30.1c0,1.1046-0.8954,2-2,2c-1.1046,0-2-0.8954-2-2s0.8954-2,2-2C33.1032,28.1032,33.9968,28.9968,34,30.1"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M33,39 c1.875-1,4.125-1,6,0"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" d="M38,38.5 c0.2408,0.0816,0.4748,0.1819,0.7,0.3"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17,60v-3c0-5,5-9,10-9 c6,5,12,5,18,0c5,0,10,4,10,9v3"/>
+    <polygon stroke="#000000" stroke-linecap="round" stroke-linejoin="round" points="34,38.7 34.3,41.1 35.1,38.8 35,38.7"/>
+    <polygon stroke="#000000" stroke-linecap="round" stroke-linejoin="round" points="37.9,38.7 37.7,41.1 36.9,38.8 36.8,38.7"/>
+    <path fill="none" stroke="#000000" stroke-linejoin="round" stroke-width="2" d="M24.9494,31.1c0,9,4.9,14,11,14 c5.9,0,11.1-5,11.1-14c0.0246-1.7187-0.3163-3.423-1-5c-3-3-7-8-7-8c-4,3-7,6-13,7C26.0494,25.1,24.9494,26.1,24.9494,31.1z"/>
+    <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M26,39.1c-4,0-4-6-4-13 s4-14,14-14s14,7,14,14s0,13-4,13"/>
+  </g>
+</svg>

+ 39 - 0
frontend/app_flowy/assets/images/emoji/1F9DD-200D-2642-FE0F.svg

@@ -0,0 +1,39 @@
+<svg id="emoji" viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
+  <g id="color">
+    <path fill="#92d3f5" d="M54.9224,60.9315s2-14.0268-10-14.0268c-3.1919,2.1279-5.9264,3.5984-9,3.5921h.125c-3.0736.0063-5.8082-1.4642-9-3.5921-12,0-10,14.0268-10,14.0268Z"/>
+    <path fill="#61b2e4" d="M45.3084,46.9047a18.0129,18.0129,0,0,1-8.9375,3.5885c13.0625.4115,12.9775,6.395,13.74,10.395h5.1129S57.3084,46.9047,45.3084,46.9047Z"/>
+    <polyline fill="#6a462f" points="18.056 52.064 18.056 42.499 25.141 42.499 25.141 46.063"/>
+    <path fill="#a57939" d="M47.9673,60.9583c-18.4989-5.1358-20.92-14.0536-20.92-14.0536s-4.2.2046-5.6776,1.3537c3.1163,6.4287,5.9346,9.5505,14.1044,12.6807"/>
+  </g>
+  <g id="skin">
+    <path fill="#fcea2b" d="M38.1326,20.5319a9.3955,9.3955,0,0,1-2.18-5.054c-.028,0-.0549-.0052-.0829-.0052a8.2719,8.2719,0,0,1-1.98,5.0591,10.8766,10.8766,0,0,1-8.7937,4.7087,17.3993,17.3993,0,0,0-.5656,4.4052c0,7.8277,5.0765,14.1732,11.3386,14.1732S47.208,37.4734,47.208,29.6457a17.3453,17.3453,0,0,0-.6258-4.6109A9.7529,9.7529,0,0,1,38.1326,20.5319Z"/>
+    <path fill="#fcea2b" d="M46.5844,24.451l11.45-1.6359S51.7663,32.6639,47.54,31.614"/>
+    <path fill="#fcea2b" d="M25.4747,24.4322,14.1572,22.8151s6.1889,9.7351,10.3663,8.6973"/>
+  </g>
+  <g id="hair">
+    <path fill="#f4aa41" d="M35.7974,11.7565A15.35,15.35,0,0,0,20.661,24.6021c8.8261,3.0574,13.1564-3.9388,13.1564-3.9388a8.2716,8.2716,0,0,0,1.98-5.0592H35.88a9.3969,9.3969,0,0,0,2.18,5.0592s3.413,6.46,12.8727,3.93A15.3509,15.3509,0,0,0,35.7974,11.7565Z"/>
+    <path fill="#f4aa41" d="M37.48,44.3806l-5.1958-.6448-4.21-2.6168-3.4133-6.106-.3793-2.8445-2.3514-.4171L19.02,29.4878V42.0409l6.159.3305v4.5958l1.9062-.19c3.17,2.1133,5.8895,3.5747,8.9375,3.5885,3.048-.0138,5.7675-1.4752,8.9375-3.5885a9.6884,9.6884,0,0,1,6.5536,2.1348,12.86,12.86,0,0,0,.6612-4.1082c0-16.6561-.27-14.3257-.27-14.3257l-2.3265,1.4249-2.2.2654-1.29,4.8924-3.6029,4.9683Z"/>
+  </g>
+  <g id="line">
+    <path d="M42.0163,28.5884a2,2,0,1,1-2-2,2.0007,2.0007,0,0,1,2,2"/>
+    <path d="M34.0163,28.5884a2,2,0,1,1-2-2,2.0007,2.0007,0,0,1,2,2"/>
+    <path d="M36.0162,38.5908a7.6528,7.6528,0,0,1-3.4473-.8579,1,1,0,0,1,.8945-1.7891,5.3774,5.3774,0,0,0,5.1055,0,1,1,0,1,1,.8945,1.7891A7.6524,7.6524,0,0,1,36.0162,38.5908Z"/>
+    <path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M50.9636,24.15C46.49,10.5289,36.0162,11.9284,36.0162,11.9284S25.663,10.545,21.147,23.9148"/>
+    <circle cx="36.0162" cy="22.1497" r="1.5"/>
+    <path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M25.4613,24.7355,14.1438,23.1184s6.1889,9.7351,10.3663,8.6973"/>
+    <path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M36.0162,15.776a20.4581,20.4581,0,0,1-3.08,5.0592"/>
+    <path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M46.571,24.7543l11.45-1.6359s-6.2678,9.8488-10.494,8.7989"/>
+    <path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M36.0162,15.776a20.4558,20.4558,0,0,0,3.08,5.0592"/>
+    <line x1="51.2159" x2="51.2159" y1="31.033" y2="44.5357" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
+    <polyline fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" points="18.088 52.108 18.088 42.543 25.173 42.543 25.173 46.108"/>
+    <line x1="20.0217" x2="20.0217" y1="42.5045" y2="37.7613" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
+    <line x1="23.3932" x2="23.3932" y1="42.5433" y2="39.102" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
+    <path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M54.9537,59.9492s2-13-10-13c-3.1919,2.1279-5.9264,3.5984-9,3.5921h.125c-3.0736.0063-5.8082-1.4642-9-3.5921-12,0-10,13-10,13"/>
+    <path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M26.4391,24.5466l-.9825.2293a17.29,17.29,0,0,0-.779,5.1733c0,7.8276,5.0765,14.1732,11.3386,14.1732s11.3386-6.3456,11.3386-14.1732a17.29,17.29,0,0,0-.7791-5.1733l-.8123-.3239"/>
+    <path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M46.571,24.7543l11.45-1.6359s-6.2678,9.8488-10.494,8.7989"/>
+    <line x1="19.9468" x2="19.9468" y1="30.333" y2="33.5011" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
+    <path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M47.8955,60.0059C29.3966,54.87,27.0787,46.9492,27.0787,46.9492s-4.2.2046-5.6776,1.3537c3.1163,6.4287,5.69,8.6387,13.8595,11.7689"/>
+    <path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M46.7244,21.2945c-2.6425-.6931-3.2409,2.473-6.127,1.7854"/>
+    <path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M31.3867,23.08c-2.886.6876-2.8758-2.7625-5.5183-2.0694"/>
+  </g>
+</svg>

+ 28 - 0
frontend/app_flowy/assets/images/emoji/1F9DE-200D-2642-FE0F.svg

@@ -0,0 +1,28 @@
+<svg id="emoji" viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
+  <g id="color">
+    <path fill="#92d3f5" d="M45.5317,50.6166l.797,2.48-1.1339,4.9948L40.0135,63.766A23.9672,23.9672,0,0,0,54.951,59.6536s2-13-10-13c-3.1918,2.1279-5.9264,3.5984-9,3.5921h.125c-3.0736.0063-5.8081-1.4642-9-3.5921-12,0-10,13-10,13,9.9776,6.0208,14.368,3.708,28.1188-1.5619l.6171-3.5208-24.0677,1.35,5.3094-.9076"/>
+    <path fill="#92d3f5" d="M54.9358,59.6536s2-13-10-13c-3.1919,2.1279-5.9264,3.5984-9,3.5921h.125c-3.0736.0063-5.8081-1.4642-9-3.5921-12,0-10,13-10,13"/>
+    <path fill="#61b2e4" d="M45.2181,46.6536a18.0129,18.0129,0,0,1-8.9375,3.5885c13.0625.4115,13.0625,5.4115,13.8247,9.4115h5.1128S57.2181,46.6536,45.2181,46.6536Z"/>
+    <path fill="#61b2e4" d="M55.2181,59.6536S43.4619,68.68,34.4813,62.6168l10.7135-4.5251,1.1339-4.9948S55.3057,50.6328,55.2181,59.6536Z"/>
+    <ellipse cx="36.0134" cy="30.1536" rx="11.3386" ry="14.1732" fill="#92d3f5"/>
+    <path fill="#fff" d="M49.1847,14.728c-13.1713,4.1165-24.51,13.5184-24.51,13.5184s-3.96-5.3394-2.2369-10.7265c0,0,2.2369-8.0773,13.036-8.0773,7.7035,0,11.5948,3.1694,13.7108,5.2854C55.331,23.4529,47.05,28.3322,47.05,28.3322a126.1662,126.1662,0,0,1-9.655-8.3413"/>
+    <path fill="#3f3f3f" d="M36.0422,46.9384c5.0174-.1253,9.5065-6.0464,10.3-10.8959-3.2477,5.68-8.6155,4.61-8.6155,4.61L36.02,38.0191l-1.7063,2.6338s-5.3678,1.0695-8.6155-4.61c.7933,4.85,5.2824,10.7706,10.3,10.8959"/>
+    <path fill="#d0cfce" d="M49.1847,14.728C55.331,23.4529,47.05,28.3322,47.05,28.3322a126.1662,126.1662,0,0,1-9.655-8.3413"/>
+    <path fill="#fcea2b" d="M36.0135,25.82h0a2,2,0,0,1-2-2v-4a2,2,0,0,1,2-2h0a2,2,0,0,1,2,2v4A2,2,0,0,1,36.0135,25.82Z"/>
+    <polygon fill="#fcea2b" points="45.195 58.092 40.783 59.923 40.783 54.678 45.812 54.678 45.195 58.092"/>
+  </g>
+  <g id="line">
+    <path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M40.0135,63.766A23.9672,23.9672,0,0,0,54.951,59.6536s2-13-10-13c-3.1918,2.1279-5.9264,3.5984-9,3.5921h.125c-3.0736.0063-5.8081-1.4642-9-3.5921-12,0-10,13-10,13,9.9776,6.0208,14.368,3.708,28.1188-1.5619l.6171-3.5208-24.0677,1.35"/>
+    <path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M45.5317,50.6166l.797,2.48-1.1339,4.9948"/>
+    <path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M25.7159,36.0725a17.002,17.002,0,0,1-1.04-5.92,18.7051,18.7051,0,0,1,.11-2"/>
+    <path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M47.2459,28.2025a17.9177,17.9177,0,0,1,.11,1.95,17.0185,17.0185,0,0,1-1.06,5.97"/>
+    <path d="M42.0135,28.7928a2,2,0,1,1-2-2,2.0007,2.0007,0,0,1,2,2"/>
+    <path d="M34.0135,28.7928a2,2,0,1,1-2-2,2.0007,2.0007,0,0,1,2,2"/>
+    <path d="M36.0134,38.7952a7.6528,7.6528,0,0,1-3.4473-.8579,1,1,0,0,1,.8946-1.7891,5.3772,5.3772,0,0,0,5.1054,0,1,1,0,0,1,.8946,1.7891A7.6528,7.6528,0,0,1,36.0134,38.7952Z"/>
+    <path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M36.0422,46.9384c5.0174-.1253,9.5065-6.0464,10.3-10.8959-3.2477,5.68-8.6155,4.61-8.6155,4.61L36.02,38.0191l-1.7063,2.6338s-5.3678,1.0695-8.6155-4.61c.7933,4.85,5.2824,10.7706,10.3,10.8959"/>
+    <path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M34.0159,21.7725c-1.28.77-2.47,1.53-3.53,2.24-3.13,2.09-5.21,3.75-5.7,4.14-.07.06-.11.09-.11.09s-3.96-5.34-2.24-10.72c0,0,2.24-8.08,13.04-8.08,7.7,0,11.59,3.17,13.71,5.29,5.58,7.91-.72,12.65-1.94,13.47-.13.09-.2.13-.2.13s-4.41-3.38-9.03-7.75"/>
+    <path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M37.9859,19.5125a66.4314,66.4314,0,0,1,11.2-4.78"/>
+    <path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M36.0135,25.82h0a2,2,0,0,1-2-2v-4a2,2,0,0,1,2-2h0a2,2,0,0,1,2,2v4A2,2,0,0,1,36.0135,25.82Z"/>
+    <line x1="40.7828" x2="40.7828" y1="54.9709" y2="59.7193" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
+  </g>
+</svg>

+ 28 - 0
frontend/app_flowy/assets/images/emoji/1F9DF.svg

@@ -0,0 +1,28 @@
+<svg id="emoji" viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
+  <g id="color">
+    <circle cx="36.9435" cy="28.0392" r="10" fill="#e67a94"/>
+    <path fill="#b1cc33" d="M35.1773,17.1567c-5.8765.5287-10.5159,6.6407-10.5159,14.1212,0,7.8277,5.0764,14.1733,11.3386,14.1733s11.3387-6.3456,11.3387-14.1733a17.2594,17.2594,0,0,0-.7674-5.0941,9.9175,9.9175,0,0,1-11.394-9.0271Z"/>
+    <path fill="#b1cc33" d="M54.9375,60.9094s2-12.6032-10-12.6032c-3.1918,2.128-5.9264,3.5985-9,3.5922h.125c-3.0736.0063-5.8081-1.4642-9-3.5922-12,0-10,12.6032-10,12.6032"/>
+    <path fill="#d0cfce" d="M46.354,48.2762l-.9075.8851a15.6367,15.6367,0,0,1-5.3451,5.81l-2.0029,5.7447-2-4.5853c-3.337.02-6.674-2.2824-9.3481-6.9693L26.2,48.25C15.27,48.9272,17.1609,60.937,17.1609,60.937l3.1531-.007,2.0839-4.7825,1.5287,4.8108,31.18-.05"/>
+    <path fill="#fcea2b" d="M32,38.2987s8-3.11,8,0C40,44.5411,32,38.2987,32,38.2987Z"/>
+    <path fill="#9b9b9a" d="M42.5077,52.7252c6.4062,1.661,6.5337,5.0343,7.1339,8.1842h5.1128s1.8925-11.904-9.0495-12.5747q-.46-.0282-.9505-.0285"/>
+    <path fill="#5c9e31" d="M30,29.9325v2.041a2.1088,2.1088,0,0,0,1.7064,2.1339A2.0016,2.0016,0,0,0,34,32.1283V29.9325a.0571.0571,0,0,0-.0571-.0571H30.0573A.057.057,0,0,0,30,29.9325Z"/>
+    <path fill="#5c9e31" d="M38,29.8754v2.2529a2,2,0,0,0,4,0V29.8754Z"/>
+    <path fill="#fff" d="M42,29.9171a2,2,0,1,1-2-2,2.0007,2.0007,0,0,1,2,2"/>
+    <path fill="#fff" d="M34,29.9171a2,2,0,1,1-2-2,2.0007,2.0007,0,0,1,2,2"/>
+  </g>
+  <g id="line">
+    <path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M35.1773,17.1567c-5.8765.5287-10.5159,6.6407-10.5159,14.1212,0,7.8277,5.0764,14.1733,11.3386,14.1733s11.3387-6.3456,11.3387-14.1733a17.2594,17.2594,0,0,0-.7674-5.0941,9.9175,9.9175,0,0,1-11.394-9.0271Z"/>
+    <path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M45.3836,25.2642a2,2,0,0,0-1.3286-2.18,1.9753,1.9753,0,0,0,.3775-.8915,1.9931,1.9931,0,0,0-2.79-2.1141,1.9923,1.9923,0,0,0-3.3909-1.3058,1.9922,1.9922,0,0,0-2.7713-.4814"/>
+    <path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M35.1773,17.1567q.0408.48.1262.9464a9.9978,9.9978,0,0,0,9.4213,8.1874q.2037.0082.4093.0082a10.0162,10.0162,0,0,0,1.4372-.1149"/>
+    <path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M32,38.2987s8-3.11,8,0C40,44.5411,32,38.2987,32,38.2987Z"/>
+    <line x1="39.4332" x2="35.134" y1="40.0153" y2="40.0153" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
+    <line x1="36.9435" x2="36.9435" y1="40.0153" y2="37.0896" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/>
+    <path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M26.6519,48.9249c2.6741,4.6869,6.0111,6.99,9.3481,6.9693l2,4.0183,2.0029-5.1777a15.6376,15.6376,0,0,0,5.3451-5.81"/>
+    <line x1="43.5496" x2="42.7939" y1="57.2401" y2="59.9029" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
+    <polyline fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" points="20.216 59.871 22.3 56.308 23.828 59.871"/>
+    <path d="M55,60.9171a1,1,0,0,1-1-1v-3c0-4.4517-4.4961-7.81-8.6518-7.9922-6.2051,5.0117-12.4912,5.0117-18.6963,0C22.4961,49.1066,18,52.4654,18,56.9171v3a1,1,0,1,1-2,0v-3c0-5.3247,5.14-9.9976,11-10h0a.9994.9994,0,0,1,.64.2319c5.625,4.6875,11.0947,4.6875,16.72,0a.9994.9994,0,0,1,.64-.2319h.0005C50.86,46.92,56,51.5924,56,56.9171v3A1,1,0,0,1,55,60.9171Z"/>
+    <path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="1.5" d="M42,29.9171a2,2,0,1,1-2-2,2.0007,2.0007,0,0,1,2,2"/>
+    <path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="1.5" d="M34,29.9171a2,2,0,1,1-2-2,2.0007,2.0007,0,0,1,2,2"/>
+  </g>
+</svg>

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

@@ -16,7 +16,7 @@ class BlankPluginBuilder extends PluginBuilder {
   String get menuName => "Blank";
 
   @override
-  PluginType get pluginType => DefaultPlugin.blank.type();
+  PluginType get pluginType => PluginType.blank;
 }
 
 class BlankPluginConfig implements PluginConfig {

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

@@ -0,0 +1,290 @@
+import 'dart:async';
+import 'package:app_flowy/plugins/grid/application/block/block_cache.dart';
+import 'package:app_flowy/plugins/grid/application/field/field_cache.dart';
+import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
+import 'package:app_flowy/plugins/grid/application/row/row_service.dart';
+import 'package:appflowy_board/appflowy_board.dart';
+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/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 'dart:collection';
+
+import 'board_data_controller.dart';
+import 'group_controller.dart';
+
+part 'board_bloc.freezed.dart';
+
+class BoardBloc extends Bloc<BoardEvent, BoardState> {
+  final BoardDataController _dataController;
+  late final AFBoardDataController afBoardDataController;
+  final MoveRowFFIService _rowService;
+  Map<String, GroupController> groupControllers = {};
+
+  GridFieldCache get fieldCache => _dataController.fieldCache;
+  String get gridId => _dataController.gridId;
+
+  BoardBloc({required ViewPB view})
+      : _rowService = MoveRowFFIService(gridId: view.id),
+        _dataController = BoardDataController(view: view),
+        super(BoardState.initial(view.id)) {
+    afBoardDataController = AFBoardDataController(
+      onMoveColumn: (
+        fromIndex,
+        toIndex,
+      ) {},
+      onMoveColumnItem: (
+        columnId,
+        fromIndex,
+        toIndex,
+      ) {
+        final fromRow = groupControllers[columnId]?.rowAtIndex(fromIndex);
+        final toRow = groupControllers[columnId]?.rowAtIndex(toIndex);
+        _moveRow(fromRow, toRow);
+      },
+      onMoveColumnItemToColumn: (
+        fromColumnId,
+        fromIndex,
+        toColumnId,
+        toIndex,
+      ) {
+        final fromRow = groupControllers[fromColumnId]?.rowAtIndex(fromIndex);
+        final toRow = groupControllers[toColumnId]?.rowAtIndex(toIndex);
+        _moveRow(fromRow, toRow);
+      },
+    );
+
+    on<BoardEvent>(
+      (event, emit) async {
+        await event.when(
+          initial: () async {
+            _startListening();
+            await _loadGrid(emit);
+          },
+          createRow: (groupId) async {
+            final result = await _dataController.createBoardCard(groupId);
+            result.fold(
+              (rowPB) {
+                emit(state.copyWith(editingRow: some(rowPB)));
+              },
+              (err) => Log.error(err),
+            );
+          },
+          endEditRow: (rowId) {
+            assert(state.editingRow.isSome());
+            state.editingRow.fold(() => null, (row) {
+              assert(row.id == rowId);
+              emit(state.copyWith(editingRow: none()));
+            });
+          },
+          didReceiveGridUpdate: (GridPB grid) {
+            emit(state.copyWith(grid: Some(grid)));
+          },
+          didReceiveRows: (List<RowInfo> rowInfos) {
+            emit(state.copyWith(rowInfos: rowInfos));
+          },
+          didReceiveError: (FlowyError error) {
+            emit(state.copyWith(noneOrError: some(error)));
+          },
+        );
+      },
+    );
+  }
+
+  void _moveRow(RowPB? fromRow, RowPB? toRow) {
+    if (fromRow != null && toRow != null) {
+      _rowService
+          .moveRow(
+        fromRowId: fromRow.id,
+        toRowId: toRow.id,
+      )
+          .then((result) {
+        result.fold((l) => null, (r) => add(BoardEvent.didReceiveError(r)));
+      });
+    }
+  }
+
+  @override
+  Future<void> close() async {
+    await _dataController.dispose();
+    for (final controller in groupControllers.values) {
+      controller.dispose();
+    }
+    return super.close();
+  }
+
+  void initializeGroups(List<GroupPB> groups) {
+    for (final group in groups) {
+      final delegate = GroupControllerDelegateImpl(afBoardDataController);
+      final controller = GroupController(
+        gridId: state.gridId,
+        group: group,
+        delegate: delegate,
+      );
+      controller.startListening();
+      groupControllers[controller.group.groupId] = (controller);
+    }
+  }
+
+  GridRowCache? getRowCache(String blockId) {
+    final GridBlockCache? blockCache = _dataController.blocks[blockId];
+    return blockCache?.rowCache;
+  }
+
+  void _startListening() {
+    _dataController.addListener(
+      onGridChanged: (grid) {
+        if (!isClosed) {
+          add(BoardEvent.didReceiveGridUpdate(grid));
+        }
+      },
+      didLoadGroups: (groups) {
+        List<AFBoardColumnData> columns = groups.map((group) {
+          return AFBoardColumnData(
+            id: group.groupId,
+            desc: group.desc,
+            items: _buildRows(group.rows),
+            customData: group,
+          );
+        }).toList();
+
+        afBoardDataController.addColumns(columns);
+        initializeGroups(groups);
+      },
+      onRowsChanged: (List<RowInfo> rowInfos, RowsChangedReason reason) {
+        add(BoardEvent.didReceiveRows(rowInfos));
+      },
+      onError: (err) {
+        Log.error(err);
+      },
+    );
+  }
+
+  List<AFColumnItem> _buildRows(List<RowPB> rows) {
+    final items = rows.map((row) {
+      return BoardColumnItem(row: row);
+    }).toList();
+
+    return <AFColumnItem>[...items];
+  }
+
+  Future<void> _loadGrid(Emitter<BoardState> emit) async {
+    final result = await _dataController.loadData();
+    result.fold(
+      (grid) => emit(
+        state.copyWith(loadingState: GridLoadingState.finish(left(unit))),
+      ),
+      (err) => emit(
+        state.copyWith(loadingState: GridLoadingState.finish(right(err))),
+      ),
+    );
+  }
+}
+
+@freezed
+class BoardEvent with _$BoardEvent {
+  const factory BoardEvent.initial() = InitialGrid;
+  const factory BoardEvent.createRow(String groupId) = _CreateRow;
+  const factory BoardEvent.endEditRow(String rowId) = _EndEditRow;
+  const factory BoardEvent.didReceiveError(FlowyError error) = _DidReceiveError;
+  const factory BoardEvent.didReceiveRows(List<RowInfo> rowInfos) =
+      _DidReceiveRows;
+  const factory BoardEvent.didReceiveGridUpdate(
+    GridPB grid,
+  ) = _DidReceiveGridUpdate;
+}
+
+@freezed
+class BoardState with _$BoardState {
+  const factory BoardState({
+    required String gridId,
+    required Option<GridPB> grid,
+    required Option<RowPB> editingRow,
+    required List<RowInfo> rowInfos,
+    required GridLoadingState loadingState,
+    required Option<FlowyError> noneOrError,
+  }) = _BoardState;
+
+  factory BoardState.initial(String gridId) => BoardState(
+        rowInfos: [],
+        grid: none(),
+        gridId: gridId,
+        editingRow: none(),
+        noneOrError: none(),
+        loadingState: const _Loading(),
+      );
+}
+
+@freezed
+class GridLoadingState with _$GridLoadingState {
+  const factory GridLoadingState.loading() = _Loading;
+  const factory GridLoadingState.finish(
+      Either<Unit, FlowyError> successOrFail) = _Finish;
+}
+
+class GridFieldEquatable extends Equatable {
+  final UnmodifiableListView<FieldPB> _fields;
+  const GridFieldEquatable(
+    UnmodifiableListView<FieldPB> fields,
+  ) : _fields = fields;
+
+  @override
+  List<Object?> get props {
+    if (_fields.isEmpty) {
+      return [];
+    }
+
+    return [
+      _fields.length,
+      _fields
+          .map((field) => field.width)
+          .reduce((value, element) => value + element),
+    ];
+  }
+
+  UnmodifiableListView<FieldPB> get value => UnmodifiableListView(_fields);
+}
+
+class BoardColumnItem extends AFColumnItem {
+  final RowPB row;
+
+  BoardColumnItem({required this.row});
+
+  @override
+  String get id => row.id;
+}
+
+class CreateCardItem extends AFColumnItem {
+  @override
+  String get id => '$CreateCardItem';
+}
+
+class GroupControllerDelegateImpl extends GroupControllerDelegate {
+  final AFBoardDataController controller;
+
+  GroupControllerDelegateImpl(this.controller);
+
+  @override
+  void insertRow(String groupId, RowPB row, int? index) {
+    final item = BoardColumnItem(row: row);
+    if (index != null) {
+      controller.insertColumnItem(groupId, index, item);
+    } else {
+      controller.addColumnItem(groupId, item);
+    }
+  }
+
+  @override
+  void removeRow(String groupId, String rowId) {
+    controller.removeColumnItem(groupId, rowId);
+  }
+
+  @override
+  void updateRow(String groupId, RowPB row) {
+    //
+  }
+}

+ 142 - 0
frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart

@@ -0,0 +1,142 @@
+import 'dart:collection';
+
+import 'package:app_flowy/plugins/grid/application/block/block_cache.dart';
+import 'package:app_flowy/plugins/grid/application/field/field_cache.dart';
+import 'package:app_flowy/plugins/grid/application/grid_service.dart';
+import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
+import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
+import 'dart:async';
+import 'package:dartz/dartz.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart';
+
+typedef OnFieldsChanged = void Function(UnmodifiableListView<FieldPB>);
+typedef OnGridChanged = void Function(GridPB);
+typedef DidLoadGroups = void Function(List<GroupPB>);
+typedef OnRowsChanged = void Function(
+  List<RowInfo>,
+  RowsChangedReason,
+);
+typedef OnError = void Function(FlowyError);
+
+class BoardDataController {
+  final String gridId;
+  final GridFFIService _gridFFIService;
+  final GridFieldCache fieldCache;
+
+  // key: the block id
+  final LinkedHashMap<String, GridBlockCache> _blocks;
+  LinkedHashMap<String, GridBlockCache> get blocks => _blocks;
+
+  OnFieldsChanged? _onFieldsChanged;
+  OnGridChanged? _onGridChanged;
+  DidLoadGroups? _didLoadGroup;
+  OnRowsChanged? _onRowsChanged;
+  OnError? _onError;
+
+  List<RowInfo> get rowInfos {
+    final List<RowInfo> rows = [];
+    for (var block in _blocks.values) {
+      rows.addAll(block.rows);
+    }
+    return rows;
+  }
+
+  BoardDataController({required ViewPB view})
+      : gridId = view.id,
+        _blocks = LinkedHashMap.new(),
+        _gridFFIService = GridFFIService(gridId: view.id),
+        fieldCache = GridFieldCache(gridId: view.id);
+
+  void addListener({
+    OnGridChanged? onGridChanged,
+    OnFieldsChanged? onFieldsChanged,
+    DidLoadGroups? didLoadGroups,
+    OnRowsChanged? onRowsChanged,
+    OnError? onError,
+  }) {
+    _onGridChanged = onGridChanged;
+    _onFieldsChanged = onFieldsChanged;
+    _didLoadGroup = didLoadGroups;
+    _onRowsChanged = onRowsChanged;
+    _onError = onError;
+
+    fieldCache.addListener(onFields: (fields) {
+      _onFieldsChanged?.call(UnmodifiableListView(fields));
+    });
+  }
+
+  Future<Either<Unit, FlowyError>> loadData() async {
+    final result = await _gridFFIService.loadGrid();
+    return Future(
+      () => result.fold(
+        (grid) async {
+          _onGridChanged?.call(grid);
+
+          return await _loadFields(grid).then((result) {
+            return result.fold(
+              (l) {
+                _loadGroups(grid.blocks);
+                return left(l);
+              },
+              (err) => right(err),
+            );
+          });
+        },
+        (err) => right(err),
+      ),
+    );
+  }
+
+  Future<Either<RowPB, FlowyError>> createBoardCard(String groupId) {
+    return _gridFFIService.createBoardCard(groupId);
+  }
+
+  Future<void> dispose() async {
+    await _gridFFIService.closeGrid();
+    await fieldCache.dispose();
+
+    for (final blockCache in _blocks.values) {
+      blockCache.dispose();
+    }
+  }
+
+  Future<Either<Unit, FlowyError>> _loadFields(GridPB grid) async {
+    final result = await _gridFFIService.getFields(fieldIds: grid.fields);
+    return Future(
+      () => result.fold(
+        (fields) {
+          fieldCache.fields = fields.items;
+          _onFieldsChanged?.call(UnmodifiableListView(fieldCache.fields));
+          return left(unit);
+        },
+        (err) => right(err),
+      ),
+    );
+  }
+
+  Future<void> _loadGroups(List<BlockPB> blocks) async {
+    for (final block in blocks) {
+      final cache = GridBlockCache(
+        gridId: gridId,
+        block: block,
+        fieldCache: fieldCache,
+      );
+
+      cache.addListener(onRowsChanged: (reason) {
+        _onRowsChanged?.call(rowInfos, reason);
+      });
+      _blocks[block.id] = cache;
+    }
+
+    final result = await _gridFFIService.loadGroups();
+    return Future(
+      () => result.fold(
+        (groups) {
+          _didLoadGroup?.call(groups.items);
+        },
+        (err) => _onError?.call(err),
+      ),
+    );
+  }
+}

+ 71 - 0
frontend/app_flowy/lib/plugins/board/application/card/board_checkbox_cell_bloc.dart

@@ -0,0 +1,71 @@
+import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+import 'dart:async';
+
+part 'board_checkbox_cell_bloc.freezed.dart';
+
+class BoardCheckboxCellBloc
+    extends Bloc<BoardCheckboxCellEvent, BoardCheckboxCellState> {
+  final GridCheckboxCellController cellController;
+  void Function()? _onCellChangedFn;
+  BoardCheckboxCellBloc({
+    required this.cellController,
+  }) : super(BoardCheckboxCellState.initial(cellController)) {
+    on<BoardCheckboxCellEvent>(
+      (event, emit) async {
+        await event.when(
+          initial: () async {
+            _startListening();
+          },
+          didReceiveCellUpdate: (cellData) {
+            emit(state.copyWith(isSelected: _isSelected(cellData)));
+          },
+        );
+      },
+    );
+  }
+
+  @override
+  Future<void> close() async {
+    if (_onCellChangedFn != null) {
+      cellController.removeListener(_onCellChangedFn!);
+      _onCellChangedFn = null;
+    }
+    cellController.dispose();
+    return super.close();
+  }
+
+  void _startListening() {
+    _onCellChangedFn = cellController.startListening(
+      onCellChanged: ((cellContent) {
+        if (!isClosed) {
+          add(BoardCheckboxCellEvent.didReceiveCellUpdate(cellContent ?? ""));
+        }
+      }),
+    );
+  }
+}
+
+@freezed
+class BoardCheckboxCellEvent with _$BoardCheckboxCellEvent {
+  const factory BoardCheckboxCellEvent.initial() = _InitialCell;
+  const factory BoardCheckboxCellEvent.didReceiveCellUpdate(
+      String cellContent) = _DidReceiveCellUpdate;
+}
+
+@freezed
+class BoardCheckboxCellState with _$BoardCheckboxCellState {
+  const factory BoardCheckboxCellState({
+    required bool isSelected,
+  }) = _CheckboxCellState;
+
+  factory BoardCheckboxCellState.initial(GridCellController context) {
+    return BoardCheckboxCellState(
+        isSelected: _isSelected(context.getCellData()));
+  }
+}
+
+bool _isSelected(String? cellData) {
+  return cellData == "Yes";
+}

+ 85 - 0
frontend/app_flowy/lib/plugins/board/application/card/board_date_cell_bloc.dart

@@ -0,0 +1,85 @@
+import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.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';
+part 'board_date_cell_bloc.freezed.dart';
+
+class BoardDateCellBloc extends Bloc<BoardDateCellEvent, BoardDateCellState> {
+  final GridDateCellController cellController;
+  void Function()? _onCellChangedFn;
+
+  BoardDateCellBloc({required this.cellController})
+      : super(BoardDateCellState.initial(cellController)) {
+    on<BoardDateCellEvent>(
+      (event, emit) async {
+        event.when(
+          initial: () => _startListening(),
+          didReceiveCellUpdate: (DateCellDataPB? cellData) {
+            emit(state.copyWith(
+                data: cellData, dateStr: _dateStrFromCellData(cellData)));
+          },
+          didReceiveFieldUpdate: (FieldPB value) =>
+              emit(state.copyWith(field: value)),
+        );
+      },
+    );
+  }
+
+  @override
+  Future<void> close() async {
+    if (_onCellChangedFn != null) {
+      cellController.removeListener(_onCellChangedFn!);
+      _onCellChangedFn = null;
+    }
+    cellController.dispose();
+    return super.close();
+  }
+
+  void _startListening() {
+    _onCellChangedFn = cellController.startListening(
+      onCellChanged: ((data) {
+        if (!isClosed) {
+          add(BoardDateCellEvent.didReceiveCellUpdate(data));
+        }
+      }),
+    );
+  }
+}
+
+@freezed
+class BoardDateCellEvent with _$BoardDateCellEvent {
+  const factory BoardDateCellEvent.initial() = _InitialCell;
+  const factory BoardDateCellEvent.didReceiveCellUpdate(DateCellDataPB? data) =
+      _DidReceiveCellUpdate;
+  const factory BoardDateCellEvent.didReceiveFieldUpdate(FieldPB field) =
+      _DidReceiveFieldUpdate;
+}
+
+@freezed
+class BoardDateCellState with _$BoardDateCellState {
+  const factory BoardDateCellState({
+    required DateCellDataPB? data,
+    required String dateStr,
+    required FieldPB field,
+  }) = _BoardDateCellState;
+
+  factory BoardDateCellState.initial(GridDateCellController context) {
+    final cellData = context.getCellData();
+
+    return BoardDateCellState(
+      field: context.field,
+      data: cellData,
+      dateStr: _dateStrFromCellData(cellData),
+    );
+  }
+}
+
+String _dateStrFromCellData(DateCellDataPB? cellData) {
+  String dateStr = "";
+  if (cellData != null) {
+    dateStr = cellData.date + " " + cellData.time;
+  }
+  return dateStr;
+}

+ 67 - 0
frontend/app_flowy/lib/plugins/board/application/card/board_number_cell_bloc.dart

@@ -0,0 +1,67 @@
+import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+import 'dart:async';
+
+part 'board_number_cell_bloc.freezed.dart';
+
+class BoardNumberCellBloc
+    extends Bloc<BoardNumberCellEvent, BoardNumberCellState> {
+  final GridNumberCellController cellController;
+  void Function()? _onCellChangedFn;
+  BoardNumberCellBloc({
+    required this.cellController,
+  }) : super(BoardNumberCellState.initial(cellController)) {
+    on<BoardNumberCellEvent>(
+      (event, emit) async {
+        await event.when(
+          initial: () async {
+            _startListening();
+          },
+          didReceiveCellUpdate: (content) {
+            emit(state.copyWith(content: content));
+          },
+        );
+      },
+    );
+  }
+
+  @override
+  Future<void> close() async {
+    if (_onCellChangedFn != null) {
+      cellController.removeListener(_onCellChangedFn!);
+      _onCellChangedFn = null;
+    }
+    cellController.dispose();
+    return super.close();
+  }
+
+  void _startListening() {
+    _onCellChangedFn = cellController.startListening(
+      onCellChanged: ((cellContent) {
+        if (!isClosed) {
+          add(BoardNumberCellEvent.didReceiveCellUpdate(cellContent ?? ""));
+        }
+      }),
+    );
+  }
+}
+
+@freezed
+class BoardNumberCellEvent with _$BoardNumberCellEvent {
+  const factory BoardNumberCellEvent.initial() = _InitialCell;
+  const factory BoardNumberCellEvent.didReceiveCellUpdate(String cellContent) =
+      _DidReceiveCellUpdate;
+}
+
+@freezed
+class BoardNumberCellState with _$BoardNumberCellState {
+  const factory BoardNumberCellState({
+    required String content,
+  }) = _BoardNumberCellState;
+
+  factory BoardNumberCellState.initial(GridCellController context) =>
+      BoardNumberCellState(
+        content: context.getCellData() ?? "",
+      );
+}

+ 76 - 0
frontend/app_flowy/lib/plugins/board/application/card/board_select_option_cell_bloc.dart

@@ -0,0 +1,76 @@
+import 'dart:async';
+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/plugins/grid/application/cell/cell_service/cell_service.dart';
+
+part 'board_select_option_cell_bloc.freezed.dart';
+
+class BoardSelectOptionCellBloc
+    extends Bloc<BoardSelectOptionCellEvent, BoardSelectOptionCellState> {
+  final GridSelectOptionCellController cellController;
+  void Function()? _onCellChangedFn;
+
+  BoardSelectOptionCellBloc({
+    required this.cellController,
+  }) : super(BoardSelectOptionCellState.initial(cellController)) {
+    on<BoardSelectOptionCellEvent>(
+      (event, emit) async {
+        await event.when(
+          initial: () async {
+            _startListening();
+          },
+          didReceiveOptions: (List<SelectOptionPB> selectedOptions) {
+            emit(state.copyWith(selectedOptions: selectedOptions));
+          },
+        );
+      },
+    );
+  }
+
+  @override
+  Future<void> close() async {
+    if (_onCellChangedFn != null) {
+      cellController.removeListener(_onCellChangedFn!);
+      _onCellChangedFn = null;
+    }
+    cellController.dispose();
+    return super.close();
+  }
+
+  void _startListening() {
+    _onCellChangedFn = cellController.startListening(
+      onCellChanged: ((selectOptionContext) {
+        if (!isClosed) {
+          add(BoardSelectOptionCellEvent.didReceiveOptions(
+            selectOptionContext?.selectOptions ?? [],
+          ));
+        }
+      }),
+    );
+  }
+}
+
+@freezed
+class BoardSelectOptionCellEvent with _$BoardSelectOptionCellEvent {
+  const factory BoardSelectOptionCellEvent.initial() = _InitialCell;
+  const factory BoardSelectOptionCellEvent.didReceiveOptions(
+    List<SelectOptionPB> selectedOptions,
+  ) = _DidReceiveOptions;
+}
+
+@freezed
+class BoardSelectOptionCellState with _$BoardSelectOptionCellState {
+  const factory BoardSelectOptionCellState({
+    required List<SelectOptionPB> selectedOptions,
+  }) = _BoardSelectOptionCellState;
+
+  factory BoardSelectOptionCellState.initial(
+      GridSelectOptionCellController context) {
+    final data = context.getCellData();
+
+    return BoardSelectOptionCellState(
+      selectedOptions: data?.selectOptions ?? [],
+    );
+  }
+}

+ 66 - 0
frontend/app_flowy/lib/plugins/board/application/card/board_text_cell_bloc.dart

@@ -0,0 +1,66 @@
+import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+import 'dart:async';
+
+part 'board_text_cell_bloc.freezed.dart';
+
+class BoardTextCellBloc extends Bloc<BoardTextCellEvent, BoardTextCellState> {
+  final GridCellController cellController;
+  void Function()? _onCellChangedFn;
+  BoardTextCellBloc({
+    required this.cellController,
+  }) : super(BoardTextCellState.initial(cellController)) {
+    on<BoardTextCellEvent>(
+      (event, emit) async {
+        await event.when(
+          initial: () async {
+            _startListening();
+          },
+          didReceiveCellUpdate: (content) {
+            emit(state.copyWith(content: content));
+          },
+        );
+      },
+    );
+  }
+
+  @override
+  Future<void> close() async {
+    if (_onCellChangedFn != null) {
+      cellController.removeListener(_onCellChangedFn!);
+      _onCellChangedFn = null;
+    }
+    cellController.dispose();
+    return super.close();
+  }
+
+  void _startListening() {
+    _onCellChangedFn = cellController.startListening(
+      onCellChanged: ((cellContent) {
+        if (!isClosed) {
+          add(BoardTextCellEvent.didReceiveCellUpdate(cellContent ?? ""));
+        }
+      }),
+    );
+  }
+}
+
+@freezed
+class BoardTextCellEvent with _$BoardTextCellEvent {
+  const factory BoardTextCellEvent.initial() = _InitialCell;
+  const factory BoardTextCellEvent.didReceiveCellUpdate(String cellContent) =
+      _DidReceiveCellUpdate;
+}
+
+@freezed
+class BoardTextCellState with _$BoardTextCellState {
+  const factory BoardTextCellState({
+    required String content,
+  }) = _BoardTextCellState;
+
+  factory BoardTextCellState.initial(GridCellController context) =>
+      BoardTextCellState(
+        content: context.getCellData() ?? "",
+      );
+}

+ 78 - 0
frontend/app_flowy/lib/plugins/board/application/card/board_url_cell_bloc.dart

@@ -0,0 +1,78 @@
+import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.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';
+
+part 'board_url_cell_bloc.freezed.dart';
+
+class BoardURLCellBloc extends Bloc<BoardURLCellEvent, BoardURLCellState> {
+  final GridURLCellController cellController;
+  void Function()? _onCellChangedFn;
+  BoardURLCellBloc({
+    required this.cellController,
+  }) : super(BoardURLCellState.initial(cellController)) {
+    on<BoardURLCellEvent>(
+      (event, emit) async {
+        event.when(
+          initial: () {
+            _startListening();
+          },
+          didReceiveCellUpdate: (cellData) {
+            emit(state.copyWith(
+              content: cellData?.content ?? "",
+              url: cellData?.url ?? "",
+            ));
+          },
+          updateURL: (String url) {
+            cellController.saveCellData(url, deduplicate: true);
+          },
+        );
+      },
+    );
+  }
+
+  @override
+  Future<void> close() async {
+    if (_onCellChangedFn != null) {
+      cellController.removeListener(_onCellChangedFn!);
+      _onCellChangedFn = null;
+    }
+    cellController.dispose();
+    return super.close();
+  }
+
+  void _startListening() {
+    _onCellChangedFn = cellController.startListening(
+      onCellChanged: ((cellData) {
+        if (!isClosed) {
+          add(BoardURLCellEvent.didReceiveCellUpdate(cellData));
+        }
+      }),
+    );
+  }
+}
+
+@freezed
+class BoardURLCellEvent with _$BoardURLCellEvent {
+  const factory BoardURLCellEvent.initial() = _InitialCell;
+  const factory BoardURLCellEvent.updateURL(String url) = _UpdateURL;
+  const factory BoardURLCellEvent.didReceiveCellUpdate(URLCellDataPB? cell) =
+      _DidReceiveCellUpdate;
+}
+
+@freezed
+class BoardURLCellState with _$BoardURLCellState {
+  const factory BoardURLCellState({
+    required String content,
+    required String url,
+  }) = _BoardURLCellState;
+
+  factory BoardURLCellState.initial(GridURLCellController context) {
+    final cellData = context.getCellData();
+    return BoardURLCellState(
+      content: cellData?.content ?? "",
+      url: cellData?.url ?? "",
+    );
+  }
+}

+ 116 - 0
frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart

@@ -0,0 +1,116 @@
+import 'dart:collection';
+import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
+import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
+import 'package:app_flowy/plugins/grid/application/row/row_service.dart';
+import 'package:equatable/equatable.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:flutter_bloc/flutter_bloc.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+import 'dart:async';
+
+import 'card_data_controller.dart';
+
+part 'card_bloc.freezed.dart';
+
+class BoardCardBloc extends Bloc<BoardCardEvent, BoardCardState> {
+  final RowFFIService _rowService;
+  final CardDataController _dataController;
+
+  BoardCardBloc({
+    required String gridId,
+    required CardDataController dataController,
+  })  : _rowService = RowFFIService(
+          gridId: gridId,
+          blockId: dataController.rowPB.blockId,
+        ),
+        _dataController = dataController,
+        super(BoardCardState.initial(
+            dataController.rowPB, dataController.loadData())) {
+    on<BoardCardEvent>(
+      (event, emit) async {
+        await event.map(
+          initial: (_InitialRow value) async {
+            await _startListening();
+          },
+          didReceiveCells: (_DidReceiveCells value) async {
+            final cells = value.gridCellMap.values
+                .map((e) => GridCellEquatable(e.field))
+                .toList();
+            emit(state.copyWith(
+              gridCellMap: value.gridCellMap,
+              cells: UnmodifiableListView(cells),
+              changeReason: value.reason,
+            ));
+          },
+        );
+      },
+    );
+  }
+
+  @override
+  Future<void> close() async {
+    _dataController.dispose();
+    return super.close();
+  }
+
+  RowInfo rowInfo() {
+    return RowInfo(
+      gridId: _rowService.gridId,
+      fields: UnmodifiableListView(
+        state.cells.map((cell) => cell._field).toList(),
+      ),
+      rowPB: state.rowPB,
+    );
+  }
+
+  Future<void> _startListening() async {
+    _dataController.addListener(
+      onRowChanged: (cells, reason) {
+        if (!isClosed) {
+          add(BoardCardEvent.didReceiveCells(cells, reason));
+        }
+      },
+    );
+  }
+}
+
+@freezed
+class BoardCardEvent with _$BoardCardEvent {
+  const factory BoardCardEvent.initial() = _InitialRow;
+  const factory BoardCardEvent.didReceiveCells(
+      GridCellMap gridCellMap, RowsChangedReason reason) = _DidReceiveCells;
+}
+
+@freezed
+class BoardCardState with _$BoardCardState {
+  const factory BoardCardState({
+    required RowPB rowPB,
+    required GridCellMap gridCellMap,
+    required UnmodifiableListView<GridCellEquatable> cells,
+    RowsChangedReason? changeReason,
+  }) = _BoardCardState;
+
+  factory BoardCardState.initial(RowPB rowPB, GridCellMap cellDataMap) =>
+      BoardCardState(
+        rowPB: rowPB,
+        gridCellMap: cellDataMap,
+        cells: UnmodifiableListView(
+          cellDataMap.values.map((e) => GridCellEquatable(e.field)).toList(),
+        ),
+      );
+}
+
+class GridCellEquatable extends Equatable {
+  final FieldPB _field;
+
+  const GridCellEquatable(FieldPB field) : _field = field;
+
+  @override
+  List<Object?> get props => [
+        _field.id,
+        _field.fieldType,
+        _field.visibility,
+        _field.width,
+      ];
+}

+ 49 - 0
frontend/app_flowy/lib/plugins/board/application/card/card_data_controller.dart

@@ -0,0 +1,49 @@
+import 'package:app_flowy/plugins/board/presentation/card/card_cell_builder.dart';
+import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
+import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_field_notifier.dart';
+import 'package:app_flowy/plugins/grid/application/field/field_cache.dart';
+import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
+import 'package:flutter/foundation.dart';
+
+typedef OnCardChanged = void Function(GridCellMap, RowsChangedReason);
+
+class CardDataController extends BoardCellBuilderDelegate {
+  final RowPB rowPB;
+  final GridFieldCache _fieldCache;
+  final GridRowCache _rowCache;
+  final List<VoidCallback> _onCardChangedListeners = [];
+
+  CardDataController({
+    required this.rowPB,
+    required GridFieldCache fieldCache,
+    required GridRowCache rowCache,
+  })  : _fieldCache = fieldCache,
+        _rowCache = rowCache;
+
+  GridCellMap loadData() {
+    return _rowCache.loadGridCells(rowPB.id);
+  }
+
+  void addListener({OnCardChanged? onRowChanged}) {
+    _onCardChangedListeners.add(_rowCache.addListener(
+      rowId: rowPB.id,
+      onCellUpdated: onRowChanged,
+    ));
+  }
+
+  void dispose() {
+    for (final fn in _onCardChangedListeners) {
+      _rowCache.removeRowListener(fn);
+    }
+  }
+
+  @override
+  GridCellFieldNotifier buildFieldNotifier() {
+    return GridCellFieldNotifier(
+        notifier: GridCellFieldNotifierImpl(_fieldCache));
+  }
+
+  @override
+  GridCellCache get cellCache => _rowCache.cellCache;
+}

+ 20 - 0
frontend/app_flowy/lib/plugins/board/application/group.dart

@@ -0,0 +1,20 @@
+import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
+
+class BoardGroupService {
+  final String gridId;
+  FieldPB? groupField;
+
+  BoardGroupService(this.gridId);
+
+  void setGroupField(FieldPB field) {
+    groupField = field;
+  }
+}
+
+abstract class CanBeGroupField {
+  String get groupContent;
+}
+
+// class SingleSelectGroup extends CanBeGroupField {
+//   final SingleSelectTypeOptionContext typeOptionContext;
+// }

+ 63 - 0
frontend/app_flowy/lib/plugins/board/application/group_controller.dart

@@ -0,0 +1,63 @@
+import 'package:flowy_sdk/log.dart';
+import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart';
+
+import 'group_listener.dart';
+
+typedef OnGroupError = void Function(FlowyError);
+
+abstract class GroupControllerDelegate {
+  void removeRow(String groupId, String rowId);
+  void insertRow(String groupId, RowPB row, int? index);
+  void updateRow(String groupId, RowPB row);
+}
+
+class GroupController {
+  final GroupPB group;
+  final GroupListener _listener;
+  final GroupControllerDelegate delegate;
+
+  GroupController({
+    required String gridId,
+    required this.group,
+    required this.delegate,
+  }) : _listener = GroupListener(group);
+
+  RowPB? rowAtIndex(int index) {
+    if (index < group.rows.length) {
+      return group.rows[index];
+    } else {
+      return null;
+    }
+  }
+
+  void startListening() {
+    _listener.start(onGroupChanged: (result) {
+      result.fold(
+        (GroupRowsChangesetPB changeset) {
+          for (final insertedRow in changeset.insertedRows) {
+            final index = insertedRow.hasIndex() ? insertedRow.index : null;
+            delegate.insertRow(
+              group.groupId,
+              insertedRow.row,
+              index,
+            );
+          }
+
+          for (final deletedRow in changeset.deletedRows) {
+            delegate.removeRow(group.groupId, deletedRow);
+          }
+
+          for (final updatedRow in changeset.updatedRows) {
+            delegate.updateRow(group.groupId, updatedRow);
+          }
+        },
+        (err) => Log.error(err),
+      );
+    });
+  }
+
+  Future<void> dispose() async {
+    _listener.stop();
+  }
+}

+ 51 - 0
frontend/app_flowy/lib/plugins/board/application/group_listener.dart

@@ -0,0 +1,51 @@
+import 'dart:typed_data';
+
+import 'package:app_flowy/core/grid_notification.dart';
+import 'package:flowy_infra/notifier.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/protobuf/flowy-grid/group.pb.dart';
+import 'package:dartz/dartz.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/group_changeset.pb.dart';
+
+typedef UpdateGroupNotifiedValue = Either<GroupRowsChangesetPB, FlowyError>;
+
+class GroupListener {
+  final GroupPB group;
+  PublishNotifier<UpdateGroupNotifiedValue>? _groupNotifier = PublishNotifier();
+  GridNotificationListener? _listener;
+  GroupListener(this.group);
+
+  void start({
+    required void Function(UpdateGroupNotifiedValue) onGroupChanged,
+  }) {
+    _groupNotifier?.addPublishListener(onGroupChanged);
+    _listener = GridNotificationListener(
+      objectId: group.groupId,
+      handler: _handler,
+    );
+  }
+
+  void _handler(
+    GridNotification ty,
+    Either<Uint8List, FlowyError> result,
+  ) {
+    switch (ty) {
+      case GridNotification.DidUpdateGroup:
+        result.fold(
+          (payload) => _groupNotifier?.value =
+              left(GroupRowsChangesetPB.fromBuffer(payload)),
+          (error) => _groupNotifier?.value = right(error),
+        );
+        break;
+      default:
+        break;
+    }
+  }
+
+  Future<void> stop() async {
+    await _listener?.stop();
+    _groupNotifier?.dispose();
+    _groupNotifier = null;
+  }
+}

+ 6 - 3
frontend/app_flowy/lib/plugins/board/board.dart

@@ -20,15 +20,18 @@ class BoardPluginBuilder implements PluginBuilder {
   String get menuName => "Board";
 
   @override
-  PluginType get pluginType => DefaultPlugin.board.type();
+  PluginType get pluginType => PluginType.board;
 
   @override
-  ViewDataType get dataType => ViewDataType.Grid;
+  ViewDataTypePB get dataType => ViewDataTypePB.Database;
+
+  @override
+  ViewLayoutTypePB? get subDataType => ViewLayoutTypePB.Board;
 }
 
 class BoardPluginConfig implements PluginConfig {
   @override
-  bool get creatable => true;
+  bool get creatable => false;
 }
 
 class BoardPlugin extends Plugin {

+ 162 - 5
frontend/app_flowy/lib/plugins/board/presentation/board_page.dart

@@ -1,17 +1,174 @@
 // ignore_for_file: unused_field
 
+import 'dart:collection';
+
+import 'package:app_flowy/plugins/board/application/card/card_data_controller.dart';
+import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
+import 'package:app_flowy/plugins/grid/application/field/field_cache.dart';
+import 'package:app_flowy/plugins/grid/application/row/row_data_controller.dart';
+import 'package:app_flowy/plugins/grid/presentation/widgets/cell/cell_builder.dart';
+import 'package:app_flowy/plugins/grid/presentation/widgets/row/row_detail.dart';
+import 'package:appflowy_board/appflowy_board.dart';
+import 'package:flowy_infra_ui/widget/error_page.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import '../../grid/application/row/row_cache.dart';
+import '../application/board_bloc.dart';
+import 'card/card.dart';
+import 'card/card_cell_builder.dart';
 
 class BoardPage extends StatelessWidget {
-  final ViewPB _view;
+  final ViewPB view;
+  BoardPage({required this.view, Key? key}) : super(key: ValueKey(view.id));
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocProvider(
+      create: (context) =>
+          BoardBloc(view: view)..add(const BoardEvent.initial()),
+      child: BlocBuilder<BoardBloc, BoardState>(
+        builder: (context, state) {
+          return state.loadingState.map(
+            loading: (_) =>
+                const Center(child: CircularProgressIndicator.adaptive()),
+            finish: (result) {
+              return result.successOrFail.fold(
+                (_) => BoardContent(),
+                (err) => FlowyErrorPage(err.toString()),
+              );
+            },
+          );
+        },
+      ),
+    );
+  }
+}
 
-  const BoardPage({required ViewPB view, Key? key})
-      : _view = view,
-        super(key: key);
+class BoardContent extends StatelessWidget {
+  final config = AFBoardConfig(
+    columnBackgroundColor: HexColor.fromHex('#F7F8FC'),
+  );
+
+  BoardContent({Key? key}) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
-    return Container();
+    return BlocBuilder<BoardBloc, BoardState>(
+      builder: (context, state) {
+        return Container(
+          color: Colors.white,
+          child: Padding(
+            padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
+            child: AFBoard(
+              // key: UniqueKey(),
+              scrollController: ScrollController(),
+              dataController: context.read<BoardBloc>().afBoardDataController,
+              headerBuilder: _buildHeader,
+              footBuilder: _buildFooter,
+              cardBuilder: (_, data) => _buildCard(context, data),
+              columnConstraints: const BoxConstraints.tightFor(width: 240),
+              config: AFBoardConfig(
+                columnBackgroundColor: HexColor.fromHex('#F7F8FC'),
+              ),
+            ),
+          ),
+        );
+      },
+    );
+  }
+
+  Widget _buildHeader(BuildContext context, AFBoardColumnData columnData) {
+    return AppFlowyColumnHeader(
+      icon: const Icon(Icons.lightbulb_circle),
+      title: Text(columnData.desc),
+      addIcon: const Icon(Icons.add, size: 20),
+      moreIcon: const Icon(Icons.more_horiz, size: 20),
+      height: 50,
+      margin: config.columnItemPadding,
+    );
+  }
+
+  Widget _buildFooter(BuildContext context, AFBoardColumnData columnData) {
+    return AppFlowyColumnFooter(
+        icon: const Icon(Icons.add, size: 20),
+        title: const Text('New'),
+        height: 50,
+        margin: config.columnItemPadding,
+        onAddButtonClick: () {
+          context.read<BoardBloc>().add(BoardEvent.createRow(columnData.id));
+        });
+  }
+
+  Widget _buildCard(BuildContext context, AFColumnItem item) {
+    final rowPB = (item as BoardColumnItem).row;
+    final rowCache = context.read<BoardBloc>().getRowCache(rowPB.blockId);
+
+    /// Return placeholder widget if the rowCache is null.
+    if (rowCache == null) return SizedBox(key: ObjectKey(item));
+
+    final fieldCache = context.read<BoardBloc>().fieldCache;
+    final gridId = context.read<BoardBloc>().gridId;
+    final cardController = CardDataController(
+      fieldCache: fieldCache,
+      rowCache: rowCache,
+      rowPB: rowPB,
+    );
+
+    final cellBuilder = BoardCellBuilder(cardController);
+    final isEditing = context.read<BoardBloc>().state.editingRow.fold(
+          () => false,
+          (editingRow) => editingRow.id == rowPB.id,
+        );
+
+    return AppFlowyColumnItemCard(
+      key: ObjectKey(item),
+      child: BoardCard(
+        gridId: gridId,
+        isEditing: isEditing,
+        cellBuilder: cellBuilder,
+        dataController: cardController,
+        onEditEditing: (rowId) {
+          context.read<BoardBloc>().add(BoardEvent.endEditRow(rowId));
+        },
+        openCard: (context) => _openCard(
+          gridId,
+          fieldCache,
+          rowPB,
+          rowCache,
+          context,
+        ),
+      ),
+    );
+  }
+
+  void _openCard(String gridId, GridFieldCache fieldCache, RowPB rowPB,
+      GridRowCache rowCache, BuildContext context) {
+    final rowInfo = RowInfo(
+      gridId: gridId,
+      fields: UnmodifiableListView(fieldCache.fields),
+      rowPB: rowPB,
+    );
+
+    final dataController = GridRowDataController(
+      rowInfo: rowInfo,
+      fieldCache: fieldCache,
+      rowCache: rowCache,
+    );
+
+    RowDetailPage(
+      cellBuilder: GridCellBuilder(delegate: dataController),
+      dataController: dataController,
+    ).show(context);
+  }
+}
+
+extension HexColor on Color {
+  static Color fromHex(String hexString) {
+    final buffer = StringBuffer();
+    if (hexString.length == 6 || hexString.length == 7) buffer.write('ff');
+    buffer.write(hexString.replaceFirst('#', ''));
+    return Color(int.parse(buffer.toString(), radix: 16));
   }
 }

+ 59 - 0
frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart

@@ -0,0 +1,59 @@
+import 'package:app_flowy/plugins/board/application/card/board_checkbox_cell_bloc.dart';
+import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
+import 'package:flowy_infra/image.dart';
+import 'package:flowy_infra_ui/style_widget/icon_button.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class BoardCheckboxCell extends StatefulWidget {
+  final GridCellControllerBuilder cellControllerBuilder;
+
+  const BoardCheckboxCell({
+    required this.cellControllerBuilder,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  State<BoardCheckboxCell> createState() => _BoardCheckboxCellState();
+}
+
+class _BoardCheckboxCellState extends State<BoardCheckboxCell> {
+  late BoardCheckboxCellBloc _cellBloc;
+
+  @override
+  void initState() {
+    final cellController =
+        widget.cellControllerBuilder.build() as GridCheckboxCellController;
+    _cellBloc = BoardCheckboxCellBloc(cellController: cellController);
+    _cellBloc.add(const BoardCheckboxCellEvent.initial());
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocProvider.value(
+      value: _cellBloc,
+      child: BlocBuilder<BoardCheckboxCellBloc, BoardCheckboxCellState>(
+        builder: (context, state) {
+          final icon = state.isSelected
+              ? svgWidget('editor/editor_check')
+              : svgWidget('editor/editor_uncheck');
+          return Align(
+            alignment: Alignment.centerLeft,
+            child: FlowyIconButton(
+              iconPadding: EdgeInsets.zero,
+              icon: icon,
+              width: 20,
+            ),
+          );
+        },
+      ),
+    );
+  }
+
+  @override
+  Future<void> dispose() async {
+    _cellBloc.close();
+    super.dispose();
+  }
+}

+ 59 - 0
frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart

@@ -0,0 +1,59 @@
+import 'package:app_flowy/plugins/board/application/card/board_date_cell_bloc.dart';
+import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
+import 'package:flowy_infra_ui/style_widget/text.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class BoardDateCell extends StatefulWidget {
+  final GridCellControllerBuilder cellControllerBuilder;
+
+  const BoardDateCell({
+    required this.cellControllerBuilder,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  State<BoardDateCell> createState() => _BoardDateCellState();
+}
+
+class _BoardDateCellState extends State<BoardDateCell> {
+  late BoardDateCellBloc _cellBloc;
+
+  @override
+  void initState() {
+    final cellController =
+        widget.cellControllerBuilder.build() as GridDateCellController;
+
+    _cellBloc = BoardDateCellBloc(cellController: cellController)
+      ..add(const BoardDateCellEvent.initial());
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocProvider.value(
+      value: _cellBloc,
+      child: BlocBuilder<BoardDateCellBloc, BoardDateCellState>(
+        builder: (context, state) {
+          if (state.dateStr.isEmpty) {
+            return const SizedBox();
+          } else {
+            return Align(
+              alignment: Alignment.centerLeft,
+              child: FlowyText.regular(
+                state.dateStr,
+                fontSize: 14,
+              ),
+            );
+          }
+        },
+      ),
+    );
+  }
+
+  @override
+  Future<void> dispose() async {
+    _cellBloc.close();
+    super.dispose();
+  }
+}

+ 59 - 0
frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart

@@ -0,0 +1,59 @@
+import 'package:app_flowy/plugins/board/application/card/board_number_cell_bloc.dart';
+import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
+import 'package:flowy_infra_ui/style_widget/text.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class BoardNumberCell extends StatefulWidget {
+  final GridCellControllerBuilder cellControllerBuilder;
+
+  const BoardNumberCell({
+    required this.cellControllerBuilder,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  State<BoardNumberCell> createState() => _BoardNumberCellState();
+}
+
+class _BoardNumberCellState extends State<BoardNumberCell> {
+  late BoardNumberCellBloc _cellBloc;
+
+  @override
+  void initState() {
+    final cellController =
+        widget.cellControllerBuilder.build() as GridNumberCellController;
+
+    _cellBloc = BoardNumberCellBloc(cellController: cellController)
+      ..add(const BoardNumberCellEvent.initial());
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocProvider.value(
+      value: _cellBloc,
+      child: BlocBuilder<BoardNumberCellBloc, BoardNumberCellState>(
+        builder: (context, state) {
+          if (state.content.isEmpty) {
+            return const SizedBox();
+          } else {
+            return Align(
+              alignment: Alignment.centerLeft,
+              child: FlowyText.regular(
+                state.content,
+                fontSize: 14,
+              ),
+            );
+          }
+        },
+      ),
+    );
+  }
+
+  @override
+  Future<void> dispose() async {
+    _cellBloc.close();
+    super.dispose();
+  }
+}

+ 63 - 0
frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart

@@ -0,0 +1,63 @@
+import 'package:app_flowy/plugins/board/application/card/board_select_option_cell_bloc.dart';
+import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
+import 'package:app_flowy/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class BoardSelectOptionCell extends StatefulWidget {
+  final GridCellControllerBuilder cellControllerBuilder;
+
+  const BoardSelectOptionCell({
+    required this.cellControllerBuilder,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  State<BoardSelectOptionCell> createState() => _BoardSelectOptionCellState();
+}
+
+class _BoardSelectOptionCellState extends State<BoardSelectOptionCell> {
+  late BoardSelectOptionCellBloc _cellBloc;
+
+  @override
+  void initState() {
+    final cellController =
+        widget.cellControllerBuilder.build() as GridSelectOptionCellController;
+    _cellBloc = BoardSelectOptionCellBloc(cellController: cellController)
+      ..add(const BoardSelectOptionCellEvent.initial());
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocProvider.value(
+      value: _cellBloc,
+      child: BlocBuilder<BoardSelectOptionCellBloc, BoardSelectOptionCellState>(
+        builder: (context, state) {
+          final children = state.selectedOptions
+              .map((option) => SelectOptionTag.fromOption(
+                    context: context,
+                    option: option,
+                  ))
+              .toList();
+          return Align(
+            alignment: Alignment.centerLeft,
+            child: AbsorbPointer(
+              child: Wrap(
+                children: children,
+                spacing: 4,
+                runSpacing: 2,
+              ),
+            ),
+          );
+        },
+      ),
+    );
+  }
+
+  @override
+  Future<void> dispose() async {
+    _cellBloc.close();
+    super.dispose();
+  }
+}

+ 61 - 0
frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart

@@ -0,0 +1,61 @@
+import 'package:app_flowy/plugins/board/application/card/board_text_cell_bloc.dart';
+import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
+import 'package:flowy_infra_ui/style_widget/text.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class BoardTextCell extends StatefulWidget {
+  final GridCellControllerBuilder cellControllerBuilder;
+  const BoardTextCell({required this.cellControllerBuilder, Key? key})
+      : super(key: key);
+
+  @override
+  State<BoardTextCell> createState() => _BoardTextCellState();
+}
+
+class _BoardTextCellState extends State<BoardTextCell> {
+  late BoardTextCellBloc _cellBloc;
+
+  @override
+  void initState() {
+    final cellController =
+        widget.cellControllerBuilder.build() as GridCellController;
+
+    _cellBloc = BoardTextCellBloc(cellController: cellController)
+      ..add(const BoardTextCellEvent.initial());
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocProvider.value(
+      value: _cellBloc,
+      child: BlocBuilder<BoardTextCellBloc, BoardTextCellState>(
+        builder: (context, state) {
+          if (state.content.isEmpty) {
+            return const SizedBox();
+          } else {
+            return Align(
+              alignment: Alignment.centerLeft,
+              child: ConstrainedBox(
+                constraints: BoxConstraints.loose(
+                  const Size(double.infinity, 100),
+                ),
+                child: FlowyText.regular(
+                  state.content,
+                  fontSize: 14,
+                ),
+              ),
+            );
+          }
+        },
+      ),
+    );
+  }
+
+  @override
+  Future<void> dispose() async {
+    _cellBloc.close();
+    super.dispose();
+  }
+}

+ 66 - 0
frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart

@@ -0,0 +1,66 @@
+import 'package:app_flowy/plugins/board/application/card/board_url_cell_bloc.dart';
+import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
+import 'package:flowy_infra/theme.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class BoardUrlCell extends StatefulWidget {
+  final GridCellControllerBuilder cellControllerBuilder;
+
+  const BoardUrlCell({
+    required this.cellControllerBuilder,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  State<BoardUrlCell> createState() => _BoardUrlCellState();
+}
+
+class _BoardUrlCellState extends State<BoardUrlCell> {
+  late BoardURLCellBloc _cellBloc;
+
+  @override
+  void initState() {
+    final cellController =
+        widget.cellControllerBuilder.build() as GridURLCellController;
+    _cellBloc = BoardURLCellBloc(cellController: cellController);
+    _cellBloc.add(const BoardURLCellEvent.initial());
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = context.watch<AppTheme>();
+    return BlocProvider.value(
+      value: _cellBloc,
+      child: BlocBuilder<BoardURLCellBloc, BoardURLCellState>(
+        builder: (context, state) {
+          if (state.content.isEmpty) {
+            return const SizedBox();
+          } else {
+            return Align(
+              alignment: Alignment.centerLeft,
+              child: RichText(
+                textAlign: TextAlign.left,
+                text: TextSpan(
+                  text: state.content,
+                  style: TextStyle(
+                    color: theme.main2,
+                    fontSize: 14,
+                    decoration: TextDecoration.underline,
+                  ),
+                ),
+              ),
+            );
+          }
+        },
+      ),
+    );
+  }
+
+  @override
+  Future<void> dispose() async {
+    _cellBloc.close();
+    super.dispose();
+  }
+}

+ 98 - 0
frontend/app_flowy/lib/plugins/board/presentation/card/card.dart

@@ -0,0 +1,98 @@
+import 'package:app_flowy/plugins/board/application/card/card_bloc.dart';
+import 'package:app_flowy/plugins/board/application/card/card_data_controller.dart';
+import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
+import 'package:app_flowy/plugins/grid/presentation/widgets/row/row_action_sheet.dart';
+import 'package:flowy_infra/image.dart';
+import 'package:flowy_infra/theme.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'card_cell_builder.dart';
+import 'card_container.dart';
+
+typedef OnEndEditing = void Function(String rowId);
+
+class BoardCard extends StatefulWidget {
+  final String gridId;
+  final bool isEditing;
+  final CardDataController dataController;
+  final BoardCellBuilder cellBuilder;
+  final OnEndEditing onEditEditing;
+  final void Function(BuildContext) openCard;
+
+  const BoardCard({
+    required this.gridId,
+    required this.isEditing,
+    required this.dataController,
+    required this.cellBuilder,
+    required this.onEditEditing,
+    required this.openCard,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  State<BoardCard> createState() => _BoardCardState();
+}
+
+class _BoardCardState extends State<BoardCard> {
+  late BoardCardBloc _cardBloc;
+
+  @override
+  void initState() {
+    _cardBloc = BoardCardBloc(
+      gridId: widget.gridId,
+      dataController: widget.dataController,
+    );
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocProvider.value(
+      value: _cardBloc,
+      child: BlocBuilder<BoardCardBloc, BoardCardState>(
+        builder: (context, state) {
+          return BoardCardContainer(
+            accessoryBuilder: (context) {
+              return [const _CardMoreOption()];
+            },
+            onTap: (context) {
+              widget.openCard(context);
+            },
+            child: Column(
+              children: _makeCells(context, state.gridCellMap),
+            ),
+          );
+        },
+      ),
+    );
+  }
+
+  List<Widget> _makeCells(BuildContext context, GridCellMap cellMap) {
+    return cellMap.values.map(
+      (cellId) {
+        final child = widget.cellBuilder.buildCell(cellId);
+        return Padding(
+          padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
+          child: child,
+        );
+      },
+    ).toList();
+  }
+}
+
+class _CardMoreOption extends StatelessWidget with CardAccessory {
+  const _CardMoreOption({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return svgWidget('home/details', color: context.read<AppTheme>().iconColor);
+  }
+
+  @override
+  void onTap(BuildContext context) {
+    GridRowActionSheet(
+      rowData: context.read<BoardCardBloc>().rowInfo(),
+    ).show(context, direction: AnchorDirection.bottomWithCenterAligned);
+  }
+}

+ 69 - 0
frontend/app_flowy/lib/plugins/board/presentation/card/card_cell_builder.dart

@@ -0,0 +1,69 @@
+import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
+import 'package:flutter/material.dart';
+
+import 'board_checkbox_cell.dart';
+import 'board_date_cell.dart';
+import 'board_number_cell.dart';
+import 'board_select_option_cell.dart';
+import 'board_text_cell.dart';
+import 'board_url_cell.dart';
+
+abstract class BoardCellBuilderDelegate
+    extends GridCellControllerBuilderDelegate {
+  GridCellCache get cellCache;
+}
+
+class BoardCellBuilder {
+  final BoardCellBuilderDelegate delegate;
+
+  BoardCellBuilder(this.delegate);
+
+  Widget buildCell(GridCellIdentifier cellId) {
+    final cellControllerBuilder = GridCellControllerBuilder(
+      delegate: delegate,
+      cellId: cellId,
+      cellCache: delegate.cellCache,
+    );
+
+    final key = cellId.key();
+    switch (cellId.fieldType) {
+      case FieldType.Checkbox:
+        return BoardCheckboxCell(
+          cellControllerBuilder: cellControllerBuilder,
+          key: key,
+        );
+      case FieldType.DateTime:
+        return BoardDateCell(
+          cellControllerBuilder: cellControllerBuilder,
+          key: key,
+        );
+      case FieldType.SingleSelect:
+        return BoardSelectOptionCell(
+          cellControllerBuilder: cellControllerBuilder,
+          key: key,
+        );
+      case FieldType.MultiSelect:
+        return BoardSelectOptionCell(
+          cellControllerBuilder: cellControllerBuilder,
+          key: key,
+        );
+      case FieldType.Number:
+        return BoardNumberCell(
+          cellControllerBuilder: cellControllerBuilder,
+          key: key,
+        );
+      case FieldType.RichText:
+        return BoardTextCell(
+          cellControllerBuilder: cellControllerBuilder,
+          key: key,
+        );
+      case FieldType.URL:
+        return BoardUrlCell(
+          cellControllerBuilder: cellControllerBuilder,
+          key: key,
+        );
+    }
+    throw UnimplementedError;
+  }
+}

+ 142 - 0
frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart

@@ -0,0 +1,142 @@
+import 'package:flowy_infra/theme.dart';
+import 'package:flowy_infra_ui/style_widget/hover.dart';
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import 'package:styled_widget/styled_widget.dart';
+
+class BoardCardContainer extends StatelessWidget {
+  final Widget child;
+  final CardAccessoryBuilder? accessoryBuilder;
+  final void Function(BuildContext) onTap;
+  const BoardCardContainer({
+    required this.child,
+    required this.onTap,
+    this.accessoryBuilder,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return ChangeNotifierProvider(
+      create: (_) => _CardContainerNotifier(),
+      child: Consumer<_CardContainerNotifier>(
+        builder: (context, notifier, _) {
+          Widget container = Center(child: child);
+          if (accessoryBuilder != null) {
+            final accessories = accessoryBuilder!(context);
+            if (accessories.isNotEmpty) {
+              container = _CardEnterRegion(
+                child: container,
+                accessories: accessories,
+              );
+            }
+          }
+
+          return GestureDetector(
+            onTap: () => onTap(context),
+            child: Padding(
+              padding: const EdgeInsets.all(8),
+              child: ConstrainedBox(
+                constraints: const BoxConstraints(minHeight: 30),
+                child: container,
+              ),
+            ),
+          );
+        },
+      ),
+    );
+  }
+}
+
+abstract class CardAccessory implements Widget {
+  void onTap(BuildContext context);
+}
+
+typedef CardAccessoryBuilder = List<CardAccessory> Function(
+  BuildContext buildContext,
+);
+
+class CardAccessoryContainer extends StatelessWidget {
+  final List<CardAccessory> accessories;
+  const CardAccessoryContainer({required this.accessories, Key? key})
+      : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = context.read<AppTheme>();
+    final children = accessories.map((accessory) {
+      final hover = FlowyHover(
+        style: HoverStyle(
+          hoverColor: theme.hover,
+          backgroundColor: theme.surface,
+        ),
+        builder: (_, onHover) => Container(
+          width: 26,
+          height: 26,
+          padding: const EdgeInsets.all(3),
+          child: accessory,
+        ),
+      );
+      return GestureDetector(
+        child: hover,
+        behavior: HitTestBehavior.opaque,
+        onTap: () => accessory.onTap(context),
+      );
+    }).toList();
+
+    return Wrap(children: children, spacing: 6);
+  }
+}
+
+class _CardEnterRegion extends StatelessWidget {
+  final Widget child;
+  final List<CardAccessory> accessories;
+  const _CardEnterRegion(
+      {required this.child, required this.accessories, Key? key})
+      : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Selector<_CardContainerNotifier, bool>(
+      selector: (context, notifier) => notifier.onEnter,
+      builder: (context, onEnter, _) {
+        List<Widget> children = [child];
+        if (onEnter) {
+          children.add(CardAccessoryContainer(accessories: accessories)
+              .positioned(right: 0));
+        }
+
+        return MouseRegion(
+          cursor: SystemMouseCursors.click,
+          onEnter: (p) =>
+              Provider.of<_CardContainerNotifier>(context, listen: false)
+                  .onEnter = true,
+          onExit: (p) =>
+              Provider.of<_CardContainerNotifier>(context, listen: false)
+                  .onEnter = false,
+          child: IntrinsicHeight(
+              child: Stack(
+            alignment: AlignmentDirectional.center,
+            fit: StackFit.expand,
+            children: children,
+          )),
+        );
+      },
+    );
+  }
+}
+
+class _CardContainerNotifier extends ChangeNotifier {
+  bool _onEnter = false;
+
+  _CardContainerNotifier();
+
+  set onEnter(bool value) {
+    if (_onEnter != value) {
+      _onEnter = value;
+      notifyListeners();
+    }
+  }
+
+  bool get onEnter => _onEnter;
+}

+ 3 - 3
frontend/app_flowy/lib/plugins/doc/document.dart

@@ -1,4 +1,4 @@
-library docuemnt_plugin;
+library document_plugin;
 
 import 'package:app_flowy/generated/locale_keys.g.dart';
 import 'package:app_flowy/startup/plugin/plugin.dart';
@@ -42,10 +42,10 @@ class DocumentPluginBuilder extends PluginBuilder {
   String get menuName => LocaleKeys.document_menuName.tr();
 
   @override
-  PluginType get pluginType => DefaultPlugin.quill.type();
+  PluginType get pluginType => PluginType.editor;
 
   @override
-  ViewDataType get dataType => ViewDataType.TextBlock;
+  ViewDataTypePB get dataType => ViewDataTypePB.Text;
 }
 
 class DocumentPlugin implements Plugin {

+ 13 - 5
frontend/app_flowy/lib/plugins/doc/presentation/banner.dart

@@ -11,7 +11,9 @@ import 'package:app_flowy/generated/locale_keys.g.dart';
 class DocumentBanner extends StatelessWidget {
   final void Function() onRestore;
   final void Function() onDelete;
-  const DocumentBanner({required this.onRestore, required this.onDelete, Key? key}) : super(key: key);
+  const DocumentBanner(
+      {required this.onRestore, required this.onDelete, Key? key})
+      : super(key: key);
 
   @override
   Widget build(BuildContext context) {
@@ -26,7 +28,8 @@ class DocumentBanner extends StatelessWidget {
           fit: BoxFit.scaleDown,
           child: Row(
             children: [
-              FlowyText.medium(LocaleKeys.deletePagePrompt_text.tr(), color: Colors.white),
+              FlowyText.medium(LocaleKeys.deletePagePrompt_text.tr(),
+                  color: Colors.white),
               const HSpace(20),
               BaseStyledButton(
                   minWidth: 160,
@@ -37,7 +40,10 @@ class DocumentBanner extends StatelessWidget {
                   downColor: theme.main1,
                   outlineColor: Colors.white,
                   borderRadius: Corners.s8Border,
-                  child: FlowyText.medium(LocaleKeys.deletePagePrompt_restore.tr(), color: Colors.white, fontSize: 14),
+                  child: FlowyText.medium(
+                      LocaleKeys.deletePagePrompt_restore.tr(),
+                      color: Colors.white,
+                      fontSize: 14),
                   onPressed: onRestore),
               const HSpace(20),
               BaseStyledButton(
@@ -49,8 +55,10 @@ class DocumentBanner extends StatelessWidget {
                   downColor: theme.main1,
                   outlineColor: Colors.white,
                   borderRadius: Corners.s8Border,
-                  child: FlowyText.medium(LocaleKeys.deletePagePrompt_deletePermanent.tr(),
-                      color: Colors.white, fontSize: 14),
+                  child: FlowyText.medium(
+                      LocaleKeys.deletePagePrompt_deletePermanent.tr(),
+                      color: Colors.white,
+                      fontSize: 14),
                   onPressed: onDelete),
             ],
           ),

+ 7 - 7
frontend/app_flowy/lib/plugins/grid/application/block/block_cache.dart

@@ -1,19 +1,19 @@
 import 'dart:async';
-import 'package:app_flowy/plugins/grid/application/grid_service.dart';
-import 'package:app_flowy/plugins/grid/application/row/row_service.dart';
 import 'package:flowy_sdk/log.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
 
+import '../field/field_cache.dart';
+import '../row/row_cache.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;
+  final BlockPB block;
   late GridRowCache _rowCache;
   late GridBlockListener _listener;
 
-  List<GridRowInfo> get rows => _rowCache.rows;
+  List<RowInfo> get rows => _rowCache.rows;
   GridRowCache get rowCache => _rowCache;
 
   GridBlockCache({
@@ -24,7 +24,7 @@ class GridBlockCache {
     _rowCache = GridRowCache(
       gridId: gridId,
       block: block,
-      notifier: GridRowCacheFieldNotifierImpl(fieldCache),
+      notifier: GridRowFieldNotifierImpl(fieldCache),
     );
 
     _listener = GridBlockListener(blockId: block.id);
@@ -42,7 +42,7 @@ class GridBlockCache {
   }
 
   void addListener({
-    required void Function(GridRowChangeReason) onChangeReason,
+    required void Function(RowsChangedReason) onRowsChanged,
     bool Function()? listenWhen,
   }) {
     _rowCache.onRowsChanged((reason) {
@@ -50,7 +50,7 @@ class GridBlockCache {
         return;
       }
 
-      onChangeReason(reason);
+      onRowsChanged(reason);
     });
   }
 }

+ 10 - 6
frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_field_notifier.dart

@@ -3,19 +3,22 @@ import 'package:flutter/foundation.dart';
 
 import 'cell_service.dart';
 
-abstract class GridFieldChangedNotifier {
-  void onFieldChanged(void Function(GridFieldPB) callback);
-  void dispose();
+abstract class IGridCellFieldNotifier {
+  void onCellFieldChanged(void Function(FieldPB) callback);
+  void onCellDispose();
 }
 
 /// 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 {
+  final IGridCellFieldNotifier notifier;
+
   /// fieldId: {objectId: callback}
-  final Map<String, Map<String, List<VoidCallback>>> _fieldListenerByFieldId = {};
+  final Map<String, Map<String, List<VoidCallback>>> _fieldListenerByFieldId =
+      {};
 
-  GridCellFieldNotifier({required GridFieldChangedNotifier notifier}) {
-    notifier.onFieldChanged(
+  GridCellFieldNotifier({required this.notifier}) {
+    notifier.onCellFieldChanged(
       (field) {
         final map = _fieldListenerByFieldId[field.id];
         if (map != null) {
@@ -55,6 +58,7 @@ class GridCellFieldNotifier {
   }
 
   Future<void> dispose() async {
+    notifier.onCellDispose();
     _fieldListenerByFieldId.clear();
   }
 }

+ 3 - 4
frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_service.dart

@@ -1,7 +1,5 @@
 import 'dart:async';
 import 'dart:collection';
-
-import 'package:app_flowy/plugins/grid/application/grid_service.dart';
 import 'package:dartz/dartz.dart';
 import 'package:equatable/equatable.dart';
 import 'package:flowy_sdk/dispatch/dispatch.dart';
@@ -18,7 +16,8 @@ import 'package:app_flowy/plugins/grid/application/cell/cell_listener.dart';
 import 'package:app_flowy/plugins/grid/application/field/field_service.dart';
 import 'dart:convert' show utf8;
 
-import '../../field/type_option/type_option_service.dart';
+import '../../field/field_cache.dart';
+import '../../field/type_option/type_option_context.dart';
 import 'cell_field_notifier.dart';
 part 'cell_service.freezed.dart';
 part 'cell_data_loader.dart';
@@ -61,7 +60,7 @@ class GridCellIdentifier with _$GridCellIdentifier {
   const factory GridCellIdentifier({
     required String gridId,
     required String rowId,
-    required GridFieldPB field,
+    required FieldPB field,
   }) = _GridCellIdentifier;
 
   // ignore: unused_element

+ 22 - 18
frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart

@@ -1,29 +1,32 @@
 part of 'cell_service.dart';
 
 typedef GridCellController = IGridCellController<String, String>;
+typedef GridCheckboxCellController = IGridCellController<String, String>;
+typedef GridNumberCellController = IGridCellController<String, String>;
 typedef GridSelectOptionCellController
     = IGridCellController<SelectOptionCellDataPB, String>;
 typedef GridDateCellController
     = IGridCellController<DateCellDataPB, CalendarData>;
 typedef GridURLCellController = IGridCellController<URLCellDataPB, String>;
 
+abstract class GridCellControllerBuilderDelegate {
+  GridCellFieldNotifier buildFieldNotifier();
+}
+
 class GridCellControllerBuilder {
   final GridCellIdentifier _cellId;
   final GridCellCache _cellCache;
-  final GridFieldCache _fieldCache;
+  final GridCellControllerBuilderDelegate delegate;
 
   GridCellControllerBuilder({
+    required this.delegate,
     required GridCellIdentifier cellId,
     required GridCellCache cellCache,
-    required GridFieldCache fieldCache,
   })  : _cellCache = cellCache,
-        _fieldCache = fieldCache,
         _cellId = cellId;
 
   IGridCellController build() {
-    final cellFieldNotifier = GridCellFieldNotifier(
-        notifier: _GridFieldChangedNotifierImpl(_fieldCache));
-
+    final cellFieldNotifier = delegate.buildFieldNotifier();
     switch (_cellId.fieldType) {
       case FieldType.Checkbox:
         final cellDataLoader = GridCellDataLoader(
@@ -57,7 +60,7 @@ class GridCellControllerBuilder {
           parser: StringCellDataParser(),
           reloadOnFieldChanged: true,
         );
-        return GridCellController(
+        return GridNumberCellController(
           cellId: _cellId,
           cellCache: _cellCache,
           cellDataLoader: cellDataLoader,
@@ -126,7 +129,7 @@ class IGridCellController<T, D> extends Equatable {
   final GridCellDataLoader<T> _cellDataLoader;
   final IGridCellDataPersistence<D> _cellDataPersistence;
 
-  late final CellListener _cellListener;
+  CellListener? _cellListener;
   ValueNotifier<T?>? _cellDataNotifier;
 
   bool isListening = false;
@@ -165,7 +168,7 @@ class IGridCellController<T, D> extends Equatable {
 
   String get fieldId => cellId.field.id;
 
-  GridFieldPB get field => cellId.field;
+  FieldPB get field => cellId.field;
 
   FieldType get fieldType => cellId.field.fieldType;
 
@@ -185,7 +188,7 @@ class IGridCellController<T, D> extends Equatable {
     /// For example:
     ///  user input: 12
     ///  cell display: $12
-    _cellListener.start(onCellChanged: (result) {
+    _cellListener?.start(onCellChanged: (result) {
       result.fold(
         (_) => _loadData(),
         (err) => Log.error(err),
@@ -239,14 +242,14 @@ class IGridCellController<T, D> extends Equatable {
         .getFieldTypeOptionData(fieldType: fieldType)
         .then((result) {
       return result.fold(
-        (data) => parser.fromBuffer(data.typeOptionData),
+        (data) => left(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.
+  /// You can set [deduplicate] 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,
@@ -288,13 +291,14 @@ class IGridCellController<T, D> extends Equatable {
       return;
     }
     _isDispose = true;
-    _cellListener.stop();
+    _cellListener?.stop();
     _loadDataOperation?.cancel();
     _saveDataOperation?.cancel();
     _cellDataNotifier = null;
 
     if (_onFieldChangedFn != null) {
       _fieldNotifier.unregister(_cacheKey, _onFieldChangedFn!);
+      _fieldNotifier.dispose();
       _onFieldChangedFn = null;
     }
   }
@@ -304,14 +308,14 @@ class IGridCellController<T, D> extends Equatable {
       [_cellsCache.get(_cacheKey) ?? "", cellId.rowId + cellId.field.id];
 }
 
-class _GridFieldChangedNotifierImpl extends GridFieldChangedNotifier {
+class GridCellFieldNotifierImpl extends IGridCellFieldNotifier {
   final GridFieldCache _cache;
   FieldChangesetCallback? _onChangesetFn;
 
-  _GridFieldChangedNotifierImpl(GridFieldCache cache) : _cache = cache;
+  GridCellFieldNotifierImpl(GridFieldCache cache) : _cache = cache;
 
   @override
-  void dispose() {
+  void onCellDispose() {
     if (_onChangesetFn != null) {
       _cache.removeListener(onChangesetListener: _onChangesetFn!);
       _onChangesetFn = null;
@@ -319,8 +323,8 @@ class _GridFieldChangedNotifierImpl extends GridFieldChangedNotifier {
   }
 
   @override
-  void onFieldChanged(void Function(GridFieldPB p1) callback) {
-    _onChangesetFn = (GridFieldChangesetPB changeset) {
+  void onCellFieldChanged(void Function(FieldPB p1) callback) {
+    _onChangesetFn = (FieldChangesetPB changeset) {
       for (final updatedField in changeset.updatedFields) {
         callback(updatedField);
       }

+ 10 - 8
frontend/app_flowy/lib/plugins/grid/application/cell/checkbox_cell_bloc.dart

@@ -6,13 +6,13 @@ import 'cell_service/cell_service.dart';
 part 'checkbox_cell_bloc.freezed.dart';
 
 class CheckboxCellBloc extends Bloc<CheckboxCellEvent, CheckboxCellState> {
-  final GridCellController cellContext;
+  final GridCheckboxCellController cellController;
   void Function()? _onCellChangedFn;
 
   CheckboxCellBloc({
     required CellService service,
-    required this.cellContext,
-  }) : super(CheckboxCellState.initial(cellContext)) {
+    required this.cellController,
+  }) : super(CheckboxCellState.initial(cellController)) {
     on<CheckboxCellEvent>(
       (event, emit) async {
         await event.when(
@@ -33,16 +33,17 @@ class CheckboxCellBloc extends Bloc<CheckboxCellEvent, CheckboxCellState> {
   @override
   Future<void> close() async {
     if (_onCellChangedFn != null) {
-      cellContext.removeListener(_onCellChangedFn!);
+      cellController.removeListener(_onCellChangedFn!);
       _onCellChangedFn = null;
     }
 
-    cellContext.dispose();
+    cellController.dispose();
     return super.close();
   }
 
   void _startListening() {
-    _onCellChangedFn = cellContext.startListening(onCellChanged: ((cellData) {
+    _onCellChangedFn =
+        cellController.startListening(onCellChanged: ((cellData) {
       if (!isClosed) {
         add(CheckboxCellEvent.didReceiveCellUpdate(cellData));
       }
@@ -50,7 +51,7 @@ class CheckboxCellBloc extends Bloc<CheckboxCellEvent, CheckboxCellState> {
   }
 
   void _updateCellData() {
-    cellContext.saveCellData(!state.isSelected ? "Yes" : "No");
+    cellController.saveCellData(!state.isSelected ? "Yes" : "No");
   }
 }
 
@@ -58,7 +59,8 @@ class CheckboxCellBloc extends Bloc<CheckboxCellEvent, CheckboxCellState> {
 class CheckboxCellEvent with _$CheckboxCellEvent {
   const factory CheckboxCellEvent.initial() = _Initial;
   const factory CheckboxCellEvent.select() = _Selected;
-  const factory CheckboxCellEvent.didReceiveCellUpdate(String? cellData) = _DidReceiveCellUpdate;
+  const factory CheckboxCellEvent.didReceiveCellUpdate(String? cellData) =
+      _DidReceiveCellUpdate;
 }
 
 @freezed

+ 19 - 19
frontend/app_flowy/lib/plugins/grid/application/cell/date_cal_bloc.dart

@@ -18,14 +18,14 @@ import 'package:fixnum/fixnum.dart' as $fixnum;
 part 'date_cal_bloc.freezed.dart';
 
 class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
-  final GridDateCellController cellContext;
+  final GridDateCellController cellController;
   void Function()? _onCellChangedFn;
 
   DateCalBloc({
-    required DateTypeOption dateTypeOption,
+    required DateTypeOptionPB dateTypeOptionPB,
     required DateCellDataPB? cellData,
-    required this.cellContext,
-  }) : super(DateCalState.initial(dateTypeOption, cellData)) {
+    required this.cellController,
+  }) : super(DateCalState.initial(dateTypeOptionPB, cellData)) {
     on<DateCalEvent>(
       (event, emit) async {
         await event.when(
@@ -102,7 +102,7 @@ class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
       }
     }
 
-    cellContext.saveCellData(newCalData, resultCallback: (result) {
+    cellController.saveCellData(newCalData, resultCallback: (result) {
       result.fold(
         () => updateCalData(Some(newCalData), none()),
         (err) {
@@ -120,7 +120,7 @@ class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
 
   String timeFormatPrompt(FlowyError error) {
     String msg = LocaleKeys.grid_field_invalidTimeFormat.tr() + ". ";
-    switch (state.dateTypeOption.timeFormat) {
+    switch (state.dateTypeOptionPB.timeFormat) {
       case TimeFormat.TwelveHour:
         msg = msg + "e.g. 01: 00 AM";
         break;
@@ -136,15 +136,15 @@ class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
   @override
   Future<void> close() async {
     if (_onCellChangedFn != null) {
-      cellContext.removeListener(_onCellChangedFn!);
+      cellController.removeListener(_onCellChangedFn!);
       _onCellChangedFn = null;
     }
-    cellContext.dispose();
+    cellController.dispose();
     return super.close();
   }
 
   void _startListening() {
-    _onCellChangedFn = cellContext.startListening(
+    _onCellChangedFn = cellController.startListening(
       onCellChanged: ((cell) {
         if (!isClosed) {
           add(DateCalEvent.didReceiveCellUpdate(cell));
@@ -159,8 +159,8 @@ class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
     TimeFormat? timeFormat,
     bool? includeTime,
   }) async {
-    state.dateTypeOption.freeze();
-    final newDateTypeOption = state.dateTypeOption.rebuild((typeOption) {
+    state.dateTypeOptionPB.freeze();
+    final newDateTypeOption = state.dateTypeOptionPB.rebuild((typeOption) {
       if (dateFormat != null) {
         typeOption.dateFormat = dateFormat;
       }
@@ -175,14 +175,14 @@ class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
     });
 
     final result = await FieldService.updateFieldTypeOption(
-      gridId: cellContext.gridId,
-      fieldId: cellContext.field.id,
+      gridId: cellController.gridId,
+      fieldId: cellController.field.id,
       typeOptionData: newDateTypeOption.writeToBuffer(),
     );
 
     result.fold(
       (l) => emit(state.copyWith(
-          dateTypeOption: newDateTypeOption,
+          dateTypeOptionPB: newDateTypeOption,
           timeHintText: _timeHintText(newDateTypeOption))),
       (err) => Log.error(err),
     );
@@ -210,7 +210,7 @@ class DateCalEvent with _$DateCalEvent {
 @freezed
 class DateCalState with _$DateCalState {
   const factory DateCalState({
-    required DateTypeOption dateTypeOption,
+    required DateTypeOptionPB dateTypeOptionPB,
     required CalendarFormat format,
     required DateTime focusedDay,
     required Option<String> timeFormatError,
@@ -220,24 +220,24 @@ class DateCalState with _$DateCalState {
   }) = _DateCalState;
 
   factory DateCalState.initial(
-    DateTypeOption dateTypeOption,
+    DateTypeOptionPB dateTypeOptionPB,
     DateCellDataPB? cellData,
   ) {
     Option<CalendarData> calData = calDataFromCellData(cellData);
     final time = calData.foldRight("", (dateData, previous) => dateData.time);
     return DateCalState(
-      dateTypeOption: dateTypeOption,
+      dateTypeOptionPB: dateTypeOptionPB,
       format: CalendarFormat.month,
       focusedDay: DateTime.now(),
       time: time,
       calData: calData,
       timeFormatError: none(),
-      timeHintText: _timeHintText(dateTypeOption),
+      timeHintText: _timeHintText(dateTypeOptionPB),
     );
   }
 }
 
-String _timeHintText(DateTypeOption typeOption) {
+String _timeHintText(DateTypeOptionPB typeOption) {
   switch (typeOption.timeFormat) {
     case TimeFormat.TwelveHour:
       return LocaleKeys.document_date_timeHintTextInTwelveHour.tr();

+ 15 - 10
frontend/app_flowy/lib/plugins/grid/application/cell/date_cell_bloc.dart

@@ -7,18 +7,21 @@ import 'cell_service/cell_service.dart';
 part 'date_cell_bloc.freezed.dart';
 
 class DateCellBloc extends Bloc<DateCellEvent, DateCellState> {
-  final GridDateCellController cellContext;
+  final GridDateCellController cellController;
   void Function()? _onCellChangedFn;
 
-  DateCellBloc({required this.cellContext}) : super(DateCellState.initial(cellContext)) {
+  DateCellBloc({required this.cellController})
+      : super(DateCellState.initial(cellController)) {
     on<DateCellEvent>(
       (event, emit) async {
         event.when(
           initial: () => _startListening(),
           didReceiveCellUpdate: (DateCellDataPB? cellData) {
-            emit(state.copyWith(data: cellData, dateStr: _dateStrFromCellData(cellData)));
+            emit(state.copyWith(
+                data: cellData, dateStr: _dateStrFromCellData(cellData)));
           },
-          didReceiveFieldUpdate: (GridFieldPB value) => emit(state.copyWith(field: value)),
+          didReceiveFieldUpdate: (FieldPB value) =>
+              emit(state.copyWith(field: value)),
         );
       },
     );
@@ -27,15 +30,15 @@ class DateCellBloc extends Bloc<DateCellEvent, DateCellState> {
   @override
   Future<void> close() async {
     if (_onCellChangedFn != null) {
-      cellContext.removeListener(_onCellChangedFn!);
+      cellController.removeListener(_onCellChangedFn!);
       _onCellChangedFn = null;
     }
-    cellContext.dispose();
+    cellController.dispose();
     return super.close();
   }
 
   void _startListening() {
-    _onCellChangedFn = cellContext.startListening(
+    _onCellChangedFn = cellController.startListening(
       onCellChanged: ((data) {
         if (!isClosed) {
           add(DateCellEvent.didReceiveCellUpdate(data));
@@ -48,8 +51,10 @@ class DateCellBloc extends Bloc<DateCellEvent, DateCellState> {
 @freezed
 class DateCellEvent with _$DateCellEvent {
   const factory DateCellEvent.initial() = _InitialCell;
-  const factory DateCellEvent.didReceiveCellUpdate(DateCellDataPB? data) = _DidReceiveCellUpdate;
-  const factory DateCellEvent.didReceiveFieldUpdate(GridFieldPB field) = _DidReceiveFieldUpdate;
+  const factory DateCellEvent.didReceiveCellUpdate(DateCellDataPB? data) =
+      _DidReceiveCellUpdate;
+  const factory DateCellEvent.didReceiveFieldUpdate(FieldPB field) =
+      _DidReceiveFieldUpdate;
 }
 
 @freezed
@@ -57,7 +62,7 @@ class DateCellState with _$DateCellState {
   const factory DateCellState({
     required DateCellDataPB? data,
     required String dateStr,
-    required GridFieldPB field,
+    required FieldPB field,
   }) = _DateCellState;
 
   factory DateCellState.initial(GridDateCellController context) {

+ 12 - 9
frontend/app_flowy/lib/plugins/grid/application/cell/number_cell_bloc.dart

@@ -8,12 +8,12 @@ import 'cell_service/cell_service.dart';
 part 'number_cell_bloc.freezed.dart';
 
 class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
-  final GridCellController cellContext;
+  final GridNumberCellController cellController;
   void Function()? _onCellChangedFn;
 
   NumberCellBloc({
-    required this.cellContext,
-  }) : super(NumberCellState.initial(cellContext)) {
+    required this.cellController,
+  }) : super(NumberCellState.initial(cellController)) {
     on<NumberCellEvent>(
       (event, emit) async {
         event.when(
@@ -24,11 +24,13 @@ class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
             emit(state.copyWith(content: content));
           },
           updateCell: (text) {
-            cellContext.saveCellData(text, resultCallback: (result) {
+            cellController.saveCellData(text, resultCallback: (result) {
               result.fold(
                 () => null,
                 (err) {
-                  if (!isClosed) add(NumberCellEvent.didReceiveCellUpdate(right(err)));
+                  if (!isClosed) {
+                    add(NumberCellEvent.didReceiveCellUpdate(right(err)));
+                  }
                 },
               );
             });
@@ -41,15 +43,15 @@ class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
   @override
   Future<void> close() async {
     if (_onCellChangedFn != null) {
-      cellContext.removeListener(_onCellChangedFn!);
+      cellController.removeListener(_onCellChangedFn!);
       _onCellChangedFn = null;
     }
-    cellContext.dispose();
+    cellController.dispose();
     return super.close();
   }
 
   void _startListening() {
-    _onCellChangedFn = cellContext.startListening(
+    _onCellChangedFn = cellController.startListening(
       onCellChanged: ((cellContent) {
         if (!isClosed) {
           add(NumberCellEvent.didReceiveCellUpdate(left(cellContent ?? "")));
@@ -63,7 +65,8 @@ class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
 class NumberCellEvent with _$NumberCellEvent {
   const factory NumberCellEvent.initial() = _Initial;
   const factory NumberCellEvent.updateCell(String text) = _UpdateCell;
-  const factory NumberCellEvent.didReceiveCellUpdate(Either<String, FlowyError> cellContent) = _DidReceiveCellUpdate;
+  const factory NumberCellEvent.didReceiveCellUpdate(
+      Either<String, FlowyError> cellContent) = _DidReceiveCellUpdate;
 }
 
 @freezed

+ 6 - 6
frontend/app_flowy/lib/plugins/grid/application/cell/select_option_cell_bloc.dart

@@ -8,12 +8,12 @@ part 'select_option_cell_bloc.freezed.dart';
 
 class SelectOptionCellBloc
     extends Bloc<SelectOptionCellEvent, SelectOptionCellState> {
-  final GridSelectOptionCellController cellContext;
+  final GridSelectOptionCellController cellController;
   void Function()? _onCellChangedFn;
 
   SelectOptionCellBloc({
-    required this.cellContext,
-  }) : super(SelectOptionCellState.initial(cellContext)) {
+    required this.cellController,
+  }) : super(SelectOptionCellState.initial(cellController)) {
     on<SelectOptionCellEvent>(
       (event, emit) async {
         await event.map(
@@ -33,15 +33,15 @@ class SelectOptionCellBloc
   @override
   Future<void> close() async {
     if (_onCellChangedFn != null) {
-      cellContext.removeListener(_onCellChangedFn!);
+      cellController.removeListener(_onCellChangedFn!);
       _onCellChangedFn = null;
     }
-    cellContext.dispose();
+    cellController.dispose();
     return super.close();
   }
 
   void _startListening() {
-    _onCellChangedFn = cellContext.startListening(
+    _onCellChangedFn = cellController.startListening(
       onCellChanged: ((selectOptionContext) {
         if (!isClosed) {
           add(SelectOptionCellEvent.didReceiveOptions(

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

@@ -111,7 +111,7 @@ class SelectOptionCellEditorBloc
   void _loadOptions() {
     _delayOperation?.cancel();
     _delayOperation = Timer(const Duration(milliseconds: 10), () {
-      _selectOptionService.getOpitonContext().then((result) {
+      _selectOptionService.getOptionContext().then((result) {
         if (isClosed) {
           return;
         }

+ 2 - 2
frontend/app_flowy/lib/plugins/grid/application/cell/select_option_service.dart

@@ -15,7 +15,7 @@ class SelectOptionService {
   String get rowId => cellId.rowId;
 
   Future<Either<Unit, FlowyError>> create({required String name}) {
-    return TypeOptionService(gridId: gridId, fieldId: fieldId)
+    return TypeOptionFFIService(gridId: gridId, fieldId: fieldId)
         .newOption(name: name)
         .then(
       (result) {
@@ -55,7 +55,7 @@ class SelectOptionService {
     return GridEventUpdateSelectOption(payload).send();
   }
 
-  Future<Either<SelectOptionCellDataPB, FlowyError>> getOpitonContext() {
+  Future<Either<SelectOptionCellDataPB, FlowyError>> getOptionContext() {
     final payload = GridCellIdPB.create()
       ..gridId = gridId
       ..fieldId = fieldId

+ 9 - 8
frontend/app_flowy/lib/plugins/grid/application/cell/text_cell_bloc.dart

@@ -6,11 +6,11 @@ import 'cell_service/cell_service.dart';
 part 'text_cell_bloc.freezed.dart';
 
 class TextCellBloc extends Bloc<TextCellEvent, TextCellState> {
-  final GridCellController cellContext;
+  final GridCellController cellController;
   void Function()? _onCellChangedFn;
   TextCellBloc({
-    required this.cellContext,
-  }) : super(TextCellState.initial(cellContext)) {
+    required this.cellController,
+  }) : super(TextCellState.initial(cellController)) {
     on<TextCellEvent>(
       (event, emit) async {
         await event.when(
@@ -18,7 +18,7 @@ class TextCellBloc extends Bloc<TextCellEvent, TextCellState> {
             _startListening();
           },
           updateText: (text) {
-            cellContext.saveCellData(text);
+            cellController.saveCellData(text);
             emit(state.copyWith(content: text));
           },
           didReceiveCellUpdate: (content) {
@@ -32,15 +32,15 @@ class TextCellBloc extends Bloc<TextCellEvent, TextCellState> {
   @override
   Future<void> close() async {
     if (_onCellChangedFn != null) {
-      cellContext.removeListener(_onCellChangedFn!);
+      cellController.removeListener(_onCellChangedFn!);
       _onCellChangedFn = null;
     }
-    cellContext.dispose();
+    cellController.dispose();
     return super.close();
   }
 
   void _startListening() {
-    _onCellChangedFn = cellContext.startListening(
+    _onCellChangedFn = cellController.startListening(
       onCellChanged: ((cellContent) {
         if (!isClosed) {
           add(TextCellEvent.didReceiveCellUpdate(cellContent ?? ""));
@@ -53,7 +53,8 @@ class TextCellBloc extends Bloc<TextCellEvent, TextCellState> {
 @freezed
 class TextCellEvent with _$TextCellEvent {
   const factory TextCellEvent.initial() = _InitialCell;
-  const factory TextCellEvent.didReceiveCellUpdate(String cellContent) = _DidReceiveCellUpdate;
+  const factory TextCellEvent.didReceiveCellUpdate(String cellContent) =
+      _DidReceiveCellUpdate;
   const factory TextCellEvent.updateText(String text) = _UpdateText;
 }
 

+ 9 - 8
frontend/app_flowy/lib/plugins/grid/application/cell/url_cell_bloc.dart

@@ -7,11 +7,11 @@ import 'cell_service/cell_service.dart';
 part 'url_cell_bloc.freezed.dart';
 
 class URLCellBloc extends Bloc<URLCellEvent, URLCellState> {
-  final GridURLCellController cellContext;
+  final GridURLCellController cellController;
   void Function()? _onCellChangedFn;
   URLCellBloc({
-    required this.cellContext,
-  }) : super(URLCellState.initial(cellContext)) {
+    required this.cellController,
+  }) : super(URLCellState.initial(cellController)) {
     on<URLCellEvent>(
       (event, emit) async {
         event.when(
@@ -25,7 +25,7 @@ class URLCellBloc extends Bloc<URLCellEvent, URLCellState> {
             ));
           },
           updateURL: (String url) {
-            cellContext.saveCellData(url, deduplicate: true);
+            cellController.saveCellData(url, deduplicate: true);
           },
         );
       },
@@ -35,15 +35,15 @@ class URLCellBloc extends Bloc<URLCellEvent, URLCellState> {
   @override
   Future<void> close() async {
     if (_onCellChangedFn != null) {
-      cellContext.removeListener(_onCellChangedFn!);
+      cellController.removeListener(_onCellChangedFn!);
       _onCellChangedFn = null;
     }
-    cellContext.dispose();
+    cellController.dispose();
     return super.close();
   }
 
   void _startListening() {
-    _onCellChangedFn = cellContext.startListening(
+    _onCellChangedFn = cellController.startListening(
       onCellChanged: ((cellData) {
         if (!isClosed) {
           add(URLCellEvent.didReceiveCellUpdate(cellData));
@@ -57,7 +57,8 @@ 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(URLCellDataPB? cell) = _DidReceiveCellUpdate;
+  const factory URLCellEvent.didReceiveCellUpdate(URLCellDataPB? cell) =
+      _DidReceiveCellUpdate;
 }
 
 @freezed

+ 9 - 8
frontend/app_flowy/lib/plugins/grid/application/cell/url_cell_editor_bloc.dart

@@ -7,11 +7,11 @@ import 'cell_service/cell_service.dart';
 part 'url_cell_editor_bloc.freezed.dart';
 
 class URLCellEditorBloc extends Bloc<URLCellEditorEvent, URLCellEditorState> {
-  final GridURLCellController cellContext;
+  final GridURLCellController cellController;
   void Function()? _onCellChangedFn;
   URLCellEditorBloc({
-    required this.cellContext,
-  }) : super(URLCellEditorState.initial(cellContext)) {
+    required this.cellController,
+  }) : super(URLCellEditorState.initial(cellController)) {
     on<URLCellEditorEvent>(
       (event, emit) async {
         event.when(
@@ -19,7 +19,7 @@ class URLCellEditorBloc extends Bloc<URLCellEditorEvent, URLCellEditorState> {
             _startListening();
           },
           updateText: (text) {
-            cellContext.saveCellData(text, deduplicate: true);
+            cellController.saveCellData(text, deduplicate: true);
             emit(state.copyWith(content: text));
           },
           didReceiveCellUpdate: (cellData) {
@@ -33,15 +33,15 @@ class URLCellEditorBloc extends Bloc<URLCellEditorEvent, URLCellEditorState> {
   @override
   Future<void> close() async {
     if (_onCellChangedFn != null) {
-      cellContext.removeListener(_onCellChangedFn!);
+      cellController.removeListener(_onCellChangedFn!);
       _onCellChangedFn = null;
     }
-    cellContext.dispose();
+    cellController.dispose();
     return super.close();
   }
 
   void _startListening() {
-    _onCellChangedFn = cellContext.startListening(
+    _onCellChangedFn = cellController.startListening(
       onCellChanged: ((cellData) {
         if (!isClosed) {
           add(URLCellEditorEvent.didReceiveCellUpdate(cellData));
@@ -54,7 +54,8 @@ class URLCellEditorBloc extends Bloc<URLCellEditorEvent, URLCellEditorState> {
 @freezed
 class URLCellEditorEvent with _$URLCellEditorEvent {
   const factory URLCellEditorEvent.initial() = _InitialCell;
-  const factory URLCellEditorEvent.didReceiveCellUpdate(URLCellDataPB? cell) = _DidReceiveCellUpdate;
+  const factory URLCellEditorEvent.didReceiveCellUpdate(URLCellDataPB? cell) =
+      _DidReceiveCellUpdate;
   const factory URLCellEditorEvent.updateText(String text) = _UpdateText;
 }
 

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

@@ -7,11 +7,13 @@ import 'field_service.dart';
 
 part 'field_action_sheet_bloc.freezed.dart';
 
-class FieldActionSheetBloc extends Bloc<FieldActionSheetEvent, FieldActionSheetState> {
+class FieldActionSheetBloc
+    extends Bloc<FieldActionSheetEvent, FieldActionSheetState> {
   final FieldService fieldService;
 
-  FieldActionSheetBloc({required GridFieldPB field, required this.fieldService})
-      : super(FieldActionSheetState.initial(FieldTypeOptionDataPB.create()..field_2 = field)) {
+  FieldActionSheetBloc({required FieldPB field, required this.fieldService})
+      : super(FieldActionSheetState.initial(
+            FieldTypeOptionDataPB.create()..field_2 = field)) {
     on<FieldActionSheetEvent>(
       (event, emit) async {
         await event.map(
@@ -57,7 +59,8 @@ class FieldActionSheetBloc extends Bloc<FieldActionSheetEvent, FieldActionSheetS
 
 @freezed
 class FieldActionSheetEvent with _$FieldActionSheetEvent {
-  const factory FieldActionSheetEvent.updateFieldName(String name) = _UpdateFieldName;
+  const factory FieldActionSheetEvent.updateFieldName(String name) =
+      _UpdateFieldName;
   const factory FieldActionSheetEvent.hideField() = _HideField;
   const factory FieldActionSheetEvent.duplicateField() = _DuplicateField;
   const factory FieldActionSheetEvent.deleteField() = _DeleteField;
@@ -72,7 +75,8 @@ class FieldActionSheetState with _$FieldActionSheetState {
     required String fieldName,
   }) = _FieldActionSheetState;
 
-  factory FieldActionSheetState.initial(FieldTypeOptionDataPB data) => FieldActionSheetState(
+  factory FieldActionSheetState.initial(FieldTypeOptionDataPB data) =>
+      FieldActionSheetState(
         fieldTypeOptionData: data,
         errorText: '',
         fieldName: data.field_2.name,

+ 192 - 0
frontend/app_flowy/lib/plugins/grid/application/field/field_cache.dart

@@ -0,0 +1,192 @@
+import 'dart:collection';
+
+import 'package:app_flowy/plugins/grid/application/field/grid_listener.dart';
+import 'package:flowy_sdk/log.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
+import 'package:flutter/foundation.dart';
+
+import '../row/row_cache.dart';
+
+class FieldsNotifier extends ChangeNotifier {
+  List<FieldPB> _fields = [];
+
+  set fields(List<FieldPB> fields) {
+    _fields = fields;
+    notifyListeners();
+  }
+
+  List<FieldPB> get fields => _fields;
+}
+
+typedef FieldChangesetCallback = void Function(FieldChangesetPB);
+typedef FieldsCallback = void Function(List<FieldPB>);
+
+class GridFieldCache {
+  final String gridId;
+  final GridFieldsListener _fieldListener;
+  FieldsNotifier? _fieldNotifier = FieldsNotifier();
+  final Map<FieldsCallback, VoidCallback> _fieldsCallbackMap = {};
+  final Map<FieldChangesetCallback, FieldChangesetCallback>
+      _changesetCallbackMap = {};
+
+  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 _changesetCallbackMap.values) {
+            listener(changeset);
+          }
+        },
+        (err) => Log.error(err),
+      );
+    });
+  }
+
+  Future<void> dispose() async {
+    await _fieldListener.stop();
+    _fieldNotifier?.dispose();
+    _fieldNotifier = null;
+  }
+
+  UnmodifiableListView<FieldPB> get unmodifiableFields =>
+      UnmodifiableListView(_fieldNotifier?.fields ?? []);
+
+  List<FieldPB> get fields => [..._fieldNotifier?.fields ?? []];
+
+  set fields(List<FieldPB> fields) {
+    _fieldNotifier?.fields = [...fields];
+  }
+
+  void addListener({
+    FieldsCallback? onFields,
+    FieldChangesetCallback? onChangeset,
+    bool Function()? listenWhen,
+  }) {
+    if (onChangeset != null) {
+      fn(c) {
+        if (listenWhen != null && listenWhen() == false) {
+          return;
+        }
+        onChangeset(c);
+      }
+
+      _changesetCallbackMap[onChangeset] = fn;
+    }
+
+    if (onFields != null) {
+      fn() {
+        if (listenWhen != null && listenWhen() == false) {
+          return;
+        }
+        onFields(fields);
+      }
+
+      _fieldsCallbackMap[onFields] = fn;
+      _fieldNotifier?.addListener(fn);
+    }
+  }
+
+  void removeListener({
+    FieldsCallback? onFieldsListener,
+    FieldChangesetCallback? onChangesetListener,
+  }) {
+    if (onFieldsListener != null) {
+      final fn = _fieldsCallbackMap.remove(onFieldsListener);
+      if (fn != null) {
+        _fieldNotifier?.removeListener(fn);
+      }
+    }
+
+    if (onChangesetListener != null) {
+      _changesetCallbackMap.remove(onChangesetListener);
+    }
+  }
+
+  void _deleteFields(List<FieldIdPB> deletedFields) {
+    if (deletedFields.isEmpty) {
+      return;
+    }
+    final List<FieldPB> newFields = fields;
+    final Map<String, FieldIdPB> deletedFieldMap = {
+      for (var fieldOrder in deletedFields) fieldOrder.fieldId: fieldOrder
+    };
+
+    newFields.retainWhere((field) => (deletedFieldMap[field.id] == null));
+    _fieldNotifier?.fields = newFields;
+  }
+
+  void _insertFields(List<IndexFieldPB> insertedFields) {
+    if (insertedFields.isEmpty) {
+      return;
+    }
+    final List<FieldPB> newFields = fields;
+    for (final indexField in insertedFields) {
+      if (newFields.length > indexField.index) {
+        newFields.insert(indexField.index, indexField.field_1);
+      } else {
+        newFields.add(indexField.field_1);
+      }
+    }
+    _fieldNotifier?.fields = newFields;
+  }
+
+  void _updateFields(List<FieldPB> updatedFields) {
+    if (updatedFields.isEmpty) {
+      return;
+    }
+    final List<FieldPB> newFields = fields;
+    for (final updatedField in updatedFields) {
+      final index =
+          newFields.indexWhere((field) => field.id == updatedField.id);
+      if (index != -1) {
+        newFields.removeAt(index);
+        newFields.insert(index, updatedField);
+      }
+    }
+    _fieldNotifier?.fields = newFields;
+  }
+}
+
+class GridRowFieldNotifierImpl extends IGridRowFieldNotifier {
+  final GridFieldCache _cache;
+  FieldChangesetCallback? _onChangesetFn;
+  FieldsCallback? _onFieldFn;
+  GridRowFieldNotifierImpl(GridFieldCache cache) : _cache = cache;
+
+  @override
+  UnmodifiableListView<FieldPB> get fields => _cache.unmodifiableFields;
+
+  @override
+  void onRowFieldsChanged(VoidCallback callback) {
+    _onFieldFn = (_) => callback();
+    _cache.addListener(onFields: _onFieldFn);
+  }
+
+  @override
+  void onRowFieldChanged(void Function(FieldPB) callback) {
+    _onChangesetFn = (FieldChangesetPB changeset) {
+      for (final updatedField in changeset.updatedFields) {
+        callback(updatedField);
+      }
+    };
+
+    _cache.addListener(onChangeset: _onChangesetFn);
+  }
+
+  @override
+  void onRowDispose() {
+    if (_onFieldFn != null) {
+      _cache.removeListener(onFieldsListener: _onFieldFn!);
+      _onFieldFn = null;
+    }
+
+    if (_onChangesetFn != null) {
+      _cache.removeListener(onChangesetListener: _onChangesetFn!);
+      _onChangesetFn = null;
+    }
+  }
+}

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

@@ -63,7 +63,7 @@ class FieldCellBloc extends Bloc<FieldCellEvent, FieldCellState> {
 @freezed
 class FieldCellEvent with _$FieldCellEvent {
   const factory FieldCellEvent.initial() = _InitialCell;
-  const factory FieldCellEvent.didReceiveFieldUpdate(GridFieldPB field) =
+  const factory FieldCellEvent.didReceiveFieldUpdate(FieldPB field) =
       _DidReceiveFieldUpdate;
   const factory FieldCellEvent.startUpdateWidth(double offset) =
       _StartUpdateWidth;
@@ -74,7 +74,7 @@ class FieldCellEvent with _$FieldCellEvent {
 class FieldCellState with _$FieldCellState {
   const factory FieldCellState({
     required String gridId,
-    required GridFieldPB field,
+    required FieldPB field,
     required double width,
   }) = _FieldCellState;
 

+ 12 - 7
frontend/app_flowy/lib/plugins/grid/application/field/field_editor_bloc.dart

@@ -1,9 +1,12 @@
 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';
 import 'package:dartz/dartz.dart';
+import 'type_option/type_option_context.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+import 'type_option/type_option_data_controller.dart';
+
 part 'field_editor_bloc.freezed.dart';
 
 class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
@@ -13,7 +16,8 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
     required String gridId,
     required String fieldName,
     required IFieldTypeOptionLoader loader,
-  })  : dataController = TypeOptionDataController(gridId: gridId, loader: loader),
+  })  : dataController =
+            TypeOptionDataController(gridId: gridId, loader: loader),
         super(FieldEditorState.initial(gridId, fieldName)) {
     on<FieldEditorEvent>(
       (event, emit) async {
@@ -24,13 +28,13 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
                 add(FieldEditorEvent.didReceiveFieldChanged(field));
               }
             });
-            await dataController.loadData();
+            await dataController.loadTypeOptionData();
           },
           updateName: (name) {
             dataController.fieldName = name;
             emit(state.copyWith(name: name));
           },
-          didReceiveFieldChanged: (GridFieldPB field) {
+          didReceiveFieldChanged: (FieldPB field) {
             emit(state.copyWith(field: Some(field)));
           },
         );
@@ -48,7 +52,8 @@ 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;
+  const factory FieldEditorEvent.didReceiveFieldChanged(FieldPB field) =
+      _DidReceiveFieldChanged;
 }
 
 @freezed
@@ -57,7 +62,7 @@ class FieldEditorState with _$FieldEditorState {
     required String gridId,
     required String errorText,
     required String name,
-    required Option<GridFieldPB> field,
+    required Option<FieldPB> field,
   }) = _FieldEditorState;
 
   factory FieldEditorState.initial(

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

@@ -7,16 +7,18 @@ import 'dart:async';
 import 'dart:typed_data';
 import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
 
-typedef UpdateFieldNotifiedValue = Either<GridFieldPB, FlowyError>;
+typedef UpdateFieldNotifiedValue = Either<FieldPB, FlowyError>;
 
 class SingleFieldListener {
   final String fieldId;
-  PublishNotifier<UpdateFieldNotifiedValue>? _updateFieldNotifier = PublishNotifier();
+  PublishNotifier<UpdateFieldNotifiedValue>? _updateFieldNotifier =
+      PublishNotifier();
   GridNotificationListener? _listener;
 
   SingleFieldListener({required this.fieldId});
 
-  void start({required void Function(UpdateFieldNotifiedValue) onFieldChanged}) {
+  void start(
+      {required void Function(UpdateFieldNotifiedValue) onFieldChanged}) {
     _updateFieldNotifier?.addPublishListener(onFieldChanged);
     _listener = GridNotificationListener(
       objectId: fieldId,
@@ -31,7 +33,8 @@ class SingleFieldListener {
     switch (ty) {
       case GridNotification.DidUpdateField:
         result.fold(
-          (payload) => _updateFieldNotifier?.value = left(GridFieldPB.fromBuffer(payload)),
+          (payload) =>
+              _updateFieldNotifier?.value = left(FieldPB.fromBuffer(payload)),
           (error) => _updateFieldNotifier?.value = right(error),
         );
         break;

+ 6 - 162
frontend/app_flowy/lib/plugins/grid/application/field/field_service.dart

@@ -1,13 +1,10 @@
 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/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),
@@ -21,14 +18,13 @@ class FieldService {
   FieldService({required this.gridId, required this.fieldId});
 
   Future<Either<Unit, FlowyError>> moveField(int fromIndex, int toIndex) {
-    final payload = MoveItemPayloadPB.create()
+    final payload = MoveFieldPayloadPB.create()
       ..gridId = gridId
-      ..itemId = fieldId
-      ..ty = MoveItemTypePB.MoveField
+      ..fieldId = fieldId
       ..fromIndex = fromIndex
       ..toIndex = toIndex;
 
-    return GridEventMoveItem(payload).send();
+    return GridEventMoveField(payload).send();
   }
 
   Future<Either<Unit, FlowyError>> updateField({
@@ -73,7 +69,7 @@ class FieldService {
   // Create the field if it does not exist. Otherwise, update the field.
   static Future<Either<Unit, FlowyError>> insertField({
     required String gridId,
-    required GridFieldPB field,
+    required FieldPB field,
     List<int>? typeOptionData,
     String? startFieldId,
   }) {
@@ -121,7 +117,7 @@ class FieldService {
   Future<Either<FieldTypeOptionDataPB, FlowyError>> getFieldTypeOptionData({
     required FieldType fieldType,
   }) {
-    final payload = GridFieldTypeOptionIdPB.create()
+    final payload = FieldTypeOptionIdPB.create()
       ..gridId = gridId
       ..fieldId = fieldId
       ..fieldType = fieldType;
@@ -138,158 +134,6 @@ class FieldService {
 class GridFieldCellContext with _$GridFieldCellContext {
   const factory GridFieldCellContext({
     required String gridId,
-    required GridFieldPB field,
+    required FieldPB field,
   }) = _GridFieldCellContext;
 }
-
-abstract class IFieldTypeOptionLoader {
-  String get gridId;
-  Future<Either<FieldTypeOptionDataPB, FlowyError>> load();
-
-  Future<Either<FieldTypeOptionDataPB, FlowyError>> switchToField(String fieldId, FieldType fieldType) {
-    final payload = EditFieldPayloadPB.create()
-      ..gridId = gridId
-      ..fieldId = fieldId
-      ..fieldType = fieldType;
-
-    return GridEventSwitchToField(payload).send();
-  }
-}
-
-class NewFieldTypeOptionLoader extends IFieldTypeOptionLoader {
-  @override
-  final String gridId;
-  NewFieldTypeOptionLoader({
-    required this.gridId,
-  });
-
-  @override
-  Future<Either<FieldTypeOptionDataPB, FlowyError>> load() {
-    final payload = CreateFieldPayloadPB.create()
-      ..gridId = gridId
-      ..fieldType = FieldType.RichText;
-
-    return GridEventCreateFieldTypeOption(payload).send();
-  }
-}
-
-class FieldTypeOptionLoader extends IFieldTypeOptionLoader {
-  @override
-  final String gridId;
-  final GridFieldPB field;
-
-  FieldTypeOptionLoader({
-    required this.gridId,
-    required this.field,
-  });
-
-  @override
-  Future<Either<FieldTypeOptionDataPB, FlowyError>> load() {
-    final payload = GridFieldTypeOptionIdPB.create()
-      ..gridId = gridId
-      ..fieldId = field.id
-      ..fieldType = field.fieldType;
-
-    return GridEventGetFieldTypeOption(payload).send();
-  }
-}
-
-class TypeOptionDataController {
-  final String gridId;
-  final IFieldTypeOptionLoader _loader;
-
-  late FieldTypeOptionDataPB _data;
-  final PublishNotifier<GridFieldPB> _fieldNotifier = PublishNotifier();
-
-  TypeOptionDataController({
-    required this.gridId,
-    required IFieldTypeOptionLoader loader,
-  }) : _loader = loader;
-
-  Future<Either<Unit, FlowyError>> loadData() async {
-    final result = await _loader.load();
-    return result.fold(
-      (data) {
-        data.freeze();
-        _data = data;
-        _fieldNotifier.value = data.field_2;
-        return left(unit);
-      },
-      (err) {
-        Log.error(err);
-        return right(err);
-      },
-    );
-  }
-
-  GridFieldPB get field => _data.field_2;
-
-  set field(GridFieldPB field) {
-    _updateData(newField: field);
-  }
-
-  List<int> get typeOptionData => _data.typeOptionData;
-
-  set fieldName(String name) {
-    _updateData(newName: name);
-  }
-
-  set typeOptionData(List<int> typeOptionData) {
-    _updateData(newTypeOptionData: typeOptionData);
-  }
-
-  void _updateData({String? newName, GridFieldPB? newField, List<int>? newTypeOptionData}) {
-    _data = _data.rebuild((rebuildData) {
-      if (newName != null) {
-        rebuildData.field_2 = rebuildData.field_2.rebuild((rebuildField) {
-          rebuildField.name = newName;
-        });
-      }
-
-      if (newField != null) {
-        rebuildData.field_2 = newField;
-      }
-
-      if (newTypeOptionData != null) {
-        rebuildData.typeOptionData = newTypeOptionData;
-      }
-    });
-
-    _fieldNotifier.value = _data.field_2;
-
-    FieldService.insertField(
-      gridId: gridId,
-      field: field,
-      typeOptionData: typeOptionData,
-    );
-  }
-
-  Future<void> switchToField(FieldType newFieldType) {
-    return _loader.switchToField(field.id, newFieldType).then((result) {
-      return result.fold(
-        (fieldTypeOptionData) {
-          _updateData(
-            newField: fieldTypeOptionData.field_2,
-            newTypeOptionData: fieldTypeOptionData.typeOptionData,
-          );
-        },
-        (err) {
-          Log.error(err);
-        },
-      );
-    });
-  }
-
-  void Function() addFieldListener(void Function(GridFieldPB) callback) {
-    listener() {
-      callback(field);
-    }
-
-    _fieldNotifier.addListener(listener);
-    return listener;
-  }
-
-  void removeFieldListener(void Function() listener) {
-    _fieldNotifier.removeListener(listener);
-  }
-}

+ 9 - 6
frontend/app_flowy/lib/plugins/grid/application/field/field_type_option_edit_bloc.dart

@@ -3,11 +3,11 @@ import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
 
-import 'field_service.dart';
-
+import 'type_option/type_option_data_controller.dart';
 part 'field_type_option_edit_bloc.freezed.dart';
 
-class FieldTypeOptionEditBloc extends Bloc<FieldTypeOptionEditEvent, FieldTypeOptionEditState> {
+class FieldTypeOptionEditBloc
+    extends Bloc<FieldTypeOptionEditEvent, FieldTypeOptionEditState> {
   final TypeOptionDataController _dataController;
   void Function()? _fieldListenFn;
 
@@ -42,16 +42,19 @@ class FieldTypeOptionEditBloc extends Bloc<FieldTypeOptionEditEvent, FieldTypeOp
 @freezed
 class FieldTypeOptionEditEvent with _$FieldTypeOptionEditEvent {
   const factory FieldTypeOptionEditEvent.initial() = _Initial;
-  const factory FieldTypeOptionEditEvent.didReceiveFieldUpdated(GridFieldPB field) = _DidReceiveFieldUpdated;
+  const factory FieldTypeOptionEditEvent.didReceiveFieldUpdated(FieldPB field) =
+      _DidReceiveFieldUpdated;
 }
 
 @freezed
 class FieldTypeOptionEditState with _$FieldTypeOptionEditState {
   const factory FieldTypeOptionEditState({
-    required GridFieldPB field,
+    required FieldPB field,
   }) = _FieldTypeOptionEditState;
 
-  factory FieldTypeOptionEditState.initial(TypeOptionDataController fieldContext) => FieldTypeOptionEditState(
+  factory FieldTypeOptionEditState.initial(
+          TypeOptionDataController fieldContext) =>
+      FieldTypeOptionEditState(
         field: fieldContext.field,
       );
 }

+ 7 - 4
frontend/app_flowy/lib/plugins/grid/application/field/grid_listener.dart

@@ -7,15 +7,17 @@ import 'dart:async';
 import 'dart:typed_data';
 import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
 
-typedef UpdateFieldNotifiedValue = Either<GridFieldChangesetPB, FlowyError>;
+typedef UpdateFieldNotifiedValue = Either<FieldChangesetPB, FlowyError>;
 
 class GridFieldsListener {
   final String gridId;
-  PublishNotifier<UpdateFieldNotifiedValue>? updateFieldsNotifier = PublishNotifier();
+  PublishNotifier<UpdateFieldNotifiedValue>? updateFieldsNotifier =
+      PublishNotifier();
   GridNotificationListener? _listener;
   GridFieldsListener({required this.gridId});
 
-  void start({required void Function(UpdateFieldNotifiedValue) onFieldsChanged}) {
+  void start(
+      {required void Function(UpdateFieldNotifiedValue) onFieldsChanged}) {
     updateFieldsNotifier?.addPublishListener(onFieldsChanged);
     _listener = GridNotificationListener(
       objectId: gridId,
@@ -27,7 +29,8 @@ class GridFieldsListener {
     switch (ty) {
       case GridNotification.DidUpdateGridField:
         result.fold(
-          (payload) => updateFieldsNotifier?.value = left(GridFieldChangesetPB.fromBuffer(payload)),
+          (payload) => updateFieldsNotifier?.value =
+              left(FieldChangesetPB.fromBuffer(payload)),
           (error) => updateFieldsNotifier?.value = right(error),
         );
         break;

+ 5 - 13
frontend/app_flowy/lib/plugins/grid/application/field/type_option/date_bloc.dart

@@ -1,20 +1,12 @@
-import 'package:app_flowy/plugins/grid/application/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 = TypeOptionWidgetContext<DateTypeOption>;
 
-class DateTypeOptionDataParser extends TypeOptionDataParser<DateTypeOption> {
-  @override
-  DateTypeOption fromBuffer(List<int> buffer) {
-    return DateTypeOption.fromBuffer(buffer);
-  }
-}
+import 'type_option_context.dart';
+part 'date_bloc.freezed.dart';
 
 class DateTypeOptionBloc
     extends Bloc<DateTypeOptionEvent, DateTypeOptionState> {
@@ -40,7 +32,7 @@ class DateTypeOptionBloc
     );
   }
 
-  DateTypeOption _updateTypeOption({
+  DateTypeOptionPB _updateTypeOption({
     DateFormat? dateFormat,
     TimeFormat? timeFormat,
     bool? includeTime,
@@ -80,9 +72,9 @@ class DateTypeOptionEvent with _$DateTypeOptionEvent {
 @freezed
 class DateTypeOptionState with _$DateTypeOptionState {
   const factory DateTypeOptionState({
-    required DateTypeOption typeOption,
+    required DateTypeOptionPB typeOption,
   }) = _DateTypeOptionState;
 
-  factory DateTypeOptionState.initial(DateTypeOption typeOption) =>
+  factory DateTypeOptionState.initial(DateTypeOptionPB typeOption) =>
       DateTypeOptionState(typeOption: typeOption);
 }

+ 22 - 23
frontend/app_flowy/lib/plugins/grid/application/field/type_option/multi_select_type_option.dart

@@ -1,25 +1,32 @@
-import 'package:app_flowy/plugins/grid/application/field/field_service.dart';
 import 'package:flowy_sdk/log.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_context.dart';
 import 'type_option_service.dart';
+import 'package:protobuf/protobuf.dart';
+
+class MultiSelectAction with ISelectOptionAction {
+  final String gridId;
+  final String fieldId;
+  final TypeOptionFFIService service;
+  final MultiSelectTypeOptionContext typeOptionContext;
+
+  MultiSelectAction({
+    required this.gridId,
+    required this.fieldId,
+    required this.typeOptionContext,
+  }) : service = TypeOptionFFIService(
+          gridId: gridId,
+          fieldId: fieldId,
+        );
 
-class MultiSelectTypeOptionContext
-    extends TypeOptionWidgetContext<MultiSelectTypeOption>
-    with SelectOptionTypeOptionAction {
-  final TypeOptionService service;
+  MultiSelectTypeOptionPB get typeOption => typeOptionContext.typeOption;
 
-  MultiSelectTypeOptionContext({
-    required MultiSelectTypeOptionWidgetDataParser dataBuilder,
-    required TypeOptionDataController dataController,
-  })  : service = TypeOptionService(
-          gridId: dataController.gridId,
-          fieldId: dataController.field.id,
-        ),
-        super(dataParser: dataBuilder, dataController: dataController);
+  set typeOption(MultiSelectTypeOptionPB newTypeOption) {
+    typeOptionContext.typeOption = newTypeOption;
+  }
 
   @override
   List<SelectOptionPB> Function(SelectOptionPB) get deleteOption {
@@ -59,7 +66,7 @@ class MultiSelectTypeOptionContext
   }
 
   @override
-  List<SelectOptionPB> Function(SelectOptionPB) get udpateOption {
+  List<SelectOptionPB> Function(SelectOptionPB) get updateOption {
     return (SelectOptionPB option) {
       typeOption.freeze();
       typeOption = typeOption.rebuild((typeOption) {
@@ -73,11 +80,3 @@ class MultiSelectTypeOptionContext
     };
   }
 }
-
-class MultiSelectTypeOptionWidgetDataParser
-    extends TypeOptionDataParser<MultiSelectTypeOption> {
-  @override
-  MultiSelectTypeOption fromBuffer(List<int> buffer) {
-    return MultiSelectTypeOption.fromBuffer(buffer);
-  }
-}

+ 4 - 14
frontend/app_flowy/lib/plugins/grid/application/field/type_option/number_bloc.dart

@@ -1,23 +1,13 @@
-import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_service.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/format.pbenum.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/number_type_option.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
 import 'package:protobuf/protobuf.dart';
+import 'type_option_context.dart';
 
 part 'number_bloc.freezed.dart';
 
-typedef NumberTypeOptionContext = TypeOptionWidgetContext<NumberTypeOption>;
-
-class NumberTypeOptionWidgetDataParser
-    extends TypeOptionDataParser<NumberTypeOption> {
-  @override
-  NumberTypeOption fromBuffer(List<int> buffer) {
-    return NumberTypeOption.fromBuffer(buffer);
-  }
-}
-
 class NumberTypeOptionBloc
     extends Bloc<NumberTypeOptionEvent, NumberTypeOptionState> {
   NumberTypeOptionBloc({required NumberTypeOptionContext typeOptionContext})
@@ -33,7 +23,7 @@ class NumberTypeOptionBloc
     );
   }
 
-  NumberTypeOption _updateNumberFormat(NumberFormat format) {
+  NumberTypeOptionPB _updateNumberFormat(NumberFormat format) {
     state.typeOption.freeze();
     return state.typeOption.rebuild((typeOption) {
       typeOption.format = format;
@@ -55,10 +45,10 @@ class NumberTypeOptionEvent with _$NumberTypeOptionEvent {
 @freezed
 class NumberTypeOptionState with _$NumberTypeOptionState {
   const factory NumberTypeOptionState({
-    required NumberTypeOption typeOption,
+    required NumberTypeOptionPB typeOption,
   }) = _NumberTypeOptionState;
 
-  factory NumberTypeOptionState.initial(NumberTypeOption typeOption) =>
+  factory NumberTypeOptionState.initial(NumberTypeOptionPB typeOption) =>
       NumberTypeOptionState(
         typeOption: typeOption,
       );

+ 22 - 13
frontend/app_flowy/lib/plugins/grid/application/field/type_option/select_option_type_option_bloc.dart

@@ -5,16 +5,17 @@ import 'dart:async';
 import 'package:dartz/dartz.dart';
 part 'select_option_type_option_bloc.freezed.dart';
 
-abstract class SelectOptionTypeOptionAction {
+abstract class ISelectOptionAction {
   Future<List<SelectOptionPB>> Function(String) get insertOption;
 
   List<SelectOptionPB> Function(SelectOptionPB) get deleteOption;
 
-  List<SelectOptionPB> Function(SelectOptionPB) get udpateOption;
+  List<SelectOptionPB> Function(SelectOptionPB) get updateOption;
 }
 
-class SelectOptionTypeOptionBloc extends Bloc<SelectOptionTypeOptionEvent, SelectOptionTypeOptionState> {
-  final SelectOptionTypeOptionAction typeOptionAction;
+class SelectOptionTypeOptionBloc
+    extends Bloc<SelectOptionTypeOptionEvent, SelectOptionTypeOptionState> {
+  final ISelectOptionAction typeOptionAction;
 
   SelectOptionTypeOptionBloc({
     required List<SelectOptionPB> options,
@@ -24,7 +25,8 @@ class SelectOptionTypeOptionBloc extends Bloc<SelectOptionTypeOptionEvent, Selec
       (event, emit) async {
         await event.when(
           createOption: (optionName) async {
-            final List<SelectOptionPB> options = await typeOptionAction.insertOption(optionName);
+            final List<SelectOptionPB> options =
+                await typeOptionAction.insertOption(optionName);
             emit(state.copyWith(options: options));
           },
           addingOption: () {
@@ -34,11 +36,13 @@ class SelectOptionTypeOptionBloc extends Bloc<SelectOptionTypeOptionEvent, Selec
             emit(state.copyWith(isEditingOption: false, newOptionName: none()));
           },
           updateOption: (option) {
-            final List<SelectOptionPB> options = typeOptionAction.udpateOption(option);
+            final List<SelectOptionPB> options =
+                typeOptionAction.updateOption(option);
             emit(state.copyWith(options: options));
           },
           deleteOption: (option) {
-            final List<SelectOptionPB> options = typeOptionAction.deleteOption(option);
+            final List<SelectOptionPB> options =
+                typeOptionAction.deleteOption(option);
             emit(state.copyWith(options: options));
           },
         );
@@ -54,11 +58,15 @@ class SelectOptionTypeOptionBloc extends Bloc<SelectOptionTypeOptionEvent, Selec
 
 @freezed
 class SelectOptionTypeOptionEvent with _$SelectOptionTypeOptionEvent {
-  const factory SelectOptionTypeOptionEvent.createOption(String optionName) = _CreateOption;
+  const factory SelectOptionTypeOptionEvent.createOption(String optionName) =
+      _CreateOption;
   const factory SelectOptionTypeOptionEvent.addingOption() = _AddingOption;
-  const factory SelectOptionTypeOptionEvent.endAddingOption() = _EndAddingOption;
-  const factory SelectOptionTypeOptionEvent.updateOption(SelectOptionPB option) = _UpdateOption;
-  const factory SelectOptionTypeOptionEvent.deleteOption(SelectOptionPB option) = _DeleteOption;
+  const factory SelectOptionTypeOptionEvent.endAddingOption() =
+      _EndAddingOption;
+  const factory SelectOptionTypeOptionEvent.updateOption(
+      SelectOptionPB option) = _UpdateOption;
+  const factory SelectOptionTypeOptionEvent.deleteOption(
+      SelectOptionPB option) = _DeleteOption;
 }
 
 @freezed
@@ -67,9 +75,10 @@ class SelectOptionTypeOptionState with _$SelectOptionTypeOptionState {
     required List<SelectOptionPB> options,
     required bool isEditingOption,
     required Option<String> newOptionName,
-  }) = _SelectOptionTyepOptionState;
+  }) = _SelectOptionTypeOptionState;
 
-  factory SelectOptionTypeOptionState.initial(List<SelectOptionPB> options) => SelectOptionTypeOptionState(
+  factory SelectOptionTypeOptionState.initial(List<SelectOptionPB> options) =>
+      SelectOptionTypeOptionState(
         options: options,
         isEditingOption: false,
         newOptionName: none(),

+ 18 - 22
frontend/app_flowy/lib/plugins/grid/application/field/type_option/single_select_type_option.dart

@@ -1,25 +1,29 @@
-import 'package:app_flowy/plugins/grid/application/field/field_service.dart';
 import 'package:flowy_sdk/log.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_context.dart';
 import 'type_option_service.dart';
 
-class SingleSelectTypeOptionContext
-    extends TypeOptionWidgetContext<SingleSelectTypeOptionPB>
-    with SelectOptionTypeOptionAction {
-  final TypeOptionService service;
+class SingleSelectAction with ISelectOptionAction {
+  final String gridId;
+  final String fieldId;
+  final SingleSelectTypeOptionContext typeOptionContext;
+  final TypeOptionFFIService service;
 
-  SingleSelectTypeOptionContext({
-    required SingleSelectTypeOptionWidgetDataParser dataBuilder,
-    required TypeOptionDataController fieldContext,
-  })  : service = TypeOptionService(
-          gridId: fieldContext.gridId,
-          fieldId: fieldContext.field.id,
-        ),
-        super(dataParser: dataBuilder, dataController: fieldContext);
+  SingleSelectAction({
+    required this.gridId,
+    required this.fieldId,
+    required this.typeOptionContext,
+  }) : service = TypeOptionFFIService(gridId: gridId, fieldId: fieldId);
+
+  SingleSelectTypeOptionPB get typeOption => typeOptionContext.typeOption;
+
+  set typeOption(SingleSelectTypeOptionPB newTypeOption) {
+    typeOptionContext.typeOption = newTypeOption;
+  }
 
   @override
   List<SelectOptionPB> Function(SelectOptionPB) get deleteOption {
@@ -59,7 +63,7 @@ class SingleSelectTypeOptionContext
   }
 
   @override
-  List<SelectOptionPB> Function(SelectOptionPB) get udpateOption {
+  List<SelectOptionPB> Function(SelectOptionPB) get updateOption {
     return (SelectOptionPB option) {
       typeOption.freeze();
       typeOption = typeOption.rebuild((typeOption) {
@@ -73,11 +77,3 @@ class SingleSelectTypeOptionContext
     };
   }
 }
-
-class SingleSelectTypeOptionWidgetDataParser
-    extends TypeOptionDataParser<SingleSelectTypeOptionPB> {
-  @override
-  SingleSelectTypeOptionPB fromBuffer(List<int> buffer) {
-    return SingleSelectTypeOptionPB.fromBuffer(buffer);
-  }
-}

+ 195 - 0
frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_context.dart

@@ -0,0 +1,195 @@
+import 'package:flowy_sdk/dispatch/dispatch.dart';
+import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_type_option.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
+import 'package:dartz/dartz.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/multi_select_type_option.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/number_type_option.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/single_select_type_option.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/text_type_option.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option.pb.dart';
+import 'package:protobuf/protobuf.dart';
+
+import 'type_option_data_controller.dart';
+
+abstract class TypeOptionDataParser<T> {
+  T fromBuffer(List<int> buffer);
+}
+
+// Number
+typedef NumberTypeOptionContext = TypeOptionContext<NumberTypeOptionPB>;
+
+class NumberTypeOptionWidgetDataParser
+    extends TypeOptionDataParser<NumberTypeOptionPB> {
+  @override
+  NumberTypeOptionPB fromBuffer(List<int> buffer) {
+    return NumberTypeOptionPB.fromBuffer(buffer);
+  }
+}
+
+// RichText
+typedef RichTextTypeOptionContext = TypeOptionContext<RichTextTypeOptionPB>;
+
+class RichTextTypeOptionWidgetDataParser
+    extends TypeOptionDataParser<RichTextTypeOptionPB> {
+  @override
+  RichTextTypeOptionPB fromBuffer(List<int> buffer) {
+    return RichTextTypeOptionPB.fromBuffer(buffer);
+  }
+}
+
+// Checkbox
+typedef CheckboxTypeOptionContext = TypeOptionContext<CheckboxTypeOptionPB>;
+
+class CheckboxTypeOptionWidgetDataParser
+    extends TypeOptionDataParser<CheckboxTypeOptionPB> {
+  @override
+  CheckboxTypeOptionPB fromBuffer(List<int> buffer) {
+    return CheckboxTypeOptionPB.fromBuffer(buffer);
+  }
+}
+
+// URL
+typedef URLTypeOptionContext = TypeOptionContext<URLTypeOptionPB>;
+
+class URLTypeOptionWidgetDataParser
+    extends TypeOptionDataParser<URLTypeOptionPB> {
+  @override
+  URLTypeOptionPB fromBuffer(List<int> buffer) {
+    return URLTypeOptionPB.fromBuffer(buffer);
+  }
+}
+
+// Date
+typedef DateTypeOptionContext = TypeOptionContext<DateTypeOptionPB>;
+
+class DateTypeOptionDataParser extends TypeOptionDataParser<DateTypeOptionPB> {
+  @override
+  DateTypeOptionPB fromBuffer(List<int> buffer) {
+    return DateTypeOptionPB.fromBuffer(buffer);
+  }
+}
+
+// SingleSelect
+typedef SingleSelectTypeOptionContext
+    = TypeOptionContext<SingleSelectTypeOptionPB>;
+
+class SingleSelectTypeOptionWidgetDataParser
+    extends TypeOptionDataParser<SingleSelectTypeOptionPB> {
+  @override
+  SingleSelectTypeOptionPB fromBuffer(List<int> buffer) {
+    return SingleSelectTypeOptionPB.fromBuffer(buffer);
+  }
+}
+
+// Multi-select
+typedef MultiSelectTypeOptionContext
+    = TypeOptionContext<MultiSelectTypeOptionPB>;
+
+class MultiSelectTypeOptionWidgetDataParser
+    extends TypeOptionDataParser<MultiSelectTypeOptionPB> {
+  @override
+  MultiSelectTypeOptionPB fromBuffer(List<int> buffer) {
+    return MultiSelectTypeOptionPB.fromBuffer(buffer);
+  }
+}
+
+class TypeOptionContext<T extends GeneratedMessage> {
+  T? _typeOptionObject;
+  final TypeOptionDataParser<T> dataParser;
+  final TypeOptionDataController _dataController;
+
+  TypeOptionContext({
+    required this.dataParser,
+    required TypeOptionDataController dataController,
+  }) : _dataController = dataController;
+
+  String get gridId => _dataController.gridId;
+
+  String get fieldId => _dataController.field.id;
+
+  Future<void> loadTypeOptionData({
+    required void Function(T) onCompleted,
+    required void Function(FlowyError) onError,
+  }) async {
+    await _dataController.loadTypeOptionData().then((result) {
+      result.fold((l) => null, (err) => onError(err));
+    });
+
+    onCompleted(typeOption);
+  }
+
+  T get typeOption {
+    if (_typeOptionObject != null) {
+      return _typeOptionObject!;
+    }
+
+    final T object = _dataController.getTypeOption(dataParser);
+    _typeOptionObject = object;
+    return object;
+  }
+
+  set typeOption(T typeOption) {
+    _dataController.typeOptionData = typeOption.writeToBuffer();
+    _typeOptionObject = typeOption;
+  }
+}
+
+abstract class TypeOptionFieldDelegate {
+  void onFieldChanged(void Function(String) callback);
+  void dispose();
+}
+
+abstract class IFieldTypeOptionLoader {
+  String get gridId;
+  Future<Either<FieldTypeOptionDataPB, FlowyError>> load();
+
+  Future<Either<FieldTypeOptionDataPB, FlowyError>> switchToField(
+      String fieldId, FieldType fieldType) {
+    final payload = EditFieldPayloadPB.create()
+      ..gridId = gridId
+      ..fieldId = fieldId
+      ..fieldType = fieldType;
+
+    return GridEventSwitchToField(payload).send();
+  }
+}
+
+class NewFieldTypeOptionLoader extends IFieldTypeOptionLoader {
+  @override
+  final String gridId;
+  NewFieldTypeOptionLoader({
+    required this.gridId,
+  });
+
+  @override
+  Future<Either<FieldTypeOptionDataPB, FlowyError>> load() {
+    final payload = CreateFieldPayloadPB.create()
+      ..gridId = gridId
+      ..fieldType = FieldType.RichText;
+
+    return GridEventCreateFieldTypeOption(payload).send();
+  }
+}
+
+class FieldTypeOptionLoader extends IFieldTypeOptionLoader {
+  @override
+  final String gridId;
+  final FieldPB field;
+
+  FieldTypeOptionLoader({
+    required this.gridId,
+    required this.field,
+  });
+
+  @override
+  Future<Either<FieldTypeOptionDataPB, FlowyError>> load() {
+    final payload = FieldTypeOptionIdPB.create()
+      ..gridId = gridId
+      ..fieldId = field.id
+      ..fieldType = field.fieldType;
+
+    return GridEventGetFieldTypeOption(payload).send();
+  }
+}

+ 123 - 0
frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_data_controller.dart

@@ -0,0 +1,123 @@
+import 'package:flowy_infra/notifier.dart';
+import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
+import 'package:app_flowy/plugins/grid/application/field/field_service.dart';
+import 'package:dartz/dartz.dart';
+import 'package:protobuf/protobuf.dart';
+import 'package:flowy_sdk/log.dart';
+
+import 'type_option_context.dart';
+
+class TypeOptionDataController {
+  final String gridId;
+  final IFieldTypeOptionLoader loader;
+  late FieldTypeOptionDataPB _data;
+  final PublishNotifier<FieldPB> _fieldNotifier = PublishNotifier();
+
+  TypeOptionDataController({
+    required this.gridId,
+    required this.loader,
+    FieldPB? field,
+  }) {
+    if (field != null) {
+      _data = FieldTypeOptionDataPB.create()
+        ..gridId = gridId
+        ..field_2 = field;
+    }
+  }
+
+  Future<Either<Unit, FlowyError>> loadTypeOptionData() async {
+    final result = await loader.load();
+    return result.fold(
+      (data) {
+        data.freeze();
+        _data = data;
+        _fieldNotifier.value = data.field_2;
+        return left(unit);
+      },
+      (err) {
+        Log.error(err);
+        return right(err);
+      },
+    );
+  }
+
+  FieldPB get field {
+    return _data.field_2;
+  }
+
+  set field(FieldPB field) {
+    _updateData(newField: field);
+  }
+
+  T getTypeOption<T>(TypeOptionDataParser<T> parser) {
+    return parser.fromBuffer(_data.typeOptionData);
+  }
+
+  set fieldName(String name) {
+    _updateData(newName: name);
+  }
+
+  set typeOptionData(List<int> typeOptionData) {
+    _updateData(newTypeOptionData: typeOptionData);
+  }
+
+  void _updateData({
+    String? newName,
+    FieldPB? newField,
+    List<int>? newTypeOptionData,
+  }) {
+    _data = _data.rebuild((rebuildData) {
+      if (newName != null) {
+        rebuildData.field_2 = rebuildData.field_2.rebuild((rebuildField) {
+          rebuildField.name = newName;
+        });
+      }
+
+      if (newField != null) {
+        rebuildData.field_2 = newField;
+      }
+
+      if (newTypeOptionData != null) {
+        rebuildData.typeOptionData = newTypeOptionData;
+      }
+    });
+
+    _fieldNotifier.value = _data.field_2;
+
+    FieldService.insertField(
+      gridId: gridId,
+      field: field,
+      typeOptionData: _data.typeOptionData,
+    );
+  }
+
+  Future<void> switchToField(FieldType newFieldType) {
+    return loader.switchToField(field.id, newFieldType).then((result) {
+      return result.fold(
+        (fieldTypeOptionData) {
+          _updateData(
+            newField: fieldTypeOptionData.field_2,
+            newTypeOptionData: fieldTypeOptionData.typeOptionData,
+          );
+        },
+        (err) {
+          Log.error(err);
+        },
+      );
+    });
+  }
+
+  void Function() addFieldListener(void Function(FieldPB) callback) {
+    listener() {
+      callback(field);
+    }
+
+    _fieldNotifier.addListener(listener);
+    return listener;
+  }
+
+  void removeFieldListener(void Function() listener) {
+    _fieldNotifier.removeListener(listener);
+  }
+}

+ 2 - 83
frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_service.dart

@@ -1,19 +1,14 @@
-import 'dart:typed_data';
-
-import 'package:app_flowy/plugins/grid/application/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/cell_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:protobuf/protobuf.dart';
 
-class TypeOptionService {
+class TypeOptionFFIService {
   final String gridId;
   final String fieldId;
 
-  TypeOptionService({
+  TypeOptionFFIService({
     required this.gridId,
     required this.fieldId,
   });
@@ -29,79 +24,3 @@ class TypeOptionService {
     return GridEventNewSelectOption(payload).send();
   }
 }
-
-abstract class TypeOptionDataParser<T> {
-  T fromBuffer(List<int> buffer);
-}
-
-class TypeOptionWidgetContext<T extends GeneratedMessage> {
-  T? _typeOptionObject;
-  final TypeOptionDataController _dataController;
-  final TypeOptionDataParser<T> dataParser;
-
-  TypeOptionWidgetContext({
-    required this.dataParser,
-    required TypeOptionDataController dataController,
-  }) : _dataController = dataController;
-
-  String get gridId => _dataController.gridId;
-
-  GridFieldPB get field => _dataController.field;
-
-  T get typeOption {
-    if (_typeOptionObject != null) {
-      return _typeOptionObject!;
-    }
-
-    final T object = dataParser.fromBuffer(_dataController.typeOptionData);
-    _typeOptionObject = object;
-    return object;
-  }
-
-  set typeOption(T typeOption) {
-    _dataController.typeOptionData = typeOption.writeToBuffer();
-    _typeOptionObject = typeOption;
-  }
-}
-
-abstract class TypeOptionFieldDelegate {
-  void onFieldChanged(void Function(String) callback);
-  void dispose();
-}
-
-class TypeOptionContext2<T> {
-  final String gridId;
-  final GridFieldPB field;
-  final FieldService _fieldService;
-  T? _data;
-  final TypeOptionDataParser dataBuilder;
-
-  TypeOptionContext2({
-    required this.gridId,
-    required this.field,
-    required this.dataBuilder,
-    Uint8List? data,
-  }) : _fieldService = FieldService(gridId: gridId, fieldId: field.id) {
-    if (data != null) {
-      _data = dataBuilder.fromBuffer(data);
-    }
-  }
-
-  Future<Either<T, FlowyError>> typeOptionData() {
-    if (_data != null) {
-      return Future(() => left(_data!));
-    }
-
-    return _fieldService
-        .getFieldTypeOptionData(fieldType: field.fieldType)
-        .then((result) {
-      return result.fold(
-        (data) {
-          _data = dataBuilder.fromBuffer(data.typeOptionData);
-          return left(_data!);
-        },
-        (err) => right(err),
-      );
-    });
-  }
-}

+ 68 - 94
frontend/app_flowy/lib/plugins/grid/application/grid_bloc.dart

@@ -1,40 +1,23 @@
 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/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 'block/block_cache.dart';
-import 'grid_service.dart';
-import 'row/row_service.dart';
+import 'grid_data_controller.dart';
+import 'row/row_cache.dart';
 import 'dart:collection';
 
 part 'grid_bloc.freezed.dart';
 
 class GridBloc extends Bloc<GridEvent, GridState> {
-  final String gridId;
-  final GridService _gridService;
-  final GridFieldCache fieldCache;
-
-  // key: the block id
-  final LinkedHashMap<String, GridBlockCache> _blocks;
-
-  List<GridRowInfo> get rowInfos {
-    final List<GridRowInfo> rows = [];
-    for (var block in _blocks.values) {
-      rows.addAll(block.rows);
-    }
-    return rows;
-  }
+  final GridDataController dataController;
 
   GridBloc({required ViewPB view})
-      : gridId = view.id,
-        _blocks = LinkedHashMap.identity(),
-        _gridService = GridService(gridId: view.id),
-        fieldCache = GridFieldCache(gridId: view.id),
+      : dataController = GridDataController(view: view),
         super(GridState.initial(view.id)) {
     on<GridEvent>(
       (event, emit) async {
@@ -44,13 +27,21 @@ class GridBloc extends Bloc<GridEvent, GridState> {
             await _loadGrid(emit);
           },
           createRow: () {
-            _gridService.createRow();
+            dataController.createRow();
           },
-          didReceiveRowUpdate: (newRowInfos, reason) {
-            emit(state.copyWith(rowInfos: newRowInfos, reason: reason));
+          didReceiveGridUpdate: (grid) {
+            emit(state.copyWith(grid: Some(grid)));
           },
           didReceiveFieldUpdate: (fields) {
-            emit(state.copyWith(rowInfos: rowInfos, fields: GridFieldEquatable(fields)));
+            emit(state.copyWith(
+              fields: GridFieldEquatable(fields),
+            ));
+          },
+          didReceiveRowUpdate: (newRowInfos, reason) {
+            emit(state.copyWith(
+              rowInfos: newRowInfos,
+              reason: reason,
+            ));
           },
         );
       },
@@ -59,89 +50,63 @@ class GridBloc extends Bloc<GridEvent, GridState> {
 
   @override
   Future<void> close() async {
-    await _gridService.closeGrid();
-    await fieldCache.dispose();
-
-    for (final blockCache in _blocks.values) {
-      blockCache.dispose();
-    }
+    await dataController.dispose();
     return super.close();
   }
 
   GridRowCache? getRowCache(String blockId, String rowId) {
-    final GridBlockCache? blockCache = _blocks[blockId];
+    final GridBlockCache? blockCache = dataController.blocks[blockId];
     return blockCache?.rowCache;
   }
 
   void _startListening() {
-    fieldCache.addListener(
-      listenWhen: () => !isClosed,
-      onFields: (fields) => add(GridEvent.didReceiveFieldUpdate(fields)),
+    dataController.addListener(
+      onGridChanged: (grid) {
+        if (!isClosed) {
+          add(GridEvent.didReceiveGridUpdate(grid));
+        }
+      },
+      onRowsChanged: (rowInfos, reason) {
+        if (!isClosed) {
+          add(GridEvent.didReceiveRowUpdate(rowInfos, reason));
+        }
+      },
+      onFieldsChanged: (fields) {
+        if (!isClosed) {
+          add(GridEvent.didReceiveFieldUpdate(fields));
+        }
+      },
     );
   }
 
   Future<void> _loadGrid(Emitter<GridState> emit) async {
-    final result = await _gridService.loadGrid();
-    return Future(
-      () => result.fold(
-        (grid) async {
-          _initialBlocks(grid.blocks);
-          await _loadFields(grid, emit);
-        },
-        (err) => emit(state.copyWith(loadingState: GridLoadingState.finish(right(err)))),
+    final result = await dataController.loadData();
+    result.fold(
+      (grid) => emit(
+        state.copyWith(loadingState: GridLoadingState.finish(left(unit))),
       ),
-    );
-  }
-
-  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;
-
-          emit(state.copyWith(
-            grid: Some(grid),
-            fields: GridFieldEquatable(fieldCache.fields),
-            rowInfos: rowInfos,
-            loadingState: GridLoadingState.finish(left(unit)),
-          ));
-        },
-        (err) => emit(state.copyWith(loadingState: GridLoadingState.finish(right(err)))),
+      (err) => emit(
+        state.copyWith(loadingState: GridLoadingState.finish(right(err))),
       ),
     );
   }
-
-  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<GridRowInfo> rows, GridRowChangeReason listState) =
-      _DidReceiveRowUpdate;
-  const factory GridEvent.didReceiveFieldUpdate(List<GridFieldPB> fields) = _DidReceiveFieldUpdate;
+  const factory GridEvent.didReceiveRowUpdate(
+    List<RowInfo> rows,
+    RowsChangedReason listState,
+  ) = _DidReceiveRowUpdate;
+  const factory GridEvent.didReceiveFieldUpdate(
+    UnmodifiableListView<FieldPB> fields,
+  ) = _DidReceiveFieldUpdate;
+
+  const factory GridEvent.didReceiveGridUpdate(
+    GridPB grid,
+  ) = _DidReceiveGridUpdate;
 }
 
 @freezed
@@ -150,13 +115,13 @@ class GridState with _$GridState {
     required String gridId,
     required Option<GridPB> grid,
     required GridFieldEquatable fields,
-    required List<GridRowInfo> rowInfos,
+    required List<RowInfo> rowInfos,
     required GridLoadingState loadingState,
-    required GridRowChangeReason reason,
+    required RowsChangedReason reason,
   }) = _GridState;
 
   factory GridState.initial(String gridId) => GridState(
-        fields: const GridFieldEquatable([]),
+        fields: GridFieldEquatable(UnmodifiableListView([])),
         rowInfos: [],
         grid: none(),
         gridId: gridId,
@@ -168,20 +133,29 @@ class GridState with _$GridState {
 @freezed
 class GridLoadingState with _$GridLoadingState {
   const factory GridLoadingState.loading() = _Loading;
-  const factory GridLoadingState.finish(Either<Unit, FlowyError> successOrFail) = _Finish;
+  const factory GridLoadingState.finish(
+      Either<Unit, FlowyError> successOrFail) = _Finish;
 }
 
 class GridFieldEquatable extends Equatable {
-  final List<GridFieldPB> _fields;
-  const GridFieldEquatable(List<GridFieldPB> fields) : _fields = fields;
+  final UnmodifiableListView<FieldPB> _fields;
+  const GridFieldEquatable(
+    UnmodifiableListView<FieldPB> fields,
+  ) : _fields = fields;
 
   @override
   List<Object?> get props {
+    if (_fields.isEmpty) {
+      return [];
+    }
+
     return [
       _fields.length,
-      _fields.map((field) => field.width).reduce((value, element) => value + element),
+      _fields
+          .map((field) => field.width)
+          .reduce((value, element) => value + element),
     ];
   }
 
-  UnmodifiableListView<GridFieldPB> get value => UnmodifiableListView(_fields);
+  UnmodifiableListView<FieldPB> get value => UnmodifiableListView(_fields);
 }

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

@@ -0,0 +1,130 @@
+import 'dart:collection';
+
+import 'package:flowy_sdk/log.dart';
+import 'package:flowy_sdk/protobuf/flowy-error/errors.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 'dart:async';
+import 'package:dartz/dartz.dart';
+import 'block/block_cache.dart';
+import 'field/field_cache.dart';
+import 'prelude.dart';
+import 'row/row_cache.dart';
+
+typedef OnFieldsChanged = void Function(UnmodifiableListView<FieldPB>);
+typedef OnGridChanged = void Function(GridPB);
+
+typedef OnRowsChanged = void Function(
+  List<RowInfo> rowInfos,
+  RowsChangedReason,
+);
+typedef ListenOnRowChangedCondition = bool Function();
+
+class GridDataController {
+  final String gridId;
+  final GridFFIService _gridFFIService;
+  final GridFieldCache fieldCache;
+
+  // key: the block id
+  final LinkedHashMap<String, GridBlockCache> _blocks;
+  UnmodifiableMapView<String, GridBlockCache> get blocks =>
+      UnmodifiableMapView(_blocks);
+
+  OnRowsChanged? _onRowChanged;
+  OnFieldsChanged? _onFieldsChanged;
+  OnGridChanged? _onGridChanged;
+
+  List<RowInfo> get rowInfos {
+    final List<RowInfo> rows = [];
+    for (var block in _blocks.values) {
+      rows.addAll(block.rows);
+    }
+    return rows;
+  }
+
+  GridDataController({required ViewPB view})
+      : gridId = view.id,
+        _blocks = LinkedHashMap.new(),
+        _gridFFIService = GridFFIService(gridId: view.id),
+        fieldCache = GridFieldCache(gridId: view.id);
+
+  void addListener({
+    required OnGridChanged onGridChanged,
+    required OnRowsChanged onRowsChanged,
+    required OnFieldsChanged onFieldsChanged,
+  }) {
+    _onGridChanged = onGridChanged;
+    _onRowChanged = onRowsChanged;
+    _onFieldsChanged = onFieldsChanged;
+
+    fieldCache.addListener(onFields: (fields) {
+      _onFieldsChanged?.call(UnmodifiableListView(fields));
+    });
+  }
+
+  Future<Either<Unit, FlowyError>> loadData() async {
+    final result = await _gridFFIService.loadGrid();
+    return Future(
+      () => result.fold(
+        (grid) async {
+          _initialBlocks(grid.blocks);
+          _onGridChanged?.call(grid);
+          return await _loadFields(grid);
+        },
+        (err) => right(err),
+      ),
+    );
+  }
+
+  void createRow() {
+    _gridFFIService.createRow();
+  }
+
+  Future<void> dispose() async {
+    await _gridFFIService.closeGrid();
+    await fieldCache.dispose();
+
+    for (final blockCache in _blocks.values) {
+      blockCache.dispose();
+    }
+  }
+
+  void _initialBlocks(List<BlockPB> blocks) {
+    for (final block in blocks) {
+      if (_blocks[block.id] != null) {
+        Log.warn("Initial duplicate block's cache: ${block.id}");
+        return;
+      }
+
+      final cache = GridBlockCache(
+        gridId: gridId,
+        block: block,
+        fieldCache: fieldCache,
+      );
+
+      cache.addListener(
+        onRowsChanged: (reason) {
+          _onRowChanged?.call(rowInfos, reason);
+        },
+      );
+
+      _blocks[block.id] = cache;
+    }
+  }
+
+  Future<Either<Unit, FlowyError>> _loadFields(GridPB grid) async {
+    final result = await _gridFFIService.getFields(fieldIds: grid.fields);
+    return Future(
+      () => result.fold(
+        (fields) {
+          fieldCache.fields = fields.items;
+          _onFieldsChanged?.call(UnmodifiableListView(fieldCache.fields));
+          return left(unit);
+        },
+        (err) => right(err),
+      ),
+    );
+  }
+}

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

@@ -4,7 +4,8 @@ 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 'grid_service.dart';
+
+import 'field/field_cache.dart';
 
 part 'grid_header_bloc.freezed.dart';
 
@@ -35,7 +36,7 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
 
   Future<void> _moveField(
       _MoveField value, Emitter<GridHeaderState> emit) async {
-    final fields = List<GridFieldPB>.from(state.fields);
+    final fields = List<FieldPB>.from(state.fields);
     fields.insert(value.toIndex, fields.removeAt(value.fromIndex));
     emit(state.copyWith(fields: fields));
 
@@ -63,19 +64,19 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
 @freezed
 class GridHeaderEvent with _$GridHeaderEvent {
   const factory GridHeaderEvent.initial() = _InitialHeader;
-  const factory GridHeaderEvent.didReceiveFieldUpdate(
-      List<GridFieldPB> fields) = _DidReceiveFieldUpdate;
+  const factory GridHeaderEvent.didReceiveFieldUpdate(List<FieldPB> fields) =
+      _DidReceiveFieldUpdate;
   const factory GridHeaderEvent.moveField(
-      GridFieldPB field, int fromIndex, int toIndex) = _MoveField;
+      FieldPB field, int fromIndex, int toIndex) = _MoveField;
 }
 
 @freezed
 class GridHeaderState with _$GridHeaderState {
-  const factory GridHeaderState({required List<GridFieldPB> fields}) =
+  const factory GridHeaderState({required List<FieldPB> fields}) =
       _GridHeaderState;
 
-  factory GridHeaderState.initial(List<GridFieldPB> fields) {
-    // final List<GridFieldPB> newFields = List.from(fields);
+  factory GridHeaderState.initial(List<FieldPB> fields) {
+    // final List<FieldPB> newFields = List.from(fields);
     // newFields.retainWhere((field) => field.visibility);
     return GridHeaderState(fields: fields);
   }

+ 20 - 197
frontend/app_flowy/lib/plugins/grid/application/grid_service.dart

@@ -1,21 +1,17 @@
-import 'dart:collection';
-
-import 'package:app_flowy/plugins/grid/application/field/grid_listener.dart';
 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/view.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/board_card.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/group.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/row_entities.pb.dart';
-import 'package:flutter/foundation.dart';
-import 'row/row_service.dart';
 
-class GridService {
+class GridFFIService {
   final String gridId;
-  GridService({
+  GridFFIService({
     required this.gridId,
   });
 
@@ -26,18 +22,24 @@ class GridService {
     return GridEventGetGrid(payload).send();
   }
 
-  Future<Either<GridRowPB, FlowyError>> createRow(
-      {Option<String>? startRowId}) {
-    CreateRowPayloadPB payload = CreateRowPayloadPB.create()..gridId = gridId;
+  Future<Either<RowPB, FlowyError>> createRow({Option<String>? startRowId}) {
+    var payload = CreateTableRowPayloadPB.create()..gridId = gridId;
     startRowId?.fold(() => null, (id) => payload.startRowId = id);
-    return GridEventCreateRow(payload).send();
+    return GridEventCreateTableRow(payload).send();
+  }
+
+  Future<Either<RowPB, FlowyError>> createBoardCard(String groupId) {
+    CreateBoardCardPayloadPB payload = CreateBoardCardPayloadPB.create()
+      ..gridId = gridId
+      ..groupId = groupId;
+    return GridEventCreateBoardCard(payload).send();
   }
 
-  Future<Either<RepeatedGridFieldPB, FlowyError>> getFields(
-      {required List<GridFieldIdPB> fieldIds}) {
+  Future<Either<RepeatedFieldPB, FlowyError>> getFields(
+      {required List<FieldIdPB> fieldIds}) {
     final payload = QueryFieldPayloadPB.create()
       ..gridId = gridId
-      ..fieldIds = RepeatedGridFieldIdPB(items: fieldIds);
+      ..fieldIds = RepeatedFieldIdPB(items: fieldIds);
     return GridEventGetFields(payload).send();
   }
 
@@ -45,188 +47,9 @@ class GridService {
     final request = ViewIdPB(value: gridId);
     return FolderEventCloseView(request).send();
   }
-}
-
-class FieldsNotifier extends ChangeNotifier {
-  List<GridFieldPB> _fields = [];
-
-  set fields(List<GridFieldPB> fields) {
-    _fields = fields;
-    notifyListeners();
-  }
-
-  List<GridFieldPB> get fields => _fields;
-}
-
-typedef FieldChangesetCallback = void Function(GridFieldChangesetPB);
-typedef FieldsCallback = void Function(List<GridFieldPB>);
-
-class GridFieldCache {
-  final String gridId;
-  final GridFieldsListener _fieldListener;
-  FieldsNotifier? _fieldNotifier = FieldsNotifier();
-  final Map<FieldsCallback, VoidCallback> _fieldsCallbackMap = {};
-  final Map<FieldChangesetCallback, FieldChangesetCallback>
-      _changesetCallbackMap = {};
-
-  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 _changesetCallbackMap.values) {
-            listener(changeset);
-          }
-        },
-        (err) => Log.error(err),
-      );
-    });
-  }
-
-  Future<void> dispose() async {
-    await _fieldListener.stop();
-    _fieldNotifier?.dispose();
-    _fieldNotifier = null;
-  }
-
-  UnmodifiableListView<GridFieldPB> get unmodifiableFields =>
-      UnmodifiableListView(_fieldNotifier?.fields ?? []);
-
-  List<GridFieldPB> get fields => [..._fieldNotifier?.fields ?? []];
-
-  set fields(List<GridFieldPB> fields) {
-    _fieldNotifier?.fields = [...fields];
-  }
-
-  void addListener({
-    FieldsCallback? onFields,
-    FieldChangesetCallback? onChangeset,
-    bool Function()? listenWhen,
-  }) {
-    if (onChangeset != null) {
-      fn(c) {
-        if (listenWhen != null && listenWhen() == false) {
-          return;
-        }
-        onChangeset(c);
-      }
-
-      _changesetCallbackMap[onChangeset] = fn;
-    }
-
-    if (onFields != null) {
-      fn() {
-        if (listenWhen != null && listenWhen() == false) {
-          return;
-        }
-        onFields(fields);
-      }
-
-      _fieldsCallbackMap[onFields] = fn;
-      _fieldNotifier?.addListener(fn);
-    }
-  }
-
-  void removeListener({
-    FieldsCallback? onFieldsListener,
-    FieldChangesetCallback? onChangesetListener,
-  }) {
-    if (onFieldsListener != null) {
-      final fn = _fieldsCallbackMap.remove(onFieldsListener);
-      if (fn != null) {
-        _fieldNotifier?.removeListener(fn);
-      }
-    }
-
-    if (onChangesetListener != null) {
-      _changesetCallbackMap.remove(onChangesetListener);
-    }
-  }
 
-  void _deleteFields(List<GridFieldIdPB> deletedFields) {
-    if (deletedFields.isEmpty) {
-      return;
-    }
-    final List<GridFieldPB> newFields = fields;
-    final Map<String, GridFieldIdPB> deletedFieldMap = {
-      for (var fieldOrder in deletedFields) fieldOrder.fieldId: fieldOrder
-    };
-
-    newFields.retainWhere((field) => (deletedFieldMap[field.id] == null));
-    _fieldNotifier?.fields = newFields;
-  }
-
-  void _insertFields(List<IndexFieldPB> insertedFields) {
-    if (insertedFields.isEmpty) {
-      return;
-    }
-    final List<GridFieldPB> newFields = fields;
-    for (final indexField in insertedFields) {
-      if (newFields.length > indexField.index) {
-        newFields.insert(indexField.index, indexField.field_1);
-      } else {
-        newFields.add(indexField.field_1);
-      }
-    }
-    _fieldNotifier?.fields = newFields;
-  }
-
-  void _updateFields(List<GridFieldPB> updatedFields) {
-    if (updatedFields.isEmpty) {
-      return;
-    }
-    final List<GridFieldPB> newFields = fields;
-    for (final updatedField in updatedFields) {
-      final index =
-          newFields.indexWhere((field) => field.id == updatedField.id);
-      if (index != -1) {
-        newFields.removeAt(index);
-        newFields.insert(index, updatedField);
-      }
-    }
-    _fieldNotifier?.fields = newFields;
-  }
-}
-
-class GridRowCacheFieldNotifierImpl extends GridRowCacheFieldNotifier {
-  final GridFieldCache _cache;
-  FieldChangesetCallback? _onChangesetFn;
-  FieldsCallback? _onFieldFn;
-  GridRowCacheFieldNotifierImpl(GridFieldCache cache) : _cache = cache;
-
-  @override
-  UnmodifiableListView<GridFieldPB> get fields => _cache.unmodifiableFields;
-
-  @override
-  void onFieldsChanged(VoidCallback callback) {
-    _onFieldFn = (_) => callback();
-    _cache.addListener(onFields: _onFieldFn);
-  }
-
-  @override
-  void onFieldChanged(void Function(GridFieldPB) callback) {
-    _onChangesetFn = (GridFieldChangesetPB changeset) {
-      for (final updatedField in changeset.updatedFields) {
-        callback(updatedField);
-      }
-    };
-
-    _cache.addListener(onChangeset: _onChangesetFn);
-  }
-
-  @override
-  void dispose() {
-    if (_onFieldFn != null) {
-      _cache.removeListener(onFieldsListener: _onFieldFn!);
-      _onFieldFn = null;
-    }
-
-    if (_onChangesetFn != null) {
-      _cache.removeListener(onChangesetListener: _onChangesetFn!);
-      _onChangesetFn = null;
-    }
+  Future<Either<RepeatedGridGroupPB, FlowyError>> loadGroups() {
+    final payload = GridIdPB(value: gridId);
+    return GridEventGetGroup(payload).send();
   }
 }

+ 2 - 2
frontend/app_flowy/lib/plugins/grid/application/prelude.dart

@@ -4,13 +4,13 @@ export 'row/row_service.dart';
 export 'grid_service.dart';
 export 'grid_header_bloc.dart';
 
-// GridFieldPB
+// FieldPB
 export 'field/field_service.dart';
 export 'field/field_action_sheet_bloc.dart';
 export 'field/field_editor_bloc.dart';
 export 'field/field_type_option_edit_bloc.dart';
 
-// GridFieldPB Type Option
+// FieldPB 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';

+ 13 - 12
frontend/app_flowy/lib/plugins/grid/application/row/row_action_sheet_bloc.dart

@@ -6,28 +6,30 @@ import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
 import 'package:dartz/dartz.dart';
 
+import 'row_cache.dart';
+
 part 'row_action_sheet_bloc.freezed.dart';
 
 class RowActionSheetBloc
     extends Bloc<RowActionSheetEvent, RowActionSheetState> {
-  final RowService _rowService;
+  final RowFFIService _rowService;
 
-  RowActionSheetBloc({required GridRowInfo rowData})
-      : _rowService = RowService(
-          gridId: rowData.gridId,
-          blockId: rowData.blockId,
-          rowId: rowData.id,
+  RowActionSheetBloc({required RowInfo rowInfo})
+      : _rowService = RowFFIService(
+          gridId: rowInfo.gridId,
+          blockId: rowInfo.rowPB.blockId,
         ),
-        super(RowActionSheetState.initial(rowData)) {
+        super(RowActionSheetState.initial(rowInfo)) {
     on<RowActionSheetEvent>(
       (event, emit) async {
         await event.map(
           deleteRow: (_DeleteRow value) async {
-            final result = await _rowService.deleteRow();
+            final result = await _rowService.deleteRow(state.rowData.rowPB.id);
             logResult(result);
           },
           duplicateRow: (_DuplicateRow value) async {
-            final result = await _rowService.duplicateRow();
+            final result =
+                await _rowService.duplicateRow(state.rowData.rowPB.id);
             logResult(result);
           },
         );
@@ -54,11 +56,10 @@ class RowActionSheetEvent with _$RowActionSheetEvent {
 @freezed
 class RowActionSheetState with _$RowActionSheetState {
   const factory RowActionSheetState({
-    required GridRowInfo rowData,
+    required RowInfo rowData,
   }) = _RowActionSheetState;
 
-  factory RowActionSheetState.initial(GridRowInfo rowData) =>
-      RowActionSheetState(
+  factory RowActionSheetState.initial(RowInfo rowData) => RowActionSheetState(
         rowData: rowData,
       );
 }

+ 32 - 35
frontend/app_flowy/lib/plugins/grid/application/row/row_bloc.dart

@@ -5,25 +5,25 @@ 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 'row_cache.dart';
+import 'row_data_controller.dart';
 import 'row_service.dart';
 
 part 'row_bloc.freezed.dart';
 
 class RowBloc extends Bloc<RowEvent, RowState> {
-  final RowService _rowService;
-  final GridRowCache _rowCache;
-  void Function()? _rowListenFn;
+  final RowFFIService _rowService;
+  final GridRowDataController _dataController;
 
   RowBloc({
-    required GridRowInfo rowInfo,
-    required GridRowCache rowCache,
-  })  : _rowService = RowService(
+    required RowInfo rowInfo,
+    required GridRowDataController dataController,
+  })  : _rowService = RowFFIService(
           gridId: rowInfo.gridId,
-          blockId: rowInfo.blockId,
-          rowId: rowInfo.id,
+          blockId: rowInfo.rowPB.blockId,
         ),
-        _rowCache = rowCache,
-        super(RowState.initial(rowInfo, rowCache.loadGridCells(rowInfo.id))) {
+        _dataController = dataController,
+        super(RowState.initial(rowInfo, dataController.loadData())) {
     on<RowEvent>(
       (event, emit) async {
         await event.map(
@@ -31,16 +31,15 @@ class RowBloc extends Bloc<RowEvent, RowState> {
             await _startListening();
           },
           createRow: (_CreateRow value) {
-            _rowService.createRow();
+            _rowService.createRow(rowInfo.rowPB.id);
           },
-          didReceiveCellDatas: (_DidReceiveCellDatas value) async {
-            final fields = value.gridCellMap.values
+          didReceiveCells: (_DidReceiveCells value) async {
+            final cells = value.gridCellMap.values
                 .map((e) => GridCellEquatable(e.field))
                 .toList();
-            final snapshots = UnmodifiableListView(fields);
             emit(state.copyWith(
               gridCellMap: value.gridCellMap,
-              snapshots: snapshots,
+              cells: UnmodifiableListView(cells),
               changeReason: value.reason,
             ));
           },
@@ -51,19 +50,17 @@ class RowBloc extends Bloc<RowEvent, RowState> {
 
   @override
   Future<void> close() async {
-    if (_rowListenFn != null) {
-      _rowCache.removeRowListener(_rowListenFn!);
-    }
-
+    _dataController.dispose();
     return super.close();
   }
 
   Future<void> _startListening() async {
-    _rowListenFn = _rowCache.addListener(
-      rowId: state.rowInfo.id,
-      onCellUpdated: (cellDatas, reason) =>
-          add(RowEvent.didReceiveCellDatas(cellDatas, reason)),
-      listenWhen: () => !isClosed,
+    _dataController.addListener(
+      onRowChanged: (cells, reason) {
+        if (!isClosed) {
+          add(RowEvent.didReceiveCells(cells, reason));
+        }
+      },
     );
   }
 }
@@ -72,33 +69,33 @@ class RowBloc extends Bloc<RowEvent, RowState> {
 class RowEvent with _$RowEvent {
   const factory RowEvent.initial() = _InitialRow;
   const factory RowEvent.createRow() = _CreateRow;
-  const factory RowEvent.didReceiveCellDatas(
-          GridCellMap gridCellMap, GridRowChangeReason reason) =
-      _DidReceiveCellDatas;
+  const factory RowEvent.didReceiveCells(
+      GridCellMap gridCellMap, RowsChangedReason reason) = _DidReceiveCells;
 }
 
 @freezed
 class RowState with _$RowState {
   const factory RowState({
-    required GridRowInfo rowInfo,
+    required RowInfo rowInfo,
     required GridCellMap gridCellMap,
-    required UnmodifiableListView<GridCellEquatable> snapshots,
-    GridRowChangeReason? changeReason,
+    required UnmodifiableListView<GridCellEquatable> cells,
+    RowsChangedReason? changeReason,
   }) = _RowState;
 
-  factory RowState.initial(GridRowInfo rowInfo, GridCellMap cellDataMap) =>
+  factory RowState.initial(RowInfo rowInfo, GridCellMap cellDataMap) =>
       RowState(
         rowInfo: rowInfo,
         gridCellMap: cellDataMap,
-        snapshots: UnmodifiableListView(
-            cellDataMap.values.map((e) => GridCellEquatable(e.field)).toList()),
+        cells: UnmodifiableListView(
+          cellDataMap.values.map((e) => GridCellEquatable(e.field)).toList(),
+        ),
       );
 }
 
 class GridCellEquatable extends Equatable {
-  final GridFieldPB _field;
+  final FieldPB _field;
 
-  const GridCellEquatable(GridFieldPB field) : _field = field;
+  const GridCellEquatable(FieldPB field) : _field = field;
 
   @override
   List<Object?> get props => [

+ 328 - 0
frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart

@@ -0,0 +1,328 @@
+import 'dart:collection';
+import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
+import 'package:flowy_sdk/dispatch/dispatch.dart';
+import 'package:flowy_sdk/log.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/row_entities.pb.dart';
+import 'package:flutter/foundation.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+part 'row_cache.freezed.dart';
+
+typedef RowUpdateCallback = void Function();
+
+abstract class IGridRowFieldNotifier {
+  UnmodifiableListView<FieldPB> get fields;
+  void onRowFieldsChanged(VoidCallback callback);
+  void onRowFieldChanged(void Function(FieldPB) callback);
+  void onRowDispose();
+}
+
+/// Cache the rows in memory
+/// Insert / delete / update row
+///
+/// Read https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid for more information.
+
+class GridRowCache {
+  final String gridId;
+  final BlockPB block;
+
+  /// _rows containers the current block's rows
+  /// Use List to reverse the order of the GridRow.
+  List<RowInfo> _rowInfos = [];
+
+  /// Use Map for faster access the raw row data.
+  final HashMap<String, RowPB> _rowByRowId;
+
+  final GridCellCache _cellCache;
+  final IGridRowFieldNotifier _fieldNotifier;
+  final _RowChangesetNotifier _rowChangeReasonNotifier;
+
+  UnmodifiableListView<RowInfo> get rows => UnmodifiableListView(_rowInfos);
+  GridCellCache get cellCache => _cellCache;
+
+  GridRowCache({
+    required this.gridId,
+    required this.block,
+    required IGridRowFieldNotifier notifier,
+  })  : _cellCache = GridCellCache(gridId: gridId),
+        _rowByRowId = HashMap(),
+        _rowChangeReasonNotifier = _RowChangesetNotifier(),
+        _fieldNotifier = notifier {
+    //
+    notifier.onRowFieldsChanged(() => _rowChangeReasonNotifier
+        .receive(const RowsChangedReason.fieldDidChange()));
+    notifier.onRowFieldChanged((field) => _cellCache.remove(field.id));
+    _rowInfos = block.rows.map((rowPB) => buildGridRow(rowPB)).toList();
+  }
+
+  Future<void> dispose() async {
+    _fieldNotifier.onRowDispose();
+    _rowChangeReasonNotifier.dispose();
+    await _cellCache.dispose();
+  }
+
+  void applyChangesets(List<GridBlockChangesetPB> changesets) {
+    for (final changeset in changesets) {
+      _deleteRows(changeset.deletedRows);
+      _insertRows(changeset.insertedRows);
+      _updateRows(changeset.updatedRows);
+      _hideRows(changeset.hideRows);
+      _showRows(changeset.visibleRows);
+    }
+  }
+
+  void _deleteRows(List<String> deletedRows) {
+    if (deletedRows.isEmpty) {
+      return;
+    }
+
+    final List<RowInfo> newRows = [];
+    final DeletedIndexs deletedIndex = [];
+    final Map<String, String> deletedRowByRowId = {
+      for (var rowId in deletedRows) rowId: rowId
+    };
+
+    _rowInfos.asMap().forEach((index, RowInfo rowInfo) {
+      if (deletedRowByRowId[rowInfo.rowPB.id] == null) {
+        newRows.add(rowInfo);
+      } else {
+        _rowByRowId.remove(rowInfo.rowPB.id);
+        deletedIndex.add(DeletedIndex(index: index, row: rowInfo));
+      }
+    });
+    _rowInfos = newRows;
+    _rowChangeReasonNotifier.receive(RowsChangedReason.delete(deletedIndex));
+  }
+
+  void _insertRows(List<InsertedRowPB> insertRows) {
+    if (insertRows.isEmpty) {
+      return;
+    }
+
+    InsertedIndexs insertIndexs = [];
+    for (final InsertedRowPB insertRow in insertRows) {
+      final insertIndex = InsertedIndex(
+        index: insertRow.index,
+        rowId: insertRow.row.id,
+      );
+      insertIndexs.add(insertIndex);
+      _rowInfos.insert(
+        insertRow.index,
+        (buildGridRow(insertRow.row)),
+      );
+    }
+
+    _rowChangeReasonNotifier.receive(RowsChangedReason.insert(insertIndexs));
+  }
+
+  void _updateRows(List<RowPB> updatedRows) {
+    if (updatedRows.isEmpty) {
+      return;
+    }
+
+    final UpdatedIndexs updatedIndexs = UpdatedIndexs();
+    for (final RowPB updatedRow in updatedRows) {
+      final rowId = updatedRow.id;
+      final index = _rowInfos.indexWhere(
+        (rowInfo) => rowInfo.rowPB.id == rowId,
+      );
+      if (index != -1) {
+        _rowByRowId[rowId] = updatedRow;
+
+        _rowInfos.removeAt(index);
+        _rowInfos.insert(index, buildGridRow(updatedRow));
+        updatedIndexs[rowId] = UpdatedIndex(index: index, rowId: rowId);
+      }
+    }
+
+    _rowChangeReasonNotifier.receive(RowsChangedReason.update(updatedIndexs));
+  }
+
+  void _hideRows(List<String> hideRows) {}
+
+  void _showRows(List<String> visibleRows) {}
+
+  void onRowsChanged(
+    void Function(RowsChangedReason) onRowChanged,
+  ) {
+    _rowChangeReasonNotifier.addListener(() {
+      onRowChanged(_rowChangeReasonNotifier.reason);
+    });
+  }
+
+  RowUpdateCallback addListener({
+    required String rowId,
+    void Function(GridCellMap, RowsChangedReason)? onCellUpdated,
+    bool Function()? listenWhen,
+  }) {
+    listenerHandler() async {
+      if (listenWhen != null && listenWhen() == false) {
+        return;
+      }
+
+      notifyUpdate() {
+        if (onCellUpdated != null) {
+          final row = _rowByRowId[rowId];
+          if (row != null) {
+            final GridCellMap cellDataMap = _makeGridCells(rowId, row);
+            onCellUpdated(cellDataMap, _rowChangeReasonNotifier.reason);
+          }
+        }
+      }
+
+      _rowChangeReasonNotifier.reason.whenOrNull(
+        update: (indexs) {
+          if (indexs[rowId] != null) notifyUpdate();
+        },
+        fieldDidChange: () => notifyUpdate(),
+      );
+    }
+
+    _rowChangeReasonNotifier.addListener(listenerHandler);
+    return listenerHandler;
+  }
+
+  void removeRowListener(VoidCallback callback) {
+    _rowChangeReasonNotifier.removeListener(callback);
+  }
+
+  GridCellMap loadGridCells(String rowId) {
+    final RowPB? data = _rowByRowId[rowId];
+    if (data == null) {
+      _loadRow(rowId);
+    }
+    return _makeGridCells(rowId, data);
+  }
+
+  Future<void> _loadRow(String rowId) async {
+    final payload = RowIdPB.create()
+      ..gridId = gridId
+      ..blockId = block.id
+      ..rowId = rowId;
+
+    final result = await GridEventGetRow(payload).send();
+    result.fold(
+      (optionRow) => _refreshRow(optionRow),
+      (err) => Log.error(err),
+    );
+  }
+
+  GridCellMap _makeGridCells(String rowId, RowPB? row) {
+    var cellDataMap = GridCellMap.new();
+    for (final field in _fieldNotifier.fields) {
+      if (field.visibility) {
+        cellDataMap[field.id] = GridCellIdentifier(
+          rowId: rowId,
+          gridId: gridId,
+          field: field,
+        );
+      }
+    }
+    return cellDataMap;
+  }
+
+  void _refreshRow(OptionalRowPB optionRow) {
+    if (!optionRow.hasRow()) {
+      return;
+    }
+    final updatedRow = optionRow.row;
+    updatedRow.freeze();
+
+    _rowByRowId[updatedRow.id] = updatedRow;
+    final index =
+        _rowInfos.indexWhere((rowInfo) => rowInfo.rowPB.id == updatedRow.id);
+    if (index != -1) {
+      // update the corresponding row in _rows if they are not the same
+      if (_rowInfos[index].rowPB != updatedRow) {
+        final rowInfo = _rowInfos.removeAt(index).copyWith(rowPB: updatedRow);
+        _rowInfos.insert(index, rowInfo);
+
+        // Calculate the update index
+        final UpdatedIndexs updatedIndexs = UpdatedIndexs();
+        updatedIndexs[rowInfo.rowPB.id] = UpdatedIndex(
+          index: index,
+          rowId: rowInfo.rowPB.id,
+        );
+
+        //
+        _rowChangeReasonNotifier
+            .receive(RowsChangedReason.update(updatedIndexs));
+      }
+    }
+  }
+
+  RowInfo buildGridRow(RowPB rowPB) {
+    return RowInfo(
+      gridId: gridId,
+      fields: _fieldNotifier.fields,
+      rowPB: rowPB,
+    );
+  }
+}
+
+class _RowChangesetNotifier extends ChangeNotifier {
+  RowsChangedReason reason = const InitialListState();
+
+  _RowChangesetNotifier();
+
+  void receive(RowsChangedReason newReason) {
+    reason = newReason;
+    reason.map(
+      insert: (_) => notifyListeners(),
+      delete: (_) => notifyListeners(),
+      update: (_) => notifyListeners(),
+      fieldDidChange: (_) => notifyListeners(),
+      initial: (_) {},
+    );
+  }
+}
+
+@freezed
+class RowInfo with _$RowInfo {
+  const factory RowInfo({
+    required String gridId,
+    required UnmodifiableListView<FieldPB> fields,
+    required RowPB rowPB,
+  }) = _RowInfo;
+}
+
+typedef InsertedIndexs = List<InsertedIndex>;
+typedef DeletedIndexs = List<DeletedIndex>;
+typedef UpdatedIndexs = LinkedHashMap<String, UpdatedIndex>;
+
+@freezed
+class RowsChangedReason with _$RowsChangedReason {
+  const factory RowsChangedReason.insert(InsertedIndexs items) = _Insert;
+  const factory RowsChangedReason.delete(DeletedIndexs items) = _Delete;
+  const factory RowsChangedReason.update(UpdatedIndexs indexs) = _Update;
+  const factory RowsChangedReason.fieldDidChange() = _FieldDidChange;
+  const factory RowsChangedReason.initial() = InitialListState;
+}
+
+class InsertedIndex {
+  final int index;
+  final String rowId;
+  InsertedIndex({
+    required this.index,
+    required this.rowId,
+  });
+}
+
+class DeletedIndex {
+  final int index;
+  final RowInfo row;
+  DeletedIndex({
+    required this.index,
+    required this.row,
+  });
+}
+
+class UpdatedIndex {
+  final int index;
+  final String rowId;
+  UpdatedIndex({
+    required this.index,
+    required this.rowId,
+  });
+}

+ 49 - 0
frontend/app_flowy/lib/plugins/grid/application/row/row_data_controller.dart

@@ -0,0 +1,49 @@
+import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_field_notifier.dart';
+import 'package:flutter/material.dart';
+import '../../presentation/widgets/cell/cell_builder.dart';
+import '../cell/cell_service/cell_service.dart';
+import '../field/field_cache.dart';
+import 'row_cache.dart';
+
+typedef OnRowChanged = void Function(GridCellMap, RowsChangedReason);
+
+class GridRowDataController extends GridCellBuilderDelegate {
+  final RowInfo rowInfo;
+  final List<VoidCallback> _onRowChangedListeners = [];
+  final GridFieldCache _fieldCache;
+  final GridRowCache _rowCache;
+
+  GridRowDataController({
+    required this.rowInfo,
+    required GridFieldCache fieldCache,
+    required GridRowCache rowCache,
+  })  : _fieldCache = fieldCache,
+        _rowCache = rowCache;
+
+  GridCellMap loadData() {
+    return _rowCache.loadGridCells(rowInfo.rowPB.id);
+  }
+
+  void addListener({OnRowChanged? onRowChanged}) {
+    _onRowChangedListeners.add(_rowCache.addListener(
+      rowId: rowInfo.rowPB.id,
+      onCellUpdated: onRowChanged,
+    ));
+  }
+
+  void dispose() {
+    for (final fn in _onRowChangedListeners) {
+      _rowCache.removeRowListener(fn);
+    }
+  }
+
+  // GridCellBuilderDelegate implementation
+  @override
+  GridCellFieldNotifier buildFieldNotifier() {
+    return GridCellFieldNotifier(
+        notifier: GridCellFieldNotifierImpl(_fieldCache));
+  }
+
+  @override
+  GridCellCache get cellCache => _rowCache.cellCache;
+}

+ 15 - 25
frontend/app_flowy/lib/plugins/grid/application/row/row_detail_bloc.dart

@@ -2,26 +2,24 @@ import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_servic
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
-import 'row_service.dart';
-
+import 'row_data_controller.dart';
 part 'row_detail_bloc.freezed.dart';
 
 class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
-  final GridRowInfo rowInfo;
-  final GridRowCache _rowCache;
-  void Function()? _rowListenFn;
+  final GridRowDataController dataController;
 
   RowDetailBloc({
-    required this.rowInfo,
-    required GridRowCache rowCache,
-  })  : _rowCache = rowCache,
-        super(RowDetailState.initial()) {
+    required this.dataController,
+  }) : super(RowDetailState.initial()) {
     on<RowDetailEvent>(
       (event, emit) async {
         await event.map(
           initial: (_Initial value) async {
             await _startListening();
-            _loadCellData();
+            final cells = dataController.loadData();
+            if (!isClosed) {
+              add(RowDetailEvent.didReceiveCellDatas(cells.values.toList()));
+            }
           },
           didReceiveCellDatas: (_DidReceiveCellDatas value) {
             emit(state.copyWith(gridCells: value.gridCells));
@@ -33,27 +31,19 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
 
   @override
   Future<void> close() async {
-    if (_rowListenFn != null) {
-      _rowCache.removeRowListener(_rowListenFn!);
-    }
+    dataController.dispose();
     return super.close();
   }
 
   Future<void> _startListening() async {
-    _rowListenFn = _rowCache.addListener(
-      rowId: rowInfo.id,
-      onCellUpdated: (cellDatas, reason) =>
-          add(RowDetailEvent.didReceiveCellDatas(cellDatas.values.toList())),
-      listenWhen: () => !isClosed,
+    dataController.addListener(
+      onRowChanged: (cells, reason) {
+        if (!isClosed) {
+          add(RowDetailEvent.didReceiveCellDatas(cells.values.toList()));
+        }
+      },
     );
   }
-
-  Future<void> _loadCellData() async {
-    final cellDataMap = _rowCache.loadGridCells(rowInfo.id);
-    if (!isClosed) {
-      add(RowDetailEvent.didReceiveCellDatas(cellDataMap.values.toList()));
-    }
-  }
 }
 
 @freezed

+ 6 - 4
frontend/app_flowy/lib/plugins/grid/application/row/row_listener.dart

@@ -8,12 +8,13 @@ import 'dart:typed_data';
 import 'package:dartz/dartz.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
 
-typedef UpdateRowNotifiedValue = Either<GridRowPB, FlowyError>;
-typedef UpdateFieldNotifiedValue = Either<List<GridFieldPB>, FlowyError>;
+typedef UpdateRowNotifiedValue = Either<RowPB, FlowyError>;
+typedef UpdateFieldNotifiedValue = Either<List<FieldPB>, FlowyError>;
 
 class RowListener {
   final String rowId;
-  PublishNotifier<UpdateRowNotifiedValue>? updateRowNotifier = PublishNotifier();
+  PublishNotifier<UpdateRowNotifiedValue>? updateRowNotifier =
+      PublishNotifier();
   GridNotificationListener? _listener;
 
   RowListener({required this.rowId});
@@ -26,7 +27,8 @@ class RowListener {
     switch (ty) {
       case GridNotification.DidUpdateRow:
         result.fold(
-          (payload) => updateRowNotifier?.value = left(GridRowPB.fromBuffer(payload)),
+          (payload) =>
+              updateRowNotifier?.value = left(RowPB.fromBuffer(payload)),
           (error) => updateRowNotifier?.value = right(error),
         );
         break;

+ 28 - 345
frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart

@@ -1,314 +1,29 @@
-import 'dart:collection';
-import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
 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-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 'package:freezed_annotation/freezed_annotation.dart';
-part 'row_service.freezed.dart';
 
-typedef RowUpdateCallback = void Function();
-
-abstract class GridRowCacheFieldNotifier {
-  UnmodifiableListView<GridFieldPB> get fields;
-  void onFieldsChanged(VoidCallback callback);
-  void onFieldChanged(void Function(GridFieldPB) callback);
-  void dispose();
-}
-
-/// Cache the rows in memory
-/// Insert / delete / update row
-///
-/// Read https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid for more information.
-
-class GridRowCache {
-  final String gridId;
-  final GridBlockPB block;
-
-  /// _rows containers the current block's rows
-  /// Use List to reverse the order of the GridRow.
-  List<GridRowInfo> _rowInfos = [];
-
-  /// Use Map for faster access the raw row data.
-  final HashMap<String, GridRowPB> _rowByRowId;
-
-  final GridCellCache _cellCache;
-  final GridRowCacheFieldNotifier _fieldNotifier;
-  final _GridRowChangesetNotifier _rowChangeReasonNotifier;
-
-  UnmodifiableListView<GridRowInfo> get rows => UnmodifiableListView(_rowInfos);
-  GridCellCache get cellCache => _cellCache;
-
-  GridRowCache({
-    required this.gridId,
-    required this.block,
-    required GridRowCacheFieldNotifier notifier,
-  })  : _cellCache = GridCellCache(gridId: gridId),
-        _rowByRowId = HashMap(),
-        _rowChangeReasonNotifier = _GridRowChangesetNotifier(),
-        _fieldNotifier = notifier {
-    //
-    notifier.onFieldsChanged(() => _rowChangeReasonNotifier
-        .receive(const GridRowChangeReason.fieldDidChange()));
-    notifier.onFieldChanged((field) => _cellCache.remove(field.id));
-    _rowInfos = block.rows
-        .map((rowInfo) => buildGridRow(rowInfo.id, rowInfo.height.toDouble()))
-        .toList();
-  }
-
-  Future<void> dispose() async {
-    _fieldNotifier.dispose();
-    _rowChangeReasonNotifier.dispose();
-    await _cellCache.dispose();
-  }
-
-  void applyChangesets(List<GridBlockChangesetPB> changesets) {
-    for (final changeset in changesets) {
-      _deleteRows(changeset.deletedRows);
-      _insertRows(changeset.insertedRows);
-      _updateRows(changeset.updatedRows);
-      _hideRows(changeset.hideRows);
-      _showRows(changeset.visibleRows);
-    }
-  }
-
-  void _deleteRows(List<String> deletedRows) {
-    if (deletedRows.isEmpty) {
-      return;
-    }
-
-    final List<GridRowInfo> newRows = [];
-    final DeletedIndexs deletedIndex = [];
-    final Map<String, String> deletedRowByRowId = {
-      for (var rowId in deletedRows) rowId: rowId
-    };
-
-    _rowInfos.asMap().forEach((index, row) {
-      if (deletedRowByRowId[row.id] == null) {
-        newRows.add(row);
-      } else {
-        _rowByRowId.remove(row.id);
-        deletedIndex.add(DeletedIndex(index: index, row: row));
-      }
-    });
-    _rowInfos = newRows;
-    _rowChangeReasonNotifier.receive(GridRowChangeReason.delete(deletedIndex));
-  }
-
-  void _insertRows(List<InsertedRowPB> insertRows) {
-    if (insertRows.isEmpty) {
-      return;
-    }
-
-    InsertedIndexs insertIndexs = [];
-    for (final insertRow in insertRows) {
-      final insertIndex = InsertedIndex(
-        index: insertRow.index,
-        rowId: insertRow.rowId,
-      );
-      insertIndexs.add(insertIndex);
-      _rowInfos.insert(insertRow.index,
-          (buildGridRow(insertRow.rowId, insertRow.height.toDouble())));
-    }
-
-    _rowChangeReasonNotifier.receive(GridRowChangeReason.insert(insertIndexs));
-  }
-
-  void _updateRows(List<UpdatedRowPB> updatedRows) {
-    if (updatedRows.isEmpty) {
-      return;
-    }
-
-    final UpdatedIndexs updatedIndexs = UpdatedIndexs();
-    for (final updatedRow in updatedRows) {
-      final rowId = updatedRow.rowId;
-      final index = _rowInfos.indexWhere((row) => row.id == rowId);
-      if (index != -1) {
-        _rowByRowId[rowId] = updatedRow.row;
-
-        _rowInfos.removeAt(index);
-        _rowInfos.insert(
-            index, buildGridRow(rowId, updatedRow.row.height.toDouble()));
-        updatedIndexs[rowId] = UpdatedIndex(index: index, rowId: rowId);
-      }
-    }
-
-    _rowChangeReasonNotifier.receive(GridRowChangeReason.update(updatedIndexs));
-  }
-
-  void _hideRows(List<String> hideRows) {}
-
-  void _showRows(List<String> visibleRows) {}
-
-  void onRowsChanged(
-    void Function(GridRowChangeReason) onRowChanged,
-  ) {
-    _rowChangeReasonNotifier.addListener(() {
-      onRowChanged(_rowChangeReasonNotifier.reason);
-    });
-  }
-
-  RowUpdateCallback addListener({
-    required String rowId,
-    void Function(GridCellMap, GridRowChangeReason)? onCellUpdated,
-    bool Function()? listenWhen,
-  }) {
-    listenrHandler() async {
-      if (listenWhen != null && listenWhen() == false) {
-        return;
-      }
-
-      notifyUpdate() {
-        if (onCellUpdated != null) {
-          final row = _rowByRowId[rowId];
-          if (row != null) {
-            final GridCellMap cellDataMap = _makeGridCells(rowId, row);
-            onCellUpdated(cellDataMap, _rowChangeReasonNotifier.reason);
-          }
-        }
-      }
-
-      _rowChangeReasonNotifier.reason.whenOrNull(
-        update: (indexs) {
-          if (indexs[rowId] != null) notifyUpdate();
-        },
-        fieldDidChange: () => notifyUpdate(),
-      );
-    }
-
-    _rowChangeReasonNotifier.addListener(listenrHandler);
-    return listenrHandler;
-  }
-
-  void removeRowListener(VoidCallback callback) {
-    _rowChangeReasonNotifier.removeListener(callback);
-  }
-
-  GridCellMap loadGridCells(String rowId) {
-    final GridRowPB? data = _rowByRowId[rowId];
-    if (data == null) {
-      _loadRow(rowId);
-    }
-    return _makeGridCells(rowId, data);
-  }
-
-  Future<void> _loadRow(String rowId) async {
-    final payload = GridRowIdPB.create()
-      ..gridId = gridId
-      ..blockId = block.id
-      ..rowId = rowId;
-
-    final result = await GridEventGetRow(payload).send();
-    result.fold(
-      (optionRow) => _refreshRow(optionRow),
-      (err) => Log.error(err),
-    );
-  }
-
-  GridCellMap _makeGridCells(String rowId, GridRowPB? row) {
-    var cellDataMap = GridCellMap.new();
-    for (final field in _fieldNotifier.fields) {
-      if (field.visibility) {
-        cellDataMap[field.id] = GridCellIdentifier(
-          rowId: rowId,
-          gridId: gridId,
-          field: field,
-        );
-      }
-    }
-    return cellDataMap;
-  }
-
-  void _refreshRow(OptionalRowPB optionRow) {
-    if (!optionRow.hasRow()) {
-      return;
-    }
-    final updatedRow = optionRow.row;
-    updatedRow.freeze();
-
-    _rowByRowId[updatedRow.id] = updatedRow;
-    final index =
-        _rowInfos.indexWhere((gridRow) => gridRow.id == updatedRow.id);
-    if (index != -1) {
-      // update the corresponding row in _rows if they are not the same
-      if (_rowInfos[index].rawRow != updatedRow) {
-        final row = _rowInfos.removeAt(index).copyWith(rawRow: updatedRow);
-        _rowInfos.insert(index, row);
-
-        // Calculate the update index
-        final UpdatedIndexs updatedIndexs = UpdatedIndexs();
-        updatedIndexs[row.id] = UpdatedIndex(index: index, rowId: row.id);
-
-        //
-        _rowChangeReasonNotifier
-            .receive(GridRowChangeReason.update(updatedIndexs));
-      }
-    }
-  }
-
-  GridRowInfo buildGridRow(String rowId, double rowHeight) {
-    return GridRowInfo(
-      gridId: gridId,
-      blockId: block.id,
-      fields: _fieldNotifier.fields,
-      id: rowId,
-      height: rowHeight,
-    );
-  }
-}
-
-class _GridRowChangesetNotifier extends ChangeNotifier {
-  GridRowChangeReason reason = const InitialListState();
-
-  _GridRowChangesetNotifier();
-
-  void receive(GridRowChangeReason newReason) {
-    reason = newReason;
-    reason.map(
-      insert: (_) => notifyListeners(),
-      delete: (_) => notifyListeners(),
-      update: (_) => notifyListeners(),
-      fieldDidChange: (_) => notifyListeners(),
-      initial: (_) {},
-    );
-  }
-}
-
-class RowService {
+class RowFFIService {
   final String gridId;
   final String blockId;
-  final String rowId;
 
-  RowService(
-      {required this.gridId, required this.blockId, required this.rowId});
+  RowFFIService({
+    required this.gridId,
+    required this.blockId,
+  });
 
-  Future<Either<GridRowPB, FlowyError>> createRow() {
-    CreateRowPayloadPB payload = CreateRowPayloadPB.create()
+  Future<Either<RowPB, FlowyError>> createRow(String rowId) {
+    final payload = CreateTableRowPayloadPB.create()
       ..gridId = gridId
       ..startRowId = rowId;
 
-    return GridEventCreateRow(payload).send();
-  }
-
-  Future<Either<Unit, FlowyError>> moveRow(
-      String rowId, int fromIndex, int toIndex) {
-    final payload = MoveItemPayloadPB.create()
-      ..gridId = gridId
-      ..itemId = rowId
-      ..ty = MoveItemTypePB.MoveRow
-      ..fromIndex = fromIndex
-      ..toIndex = toIndex;
-
-    return GridEventMoveItem(payload).send();
+    return GridEventCreateTableRow(payload).send();
   }
 
-  Future<Either<OptionalRowPB, FlowyError>> getRow() {
-    final payload = GridRowIdPB.create()
+  Future<Either<OptionalRowPB, FlowyError>> getRow(String rowId) {
+    final payload = RowIdPB.create()
       ..gridId = gridId
       ..blockId = blockId
       ..rowId = rowId;
@@ -316,8 +31,8 @@ class RowService {
     return GridEventGetRow(payload).send();
   }
 
-  Future<Either<Unit, FlowyError>> deleteRow() {
-    final payload = GridRowIdPB.create()
+  Future<Either<Unit, FlowyError>> deleteRow(String rowId) {
+    final payload = RowIdPB.create()
       ..gridId = gridId
       ..blockId = blockId
       ..rowId = rowId;
@@ -325,8 +40,8 @@ class RowService {
     return GridEventDeleteRow(payload).send();
   }
 
-  Future<Either<Unit, FlowyError>> duplicateRow() {
-    final payload = GridRowIdPB.create()
+  Future<Either<Unit, FlowyError>> duplicateRow(String rowId) {
+    final payload = RowIdPB.create()
       ..gridId = gridId
       ..blockId = blockId
       ..rowId = rowId;
@@ -335,54 +50,22 @@ class RowService {
   }
 }
 
-@freezed
-class GridRowInfo with _$GridRowInfo {
-  const factory GridRowInfo({
-    required String gridId,
-    required String blockId,
-    required String id,
-    required UnmodifiableListView<GridFieldPB> fields,
-    required double height,
-    GridRowPB? rawRow,
-  }) = _GridRowInfo;
-}
-
-typedef InsertedIndexs = List<InsertedIndex>;
-typedef DeletedIndexs = List<DeletedIndex>;
-typedef UpdatedIndexs = LinkedHashMap<String, UpdatedIndex>;
-
-@freezed
-class GridRowChangeReason with _$GridRowChangeReason {
-  const factory GridRowChangeReason.insert(InsertedIndexs items) = _Insert;
-  const factory GridRowChangeReason.delete(DeletedIndexs items) = _Delete;
-  const factory GridRowChangeReason.update(UpdatedIndexs indexs) = _Update;
-  const factory GridRowChangeReason.fieldDidChange() = _FieldDidChange;
-  const factory GridRowChangeReason.initial() = InitialListState;
-}
+class MoveRowFFIService {
+  final String gridId;
 
-class InsertedIndex {
-  final int index;
-  final String rowId;
-  InsertedIndex({
-    required this.index,
-    required this.rowId,
+  MoveRowFFIService({
+    required this.gridId,
   });
-}
 
-class DeletedIndex {
-  final int index;
-  final GridRowInfo row;
-  DeletedIndex({
-    required this.index,
-    required this.row,
-  });
-}
+  Future<Either<Unit, FlowyError>> moveRow({
+    required String fromRowId,
+    required String toRowId,
+  }) {
+    var payload = MoveRowPayloadPB.create()
+      ..viewId = gridId
+      ..fromRowId = fromRowId
+      ..toRowId = toRowId;
 
-class UpdatedIndex {
-  final int index;
-  final String rowId;
-  UpdatedIndex({
-    required this.index,
-    required this.rowId,
-  });
+    return GridEventMoveRow(payload).send();
+  }
 }

+ 7 - 6
frontend/app_flowy/lib/plugins/grid/application/setting/property_bloc.dart

@@ -1,16 +1,17 @@
 import 'package:app_flowy/plugins/grid/application/field/field_service.dart';
-import 'package:app_flowy/plugins/grid/application/grid_service.dart';
 import 'package:flowy_sdk/log.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';
 
+import '../field/field_cache.dart';
+
 part 'property_bloc.freezed.dart';
 
 class GridPropertyBloc extends Bloc<GridPropertyEvent, GridPropertyState> {
   final GridFieldCache _fieldCache;
-  Function(List<GridFieldPB>)? _onFieldsFn;
+  Function(List<FieldPB>)? _onFieldsFn;
 
   GridPropertyBloc({required String gridId, required GridFieldCache fieldCache})
       : _fieldCache = fieldCache,
@@ -66,8 +67,8 @@ class GridPropertyEvent with _$GridPropertyEvent {
   const factory GridPropertyEvent.initial() = _Initial;
   const factory GridPropertyEvent.setFieldVisibility(
       String fieldId, bool visibility) = _SetFieldVisibility;
-  const factory GridPropertyEvent.didReceiveFieldUpdate(
-      List<GridFieldPB> fields) = _DidReceiveFieldUpdate;
+  const factory GridPropertyEvent.didReceiveFieldUpdate(List<FieldPB> fields) =
+      _DidReceiveFieldUpdate;
   const factory GridPropertyEvent.moveField(int fromIndex, int toIndex) =
       _MoveField;
 }
@@ -76,10 +77,10 @@ class GridPropertyEvent with _$GridPropertyEvent {
 class GridPropertyState with _$GridPropertyState {
   const factory GridPropertyState({
     required String gridId,
-    required List<GridFieldPB> fields,
+    required List<FieldPB> fields,
   }) = _GridPropertyState;
 
-  factory GridPropertyState.initial(String gridId, List<GridFieldPB> fields) =>
+  factory GridPropertyState.initial(String gridId, List<FieldPB> fields) =>
       GridPropertyState(
         gridId: gridId,
         fields: fields,

+ 5 - 2
frontend/app_flowy/lib/plugins/grid/grid.dart

@@ -22,10 +22,13 @@ class GridPluginBuilder implements PluginBuilder {
   String get menuName => LocaleKeys.grid_menuName.tr();
 
   @override
-  PluginType get pluginType => DefaultPlugin.grid.type();
+  PluginType get pluginType => PluginType.grid;
 
   @override
-  ViewDataType get dataType => ViewDataType.Grid;
+  ViewDataTypePB get dataType => ViewDataTypePB.Database;
+
+  @override
+  ViewLayoutTypePB? get subDataType => ViewLayoutTypePB.Grid;
 }
 
 class GridPluginConfig implements PluginConfig {

+ 6 - 5
frontend/app_flowy/lib/plugins/grid/presentation/controller/grid_scroll.dart

@@ -2,19 +2,20 @@ import 'package:flutter/material.dart';
 import 'package:linked_scroll_controller/linked_scroll_controller.dart';
 
 class GridScrollController {
-  final LinkedScrollControllerGroup _scrollGroupContorller;
+  final LinkedScrollControllerGroup _scrollGroupController;
   final ScrollController verticalController;
   final ScrollController horizontalController;
 
   final List<ScrollController> _linkHorizontalControllers = [];
 
-  GridScrollController({required LinkedScrollControllerGroup scrollGroupContorller})
-      : _scrollGroupContorller = scrollGroupContorller,
+  GridScrollController(
+      {required LinkedScrollControllerGroup scrollGroupController})
+      : _scrollGroupController = scrollGroupController,
         verticalController = ScrollController(),
-        horizontalController = scrollGroupContorller.addAndGet();
+        horizontalController = scrollGroupController.addAndGet();
 
   ScrollController linkHorizontalController() {
-    final controller = _scrollGroupContorller.addAndGet();
+    final controller = _scrollGroupController.addAndGet();
     _linkHorizontalControllers.add(controller);
     return controller;
   }

+ 62 - 22
frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart

@@ -1,6 +1,7 @@
+import 'package:app_flowy/plugins/grid/application/field/field_cache.dart';
+import 'package:app_flowy/plugins/grid/application/row/row_data_controller.dart';
 import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/plugins/grid/application/grid_bloc.dart';
-import 'package:app_flowy/plugins/grid/application/row/row_service.dart';
 import 'package:flowy_infra/theme.dart';
 import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
 import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart';
@@ -11,12 +12,15 @@ import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:flutter/material.dart';
 import 'package:linked_scroll_controller/linked_scroll_controller.dart';
+import '../application/row/row_cache.dart';
 import 'controller/grid_scroll.dart';
 import 'layout/layout.dart';
 import 'layout/sizes.dart';
+import 'widgets/cell/cell_builder.dart';
 import 'widgets/row/grid_row.dart';
 import 'widgets/footer/grid_footer.dart';
 import 'widgets/header/grid_header.dart';
+import 'widgets/row/row_detail.dart';
 import 'widgets/shortcuts.dart';
 import 'widgets/toolbar/grid_toolbar.dart';
 
@@ -79,7 +83,7 @@ class FlowyGrid extends StatefulWidget {
 
 class _FlowyGridState extends State<FlowyGrid> {
   final _scrollController = GridScrollController(
-      scrollGroupContorller: LinkedScrollControllerGroup());
+      scrollGroupController: LinkedScrollControllerGroup());
   late ScrollController headerScrollController;
 
   @override
@@ -153,7 +157,7 @@ class _FlowyGridState extends State<FlowyGrid> {
   }
 
   Widget _gridHeader(BuildContext context, String gridId) {
-    final fieldCache = context.read<GridBloc>().fieldCache;
+    final fieldCache = context.read<GridBloc>().dataController.fieldCache;
     return GridHeaderSliverAdaptor(
       gridId: gridId,
       fieldCache: fieldCache,
@@ -169,7 +173,7 @@ class _GridToolbarAdaptor extends StatelessWidget {
   Widget build(BuildContext context) {
     return BlocSelector<GridBloc, GridState, GridToolbarContext>(
       selector: (state) {
-        final fieldCache = context.read<GridBloc>().fieldCache;
+        final fieldCache = context.read<GridBloc>().dataController.fieldCache;
         return GridToolbarContext(
           gridId: state.gridId,
           fieldCache: fieldCache,
@@ -221,7 +225,7 @@ class _GridRowsState extends State<_GridRows> {
           initialItemCount: context.read<GridBloc>().state.rowInfos.length,
           itemBuilder:
               (BuildContext context, int index, Animation<double> animation) {
-            final GridRowInfo rowInfo =
+            final RowInfo rowInfo =
                 context.read<GridBloc>().state.rowInfos[index];
             return _renderRow(context, rowInfo, animation);
           },
@@ -232,25 +236,61 @@ class _GridRowsState extends State<_GridRows> {
 
   Widget _renderRow(
     BuildContext context,
-    GridRowInfo rowInfo,
+    RowInfo rowInfo,
     Animation<double> animation,
   ) {
-    final rowCache =
-        context.read<GridBloc>().getRowCache(rowInfo.blockId, rowInfo.id);
-    final fieldCache = context.read<GridBloc>().fieldCache;
-    if (rowCache != null) {
-      return SizeTransition(
-        sizeFactor: animation,
-        child: GridRowWidget(
-          rowData: rowInfo,
-          rowCache: rowCache,
-          fieldCache: fieldCache,
-          key: ValueKey(rowInfo.id),
-        ),
-      );
-    } else {
-      return const SizedBox();
-    }
+    final rowCache = context.read<GridBloc>().getRowCache(
+          rowInfo.rowPB.blockId,
+          rowInfo.rowPB.id,
+        );
+
+    /// Return placeholder widget if the rowCache is null.
+    if (rowCache == null) return const SizedBox();
+
+    final fieldCache = context.read<GridBloc>().dataController.fieldCache;
+    final dataController = GridRowDataController(
+      rowInfo: rowInfo,
+      fieldCache: fieldCache,
+      rowCache: rowCache,
+    );
+
+    return SizeTransition(
+      sizeFactor: animation,
+      child: GridRowWidget(
+        rowInfo: rowInfo,
+        dataController: dataController,
+        cellBuilder: GridCellBuilder(delegate: dataController),
+        openDetailPage: (context, cellBuilder) {
+          _openRowDetailPage(
+            context,
+            rowInfo,
+            fieldCache,
+            rowCache,
+            cellBuilder,
+          );
+        },
+        key: ValueKey(rowInfo.rowPB.id),
+      ),
+    );
+  }
+
+  void _openRowDetailPage(
+    BuildContext context,
+    RowInfo rowInfo,
+    GridFieldCache fieldCache,
+    GridRowCache rowCache,
+    GridCellBuilder cellBuilder,
+  ) {
+    final dataController = GridRowDataController(
+      rowInfo: rowInfo,
+      fieldCache: fieldCache,
+      rowCache: rowCache,
+    );
+
+    RowDetailPage(
+      cellBuilder: cellBuilder,
+      dataController: dataController,
+    ).show(context);
   }
 }
 

+ 7 - 3
frontend/app_flowy/lib/plugins/grid/presentation/layout/layout.dart

@@ -2,11 +2,15 @@ import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
 import 'sizes.dart';
 
 class GridLayout {
-  static double headerWidth(List<GridFieldPB> fields) {
+  static double headerWidth(List<FieldPB> fields) {
     if (fields.isEmpty) return 0;
 
-    final fieldsWidth = fields.map((field) => field.width.toDouble()).reduce((value, element) => value + element);
+    final fieldsWidth = fields
+        .map((field) => field.width.toDouble())
+        .reduce((value, element) => value + element);
 
-    return fieldsWidth + GridSize.leadingHeaderPadding + GridSize.trailHeaderPadding;
+    return fieldsWidth +
+        GridSize.leadingHeaderPadding +
+        GridSize.trailHeaderPadding;
   }
 }

+ 2 - 12
frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_accessory.dart

@@ -8,6 +8,8 @@ import 'package:styled_widget/styled_widget.dart';
 import 'package:app_flowy/generated/locale_keys.g.dart';
 import 'package:easy_localization/easy_localization.dart';
 
+import 'cell_builder.dart';
+
 class GridCellAccessoryBuildContext {
   final BuildContext anchorContext;
   final bool isCellEditing;
@@ -57,18 +59,6 @@ class PrimaryCellAccessory extends StatelessWidget with GridCellAccessory {
   bool enable() => !isCellEditing;
 }
 
-typedef AccessoryBuilder = List<GridCellAccessory> Function(
-    GridCellAccessoryBuildContext buildContext);
-
-abstract class CellAccessory extends Widget {
-  const CellAccessory({Key? key}) : super(key: key);
-
-  // The hover will show if the isHover's value is true
-  ValueNotifier<bool>? get onAccessoryHover;
-
-  AccessoryBuilder? get accessoryBuilder;
-}
-
 class AccessoryHover extends StatefulWidget {
   final CellAccessory child;
   final EdgeInsets contentPadding;

+ 55 - 31
frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_builder.dart

@@ -1,5 +1,4 @@
 import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
-import 'package:app_flowy/plugins/grid/application/grid_service.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter/widgets.dart';
@@ -13,53 +12,66 @@ import 'select_option_cell/select_option_cell.dart';
 import 'text_cell.dart';
 import 'url_cell/url_cell.dart';
 
+abstract class GridCellBuilderDelegate
+    extends GridCellControllerBuilderDelegate {
+  GridCellCache get cellCache;
+}
+
 class GridCellBuilder {
-  final GridCellCache cellCache;
-  final GridFieldCache fieldCache;
+  final GridCellBuilderDelegate delegate;
   GridCellBuilder({
-    required this.cellCache,
-    required this.fieldCache,
+    required this.delegate,
   });
 
-  GridCellWidget build(GridCellIdentifier cell, {GridCellStyle? style}) {
+  GridCellWidget build(GridCellIdentifier cellId, {GridCellStyle? style}) {
     final cellControllerBuilder = GridCellControllerBuilder(
-      cellId: cell,
-      cellCache: cellCache,
-      fieldCache: fieldCache,
+      cellId: cellId,
+      cellCache: delegate.cellCache,
+      delegate: delegate,
     );
-    final key = cell.key();
-    switch (cell.fieldType) {
+
+    final key = cellId.key();
+    switch (cellId.fieldType) {
       case FieldType.Checkbox:
         return GridCheckboxCell(
-            cellControllerBuilder: cellControllerBuilder, key: key);
+          cellControllerBuilder: cellControllerBuilder,
+          key: key,
+        );
       case FieldType.DateTime:
         return GridDateCell(
-            cellControllerBuilder: cellControllerBuilder,
-            key: key,
-            style: style);
+          cellControllerBuilder: cellControllerBuilder,
+          key: key,
+          style: style,
+        );
       case FieldType.SingleSelect:
         return GridSingleSelectCell(
-            cellContorllerBuilder: cellControllerBuilder,
-            style: style,
-            key: key);
+          cellControllerBuilder: cellControllerBuilder,
+          style: style,
+          key: key,
+        );
       case FieldType.MultiSelect:
         return GridMultiSelectCell(
-            cellContorllerBuilder: cellControllerBuilder,
-            style: style,
-            key: key);
+          cellControllerBuilder: cellControllerBuilder,
+          style: style,
+          key: key,
+        );
       case FieldType.Number:
         return GridNumberCell(
-            cellContorllerBuilder: cellControllerBuilder, key: key);
+          cellControllerBuilder: cellControllerBuilder,
+          key: key,
+        );
       case FieldType.RichText:
         return GridTextCell(
-            cellContorllerBuilder: cellControllerBuilder,
-            style: style,
-            key: key);
+          cellControllerBuilder: cellControllerBuilder,
+          style: style,
+          key: key,
+        );
       case FieldType.URL:
         return GridURLCell(
-            cellContorllerBuilder: cellControllerBuilder,
-            style: style,
-            key: key);
+          cellControllerBuilder: cellControllerBuilder,
+          style: style,
+          key: key,
+        );
     }
     throw UnimplementedError;
   }
@@ -82,6 +94,18 @@ abstract class CellEditable {
   ValueNotifier<bool> get onCellEditing;
 }
 
+typedef AccessoryBuilder = List<GridCellAccessory> Function(
+    GridCellAccessoryBuildContext buildContext);
+
+abstract class CellAccessory extends Widget {
+  const CellAccessory({Key? key}) : super(key: key);
+
+  // The hover will show if the isHover's value is true
+  ValueNotifier<bool>? get onAccessoryHover;
+
+  AccessoryBuilder? get accessoryBuilder;
+}
+
 abstract class GridCellWidget extends StatefulWidget
     implements CellAccessory, CellEditable, CellShortcuts {
   GridCellWidget({Key? key}) : super(key: key) {
@@ -93,7 +117,7 @@ abstract class GridCellWidget extends StatefulWidget
   @override
   final ValueNotifier<bool> onCellFocus = ValueNotifier<bool>(false);
 
-  // When the cell is focused, we assume that the accessory alse be hovered.
+  // When the cell is focused, we assume that the accessory also be hovered.
   @override
   ValueNotifier<bool> get onAccessoryHover => onCellFocus;
 
@@ -150,7 +174,7 @@ abstract class GridCellState<T extends GridCellWidget> extends State<T> {
 
 abstract class GridFocusNodeCellState<T extends GridCellWidget>
     extends GridCellState<T> {
-  SingleListenrFocusNode focusNode = SingleListenrFocusNode();
+  SingleListenerFocusNode focusNode = SingleListenerFocusNode();
 
   @override
   void initState() {
@@ -219,7 +243,7 @@ class GridCellFocusListener extends ChangeNotifier {
 
 abstract class GridCellStyle {}
 
-class SingleListenrFocusNode extends FocusNode {
+class SingleListenerFocusNode extends FocusNode {
   VoidCallback? _listener;
 
   void setListener(VoidCallback listener) {

+ 20 - 16
frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_cotainer.dart → frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_container.dart

@@ -25,24 +25,28 @@ class CellContainer extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     return ChangeNotifierProxyProvider<RegionStateNotifier,
-        CellContainerNotifier>(
-      create: (_) => CellContainerNotifier(child),
+        _CellContainerNotifier>(
+      create: (_) => _CellContainerNotifier(child),
       update: (_, rowStateNotifier, cellStateNotifier) =>
           cellStateNotifier!..onEnter = rowStateNotifier.onEnter,
-      child: Selector<CellContainerNotifier, bool>(
+      child: Selector<_CellContainerNotifier, bool>(
         selector: (context, notifier) => notifier.isFocus,
         builder: (context, isFocus, _) {
           Widget container = Center(child: GridCellShortcuts(child: child));
 
           if (accessoryBuilder != null) {
-            final accessories = accessoryBuilder!(GridCellAccessoryBuildContext(
-              anchorContext: context,
-              isCellEditing: isFocus,
-            ));
+            final accessories = accessoryBuilder!(
+              GridCellAccessoryBuildContext(
+                anchorContext: context,
+                isCellEditing: isFocus,
+              ),
+            );
 
             if (accessories.isNotEmpty) {
-              container =
-                  CellEnterRegion(child: container, accessories: accessories);
+              container = _GridCellEnterRegion(
+                child: container,
+                accessories: accessories,
+              );
             }
           }
 
@@ -74,16 +78,16 @@ class CellContainer extends StatelessWidget {
   }
 }
 
-class CellEnterRegion extends StatelessWidget {
+class _GridCellEnterRegion extends StatelessWidget {
   final Widget child;
   final List<GridCellAccessory> accessories;
-  const CellEnterRegion(
+  const _GridCellEnterRegion(
       {required this.child, required this.accessories, Key? key})
       : super(key: key);
 
   @override
   Widget build(BuildContext context) {
-    return Selector<CellContainerNotifier, bool>(
+    return Selector<_CellContainerNotifier, bool>(
       selector: (context, notifier) => notifier.onEnter,
       builder: (context, onEnter, _) {
         List<Widget> children = [child];
@@ -95,10 +99,10 @@ class CellEnterRegion extends StatelessWidget {
         return MouseRegion(
           cursor: SystemMouseCursors.click,
           onEnter: (p) =>
-              Provider.of<CellContainerNotifier>(context, listen: false)
+              Provider.of<_CellContainerNotifier>(context, listen: false)
                   .onEnter = true,
           onExit: (p) =>
-              Provider.of<CellContainerNotifier>(context, listen: false)
+              Provider.of<_CellContainerNotifier>(context, listen: false)
                   .onEnter = false,
           child: Stack(
             alignment: AlignmentDirectional.center,
@@ -111,13 +115,13 @@ class CellEnterRegion extends StatelessWidget {
   }
 }
 
-class CellContainerNotifier extends ChangeNotifier {
+class _CellContainerNotifier extends ChangeNotifier {
   final CellEditable cellEditable;
   VoidCallback? _onCellFocusListener;
   bool _isFocus = false;
   bool _onEnter = false;
 
-  CellContainerNotifier(this.cellEditable) {
+  _CellContainerNotifier(this.cellEditable) {
     _onCellFocusListener = () => isFocus = cellEditable.onCellFocus.value;
     cellEditable.onCellFocus.addListener(_onCellFocusListener!);
   }

+ 10 - 8
frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_shortcuts.dart

@@ -24,14 +24,16 @@ class GridCellShortcuts extends StatelessWidget {
     return Shortcuts(
       shortcuts: {
         LogicalKeySet(LogicalKeyboardKey.enter): const GridCellEnterIdent(),
-        LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyC): const GridCellCopyIntent(),
-        LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyV): const GridCellInsertIntent(),
+        LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyC):
+            const GridCellCopyIntent(),
+        LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyV):
+            const GridCellPasteIntent(),
       },
       child: Actions(
         actions: {
           GridCellEnterIdent: GridCellEnterAction(child: child),
           GridCellCopyIntent: GridCellCopyAction(child: child),
-          GridCellInsertIntent: GridCellInsertAction(child: child),
+          GridCellPasteIntent: GridCellPasteAction(child: child),
         },
         child: child,
       ),
@@ -78,16 +80,16 @@ class GridCellCopyAction extends Action<GridCellCopyIntent> {
   }
 }
 
-class GridCellInsertIntent extends Intent {
-  const GridCellInsertIntent();
+class GridCellPasteIntent extends Intent {
+  const GridCellPasteIntent();
 }
 
-class GridCellInsertAction extends Action<GridCellInsertIntent> {
+class GridCellPasteAction extends Action<GridCellPasteIntent> {
   final CellShortcuts child;
-  GridCellInsertAction({required this.child});
+  GridCellPasteAction({required this.child});
 
   @override
-  void invoke(covariant GridCellInsertIntent intent) {
+  void invoke(covariant GridCellPasteIntent intent) {
     final callback = child.shortcutHandlers[CellKeyboardKey.onInsert];
     if (callback != null) {
       callback();

+ 3 - 2
frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/checkbox_cell.dart

@@ -22,8 +22,9 @@ class _CheckboxCellState extends GridCellState<GridCheckboxCell> {
 
   @override
   void initState() {
-    final cellContext = widget.cellControllerBuilder.build();
-    _cellBloc = getIt<CheckboxCellBloc>(param1: cellContext)
+    final cellController =
+        widget.cellControllerBuilder.build() as GridCheckboxCellController;
+    _cellBloc = getIt<CheckboxCellBloc>(param1: cellController)
       ..add(const CheckboxCellEvent.initial());
     super.initState();
   }

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

@@ -43,8 +43,8 @@ class _DateCellState extends GridCellState<GridDateCell> {
 
   @override
   void initState() {
-    final cellContext = widget.cellControllerBuilder.build();
-    _cellBloc = getIt<DateCellBloc>(param1: cellContext)
+    final cellController = widget.cellControllerBuilder.build();
+    _cellBloc = getIt<DateCellBloc>(param1: cellController)
       ..add(const DateCellEvent.initial());
     super.initState();
   }
@@ -84,7 +84,7 @@ class _DateCellState extends GridCellState<GridDateCell> {
         DateCellEditor(onDismissed: () => widget.onCellEditing.value = false);
     calendar.show(
       context,
-      cellController: bloc.cellContext.clone(),
+      cellController: bloc.cellController.clone(),
     );
   }
 

+ 23 - 21
frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_editor.dart

@@ -1,5 +1,6 @@
 import 'package:app_flowy/generated/locale_keys.g.dart';
 import 'package:app_flowy/plugins/grid/application/cell/date_cal_bloc.dart';
+import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra/theme.dart';
@@ -14,7 +15,6 @@ import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:table_calendar/table_calendar.dart';
 import 'package:app_flowy/plugins/grid/application/prelude.dart';
-
 import '../../../layout/sizes.dart';
 import '../../header/type_option/date.dart';
 
@@ -38,11 +38,12 @@ class DateCellEditor with FlowyOverlayDelegate {
 
     final result =
         await cellController.getFieldTypeOption(DateTypeOptionDataParser());
+
     result.fold(
-      (dateTypeOption) {
+      (dateTypeOptionPB) {
         final calendar = _CellCalendarWidget(
           cellContext: cellController,
-          dateTypeOption: dateTypeOption,
+          dateTypeOptionPB: dateTypeOptionPB,
         );
 
         FlowyOverlay.of(context).insertWithAnchor(
@@ -78,11 +79,11 @@ class DateCellEditor with FlowyOverlayDelegate {
 
 class _CellCalendarWidget extends StatelessWidget {
   final GridDateCellController cellContext;
-  final DateTypeOption dateTypeOption;
+  final DateTypeOptionPB dateTypeOptionPB;
 
   const _CellCalendarWidget({
     required this.cellContext,
-    required this.dateTypeOption,
+    required this.dateTypeOptionPB,
     Key? key,
   }) : super(key: key);
 
@@ -92,9 +93,9 @@ class _CellCalendarWidget extends StatelessWidget {
     return BlocProvider(
       create: (context) {
         return DateCalBloc(
-          dateTypeOption: dateTypeOption,
+          dateTypeOptionPB: dateTypeOptionPB,
           cellData: cellContext.getCellData(),
-          cellContext: cellContext,
+          cellController: cellContext,
         )..add(const DateCalEvent.initial());
       },
       child: BlocBuilder<DateCalBloc, DateCalState>(
@@ -196,7 +197,7 @@ class _IncludeTimeButton extends StatelessWidget {
   Widget build(BuildContext context) {
     final theme = context.watch<AppTheme>();
     return BlocSelector<DateCalBloc, DateCalState, bool>(
-      selector: (state) => state.dateTypeOption.includeTime,
+      selector: (state) => state.dateTypeOptionPB.includeTime,
       builder: (context, includeTime) {
         return SizedBox(
           height: 50,
@@ -243,7 +244,7 @@ class _TimeTextFieldState extends State<_TimeTextField> {
   void initState() {
     _focusNode = FocusNode();
     _controller = TextEditingController(text: widget.bloc.state.time);
-    if (widget.bloc.state.dateTypeOption.includeTime) {
+    if (widget.bloc.state.dateTypeOptionPB.includeTime) {
       _focusNode.addListener(() {
         if (mounted) {
           _CalDateTimeSetting.hide(context);
@@ -264,7 +265,7 @@ class _TimeTextFieldState extends State<_TimeTextField> {
       },
       listenWhen: (p, c) => p.time != c.time,
       builder: (context, state) {
-        if (state.dateTypeOption.includeTime) {
+        if (state.dateTypeOptionPB.includeTime) {
           return Padding(
             padding: kMargin,
             child: RoundedInputField(
@@ -306,23 +307,24 @@ class _DateTypeOptionButton extends StatelessWidget {
     final title = LocaleKeys.grid_field_dateFormat.tr() +
         " &" +
         LocaleKeys.grid_field_timeFormat.tr();
-    return BlocSelector<DateCalBloc, DateCalState, DateTypeOption>(
-      selector: (state) => state.dateTypeOption,
-      builder: (context, dateTypeOption) {
+    return BlocSelector<DateCalBloc, DateCalState, DateTypeOptionPB>(
+      selector: (state) => state.dateTypeOptionPB,
+      builder: (context, dateTypeOptionPB) {
         return FlowyButton(
           text: FlowyText.medium(title, fontSize: 12),
           hoverColor: theme.hover,
           margin: kMargin,
-          onTap: () => _showTimeSetting(dateTypeOption, context),
+          onTap: () => _showTimeSetting(dateTypeOptionPB, context),
           rightIcon: svgWidget("grid/more", color: theme.iconColor),
         );
       },
     );
   }
 
-  void _showTimeSetting(DateTypeOption dateTypeOption, BuildContext context) {
+  void _showTimeSetting(
+      DateTypeOptionPB dateTypeOptionPB, BuildContext context) {
     final setting = _CalDateTimeSetting(
-      dateTypeOption: dateTypeOption,
+      dateTypeOptionPB: dateTypeOptionPB,
       onEvent: (event) => context.read<DateCalBloc>().add(event),
     );
     setting.show(context);
@@ -330,10 +332,10 @@ class _DateTypeOptionButton extends StatelessWidget {
 }
 
 class _CalDateTimeSetting extends StatefulWidget {
-  final DateTypeOption dateTypeOption;
+  final DateTypeOptionPB dateTypeOptionPB;
   final Function(DateCalEvent) onEvent;
   const _CalDateTimeSetting(
-      {required this.dateTypeOption, required this.onEvent, Key? key})
+      {required this.dateTypeOptionPB, required this.onEvent, Key? key})
       : super(key: key);
 
   @override
@@ -370,17 +372,17 @@ class _CalDateTimeSettingState extends State<_CalDateTimeSetting> {
     List<Widget> children = [
       DateFormatButton(onTap: () {
         final list = DateFormatList(
-          selectedFormat: widget.dateTypeOption.dateFormat,
+          selectedFormat: widget.dateTypeOptionPB.dateFormat,
           onSelected: (format) =>
               widget.onEvent(DateCalEvent.setDateFormat(format)),
         );
         _showOverlay(context, list);
       }),
       TimeFormatButton(
-        timeFormat: widget.dateTypeOption.timeFormat,
+        timeFormat: widget.dateTypeOptionPB.timeFormat,
         onTap: () {
           final list = TimeFormatList(
-            selectedFormat: widget.dateTypeOption.timeFormat,
+            selectedFormat: widget.dateTypeOptionPB.timeFormat,
             onSelected: (format) =>
                 widget.onEvent(DateCalEvent.setTimeFormat(format)),
           );

+ 9 - 7
frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/number_cell.dart

@@ -7,10 +7,10 @@ import 'package:flutter_bloc/flutter_bloc.dart';
 import 'cell_builder.dart';
 
 class GridNumberCell extends GridCellWidget {
-  final GridCellControllerBuilder cellContorllerBuilder;
+  final GridCellControllerBuilder cellControllerBuilder;
 
   GridNumberCell({
-    required this.cellContorllerBuilder,
+    required this.cellControllerBuilder,
     Key? key,
   }) : super(key: key);
 
@@ -25,8 +25,8 @@ class _NumberCellState extends GridFocusNodeCellState<GridNumberCell> {
 
   @override
   void initState() {
-    final cellContext = widget.cellContorllerBuilder.build();
-    _cellBloc = getIt<NumberCellBloc>(param1: cellContext)
+    final cellController = widget.cellControllerBuilder.build();
+    _cellBloc = getIt<NumberCellBloc>(param1: cellController)
       ..add(const NumberCellEvent.initial());
     _controller =
         TextEditingController(text: contentFromState(_cellBloc.state));
@@ -49,8 +49,10 @@ class _NumberCellState extends GridFocusNodeCellState<GridNumberCell> {
           controller: _controller,
           focusNode: focusNode,
           onEditingComplete: () => focusNode.unfocus(),
-          maxLines: null,
+          onSubmitted: (_) => focusNode.unfocus(),
+          maxLines: 1,
           style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
+          textInputAction: TextInputAction.done,
           decoration: const InputDecoration(
             contentPadding: EdgeInsets.zero,
             border: InputBorder.none,
@@ -63,7 +65,7 @@ class _NumberCellState extends GridFocusNodeCellState<GridNumberCell> {
 
   @override
   Future<void> dispose() async {
-    _delayOperation?.cancel();
+    _delayOperation = null;
     _cellBloc.close();
     super.dispose();
   }
@@ -72,7 +74,7 @@ class _NumberCellState extends GridFocusNodeCellState<GridNumberCell> {
   Future<void> focusChanged() async {
     if (mounted) {
       _delayOperation?.cancel();
-      _delayOperation = Timer(const Duration(milliseconds: 300), () {
+      _delayOperation = Timer(const Duration(milliseconds: 30), () {
         if (_cellBloc.isClosed == false &&
             _controller.text != contentFromState(_cellBloc.state)) {
           _cellBloc.add(NumberCellEvent.updateCell(_controller.text));

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

@@ -73,7 +73,7 @@ class SelectOptionTag extends StatelessWidget {
     Key? key,
   }) : super(key: key);
 
-  factory SelectOptionTag.fromSelectOption({
+  factory SelectOptionTag.fromOption({
     required BuildContext context,
     required SelectOptionPB option,
     VoidCallback? onSelected,
@@ -91,7 +91,8 @@ class SelectOptionTag extends StatelessWidget {
   Widget build(BuildContext context) {
     return ChoiceChip(
       pressElevation: 1,
-      label: FlowyText.medium(name, fontSize: 12, overflow: TextOverflow.ellipsis),
+      label:
+          FlowyText.medium(name, fontSize: 12, overflow: TextOverflow.ellipsis),
       selectedColor: color,
       backgroundColor: color,
       labelPadding: const EdgeInsets.symmetric(horizontal: 6),
@@ -133,7 +134,7 @@ class SelectOptionTagCell extends StatelessWidget {
                   Flexible(
                     fit: FlexFit.loose,
                     flex: 2,
-                    child: SelectOptionTag.fromSelectOption(
+                    child: SelectOptionTag.fromOption(
                       context: context,
                       option: option,
                       onSelected: () => onSelected(option),

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác