Ver código fonte

Merge branch 'main' of github.com:AppFlowy-IO/AppFlowy

Sean Riley Hawkins 2 anos atrás
pai
commit
3b8a74fe89
100 arquivos alterados com 2835 adições e 450 exclusões
  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. 6 1
      README.md
  6. 1 1
      doc/CONTRIBUTING.md
  7. 1 1
      frontend/app_flowy/analysis_options.yaml
  8. 66 0
      frontend/app_flowy/assets/translations/fr-FR.json
  9. 1 1
      frontend/app_flowy/lib/plugins/blank/blank.dart
  10. 141 30
      frontend/app_flowy/lib/plugins/board/application/board_bloc.dart
  11. 43 14
      frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart
  12. 71 0
      frontend/app_flowy/lib/plugins/board/application/card/board_checkbox_cell_bloc.dart
  13. 85 0
      frontend/app_flowy/lib/plugins/board/application/card/board_date_cell_bloc.dart
  14. 67 0
      frontend/app_flowy/lib/plugins/board/application/card/board_number_cell_bloc.dart
  15. 78 0
      frontend/app_flowy/lib/plugins/board/application/card/board_url_cell_bloc.dart
  16. 116 0
      frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart
  17. 49 0
      frontend/app_flowy/lib/plugins/board/application/card/card_data_controller.dart
  18. 80 0
      frontend/app_flowy/lib/plugins/board/application/group_controller.dart
  19. 51 0
      frontend/app_flowy/lib/plugins/board/application/group_listener.dart
  20. 2 2
      frontend/app_flowy/lib/plugins/board/board.dart
  21. 79 10
      frontend/app_flowy/lib/plugins/board/presentation/board_page.dart
  22. 59 0
      frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart
  23. 59 0
      frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart
  24. 59 0
      frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart
  25. 16 4
      frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart
  26. 16 4
      frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart
  27. 66 0
      frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart
  28. 90 5
      frontend/app_flowy/lib/plugins/board/presentation/card/card.dart
  29. 69 0
      frontend/app_flowy/lib/plugins/board/presentation/card/card_cell_builder.dart
  30. 142 0
      frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart
  31. 2 2
      frontend/app_flowy/lib/plugins/doc/document.dart
  32. 2 2
      frontend/app_flowy/lib/plugins/grid/application/block/block_cache.dart
  33. 7 5
      frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart
  34. 1 1
      frontend/app_flowy/lib/plugins/grid/application/cell/checkbox_cell_bloc.dart
  35. 1 1
      frontend/app_flowy/lib/plugins/grid/application/cell/number_cell_bloc.dart
  36. 1 1
      frontend/app_flowy/lib/plugins/grid/application/cell/select_option_editor_bloc.dart
  37. 1 1
      frontend/app_flowy/lib/plugins/grid/application/cell/select_option_service.dart
  38. 3 4
      frontend/app_flowy/lib/plugins/grid/application/field/field_service.dart
  39. 2 2
      frontend/app_flowy/lib/plugins/grid/application/grid_bloc.dart
  40. 5 5
      frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart
  41. 11 4
      frontend/app_flowy/lib/plugins/grid/application/grid_service.dart
  42. 9 9
      frontend/app_flowy/lib/plugins/grid/application/row/row_action_sheet_bloc.dart
  43. 12 13
      frontend/app_flowy/lib/plugins/grid/application/row/row_bloc.dart
  44. 47 47
      frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart
  45. 4 8
      frontend/app_flowy/lib/plugins/grid/application/row/row_data_controller.dart
  46. 61 22
      frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart
  47. 2 2
      frontend/app_flowy/lib/plugins/grid/grid.dart
  48. 5 3
      frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart
  49. 2 12
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_accessory.dart
  50. 12 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_builder.dart
  51. 20 16
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_container.dart
  52. 10 8
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_shortcuts.dart
  53. 2 1
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/checkbox_cell.dart
  54. 1 1
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_editor.dart
  55. 5 3
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/number_cell.dart
  56. 4 3
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart
  57. 23 20
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart
  58. 4 2
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/text_field.dart
  59. 2 2
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/text_cell.dart
  60. 2 2
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/url_cell.dart
  61. 1 1
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/footer/grid_footer.dart
  62. 1 1
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/grid_header.dart
  63. 17 18
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart
  64. 6 3
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_action_sheet.dart
  65. 1 1
      frontend/app_flowy/lib/plugins/trash/menu.dart
  66. 1 1
      frontend/app_flowy/lib/plugins/trash/trash.dart
  67. 21 21
      frontend/app_flowy/lib/startup/plugin/plugin.dart
  68. 13 7
      frontend/app_flowy/lib/startup/plugin/src/sandbox.dart
  69. 2 2
      frontend/app_flowy/lib/workspace/application/app/app_bloc.dart
  70. 2 6
      frontend/app_flowy/lib/workspace/application/app/app_service.dart
  71. 1 1
      frontend/app_flowy/lib/workspace/application/menu/menu_bloc.dart
  72. 13 0
      frontend/app_flowy/lib/workspace/application/view/view_ext.dart
  73. 1 0
      frontend/app_flowy/lib/workspace/presentation/home/home_screen.dart
  74. 1 1
      frontend/app_flowy/lib/workspace/presentation/home/home_stack.dart
  75. 1 1
      frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/header.dart
  76. 4 0
      frontend/app_flowy/linux/flutter/generated_plugin_registrant.cc
  77. 1 0
      frontend/app_flowy/linux/flutter/generated_plugins.cmake
  78. 2 0
      frontend/app_flowy/macos/Flutter/GeneratedPluginRegistrant.swift
  79. 9 8
      frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart
  80. 1 1
      frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart
  81. 7 3
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart
  82. 2 2
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart
  83. 38 20
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart
  84. 17 10
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart
  85. 5 5
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart
  86. 3 3
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_interceptor.dart
  87. 8 8
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart
  88. 6 5
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart
  89. 2 1
      frontend/app_flowy/packages/appflowy_editor/.gitignore
  90. 0 0
      frontend/app_flowy/packages/appflowy_editor/.metadata
  91. 5 0
      frontend/app_flowy/packages/appflowy_editor/CHANGELOG.md
  92. 661 0
      frontend/app_flowy/packages/appflowy_editor/LICENSE
  93. 97 0
      frontend/app_flowy/packages/appflowy_editor/README.md
  94. 0 0
      frontend/app_flowy/packages/appflowy_editor/analysis_options.yaml
  95. 0 0
      frontend/app_flowy/packages/appflowy_editor/assets/images/check.svg
  96. 0 0
      frontend/app_flowy/packages/appflowy_editor/assets/images/point.svg
  97. 0 0
      frontend/app_flowy/packages/appflowy_editor/assets/images/quote.svg
  98. 0 0
      frontend/app_flowy/packages/appflowy_editor/assets/images/selection_menu/bulleted_list.svg
  99. 0 0
      frontend/app_flowy/packages/appflowy_editor/assets/images/selection_menu/checkbox.svg
  100. 0 0
      frontend/app_flowy/packages/appflowy_editor/assets/images/selection_menu/h1.svg

+ 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
@@ -63,11 +64,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

+ 6 - 1
README.md

@@ -56,7 +56,12 @@ Please see the [changelog](https://www.appflowy.io/whatsnew) for more details ab
 
 ## Contributing
 
-Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. Please look at [CONTRIBUTING.md](https://github.com/AppFlowy-IO/appflowy/blob/main/doc/CONTRIBUTING.md) for details.
+Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. Please look at [Contributing to AppFlowy](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/contributing-to-appflowy) for details.
+
+If your Pull Request is accepted as it fixes a bug, adds functionality, or makes AppFlowy's codebase significantly easier to use or understand, congratulations! If your administrative and managerial work behind the scenes that sustains the community as a whole, congratulations! You are now an official contributor to AppFlowy. Get in touch with us ([link](https://tally.so/r/mKP5z3)) to receive the very special Contributor T-shirt!
+Proudly wear your T-shirt and show it to us by tagging [@appflowy](https://twitter.com/appflowy) on Twitter.
+
+![DSCF3560](https://user-images.githubusercontent.com/12026239/186106423-a923f6fe-b169-477b-87e4-ffb2e375e0f6.jpg)
 
 ## Why Are We Building This?
 

+ 1 - 1
doc/CONTRIBUTING.md

@@ -2,6 +2,6 @@
 
 # Contributing to AppFlowy
 
-Hello, and welcome! Whether you are trying to report a bug, proposing a feature request, or want to work on the code you should go visit [our documentation](https://appflowy.gitbook.io)
+Hello, and welcome! Whether you are trying to report a bug, proposing a feature request, or want to work on the code you should go visit [Contributing to AppFlowy](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/contributing-to-appflowy)
 
 We look forward to hearing from you!

+ 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:

+ 66 - 0
frontend/app_flowy/assets/translations/fr-FR.json

@@ -141,5 +141,71 @@
       "lightLabel": "Mode clair",
       "darkLabel": "Mode sombre"
     }
+  },
+  "grid": {
+    "settings": {
+      "filter": "Filtrer",
+      "sortBy": "Trier par",
+      "Properties": "Propriétés"
+    },
+    "field": {
+      "hide": "Cacher",
+      "insertLeft": "Insérer à gauche",
+      "insertRight": "Insérer à droite",
+      "duplicate": "Dupliquer",
+      "delete": "Supprimer",
+      "textFieldName": "Texte",
+      "checkboxFieldName": "Case à cocher",
+      "dateFieldName": "Date",
+      "numberFieldName": "Nombre",
+      "singleSelectFieldName": "Sélectionner",
+      "multiSelectFieldName": "Multisélection",
+      "urlFieldName": "URL",
+      "numberFormat": " Format du nombre",
+      "dateFormat": " Format de la date",
+      "includeTime": " Inclure l'heure",
+      "dateFormatFriendly": "Mois Jour, Année",
+      "dateFormatISO": "Année-Mois-Jour",
+      "dateFormatLocal": "Année/Mois/Jour",
+      "dateFormatUS": "Année/Mois/Jour",
+      "timeFormat": " Format du temps",
+      "invalidTimeFormat": "Format invalide",
+      "timeFormatTwelveHour": "12 heures",
+      "timeFormatTwentyFourHour": "24 heures",
+      "addSelectOption": "Ajouter une option",
+      "optionTitle": "Options",
+      "addOption": "Ajouter une option",
+      "editProperty": "Modifier la propriété"
+    },
+    "row": {
+      "duplicate": "Dupliquer",
+      "delete": "Supprimer",
+      "textPlaceholder": "Vide",
+      "copyProperty": "Copie de la propriété dans le presse-papiers"
+    },
+    "selectOption": {
+      "create": "Créer",
+      "purpleColor": "Violet",
+      "pinkColor": "Rose",
+      "lightPinkColor": "Rose clair",
+      "orangeColor": "Orange",
+      "yellowColor": "Jaune",
+      "limeColor": "Citron vert",
+      "greenColor": "Vert",
+      "aquaColor": "Aqua",
+      "blueColor": "Bleu",
+      "deleteTag": "Supprimer l'étiquette",
+      "colorPannelTitle": "Couleurs",
+      "pannelTitle": "Sélectionnez une option ou créez-en une",
+      "searchOption": "Rechercher une option"
+    },
+    "menuName": "Grille"
+  },
+  "document": {
+    "menuName": "Doc",
+    "date": {
+      "timeHintTextInTwelveHour": "12:00 AM",
+      "timeHintTextInTwentyFourHour": "12:00"
+    }
   }
 }

+ 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 {

+ 141 - 30
frontend/app_flowy/lib/plugins/board/application/board_bloc.dart

@@ -1,6 +1,8 @@
 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';
@@ -13,32 +15,51 @@ 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 boardDataController;
+  late final AFBoardDataController afBoardDataController;
+  final MoveRowFFIService _rowService;
+  LinkedHashMap<String, GroupController> groupControllers = LinkedHashMap.new();
+
+  GridFieldCache get fieldCache => _dataController.fieldCache;
+  String get gridId => _dataController.gridId;
 
   BoardBloc({required ViewPB view})
-      : _dataController = BoardDataController(view: view),
+      : _rowService = MoveRowFFIService(gridId: view.id),
+        _dataController = BoardDataController(view: view),
         super(BoardState.initial(view.id)) {
-    boardDataController = AFBoardDataController(
+    afBoardDataController = AFBoardDataController(
       onMoveColumn: (
+        fromColumnId,
         fromIndex,
+        toColumnId,
         toIndex,
-      ) {},
+      ) {
+        _moveGroup(fromColumnId, toColumnId);
+      },
       onMoveColumnItem: (
         columnId,
         fromIndex,
         toIndex,
-      ) {},
+      ) {
+        final fromRow = groupControllers[columnId]?.rowAtIndex(fromIndex);
+        final toRow = groupControllers[columnId]?.rowAtIndex(toIndex);
+        _moveRow(fromRow, columnId, toRow);
+      },
       onMoveColumnItemToColumn: (
         fromColumnId,
         fromIndex,
         toColumnId,
         toIndex,
-      ) {},
+      ) {
+        final fromRow = groupControllers[fromColumnId]?.rowAtIndex(fromIndex);
+        final toRow = groupControllers[toColumnId]?.rowAtIndex(toIndex);
+        _moveRow(fromRow, toColumnId, toRow);
+      },
     );
 
     on<BoardEvent>(
@@ -48,27 +69,84 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
             _startListening();
             await _loadGrid(emit);
           },
-          createRow: () {
-            _dataController.createRow();
+          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)));
           },
