Prechádzať zdrojové kódy

Merge branch 'main' into feat/flowy-overlay

Vincent Chan 2 rokov pred
rodič
commit
9b5184cd72
100 zmenil súbory, kde vykonal 1915 pridanie a 426 odobranie
  1. 1 0
      .github/workflows/ci.yaml
  2. 4 0
      .github/workflows/flowy_editor_test.yml
  3. 1 0
      .github/workflows/release.yml
  4. 30 7
      CHANGELOG.md
  5. 38 0
      frontend/.vscode/launch.json
  6. 27 0
      frontend/.vscode/tasks.json
  7. 6 1
      frontend/Makefile.toml
  8. 64 0
      frontend/app_flowy/android/README.md
  9. 10 3
      frontend/app_flowy/android/app/build.gradle
  10. 2 1
      frontend/app_flowy/android/app/src/main/AndroidManifest.xml
  11. 1 1
      frontend/app_flowy/android/build.gradle
  12. 1 0
      frontend/app_flowy/android/gradle.properties
  13. 1 2
      frontend/app_flowy/android/gradle/wrapper/gradle-wrapper.properties
  14. 16 0
      frontend/app_flowy/android/settings.gradle
  15. 4 0
      frontend/app_flowy/assets/translations/ca-ES.json
  16. 4 0
      frontend/app_flowy/assets/translations/de-DE.json
  17. 15 3
      frontend/app_flowy/assets/translations/en.json
  18. 4 0
      frontend/app_flowy/assets/translations/es-VE.json
  19. 4 0
      frontend/app_flowy/assets/translations/fr-CA.json
  20. 4 0
      frontend/app_flowy/assets/translations/fr-FR.json
  21. 4 0
      frontend/app_flowy/assets/translations/hu-HU.json
  22. 4 0
      frontend/app_flowy/assets/translations/id-ID.json
  23. 4 0
      frontend/app_flowy/assets/translations/it-IT.json
  24. 4 0
      frontend/app_flowy/assets/translations/ja-JP.json
  25. 4 0
      frontend/app_flowy/assets/translations/pl-PL.json
  26. 4 0
      frontend/app_flowy/assets/translations/pt-BR.json
  27. 4 0
      frontend/app_flowy/assets/translations/pt-PT.json
  28. 4 0
      frontend/app_flowy/assets/translations/ru-RU.json
  29. 4 0
      frontend/app_flowy/assets/translations/tr-TR.json
  30. 80 3
      frontend/app_flowy/assets/translations/zh-CN.json
  31. 4 0
      frontend/app_flowy/assets/translations/zh-TW.json
  32. 4 3
      frontend/app_flowy/lib/core/frameless_window.dart
  33. 4 0
      frontend/app_flowy/lib/main.dart
  34. 145 52
      frontend/app_flowy/lib/plugins/board/application/board_bloc.dart
  35. 38 6
      frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart
  36. 50 0
      frontend/app_flowy/lib/plugins/board/application/board_listener.dart
  37. 1 1
      frontend/app_flowy/lib/plugins/board/application/card/board_date_cell_bloc.dart
  38. 0 1
      frontend/app_flowy/lib/plugins/board/application/card/board_select_option_cell_bloc.dart
  39. 14 0
      frontend/app_flowy/lib/plugins/board/application/card/board_text_cell_bloc.dart
  40. 41 29
      frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart
  41. 44 17
      frontend/app_flowy/lib/plugins/board/application/group_controller.dart
  42. 2 2
      frontend/app_flowy/lib/plugins/board/application/group_listener.dart
  43. 46 0
      frontend/app_flowy/lib/plugins/board/application/toolbar/board_setting_bloc.dart
  44. 1 1
      frontend/app_flowy/lib/plugins/board/board.dart
  45. 199 51
      frontend/app_flowy/lib/plugins/board/presentation/board_page.dart
  46. 62 0
      frontend/app_flowy/lib/plugins/board/presentation/card/board_cell.dart
  47. 4 0
      frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart
  48. 6 1
      frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart
  49. 4 1
      frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart
  50. 60 14
      frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart
  51. 112 21
      frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart
  52. 3 0
      frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart
  53. 85 15
      frontend/app_flowy/lib/plugins/board/presentation/card/card.dart
  54. 17 1
      frontend/app_flowy/lib/plugins/board/presentation/card/card_cell_builder.dart
  55. 34 10
      frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart
  56. 3 0
      frontend/app_flowy/lib/plugins/board/presentation/card/define.dart
  57. 168 0
      frontend/app_flowy/lib/plugins/board/presentation/toolbar/board_setting.dart
  58. 60 0
      frontend/app_flowy/lib/plugins/board/presentation/toolbar/board_toolbar.dart
  59. 4 4
      frontend/app_flowy/lib/plugins/doc/presentation/banner.dart
  60. 9 4
      frontend/app_flowy/lib/plugins/doc/presentation/style_widgets.dart
  61. 2 2
      frontend/app_flowy/lib/plugins/doc/presentation/toolbar/check_button.dart
  62. 42 22
      frontend/app_flowy/lib/plugins/doc/presentation/toolbar/color_picker.dart
  63. 21 14
      frontend/app_flowy/lib/plugins/doc/presentation/toolbar/header_button.dart
  64. 5 3
      frontend/app_flowy/lib/plugins/doc/presentation/toolbar/link_button.dart
  65. 5 3
      frontend/app_flowy/lib/plugins/doc/presentation/toolbar/toggle_button.dart
  66. 11 6
      frontend/app_flowy/lib/plugins/doc/presentation/toolbar/tool_bar.dart
  67. 4 2
      frontend/app_flowy/lib/plugins/grid/application/cell/cell_listener.dart
  68. 8 1
      frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_cache.dart
  69. 16 12
      frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_data_loader.dart
  70. 1 1
      frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_service.dart
  71. 5 2
      frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart
  72. 3 3
      frontend/app_flowy/lib/plugins/grid/application/cell/date_cal_bloc.dart
  73. 1 1
      frontend/app_flowy/lib/plugins/grid/application/cell/date_cell_bloc.dart
  74. 4 2
      frontend/app_flowy/lib/plugins/grid/application/cell/select_option_editor_bloc.dart
  75. 2 1
      frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart
  76. 9 1
      frontend/app_flowy/lib/plugins/grid/application/grid_service.dart
  77. 4 2
      frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart
  78. 2 2
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_accessory.dart
  79. 1 1
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_container.dart
  80. 3 4
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_editor.dart
  81. 5 2
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart
  82. 2 2
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart
  83. 1 1
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart
  84. 1 1
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/text_field.dart
  85. 1 1
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/cell_editor.dart
  86. 6 1
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/grid_header.dart
  87. 2 2
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/number.dart
  88. 8 2
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart
  89. 1 1
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_action_sheet.dart
  90. 70 18
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart
  91. 0 1
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_setting.dart
  92. 1 1
      frontend/app_flowy/lib/plugins/trash/trash.dart
  93. 27 31
      frontend/app_flowy/lib/startup/tasks/app_widget.dart
  94. 13 6
      frontend/app_flowy/lib/user/presentation/router.dart
  95. 1 1
      frontend/app_flowy/lib/user/presentation/sign_in_screen.dart
  96. 1 1
      frontend/app_flowy/lib/user/presentation/sign_up_screen.dart
  97. 9 10
      frontend/app_flowy/lib/workspace/application/markdown/src/inline_parser.dart
  98. 6 2
      frontend/app_flowy/lib/workspace/presentation/home/home_screen.dart
  99. 2 2
      frontend/app_flowy/lib/workspace/presentation/home/home_stack.dart
  100. 32 0
      frontend/app_flowy/lib/workspace/presentation/home/hotkeys.dart

+ 1 - 0
.github/workflows/ci.yaml

@@ -59,6 +59,7 @@ jobs:
             sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list
             sudo apt-get update
             sudo apt-get install -y dart curl build-essential libsqlite3-dev libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev
+            sudo apt-get install keybinder-3.0
           elif [ "$RUNNER_OS" == "macOS" ]; then
             echo 'do nothing'
           fi

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

@@ -4,10 +4,14 @@ on:
   push:
     branches:
       - "main"
+    paths:
+      - "frontend/app_flowy/packages/appflowy_editor"
 
   pull_request:
     branches:
       - "main"
+    paths:
+      - "frontend/app_flowy/packages/appflowy_editor"
 
 env:
   CARGO_TERM_COLOR: always

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

@@ -58,6 +58,7 @@ jobs:
           sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
           sudo apt-get update
           sudo apt-get install -y build-essential libsqlite3-dev libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev
+          sudo apt-get install keybinder-3.0
           source $HOME/.cargo/env
           cargo install --force cargo-make
           cargo install --force duckscript_cli

+ 30 - 7
CHANGELOG.md

@@ -1,6 +1,29 @@
 # Release Notes
 
-## Version 0.0.4 - 2022-06-06
+## Version 0.0.5 - beta.2 - beta.1 - 09/01/2022
+
+New features
+- Board-view database
+  - Support start editing after creating a new card
+  - Support editing the card directly by clicking the edit button
+  - Add the `No Status` column to display the cards while their status is empty
+
+### Bug Fixes
+- Optimize insert card animation
+- Fix some UI bugs
+
+## Version 0.0.5 - beta.1 - 08/25/2022
+
+New features
+- Board-view database  
+  - Group by single select
+  - drag and drop cards
+  - insert / delete cards
+
+![Aug-25-2022 16-22-38](https://user-images.githubusercontent.com/86001920/186614248-23186dfe-410e-427a-8cc6-865b1f79e074.gif)
+
+
+## Version 0.0.4 - 06/06/2022
 - Drag to adjust the width of a column
 - Upgrade to Flutter 3.0
 - Native support for M1 chip
@@ -12,12 +35,12 @@
 - Fixed some bugs
 
 
-## Version 0.0.4 - beta.3 - 2022-05-02
+## Version 0.0.4 - beta.3 - 05/02/2022
 - Drag to reorder app/ view/ field
 - Row record open as a page
 - Auto resize the height of the row in the grid
 - Support more number formats
-- Search column options, supporting Single select, Multi-select, and number format
+- Search column options, supporting Single-select, Multi-select, and number format
 
 ![May-03-2022 10-03-00](https://user-images.githubusercontent.com/86001920/166394640-a8f1f3bc-5f20-4033-93e9-16bc308d7005.gif)
 
@@ -27,7 +50,7 @@
 - Fixed some bugs
 
 
-## Version 0.0.4 - beta.2 - 2022-04-11
+## Version 0.0.4 - beta.2 - 04/11/2022
 
   - Support properties: Text, Number, Date, Checkbox, Select, Multi-select
   - Insert / delete rows
@@ -35,16 +58,16 @@
   - Edit property
     ![](https://user-images.githubusercontent.com/12026239/162753644-bf2f4e7a-2367-4d48-87e6-35e244e83a5b.png)
 
-## Version 0.0.4 - beta.1 - 2022-04-08
+## Version 0.0.4 - beta.1 - 04/08/2022
 v0.0.4 - beta.1 is pre-release
 
 New features
 - Table-view database
-   - supported column types: Text, Checbox, Single-select, Multi-select, Numbers
+   - supported column types: Text, Checkbox, Single-select, Multi-select, Numbers
    - hide / delete columns
    - insert rows
 
-## Version 0.0.3 - 2022-02-23
+## Version 0.0.3 - 02/23/2022
 v0.0.3 is production ready, available on Linux, macOS, and Windows
 
 New features

+ 38 - 0
frontend/.vscode/launch.json

@@ -16,6 +16,18 @@
             },
             "cwd": "${workspaceRoot}/app_flowy"
         },
+        {
+            // This task builds the Rust and Dart code of AppFlowy for android.
+            "name": "AF: Run Android",
+            "request": "launch",
+            "program": "./lib/main.dart",
+            "type": "dart",
+            "preLaunchTask": "AF: build_mobile_sdk",
+            "env": {
+                "RUST_LOG": "info"
+            },
+            "cwd": "${workspaceRoot}/app_flowy"
+        },
         {
             "name": "AF: Debug Rust",
             "request": "attach",
@@ -48,6 +60,21 @@
             },
             "cwd": "${workspaceRoot}/app_flowy"
         },
+        {
+            // This task builds will:
+            // - call the clean task,
+            // - rebuild all the generated Files (including freeze and language files)
+            // - rebuild the the Rust and Dart code of AppFlowy.
+            "name": "AF: Clean + Rebuild All (Android)",
+            "request": "launch",
+            "program": "./lib/main.dart",
+            "type": "dart",
+            "preLaunchTask": "AF: Clean + Rebuild All (Android)",
+            "env": {
+                "RUST_LOG": "info"
+            },
+            "cwd": "${workspaceRoot}/app_flowy"
+        },
         {
             "name": "AF: Build All (rustlog: trace)",
             "request": "launch",
@@ -59,6 +86,17 @@
             },
             "cwd": "${workspaceRoot}/app_flowy"
         },
+        {
+            "name": "AF: Build All Android (rustlog: trace)",
+            "request": "launch",
+            "program": "./lib/main.dart",
+            "type": "dart",
+            "preLaunchTask": "AF: build_mobile_sdk",
+            "env": {
+                "RUST_LOG": "trace"
+            },
+            "cwd": "${workspaceRoot}/app_flowy"
+        },
         {
             "name": "AF: app_flowy (profile mode)",
             "request": "launch",

+ 27 - 0
frontend/.vscode/tasks.json

@@ -27,6 +27,33 @@
 				"panel": "new"
 			}
 		},
+		{
+			"label": "AF: Clean + Rebuild All (Android)",
+			"type": "shell",
+			"dependsOrder": "sequence",
+			"dependsOn": [
+				"AF: Rust Clean",
+				"AF: Flutter Clean",
+				"AF: build_flowy_sdk_for_android",
+				"AF: Flutter Pub Get",
+				"AF: Flutter Package Get",
+				"AF: Generate Language Files",
+				"AF: Generate Freezed Files",
+			],
+			"presentation": {
+				"reveal": "always",
+				"panel": "new",
+			},
+		},
+		{
+			"label": "AF: build_flowy_sdk_for_android",
+			"type": "shell",
+			"command": "cargo make --profile development-android flowy-sdk-dev-android",
+			"group": "build",
+			"options": {
+				"cwd": "${workspaceFolder}"
+			}
+		},
 		{
 			"label": "AF: build_flowy_sdk",
 			"type": "shell",

+ 6 - 1
frontend/Makefile.toml

@@ -22,7 +22,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
 CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
 CARGO_MAKE_CRATE_NAME = "dart-ffi"
 LIB_NAME = "dart_ffi"
-CURRENT_APP_VERSION = "0.0.4"
+CURRENT_APP_VERSION = "0.0.5"
 FEATURES = "flutter"
 PRODUCT_NAME = "AppFlowy"
 # CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html
@@ -161,6 +161,11 @@ TARGET_OS = "ios"
 FLUTTER_OUTPUT_DIR = "Release"
 PRODUCT_EXT = "ipa"
 
+[env.development-android]
+BUILD_FLAG = "debug"
+TARGET_OS = "android"
+CRATE_TYPE = "cdylib"
+FLUTTER_OUTPUT_DIR = "Debug"
 
 [tasks.setup-crate-type]
 private = true

+ 64 - 0
frontend/app_flowy/android/README.md

@@ -0,0 +1,64 @@
+# Description
+
+This is a guide on how to build the rust SDK for AppFlowy on android.
+Compiling the sdk is easy it just needs a few tweaks.
+When compiling for android we need the following pre-requisites:
+
+- Android NDK Tools. (v24 has been tested).
+- Cargo NDK. (@latest version).
+
+**Getting the tools**
+- Install cargo-ndk ```bash cargo install cargo-ndk```.
+- [Download](https://developer.android.com/ndk/downloads/) Android NDK version 24.
+- When downloading Android NDK you can get the compressed version as a standalone from the site.
+    Or you can download it through [Android Studio](https://developer.android.com/studio).
+- After downloading the two you need to set the environment variables. For Windows that's a seperate process.
+    On MacOs and Linux the process is similar.
+- The variables needed are '$ANDROID_NDK_HOME', this will point to where the NDK is located.
+---
+
+**Cargo Config File**
+This code needs to be written in ~/.cargo/config, this helps cargo know where to locate the android tools(linker and archiver).
+**NB** Keep in mind just replace 'user' with your own user name. Or just point it to the location of where you put the NDK.
+
+```toml
+[target.aarch64-linux-android]
+ar = "/home/user/Android/Sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
+linker = "/home/user/Android/Sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android29-clang"
+
+[target.armv7-linux-androideabi]
+ar = "/home/user/Android/Sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
+linker = "/home/user/Android/Sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi29-clang"
+
+[target.i686-linux-android]
+ar = "/home/user/Android/Sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
+linker = "/home/user/Android/Sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/linux-x86_64/bin/i686-linux-android29-clang"
+
+[target.x86_64-linux-android]
+ar = "/home/user/Android/Sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
+linker = "/home/user/Android/Sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android29-clang"
+```
+
+**Clang Fix**
+ In order to get clang to work properly with version 24 you need to create this file.
+ libgcc.a, then add this one line.
+ ```
+ INPUT(-lunwind)
+ ```
+
+**Folder path: 'Android/Sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/linux-x86_64/lib64/clang/14.0.1/lib/linux'.**
+After that you have to copy this file into three different folders namely aarch64, arm, i386 and x86_64.
+We have to do this so we Android NDK can find clang on our system, if we used NDK 22 we wouldnt have to do this process.
+Though using NDK v22 will not give us alot of features to work with.
+This github [issue](https://github.com/fzyzcjy/flutter_rust_bridge/issues/419) explains the reason why we are doing this.
+
+ ---
+
+ **Android NDK**
+
+ After installing the NDK tools for android you should export the PATH to your config file
+ (.vimrc, .zshrc, .profile, .bashrc file), That way it can be found.
+
+ ```vim
+ export PATH=/home/sean/Android/Sdk/ndk/24.0.8215888
+ ```

+ 10 - 3
frontend/app_flowy/android/app/build.gradle

@@ -26,7 +26,8 @@ apply plugin: 'kotlin-android'
 apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
 
 android {
-    compileSdkVersion 30
+    compileSdkVersion 31
+    ndkVersion "24.0.8215888"
 
     compileOptions {
         sourceCompatibility JavaVersion.VERSION_1_8
@@ -39,21 +40,26 @@ android {
 
     sourceSets {
         main.java.srcDirs += 'src/main/kotlin'
+        main.jniLibs.srcDirs += 'jniLibs/'
     }
 
     defaultConfig {
         // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
         applicationId "com.example.app_flowy"
-        minSdkVersion 16
-        targetSdkVersion 30
+        minSdkVersion 19
+        targetSdkVersion 31
         versionCode flutterVersionCode.toInteger()
         versionName flutterVersionName
+        multiDexEnabled true
     }
 
     buildTypes {
         release {
             // TODO: Add your own signing config for the release build.
             // Signing with the debug keys for now, so `flutter run --release` works.
+            minifyEnabled true
+            shrinkResources true
+
             signingConfig signingConfigs.debug
         }
     }
@@ -65,4 +71,5 @@ flutter {
 
 dependencies {
     implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+    implementation "com.android.support:multidex:2.0.1"
 }

+ 2 - 1
frontend/app_flowy/android/app/src/main/AndroidManifest.xml

@@ -2,7 +2,8 @@
     package="com.example.app_flowy">
    <application
         android:label="app_flowy"
-        android:icon="@mipmap/ic_launcher">
+        android:icon="@mipmap/ic_launcher"
+        android:name="${applicationName}">
         <activity
             android:name=".MainActivity"
             android:launchMode="singleTop"

+ 1 - 1
frontend/app_flowy/android/build.gradle

@@ -1,5 +1,5 @@
 buildscript {
-    ext.kotlin_version = '1.3.50'
+    ext.kotlin_version = '1.6.10'
     repositories {
         google()
         mavenCentral()

+ 1 - 0
frontend/app_flowy/android/gradle.properties

@@ -1,3 +1,4 @@
 org.gradle.jvmargs=-Xmx1536M
 android.useAndroidX=true
 android.enableJetifier=true
+org.gradle.caching=true

+ 1 - 2
frontend/app_flowy/android/gradle/wrapper/gradle-wrapper.properties

@@ -1,6 +1,5 @@
-#Fri Jun 23 08:50:38 CEST 2017
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-all.zip
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip

+ 16 - 0
frontend/app_flowy/android/settings.gradle

@@ -9,3 +9,19 @@ localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
 def flutterSdkPath = properties.getProperty("flutter.sdk")
 assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
 apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
+
+
+def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
+
+def plugins = new Properties()
+def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
+
+if(pluginsFile.exists()){
+    pluginsFile.withReader('UTF-8'){reader -> plugins.load(reader)}
+}
+
+plugins.each{name, path ->
+        def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
+        include ":$name"
+        project(":$name").projectDir  = pluginDirectory
+}

+ 4 - 0
frontend/app_flowy/assets/translations/ca-ES.json

@@ -141,5 +141,9 @@
       "lightLabel": "Mode Clar",
       "darkLabel": "Mode Fosc"
     }
+  },
+  "sideBar": {
+    "openSidebar": "Open sidebar",
+    "closeSidebar": "Close sidebar"
   }
 }

+ 4 - 0
frontend/app_flowy/assets/translations/de-DE.json

@@ -141,6 +141,10 @@
         "lightLabel": "Heller Modus",
         "darkLabel": "Dunkler Modus"
       }
+    },
+    "sideBar": {
+      "openSidebar": "Open sidebar",
+      "closeSidebar": "Close sidebar"
     }
   }
   

+ 15 - 3
frontend/app_flowy/assets/translations/en.json

@@ -95,7 +95,13 @@
   "tooltip": {
     "lightMode": "Switch to Light mode",
     "darkMode": "Switch to Dark mode",
-    "openAsPage": "Open as a Page"
+    "openAsPage": "Open as a Page",
+    "addNewRow": "Add a new row",
+    "openMenu": "Click to open menu"
+  },
+  "sideBar": {
+    "closeSidebar": "Close side bar",
+    "openSidebar": "Open side bar"
   },
   "notifications": {
     "export": {
@@ -183,7 +189,8 @@
       "addSelectOption": "Add an option",
       "optionTitle": "Options",
       "addOption": "Add option",
-      "editProperty": "Edit property"
+      "editProperty": "Edit property",
+      "newColumn": "New column"
     },
     "row": {
       "duplicate": "Duplicate",
@@ -215,5 +222,10 @@
       "timeHintTextInTwelveHour": "12:00 AM",
       "timeHintTextInTwentyFourHour": "12:00"
     }
+  },
+  "board": {
+    "column": {
+      "create_new_card": "New"
+    }
   }
-}
+}

+ 4 - 0
frontend/app_flowy/assets/translations/es-VE.json

@@ -213,5 +213,9 @@
       "timeHintTextInTwelveHour": "12:00 AM",
       "timeHintTextInTwentyFourHour": "12:00"
     }
+  },
+  "sideBar": {
+    "openSidebar": "Open sidebar",
+    "closeSidebar": "Close sidebar"
   }
 }

+ 4 - 0
frontend/app_flowy/assets/translations/fr-CA.json

@@ -141,5 +141,9 @@
       "lightLabel": "Mode clair",
       "darkLabel": "Mode sombre"
     }
+  },
+  "sideBar": {
+    "openSidebar": "Open sidebar",
+    "closeSidebar": "Close sidebar"
   }
 }

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

@@ -142,6 +142,10 @@
       "darkLabel": "Mode sombre"
     }
   },
+  "sideBar": {
+    "openSidebar": "Open sidebar",
+    "closeSidebar": "Close sidebar"
+  },
   "grid": {
     "settings": {
       "filter": "Filtrer",

+ 4 - 0
frontend/app_flowy/assets/translations/hu-HU.json

@@ -141,5 +141,9 @@
       "lightLabel": "Világos mód",
       "darkLabel": "Éjjeli mód"
     }
+  },
+  "sideBar": {
+    "openSidebar": "Open sidebar",
+    "closeSidebar": "Close sidebar"
   }
 }

+ 4 - 0
frontend/app_flowy/assets/translations/id-ID.json

@@ -214,5 +214,9 @@
       "timeHintTextInTwelveHour": "12:00 AM",
       "timeHintTextInTwentyFourHour": "12:00"
     }
+  },
+  "sideBar": {
+    "openSidebar": "Open sidebar",
+    "closeSidebar": "Close sidebar"
   }
 }

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

@@ -147,5 +147,9 @@
   },
   "document":{
     "menuName":"Documento"
+  },
+  "sideBar": {
+    "openSidebar": "Open sidebar",
+    "closeSidebar": "Close sidebar"
   }
 }

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

@@ -195,5 +195,9 @@
       "pannelTitle": "選択候補を検索 または 作成する",
       "searchOption": "選択候補を検索"
     }
+  },
+  "sideBar": {
+    "openSidebar": "Open sidebar",
+    "closeSidebar": "Close sidebar"
   }
 }

+ 4 - 0
frontend/app_flowy/assets/translations/pl-PL.json

@@ -141,5 +141,9 @@
       "lightLabel": "Tryb Jasny",
       "darkLabel": "Tryb Ciemny"
     }
+  },
+  "sideBar": {
+    "openSidebar": "Open sidebar",
+    "closeSidebar": "Close sidebar"
   }
 }

+ 4 - 0
frontend/app_flowy/assets/translations/pt-BR.json

@@ -141,6 +141,10 @@
         "lightLabel": "Modo Claro",
         "darkLabel": "Modo Escuro"
       }
+    },
+    "sideBar": {
+      "openSidebar": "Open sidebar",
+      "closeSidebar": "Close sidebar"
     }
   }
  

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

@@ -141,6 +141,10 @@
         "lightLabel": "Modo Claro",
         "darkLabel": "Modo Escuro"
       }
+    },
+    "sideBar": {
+      "openSidebar": "Open sidebar",
+      "closeSidebar": "Close sidebar"
     }
   }
 

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

@@ -203,6 +203,10 @@
         "timeHintTextInTwelveHour": "12:00 AM",
         "timeHintTextInTwentyFourHour": "12:00"
       }
+    },
+    "sideBar": {
+      "openSidebar": "Open sidebar",
+      "closeSidebar": "Close sidebar"
     }
   }
   

+ 4 - 0
frontend/app_flowy/assets/translations/tr-TR.json

@@ -141,5 +141,9 @@
       "lightLabel": "Aydınlık Mod",
       "darkLabel": "Karanlık Mod"
     }
+  },
+  "sideBar": {
+    "openSidebar": "Open sidebar",
+    "closeSidebar": "Close sidebar"
   }
 }

+ 80 - 3
frontend/app_flowy/assets/translations/zh-CN.json

@@ -93,8 +93,14 @@
     "highlight": "高亮"
   },
   "tooltip": {
-    "lightMode": "切换到灯光模式",
-    "darkMode": "切换到暗模式"
+    "lightMode": "切换到亮色模式",
+    "darkMode": "切换到暗色模式"
+  },
+  "notifications": {
+    "export": {
+      "markdown": "导出笔记为Markdown文档",
+      "path": "Documents/flowy"
+    }
   },
   "contactsPage": {
     "title": "联系人",
@@ -135,11 +141,82 @@
     "menu": {
       "appearance": "外观",
       "language": "语言",
+      "user": "用户",
       "open": "打开设置"
     },
     "appearance": {
       "lightLabel": "日间模式",
       "darkLabel": "夜间模式"
     }
+  },
+  "sideBar": {
+    "openSidebar": "打开侧边栏",
+    "closeSidebar": "关闭侧边栏"
+  },
+  "grid": {
+    "settings": {
+      "filter": "过滤器",
+      "sortBy": "排序",
+      "Properties": "属性"
+    },
+    "field": {
+      "hide": "隐藏",
+      "insertLeft": "左侧插入",
+      "insertRight": "右侧插入",
+      "duplicate": "拷贝",
+      "delete": "删除",
+      "textFieldName": "文本",
+      "checkboxFieldName": "勾选框",
+      "dateFieldName": "日期",
+      "numberFieldName": "数字",
+      "singleSelectFieldName": "单项选择器",
+      "multiSelectFieldName": "多项选择器",
+      "urlFieldName": "链接",
+      "numberFormat": " 数字格式",
+      "dateFormat": " 日期格式",
+      "includeTime": " 包含时间",
+      "dateFormatFriendly": "月 日,年",
+      "dateFormatISO": "年-月-日",
+      "dateFormatLocal": "年/月/日",
+      "dateFormatUS": "年/月/日",
+      "timeFormat": " 时间格式",
+      "invalidTimeFormat": "时间格式错误",
+      "timeFormatTwelveHour": "12小时制",
+      "timeFormatTwentyFourHour": "24小时制",
+      "addSelectOption": "添加一个标签",
+      "optionTitle": "标签",
+      "addOption": "添加标签",
+      "editProperty": "编辑列属性"
+    },
+    "row": {
+      "duplicate": "复制",
+      "delete": "删除",
+      "textPlaceholder": "空",
+      "copyProperty": "复制列"
+    },
+    "selectOption": {
+      "create": "新建",
+      "purpleColor": "紫色",
+      "pinkColor": "粉色",
+      "lightPinkColor": "浅粉色",
+      "orangeColor": "橙色",
+      "yellowColor": "黄色",
+      "limeColor": "鲜绿色",
+      "greenColor": "绿色",
+      "aquaColor": "水蓝色",
+      "blueColor": "蓝色",
+      "deleteTag": "删除标签",
+      "colorPannelTitle": "颜色",
+      "pannelTitle": "选择或新建一个标签",
+      "searchOption": "搜索标签"
+    },
+    "menuName": "网格"
+  },
+  "document": {
+    "menuName": "文档",
+    "date": {
+      "timeHintTextInTwelveHour": "12:00 AM",
+      "timeHintTextInTwentyFourHour": "12:00"
+    }
   }
-}
+}

+ 4 - 0
frontend/app_flowy/assets/translations/zh-TW.json

@@ -214,5 +214,9 @@
       "timeHintTextInTwelveHour": "12:00 AM",
       "timeHintTextInTwentyFourHour": "12:00"
     }
+  },
+  "sideBar": {
+    "openSidebar": "Open sidebar",
+    "closeSidebar": "Close sidebar"
   }
 }

+ 4 - 3
frontend/app_flowy/lib/core/frameless_window.dart

@@ -31,10 +31,10 @@ class MoveWindowDetector extends StatefulWidget {
   final Widget? child;
 
   @override
-  _MoveWindowDetectorState createState() => _MoveWindowDetectorState();
+  MoveWindowDetectorState createState() => MoveWindowDetectorState();
 }
 