-          didReceiveGroups: (List<GroupPB> groups) {
-            emit(state.copyWith(groups: groups));
+          didReceiveRows: (List<RowInfo> rowInfos) {
+            emit(state.copyWith(rowInfos: rowInfos));
+          },
+          didReceiveError: (FlowyError error) {
+            emit(state.copyWith(noneOrError: some(error)));
           },
         );
       },
     );
   }
 
+  void _moveRow(RowPB? fromRow, String columnId, RowPB? toRow) {
+    if (fromRow != null) {
+      _rowService
+          .moveGroupRow(
+        fromRowId: fromRow.id,
+        toGroupId: columnId,
+        toRowId: toRow?.id,
+      )
+          .then((result) {
+        result.fold((l) => null, (r) => add(BoardEvent.didReceiveError(r)));
+      });
+    }
+  }
+
+  void _moveGroup(String fromColumnId, String toColumnId) {
+    _rowService
+        .moveGroup(
+      fromGroupId: fromColumnId,
+      toGroupId: toColumnId,
+    )
+        .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();
   }
 
-  GridRowCache? getRowCache(String blockId, String rowId) {
+  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;
   }
@@ -80,7 +158,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
           add(BoardEvent.didReceiveGridUpdate(grid));
         }
       },
-      onGroupChanged: (groups) {
+      didLoadGroups: (groups) {
         List<AFBoardColumnData> columns = groups.map((group) {
           return AFBoardColumnData(
             id: group.groupId,
@@ -90,7 +168,11 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
           );
         }).toList();
 
-        boardDataController.addColumns(columns);
+        afBoardDataController.addColumns(columns);
+        initializeGroups(groups);
+      },
+      onRowsChanged: (List<RowInfo> rowInfos, RowsChangedReason reason) {
+        add(BoardEvent.didReceiveRows(rowInfos));
       },
       onError: (err) {
         Log.error(err);
@@ -98,18 +180,12 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
     );
   }
 
-  List<BoardColumnItem> _buildRows(List<RowPB> rows) {
-    return rows.map((row) {
-      final rowInfo = RowInfo(
-        gridId: _dataController.gridId,
-        blockId: row.blockId,
-        id: row.id,
-        fields: _dataController.fieldCache.unmodifiableFields,
-        height: row.height.toDouble(),
-        rawRow: row,
-      );
-      return BoardColumnItem(row: rowInfo);
+  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 {
@@ -128,9 +204,11 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
 @freezed
 class BoardEvent with _$BoardEvent {
   const factory BoardEvent.initial() = InitialGrid;
-  const factory BoardEvent.createRow() = _CreateRow;
-  const factory BoardEvent.didReceiveGroups(List<GroupPB> groups) =
-      _DidReceiveGroup;
+  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;
@@ -141,16 +219,18 @@ class BoardState with _$BoardState {
   const factory BoardState({
     required String gridId,
     required Option<GridPB> grid,
-    required List<GroupPB> groups,
+    required Option<RowPB> editingRow,
     required List<RowInfo> rowInfos,
     required GridLoadingState loadingState,
+    required Option<FlowyError> noneOrError,
   }) = _BoardState;
 
   factory BoardState.initial(String gridId) => BoardState(
         rowInfos: [],
-        groups: [],
         grid: none(),
         gridId: gridId,
+        editingRow: none(),
+        noneOrError: none(),
         loadingState: const _Loading(),
       );
 }
@@ -186,10 +266,41 @@ class GridFieldEquatable extends Equatable {
 }
 
 class BoardColumnItem extends AFColumnItem {
-  final RowInfo row;
+  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) {
+    //
+  }
+}

+ 43 - 14
frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart

@@ -3,6 +3,7 @@ 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';
@@ -11,39 +12,53 @@ import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart';
 
 typedef OnFieldsChanged = void Function(UnmodifiableListView<FieldPB>);
 typedef OnGridChanged = void Function(GridPB);
-typedef OnGroupChanged = void Function(List<GroupPB>);
+typedef DidLoadGroups = void Function(List<GroupPB>);
+typedef OnRowsChanged = void Function(
+  List<RowInfo>,
+  RowsChangedReason,
+);
 typedef OnError = void Function(FlowyError);
 
 class BoardDataController {
   final String gridId;
-  final GridService _gridFFIService;
+  final GridFFIService _gridFFIService;
   final GridFieldCache fieldCache;
 
   // key: the block id
   final LinkedHashMap<String, GridBlockCache> _blocks;
-  UnmodifiableMapView<String, GridBlockCache> get blocks =>
-      UnmodifiableMapView(_blocks);
+  LinkedHashMap<String, GridBlockCache> get blocks => _blocks;
 
   OnFieldsChanged? _onFieldsChanged;
   OnGridChanged? _onGridChanged;
-  OnGroupChanged? _onGroupChanged;
+  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.identity(),
-        _gridFFIService = GridService(gridId: view.id),
+        _blocks = LinkedHashMap.new(),
+        _gridFFIService = GridFFIService(gridId: view.id),
         fieldCache = GridFieldCache(gridId: view.id);
 
   void addListener({
     OnGridChanged? onGridChanged,
     OnFieldsChanged? onFieldsChanged,
-    OnGroupChanged? onGroupChanged,
+    DidLoadGroups? didLoadGroups,
+    OnRowsChanged? onRowsChanged,
     OnError? onError,
   }) {
     _onGridChanged = onGridChanged;
     _onFieldsChanged = onFieldsChanged;
-    _onGroupChanged = onGroupChanged;
+    _didLoadGroup = didLoadGroups;
+    _onRowsChanged = onRowsChanged;
     _onError = onError;
 
     fieldCache.addListener(onFields: (fields) {
@@ -57,10 +72,11 @@ class BoardDataController {
       () => result.fold(
         (grid) async {
           _onGridChanged?.call(grid);
+
           return await _loadFields(grid).then((result) {
             return result.fold(
               (l) {
-                _loadGroups();
+                _loadGroups(grid.blocks);
                 return left(l);
               },
               (err) => right(err),
@@ -72,8 +88,8 @@ class BoardDataController {
     );
   }
 
-  void createRow() {
-    _gridFFIService.createRow();
+  Future<Either<RowPB, FlowyError>> createBoardCard(String groupId) {
+    return _gridFFIService.createBoardCard(groupId);
   }
 
   Future<void> dispose() async {
@@ -99,12 +115,25 @@ class BoardDataController {
     );
   }
 
-  Future<void> _loadGroups() async {
+  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) {
-          _onGroupChanged?.call(groups.items);
+          _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() ?? "",
+      );
+}

+ 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;
+}

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

@@ -0,0 +1,80 @@
+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;
+
+            if (insertedRow.hasIndex() &&
+                group.rows.length > insertedRow.index) {
+              group.rows.insert(insertedRow.index, insertedRow.row);
+            } else {
+              group.rows.add(insertedRow.row);
+            }
+
+            delegate.insertRow(
+              group.groupId,
+              insertedRow.row,
+              index,
+            );
+          }
+
+          for (final deletedRow in changeset.deletedRows) {
+            group.rows.removeWhere((rowPB) => rowPB.id == deletedRow);
+            delegate.removeRow(group.groupId, deletedRow);
+          }
+
+          for (final updatedRow in changeset.updatedRows) {
+            final index = group.rows.indexWhere(
+              (rowPB) => rowPB.id == updatedRow.id,
+            );
+
+            if (index != -1) {
+              group.rows[index] = updatedRow;
+            }
+
+            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;
+  }
+}

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

@@ -20,13 +20,13 @@ class BoardPluginBuilder implements PluginBuilder {
   String get menuName => "Board";
 
   @override
-  PluginType get pluginType => DefaultPlugin.board.type();
+  PluginType get pluginType => PluginType.board;
 
   @override
   ViewDataTypePB get dataType => ViewDataTypePB.Database;
 
   @override
-  SubViewDataTypePB get subDataType => SubViewDataTypePB.Board;
+  ViewLayoutTypePB? get subDataType => ViewLayoutTypePB.Board;
 }
 
 class BoardPluginConfig implements PluginConfig {

+ 79 - 10
frontend/app_flowy/lib/plugins/board/presentation/board_page.dart

@@ -1,12 +1,23 @@
 // 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;
@@ -49,12 +60,14 @@ class BoardContent extends StatelessWidget {
         return Container(
           color: Colors.white,
           child: Padding(
-            padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
+            padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
             child: AFBoard(
-              dataController: context.read<BoardBloc>().boardDataController,
+              // key: UniqueKey(),
+              scrollController: ScrollController(),
+              dataController: context.read<BoardBloc>().afBoardDataController,
               headerBuilder: _buildHeader,
               footBuilder: _buildFooter,
-              cardBuilder: _buildCard,
+              cardBuilder: (_, data) => _buildCard(context, data),
               columnConstraints: const BoxConstraints.tightFor(width: 240),
               config: AFBoardConfig(
                 columnBackgroundColor: HexColor.fromHex('#F7F8FC'),
@@ -79,20 +92,76 @@ class BoardContent extends StatelessWidget {
 
   Widget _buildFooter(BuildContext context, AFBoardColumnData columnData) {
     return AppFlowyColumnFooter(
-      icon: const Icon(Icons.add, size: 20),
-      title: const Text('New'),
-      height: 50,
-      margin: config.columnItemPadding,
-    );
+        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 rowInfo = (item as BoardColumnItem).row;
+    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(rowInfo: rowInfo),
+      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 {

+ 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();
+  }
+}

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

@@ -1,6 +1,6 @@
 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/select_option_cell.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';
 
@@ -34,9 +34,21 @@ class _BoardSelectOptionCellState extends State<BoardSelectOptionCell> {
       value: _cellBloc,
       child: BlocBuilder<BoardSelectOptionCellBloc, BoardSelectOptionCellState>(
         builder: (context, state) {
-          return SelectOptionWrap(
-            selectOptions: state.selectedOptions,
-            cellControllerBuilder: widget.cellControllerBuilder,
+          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,
+              ),
+            ),
           );
         },
       ),

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

@@ -32,10 +32,22 @@ class _BoardTextCellState extends State<BoardTextCell> {
       value: _cellBloc,
       child: BlocBuilder<BoardTextCellBloc, BoardTextCellState>(
         builder: (context, state) {
-          return SizedBox(
-            height: 30,
-            child: FlowyText.medium(state.content),
-          );
+          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,
+                ),
+              ),
+            );
+          }
         },
       ),
     );

+ 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();
+  }
+}

+ 90 - 5
frontend/app_flowy/lib/plugins/board/presentation/card/card.dart

@@ -1,13 +1,98 @@
-import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
+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';
 
-class BoardCard extends StatelessWidget {
-  final RowInfo rowInfo;
+typedef OnEndEditing = void Function(String rowId);
 
-  const BoardCard({required this.rowInfo, Key? key}) : super(key: key);
+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 const SizedBox(height: 20, child: Text('1234'));
+    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;
+}

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

@@ -42,10 +42,10 @@ class DocumentPluginBuilder extends PluginBuilder {
   String get menuName => LocaleKeys.document_menuName.tr();
 
   @override
-  PluginType get pluginType => DefaultPlugin.editor.type();
+  PluginType get pluginType => PluginType.editor;
 
   @override
-  ViewDataTypePB get dataType => ViewDataTypePB.TextBlock;
+  ViewDataTypePB get dataType => ViewDataTypePB.Text;
 }
 
 class DocumentPlugin implements Plugin {

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

@@ -42,7 +42,7 @@ class GridBlockCache {
   }
 
   void addListener({
-    required void Function(RowChangeReason) onChangeReason,
+    required void Function(RowsChangedReason) onRowsChanged,
     bool Function()? listenWhen,
   }) {
     _rowCache.onRowsChanged((reason) {
@@ -50,7 +50,7 @@ class GridBlockCache {
         return;
       }
 
-      onChangeReason(reason);
+      onRowsChanged(reason);
     });
   }
 }

+ 7 - 5
frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart

@@ -1,6 +1,8 @@
 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
@@ -58,7 +60,7 @@ class GridCellControllerBuilder {
           parser: StringCellDataParser(),
           reloadOnFieldChanged: true,
         );
-        return GridCellController(
+        return GridNumberCellController(
           cellId: _cellId,
           cellCache: _cellCache,
           cellDataLoader: cellDataLoader,
@@ -127,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;
@@ -186,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),
@@ -240,7 +242,7 @@ 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),
       );
     });
@@ -289,7 +291,7 @@ class IGridCellController<T, D> extends Equatable {
       return;
     }
     _isDispose = true;
-    _cellListener.stop();
+    _cellListener?.stop();
     _loadDataOperation?.cancel();
     _saveDataOperation?.cancel();
     _cellDataNotifier = null;

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

@@ -6,7 +6,7 @@ import 'cell_service/cell_service.dart';
 part 'checkbox_cell_bloc.freezed.dart';
 
 class CheckboxCellBloc extends Bloc<CheckboxCellEvent, CheckboxCellState> {
-  final GridCellController cellController;
+  final GridCheckboxCellController cellController;
   void Function()? _onCellChangedFn;
 
   CheckboxCellBloc({

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

@@ -8,7 +8,7 @@ import 'cell_service/cell_service.dart';
 part 'number_cell_bloc.freezed.dart';
 
 class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
-  final GridCellController cellController;
+  final GridNumberCellController cellController;
   void Function()? _onCellChangedFn;
 
   NumberCellBloc({

+ 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;
         }

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

@@ -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

+ 3 - 4
frontend/app_flowy/lib/plugins/grid/application/field/field_service.dart

@@ -18,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({

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

@@ -98,7 +98,7 @@ class GridEvent with _$GridEvent {
   const factory GridEvent.createRow() = _CreateRow;
   const factory GridEvent.didReceiveRowUpdate(
     List<RowInfo> rows,
-    RowChangeReason listState,
+    RowsChangedReason listState,
   ) = _DidReceiveRowUpdate;
   const factory GridEvent.didReceiveFieldUpdate(
     UnmodifiableListView<FieldPB> fields,
@@ -117,7 +117,7 @@ class GridState with _$GridState {
     required GridFieldEquatable fields,
     required List<RowInfo> rowInfos,
     required GridLoadingState loadingState,
-    required RowChangeReason reason,
+    required RowsChangedReason reason,
   }) = _GridState;
 
   factory GridState.initial(String gridId) => GridState(

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

@@ -18,13 +18,13 @@ typedef OnGridChanged = void Function(GridPB);
 
 typedef OnRowsChanged = void Function(
   List<RowInfo> rowInfos,
-  RowChangeReason,
+  RowsChangedReason,
 );
 typedef ListenOnRowChangedCondition = bool Function();
 
 class GridDataController {
   final String gridId;
-  final GridService _gridFFIService;
+  final GridFFIService _gridFFIService;
   final GridFieldCache fieldCache;
 
   // key: the block id
@@ -46,8 +46,8 @@ class GridDataController {
 
   GridDataController({required ViewPB view})
       : gridId = view.id,
-        _blocks = LinkedHashMap.identity(),
-        _gridFFIService = GridService(gridId: view.id),
+        _blocks = LinkedHashMap.new(),
+        _gridFFIService = GridFFIService(gridId: view.id),
         fieldCache = GridFieldCache(gridId: view.id);
 
   void addListener({
@@ -105,7 +105,7 @@ class GridDataController {
       );
 
       cache.addListener(
-        onChangeReason: (reason) {
+        onRowsChanged: (reason) {
           _onRowChanged?.call(rowInfos, reason);
         },
       );

+ 11 - 4
frontend/app_flowy/lib/plugins/grid/application/grid_service.dart

@@ -8,9 +8,9 @@ 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';
 
-class GridService {
+class GridFFIService {
   final String gridId;
-  GridService({
+  GridFFIService({
     required this.gridId,
   });
 
@@ -22,9 +22,16 @@ class GridService {
   }
 
   Future<Either<RowPB, FlowyError>> createRow({Option<String>? startRowId}) {
-    CreateRowPayloadPB payload = CreateRowPayloadPB.create()..gridId = gridId;
+    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<RepeatedFieldPB, FlowyError>> getFields(

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

@@ -12,24 +12,24 @@ part 'row_action_sheet_bloc.freezed.dart';
 
 class RowActionSheetBloc
     extends Bloc<RowActionSheetEvent, RowActionSheetState> {
-  final RowService _rowService;
+  final RowFFIService _rowService;
 
-  RowActionSheetBloc({required RowInfo 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);
           },
         );

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

@@ -12,16 +12,15 @@ import 'row_service.dart';
 part 'row_bloc.freezed.dart';
 
 class RowBloc extends Bloc<RowEvent, RowState> {
-  final RowService _rowService;
+  final RowFFIService _rowService;
   final GridRowDataController _dataController;
 
   RowBloc({
     required RowInfo rowInfo,
     required GridRowDataController dataController,
-  })  : _rowService = RowService(
+  })  : _rowService = RowFFIService(
           gridId: rowInfo.gridId,
-          blockId: rowInfo.blockId,
-          rowId: rowInfo.id,
+          blockId: rowInfo.rowPB.blockId,
         ),
         _dataController = dataController,
         super(RowState.initial(rowInfo, dataController.loadData())) {
@@ -32,16 +31,15 @@ class RowBloc extends Bloc<RowEvent, RowState> {
             await _startListening();
           },
           createRow: (_CreateRow value) {
-            _rowService.createRow();
+            _rowService.createRow(rowInfo.rowPB.id);
           },
           didReceiveCells: (_DidReceiveCells value) async {
-            final fields = value.gridCellMap.values
+            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,
             ));
           },
@@ -72,7 +70,7 @@ class RowEvent with _$RowEvent {
   const factory RowEvent.initial() = _InitialRow;
   const factory RowEvent.createRow() = _CreateRow;
   const factory RowEvent.didReceiveCells(
-      GridCellMap gridCellMap, RowChangeReason reason) = _DidReceiveCells;
+      GridCellMap gridCellMap, RowsChangedReason reason) = _DidReceiveCells;
 }
 
 @freezed
@@ -80,16 +78,17 @@ class RowState with _$RowState {
   const factory RowState({
     required RowInfo rowInfo,
     required GridCellMap gridCellMap,
-    required UnmodifiableListView<GridCellEquatable> snapshots,
-    RowChangeReason? changeReason,
+    required UnmodifiableListView<GridCellEquatable> cells,
+    RowsChangedReason? changeReason,
   }) = _RowState;
 
   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(),
+        ),
       );
 }
 

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

@@ -51,11 +51,9 @@ class GridRowCache {
         _fieldNotifier = notifier {
     //
     notifier.onRowFieldsChanged(() => _rowChangeReasonNotifier
-        .receive(const RowChangeReason.fieldDidChange()));
+        .receive(const RowsChangedReason.fieldDidChange()));
     notifier.onRowFieldChanged((field) => _cellCache.remove(field.id));
-    _rowInfos = block.rows
-        .map((rowInfo) => buildGridRow(rowInfo.id, rowInfo.height.toDouble()))
-        .toList();
+    _rowInfos = block.rows.map((rowPB) => buildGridRow(rowPB)).toList();
   }
 
   Future<void> dispose() async {
@@ -85,16 +83,16 @@ class GridRowCache {
       for (var rowId in deletedRows) rowId: rowId
     };
 
-    _rowInfos.asMap().forEach((index, row) {
-      if (deletedRowByRowId[row.id] == null) {
-        newRows.add(row);
+    _rowInfos.asMap().forEach((index, RowInfo rowInfo) {
+      if (deletedRowByRowId[rowInfo.rowPB.id] == null) {
+        newRows.add(rowInfo);
       } else {
-        _rowByRowId.remove(row.id);
-        deletedIndex.add(DeletedIndex(index: index, row: row));
+        _rowByRowId.remove(rowInfo.rowPB.id);
+        deletedIndex.add(DeletedIndex(index: index, row: rowInfo));
       }
     });
     _rowInfos = newRows;
-    _rowChangeReasonNotifier.receive(RowChangeReason.delete(deletedIndex));
+    _rowChangeReasonNotifier.receive(RowsChangedReason.delete(deletedIndex));
   }
 
   void _insertRows(List<InsertedRowPB> insertRows) {
@@ -103,39 +101,42 @@ class GridRowCache {
     }
 
     InsertedIndexs insertIndexs = [];
-    for (final insertRow in insertRows) {
+    for (final InsertedRowPB insertRow in insertRows) {
       final insertIndex = InsertedIndex(
         index: insertRow.index,
-        rowId: insertRow.rowId,
+        rowId: insertRow.row.id,
       );
       insertIndexs.add(insertIndex);
-      _rowInfos.insert(insertRow.index,
-          (buildGridRow(insertRow.rowId, insertRow.height.toDouble())));
+      _rowInfos.insert(
+        insertRow.index,
+        (buildGridRow(insertRow.row)),
+      );
     }
 
-    _rowChangeReasonNotifier.receive(RowChangeReason.insert(insertIndexs));
+    _rowChangeReasonNotifier.receive(RowsChangedReason.insert(insertIndexs));
   }
 
-  void _updateRows(List<UpdatedRowPB> updatedRows) {
+  void _updateRows(List<RowPB> 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);
+    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.row;
+        _rowByRowId[rowId] = updatedRow;
 
         _rowInfos.removeAt(index);
-        _rowInfos.insert(
-            index, buildGridRow(rowId, updatedRow.row.height.toDouble()));
+        _rowInfos.insert(index, buildGridRow(updatedRow));
         updatedIndexs[rowId] = UpdatedIndex(index: index, rowId: rowId);
       }
     }
 
-    _rowChangeReasonNotifier.receive(RowChangeReason.update(updatedIndexs));
+    _rowChangeReasonNotifier.receive(RowsChangedReason.update(updatedIndexs));
   }
 
   void _hideRows(List<String> hideRows) {}
@@ -143,7 +144,7 @@ class GridRowCache {
   void _showRows(List<String> visibleRows) {}
 
   void onRowsChanged(
-    void Function(RowChangeReason) onRowChanged,
+    void Function(RowsChangedReason) onRowChanged,
   ) {
     _rowChangeReasonNotifier.addListener(() {
       onRowChanged(_rowChangeReasonNotifier.reason);
@@ -152,7 +153,7 @@ class GridRowCache {
 
   RowUpdateCallback addListener({
     required String rowId,
-    void Function(GridCellMap, RowChangeReason)? onCellUpdated,
+    void Function(GridCellMap, RowsChangedReason)? onCellUpdated,
     bool Function()? listenWhen,
   }) {
     listenerHandler() async {
@@ -230,40 +231,42 @@ class GridRowCache {
 
     _rowByRowId[updatedRow.id] = updatedRow;
     final index =
-        _rowInfos.indexWhere((gridRow) => gridRow.id == updatedRow.id);
+        _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].rawRow != updatedRow) {
-        final row = _rowInfos.removeAt(index).copyWith(rawRow: updatedRow);
-        _rowInfos.insert(index, row);
+      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[row.id] = UpdatedIndex(index: index, rowId: row.id);
+        updatedIndexs[rowInfo.rowPB.id] = UpdatedIndex(
+          index: index,
+          rowId: rowInfo.rowPB.id,
+        );
 
         //
-        _rowChangeReasonNotifier.receive(RowChangeReason.update(updatedIndexs));
+        _rowChangeReasonNotifier
+            .receive(RowsChangedReason.update(updatedIndexs));
       }
     }
   }
 
-  RowInfo buildGridRow(String rowId, double rowHeight) {
+  RowInfo buildGridRow(RowPB rowPB) {
     return RowInfo(
       gridId: gridId,
-      blockId: block.id,
       fields: _fieldNotifier.fields,
-      id: rowId,
-      height: rowHeight,
+      rowPB: rowPB,
     );
   }
 }
 
 class _RowChangesetNotifier extends ChangeNotifier {
-  RowChangeReason reason = const InitialListState();
+  RowsChangedReason reason = const InitialListState();
 
   _RowChangesetNotifier();
 
-  void receive(RowChangeReason newReason) {
+  void receive(RowsChangedReason newReason) {
     reason = newReason;
     reason.map(
       insert: (_) => notifyListeners(),
@@ -279,11 +282,8 @@ class _RowChangesetNotifier extends ChangeNotifier {
 class RowInfo with _$RowInfo {
   const factory RowInfo({
     required String gridId,
-    required String blockId,
-    required String id,
     required UnmodifiableListView<FieldPB> fields,
-    required double height,
-    RowPB? rawRow,
+    required RowPB rowPB,
   }) = _RowInfo;
 }
 
@@ -292,12 +292,12 @@ typedef DeletedIndexs = List<DeletedIndex>;
 typedef UpdatedIndexs = LinkedHashMap<String, UpdatedIndex>;
 
 @freezed
-class RowChangeReason with _$RowChangeReason {
-  const factory RowChangeReason.insert(InsertedIndexs items) = _Insert;
-  const factory RowChangeReason.delete(DeletedIndexs items) = _Delete;
-  const factory RowChangeReason.update(UpdatedIndexs indexs) = _Update;
-  const factory RowChangeReason.fieldDidChange() = _FieldDidChange;
-  const factory RowChangeReason.initial() = InitialListState;
+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 {

+ 4 - 8
frontend/app_flowy/lib/plugins/grid/application/row/row_data_controller.dart

@@ -5,7 +5,7 @@ import '../cell/cell_service/cell_service.dart';
 import '../field/field_cache.dart';
 import 'row_cache.dart';
 
-typedef OnRowChanged = void Function(GridCellMap, RowChangeReason);
+typedef OnRowChanged = void Function(GridCellMap, RowsChangedReason);
 
 class GridRowDataController extends GridCellBuilderDelegate {
   final RowInfo rowInfo;
@@ -13,10 +13,6 @@ class GridRowDataController extends GridCellBuilderDelegate {
   final GridFieldCache _fieldCache;
   final GridRowCache _rowCache;
 
-  GridFieldCache get fieldCache => _fieldCache;
-
-  GridRowCache get rowCache => _rowCache;
-
   GridRowDataController({
     required this.rowInfo,
     required GridFieldCache fieldCache,
@@ -25,12 +21,12 @@ class GridRowDataController extends GridCellBuilderDelegate {
         _rowCache = rowCache;
 
   GridCellMap loadData() {
-    return _rowCache.loadGridCells(rowInfo.id);
+    return _rowCache.loadGridCells(rowInfo.rowPB.id);
   }
 
   void addListener({OnRowChanged? onRowChanged}) {
     _onRowChangedListeners.add(_rowCache.addListener(
-      rowId: rowInfo.id,
+      rowId: rowInfo.rowPB.id,
       onCellUpdated: onRowChanged,
     ));
   }
@@ -49,5 +45,5 @@ class GridRowDataController extends GridCellBuilderDelegate {
   }
 
   @override
-  GridCellCache get cellCache => rowCache.cellCache;
+  GridCellCache get cellCache => _rowCache.cellCache;
 }

+ 61 - 22
frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart

@@ -3,37 +3,27 @@ import 'package:flowy_sdk/dispatch/dispatch.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/grid_entities.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/group_changeset.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/row_entities.pb.dart';
 
-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<RowPB, 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();
+    return GridEventCreateTableRow(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();
-  }
-
-  Future<Either<OptionalRowPB, FlowyError>> getRow() {
+  Future<Either<OptionalRowPB, FlowyError>> getRow(String rowId) {
     final payload = RowIdPB.create()
       ..gridId = gridId
       ..blockId = blockId
@@ -42,7 +32,7 @@ class RowService {
     return GridEventGetRow(payload).send();
   }
 
-  Future<Either<Unit, FlowyError>> deleteRow() {
+  Future<Either<Unit, FlowyError>> deleteRow(String rowId) {
     final payload = RowIdPB.create()
       ..gridId = gridId
       ..blockId = blockId
@@ -51,7 +41,7 @@ class RowService {
     return GridEventDeleteRow(payload).send();
   }
 
-  Future<Either<Unit, FlowyError>> duplicateRow() {
+  Future<Either<Unit, FlowyError>> duplicateRow(String rowId) {
     final payload = RowIdPB.create()
       ..gridId = gridId
       ..blockId = blockId
@@ -60,3 +50,52 @@ class RowService {
     return GridEventDuplicateRow(payload).send();
   }
 }
+
+class MoveRowFFIService {
+  final String gridId;
+
+  MoveRowFFIService({
+    required this.gridId,
+  });
+
+  Future<Either<Unit, FlowyError>> moveRow({
+    required String fromRowId,
+    required String toRowId,
+  }) {
+    var payload = MoveRowPayloadPB.create()
+      ..viewId = gridId
+      ..fromRowId = fromRowId
+      ..toRowId = toRowId;
+
+    return GridEventMoveRow(payload).send();
+  }
+
+  Future<Either<Unit, FlowyError>> moveGroupRow({
+    required String fromRowId,
+    required String toGroupId,
+    required String? toRowId,
+  }) {
+    var payload = MoveGroupRowPayloadPB.create()
+      ..viewId = gridId
+      ..fromRowId = fromRowId
+      ..toGroupId = toGroupId;
+
+    if (toRowId != null) {
+      payload.toRowId = toRowId;
+    }
+
+    return GridEventMoveGroupRow(payload).send();
+  }
+
+  Future<Either<Unit, FlowyError>> moveGroup({
+    required String fromGroupId,
+    required String toGroupId,
+  }) {
+    final payload = MoveGroupPayloadPB.create()
+      ..viewId = gridId
+      ..fromGroupId = fromGroupId
+      ..toGroupId = toGroupId;
+
+    return GridEventMoveGroup(payload).send();
+  }
+}

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

@@ -22,13 +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
   ViewDataTypePB get dataType => ViewDataTypePB.Database;
 
   @override
-  SubViewDataTypePB? get subDataType => SubViewDataTypePB.Grid;
+  ViewLayoutTypePB? get subDataType => ViewLayoutTypePB.Grid;
 }
 
 class GridPluginConfig implements PluginConfig {

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

@@ -239,8 +239,10 @@ class _GridRowsState extends State<_GridRows> {
     RowInfo rowInfo,
     Animation<double> animation,
   ) {
-    final rowCache =
-        context.read<GridBloc>().getRowCache(rowInfo.blockId, rowInfo.id);
+    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();
@@ -267,7 +269,7 @@ class _GridRowsState extends State<_GridRows> {
             cellBuilder,
           );
         },
-        key: ValueKey(rowInfo.id),
+        key: ValueKey(rowInfo.rowPB.id),
       ),
     );
   }

+ 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;

+ 12 - 0
frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_builder.dart

@@ -94,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) {

+ 20 - 16
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();

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

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

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

@@ -15,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';
 
@@ -39,6 +38,7 @@ class DateCellEditor with FlowyOverlayDelegate {
 
     final result =
         await cellController.getFieldTypeOption(DateTypeOptionDataParser());
+
     result.fold(
       (dateTypeOptionPB) {
         final calendar = _CellCalendarWidget(

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

@@ -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),

+ 23 - 20
frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart

@@ -153,21 +153,25 @@ class SelectOptionWrap extends StatelessWidget {
     if (selectOptions.isEmpty && cellStyle != null) {
       child = Align(
         alignment: Alignment.centerLeft,
-        child: FlowyText.medium(cellStyle!.placeholder,
-            fontSize: 14, color: theme.shader3),
+        child: FlowyText.medium(
+          cellStyle!.placeholder,
+          fontSize: 14,
+          color: theme.shader3,
+        ),
       );
     } else {
-      final tags = selectOptions
-          .map(
-            (option) => SelectOptionTag.fromSelectOption(
-              context: context,
-              option: option,
-            ),
-          )
-          .toList();
       child = Align(
         alignment: Alignment.centerLeft,
-        child: Wrap(children: tags, spacing: 4, runSpacing: 2),
+        child: Wrap(
+          children: selectOptions
+              .map((option) => SelectOptionTag.fromOption(
+                    context: context,
+                    option: option,
+                  ))
+              .toList(),
+          spacing: 4,
+          runSpacing: 2,
+        ),
       );
     }
 
@@ -176,15 +180,14 @@ class SelectOptionWrap extends StatelessWidget {
       fit: StackFit.expand,
       children: [
         child,
-        InkWell(
-          onTap: () {
-            onFocus?.call(true);
-            final cellContext =
-                cellControllerBuilder.build() as GridSelectOptionCellController;
-            SelectOptionCellEditor.show(
-                context, cellContext, () => onFocus?.call(false));
-          },
-        ),
+        InkWell(onTap: () {
+          onFocus?.call(true);
+          SelectOptionCellEditor.show(
+            context,
+            cellControllerBuilder.build() as GridSelectOptionCellController,
+            () => onFocus?.call(false),
+          );
+        }),
       ],
     );
   }

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

@@ -49,7 +49,8 @@ class SelectOptionTextField extends StatelessWidget {
       initialTags: selectedOptionMap.keys.toList(),
       focusNode: _focusNode,
       textSeparators: const [' ', ','],
-      inputfieldBuilder: (BuildContext context, editController, focusNode, error, onChanged, onSubmitted) {
+      inputfieldBuilder: (BuildContext context, editController, focusNode,
+          error, onChanged, onSubmitted) {
         return ((context, sc, tags, onTagDelegate) {
           return TextField(
             autofocus: true,
@@ -99,7 +100,8 @@ class SelectOptionTextField extends StatelessWidget {
     }
 
     final children = selectedOptionMap.values
-        .map((option) => SelectOptionTag.fromSelectOption(context: context, option: option))
+        .map((option) =>
+            SelectOptionTag.fromOption(context: context, option: option))
         .toList();
     return Padding(
       padding: const EdgeInsets.all(8.0),

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

@@ -76,7 +76,7 @@ class _GridTextCellState extends GridFocusNodeCellState<GridTextCell> {
 
   @override
   Future<void> dispose() async {
-    _delayOperation?.cancel();
+    _delayOperation = null;
     _cellBloc.close();
     super.dispose();
   }
@@ -85,7 +85,7 @@ class _GridTextCellState extends GridFocusNodeCellState<GridTextCell> {
   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 != _cellBloc.state.content) {
           _cellBloc.add(TextCellEvent.updateText(_controller.text));

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

@@ -90,9 +90,9 @@ class _GridURLCellState extends GridCellState<GridURLCell> {
 
   @override
   void initState() {
-    final cellContext =
+    final cellController =
         widget.cellControllerBuilder.build() as GridURLCellController;
-    _cellBloc = URLCellBloc(cellController: cellContext);
+    _cellBloc = URLCellBloc(cellController: cellController);
     _cellBloc.add(const URLCellEvent.initial());
     super.initState();
   }

+ 1 - 1
frontend/app_flowy/lib/plugins/grid/presentation/widgets/footer/grid_footer.dart

@@ -14,7 +14,7 @@ class GridAddRowButton extends StatelessWidget {
     final theme = context.watch<AppTheme>();
     return FlowyButton(
       text: const FlowyText.medium('New row', fontSize: 12),
-      hoverColor: theme.hover,
+      hoverColor: theme.shader6,
       onTap: () => context.read<GridBloc>().add(const GridEvent.createRow()),
       leftIcon: svgWidget("home/add"),
     );

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

@@ -156,7 +156,7 @@ class CreateFieldButton extends StatelessWidget {
 
     return FlowyButton(
       text: const FlowyText.medium('New column', fontSize: 12),
-      hoverColor: theme.hover,
+      hoverColor: theme.shader6,
       onTap: () => FieldEditor(
         gridId: gridId,
         fieldName: "",

+ 17 - 18
frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart

@@ -52,7 +52,7 @@ class _GridRowWidgetState extends State<GridRowWidget> {
       value: _rowBloc,
       child: _RowEnterRegion(
         child: BlocBuilder<RowBloc, RowState>(
-          buildWhen: (p, c) => p.rowInfo.height != c.rowInfo.height,
+          buildWhen: (p, c) => p.rowInfo.rowPB.height != c.rowInfo.rowPB.height,
           builder: (context, state) {
             final children = [
               const _RowLeading(),
@@ -164,7 +164,7 @@ class RowContent extends StatelessWidget {
   Widget build(BuildContext context) {
     return BlocBuilder<RowBloc, RowState>(
       buildWhen: (previous, current) =>
-          !listEquals(previous.snapshots, current.snapshots),
+          !listEquals(previous.cells, current.cells),
       builder: (context, state) {
         return IntrinsicHeight(
             child: Row(
@@ -181,28 +181,27 @@ class RowContent extends StatelessWidget {
     return gridCellMap.values.map(
       (cellId) {
         final GridCellWidget child = builder.build(cellId);
-        accessoryBuilder(GridCellAccessoryBuildContext buildContext) {
-          final builder = child.accessoryBuilder;
-          List<GridCellAccessory> accessories = [];
-          if (cellId.field.isPrimary) {
-            accessories.add(PrimaryCellAccessory(
-              onTapCallback: onExpand,
-              isCellEditing: buildContext.isCellEditing,
-            ));
-          }
-
-          if (builder != null) {
-            accessories.addAll(builder(buildContext));
-          }
-          return accessories;
-        }
 
         return CellContainer(
           width: cellId.field.width.toDouble(),
           child: child,
           rowStateNotifier:
               Provider.of<RegionStateNotifier>(context, listen: false),
-          accessoryBuilder: accessoryBuilder,
+          accessoryBuilder: (buildContext) {
+            final builder = child.accessoryBuilder;
+            List<GridCellAccessory> accessories = [];
+            if (cellId.field.isPrimary) {
+              accessories.add(PrimaryCellAccessory(
+                onTapCallback: onExpand,
+                isCellEditing: buildContext.isCellEditing,
+              ));
+            }
+
+            if (builder != null) {
+              accessories.addAll(builder(buildContext));
+            }
+            return accessories;
+          },
         );
       },
     ).toList();

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

@@ -21,7 +21,7 @@ class GridRowActionSheet extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     return BlocProvider(
-      create: (context) => RowActionSheetBloc(rowData: rowData),
+      create: (context) => RowActionSheetBloc(rowInfo: rowData),
       child: BlocBuilder<RowActionSheetBloc, RowActionSheetState>(
         builder: (context, state) {
           final cells = _RowAction.values
@@ -53,7 +53,10 @@ class GridRowActionSheet extends StatelessWidget {
     );
   }
 
-  void show(BuildContext overlayContext) {
+  void show(
+    BuildContext overlayContext, {
+    AnchorDirection direction = AnchorDirection.leftWithCenterAligned,
+  }) {
     FlowyOverlay.of(overlayContext).insertWithAnchor(
       widget: OverlayContainer(
         child: this,
@@ -61,7 +64,7 @@ class GridRowActionSheet extends StatelessWidget {
       ),
       identifier: GridRowActionSheet.identifier(),
       anchorContext: overlayContext,
-      anchorDirection: AnchorDirection.leftWithCenterAligned,
+      anchorDirection: direction,
     );
   }
 

+ 1 - 1
frontend/app_flowy/lib/plugins/trash/menu.dart

@@ -23,7 +23,7 @@ class MenuTrash extends StatelessWidget {
         onTap: () {
           getIt<MenuSharedState>().latestOpenView = null;
           getIt<HomeStackManager>()
-              .setPlugin(makePlugin(pluginType: DefaultPlugin.trash.type()));
+              .setPlugin(makePlugin(pluginType: PluginType.trash));
         },
         child: _render(context),
       ),

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

@@ -34,7 +34,7 @@ class TrashPluginBuilder extends PluginBuilder {
   String get menuName => "TrashPB";
 
   @override
-  PluginType get pluginType => DefaultPlugin.trash.type();
+  PluginType get pluginType => PluginType.trash;
 }
 
 class TrashPluginConfig implements PluginConfig {

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

@@ -9,7 +9,7 @@ import 'package:flutter/widgets.dart';
 
 export "./src/sandbox.dart";
 
-enum DefaultPlugin {
+enum PluginType {
   editor,
   blank,
   trash,
@@ -17,24 +17,24 @@ enum DefaultPlugin {
   board,
 }
 
-extension FlowyDefaultPluginExt on DefaultPlugin {
-  int type() {
-    switch (this) {
-      case DefaultPlugin.editor:
-        return 0;
-      case DefaultPlugin.blank:
-        return 1;
-      case DefaultPlugin.trash:
-        return 2;
-      case DefaultPlugin.grid:
-        return 3;
-      case DefaultPlugin.board:
-        return 4;
-    }
-  }
-}
-
-typedef PluginType = int;
+// extension FlowyDefaultPluginExt on DefaultPlugin {
+//   int type() {
+//     switch (this) {
+//       case DefaultPlugin.editor:
+//         return 0;
+//       case DefaultPlugin.blank:
+//         return 1;
+//       case DefaultPlugin.trash:
+//         return 2;
+//       case DefaultPlugin.grid:
+//         return 3;
+//       case DefaultPlugin.board:
+//         return 4;
+//     }
+//   }
+// }
+
+// typedef PluginType = int;
 typedef PluginId = String;
 
 abstract class Plugin {
@@ -54,9 +54,9 @@ abstract class PluginBuilder {
 
   PluginType get pluginType;
 
-  ViewDataTypePB get dataType => ViewDataTypePB.TextBlock;
+  ViewDataTypePB get dataType => ViewDataTypePB.Text;
 
-  SubViewDataTypePB? get subDataType => null;
+  ViewLayoutTypePB? get subDataType => ViewLayoutTypePB.Document;
 }
 
 abstract class PluginConfig {

+ 13 - 7
frontend/app_flowy/lib/startup/plugin/src/sandbox.dart

@@ -6,8 +6,10 @@ import '../plugin.dart';
 import 'runner.dart';
 
 class PluginSandbox {
-  final LinkedHashMap<PluginType, PluginBuilder> _pluginBuilders = LinkedHashMap();
-  final Map<PluginType, PluginConfig> _pluginConfigs = <PluginType, PluginConfig>{};
+  final LinkedHashMap<PluginType, PluginBuilder> _pluginBuilders =
+      LinkedHashMap();
+  final Map<PluginType, PluginConfig> _pluginConfigs =
+      <PluginType, PluginConfig>{};
   late PluginRunner pluginRunner;
 
   PluginSandbox() {
@@ -15,9 +17,11 @@ class PluginSandbox {
   }
 
   int indexOf(PluginType pluginType) {
-    final index = _pluginBuilders.keys.toList().indexWhere((ty) => ty == pluginType);
+    final index =
+        _pluginBuilders.keys.toList().indexWhere((ty) => ty == pluginType);
     if (index == -1) {
-      throw PlatformException(code: '-1', message: "Can't find the flowy plugin type: $pluginType");
+      throw PlatformException(
+          code: '-1', message: "Can't find the flowy plugin type: $pluginType");
     }
     return index;
   }
@@ -27,9 +31,11 @@ class PluginSandbox {
     return plugin;
   }
 
-  void registerPlugin(PluginType pluginType, PluginBuilder builder, {PluginConfig? config}) {
+  void registerPlugin(PluginType pluginType, PluginBuilder builder,
+      {PluginConfig? config}) {
     if (_pluginBuilders.containsKey(pluginType)) {
-      throw PlatformException(code: '-1', message: "$pluginType was registered before");
+      throw PlatformException(
+          code: '-1', message: "$pluginType was registered before");
     }
     _pluginBuilders[pluginType] = builder;
 
@@ -38,7 +44,7 @@ class PluginSandbox {
     }
   }
 
-  List<int> get supportPluginTypes => _pluginBuilders.keys.toList();
+  List<PluginType> get supportPluginTypes => _pluginBuilders.keys.toList();
 
   List<PluginBuilder> get builders => _pluginBuilders.values.toList();
 

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

@@ -86,7 +86,7 @@ class AppBloc extends Bloc<AppEvent, AppState> {
       desc: value.desc,
       dataType: value.dataType,
       pluginType: value.pluginType,
-      subDataType: value.subDataType,
+      layout: value.layout,
     );
     viewOrFailed.fold(
       (view) => emit(state.copyWith(
@@ -140,7 +140,7 @@ class AppEvent with _$AppEvent {
     String name,
     String desc,
     ViewDataTypePB dataType,
-    SubViewDataTypePB? subDataType,
+    ViewLayoutTypePB layout,
     PluginType pluginType,
   ) = CreateView;
   const factory AppEvent.delete() = Delete;

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

@@ -26,18 +26,14 @@ class AppService {
     required String desc,
     required ViewDataTypePB dataType,
     required PluginType pluginType,
-    SubViewDataTypePB? subDataType,
+    required ViewLayoutTypePB layout,
   }) {
     var payload = CreateViewPayloadPB.create()
       ..belongToId = appId
       ..name = name
       ..desc = desc
       ..dataType = dataType
-      ..pluginType = pluginType;
-
-    if (subDataType != null) {
-      payload.subDataType = subDataType;
-    }
+      ..layout = layout;
 
     return FolderEventCreateView(payload).send();
   }

+ 1 - 1
frontend/app_flowy/lib/workspace/application/menu/menu_bloc.dart

@@ -113,6 +113,6 @@ class MenuState with _$MenuState {
   factory MenuState.initial() => MenuState(
         apps: [],
         successOrFailure: left(unit),
-        plugin: makePlugin(pluginType: DefaultPlugin.blank.type()),
+        plugin: makePlugin(pluginType: PluginType.blank),
       );
 }

+ 13 - 0
frontend/app_flowy/lib/workspace/application/view/view_ext.dart

@@ -40,6 +40,19 @@ extension ViewExtension on ViewPB {
     return widget;
   }
 
+  PluginType get pluginType {
+    switch (layout) {
+      case ViewLayoutTypePB.Board:
+        return PluginType.board;
+      case ViewLayoutTypePB.Document:
+        return PluginType.editor;
+      case ViewLayoutTypePB.Grid:
+        return PluginType.grid;
+    }
+
+    throw UnimplementedError;
+  }
+
   Plugin plugin() {
     final plugin = makePlugin(pluginType: pluginType, data: this);
     return plugin;

+ 1 - 0
frontend/app_flowy/lib/workspace/presentation/home/home_screen.dart

@@ -1,5 +1,6 @@
 import 'package:app_flowy/startup/plugin/plugin.dart';
 import 'package:app_flowy/workspace/application/home/home_bloc.dart';
+import 'package:app_flowy/workspace/application/view/view_ext.dart';
 import 'package:app_flowy/workspace/presentation/widgets/edit_panel/panel_animation.dart';
 import 'package:app_flowy/workspace/presentation/widgets/float_bubble/question_bubble.dart';
 import 'package:app_flowy/startup/startup.dart';

+ 1 - 1
frontend/app_flowy/lib/workspace/presentation/home/home_stack.dart

@@ -107,7 +107,7 @@ class HomeStackNotifier extends ChangeNotifier {
   Widget get titleWidget => _plugin.display.leftBarItem;
 
   HomeStackNotifier({Plugin? plugin})
-      : _plugin = plugin ?? makePlugin(pluginType: DefaultPlugin.blank.type());
+      : _plugin = plugin ?? makePlugin(pluginType: PluginType.blank);
 
   set plugin(Plugin newPlugin) {
     if (newPlugin.id == _plugin.id) {

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

@@ -114,7 +114,7 @@ class MenuAppHeader extends StatelessWidget {
                 LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
                 "",
                 pluginBuilder.dataType,
-                pluginBuilder.subDataType,
+                pluginBuilder.subDataType!,
                 pluginBuilder.pluginType,
               ));
         },

+ 4 - 0
frontend/app_flowy/linux/flutter/generated_plugin_registrant.cc

@@ -7,6 +7,7 @@
 #include "generated_plugin_registrant.h"
 
 #include <flowy_infra_ui/flowy_infra_u_i_plugin.h>
+#include <rich_clipboard_linux/rich_clipboard_plugin.h>
 #include <url_launcher_linux/url_launcher_plugin.h>
 #include <window_size/window_size_plugin.h>
 
@@ -14,6 +15,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
   g_autoptr(FlPluginRegistrar) flowy_infra_ui_registrar =
       fl_plugin_registry_get_registrar_for_plugin(registry, "FlowyInfraUIPlugin");
   flowy_infra_u_i_plugin_register_with_registrar(flowy_infra_ui_registrar);
+  g_autoptr(FlPluginRegistrar) rich_clipboard_linux_registrar =
+      fl_plugin_registry_get_registrar_for_plugin(registry, "RichClipboardPlugin");
+  rich_clipboard_plugin_register_with_registrar(rich_clipboard_linux_registrar);
   g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
       fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
   url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);

+ 1 - 0
frontend/app_flowy/linux/flutter/generated_plugins.cmake

@@ -4,6 +4,7 @@
 
 list(APPEND FLUTTER_PLUGIN_LIST
   flowy_infra_ui
+  rich_clipboard_linux
   url_launcher_linux
   window_size
 )

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

@@ -11,6 +11,7 @@ import flowy_infra_ui
 import flowy_sdk
 import package_info_plus_macos
 import path_provider_macos
+import rich_clipboard_macos
 import shared_preferences_macos
 import url_launcher_macos
 import window_size
@@ -22,6 +23,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
   FlowySdkPlugin.register(with: registry.registrar(forPlugin: "FlowySdkPlugin"))
   FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin"))
   PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
+  RichClipboardPlugin.register(with: registry.registrar(forPlugin: "RichClipboardPlugin"))
   SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
   UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
   WindowSizePlugin.register(with: registry.registrar(forPlugin: "WindowSizePlugin"))

+ 9 - 8
frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart

@@ -10,7 +10,7 @@ class MultiBoardListExample extends StatefulWidget {
 
 class _MultiBoardListExampleState extends State<MultiBoardListExample> {
   final AFBoardDataController boardDataController = AFBoardDataController(
-    onMoveColumn: (fromIndex, toIndex) {
+    onMoveColumn: (fromColumnId, fromIndex, toColumnId, toIndex) {
       debugPrint('Move column from $fromIndex to $toIndex');
     },
     onMoveColumnItem: (columnId, fromIndex, toIndex) {
@@ -23,18 +23,19 @@ class _MultiBoardListExampleState extends State<MultiBoardListExample> {
 
   @override
   void initState() {
-    final column1 = AFBoardColumnData(id: "To Do", items: [
+    List<AFColumnItem> a = [
       TextItem("Card 1"),
       TextItem("Card 2"),
-      RichTextItem(title: "Card 3", subtitle: 'Aug 1, 2020 4:05 PM'),
+      // RichTextItem(title: "Card 3", subtitle: 'Aug 1, 2020 4:05 PM'),
       TextItem("Card 4"),
-    ]);
-    final column2 = AFBoardColumnData(id: "In Progress", items: [
-      RichTextItem(title: "Card 5", subtitle: 'Aug 1, 2020 4:05 PM'),
-      TextItem("Card 6"),
+    ];
+    final column1 = AFBoardColumnData(id: "To Do", items: a);
+    final column2 = AFBoardColumnData(id: "In Progress", items: <AFColumnItem>[
+      // RichTextItem(title: "Card 5", subtitle: 'Aug 1, 2020 4:05 PM'),
+      // TextItem("Card 6"),
     ]);
 
-    final column3 = AFBoardColumnData(id: "Done", items: []);
+    final column3 = AFBoardColumnData(id: "Done", items: <AFColumnItem>[]);
 
     boardDataController.addColumn(column1);
     boardDataController.addColumn(column2);

+ 1 - 1
frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart

@@ -6,7 +6,7 @@ const DART_LOG = "Dart_LOG";
 class Log {
   // static const enableLog = bool.hasEnvironment(DART_LOG);
   // static final shared = Log();
-  static const enableLog = true;
+  static const enableLog = false;
 
   static void info(String? message) {
     if (enableLog) {

+ 7 - 3
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart

@@ -3,7 +3,7 @@ import 'package:provider/provider.dart';
 import 'board_column/board_column.dart';
 import 'board_column/board_column_data.dart';
 import 'board_data.dart';
-import 'reorder_flex/drag_target_inteceptor.dart';
+import 'reorder_flex/drag_target_interceptor.dart';
 import 'reorder_flex/reorder_flex.dart';
 import 'reorder_phantom/phantom_controller.dart';
 import '../rendering/board_overlay.dart';
@@ -46,6 +46,8 @@ class AFBoard extends StatelessWidget {
   ///
   final BoardPhantomController phantomController;
 
+  final ScrollController? scrollController;
+
   final AFBoardConfig config;
 
   AFBoard({
@@ -54,6 +56,7 @@ class AFBoard extends StatelessWidget {
     this.background,
     this.footBuilder,
     this.headerBuilder,
+    this.scrollController,
     this.columnConstraints = const BoxConstraints(maxWidth: 200),
     this.config = const AFBoardConfig(),
     Key? key,
@@ -69,6 +72,7 @@ class AFBoard extends StatelessWidget {
           return BoardContent(
             config: config,
             dataController: dataController,
+            scrollController: scrollController,
             background: background,
             delegate: phantomController,
             columnConstraints: columnConstraints,
@@ -139,7 +143,7 @@ class _BoardContentState extends State<BoardContent> {
   void initState() {
     _overlayEntry = BoardOverlayEntry(
       builder: (BuildContext context) {
-        final interceptor = OverlappingDragTargetInteceptor(
+        final interceptor = OverlappingDragTargetInterceptor(
           reorderFlexId: widget.dataController.identifier,
           acceptedReorderFlexId: widget.dataController.columnIds,
           delegate: widget.delegate,
@@ -202,7 +206,7 @@ class _BoardContentState extends State<BoardContent> {
         return ChangeNotifierProvider.value(
           key: ValueKey(columnData.id),
           value: widget.dataController.columnController(columnData.id),
-          child: Consumer<BoardColumnDataController>(
+          child: Consumer<AFBoardColumnDataController>(
             builder: (context, value, child) {
               return ConstrainedBox(
                 constraints: widget.columnConstraints,

+ 2 - 2
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart

@@ -5,7 +5,7 @@ import '../../rendering/board_overlay.dart';
 import '../../utils/log.dart';
 import '../reorder_phantom/phantom_controller.dart';
 import '../reorder_flex/reorder_flex.dart';
-import '../reorder_flex/drag_target_inteceptor.dart';
+import '../reorder_flex/drag_target_interceptor.dart';
 import 'board_column_data.dart';
 
 typedef OnColumnDragStarted = void Function(int index);
@@ -37,7 +37,7 @@ typedef AFBoardColumnFooterBuilder = Widget Function(
   AFBoardColumnData columnData,
 );
 
-abstract class AFBoardColumnDataDataSource extends ReoderFlextDataSource {
+abstract class AFBoardColumnDataDataSource extends ReoderFlexDataSource {
   AFBoardColumnData get columnData;
 
   List<String> get acceptedColumnIds;

+ 38 - 20
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart

@@ -12,7 +12,7 @@ abstract class AFColumnItem extends ReoderFlexItem {
   String toString() => id;
 }
 
-/// [BoardColumnDataController] is used to handle the [AFBoardColumnData].
+/// [AFBoardColumnDataController] is used to handle the [AFBoardColumnData].
 /// * Remove an item by calling [removeAt] method.
 /// * Move item to another position by calling [move] method.
 /// * Insert item to index by calling [insert] method
@@ -20,10 +20,10 @@ abstract class AFColumnItem extends ReoderFlexItem {
 ///
 /// All there operations will notify listeners by default.
 ///
-class BoardColumnDataController extends ChangeNotifier with EquatableMixin {
+class AFBoardColumnDataController extends ChangeNotifier with EquatableMixin {
   final AFBoardColumnData columnData;
 
-  BoardColumnDataController({
+  AFBoardColumnDataController({
     required this.columnData,
   });
 
@@ -42,7 +42,8 @@ class BoardColumnDataController extends ChangeNotifier with EquatableMixin {
   AFColumnItem removeAt(int index, {bool notify = true}) {
     assert(index >= 0);
 
-    Log.debug('[$BoardColumnDataController] $columnData remove item at $index');
+    Log.debug(
+        '[$AFBoardColumnDataController] $columnData remove item at $index');
     final item = columnData._items.removeAt(index);
     if (notify) {
       notifyListeners();
@@ -50,8 +51,11 @@ class BoardColumnDataController extends ChangeNotifier with EquatableMixin {
     return item;
   }
 
-  int removeWhere(bool Function(AFColumnItem) condition) {
-    return items.indexWhere(condition);
+  void removeWhere(bool Function(AFColumnItem) condition) {
+    final index = items.indexWhere(condition);
+    if (index != -1) {
+      removeAt(index);
+    }
   }
 
   /// Move the item from [fromIndex] to [toIndex]. It will do nothing if the
@@ -64,7 +68,7 @@ class BoardColumnDataController extends ChangeNotifier with EquatableMixin {
       return false;
     }
     Log.debug(
-        '[$BoardColumnDataController] $columnData move item from $fromIndex to $toIndex');
+        '[$AFBoardColumnDataController] $columnData move item from $fromIndex to $toIndex');
     final item = columnData._items.removeAt(fromIndex);
     columnData._items.insert(toIndex, item);
     notifyListeners();
@@ -78,38 +82,51 @@ class BoardColumnDataController extends ChangeNotifier with EquatableMixin {
   bool insert(int index, AFColumnItem item, {bool notify = true}) {
     assert(index >= 0);
     Log.debug(
-        '[$BoardColumnDataController] $columnData insert $item at $index');
+        '[$AFBoardColumnDataController] $columnData insert $item at $index');
 
-    if (columnData._items.length > index) {
-      columnData._items.insert(index, item);
+    if (_containsItem(item)) {
+      return false;
     } else {
-      columnData._items.add(item);
+      if (columnData._items.length > index) {
+        columnData._items.insert(index, item);
+      } else {
+        columnData._items.add(item);
+      }
+
+      if (notify) notifyListeners();
+      return true;
     }
-
-    if (notify) notifyListeners();
-    return true;
   }
 
   bool add(AFColumnItem item, {bool notify = true}) {
-    columnData._items.add(item);
-    if (notify) notifyListeners();
-    return true;
+    if (_containsItem(item)) {
+      return false;
+    } else {
+      columnData._items.add(item);
+      if (notify) notifyListeners();
+      return true;
+    }
   }
 
   /// Replace the item at index with the [newItem].
   void replace(int index, AFColumnItem newItem) {
     if (columnData._items.isEmpty) {
       columnData._items.add(newItem);
-      Log.debug('[$BoardColumnDataController] $columnData add $newItem');
+      Log.debug('[$AFBoardColumnDataController] $columnData add $newItem');
     } else {
       final removedItem = columnData._items.removeAt(index);
       columnData._items.insert(index, newItem);
       Log.debug(
-          '[$BoardColumnDataController] $columnData replace $removedItem with $newItem at $index');
+          '[$AFBoardColumnDataController] $columnData replace $removedItem with $newItem at $index');
     }
 
     notifyListeners();
   }
+
+  bool _containsItem(AFColumnItem item) {
+    return columnData._items.indexWhere((element) => element.id == item.id) !=
+        -1;
+  }
 }
 
 /// [AFBoardColumnData] represents the data of each Column of the Board.
@@ -128,7 +145,8 @@ class AFBoardColumnData<CustomData> extends ReoderFlexItem with EquatableMixin {
   }) : _items = items;
 
   /// Returns the readonly List<ColumnItem>
-  UnmodifiableListView<AFColumnItem> get items => UnmodifiableListView(_items);
+  UnmodifiableListView<AFColumnItem> get items =>
+      UnmodifiableListView([..._items]);
 
   @override
   List<Object?> get props => [id, ..._items];

+ 17 - 10
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart

@@ -8,7 +8,12 @@ import 'reorder_flex/reorder_flex.dart';
 import 'package:flutter/material.dart';
 import 'reorder_phantom/phantom_controller.dart';
 
-typedef OnMoveColumn = void Function(int fromIndex, int toIndex);
+typedef OnMoveColumn = void Function(
+  String fromColumnId,
+  int fromIndex,
+  String toColumnId,
+  int toIndex,
+);
 
 typedef OnMoveColumnItem = void Function(
   String columnId,
@@ -24,7 +29,7 @@ typedef OnMoveColumnItemToColumn = void Function(
 );
 
 class AFBoardDataController extends ChangeNotifier
-    with EquatableMixin, BoardPhantomControllerDelegate, ReoderFlextDataSource {
+    with EquatableMixin, BoardPhantomControllerDelegate, ReoderFlexDataSource {
   final List<AFBoardColumnData> _columnDatas = [];
   final OnMoveColumn? onMoveColumn;
   final OnMoveColumnItem? onMoveColumnItem;
@@ -35,7 +40,7 @@ class AFBoardDataController extends ChangeNotifier
   List<String> get columnIds =>
       _columnDatas.map((columnData) => columnData.id).toList();
 
-  final LinkedHashMap<String, BoardColumnDataController> _columnControllers =
+  final LinkedHashMap<String, AFBoardColumnDataController> _columnControllers =
       LinkedHashMap();
 
   AFBoardDataController({
@@ -47,7 +52,7 @@ class AFBoardDataController extends ChangeNotifier
   void addColumn(AFBoardColumnData columnData, {bool notify = true}) {
     if (_columnControllers[columnData.id] != null) return;
 
-    final controller = BoardColumnDataController(columnData: columnData);
+    final controller = AFBoardColumnDataController(columnData: columnData);
     _columnDatas.add(columnData);
     _columnControllers[columnData.id] = controller;
     if (notify) notifyListeners();
@@ -84,11 +89,11 @@ class AFBoardDataController extends ChangeNotifier
     if (columnIds.isNotEmpty && notify) notifyListeners();
   }
 
-  BoardColumnDataController columnController(String columnId) {
+  AFBoardColumnDataController columnController(String columnId) {
     return _columnControllers[columnId]!;
   }
 
-  BoardColumnDataController? getColumnController(String columnId) {
+  AFBoardColumnDataController? getColumnController(String columnId) {
     final columnController = _columnControllers[columnId];
     if (columnController == null) {
       Log.warn('Column:[$columnId] \'s controller is not exist');
@@ -98,9 +103,11 @@ class AFBoardDataController extends ChangeNotifier
   }
 
   void moveColumn(int fromIndex, int toIndex, {bool notify = true}) {
-    final columnData = _columnDatas.removeAt(fromIndex);
-    _columnDatas.insert(toIndex, columnData);
-    onMoveColumn?.call(fromIndex, toIndex);
+    final toColumnData = _columnDatas[toIndex];
+    final fromColumnData = _columnDatas.removeAt(fromIndex);
+
+    _columnDatas.insert(toIndex, fromColumnData);
+    onMoveColumn?.call(fromColumnData.id, fromIndex, toColumnData.id, toIndex);
     if (notify) notifyListeners();
   }
 
@@ -153,7 +160,7 @@ class AFBoardDataController extends ChangeNotifier
   }
 
   @override
-  BoardColumnDataController? controller(String columnId) {
+  AFBoardColumnDataController? controller(String columnId) {
     return _columnControllers[columnId];
   }
 

+ 5 - 5
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart

@@ -13,14 +13,14 @@ abstract class ReorderFlexDraggableTargetBuilder {
     Widget child,
     DragTargetOnStarted onDragStarted,
     DragTargetOnEnded<T> onDragEnded,
-    DragTargetWillAccpet<T> onWillAccept,
+    DragTargetWillAccepted<T> onWillAccept,
     AnimationController insertAnimationController,
     AnimationController deleteAnimationController,
   );
 }
 
 ///
-typedef DragTargetWillAccpet<T extends DragTargetData> = bool Function(
+typedef DragTargetWillAccepted<T extends DragTargetData> = bool Function(
     T dragTargetData);
 
 ///
@@ -51,7 +51,7 @@ class ReorderDragTarget<T extends DragTargetData> extends StatefulWidget {
   ///
   /// [toAccept] represents the dragTarget index, which is the value passed in
   /// when creating the [ReorderDragTarget].
-  final DragTargetWillAccpet<T> onWillAccept;
+  final DragTargetWillAccepted<T> onWillAccept;
 
   /// Called when an acceptable piece of data was dropped over this drag target.
   ///
@@ -228,7 +228,7 @@ class DragTargetAnimation {
         value: 0.0, vsync: vsync, duration: const Duration(milliseconds: 10));
   }
 
-  void startDargging() {
+  void startDragging() {
     entranceController.value = 1.0;
   }
 
@@ -386,7 +386,7 @@ class FakeDragTarget<T extends DragTargetData> extends StatefulWidget {
   final FakeDragTargetEventData eventData;
   final DragTargetOnStarted onDragStarted;
   final DragTargetOnEnded<T> onDragEnded;
-  final DragTargetWillAccpet<T> onWillAccept;
+  final DragTargetWillAccepted<T> onWillAccept;
   final Widget child;
   final AnimationController insertAnimationController;
   final AnimationController deleteAnimationController;

+ 3 - 3
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_inteceptor.dart → frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_interceptor.dart

@@ -40,18 +40,18 @@ abstract class OverlapDragTargetDelegate {
   bool canMoveTo(String dragTargetId);
 }
 
-/// [OverlappingDragTargetInteceptor] is used to receive the overlapping
+/// [OverlappingDragTargetInterceptor] is used to receive the overlapping
 /// [DragTarget] event. If a [DragTarget] child is [DragTarget], it will
 /// receive the [DragTarget] event when being dragged.
 ///
 /// Receive the [DragTarget] event if the [acceptedReorderFlexId] contains
 /// the passed in dragTarget' reorderFlexId.
-class OverlappingDragTargetInteceptor extends DragTargetInterceptor {
+class OverlappingDragTargetInterceptor extends DragTargetInterceptor {
   final String reorderFlexId;
   final List<String> acceptedReorderFlexId;
   final OverlapDragTargetDelegate delegate;
 
-  OverlappingDragTargetInteceptor({
+  OverlappingDragTargetInterceptor({
     required this.delegate,
     required this.reorderFlexId,
     required this.acceptedReorderFlexId,

+ 8 - 8
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart

@@ -7,25 +7,25 @@ import '../../utils/log.dart';
 import 'reorder_mixin.dart';
 import 'drag_target.dart';
 import 'drag_state.dart';
-import 'drag_target_inteceptor.dart';
+import 'drag_target_interceptor.dart';
 
 typedef OnDragStarted = void Function(int index);
 typedef OnDragEnded = void Function();
 typedef OnReorder = void Function(int fromIndex, int toIndex);
 typedef OnDeleted = void Function(int deletedIndex);
 typedef OnInserted = void Function(int insertedIndex);
-typedef OnReveivePassedInPhantom = void Function(
+typedef OnReceivePassedInPhantom = void Function(
     FlexDragTargetData dragTargetData, int phantomIndex);
 
-abstract class ReoderFlextDataSource {
+abstract class ReoderFlexDataSource {
   /// [identifier] represents the id the [ReorderFlex]. It must be unique.
   String get identifier;
 
-  /// The number of [ReoderFlexItem]s will be displaied in the [ReorderFlex].
+  /// The number of [ReoderFlexItem]s will be displayed in the [ReorderFlex].
   UnmodifiableListView<ReoderFlexItem> get items;
 }
 
-/// Each item displaied in the [ReorderFlex] required to implement the [ReoderFlexItem].
+/// Each item displayed in the [ReorderFlex] required to implement the [ReoderFlexItem].
 abstract class ReoderFlexItem {
   /// [id] is used to identify the item. It must be unique.
   String get id;
@@ -70,7 +70,7 @@ class ReorderFlex extends StatefulWidget {
   /// [onDragEnded] is called when dragTarget did end dragging
   final OnDragEnded? onDragEnded;
 
-  final ReoderFlextDataSource dataSource;
+  final ReoderFlexDataSource dataSource;
 
   final DragTargetInterceptor? interceptor;
 
@@ -187,7 +187,7 @@ class ReorderFlexState extends State<ReorderFlex>
   void _requestAnimationToNextIndex({bool isAcceptingNewTarget = false}) {
     /// Update the dragState and animate to the next index if the current
     /// dragging animation is completed. Otherwise, it will get called again
-    /// when the animation finishs.
+    /// when the animation finish.
 
     if (_animation.entranceController.isCompleted) {
       dragState.removePhantom();
@@ -425,7 +425,7 @@ class ReorderFlexState extends State<ReorderFlex>
   ) {
     setState(() {
       dragState.startDragging(draggingWidget, dragIndex, feedbackSize);
-      _animation.startDargging();
+      _animation.startDragging();
     });
   }
 

+ 6 - 5
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart

@@ -1,13 +1,14 @@
-import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
+
 import '../../utils/log.dart';
 import '../board_column/board_column_data.dart';
 import '../reorder_flex/drag_state.dart';
 import '../reorder_flex/drag_target.dart';
-import '../reorder_flex/drag_target_inteceptor.dart';
+import '../reorder_flex/drag_target_interceptor.dart';
 import 'phantom_state.dart';
 
 abstract class BoardPhantomControllerDelegate {
-  BoardColumnDataController? controller(String columnId);
+  AFBoardColumnDataController? controller(String columnId);
 
   bool removePhantom(String columnId);
 
@@ -61,7 +62,7 @@ class BoardPhantomController extends OverlapDragTargetDelegate
     columnsState.setColumnIsDragging(columnId, false);
   }
 
-  /// Remove the phanton in the column when the column is end dragging.
+  /// Remove the phantom in the column when the column is end dragging.
   void columnEndDragging(String columnId) {
     columnsState.setColumnIsDragging(columnId, true);
     if (phantomRecord == null) return;
@@ -331,7 +332,7 @@ class PhantomDraggableBuilder extends ReorderFlexDraggableTargetBuilder {
     Widget child,
     DragTargetOnStarted onDragStarted,
     DragTargetOnEnded<T> onDragEnded,
-    DragTargetWillAccpet<T> onWillAccept,
+    DragTargetWillAccepted<T> onWillAccept,
     AnimationController insertAnimationController,
     AnimationController deleteAnimationController,
   ) {

+ 2 - 1
frontend/app_flowy/packages/flowy_editor/.gitignore → frontend/app_flowy/packages/appflowy_editor/.gitignore

@@ -19,7 +19,7 @@ migrate_working_dir/
 # The .vscode folder contains launch configuration and tasks you configure in
 # VS Code which you may wish to be included in version control, so this line
 # is commented out by default.
-#.vscode/
+.vscode/
 
 # Flutter/Dart/Pub related
 # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
@@ -28,3 +28,4 @@ migrate_working_dir/
 .dart_tool/
 .packages
 build/
+coverage/

+ 0 - 0
frontend/app_flowy/packages/flowy_editor/.metadata → frontend/app_flowy/packages/appflowy_editor/.metadata


+ 5 - 0
frontend/app_flowy/packages/appflowy_editor/CHANGELOG.md

@@ -0,0 +1,5 @@
+## 0.0.2
+Minor Updates to Documentation.
+
+## 0.0.1
+Initial Version of the library.

+ 661 - 0
frontend/app_flowy/packages/appflowy_editor/LICENSE

@@ -0,0 +1,661 @@
+                    GNU AFFERO GENERAL PUBLIC LICENSE
+                       Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+  A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate.  Many developers of free software are heartened and
+encouraged by the resulting cooperation.  However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+  The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community.  It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server.  Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+  An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals.  This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU Affero General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Remote Network Interaction; Use with the GNU General Public License.
+
+  Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software.  This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time.  Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as published
+    by the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source.  For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code.  There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+<https://www.gnu.org/licenses/>.

+ 97 - 0
frontend/app_flowy/packages/appflowy_editor/README.md

@@ -0,0 +1,97 @@
+<!-- 
+This README describes the package. If you publish this package to pub.dev,
+this README's contents appear on the landing page for your package.
+
+For information about how to write a good package README, see the guide for
+[writing package pages](https://dart.dev/guides/libraries/writing-package-pages). 
+
+For general information about developing packages, see the Dart guide for
+[creating packages](https://dart.dev/guides/libraries/create-library-packages)
+and the Flutter guide for
+[developing packages and plugins](https://flutter.dev/developing-packages). 
+-->
+
+<h1 align="center"><b>AppFlowy Editor</b></h1>
+
+<p align="center">A highly customizable rich-text editor for Flutter</p>
+
+<p align="center">
+    <a href="https://discord.gg/ZCCYN4Anzq"><b>Discord</b></a> •
+    <a href="https://twitter.com/appflowy"><b>Twitter</b></a>
+</p>
+
+<div align="center">
+    <img src="https://raw.githubusercontent.com/AppFlowy-IO/AppFlowy/main/frontend/app_flowy/packages/appflowy_editor/documentation/images/example.png" width = "900"/>
+</div>
+
+## Key Features
+
+* Allow you to build rich, intuitive editors
+* Design and modify it your way by customizing components, shortcut events, and many more coming soon including menu options and themes
+* [Test-covered](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/documentation/testing.md) and maintained by AppFlowy's core team along with a community of more than 1,000 builders
+
+
+## Getting started
+
+```shell
+flutter pub add appflowy_editor
+flutter pub get
+```
+
+## How to use
+
+Let's create a new AppFlowyEditor object 
+```dart
+final editorState = EditorState.empty(); // an empty state
+final editor = AppFlowyEditor(
+    editorState: editorState,
+    keyEventHandlers: const [],
+    customBuilders: const {},
+);
+```
+
+You can also create an editor from a JSON file
+```dart
+final json = ...;
+final editorState = EditorState(StateTree.fromJson(data));
+final editor = AppFlowyEditor(
+    editorState: editorState,
+    keyEventHandlers: const [],
+    customBuilders: const {},
+);
+```
+
+To get a sense for how you might use it, run this example:
+```shell
+git clone https://github.com/AppFlowy-IO/AppFlowy.git
+cd frontend/app_flowy/packages/appflowy_editor/example
+flutter run
+```
+
+
+## How to customize 
+### Customize a component
+Please refer to [customizing a component](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/documentation/customizing.md#customize-a-component) for more details.
+
+
+### Customize a shortcut event
+Please refer to [customizing a shortcut event](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/documentation/customizing.md#customize-a-shortcut-event) for more details.
+
+## More Examples
+* Customize a component
+    * [Checkbox Text](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart) shows you how to extend new styles based on existing rich text components
+    * [Image](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/network_image_node_widget.dart) teaches you how to extend a new node and render it
+    * And more examples on [rich-text plugins](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text)
+* Customize a shortcut event
+    * [BIUS](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart) shows you how to make text bold/italic/underline/strikethrough through shortcut keys
+    * [Paste HTML](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart) gives you an idea on how to handle pasted styles through shortcut keys
+    * Need more examples? Check out [Internal key event handlers](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers)
+
+## Glossary
+Please refer to the API documentation.
+
+## Contributing
+Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are greatly appreciated. Please look at [CONTRIBUTING.md](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/contributing-to-appflowy) for details.
+
+## License
+Distributed under the AGPLv3 License. See LICENSE for more information.

+ 0 - 0
frontend/app_flowy/packages/flowy_editor/analysis_options.yaml → frontend/app_flowy/packages/appflowy_editor/analysis_options.yaml


+ 0 - 0
frontend/app_flowy/packages/flowy_editor/assets/images/check.svg → frontend/app_flowy/packages/appflowy_editor/assets/images/check.svg


+ 0 - 0
frontend/app_flowy/packages/flowy_editor/assets/images/point.svg → frontend/app_flowy/packages/appflowy_editor/assets/images/point.svg


+ 0 - 0
frontend/app_flowy/packages/flowy_editor/assets/images/quote.svg → frontend/app_flowy/packages/appflowy_editor/assets/images/quote.svg


+ 0 - 0
frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/bullets.svg → frontend/app_flowy/packages/appflowy_editor/assets/images/selection_menu/bulleted_list.svg


+ 0 - 0
frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/checkbox.svg → frontend/app_flowy/packages/appflowy_editor/assets/images/selection_menu/checkbox.svg


+ 0 - 0
frontend/app_flowy/packages/flowy_editor/assets/images/popup_list/h1.svg → frontend/app_flowy/packages/appflowy_editor/assets/images/selection_menu/h1.svg


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