-class _MoveWindowDetectorState extends State<MoveWindowDetector> {
+class MoveWindowDetectorState extends State<MoveWindowDetector> {
   double winX = 0;
   double winY = 0;
 
@@ -59,7 +59,8 @@ class _MoveWindowDetectorState extends State<MoveWindowDetector> {
         final double dy = windowPos[1];
         final deltaX = details.globalPosition.dx - winX;
         final deltaY = details.globalPosition.dy - winY;
-        await CocoaWindowChannel.instance.setWindowPosition(Offset(dx + deltaX, dy - deltaY));
+        await CocoaWindowChannel.instance
+            .setWindowPosition(Offset(dx + deltaX, dy - deltaY));
       },
       child: widget.child,
     );

+ 4 - 0
frontend/app_flowy/lib/main.dart

@@ -1,6 +1,7 @@
 import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/user/presentation/splash_screen.dart';
 import 'package:easy_localization/easy_localization.dart';
+import 'package:hotkey_manager/hotkey_manager.dart';
 import 'package:flutter/material.dart';
 
 class FlowyApp implements EntryPoint {
@@ -14,5 +15,8 @@ void main() async {
   WidgetsFlutterBinding.ensureInitialized();
   await EasyLocalization.ensureInitialized();
 
+  WidgetsFlutterBinding.ensureInitialized();
+  await hotKeyManager.unregisterAll();
+
   await FlowyRunner.run(FlowyApp());
 }

+ 145 - 52
frontend/app_flowy/lib/plugins/board/application/board_bloc.dart

@@ -20,19 +20,19 @@ import 'group_controller.dart';
 part 'board_bloc.freezed.dart';
 
 class BoardBloc extends Bloc<BoardEvent, BoardState> {
-  final BoardDataController _dataController;
-  late final AFBoardDataController afBoardDataController;
+  final BoardDataController _gridDataController;
+  late final AFBoardDataController boardController;
   final MoveRowFFIService _rowService;
-  LinkedHashMap<String, GroupController> groupControllers = LinkedHashMap.new();
+  LinkedHashMap<String, GroupController> groupControllers = LinkedHashMap();
 
-  GridFieldCache get fieldCache => _dataController.fieldCache;
-  String get gridId => _dataController.gridId;
+  GridFieldCache get fieldCache => _gridDataController.fieldCache;
+  String get gridId => _gridDataController.gridId;
 
   BoardBloc({required ViewPB view})
       : _rowService = MoveRowFFIService(gridId: view.id),
-        _dataController = BoardDataController(view: view),
+        _gridDataController = BoardDataController(view: view),
         super(BoardState.initial(view.id)) {
-    afBoardDataController = AFBoardDataController(
+    boardController = AFBoardDataController(
       onMoveColumn: (
         fromColumnId,
         fromIndex,
@@ -69,31 +69,51 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
             _startListening();
             await _loadGrid(emit);
           },
-          createRow: (groupId) async {
-            final result = await _dataController.createBoardCard(groupId);
+          createBottomRow: (groupId) async {
+            final startRowId = groupControllers[groupId]?.lastRow()?.id;
+            final result = await _gridDataController.createBoardCard(
+              groupId,
+              startRowId: startRowId,
+            );
+            result.fold(
+              (_) {},
+              (err) => Log.error(err),
+            );
+          },
+          createHeaderRow: (String groupId) async {
+            final result = await _gridDataController.createBoardCard(groupId);
             result.fold(
-              (rowPB) {
-                emit(state.copyWith(editingRow: some(rowPB)));
-              },
+              (_) {},
               (err) => Log.error(err),
             );
           },
+          didCreateRow: (String groupId, RowPB row, int? index) {
+            emit(state.copyWith(
+              editingRow: Some(BoardEditingRow(
+                columnId: groupId,
+                row: row,
+                index: index,
+              )),
+            ));
+          },
           endEditRow: (rowId) {
             assert(state.editingRow.isSome());
-            state.editingRow.fold(() => null, (row) {
-              assert(row.id == rowId);
+            state.editingRow.fold(() => null, (editingRow) {
+              assert(editingRow.row.id == rowId);
               emit(state.copyWith(editingRow: none()));
             });
           },
           didReceiveGridUpdate: (GridPB grid) {
             emit(state.copyWith(grid: Some(grid)));
           },
-          didReceiveRows: (List<RowInfo> rowInfos) {
-            emit(state.copyWith(rowInfos: rowInfos));
-          },
           didReceiveError: (FlowyError error) {
             emit(state.copyWith(noneOrError: some(error)));
           },
+          didReceiveGroups: (List<GroupPB> groups) {
+            emit(state.copyWith(
+              groupIds: groups.map((group) => group.groupId).toList(),
+            ));
+          },
         );
       },
     );
@@ -126,7 +146,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
 
   @override
   Future<void> close() async {
-    await _dataController.dispose();
+    await _gridDataController.dispose();
     for (final controller in groupControllers.values) {
       controller.dispose();
     }
@@ -135,7 +155,12 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
 
   void initializeGroups(List<GroupPB> groups) {
     for (final group in groups) {
-      final delegate = GroupControllerDelegateImpl(afBoardDataController);
+      final delegate = GroupControllerDelegateImpl(
+        controller: boardController,
+        onNewColumnItem: (groupId, row, index) {
+          add(BoardEvent.didCreateRow(groupId, row, index));
+        },
+      );
       final controller = GroupController(
         gridId: state.gridId,
         group: group,
@@ -147,12 +172,12 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
   }
 
   GridRowCache? getRowCache(String blockId) {
-    final GridBlockCache? blockCache = _dataController.blocks[blockId];
+    final GridBlockCache? blockCache = _gridDataController.blocks[blockId];
     return blockCache?.rowCache;
   }
 
   void _startListening() {
-    _dataController.addListener(
+    _gridDataController.addListener(
       onGridChanged: (grid) {
         if (!isClosed) {
           add(BoardEvent.didReceiveGridUpdate(grid));
@@ -162,17 +187,31 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
         List<AFBoardColumnData> columns = groups.map((group) {
           return AFBoardColumnData(
             id: group.groupId,
-            desc: group.desc,
-            items: _buildRows(group.rows),
+            name: group.desc,
+            items: _buildRows(group),
             customData: group,
           );
         }).toList();
 
-        afBoardDataController.addColumns(columns);
+        boardController.addColumns(columns);
         initializeGroups(groups);
+        add(BoardEvent.didReceiveGroups(groups));
+      },
+      onDeletedGroup: (groupIds) {
+        //
       },
-      onRowsChanged: (List<RowInfo> rowInfos, RowsChangedReason reason) {
-        add(BoardEvent.didReceiveRows(rowInfos));
+      onInsertedGroup: (insertedGroups) {
+        //
+      },
+      onUpdatedGroup: (updatedGroups) {
+        //
+        for (final group in updatedGroups) {
+          final columnController =
+              boardController.getColumnController(group.groupId);
+          if (columnController != null) {
+            columnController.updateColumnName(group.desc);
+          }
+        }
       },
       onError: (err) {
         Log.error(err);
@@ -180,16 +219,19 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
     );
   }
 
-  List<AFColumnItem> _buildRows(List<RowPB> rows) {
-    final items = rows.map((row) {
-      return BoardColumnItem(row: row);
+  List<AFColumnItem> _buildRows(GroupPB group) {
+    final items = group.rows.map((row) {
+      return BoardColumnItem(
+        row: row,
+        fieldId: group.fieldId,
+      );
     }).toList();
 
     return <AFColumnItem>[...items];
   }
 
   Future<void> _loadGrid(Emitter<BoardState> emit) async {
-    final result = await _dataController.loadData();
+    final result = await _gridDataController.loadData();
     result.fold(
       (grid) => emit(
         state.copyWith(loadingState: GridLoadingState.finish(left(unit))),
@@ -203,15 +245,21 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
 
 @freezed
 class BoardEvent with _$BoardEvent {
-  const factory BoardEvent.initial() = InitialGrid;
-  const factory BoardEvent.createRow(String groupId) = _CreateRow;
+  const factory BoardEvent.initial() = _InitialBoard;
+  const factory BoardEvent.createBottomRow(String groupId) = _CreateBottomRow;
+  const factory BoardEvent.createHeaderRow(String groupId) = _CreateHeaderRow;
+  const factory BoardEvent.didCreateRow(
+    String groupId,
+    RowPB row,
+    int? index,
+  ) = _DidCreateRow;
   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;
+  const factory BoardEvent.didReceiveGroups(List<GroupPB> groups) =
+      _DidReceiveGroups;
 }
 
 @freezed
@@ -219,16 +267,16 @@ class BoardState with _$BoardState {
   const factory BoardState({
     required String gridId,
     required Option<GridPB> grid,
-    required Option<RowPB> editingRow,
-    required List<RowInfo> rowInfos,
+    required List<String> groupIds,
+    required Option<BoardEditingRow> editingRow,
     required GridLoadingState loadingState,
     required Option<FlowyError> noneOrError,
   }) = _BoardState;
 
   factory BoardState.initial(String gridId) => BoardState(
-        rowInfos: [],
         grid: none(),
         gridId: gridId,
+        groupIds: [],
         editingRow: none(),
         noneOrError: none(),
         loadingState: const _Loading(),
@@ -268,39 +316,84 @@ class GridFieldEquatable extends Equatable {
 class BoardColumnItem extends AFColumnItem {
   final RowPB row;
 
-  BoardColumnItem({required this.row});
+  final String fieldId;
 
-  @override
-  String get id => row.id;
-}
+  final bool requestFocus;
+
+  BoardColumnItem({
+    required this.row,
+    required this.fieldId,
+    this.requestFocus = false,
+  });
 
-class CreateCardItem extends AFColumnItem {
   @override
-  String get id => '$CreateCardItem';
+  String get id => row.id;
 }
 
 class GroupControllerDelegateImpl extends GroupControllerDelegate {
   final AFBoardDataController controller;
+  final void Function(String, RowPB, int?) onNewColumnItem;
 
-  GroupControllerDelegateImpl(this.controller);
+  GroupControllerDelegateImpl({
+    required this.controller,
+    required this.onNewColumnItem,
+  });
 
   @override
-  void insertRow(String groupId, RowPB row, int? index) {
-    final item = BoardColumnItem(row: row);
+  void insertRow(GroupPB group, RowPB row, int? index) {
     if (index != null) {
-      controller.insertColumnItem(groupId, index, item);
+      final item = BoardColumnItem(row: row, fieldId: group.fieldId);
+      controller.insertColumnItem(group.groupId, index, item);
     } else {
-      controller.addColumnItem(groupId, item);
+      final item = BoardColumnItem(
+        row: row,
+        fieldId: group.fieldId,
+      );
+      controller.addColumnItem(group.groupId, item);
     }
   }
 
   @override
-  void removeRow(String groupId, String rowId) {
-    controller.removeColumnItem(groupId, rowId);
+  void removeRow(GroupPB group, String rowId) {
+    controller.removeColumnItem(group.groupId, rowId);
+  }
+
+  @override
+  void updateRow(GroupPB group, RowPB row) {
+    controller.updateColumnItem(
+      group.groupId,
+      BoardColumnItem(
+        row: row,
+        fieldId: group.fieldId,
+      ),
+    );
   }
 
   @override
-  void updateRow(String groupId, RowPB row) {
-    //
+  void addNewRow(GroupPB group, RowPB row, int? index) {
+    final item = BoardColumnItem(
+      row: row,
+      fieldId: group.fieldId,
+      requestFocus: true,
+    );
+
+    if (index != null) {
+      controller.insertColumnItem(group.groupId, index, item);
+    } else {
+      controller.addColumnItem(group.groupId, item);
+    }
+    onNewColumnItem(group.groupId, row, index);
   }
 }
+
+class BoardEditingRow {
+  String columnId;
+  RowPB row;
+  int? index;
+
+  BoardEditingRow({
+    required this.columnId,
+    required this.row,
+    required this.index,
+  });
+}

+ 38 - 6
frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart

@@ -10,9 +10,15 @@ import 'dart:async';
 import 'package:dartz/dartz.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart';
 
+import 'board_listener.dart';
+
 typedef OnFieldsChanged = void Function(UnmodifiableListView<FieldPB>);
 typedef OnGridChanged = void Function(GridPB);
 typedef DidLoadGroups = void Function(List<GroupPB>);
+typedef OnUpdatedGroup = void Function(List<GroupPB>);
+typedef OnDeletedGroup = void Function(List<String>);
+typedef OnInsertedGroup = void Function(List<InsertedGroupPB>);
+
 typedef OnRowsChanged = void Function(
   List<RowInfo>,
   RowsChangedReason,
@@ -23,6 +29,7 @@ class BoardDataController {
   final String gridId;
   final GridFFIService _gridFFIService;
   final GridFieldCache fieldCache;
+  final BoardListener _listener;
 
   // key: the block id
   final LinkedHashMap<String, GridBlockCache> _blocks;
@@ -44,16 +51,21 @@ class BoardDataController {
 
   BoardDataController({required ViewPB view})
       : gridId = view.id,
-        _blocks = LinkedHashMap.new(),
+        _listener = BoardListener(view.id),
+        // ignore: prefer_collection_literals
+        _blocks = LinkedHashMap(),
         _gridFFIService = GridFFIService(gridId: view.id),
         fieldCache = GridFieldCache(gridId: view.id);
 
   void addListener({
-    OnGridChanged? onGridChanged,
+    required OnGridChanged onGridChanged,
     OnFieldsChanged? onFieldsChanged,
-    DidLoadGroups? didLoadGroups,
+    required DidLoadGroups didLoadGroups,
     OnRowsChanged? onRowsChanged,
-    OnError? onError,
+    required OnUpdatedGroup onUpdatedGroup,
+    required OnDeletedGroup onDeletedGroup,
+    required OnInsertedGroup onInsertedGroup,
+    required OnError? onError,
   }) {
     _onGridChanged = onGridChanged;
     _onFieldsChanged = onFieldsChanged;
@@ -64,6 +76,25 @@ class BoardDataController {
     fieldCache.addListener(onFields: (fields) {
       _onFieldsChanged?.call(UnmodifiableListView(fields));
     });
+
+    _listener.start(onBoardChanged: (result) {
+      result.fold(
+        (changeset) {
+          if (changeset.updateGroups.isNotEmpty) {
+            onUpdatedGroup.call(changeset.updateGroups);
+          }
+
+          if (changeset.insertedGroups.isNotEmpty) {
+            onInsertedGroup.call(changeset.insertedGroups);
+          }
+
+          if (changeset.deletedGroups.isNotEmpty) {
+            onDeletedGroup.call(changeset.deletedGroups);
+          }
+        },
+        (e) => _onError?.call(e),
+      );
+    });
   }
 
   Future<Either<Unit, FlowyError>> loadData() async {
@@ -88,8 +119,9 @@ class BoardDataController {
     );
   }
 
-  Future<Either<RowPB, FlowyError>> createBoardCard(String groupId) {
-    return _gridFFIService.createBoardCard(groupId);
+  Future<Either<RowPB, FlowyError>> createBoardCard(String groupId,
+      {String? startRowId}) {
+    return _gridFFIService.createBoardCard(groupId, startRowId);
   }
 
   Future<void> dispose() async {

+ 50 - 0
frontend/app_flowy/lib/plugins/board/application/board_listener.dart

@@ -0,0 +1,50 @@
+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:dartz/dartz.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/group_changeset.pb.dart';
+
+typedef UpdateBoardNotifiedValue = Either<GroupViewChangesetPB, FlowyError>;
+
+class BoardListener {
+  final String viewId;
+  PublishNotifier<UpdateBoardNotifiedValue>? _groupNotifier = PublishNotifier();
+  GridNotificationListener? _listener;
+  BoardListener(this.viewId);
+
+  void start({
+    required void Function(UpdateBoardNotifiedValue) onBoardChanged,
+  }) {
+    _groupNotifier?.addPublishListener(onBoardChanged);
+    _listener = GridNotificationListener(
+      objectId: viewId,
+      handler: _handler,
+    );
+  }
+
+  void _handler(
+    GridNotification ty,
+    Either<Uint8List, FlowyError> result,
+  ) {
+    switch (ty) {
+      case GridNotification.DidUpdateGroupView:
+        result.fold(
+          (payload) => _groupNotifier?.value =
+              left(GroupViewChangesetPB.fromBuffer(payload)),
+          (error) => _groupNotifier?.value = right(error),
+        );
+        break;
+      default:
+        break;
+    }
+  }
+
+  Future<void> stop() async {
+    await _listener?.stop();
+    _groupNotifier?.dispose();
+    _groupNotifier = null;
+  }
+}

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

@@ -79,7 +79,7 @@ class BoardDateCellState with _$BoardDateCellState {
 String _dateStrFromCellData(DateCellDataPB? cellData) {
   String dateStr = "";
   if (cellData != null) {
-    dateStr = cellData.date + " " + cellData.time;
+    dateStr = "${cellData.date} ${cellData.time}";
   }
   return dateStr;
 }

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

@@ -68,7 +68,6 @@ class BoardSelectOptionCellState with _$BoardSelectOptionCellState {
   factory BoardSelectOptionCellState.initial(
       GridSelectOptionCellController context) {
     final data = context.getCellData();
-
     return BoardSelectOptionCellState(
       selectedOptions: data?.selectOptions ?? [],
     );

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

@@ -1,4 +1,5 @@
 import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
+import 'package:flutter/foundation.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
@@ -20,6 +21,15 @@ class BoardTextCellBloc extends Bloc<BoardTextCellEvent, BoardTextCellState> {
           didReceiveCellUpdate: (content) {
             emit(state.copyWith(content: content));
           },
+          updateText: (text) {
+            if (text != state.content) {
+              cellController.saveCellData(text);
+              emit(state.copyWith(content: text));
+            }
+          },
+          enableEdit: (bool enabled) {
+            emit(state.copyWith(enableEdit: enabled));
+          },
         );
       },
     );
@@ -49,6 +59,8 @@ class BoardTextCellBloc extends Bloc<BoardTextCellEvent, BoardTextCellState> {
 @freezed
 class BoardTextCellEvent with _$BoardTextCellEvent {
   const factory BoardTextCellEvent.initial() = _InitialCell;
+  const factory BoardTextCellEvent.updateText(String text) = _UpdateContent;
+  const factory BoardTextCellEvent.enableEdit(bool enabled) = _EnableEdit;
   const factory BoardTextCellEvent.didReceiveCellUpdate(String cellContent) =
       _DidReceiveCellUpdate;
 }
@@ -57,10 +69,12 @@ class BoardTextCellEvent with _$BoardTextCellEvent {
 class BoardTextCellState with _$BoardTextCellState {
   const factory BoardTextCellState({
     required String content,
+    required bool enableEdit,
   }) = _BoardTextCellState;
 
   factory BoardTextCellState.initial(GridCellController context) =>
       BoardTextCellState(
         content: context.getCellData() ?? "",
+        enableEdit: false,
       );
 }

+ 41 - 29
frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart

@@ -4,7 +4,6 @@ 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';
@@ -14,10 +13,12 @@ import 'card_data_controller.dart';
 part 'card_bloc.freezed.dart';
 
 class BoardCardBloc extends Bloc<BoardCardEvent, BoardCardState> {
+  final String fieldId;
   final RowFFIService _rowService;
   final CardDataController _dataController;
 
   BoardCardBloc({
+    required this.fieldId,
     required String gridId,
     required CardDataController dataController,
   })  : _rowService = RowFFIService(
@@ -25,22 +26,22 @@ class BoardCardBloc extends Bloc<BoardCardEvent, BoardCardState> {
           blockId: dataController.rowPB.blockId,
         ),
         _dataController = dataController,
-        super(BoardCardState.initial(
-            dataController.rowPB, dataController.loadData())) {
+        super(
+          BoardCardState.initial(
+            dataController.rowPB,
+            _makeCells(fieldId, dataController.loadData()),
+          ),
+        ) {
     on<BoardCardEvent>(
       (event, emit) async {
-        await event.map(
-          initial: (_InitialRow value) async {
+        await event.when(
+          initial: () async {
             await _startListening();
           },
-          didReceiveCells: (_DidReceiveCells value) async {
-            final cells = value.gridCellMap.values
-                .map((e) => GridCellEquatable(e.field))
-                .toList();
+          didReceiveCells: (cells, reason) async {
             emit(state.copyWith(
-              gridCellMap: value.gridCellMap,
-              cells: UnmodifiableListView(cells),
-              changeReason: value.reason,
+              cells: cells,
+              changeReason: reason,
             ));
           },
         );
@@ -58,7 +59,7 @@ class BoardCardBloc extends Bloc<BoardCardEvent, BoardCardState> {
     return RowInfo(
       gridId: _rowService.gridId,
       fields: UnmodifiableListView(
-        state.cells.map((cell) => cell._field).toList(),
+        state.cells.map((cell) => cell.identifier.field).toList(),
       ),
       rowPB: state.rowPB,
     );
@@ -66,8 +67,9 @@ class BoardCardBloc extends Bloc<BoardCardEvent, BoardCardState> {
 
   Future<void> _startListening() async {
     _dataController.addListener(
-      onRowChanged: (cells, reason) {
+      onRowChanged: (cellMap, reason) {
         if (!isClosed) {
+          final cells = _makeCells(fieldId, cellMap);
           add(BoardCardEvent.didReceiveCells(cells, reason));
         }
       },
@@ -75,42 +77,52 @@ class BoardCardBloc extends Bloc<BoardCardEvent, BoardCardState> {
   }
 }
 
+UnmodifiableListView<BoardCellEquatable> _makeCells(
+    String fieldId, GridCellMap originalCellMap) {
+  List<BoardCellEquatable> cells = [];
+  for (final entry in originalCellMap.entries) {
+    if (entry.value.fieldId != fieldId) {
+      cells.add(BoardCellEquatable(entry.value));
+    }
+  }
+  return UnmodifiableListView(cells);
+}
+
 @freezed
 class BoardCardEvent with _$BoardCardEvent {
   const factory BoardCardEvent.initial() = _InitialRow;
   const factory BoardCardEvent.didReceiveCells(
-      GridCellMap gridCellMap, RowsChangedReason reason) = _DidReceiveCells;
+    UnmodifiableListView<BoardCellEquatable> cells,
+    RowsChangedReason reason,
+  ) = _DidReceiveCells;
 }
 
 @freezed
 class BoardCardState with _$BoardCardState {
   const factory BoardCardState({
     required RowPB rowPB,
-    required GridCellMap gridCellMap,
-    required UnmodifiableListView<GridCellEquatable> cells,
+    required UnmodifiableListView<BoardCellEquatable> cells,
     RowsChangedReason? changeReason,
   }) = _BoardCardState;
 
-  factory BoardCardState.initial(RowPB rowPB, GridCellMap cellDataMap) =>
+  factory BoardCardState.initial(
+          RowPB rowPB, UnmodifiableListView<BoardCellEquatable> cells) =>
       BoardCardState(
         rowPB: rowPB,
-        gridCellMap: cellDataMap,
-        cells: UnmodifiableListView(
-          cellDataMap.values.map((e) => GridCellEquatable(e.field)).toList(),
-        ),
+        cells: cells,
       );
 }
 
-class GridCellEquatable extends Equatable {
-  final FieldPB _field;
+class BoardCellEquatable extends Equatable {
+  final GridCellIdentifier identifier;
 
-  const GridCellEquatable(FieldPB field) : _field = field;
+  const BoardCellEquatable(this.identifier);
 
   @override
   List<Object?> get props => [
-        _field.id,
-        _field.fieldType,
-        _field.visibility,
-        _field.width,
+        identifier.field.id,
+        identifier.field.fieldType,
+        identifier.field.visibility,
+        identifier.field.width,
       ];
 }

+ 44 - 17
frontend/app_flowy/lib/plugins/board/application/group_controller.dart

@@ -1,15 +1,15 @@
 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);
+  void removeRow(GroupPB group, String rowId);
+  void insertRow(GroupPB group, RowPB row, int? index);
+  void updateRow(GroupPB group, RowPB row);
+  void addNewRow(GroupPB group, RowPB row, int? index);
 }
 
 class GroupController {
@@ -31,13 +31,22 @@ class GroupController {
     }
   }
 
+  RowPB? lastRow() {
+    if (group.rows.isEmpty) return null;
+    return group.rows.last;
+  }
+
   void startListening() {
     _listener.start(onGroupChanged: (result) {
       result.fold(
-        (GroupRowsChangesetPB changeset) {
+        (GroupChangesetPB changeset) {
+          for (final deletedRow in changeset.deletedRows) {
+            group.rows.removeWhere((rowPB) => rowPB.id == deletedRow);
+            delegate.removeRow(group, deletedRow);
+          }
+
           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);
@@ -45,16 +54,11 @@ class GroupController {
               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);
+            if (insertedRow.isNew) {
+              delegate.addNewRow(group, insertedRow.row, index);
+            } else {
+              delegate.insertRow(group, insertedRow.row, index);
+            }
           }
 
           for (final updatedRow in changeset.updatedRows) {
@@ -66,7 +70,7 @@ class GroupController {
               group.rows[index] = updatedRow;
             }
 
-            delegate.updateRow(group.groupId, updatedRow);
+            delegate.updateRow(group, updatedRow);
           }
         },
         (err) => Log.error(err),
@@ -74,6 +78,29 @@ class GroupController {
     });
   }
 
+  // GroupChangesetPB _transformChangeset(GroupChangesetPB changeset) {
+  //   final insertedRows = changeset.insertedRows
+  //       .where(
+  //         (delete) => !changeset.deletedRows.contains(delete.row.id),
+  //       )
+  //       .toList();
+
+  //   final deletedRows = changeset.deletedRows
+  //       .where((deletedRowId) =>
+  //           changeset.insertedRows
+  //               .indexWhere((insert) => insert.row.id == deletedRowId) ==
+  //           -1)
+  //       .toList();
+
+  //   return changeset.rebuild((rebuildChangeset) {
+  //     rebuildChangeset.insertedRows.clear();
+  //     rebuildChangeset.insertedRows.addAll(insertedRows);
+
+  //     rebuildChangeset.deletedRows.clear();
+  //     rebuildChangeset.deletedRows.addAll(deletedRows);
+  //   });
+  // }
+
   Future<void> dispose() async {
     _listener.stop();
   }

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

@@ -8,7 +8,7 @@ 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>;
+typedef UpdateGroupNotifiedValue = Either<GroupChangesetPB, FlowyError>;
 
 class GroupListener {
   final GroupPB group;
@@ -34,7 +34,7 @@ class GroupListener {
       case GridNotification.DidUpdateGroup:
         result.fold(
           (payload) => _groupNotifier?.value =
-              left(GroupRowsChangesetPB.fromBuffer(payload)),
+              left(GroupChangesetPB.fromBuffer(payload)),
           (error) => _groupNotifier?.value = right(error),
         );
         break;

+ 46 - 0
frontend/app_flowy/lib/plugins/board/application/toolbar/board_setting_bloc.dart

@@ -0,0 +1,46 @@
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+import 'dart:async';
+import 'package:dartz/dartz.dart';
+
+part 'board_setting_bloc.freezed.dart';
+
+class BoardSettingBloc extends Bloc<BoardSettingEvent, BoardSettingState> {
+  final String gridId;
+  BoardSettingBloc({required this.gridId})
+      : super(BoardSettingState.initial()) {
+    on<BoardSettingEvent>(
+      (event, emit) async {
+        event.when(performAction: (action) {
+          emit(state.copyWith(selectedAction: Some(action)));
+        });
+      },
+    );
+  }
+
+  @override
+  Future<void> close() async {
+    return super.close();
+  }
+}
+
+@freezed
+class BoardSettingEvent with _$BoardSettingEvent {
+  const factory BoardSettingEvent.performAction(BoardSettingAction action) =
+      _PerformAction;
+}
+
+@freezed
+class BoardSettingState with _$BoardSettingState {
+  const factory BoardSettingState({
+    required Option<BoardSettingAction> selectedAction,
+  }) = _BoardSettingState;
+
+  factory BoardSettingState.initial() => BoardSettingState(
+        selectedAction: none(),
+      );
+}
+
+enum BoardSettingAction {
+  properties,
+}

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

@@ -31,7 +31,7 @@ class BoardPluginBuilder implements PluginBuilder {
 
 class BoardPluginConfig implements PluginConfig {
   @override
-  bool get creatable => false;
+  bool get creatable => true;
 }
 
 class BoardPlugin extends Plugin {

+ 199 - 51
frontend/app_flowy/lib/plugins/board/presentation/board_page.dart

@@ -2,6 +2,7 @@
 
 import 'dart:collection';
 
+import 'package:app_flowy/generated/locale_keys.g.dart';
 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';
@@ -9,16 +10,22 @@ 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:easy_localization/easy_localization.dart';
+import 'package:flowy_infra/image.dart';
+import 'package:flowy_infra/theme.dart';
+import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui_web.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:flowy_sdk/protobuf/flowy-grid/group.pbserver.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';
+import 'toolbar/board_toolbar.dart';
 
 class BoardPage extends StatelessWidget {
   final ViewPB view;
@@ -30,13 +37,15 @@ class BoardPage extends StatelessWidget {
       create: (context) =>
           BoardBloc(view: view)..add(const BoardEvent.initial()),
       child: BlocBuilder<BoardBloc, BoardState>(
+        buildWhen: (previous, current) =>
+            previous.loadingState != current.loadingState,
         builder: (context, state) {
           return state.loadingState.map(
             loading: (_) =>
                 const Center(child: CircularProgressIndicator.adaptive()),
             finish: (result) {
               return result.successOrFail.fold(
-                (_) => BoardContent(),
+                (_) => const BoardContent(),
                 (err) => FlowyErrorPage(err.toString()),
               );
             },
@@ -47,67 +56,172 @@ class BoardPage extends StatelessWidget {
   }
 }
 
-class BoardContent extends StatelessWidget {
+class BoardContent extends StatefulWidget {
+  const BoardContent({Key? key}) : super(key: key);
+
+  @override
+  State<BoardContent> createState() => _BoardContentState();
+}
+
+class _BoardContentState extends State<BoardContent> {
+  late ScrollController scrollController;
+  late AFBoardScrollManager scrollManager;
+
   final config = AFBoardConfig(
     columnBackgroundColor: HexColor.fromHex('#F7F8FC'),
   );
 
-  BoardContent({Key? key}) : super(key: key);
+  @override
+  void initState() {
+    scrollController = ScrollController();
+    scrollManager = AFBoardScrollManager();
+    super.initState();
+  }
 
   @override
   Widget build(BuildContext context) {
-    return BlocBuilder<BoardBloc, BoardState>(
-      builder: (context, state) {
-        return Container(
-          color: Colors.white,
-          child: Padding(
-            padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
-            child: AFBoard(
-              // key: UniqueKey(),
-              scrollController: ScrollController(),
-              dataController: context.read<BoardBloc>().afBoardDataController,
-              headerBuilder: _buildHeader,
-              footBuilder: _buildFooter,
-              cardBuilder: (_, data) => _buildCard(context, data),
-              columnConstraints: const BoxConstraints.tightFor(width: 240),
-              config: AFBoardConfig(
-                columnBackgroundColor: HexColor.fromHex('#F7F8FC'),
+    return BlocListener<BoardBloc, BoardState>(
+      listener: (context, state) => _handleEditState(state, context),
+      child: BlocBuilder<BoardBloc, BoardState>(
+        buildWhen: (previous, current) =>
+            previous.groupIds.length != current.groupIds.length,
+        builder: (context, state) {
+          final theme = context.read<AppTheme>();
+          return Container(
+            color: theme.surface,
+            child: Padding(
+              padding: const EdgeInsets.symmetric(horizontal: 20),
+              child: Column(
+                children: [
+                  const _ToolbarBlocAdaptor(),
+                  Expanded(
+                    child: AFBoard(
+                      scrollManager: scrollManager,
+                      scrollController: scrollController,
+                      dataController: context.read<BoardBloc>().boardController,
+                      headerBuilder: _buildHeader,
+                      footBuilder: _buildFooter,
+                      cardBuilder: (_, column, columnItem) => _buildCard(
+                        context,
+                        column,
+                        columnItem,
+                      ),
+                      columnConstraints:
+                          const BoxConstraints.tightFor(width: 300),
+                      config: AFBoardConfig(
+                        columnBackgroundColor: HexColor.fromHex('#F7F8FC'),
+                      ),
+                    ),
+                  ),
+                ],
               ),
             ),
-          ),
-        );
+          );
+        },
+      ),
+    );
+  }
+
+  void _handleEditState(BoardState state, BuildContext context) {
+    state.editingRow.fold(
+      () => null,
+      (editingRow) {
+        WidgetsBinding.instance.addPostFrameCallback((_) {
+          if (editingRow.index != null) {
+            context
+                .read<BoardBloc>()
+                .add(BoardEvent.endEditRow(editingRow.row.id));
+          } else {
+            scrollManager.scrollToBottom(editingRow.columnId, () {
+              context
+                  .read<BoardBloc>()
+                  .add(BoardEvent.endEditRow(editingRow.row.id));
+            });
+          }
+        });
       },
     );
   }
 
-  Widget _buildHeader(BuildContext context, AFBoardColumnData columnData) {
+  @override
+  void dispose() {
+    scrollController.dispose();
+    super.dispose();
+  }
+
+  Widget _buildHeader(
+    BuildContext context,
+    AFBoardColumnData columnData,
+  ) {
     return AppFlowyColumnHeader(
-      icon: const Icon(Icons.lightbulb_circle),
-      title: Text(columnData.desc),
-      addIcon: const Icon(Icons.add, size: 20),
-      moreIcon: const Icon(Icons.more_horiz, size: 20),
+      title: Flexible(
+        fit: FlexFit.tight,
+        child: FlowyText.medium(
+          columnData.headerData.columnName,
+          fontSize: 14,
+          overflow: TextOverflow.clip,
+          color: context.read<AppTheme>().textColor,
+        ),
+      ),
+      addIcon: SizedBox(
+        height: 20,
+        width: 20,
+        child: svgWidget(
+          "home/add",
+          color: context.read<AppTheme>().iconColor,
+        ),
+      ),
+      onAddButtonClick: () {
+        context.read<BoardBloc>().add(
+              BoardEvent.createHeaderRow(columnData.id),
+            );
+      },
       height: 50,
-      margin: config.columnItemPadding,
+      margin: config.headerPadding,
     );
   }
 
   Widget _buildFooter(BuildContext context, AFBoardColumnData columnData) {
-    return AppFlowyColumnFooter(
-        icon: const Icon(Icons.add, size: 20),
-        title: const Text('New'),
+    final group = columnData.customData as GroupPB;
+    if (group.isDefault) {
+      return const SizedBox();
+    } else {
+      return AppFlowyColumnFooter(
+        icon: SizedBox(
+          height: 20,
+          width: 20,
+          child: svgWidget(
+            "home/add",
+            color: context.read<AppTheme>().iconColor,
+          ),
+        ),
+        title: FlowyText.medium(
+          LocaleKeys.board_column_create_new_card.tr(),
+          fontSize: 14,
+          color: context.read<AppTheme>().textColor,
+        ),
         height: 50,
-        margin: config.columnItemPadding,
+        margin: config.footerPadding,
         onAddButtonClick: () {
-          context.read<BoardBloc>().add(BoardEvent.createRow(columnData.id));
-        });
+          context.read<BoardBloc>().add(
+                BoardEvent.createBottomRow(columnData.id),
+              );
+        },
+      );
+    }
   }
 
-  Widget _buildCard(BuildContext context, AFColumnItem item) {
-    final rowPB = (item as BoardColumnItem).row;
+  Widget _buildCard(
+    BuildContext context,
+    AFBoardColumnData column,
+    AFColumnItem columnItem,
+  ) {
+    final boardColumnItem = columnItem as BoardColumnItem;
+    final rowPB = 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));
+    if (rowCache == null) return SizedBox(key: ObjectKey(columnItem));
 
     final fieldCache = context.read<BoardBloc>().fieldCache;
     final gridId = context.read<BoardBloc>().gridId;
@@ -118,21 +232,25 @@ class BoardContent extends StatelessWidget {
     );
 
     final cellBuilder = BoardCellBuilder(cardController);
-    final isEditing = context.read<BoardBloc>().state.editingRow.fold(
-          () => false,
-          (editingRow) => editingRow.id == rowPB.id,
-        );
+    bool isEditing = false;
+    context.read<BoardBloc>().state.editingRow.fold(
+      () => null,
+      (editingRow) {
+        isEditing = editingRow.row.id == columnItem.row.id;
+      },
+    );
 
     return AppFlowyColumnItemCard(
-      key: ObjectKey(item),
+      key: ValueKey(columnItem.id),
+      margin: config.cardPadding,
+      decoration: _makeBoxDecoration(context),
       child: BoardCard(
         gridId: gridId,
+        groupId: column.id,
+        fieldId: boardColumnItem.fieldId,
         isEditing: isEditing,
         cellBuilder: cellBuilder,
         dataController: cardController,
-        onEditEditing: (rowId) {
-          context.read<BoardBloc>().add(BoardEvent.endEditRow(rowId));
-        },
         openCard: (context) => _openCard(
           gridId,
           fieldCache,
@@ -144,6 +262,16 @@ class BoardContent extends StatelessWidget {
     );
   }
 
+  BoxDecoration _makeBoxDecoration(BuildContext context) {
+    final theme = context.read<AppTheme>();
+    final borderSide = BorderSide(color: theme.shader6, width: 1.0);
+    return BoxDecoration(
+      color: theme.surface,
+      border: Border.fromBorderSide(borderSide),
+      borderRadius: const BorderRadius.all(Radius.circular(6)),
+    );
+  }
+
   void _openCard(String gridId, GridFieldCache fieldCache, RowPB rowPB,
       GridRowCache rowCache, BuildContext context) {
     final rowInfo = RowInfo(
@@ -159,13 +287,33 @@ class BoardContent extends StatelessWidget {
     );
 
     FlowyOverlay.show(
-        context: context,
-        builder: (BuildContext context) {
-          return RowDetailPage(
-            cellBuilder: GridCellBuilder(delegate: dataController),
-            dataController: dataController,
-          );
-        });
+      context: context,
+      builder: (BuildContext context) {
+        return RowDetailPage(
+          cellBuilder: GridCellBuilder(delegate: dataController),
+          dataController: dataController,
+        );
+      },
+    );
+  }
+}
+
+class _ToolbarBlocAdaptor extends StatelessWidget {
+  const _ToolbarBlocAdaptor({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<BoardBloc, BoardState>(
+      builder: (context, state) {
+        final bloc = context.read<BoardBloc>();
+        final toolbarContext = BoardToolbarContext(
+          viewId: bloc.gridId,
+          fieldCache: bloc.fieldCache,
+        );
+
+        return BoardToolbar(toolbarContext: toolbarContext);
+      },
+    );
   }
 }
 

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

@@ -0,0 +1,62 @@
+import 'package:app_flowy/plugins/grid/application/prelude.dart';
+import 'package:flowy_infra/notifier.dart';
+
+abstract class FocusableBoardCell {
+  set becomeFocus(bool isFocus);
+}
+
+class EditableCellNotifier {
+  final Notifier becomeFirstResponder = Notifier();
+
+  final Notifier resignFirstResponder = Notifier();
+
+  EditableCellNotifier();
+}
+
+class EditableRowNotifier {
+  Map<EditableCellId, EditableCellNotifier> cells = {};
+
+  void insertCell(
+    GridCellIdentifier cellIdentifier,
+    EditableCellNotifier notifier,
+  ) {
+    cells[EditableCellId.from(cellIdentifier)] = notifier;
+  }
+
+  void becomeFirstResponder() {
+    for (final notifier in cells.values) {
+      notifier.becomeFirstResponder.notify();
+    }
+  }
+
+  void resignFirstResponder() {
+    for (final notifier in cells.values) {
+      notifier.resignFirstResponder.notify();
+    }
+  }
+
+  void dispose() {
+    for (final notifier in cells.values) {
+      notifier.resignFirstResponder.notify();
+    }
+
+    cells.clear();
+  }
+}
+
+abstract class EditableCell {
+  EditableCellNotifier? get editableNotifier;
+}
+
+class EditableCellId {
+  String fieldId;
+  String rowId;
+
+  EditableCellId(this.rowId, this.fieldId);
+
+  factory EditableCellId.from(GridCellIdentifier cellIdentifier) =>
+      EditableCellId(
+        cellIdentifier.rowId,
+        cellIdentifier.fieldId,
+      );
+}

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

@@ -6,9 +6,11 @@ import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
 class BoardCheckboxCell extends StatefulWidget {
+  final String groupId;
   final GridCellControllerBuilder cellControllerBuilder;
 
   const BoardCheckboxCell({
+    required this.groupId,
     required this.cellControllerBuilder,
     Key? key,
   }) : super(key: key);
@@ -34,6 +36,8 @@ class _BoardCheckboxCellState extends State<BoardCheckboxCell> {
     return BlocProvider.value(
       value: _cellBloc,
       child: BlocBuilder<BoardCheckboxCellBloc, BoardCheckboxCellState>(
+        buildWhen: (previous, current) =>
+            previous.isSelected != current.isSelected,
         builder: (context, state) {
           final icon = state.isSelected
               ? svgWidget('editor/editor_check')

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

@@ -1,13 +1,16 @@
 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/theme.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 String groupId;
   final GridCellControllerBuilder cellControllerBuilder;
 
   const BoardDateCell({
+    required this.groupId,
     required this.cellControllerBuilder,
     Key? key,
   }) : super(key: key);
@@ -34,6 +37,7 @@ class _BoardDateCellState extends State<BoardDateCell> {
     return BlocProvider.value(
       value: _cellBloc,
       child: BlocBuilder<BoardDateCellBloc, BoardDateCellState>(
+        buildWhen: (previous, current) => previous.dateStr != current.dateStr,
         builder: (context, state) {
           if (state.dateStr.isEmpty) {
             return const SizedBox();
@@ -42,7 +46,8 @@ class _BoardDateCellState extends State<BoardDateCell> {
               alignment: Alignment.centerLeft,
               child: FlowyText.regular(
                 state.dateStr,
-                fontSize: 14,
+                fontSize: 13,
+                color: context.read<AppTheme>().shader3,
               ),
             );
           }

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

@@ -5,9 +5,11 @@ import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
 class BoardNumberCell extends StatefulWidget {
+  final String groupId;
   final GridCellControllerBuilder cellControllerBuilder;
 
   const BoardNumberCell({
+    required this.groupId,
     required this.cellControllerBuilder,
     Key? key,
   }) : super(key: key);
@@ -34,13 +36,14 @@ class _BoardNumberCellState extends State<BoardNumberCell> {
     return BlocProvider.value(
       value: _cellBloc,
       child: BlocBuilder<BoardNumberCellBloc, BoardNumberCellState>(
+        buildWhen: (previous, current) => previous.content != current.content,
         builder: (context, state) {
           if (state.content.isEmpty) {
             return const SizedBox();
           } else {
             return Align(
               alignment: Alignment.centerLeft,
-              child: FlowyText.regular(
+              child: FlowyText.medium(
                 state.content,
                 fontSize: 14,
               ),

+ 60 - 14
frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart

@@ -1,14 +1,22 @@
 import 'package:app_flowy/plugins/board/application/card/board_select_option_cell_bloc.dart';
 import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
 import 'package:app_flowy/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart';
+import 'package:app_flowy/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
-class BoardSelectOptionCell extends StatefulWidget {
+import 'board_cell.dart';
+
+class BoardSelectOptionCell extends StatefulWidget with EditableCell {
+  final String groupId;
   final GridCellControllerBuilder cellControllerBuilder;
+  @override
+  final EditableCellNotifier? editableNotifier;
 
   const BoardSelectOptionCell({
+    required this.groupId,
     required this.cellControllerBuilder,
+    this.editableNotifier,
     Key? key,
   }) : super(key: key);
 
@@ -33,23 +41,41 @@ class _BoardSelectOptionCellState extends State<BoardSelectOptionCell> {
     return BlocProvider.value(
       value: _cellBloc,
       child: BlocBuilder<BoardSelectOptionCellBloc, BoardSelectOptionCellState>(
+        buildWhen: (previous, current) {
+          return previous.selectedOptions != current.selectedOptions;
+        },
         builder: (context, state) {
-          final children = state.selectedOptions
-              .map((option) => SelectOptionTag.fromOption(
+          if (state.selectedOptions
+                  .where((element) => element.id == widget.groupId)
+                  .isNotEmpty ||
+              state.selectedOptions.isEmpty) {
+            return const SizedBox();
+          } else {
+            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,
+                  ),
+                )
+                .toList();
+
+            return IntrinsicHeight(
+              child: Stack(
+                alignment: AlignmentDirectional.center,
+                fit: StackFit.expand,
+                children: [
+                  Padding(
+                    padding: const EdgeInsets.symmetric(vertical: 6),
+                    child: Wrap(spacing: 4, runSpacing: 2, children: children),
+                  ),
+                  _SelectOptionDialog(
+                    controller: widget.cellControllerBuilder.build(),
+                  ),
+                ],
               ),
-            ),
-          );
+            );
+          }
         },
       ),
     );
@@ -61,3 +87,23 @@ class _BoardSelectOptionCellState extends State<BoardSelectOptionCell> {
     super.dispose();
   }
 }
+
+class _SelectOptionDialog extends StatelessWidget {
+  final GridSelectOptionCellController _controller;
+  const _SelectOptionDialog({
+    Key? key,
+    required IGridCellController controller,
+  })  : _controller = controller as GridSelectOptionCellController,
+        super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return InkWell(onTap: () {
+      SelectOptionCellEditor.show(
+        context,
+        _controller,
+        () {},
+      );
+    });
+  }
+}

+ 112 - 21
frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart

@@ -1,13 +1,27 @@
 import 'package:app_flowy/plugins/board/application/card/board_text_cell_bloc.dart';
 import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
+import 'package:app_flowy/plugins/grid/presentation/widgets/cell/cell_builder.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
-class BoardTextCell extends StatefulWidget {
+import 'board_cell.dart';
+import 'define.dart';
+
+class BoardTextCell extends StatefulWidget with EditableCell {
+  final String groupId;
+  final bool isFocus;
+  @override
+  final EditableCellNotifier? editableNotifier;
   final GridCellControllerBuilder cellControllerBuilder;
-  const BoardTextCell({required this.cellControllerBuilder, Key? key})
-      : super(key: key);
+
+  const BoardTextCell({
+    required this.groupId,
+    required this.cellControllerBuilder,
+    this.editableNotifier,
+    this.isFocus = false,
+    Key? key,
+  }) : super(key: key);
 
   @override
   State<BoardTextCell> createState() => _BoardTextCellState();
@@ -15,14 +29,48 @@ class BoardTextCell extends StatefulWidget {
 
 class _BoardTextCellState extends State<BoardTextCell> {
   late BoardTextCellBloc _cellBloc;
+  late TextEditingController _controller;
+  bool focusWhenInit = false;
+  SingleListenerFocusNode focusNode = SingleListenerFocusNode();
 
   @override
   void initState() {
     final cellController =
         widget.cellControllerBuilder.build() as GridCellController;
-
     _cellBloc = BoardTextCellBloc(cellController: cellController)
       ..add(const BoardTextCellEvent.initial());
+    _controller = TextEditingController(text: _cellBloc.state.content);
+    focusWhenInit = widget.isFocus;
+
+    if (widget.isFocus) {
+      focusNode.requestFocus();
+    }
+
+    focusNode.addListener(() {
+      if (!focusNode.hasFocus) {
+        _cellBloc.add(const BoardTextCellEvent.enableEdit(false));
+
+        if (focusWhenInit) {
+          setState(() {
+            focusWhenInit = false;
+          });
+        }
+      }
+    });
+
+    widget.editableNotifier?.becomeFirstResponder.addListener(() {
+      if (!mounted) return;
+      WidgetsBinding.instance.addPostFrameCallback((_) {
+        focusNode.requestFocus();
+      });
+      _cellBloc.add(const BoardTextCellEvent.enableEdit(true));
+    });
+
+    widget.editableNotifier?.resignFirstResponder.addListener(() {
+      if (!mounted) return;
+      _cellBloc.add(const BoardTextCellEvent.enableEdit(false));
+    });
+
     super.initState();
   }
 
@@ -30,32 +78,75 @@ class _BoardTextCellState extends State<BoardTextCell> {
   Widget build(BuildContext context) {
     return BlocProvider.value(
       value: _cellBloc,
-      child: BlocBuilder<BoardTextCellBloc, BoardTextCellState>(
-        builder: (context, state) {
-          if (state.content.isEmpty) {
-            return const SizedBox();
-          } else {
-            return Align(
-              alignment: Alignment.centerLeft,
-              child: ConstrainedBox(
-                constraints: BoxConstraints.loose(
-                  const Size(double.infinity, 100),
-                ),
-                child: FlowyText.regular(
-                  state.content,
-                  fontSize: 14,
-                ),
-              ),
-            );
+      child: BlocListener<BoardTextCellBloc, BoardTextCellState>(
+        listener: (context, state) {
+          if (_controller.text != state.content) {
+            _controller.text = state.content;
           }
         },
+        child: BlocBuilder<BoardTextCellBloc, BoardTextCellState>(
+          builder: (context, state) {
+            if (state.content.isEmpty &&
+                state.enableEdit == false &&
+                focusWhenInit == false) {
+              return const SizedBox();
+            }
+
+            //
+            Widget child;
+            if (state.enableEdit || focusWhenInit) {
+              child = _buildTextField();
+            } else {
+              child = _buildText(state);
+            }
+            return Align(alignment: Alignment.centerLeft, child: child);
+          },
+        ),
       ),
     );
   }
 
+  Future<void> focusChanged() async {
+    _cellBloc.add(BoardTextCellEvent.updateText(_controller.text));
+  }
+
   @override
   Future<void> dispose() async {
     _cellBloc.close();
+    _controller.dispose();
+    focusNode.dispose();
     super.dispose();
   }
+
+  Widget _buildText(BoardTextCellState state) {
+    return Padding(
+      padding: EdgeInsets.symmetric(
+        vertical: BoardSizes.cardCellVPadding,
+      ),
+      child: FlowyText.medium(state.content, fontSize: 14),
+    );
+  }
+
+  Widget _buildTextField() {
+    return TextField(
+      controller: _controller,
+      focusNode: focusNode,
+      onChanged: (value) => focusChanged(),
+      onEditingComplete: () => focusNode.unfocus(),
+      maxLines: 1,
+      style: const TextStyle(
+        fontSize: 14,
+        fontWeight: FontWeight.w500,
+        fontFamily: 'Mulish',
+      ),
+      decoration: InputDecoration(
+        // Magic number 4 makes the textField take up the same space as FlowyText
+        contentPadding: EdgeInsets.symmetric(
+          vertical: BoardSizes.cardCellVPadding + 4,
+        ),
+        border: InputBorder.none,
+        isDense: true,
+      ),
+    );
+  }
 }

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

@@ -5,9 +5,11 @@ import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
 class BoardUrlCell extends StatefulWidget {
+  final String groupId;
   final GridCellControllerBuilder cellControllerBuilder;
 
   const BoardUrlCell({
+    required this.groupId,
     required this.cellControllerBuilder,
     Key? key,
   }) : super(key: key);
@@ -34,6 +36,7 @@ class _BoardUrlCellState extends State<BoardUrlCell> {
     return BlocProvider.value(
       value: _cellBloc,
       child: BlocBuilder<BoardURLCellBloc, BoardURLCellState>(
+        buildWhen: (previous, current) => previous.content != current.content,
         builder: (context, state) {
           if (state.content.isEmpty) {
             return const SizedBox();

+ 85 - 15
frontend/app_flowy/lib/plugins/board/presentation/card/card.dart

@@ -7,25 +7,26 @@ 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 'board_cell.dart';
 import 'card_cell_builder.dart';
 import 'card_container.dart';
 
-typedef OnEndEditing = void Function(String rowId);
-
 class BoardCard extends StatefulWidget {
   final String gridId;
+  final String groupId;
+  final String fieldId;
   final bool isEditing;
   final CardDataController dataController;
   final BoardCellBuilder cellBuilder;
-  final OnEndEditing onEditEditing;
   final void Function(BuildContext) openCard;
 
   const BoardCard({
     required this.gridId,
+    required this.groupId,
+    required this.fieldId,
     required this.isEditing,
     required this.dataController,
     required this.cellBuilder,
-    required this.onEditEditing,
     required this.openCard,
     Key? key,
   }) : super(key: key);
@@ -36,13 +37,16 @@ class BoardCard extends StatefulWidget {
 
 class _BoardCardState extends State<BoardCard> {
   late BoardCardBloc _cardBloc;
+  late EditableRowNotifier rowNotifier;
 
   @override
   void initState() {
+    rowNotifier = EditableRowNotifier();
     _cardBloc = BoardCardBloc(
       gridId: widget.gridId,
+      fieldId: widget.fieldId,
       dataController: widget.dataController,
-    );
+    )..add(const BoardCardEvent.initial());
     super.initState();
   }
 
@@ -51,16 +55,28 @@ class _BoardCardState extends State<BoardCard> {
     return BlocProvider.value(
       value: _cardBloc,
       child: BlocBuilder<BoardCardBloc, BoardCardState>(
+        buildWhen: (previous, current) {
+          return previous.cells.length != current.cells.length;
+        },
         builder: (context, state) {
           return BoardCardContainer(
             accessoryBuilder: (context) {
-              return [const _CardMoreOption()];
+              return [
+                _CardEditOption(
+                  startEditing: () => rowNotifier.becomeFirstResponder(),
+                ),
+                const _CardMoreOption(),
+              ];
             },
             onTap: (context) {
               widget.openCard(context);
             },
             child: Column(
-              children: _makeCells(context, state.gridCellMap),
+              mainAxisSize: MainAxisSize.min,
+              children: _makeCells(
+                context,
+                state.cells.map((cell) => cell.identifier).toList(),
+              ),
             ),
           );
         },
@@ -68,16 +84,42 @@ class _BoardCardState extends State<BoardCard> {
     );
   }
 
-  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),
+  List<Widget> _makeCells(
+    BuildContext context,
+    List<GridCellIdentifier> cells,
+  ) {
+    final List<Widget> children = [];
+    cells.asMap().forEach(
+      (int index, GridCellIdentifier cellId) {
+        final cellNotifier = EditableCellNotifier();
+        Widget child = widget.cellBuilder.buildCell(
+          widget.groupId,
+          cellId,
+          widget.isEditing,
+          cellNotifier,
+        );
+
+        if (index == 0) {
+          rowNotifier.insertCell(cellId, cellNotifier);
+        }
+
+        child = Padding(
+          key: cellId.key(),
+          padding: const EdgeInsets.only(left: 4, right: 4),
           child: child,
         );
+
+        children.add(child);
       },
-    ).toList();
+    );
+    return children;
+  }
+
+  @override
+  Future<void> dispose() async {
+    rowNotifier.dispose();
+    _cardBloc.close();
+    super.dispose();
   }
 }
 
@@ -86,7 +128,11 @@ class _CardMoreOption extends StatelessWidget with CardAccessory {
 
   @override
   Widget build(BuildContext context) {
-    return svgWidget('home/details', color: context.read<AppTheme>().iconColor);
+    return Padding(
+      padding: const EdgeInsets.all(3.0),
+      child:
+          svgWidget('grid/details', color: context.read<AppTheme>().iconColor),
+    );
   }
 
   @override
@@ -96,3 +142,27 @@ class _CardMoreOption extends StatelessWidget with CardAccessory {
     ).show(context, direction: AnchorDirection.bottomWithCenterAligned);
   }
 }
+
+class _CardEditOption extends StatelessWidget with CardAccessory {
+  final VoidCallback startEditing;
+  const _CardEditOption({
+    required this.startEditing,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: const EdgeInsets.all(3.0),
+      child: svgWidget(
+        'editor/edit',
+        color: context.read<AppTheme>().iconColor,
+      ),
+    );
+  }
+
+  @override
+  void onTap(BuildContext context) {
+    startEditing();
+  }
+}

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

@@ -2,6 +2,7 @@ import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_servic
 import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
 import 'package:flutter/material.dart';
 
+import 'board_cell.dart';
 import 'board_checkbox_cell.dart';
 import 'board_date_cell.dart';
 import 'board_number_cell.dart';
@@ -19,7 +20,12 @@ class BoardCellBuilder {
 
   BoardCellBuilder(this.delegate);
 
-  Widget buildCell(GridCellIdentifier cellId) {
+  Widget buildCell(
+    String groupId,
+    GridCellIdentifier cellId,
+    bool isEditing,
+    EditableCellNotifier cellNotifier,
+  ) {
     final cellControllerBuilder = GridCellControllerBuilder(
       delegate: delegate,
       cellId: cellId,
@@ -30,36 +36,46 @@ class BoardCellBuilder {
     switch (cellId.fieldType) {
       case FieldType.Checkbox:
         return BoardCheckboxCell(
+          groupId: groupId,
           cellControllerBuilder: cellControllerBuilder,
           key: key,
         );
       case FieldType.DateTime:
         return BoardDateCell(
+          groupId: groupId,
           cellControllerBuilder: cellControllerBuilder,
           key: key,
         );
       case FieldType.SingleSelect:
         return BoardSelectOptionCell(
+          groupId: groupId,
           cellControllerBuilder: cellControllerBuilder,
           key: key,
         );
       case FieldType.MultiSelect:
         return BoardSelectOptionCell(
+          groupId: groupId,
           cellControllerBuilder: cellControllerBuilder,
+          editableNotifier: cellNotifier,
           key: key,
         );
       case FieldType.Number:
         return BoardNumberCell(
+          groupId: groupId,
           cellControllerBuilder: cellControllerBuilder,
           key: key,
         );
       case FieldType.RichText:
         return BoardTextCell(
+          groupId: groupId,
           cellControllerBuilder: cellControllerBuilder,
+          isFocus: isEditing,
+          editableNotifier: cellNotifier,
           key: key,
         );
       case FieldType.URL:
         return BoardUrlCell(
+          groupId: groupId,
           cellControllerBuilder: cellControllerBuilder,
           key: key,
         );

+ 34 - 10
frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart

@@ -26,8 +26,8 @@ class BoardCardContainer extends StatelessWidget {
             final accessories = accessoryBuilder!(context);
             if (accessories.isNotEmpty) {
               container = _CardEnterRegion(
-                child: container,
                 accessories: accessories,
+                child: container,
               );
             }
           }
@@ -69,25 +69,48 @@ class CardAccessoryContainer extends StatelessWidget {
         style: HoverStyle(
           hoverColor: theme.hover,
           backgroundColor: theme.surface,
+          borderRadius: BorderRadius.zero,
         ),
-        builder: (_, onHover) => Container(
-          width: 26,
-          height: 26,
-          padding: const EdgeInsets.all(3),
+        builder: (_, onHover) => SizedBox(
+          width: 24,
+          height: 24,
           child: accessory,
         ),
       );
       return GestureDetector(
-        child: hover,
         behavior: HitTestBehavior.opaque,
         onTap: () => accessory.onTap(context),
+        child: hover,
       );
     }).toList();
 
-    return Wrap(children: children, spacing: 6);
+    return Container(
+      clipBehavior: Clip.hardEdge,
+      decoration: _makeBoxDecoration(context),
+      child: Row(children: children),
+    );
   }
 }
 
+BoxDecoration _makeBoxDecoration(BuildContext context) {
+  final theme = context.read<AppTheme>();
+  final borderSide = BorderSide(color: theme.shader6, width: 1.0);
+  return BoxDecoration(
+    color: Colors.transparent,
+    border: Border.fromBorderSide(borderSide),
+    // boxShadow: const [
+    //   BoxShadow(
+    //     color: Colors.transparent,
+    //     spreadRadius: 0,
+    //     blurRadius: 5,
+    //     offset: Offset.zero,
+    //   )
+    // ],
+
+    borderRadius: const BorderRadius.all(Radius.circular(4)),
+  );
+}
+
 class _CardEnterRegion extends StatelessWidget {
   final Widget child;
   final List<CardAccessory> accessories;
@@ -102,8 +125,9 @@ class _CardEnterRegion extends StatelessWidget {
       builder: (context, onEnter, _) {
         List<Widget> children = [child];
         if (onEnter) {
-          children.add(CardAccessoryContainer(accessories: accessories)
-              .positioned(right: 0));
+          children.add(CardAccessoryContainer(
+            accessories: accessories,
+          ).positioned(right: 0));
         }
 
         return MouseRegion(
@@ -116,7 +140,7 @@ class _CardEnterRegion extends StatelessWidget {
                   .onEnter = false,
           child: IntrinsicHeight(
               child: Stack(
-            alignment: AlignmentDirectional.center,
+            alignment: AlignmentDirectional.topEnd,
             fit: StackFit.expand,
             children: children,
           )),

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

@@ -0,0 +1,3 @@
+class BoardSizes {
+  static double get cardCellVPadding => 6;
+}

+ 168 - 0
frontend/app_flowy/lib/plugins/board/presentation/toolbar/board_setting.dart

@@ -0,0 +1,168 @@
+import 'package:app_flowy/generated/locale_keys.g.dart';
+import 'package:app_flowy/plugins/board/application/toolbar/board_setting_bloc.dart';
+import 'package:app_flowy/plugins/grid/application/field/field_cache.dart';
+import 'package:app_flowy/plugins/grid/presentation/layout/sizes.dart';
+import 'package:app_flowy/plugins/grid/presentation/widgets/toolbar/grid_property.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra/image.dart';
+import 'package:flowy_infra/theme.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flowy_infra_ui/style_widget/button.dart';
+import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
+import 'package:flowy_infra_ui/style_widget/text.dart';
+import 'package:flowy_infra_ui/widget/spacing.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'board_toolbar.dart';
+
+class BoardSettingContext {
+  final String viewId;
+  final GridFieldCache fieldCache;
+  BoardSettingContext({
+    required this.viewId,
+    required this.fieldCache,
+  });
+
+  factory BoardSettingContext.from(BoardToolbarContext toolbarContext) =>
+      BoardSettingContext(
+        viewId: toolbarContext.viewId,
+        fieldCache: toolbarContext.fieldCache,
+      );
+}
+
+class BoardSettingList extends StatelessWidget {
+  final BoardSettingContext settingContext;
+  final Function(BoardSettingAction, BoardSettingContext) onAction;
+  const BoardSettingList({
+    required this.settingContext,
+    required this.onAction,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocProvider(
+      create: (context) => BoardSettingBloc(gridId: settingContext.viewId),
+      child: BlocListener<BoardSettingBloc, BoardSettingState>(
+        listenWhen: (previous, current) =>
+            previous.selectedAction != current.selectedAction,
+        listener: (context, state) {
+          state.selectedAction.foldLeft(null, (_, action) {
+            FlowyOverlay.of(context).remove(identifier());
+            onAction(action, settingContext);
+          });
+        },
+        child: BlocBuilder<BoardSettingBloc, BoardSettingState>(
+          builder: (context, state) {
+            return _renderList();
+          },
+        ),
+      ),
+    );
+  }
+
+  Widget _renderList() {
+    final cells = BoardSettingAction.values.map((action) {
+      return _SettingItem(action: action);
+    }).toList();
+
+    return SizedBox(
+      width: 140,
+      child: ListView.separated(
+        shrinkWrap: true,
+        controller: ScrollController(),
+        itemCount: cells.length,
+        separatorBuilder: (context, index) {
+          return VSpace(GridSize.typeOptionSeparatorHeight);
+        },
+        physics: StyledScrollPhysics(),
+        itemBuilder: (BuildContext context, int index) {
+          return cells[index];
+        },
+      ),
+    );
+  }
+
+  static void show(BuildContext context, BoardSettingContext settingContext) {
+    final list = BoardSettingList(
+      settingContext: settingContext,
+      onAction: (action, settingContext) {
+        switch (action) {
+          case BoardSettingAction.properties:
+            GridPropertyList(
+                    gridId: settingContext.viewId,
+                    fieldCache: settingContext.fieldCache)
+                .show(context);
+            break;
+        }
+      },
+    );
+
+    FlowyOverlay.of(context).insertWithAnchor(
+      widget: OverlayContainer(
+        constraints: BoxConstraints.loose(const Size(140, 400)),
+        child: list,
+      ),
+      identifier: identifier(),
+      anchorContext: context,
+      anchorDirection: AnchorDirection.bottomRight,
+      style: FlowyOverlayStyle(blur: false),
+    );
+  }
+
+  static String identifier() {
+    return (BoardSettingList).toString();
+  }
+}
+
+class _SettingItem extends StatelessWidget {
+  final BoardSettingAction action;
+
+  const _SettingItem({
+    required this.action,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = context.read<AppTheme>();
+    final isSelected = context
+        .read<BoardSettingBloc>()
+        .state
+        .selectedAction
+        .foldLeft(false, (_, selectedAction) => selectedAction == action);
+
+    return SizedBox(
+      height: 30,
+      child: FlowyButton(
+        isSelected: isSelected,
+        text: FlowyText.medium(action.title(),
+            fontSize: 12, color: theme.textColor),
+        hoverColor: theme.hover,
+        onTap: () {
+          context
+              .read<BoardSettingBloc>()
+              .add(BoardSettingEvent.performAction(action));
+        },
+        leftIcon: svgWidget(action.iconName(), color: theme.iconColor),
+      ),
+    );
+  }
+}
+
+extension _GridSettingExtension on BoardSettingAction {
+  String iconName() {
+    switch (this) {
+      case BoardSettingAction.properties:
+        return 'grid/setting/properties';
+    }
+  }
+
+  String title() {
+    switch (this) {
+      case BoardSettingAction.properties:
+        return LocaleKeys.grid_settings_Properties.tr();
+    }
+  }
+}

+ 60 - 0
frontend/app_flowy/lib/plugins/board/presentation/toolbar/board_toolbar.dart

@@ -0,0 +1,60 @@
+import 'package:app_flowy/plugins/grid/application/field/field_cache.dart';
+import 'package:flowy_infra/image.dart';
+import 'package:flowy_infra/theme.dart';
+import 'package:flowy_infra_ui/style_widget/icon_button.dart';
+import 'package:flutter/widgets.dart';
+import 'package:provider/provider.dart';
+
+import 'board_setting.dart';
+
+class BoardToolbarContext {
+  final String viewId;
+  final GridFieldCache fieldCache;
+
+  BoardToolbarContext({
+    required this.viewId,
+    required this.fieldCache,
+  });
+}
+
+class BoardToolbar extends StatelessWidget {
+  final BoardToolbarContext toolbarContext;
+  const BoardToolbar({
+    required this.toolbarContext,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return SizedBox(
+      height: 40,
+      child: Row(
+        children: [
+          _SettingButton(
+            settingContext: BoardSettingContext.from(toolbarContext),
+          ),
+        ],
+      ),
+    );
+  }
+}
+
+class _SettingButton extends StatelessWidget {
+  final BoardSettingContext settingContext;
+  const _SettingButton({required this.settingContext, Key? key})
+      : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = context.read<AppTheme>();
+    return FlowyIconButton(
+      hoverColor: theme.hover,
+      width: 22,
+      onPressed: () => BoardSettingList.show(context, settingContext),
+      icon: Padding(
+        padding: const EdgeInsets.symmetric(vertical: 3.0, horizontal: 3.0),
+        child: svgWidget("grid/setting/setting"),
+      ),
+    );
+  }
+}

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

@@ -40,11 +40,11 @@ class DocumentBanner extends StatelessWidget {
                   downColor: theme.main1,
                   outlineColor: Colors.white,
                   borderRadius: Corners.s8Border,
+                  onPressed: onRestore,
                   child: FlowyText.medium(
                       LocaleKeys.deletePagePrompt_restore.tr(),
                       color: Colors.white,
-                      fontSize: 14),
-                  onPressed: onRestore),
+                      fontSize: 14)),
               const HSpace(20),
               BaseStyledButton(
                   minWidth: 220,
@@ -55,11 +55,11 @@ class DocumentBanner extends StatelessWidget {
                   downColor: theme.main1,
                   outlineColor: Colors.white,
                   borderRadius: Corners.s8Border,
+                  onPressed: onDelete,
                   child: FlowyText.medium(
                       LocaleKeys.deletePagePrompt_deletePermanent.tr(),
                       color: Colors.white,
-                      fontSize: 14),
-                  onPressed: onDelete),
+                      fontSize: 14)),
             ],
           ),
         ),

+ 9 - 4
frontend/app_flowy/lib/plugins/doc/presentation/style_widgets.dart

@@ -16,7 +16,10 @@ class EditorCheckboxBuilder extends QuillCheckboxBuilder {
   EditorCheckboxBuilder(this.theme);
 
   @override
-  Widget build({required BuildContext context, required bool isChecked, required ValueChanged<bool> onChanged}) {
+  Widget build(
+      {required BuildContext context,
+      required bool isChecked,
+      required ValueChanged<bool> onChanged}) {
     return FlowyEditorCheckbox(
       theme: theme,
       isChecked: isChecked,
@@ -37,10 +40,10 @@ class FlowyEditorCheckbox extends StatefulWidget {
   }) : super(key: key);
 
   @override
-  _FlowyEditorCheckboxState createState() => _FlowyEditorCheckboxState();
+  FlowyEditorCheckboxState createState() => FlowyEditorCheckboxState();
 }
 
-class _FlowyEditorCheckboxState extends State<FlowyEditorCheckbox> {
+class FlowyEditorCheckboxState extends State<FlowyEditorCheckbox> {
   late bool isChecked;
 
   @override
@@ -51,7 +54,9 @@ class _FlowyEditorCheckboxState extends State<FlowyEditorCheckbox> {
 
   @override
   Widget build(BuildContext context) {
-    final icon = isChecked ? svgWidget('editor/editor_check') : svgWidget('editor/editor_uncheck');
+    final icon = isChecked
+        ? svgWidget('editor/editor_check')
+        : svgWidget('editor/editor_uncheck');
     return Align(
       alignment: Alignment.centerLeft,
       child: FlowyIconButton(

+ 2 - 2
frontend/app_flowy/lib/plugins/doc/presentation/toolbar/check_button.dart

@@ -28,10 +28,10 @@ class FlowyCheckListButton extends StatefulWidget {
   final String tooltipText;
 
   @override
-  _FlowyCheckListButtonState createState() => _FlowyCheckListButtonState();
+  FlowyCheckListButtonState createState() => FlowyCheckListButtonState();
 }
 
-class _FlowyCheckListButtonState extends State<FlowyCheckListButton> {
+class FlowyCheckListButtonState extends State<FlowyCheckListButton> {
   bool? _isToggled;
 
   Style get _selectionStyle => widget.controller.getSelectionStyle();

+ 42 - 22
frontend/app_flowy/lib/plugins/doc/presentation/toolbar/color_picker.dart

@@ -24,10 +24,10 @@ class FlowyColorButton extends StatefulWidget {
   final QuillIconTheme? iconTheme;
 
   @override
-  _FlowyColorButtonState createState() => _FlowyColorButtonState();
+  FlowyColorButtonState createState() => FlowyColorButtonState();
 }
 
-class _FlowyColorButtonState extends State<FlowyColorButton> {
+class FlowyColorButtonState extends State<FlowyColorButton> {
   late bool _isToggledColor;
   late bool _isToggledBackground;
   late bool _isWhite;
@@ -37,10 +37,14 @@ class _FlowyColorButtonState extends State<FlowyColorButton> {
 
   void _didChangeEditingValue() {
     setState(() {
-      _isToggledColor = _getIsToggledColor(widget.controller.getSelectionStyle().attributes);
-      _isToggledBackground = _getIsToggledBackground(widget.controller.getSelectionStyle().attributes);
-      _isWhite = _isToggledColor && _selectionStyle.attributes['color']!.value == '#ffffff';
-      _isWhitebackground = _isToggledBackground && _selectionStyle.attributes['background']!.value == '#ffffff';
+      _isToggledColor =
+          _getIsToggledColor(widget.controller.getSelectionStyle().attributes);
+      _isToggledBackground = _getIsToggledBackground(
+          widget.controller.getSelectionStyle().attributes);
+      _isWhite = _isToggledColor &&
+          _selectionStyle.attributes['color']!.value == '#ffffff';
+      _isWhitebackground = _isToggledBackground &&
+          _selectionStyle.attributes['background']!.value == '#ffffff';
     });
   }
 
@@ -49,8 +53,10 @@ class _FlowyColorButtonState extends State<FlowyColorButton> {
     super.initState();
     _isToggledColor = _getIsToggledColor(_selectionStyle.attributes);
     _isToggledBackground = _getIsToggledBackground(_selectionStyle.attributes);
-    _isWhite = _isToggledColor && _selectionStyle.attributes['color']!.value == '#ffffff';
-    _isWhitebackground = _isToggledBackground && _selectionStyle.attributes['background']!.value == '#ffffff';
+    _isWhite = _isToggledColor &&
+        _selectionStyle.attributes['color']!.value == '#ffffff';
+    _isWhitebackground = _isToggledBackground &&
+        _selectionStyle.attributes['background']!.value == '#ffffff';
     widget.controller.addListener(_didChangeEditingValue);
   }
 
@@ -69,9 +75,12 @@ class _FlowyColorButtonState extends State<FlowyColorButton> {
       oldWidget.controller.removeListener(_didChangeEditingValue);
       widget.controller.addListener(_didChangeEditingValue);
       _isToggledColor = _getIsToggledColor(_selectionStyle.attributes);
-      _isToggledBackground = _getIsToggledBackground(_selectionStyle.attributes);
-      _isWhite = _isToggledColor && _selectionStyle.attributes['color']!.value == '#ffffff';
-      _isWhitebackground = _isToggledBackground && _selectionStyle.attributes['background']!.value == '#ffffff';
+      _isToggledBackground =
+          _getIsToggledBackground(_selectionStyle.attributes);
+      _isWhite = _isToggledColor &&
+          _selectionStyle.attributes['color']!.value == '#ffffff';
+      _isWhitebackground = _isToggledBackground &&
+          _selectionStyle.attributes['background']!.value == '#ffffff';
     }
   }
 
@@ -88,9 +97,10 @@ class _FlowyColorButtonState extends State<FlowyColorButton> {
     final fillColor = _isToggledColor && !widget.background && _isWhite
         ? stringToColor('#ffffff')
         : (widget.iconTheme?.iconUnselectedFillColor ?? theme.canvasColor);
-    final fillColorBackground = _isToggledBackground && widget.background && _isWhitebackground
-        ? stringToColor('#ffffff')
-        : (widget.iconTheme?.iconUnselectedFillColor ?? theme.canvasColor);
+    final fillColorBackground =
+        _isToggledBackground && widget.background && _isWhitebackground
+            ? stringToColor('#ffffff')
+            : (widget.iconTheme?.iconUnselectedFillColor ?? theme.canvasColor);
 
     return Tooltip(
       message: LocaleKeys.toolbar_highlight.tr(),
@@ -99,7 +109,8 @@ class _FlowyColorButtonState extends State<FlowyColorButton> {
         highlightElevation: 0,
         hoverElevation: 0,
         size: widget.iconSize * kIconButtonFactor,
-        icon: Icon(widget.icon, size: widget.iconSize, color: theme.iconTheme.color),
+        icon: Icon(widget.icon,
+            size: widget.iconSize, color: theme.iconTheme.color),
         fillColor: widget.background ? fillColorBackground : fillColor,
         onPressed: _showColorPicker,
       ),
@@ -112,13 +123,16 @@ class _FlowyColorButtonState extends State<FlowyColorButton> {
       hex = hex.substring(2);
     }
     hex = '#$hex';
-    widget.controller.formatSelection(widget.background ? BackgroundAttribute(hex) : ColorAttribute(hex));
+    widget.controller.formatSelection(
+        widget.background ? BackgroundAttribute(hex) : ColorAttribute(hex));
     Navigator.of(context).pop();
   }
 
   void _showColorPicker() {
     final style = widget.controller.getSelectionStyle();
-    final values = style.values.where((v) => v.key == Attribute.background.key).map((v) => v.value);
+    final values = style.values
+        .where((v) => v.key == Attribute.background.key)
+        .map((v) => v.value);
     int initialColor = 0;
     if (values.isNotEmpty) {
       assert(values.length == 1);
@@ -160,7 +174,9 @@ class FlowyColorPicker extends StatefulWidget {
   ];
   final Function(Color?) onColorChanged;
   final int initialColor;
-  FlowyColorPicker({Key? key, required this.onColorChanged, this.initialColor = 0}) : super(key: key);
+  FlowyColorPicker(
+      {Key? key, required this.onColorChanged, this.initialColor = 0})
+      : super(key: key);
 
   @override
   State<FlowyColorPicker> createState() => _FlowyColorPickerState();
@@ -178,8 +194,10 @@ class _FlowyColorPickerState extends State<FlowyColorPicker> {
     const double crossAxisSpacing = 10;
     final numberOfRows = (widget.colors.length / crossAxisCount).ceil();
 
-    const perRowHeight = ((width - ((crossAxisCount - 1) * mainAxisSpacing)) / crossAxisCount);
-    final totalHeight = numberOfRows * perRowHeight + numberOfRows * crossAxisSpacing;
+    const perRowHeight =
+        ((width - ((crossAxisCount - 1) * mainAxisSpacing)) / crossAxisCount);
+    final totalHeight =
+        numberOfRows * perRowHeight + numberOfRows * crossAxisSpacing;
 
     return Container(
       constraints: BoxConstraints.tightFor(width: width, height: totalHeight),
@@ -198,7 +216,8 @@ class _FlowyColorPickerState extends State<FlowyColorPicker> {
             delegate: SliverChildBuilderDelegate(
               (BuildContext context, int index) {
                 if (widget.colors.length > index) {
-                  final isSelected = widget.colors[index] == widget.initialColor;
+                  final isSelected =
+                      widget.colors[index] == widget.initialColor;
                   return ColorItem(
                     color: Color(widget.colors[index]),
                     onPressed: widget.onColorChanged,
@@ -242,7 +261,8 @@ class ColorItem extends StatelessWidget {
       );
     } else {
       return RawMaterialButton(
-        shape: const CircleBorder(side: BorderSide(color: Colors.white, width: 8)) +
+        shape: const CircleBorder(
+                side: BorderSide(color: Colors.white, width: 8)) +
             CircleBorder(side: BorderSide(color: color, width: 4)),
         onPressed: () {
           if (isSelected) {

+ 21 - 14
frontend/app_flowy/lib/plugins/doc/presentation/toolbar/header_button.dart

@@ -16,10 +16,10 @@ class FlowyHeaderStyleButton extends StatefulWidget {
   final double iconSize;
 
   @override
-  _FlowyHeaderStyleButtonState createState() => _FlowyHeaderStyleButtonState();
+  FlowyHeaderStyleButtonState createState() => FlowyHeaderStyleButtonState();
 }
 
-class _FlowyHeaderStyleButtonState extends State<FlowyHeaderStyleButton> {
+class FlowyHeaderStyleButtonState extends State<FlowyHeaderStyleButton> {
   Attribute? _value;
 
   Style get _selectionStyle => widget.controller.getSelectionStyle();
@@ -28,22 +28,27 @@ class _FlowyHeaderStyleButtonState extends State<FlowyHeaderStyleButton> {
   void initState() {
     super.initState();
     setState(() {
-      _value = _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header;
+      _value =
+          _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header;
     });
     widget.controller.addListener(_didChangeEditingValue);
   }
 
   @override
   Widget build(BuildContext context) {
-    final _valueToText = <Attribute, String>{
+    final valueToText = <Attribute, String>{
       Attribute.h1: 'H1',
       Attribute.h2: 'H2',
       Attribute.h3: 'H3',
     };
 
-    final _valueAttribute = <Attribute>[Attribute.h1, Attribute.h2, Attribute.h3];
-    final _valueString = <String>['H1', 'H2', 'H3'];
-    final _attributeImageName = <String>['editor/H1', 'editor/H2', 'editor/H3'];
+    final valueAttribute = <Attribute>[
+      Attribute.h1,
+      Attribute.h2,
+      Attribute.h3
+    ];
+    final valueString = <String>['H1', 'H2', 'H3'];
+    final attributeImageName = <String>['editor/H1', 'editor/H2', 'editor/H3'];
 
     return Row(
       mainAxisSize: MainAxisSize.min,
@@ -52,18 +57,18 @@ class _FlowyHeaderStyleButtonState extends State<FlowyHeaderStyleButton> {
         //     _valueToText[_value] == _valueString[index] ? svg('editor/H1', color: Colors.white) : svg('editor/H1');
 
         final headerTitle = "${LocaleKeys.toolbar_header.tr()} ${index + 1}";
-        final _isToggled = _valueToText[_value] == _valueString[index];
+        final isToggled = valueToText[_value] == valueString[index];
         return ToolbarIconButton(
           onPressed: () {
-            if (_isToggled) {
+            if (isToggled) {
               widget.controller.formatSelection(Attribute.header);
             } else {
-              widget.controller.formatSelection(_valueAttribute[index]);
+              widget.controller.formatSelection(valueAttribute[index]);
             }
           },
           width: widget.iconSize * kIconButtonFactor,
-          iconName: _attributeImageName[index],
-          isToggled: _isToggled,
+          iconName: attributeImageName[index],
+          isToggled: isToggled,
           tooltipText: headerTitle,
         );
       }),
@@ -72,7 +77,8 @@ class _FlowyHeaderStyleButtonState extends State<FlowyHeaderStyleButton> {
 
   void _didChangeEditingValue() {
     setState(() {
-      _value = _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header;
+      _value =
+          _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header;
     });
   }
 
@@ -82,7 +88,8 @@ class _FlowyHeaderStyleButtonState extends State<FlowyHeaderStyleButton> {
     if (oldWidget.controller != widget.controller) {
       oldWidget.controller.removeListener(_didChangeEditingValue);
       widget.controller.addListener(_didChangeEditingValue);
-      _value = _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header;
+      _value =
+          _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header;
     }
   }
 

+ 5 - 3
frontend/app_flowy/lib/plugins/doc/presentation/toolbar/link_button.dart

@@ -19,10 +19,10 @@ class FlowyLinkStyleButton extends StatefulWidget {
   final double iconSize;
 
   @override
-  _FlowyLinkStyleButtonState createState() => _FlowyLinkStyleButtonState();
+  FlowyLinkStyleButtonState createState() => FlowyLinkStyleButtonState();
 }
 
-class _FlowyLinkStyleButtonState extends State<FlowyLinkStyleButton> {
+class FlowyLinkStyleButtonState extends State<FlowyLinkStyleButton> {
   void _didChangeSelection() {
     setState(() {});
   }
@@ -75,7 +75,9 @@ class _FlowyLinkStyleButtonState extends State<FlowyLinkStyleButton> {
 
   void _openLinkDialog(BuildContext context) {
     final style = widget.controller.getSelectionStyle();
-    final values = style.values.where((v) => v.key == Attribute.link.key).map((v) => v.value);
+    final values = style.values
+        .where((v) => v.key == Attribute.link.key)
+        .map((v) => v.value);
     String value = "";
     if (values.isNotEmpty) {
       assert(values.length == 1);

+ 5 - 3
frontend/app_flowy/lib/plugins/doc/presentation/toolbar/toggle_button.dart

@@ -21,10 +21,10 @@ class FlowyToggleStyleButton extends StatefulWidget {
   }) : super(key: key);
 
   @override
-  _ToggleStyleButtonState createState() => _ToggleStyleButtonState();
+  ToggleStyleButtonState createState() => ToggleStyleButtonState();
 }
 
-class _ToggleStyleButtonState extends State<FlowyToggleStyleButton> {
+class ToggleStyleButtonState extends State<FlowyToggleStyleButton> {
   bool? _isToggled;
   Style get _selectionStyle => widget.controller.getSelectionStyle();
   @override
@@ -77,6 +77,8 @@ class _ToggleStyleButtonState extends State<FlowyToggleStyleButton> {
   }
 
   void _toggleAttribute() {
-    widget.controller.formatSelection(_isToggled! ? Attribute.clone(widget.attribute, null) : widget.attribute);
+    widget.controller.formatSelection(_isToggled!
+        ? Attribute.clone(widget.attribute, null)
+        : widget.attribute);
   }
 }

+ 11 - 6
frontend/app_flowy/lib/plugins/doc/presentation/toolbar/tool_bar.dart

@@ -32,7 +32,8 @@ class EditorToolbar extends StatelessWidget implements PreferredSizeWidget {
     return Container(
       color: Theme.of(context).canvasColor,
       constraints: BoxConstraints.tightFor(height: preferredSize.height),
-      child: ToolbarButtonList(buttons: children).padding(horizontal: 4, vertical: 4),
+      child: ToolbarButtonList(buttons: children)
+          .padding(horizontal: 4, vertical: 4),
     );
   }
 
@@ -168,10 +169,11 @@ class ToolbarButtonList extends StatefulWidget {
   final List<Widget> buttons;
 
   @override
-  _ToolbarButtonListState createState() => _ToolbarButtonListState();
+  ToolbarButtonListState createState() => ToolbarButtonListState();
 }
 
-class _ToolbarButtonListState extends State<ToolbarButtonList> with WidgetsBindingObserver {
+class ToolbarButtonListState extends State<ToolbarButtonList>
+    with WidgetsBindingObserver {
   final ScrollController _controller = ScrollController();
   bool _showLeftArrow = false;
   bool _showRightArrow = false;
@@ -196,7 +198,8 @@ class _ToolbarButtonListState extends State<ToolbarButtonList> with WidgetsBindi
     return LayoutBuilder(
       builder: (BuildContext context, BoxConstraints constraints) {
         List<Widget> children = [];
-        double width = (widget.buttons.length + 2) * defaultIconSize * kIconButtonFactor;
+        double width =
+            (widget.buttons.length + 2) * defaultIconSize * kIconButtonFactor;
         final isFit = constraints.maxWidth > width;
         if (!isFit) {
           children.add(_buildLeftArrow());
@@ -233,8 +236,10 @@ class _ToolbarButtonListState extends State<ToolbarButtonList> with WidgetsBindi
   void _handleScroll() {
     if (!mounted) return;
     setState(() {
-      _showLeftArrow = _controller.position.minScrollExtent != _controller.position.pixels;
-      _showRightArrow = _controller.position.maxScrollExtent != _controller.position.pixels;
+      _showLeftArrow =
+          _controller.position.minScrollExtent != _controller.position.pixels;
+      _showRightArrow =
+          _controller.position.maxScrollExtent != _controller.position.pixels;
     });
   }
 

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

@@ -11,13 +11,15 @@ typedef UpdateFieldNotifiedValue = Either<Unit, FlowyError>;
 class CellListener {
   final String rowId;
   final String fieldId;
-  PublishNotifier<UpdateFieldNotifiedValue>? _updateCellNotifier = PublishNotifier();
+  PublishNotifier<UpdateFieldNotifiedValue>? _updateCellNotifier =
+      PublishNotifier();
   GridNotificationListener? _listener;
   CellListener({required this.rowId, required this.fieldId});
 
   void start({required void Function(UpdateFieldNotifiedValue) onCellChanged}) {
     _updateCellNotifier?.addPublishListener(onCellChanged);
-    _listener = GridNotificationListener(objectId: "$rowId:$fieldId", handler: _handler);
+    _listener = GridNotificationListener(
+        objectId: "$rowId:$fieldId", handler: _handler);
   }
 
   void _handler(GridNotification ty, Either<Uint8List, FlowyError> result) {

+ 8 - 1
frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_cache.dart

@@ -33,10 +33,17 @@ class GridCellCache {
     required this.gridId,
   });
 
-  void remove(String fieldId) {
+  void removeCellWithFieldId(String fieldId) {
     _cellDataByFieldId.remove(fieldId);
   }
 
+  void remove(GridCellCacheKey key) {
+    var map = _cellDataByFieldId[key.fieldId];
+    if (map != null) {
+      map.remove(key.rowId);
+    }
+  }
+
   void insert<T extends GridCell>(GridCellCacheKey key, T value) {
     var map = _cellDataByFieldId[key.fieldId];
     if (map == null) {

+ 16 - 12
frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_data_loader.dart

@@ -24,18 +24,21 @@ class GridCellDataLoader<T> {
   Future<T?> loadData() {
     final fut = service.getCell(cellId: cellId);
     return fut.then(
-      (result) => result.fold((GridCellPB cell) {
-        try {
-          return parser.parserData(cell.data);
-        } catch (e, s) {
-          Log.error('$parser parser cellData failed, $e');
-          Log.error('Stack trace \n $s');
+      (result) => result.fold(
+        (GridCellPB cell) {
+          try {
+            return parser.parserData(cell.data);
+          } catch (e, s) {
+            Log.error('$parser parser cellData failed, $e');
+            Log.error('Stack trace \n $s');
+            return null;
+          }
+        },
+        (err) {
+          Log.error(err);
           return null;
-        }
-      }, (err) {
-        Log.error(err);
-        return null;
-      }),
+        },
+      ),
     );
   }
 }
@@ -58,7 +61,8 @@ class DateCellDataParser implements IGridCellDataParser<DateCellDataPB> {
   }
 }
 
-class SelectOptionCellDataParser implements IGridCellDataParser<SelectOptionCellDataPB> {
+class SelectOptionCellDataParser
+    implements IGridCellDataParser<SelectOptionCellDataPB> {
   @override
   SelectOptionCellDataPB? parserData(List<int> data) {
     if (data.isEmpty) {

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

@@ -71,6 +71,6 @@ class GridCellIdentifier with _$GridCellIdentifier {
   FieldType get fieldType => field.fieldType;
 
   ValueKey key() {
-    return ValueKey(rowId + fieldId + "${field.fieldType}");
+    return ValueKey("$rowId$fieldId${field.fieldType}");
   }
 }

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

@@ -190,7 +190,10 @@ class IGridCellController<T, D> extends Equatable {
     ///  cell display: $12
     _cellListener?.start(onCellChanged: (result) {
       result.fold(
-        (_) => _loadData(),
+        (_) {
+          _cellsCache.remove(_cacheKey);
+          _loadData();
+        },
         (err) => Log.error(err),
       );
     });
@@ -279,8 +282,8 @@ class IGridCellController<T, D> extends Equatable {
     _loadDataOperation?.cancel();
     _loadDataOperation = Timer(const Duration(milliseconds: 10), () {
       _cellDataLoader.loadData().then((data) {
-        _cellDataNotifier?.value = data;
         _cellsCache.insert(_cacheKey, GridCell(object: data));
+        _cellDataNotifier?.value = data;
       });
     });
   }

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

@@ -119,13 +119,13 @@ class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
   }
 
   String timeFormatPrompt(FlowyError error) {
-    String msg = LocaleKeys.grid_field_invalidTimeFormat.tr() + ". ";
+    String msg = "${LocaleKeys.grid_field_invalidTimeFormat.tr()}. ";
     switch (state.dateTypeOptionPB.timeFormat) {
       case TimeFormat.TwelveHour:
-        msg = msg + "e.g. 01: 00 AM";
+        msg = "${msg}e.g. 01: 00 AM";
         break;
       case TimeFormat.TwentyFourHour:
-        msg = msg + "e.g. 13: 00";
+        msg = "${msg}e.g. 13: 00";
         break;
       default:
         break;

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

@@ -79,7 +79,7 @@ class DateCellState with _$DateCellState {
 String _dateStrFromCellData(DateCellDataPB? cellData) {
   String dateStr = "";
   if (cellData != null) {
-    dateStr = cellData.date + " " + cellData.time;
+    dateStr = "${cellData.date} ${cellData.time}";
   }
   return dateStr;
 }

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

@@ -1,12 +1,14 @@
 import 'dart:async';
+
+import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
+import 'package:collection/collection.dart';
 import 'package:dartz/dartz.dart';
 import 'package:flowy_sdk/log.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
-import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
+
 import 'select_option_service.dart';
-import 'package:collection/collection.dart';
 
 part 'select_option_editor_bloc.freezed.dart';
 

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

@@ -46,7 +46,8 @@ class GridDataController {
 
   GridDataController({required ViewPB view})
       : gridId = view.id,
-        _blocks = LinkedHashMap.new(),
+        // ignore: prefer_collection_literals
+        _blocks = LinkedHashMap(),
         _gridFFIService = GridFFIService(gridId: view.id),
         fieldCache = GridFieldCache(gridId: view.id);
 

+ 9 - 1
frontend/app_flowy/lib/plugins/grid/application/grid_service.dart

@@ -27,10 +27,18 @@ class GridFFIService {
     return GridEventCreateTableRow(payload).send();
   }
 
-  Future<Either<RowPB, FlowyError>> createBoardCard(String groupId) {
+  Future<Either<RowPB, FlowyError>> createBoardCard(
+    String groupId,
+    String? startRowId,
+  ) {
     CreateBoardCardPayloadPB payload = CreateBoardCardPayloadPB.create()
       ..gridId = gridId
       ..groupId = groupId;
+
+    if (startRowId != null) {
+      payload.startRowId = startRowId;
+    }
+
     return GridEventCreateBoardCard(payload).send();
   }
 

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

@@ -52,7 +52,8 @@ class GridRowCache {
     //
     notifier.onRowFieldsChanged(() => _rowChangeReasonNotifier
         .receive(const RowsChangedReason.fieldDidChange()));
-    notifier.onRowFieldChanged((field) => _cellCache.remove(field.id));
+    notifier.onRowFieldChanged(
+        (field) => _cellCache.removeCellWithFieldId(field.id));
     _rowInfos = block.rows.map((rowPB) => buildGridRow(rowPB)).toList();
   }
 
@@ -209,7 +210,8 @@ class GridRowCache {
   }
 
   GridCellMap _makeGridCells(String rowId, RowPB? row) {
-    var cellDataMap = GridCellMap.new();
+    // ignore: prefer_collection_literals
+    var cellDataMap = GridCellMap();
     for (final field in _fieldNotifier.fields) {
       if (field.visibility) {
         cellDataMap[field.id] = GridCellIdentifier(

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

@@ -190,12 +190,12 @@ class CellAccessoryContainer extends StatelessWidget {
         ),
       );
       return GestureDetector(
-        child: hover,
         behavior: HitTestBehavior.opaque,
         onTap: () => accessory.onTap(),
+        child: hover,
       );
     }).toList();
 
-    return Wrap(children: children, spacing: 6);
+    return Wrap(spacing: 6, children: children);
   }
 }

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

@@ -44,8 +44,8 @@ class CellContainer extends StatelessWidget {
 
             if (accessories.isNotEmpty) {
               container = _GridCellEnterRegion(
-                child: container,
                 accessories: accessories,
+                child: container,
               );
             }
           }

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

@@ -297,9 +297,8 @@ class _DateTypeOptionButton extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     final theme = context.watch<AppTheme>();
-    final title = LocaleKeys.grid_field_dateFormat.tr() +
-        " &" +
-        LocaleKeys.grid_field_timeFormat.tr();
+    final title =
+        "${LocaleKeys.grid_field_dateFormat.tr()} &${LocaleKeys.grid_field_timeFormat.tr()}";
     return BlocSelector<DateCalBloc, DateCalState, DateTypeOptionPB>(
       selector: (state) => state.dateTypeOptionPB,
       builder: (context, dateTypeOptionPB) {
@@ -406,8 +405,8 @@ class _CalDateTimeSettingState extends State<_CalDateTimeSetting> {
     overlayIdentifier = child.toString();
     FlowyOverlay.of(context).insertWithAnchor(
       widget: OverlayContainer(
-        child: child,
         constraints: BoxConstraints.loose(const Size(460, 440)),
+        child: child,
       ),
       identifier: overlayIdentifier!,
       anchorContext: context,

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

@@ -91,8 +91,11 @@ 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.clip,
+      ),
       selectedColor: color,
       backgroundColor: color,
       labelPadding: const EdgeInsets.symmetric(horizontal: 6),

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

@@ -178,14 +178,14 @@ class _SelectOptionWrapState extends State<SelectOptionWrap> {
       child = Align(
         alignment: Alignment.centerLeft,
         child: Wrap(
+          spacing: 4,
+          runSpacing: 2,
           children: widget.selectOptions
               .map((option) => SelectOptionTag.fromOption(
                     context: context,
                     option: option,
                   ))
               .toList(),
-          spacing: 4,
-          runSpacing: 2,
         ),
       );
     }

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

@@ -75,8 +75,8 @@ class SelectOptionCellEditor extends StatelessWidget with FlowyOverlayDelegate {
     //
     FlowyOverlay.of(context).insertWithAnchor(
       widget: OverlayContainer(
-        child: SizedBox(width: _editorPannelWidth, child: editor),
         constraints: BoxConstraints.loose(const Size(_editorPannelWidth, 300)),
+        child: SizedBox(width: _editorPannelWidth, child: editor),
       ),
       identifier: SelectOptionCellEditor.identifier(),
       anchorContext: context,

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

@@ -108,7 +108,7 @@ class SelectOptionTextField extends StatelessWidget {
       child: SingleChildScrollView(
         controller: sc,
         scrollDirection: Axis.horizontal,
-        child: Wrap(children: children, spacing: 4),
+        child: Wrap(spacing: 4, children: children),
       ),
     );
   }

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

@@ -30,11 +30,11 @@ class URLCellEditor extends StatefulWidget with FlowyOverlayDelegate {
     //
     FlowyOverlay.of(context).insertWithAnchor(
       widget: OverlayContainer(
+        constraints: BoxConstraints.loose(const Size(300, 160)),
         child: SizedBox(
           width: 200,
           child: Padding(padding: const EdgeInsets.all(6), child: editor),
         ),
-        constraints: BoxConstraints.loose(const Size(300, 160)),
       ),
       identifier: URLCellEditor.identifier(),
       anchorContext: context,

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

@@ -1,7 +1,9 @@
+import 'package:app_flowy/generated/locale_keys.g.dart';
 import 'package:app_flowy/plugins/grid/application/field/field_cache.dart';
 import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart';
 import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/plugins/grid/application/prelude.dart';
+import 'package:easy_localization/easy_localization.dart';
 import 'package:appflowy_popover/popover.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra/theme.dart';
@@ -178,7 +180,10 @@ class CreateFieldButton extends StatelessWidget {
       triggerActions: PopoverTriggerActionFlags.click,
       direction: PopoverDirection.bottomWithRightAligned,
       child: FlowyButton(
-        text: const FlowyText.medium('New column', fontSize: 12),
+        text: FlowyText.medium(
+          LocaleKeys.grid_field_newColumn.tr(),
+          fontSize: 12,
+        ),
         hoverColor: theme.shader6,
         onTap: () {},
         leftIcon: svgWidget("home/add"),

+ 2 - 2
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/number.dart

@@ -101,10 +101,10 @@ class NumberTypeOptionWidget extends TypeOptionWidget {
   }
 }
 
-typedef _SelectNumberFormatCallback = Function(NumberFormat format);
+typedef SelectNumberFormatCallback = Function(NumberFormat format);
 
 class NumberFormatList extends StatelessWidget {
-  final _SelectNumberFormatCallback onSelected;
+  final SelectNumberFormatCallback onSelected;
   final NumberFormat selectedFormat;
   const NumberFormatList(
       {required this.selectedFormat, required this.onSelected, Key? key})

+ 8 - 2
frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart

@@ -14,6 +14,8 @@ import '../cell/cell_accessory.dart';
 import '../cell/cell_container.dart';
 import '../cell/prelude.dart';
 import 'row_action_sheet.dart';
+import "package:app_flowy/generated/locale_keys.g.dart";
+import 'package:easy_localization/easy_localization.dart';
 
 class GridRowWidget extends StatefulWidget {
   final RowInfo rowInfo;
@@ -122,10 +124,13 @@ class _InsertRowButton extends StatelessWidget {
   Widget build(BuildContext context) {
     final theme = context.watch<AppTheme>();
     return FlowyIconButton(
+      tooltipText: LocaleKeys.tooltip_addNewRow.tr(),
       hoverColor: theme.hover,
       width: 20,
       height: 30,
-      onPressed: () => context.read<RowBloc>().add(const RowEvent.createRow()),
+      onPressed: () => context.read<RowBloc>().add(
+            const RowEvent.createRow(),
+          ),
       iconPadding: const EdgeInsets.all(3),
       icon: svgWidget("home/add"),
     );
@@ -139,6 +144,7 @@ class _DeleteRowButton extends StatelessWidget {
   Widget build(BuildContext context) {
     final theme = context.watch<AppTheme>();
     return FlowyIconButton(
+      tooltipText: LocaleKeys.tooltip_openMenu.tr(),
       hoverColor: theme.hover,
       width: 20,
       height: 30,
@@ -184,7 +190,6 @@ class RowContent extends StatelessWidget {
 
         return CellContainer(
           width: cellId.field.width.toDouble(),
-          child: child,
           rowStateNotifier:
               Provider.of<RegionStateNotifier>(context, listen: false),
           accessoryBuilder: (buildContext) {
@@ -202,6 +207,7 @@ class RowContent extends StatelessWidget {
             }
             return accessories;
           },
+          child: child,
         );
       },
     ).toList();

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

@@ -59,8 +59,8 @@ class GridRowActionSheet extends StatelessWidget {
   }) {
     FlowyOverlay.of(overlayContext).insertWithAnchor(
       widget: OverlayContainer(
-        child: this,
         constraints: BoxConstraints.loose(const Size(140, 200)),
+        child: this,
       ),
       identifier: GridRowActionSheet.identifier(),
       anchorContext: overlayContext,

+ 70 - 18
frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart

@@ -5,8 +5,10 @@ import 'package:app_flowy/plugins/grid/application/row/row_detail_bloc.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra/theme.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flowy_infra_ui/style_widget/button.dart';
 import 'package:flowy_infra_ui/style_widget/icon_button.dart';
 import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart';
+import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flowy_infra_ui/widget/spacing.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:app_flowy/generated/locale_keys.g.dart';
@@ -61,7 +63,12 @@ class _RowDetailPageState extends State<RowDetailPage> {
                   children: const [Spacer(), _CloseButton()],
                 ),
               ),
-              Expanded(child: _PropertyList(cellBuilder: widget.cellBuilder)),
+              Expanded(
+                child: _PropertyList(
+                  cellBuilder: widget.cellBuilder,
+                  viewId: widget.dataController.rowInfo.gridId,
+                ),
+              ),
             ],
           ),
         ),
@@ -88,9 +95,11 @@ class _CloseButton extends StatelessWidget {
 }
 
 class _PropertyList extends StatelessWidget {
+  final String viewId;
   final GridCellBuilder cellBuilder;
   final ScrollController _scrollController;
   _PropertyList({
+    required this.viewId,
     required this.cellBuilder,
     Key? key,
   })  : _scrollController = ScrollController(),
@@ -101,22 +110,65 @@ class _PropertyList extends StatelessWidget {
     return BlocBuilder<RowDetailBloc, RowDetailState>(
       buildWhen: (previous, current) => previous.gridCells != current.gridCells,
       builder: (context, state) {
-        return ScrollbarListStack(
-          axis: Axis.vertical,
-          controller: _scrollController,
-          barSize: GridSize.scrollBarSize,
-          child: ListView.separated(
-            controller: _scrollController,
-            itemCount: state.gridCells.length,
-            itemBuilder: (BuildContext context, int index) {
-              return _RowDetailCell(
-                cellId: state.gridCells[index],
-                cellBuilder: cellBuilder,
-              );
-            },
-            separatorBuilder: (BuildContext context, int index) {
-              return const VSpace(2);
-            },
+        return Column(
+          children: [
+            Expanded(
+              child: ScrollbarListStack(
+                axis: Axis.vertical,
+                controller: _scrollController,
+                barSize: GridSize.scrollBarSize,
+                child: ListView.separated(
+                  controller: _scrollController,
+                  itemCount: state.gridCells.length,
+                  itemBuilder: (BuildContext context, int index) {
+                    return _RowDetailCell(
+                      cellId: state.gridCells[index],
+                      cellBuilder: cellBuilder,
+                    );
+                  },
+                  separatorBuilder: (BuildContext context, int index) {
+                    return const VSpace(2);
+                  },
+                ),
+              ),
+            ),
+            _CreateFieldButton(viewId: viewId),
+          ],
+        );
+      },
+    );
+  }
+}
+
+class _CreateFieldButton extends StatelessWidget {
+  final String viewId;
+  const _CreateFieldButton({required this.viewId, Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = context.read<AppTheme>();
+
+    return Popover(
+      triggerActions: PopoverTriggerActionFlags.click,
+      child: SizedBox(
+        height: 40,
+        child: FlowyButton(
+          text: FlowyText.medium(
+            LocaleKeys.grid_field_newColumn.tr(),
+            fontSize: 12,
+          ),
+          hoverColor: theme.shader6,
+          onTap: () {},
+          leftIcon: svgWidget("home/add"),
+        ),
+      ),
+      popupBuilder: (BuildContext context) {
+        return OverlayContainer(
+          constraints: BoxConstraints.loose(const Size(240, 200)),
+          child: FieldEditor(
+            gridId: viewId,
+            fieldName: "",
+            typeOptionLoader: NewFieldTypeOptionLoader(gridId: viewId),
           ),
         );
       },
@@ -150,9 +202,9 @@ class _RowDetailCellState extends State<_RowDetailCell> {
       behavior: HitTestBehavior.translucent,
       onTap: () => cell.beginFocus.notify(),
       child: AccessoryHover(
-        child: cell,
         contentPadding:
             const EdgeInsets.symmetric(horizontal: 10, vertical: 12),
+        child: cell,
       ),
     );
 

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

@@ -1,5 +1,4 @@
 import 'package:app_flowy/plugins/grid/application/setting/setting_bloc.dart';
-import 'package:appflowy_popover/popover.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra/theme.dart';

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

@@ -91,12 +91,12 @@ class _TrashPageState extends State<TrashPage> {
         builder: (context, state) {
           return SizedBox.expand(
             child: Column(
+              mainAxisAlignment: MainAxisAlignment.start,
               children: [
                 _renderTopBar(context, theme, state),
                 const VSpace(32),
                 _renderTrashList(context, state),
               ],
-              mainAxisAlignment: MainAxisAlignment.start,
             ).padding(horizontal: horizontalPadding, vertical: 48),
           );
         },

+ 27 - 31
frontend/app_flowy/lib/startup/tasks/app_widget.dart

@@ -20,39 +20,35 @@ class InitAppWidgetTask extends LaunchTask {
     final setting = await UserSettingsService().getAppearanceSettings();
     final settingModel = AppearanceSettingModel(setting);
     final app = ApplicationWidget(
-      child: widget,
       settingModel: settingModel,
+      child: widget,
     );
-    BlocOverrides.runZoned(
-      () {
-        runApp(
-          EasyLocalization(
-            supportedLocales: const [
-              // In alphabetical order
-              Locale('ca', 'ES'),
-              Locale('de', 'DE'),
-              Locale('en'),
-              Locale('es', 'VE'),
-              Locale('fr', 'FR'),
-              Locale('fr', 'CA'),
-              Locale('hu', 'HU'),
-              Locale('id', 'ID'),
-              Locale('it', 'IT'),
-              Locale('ja', 'JP'),
-              Locale('pl', 'PL'),
-              Locale('pt', 'BR'),
-              Locale('ru', 'RU'),
-              Locale('tr', 'TR'),
-              Locale('zh', 'CN'),
-            ],
-            path: 'assets/translations',
-            fallbackLocale: const Locale('en'),
-            saveLocale: false,
-            child: app,
-          ),
-        );
-      },
-      blocObserver: ApplicationBlocObserver(),
+    Bloc.observer = ApplicationBlocObserver();
+    runApp(
+      EasyLocalization(
+        supportedLocales: const [
+          // In alphabetical order
+          Locale('ca', 'ES'),
+          Locale('de', 'DE'),
+          Locale('en'),
+          Locale('es', 'VE'),
+          Locale('fr', 'FR'),
+          Locale('fr', 'CA'),
+          Locale('hu', 'HU'),
+          Locale('id', 'ID'),
+          Locale('it', 'IT'),
+          Locale('ja', 'JP'),
+          Locale('pl', 'PL'),
+          Locale('pt', 'BR'),
+          Locale('ru', 'RU'),
+          Locale('tr', 'TR'),
+          Locale('zh', 'CN'),
+        ],
+        path: 'assets/translations',
+        fallbackLocale: const Locale('en'),
+        saveLocale: false,
+        child: app,
+      ),
     );
 
     return Future(() => {});

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

@@ -28,16 +28,19 @@ class AuthRouter {
     );
   }
 
-  void pushHomeScreen(BuildContext context, UserProfilePB profile, CurrentWorkspaceSettingPB workspaceSetting) {
+  void pushHomeScreen(BuildContext context, UserProfilePB profile,
+      CurrentWorkspaceSettingPB workspaceSetting) {
     Navigator.push(
       context,
-      PageRoutes.fade(() => HomeScreen(profile, workspaceSetting), RouteDurations.slow.inMilliseconds * .001),
+      PageRoutes.fade(() => HomeScreen(profile, workspaceSetting),
+          RouteDurations.slow.inMilliseconds * .001),
     );
   }
 }
 
 class SplashRoute {
-  Future<void> pushWelcomeScreen(BuildContext context, UserProfilePB userProfile) async {
+  Future<void> pushWelcomeScreen(
+      BuildContext context, UserProfilePB userProfile) async {
     final screen = WelcomeScreen(userProfile: userProfile);
     final workspaceId = await Navigator.of(context).push(
       PageRoutes.fade(
@@ -46,20 +49,24 @@ class SplashRoute {
       ),
     );
 
+    // ignore: use_build_context_synchronously
     pushHomeScreen(context, userProfile, workspaceId);
   }
 
-  void pushHomeScreen(BuildContext context, UserProfilePB userProfile, CurrentWorkspaceSettingPB workspaceSetting) {
+  void pushHomeScreen(BuildContext context, UserProfilePB userProfile,
+      CurrentWorkspaceSettingPB workspaceSetting) {
     Navigator.push(
       context,
-      PageRoutes.fade(() => HomeScreen(userProfile, workspaceSetting), RouteDurations.slow.inMilliseconds * .001),
+      PageRoutes.fade(() => HomeScreen(userProfile, workspaceSetting),
+          RouteDurations.slow.inMilliseconds * .001),
     );
   }
 
   void pushSignInScreen(BuildContext context) {
     Navigator.push(
       context,
-      PageRoutes.fade(() => SignInScreen(router: getIt<AuthRouter>()), RouteDurations.slow.inMilliseconds * .001),
+      PageRoutes.fade(() => SignInScreen(router: getIt<AuthRouter>()),
+          RouteDurations.slow.inMilliseconds * .001),
     );
   }
 

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

@@ -94,6 +94,7 @@ class SignUpPrompt extends StatelessWidget {
   Widget build(BuildContext context) {
     final theme = context.watch<AppTheme>();
     return Row(
+      mainAxisAlignment: MainAxisAlignment.center,
       children: [
         Text(LocaleKeys.signIn_dontHaveAnAccount.tr(), style: TextStyle(color: theme.shader3, fontSize: 12)),
         TextButton(
@@ -107,7 +108,6 @@ class SignUpPrompt extends StatelessWidget {
           ),
         ),
       ],
-      mainAxisAlignment: MainAxisAlignment.center,
     );
   }
 }

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

@@ -86,6 +86,7 @@ class SignUpPrompt extends StatelessWidget {
   Widget build(BuildContext context) {
     final theme = context.watch<AppTheme>();
     return Row(
+      mainAxisAlignment: MainAxisAlignment.center,
       children: [
         Text(
           LocaleKeys.signUp_alreadyHaveAnAccount.tr(),
@@ -97,7 +98,6 @@ class SignUpPrompt extends StatelessWidget {
           child: Text(LocaleKeys.signIn_buttonText.tr(), style: TextStyle(color: theme.main1)),
         ),
       ],
-      mainAxisAlignment: MainAxisAlignment.center,
     );
   }
 }

+ 9 - 10
frontend/app_flowy/lib/workspace/application/markdown/src/inline_parser.dart

@@ -399,8 +399,8 @@ class AutolinkExtensionSyntax extends InlineSyntax {
   }
 }
 
-class _DelimiterRun {
-  _DelimiterRun._(
+class DelimiterRun {
+  DelimiterRun._(
       {this.char,
       this.length,
       this.isLeftFlanking,
@@ -420,8 +420,7 @@ class _DelimiterRun {
   final bool? isFollowedByPunctuation;
 
   // ignore: prefer_constructors_over_static_methods
-  static _DelimiterRun? tryParse(
-      InlineParser parser, int runStart, int runEnd) {
+  static DelimiterRun? tryParse(InlineParser parser, int runStart, int runEnd) {
     bool leftFlanking,
         rightFlanking,
         precededByPunctuation,
@@ -466,7 +465,7 @@ class _DelimiterRun {
       return null;
     }
 
-    return _DelimiterRun._(
+    return DelimiterRun._(
         char: parser.charAt(runStart),
         length: runEnd - runStart + 1,
         isLeftFlanking: leftFlanking,
@@ -516,7 +515,7 @@ class TagSyntax extends InlineSyntax {
       return true;
     }
 
-    final delimiterRun = _DelimiterRun.tryParse(parser, matchStart, matchEnd);
+    final delimiterRun = DelimiterRun.tryParse(parser, matchStart, matchEnd);
     if (delimiterRun != null && delimiterRun.canOpen) {
       parser.openTag(TagState(parser.pos, matchEnd + 1, this, delimiterRun));
       return true;
@@ -531,7 +530,7 @@ class TagSyntax extends InlineSyntax {
     final matchStart = parser.pos;
     final matchEnd = parser.pos + runLength - 1;
     final openingRunLength = state.endPos - state.startPos;
-    final delimiterRun = _DelimiterRun.tryParse(parser, matchStart, matchEnd);
+    final delimiterRun = DelimiterRun.tryParse(parser, matchStart, matchEnd);
 
     if (openingRunLength == 1 && runLength == 1) {
       parser.addNode(Element('em', state.children));
@@ -579,7 +578,7 @@ class StrikethroughSyntax extends TagSyntax {
     final runLength = match.group(0)!.length;
     final matchStart = parser.pos;
     final matchEnd = parser.pos + runLength - 1;
-    final delimiterRun = _DelimiterRun.tryParse(parser, matchStart, matchEnd)!;
+    final delimiterRun = DelimiterRun.tryParse(parser, matchStart, matchEnd)!;
     if (!delimiterRun.isRightFlanking!) {
       return false;
     }
@@ -1170,7 +1169,7 @@ class TagState {
   /// The children of this node. Will be `null` for text nodes.
   final List<Node> children;
 
-  final _DelimiterRun? openingDelimiterRun;
+  final DelimiterRun? openingDelimiterRun;
 
   /// Attempts to close this tag by matching the current text against its end
   /// pattern.
@@ -1193,7 +1192,7 @@ class TagState {
     final closingMatchStart = parser.pos;
     final closingMatchEnd = parser.pos + runLength - 1;
     final closingDelimiterRun =
-        _DelimiterRun.tryParse(parser, closingMatchStart, closingMatchEnd);
+        DelimiterRun.tryParse(parser, closingMatchStart, closingMatchEnd);
     if (closingDelimiterRun != null && closingDelimiterRun.canClose) {
       // Emphasis rules #9 and #10:
       final oneRunOpensAndCloses =

+ 6 - 2
frontend/app_flowy/lib/workspace/presentation/home/home_screen.dart

@@ -1,5 +1,7 @@
 import 'package:app_flowy/startup/plugin/plugin.dart';
 import 'package:app_flowy/workspace/application/home/home_bloc.dart';
+
+import 'package:app_flowy/workspace/presentation/home/hotkeys.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';
@@ -54,7 +56,8 @@ class _HomeScreenState extends State<HomeScreen> {
           },
         ),
       ],
-      child: Scaffold(
+      child: HomeHotKeys(
+          child: Scaffold(
         body: BlocListener<HomeBloc, HomeState>(
           listenWhen: (p, c) => p.unauthorized != c.unauthorized,
           listener: (context, state) {
@@ -80,7 +83,7 @@ class _HomeScreenState extends State<HomeScreen> {
             },
           ),
         ),
-      ),
+      )),
     );
   }
 
@@ -145,6 +148,7 @@ class _HomeScreenState extends State<HomeScreen> {
     return FocusTraversalGroup(child: RepaintBoundary(child: homeMenu));
   }
 
+
   Widget _buildEditPanel(
       {required HomeState homeState,
       required BuildContext context,

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

@@ -58,10 +58,10 @@ class FadingIndexedStack extends StatefulWidget {
   }) : super(key: key);
 
   @override
-  _FadingIndexedStackState createState() => _FadingIndexedStackState();
+  FadingIndexedStackState createState() => FadingIndexedStackState();
 }
 
-class _FadingIndexedStackState extends State<FadingIndexedStack> {
+class FadingIndexedStackState extends State<FadingIndexedStack> {
   double _targetOpacity = 1;
 
   @override

+ 32 - 0
frontend/app_flowy/lib/workspace/presentation/home/hotkeys.dart

@@ -0,0 +1,32 @@
+import 'dart:io';
+
+import 'package:app_flowy/startup/startup.dart';
+import 'package:app_flowy/workspace/application/home/home_bloc.dart';
+import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
+import 'package:flutter/material.dart';
+import 'package:hotkey_manager/hotkey_manager.dart';
+import 'package:provider/provider.dart';
+
+class HomeHotKeys extends StatelessWidget {
+  final Widget child;
+  const HomeHotKeys({required this.child, Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    HotKey hotKey = HotKey(
+      KeyCode.backslash,
+      modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control],
+      // Set hotkey scope (default is HotKeyScope.system)
+      scope: HotKeyScope.inapp, // Set as inapp-wide hotkey.
+    );
+    hotKeyManager.register(
+      hotKey,
+      keyDownHandler: (hotKey) {
+        context.read<HomeBloc>().add(const HomeEvent.collapseMenu());
+        getIt<HomeStackManager>().collapsedNotifier.value =
+            !getIt<HomeStackManager>().collapsedNotifier.currentValue!;
+      },
+    );
+    return child;
+  }
+}

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov