Bladeren bron

feat: row document (#2792)

* chore: create orphan view handler

* feat: save icon url and cover url in view

* feat: implement emoji picker UI

* chore: config ui

* chore: config ui again

* chore: replace RowPB with RowMetaPB to exposing more row information

* fix: compile error

* feat: show emoji in row

* chore: update

* test: insert emoji test

* test: add update emoji test

* test: add remove emoji test

* test: add create field tests

* test: add create row and delete row integration tests

* test: add create row from row menu

* test: document in row detail page

* test: delete, duplicate row in row detail page

* test: check the row count displayed in grid page

* test: rename existing field in grid page

* test: update field type of exisiting field in grid page

* test: delete field test

* test: add duplicate field test

* test: add hide field test

* test: add edit text cell test

* test: add insert text to text cell test

* test: add edit number cell test

* test: add edit multiple number cells

* test: add edit checkbox cell test

* feat: integrate editor into database row

* test: add edit create time and last edit time cell test

* test: add edit date cell by selecting a date test

* chore: remove unused code

* chore: update checklist bg color

* test: add update database layout test

---------

Co-authored-by: Lucas.Xu <[email protected]>
Nathan.fooo 1 jaar geleden
bovenliggende
commit
27dd719aa8
100 gewijzigde bestanden met toevoegingen van 3632 en 1034 verwijderingen
  1. 185 0
      frontend/appflowy_flutter/integration_test/database_cell_test.dart
  2. 202 0
      frontend/appflowy_flutter/integration_test/database_field_test.dart
  3. 241 0
      frontend/appflowy_flutter/integration_test/database_row_page_test.dart
  4. 85 0
      frontend/appflowy_flutter/integration_test/database_row_test.dart
  5. 66 0
      frontend/appflowy_flutter/integration_test/database_setting_test.dart
  6. 16 6
      frontend/appflowy_flutter/integration_test/util/common_operations.dart
  7. 481 0
      frontend/appflowy_flutter/integration_test/util/database_test_op.dart
  8. 54 0
      frontend/appflowy_flutter/integration_test/util/ime.dart
  9. 44 25
      frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller.dart
  10. 8 2
      frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_service.dart
  11. 23 19
      frontend/appflowy_flutter/lib/plugins/database_view/application/cell/checklist_cell_service.dart
  12. 8 7
      frontend/appflowy_flutter/lib/plugins/database_view/application/cell/select_option_cell_service.dart
  13. 9 9
      frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart
  14. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/application/database_view_service.dart
  15. 4 1
      frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart
  16. 2 6
      frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_cell_bloc.dart
  17. 6 8
      frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_listener.dart
  18. 8 0
      frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_service.dart
  19. 163 0
      frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_banner_bloc.dart
  20. 51 48
      frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart
  21. 12 9
      frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_data_controller.dart
  22. 20 19
      frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_list.dart
  23. 49 0
      frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_meta_listener.dart
  24. 28 1
      frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_service.dart
  25. 5 3
      frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_cache.dart
  26. 9 9
      frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart
  27. 9 9
      frontend/appflowy_flutter/lib/plugins/database_view/board/application/group_controller.dart
  28. 5 1
      frontend/appflowy_flutter/lib/plugins/database_view/board/board.dart
  29. 7 6
      frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart
  30. 2 2
      frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart
  31. 5 1
      frontend/appflowy_flutter/lib/plugins/database_view/calendar/calendar.dart
  32. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_day.dart
  33. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart
  34. 0 61
      frontend/appflowy_flutter/lib/plugins/database_view/database_view.dart
  35. 14 9
      frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart
  36. 10 10
      frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_action_sheet_bloc.dart
  37. 7 7
      frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_bloc.dart
  38. 3 3
      frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_detail_bloc.dart
  39. 126 0
      frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_document_bloc.dart
  40. 5 1
      frontend/appflowy_flutter/lib/plugins/database_view/grid/grid.dart
  41. 50 30
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart
  42. 9 9
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_editor.dart
  43. 3 3
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart
  44. 9 4
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/action.dart
  45. 24 15
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/row.dart
  46. 5 4
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card.dart
  47. 8 10
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_bloc.dart
  48. 7 15
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/database_view_widget.dart
  49. 1 0
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart
  50. 7 0
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/cells.dart
  51. 20 2
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checkbox_cell/checkbox_cell.dart
  52. 3 1
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_bloc.dart
  53. 3 1
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor_bloc.dart
  54. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_progress_bar.dart
  55. 3 1
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor_bloc.dart
  56. 43 18
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/text_cell/text_cell.dart
  57. 11 0
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart
  58. 174 0
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_action.dart
  59. 268 0
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_banner.dart
  60. 78 375
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart
  61. 120 0
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_document.dart
  62. 192 0
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_property.dart
  63. 3 3
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/database_setting.dart
  64. 1 1
      frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart
  65. 5 1
      frontend/appflowy_flutter/lib/plugins/document/document.dart
  66. 26 5
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart
  67. 1 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart
  68. 2 2
      frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_button.dart
  69. 27 21
      frontend/appflowy_flutter/lib/plugins/util.dart
  70. 0 5
      frontend/appflowy_flutter/lib/startup/deps_resolver.dart
  71. 0 3
      frontend/appflowy_flutter/lib/startup/plugin/plugin.dart
  72. 2 2
      frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart
  73. 25 4
      frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart
  74. 31 43
      frontend/appflowy_flutter/lib/workspace/application/view/view_listener.dart
  75. 35 1
      frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart
  76. 5 9
      frontend/appflowy_flutter/lib/workspace/presentation/home/home_screen.dart
  77. 0 2
      frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart
  78. 3 1
      frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/section/section.dart
  79. 8 14
      frontend/appflowy_flutter/lib/workspace/presentation/widgets/left_bar_item.dart
  80. 3 3
      frontend/appflowy_flutter/pubspec.lock
  81. 2 2
      frontend/appflowy_flutter/pubspec.yaml
  82. 3 2
      frontend/appflowy_flutter/test/bloc_test/board_test/util.dart
  83. 6 6
      frontend/appflowy_flutter/test/bloc_test/grid_test/grid_bloc_test.dart
  84. 4 3
      frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart
  85. 18 10
      frontend/appflowy_tauri/src-tauri/Cargo.lock
  86. 6 6
      frontend/appflowy_tauri/src-tauri/Cargo.toml
  87. 16 9
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/group/group_controller.ts
  88. 12 15
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/row/row_cache.ts
  89. 2 2
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/database_view_cache.ts
  90. 18 10
      frontend/rust-lib/Cargo.lock
  91. 5 5
      frontend/rust-lib/Cargo.toml
  92. 2 1
      frontend/rust-lib/flowy-database2/src/entities/calendar_entities.rs
  93. 2 2
      frontend/rust-lib/flowy-database2/src/entities/database_entities.rs
  94. 7 3
      frontend/rust-lib/flowy-database2/src/entities/group_entities/group.rs
  95. 4 4
      frontend/rust-lib/flowy-database2/src/entities/group_entities/group_changeset.rs
  96. 164 16
      frontend/rust-lib/flowy-database2/src/entities/row_entities.rs
  97. 53 2
      frontend/rust-lib/flowy-database2/src/event_handler.rs
  98. 13 1
      frontend/rust-lib/flowy-database2/src/event_map.rs
  99. 2 0
      frontend/rust-lib/flowy-database2/src/notification.rs
  100. 102 41
      frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs

+ 185 - 0
frontend/appflowy_flutter/integration_test/database_cell_test.dart

@@ -0,0 +1,185 @@
+import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+
+import 'util/database_test_op.dart';
+import 'util/util.dart';
+
+void main() {
+  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+  group('grid cell', () {
+    const location = 'appflowy';
+
+    setUp(() async {
+      await TestFolder.cleanTestLocation(location);
+      await TestFolder.setTestLocation(location);
+    });
+
+    tearDown(() async {
+      await TestFolder.cleanTestLocation(location);
+    });
+
+    tearDownAll(() async {
+      await TestFolder.cleanTestLocation(null);
+    });
+
+    testWidgets('edit text cell', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      await tester.tapAddButton();
+      await tester.tapCreateGridButton();
+
+      await tester.editCell(
+        rowIndex: 0,
+        fieldType: FieldType.RichText,
+        input: 'hello world',
+      );
+
+      await tester.assertCellContent(
+        rowIndex: 0,
+        fieldType: FieldType.RichText,
+        content: 'hello world',
+      );
+
+      await tester.pumpAndSettle();
+    });
+
+    testWidgets('edit number cell', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      await tester.tapAddButton();
+      await tester.tapCreateGridButton();
+
+      const fieldType = FieldType.Number;
+
+      // Create a number field
+      await tester.createField(fieldType, fieldType.name);
+
+      await tester.editCell(
+        rowIndex: 0,
+        fieldType: fieldType,
+        input: '-1',
+      );
+      // edit the next cell to force the previous cell at row 0 to lose focus
+      await tester.editCell(
+        rowIndex: 1,
+        fieldType: fieldType,
+        input: '0.2',
+      );
+      // -1 -> -1
+      await tester.assertCellContent(
+        rowIndex: 0,
+        fieldType: fieldType,
+        content: '-1',
+      );
+
+      // edit the next cell to force the previous cell at row 1 to lose focus
+      await tester.editCell(
+        rowIndex: 2,
+        fieldType: fieldType,
+        input: '.1',
+      );
+      // 0.2 -> 0.2
+      await tester.assertCellContent(
+        rowIndex: 1,
+        fieldType: fieldType,
+        content: '0.2',
+      );
+
+      // edit the next cell to force the previous cell at row 2 to lose focus
+      await tester.editCell(
+        rowIndex: 0,
+        fieldType: fieldType,
+        input: '',
+      );
+      // .1 -> 0.1
+      await tester.assertCellContent(
+        rowIndex: 2,
+        fieldType: fieldType,
+        content: '0.1',
+      );
+
+      await tester.pumpAndSettle();
+    });
+
+    testWidgets('edit checkbox cell', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      await tester.tapAddButton();
+      await tester.tapCreateGridButton();
+
+      await tester.assertCheckboxCell(rowIndex: 0, isSelected: false);
+      await tester.tapCheckboxCellInGrid(rowIndex: 0);
+      await tester.assertCheckboxCell(rowIndex: 0, isSelected: true);
+
+      await tester.tapCheckboxCellInGrid(rowIndex: 1);
+      await tester.tapCheckboxCellInGrid(rowIndex: 2);
+      await tester.assertCheckboxCell(rowIndex: 1, isSelected: true);
+      await tester.assertCheckboxCell(rowIndex: 2, isSelected: true);
+
+      await tester.pumpAndSettle();
+    });
+
+    testWidgets('edit create time cell', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      await tester.tapAddButton();
+      await tester.tapCreateGridButton();
+
+      const fieldType = FieldType.CreatedTime;
+      // Create a create time field
+      // The create time field is not editable
+      await tester.createField(fieldType, fieldType.name);
+
+      await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
+
+      await tester.findDateEditor(findsNothing);
+
+      await tester.pumpAndSettle();
+    });
+
+    testWidgets('edit last time cell', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      await tester.tapAddButton();
+      await tester.tapCreateGridButton();
+
+      const fieldType = FieldType.LastEditedTime;
+      // Create a last time field
+      // The last time field is not editable
+      await tester.createField(fieldType, fieldType.name);
+
+      await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
+
+      await tester.findDateEditor(findsNothing);
+
+      await tester.pumpAndSettle();
+    });
+
+    testWidgets('edit time cell', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      await tester.tapAddButton();
+      await tester.tapCreateGridButton();
+
+      const fieldType = FieldType.DateTime;
+      await tester.createField(fieldType, fieldType.name);
+
+      // Tap the cell to invoke the field editor
+      await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
+      await tester.findDateEditor(findsOneWidget);
+
+      // Select the date
+      await tester.selectDay(content: 3);
+
+      await tester.pumpAndSettle();
+    });
+  });
+}

+ 202 - 0
frontend/appflowy_flutter/integration_test/database_field_test.dart

@@ -0,0 +1,202 @@
+import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+
+import 'util/database_test_op.dart';
+import 'util/util.dart';
+
+void main() {
+  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+  group('grid page', () {
+    const location = 'appflowy';
+
+    setUp(() async {
+      await TestFolder.cleanTestLocation(location);
+      await TestFolder.setTestLocation(location);
+    });
+
+    tearDown(() async {
+      await TestFolder.cleanTestLocation(location);
+    });
+
+    tearDownAll(() async {
+      await TestFolder.cleanTestLocation(null);
+    });
+
+    testWidgets('rename existing field', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      await tester.tapAddButton();
+      await tester.tapCreateGridButton();
+
+      // Invoke the field editor
+      await tester.tapGridFieldWithName('Name');
+      await tester.tapEditPropertyButton();
+
+      await tester.renameField('hello world');
+      await tester.dismissFieldEditor();
+
+      await tester.tapGridFieldWithName('hello world');
+      await tester.pumpAndSettle();
+    });
+
+    testWidgets('update field type of existing field', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      await tester.tapAddButton();
+      await tester.tapCreateGridButton();
+
+      // Invoke the field editor
+      await tester.tapGridFieldWithName('Type');
+      await tester.tapEditPropertyButton();
+
+      await tester.tapTypeOptionButton();
+      await tester.selectFieldType(FieldType.Checkbox);
+      await tester.dismissFieldEditor();
+
+      await tester.assertFieldTypeWithFieldName(
+        'Type',
+        FieldType.Checkbox,
+      );
+      await tester.pumpAndSettle();
+    });
+
+    testWidgets('create a field and rename it', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      // create a new grid
+      await tester.tapAddButton();
+      await tester.tapCreateGridButton();
+
+      // create a field
+      await tester.createField(FieldType.Checklist, 'checklist');
+
+      // check the field is created successfully
+      await tester.findFieldWithName('checklist');
+      await tester.pumpAndSettle();
+    });
+
+    testWidgets('delete field', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      await tester.tapAddButton();
+      await tester.tapCreateGridButton();
+
+      // create a field
+      await tester.createField(FieldType.Checkbox, 'New field 1');
+
+      // Delete the field
+      await tester.tapGridFieldWithName('New field 1');
+      await tester.tapDeletePropertyButton();
+
+      // confirm delete
+      await tester.tapDialogOkButton();
+
+      await tester.noFieldWithName('New field 1');
+      await tester.pumpAndSettle();
+    });
+
+    testWidgets('duplicate field', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      await tester.tapAddButton();
+      await tester.tapCreateGridButton();
+
+      // create a field
+      await tester.scrollToRight(find.byType(GridPage));
+      await tester.tapNewPropertyButton();
+      await tester.renameField('New field 1');
+      await tester.dismissFieldEditor();
+      await tester.createField(FieldType.RichText, 'New field 1');
+
+      // Delete the field
+      await tester.tapGridFieldWithName('New field 1');
+      await tester.tapDuplicatePropertyButton();
+
+      await tester.findFieldWithName('New field 1 (copy)');
+      await tester.pumpAndSettle();
+    });
+
+    testWidgets('hide field', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      await tester.tapAddButton();
+      await tester.tapCreateGridButton();
+
+      // create a field
+      await tester.scrollToRight(find.byType(GridPage));
+      await tester.tapNewPropertyButton();
+      await tester.renameField('New field 1');
+      await tester.dismissFieldEditor();
+
+      // Delete the field
+      await tester.tapGridFieldWithName('New field 1');
+      await tester.tapHidePropertyButton();
+
+      await tester.noFieldWithName('New field 1');
+      await tester.pumpAndSettle();
+    });
+
+    testWidgets('create checklist field ', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      await tester.tapAddButton();
+      await tester.tapCreateGridButton();
+
+      await tester.scrollToRight(find.byType(GridPage));
+      await tester.tapNewPropertyButton();
+
+      // Open the type option menu
+      await tester.tapTypeOptionButton();
+
+      await tester.selectFieldType(FieldType.Checklist);
+
+      // After update the field type, the cells should be updated
+      await tester.findCellByFieldType(FieldType.Checklist);
+
+      await tester.pumpAndSettle();
+    });
+
+    testWidgets('create list of fields', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      await tester.tapAddButton();
+      await tester.tapCreateGridButton();
+
+      for (final fieldType in [
+        FieldType.Checklist,
+        FieldType.DateTime,
+        FieldType.Number,
+        FieldType.URL,
+        FieldType.MultiSelect,
+        FieldType.LastEditedTime,
+        FieldType.CreatedTime,
+        FieldType.Checkbox,
+      ]) {
+        await tester.scrollToRight(find.byType(GridPage));
+        await tester.tapNewPropertyButton();
+        await tester.renameField(fieldType.name);
+
+        // Open the type option menu
+        await tester.tapTypeOptionButton();
+
+        await tester.selectFieldType(fieldType);
+        await tester.dismissFieldEditor();
+
+        // After update the field type, the cells should be updated
+        await tester.findCellByFieldType(fieldType);
+        await tester.pumpAndSettle();
+      }
+    });
+  });
+}

+ 241 - 0
frontend/appflowy_flutter/integration_test/database_row_page_test.dart

@@ -0,0 +1,241 @@
+import 'package:appflowy/plugins/database_view/widgets/row/row_banner.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flowy_infra_ui/style_widget/text.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+
+import 'util/database_test_op.dart';
+import 'util/ime.dart';
+import 'util/util.dart';
+
+void main() {
+  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+  group('grid', () {
+    const location = 'appflowy';
+
+    setUp(() async {
+      await TestFolder.cleanTestLocation(location);
+      await TestFolder.setTestLocation(location);
+    });
+
+    tearDown(() async {
+      await TestFolder.cleanTestLocation(location);
+    });
+
+    tearDownAll(() async {
+      await TestFolder.cleanTestLocation(null);
+    });
+
+    testWidgets('open first row of the grid', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      // Create a new grid
+      await tester.tapAddButton();
+      await tester.tapCreateGridButton();
+
+      // Hover first row and then open the row page
+      await tester.openFirstRowDetailPage();
+    });
+
+    testWidgets('insert emoji in the row detail page', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      // Create a new grid
+      await tester.tapAddButton();
+      await tester.tapCreateGridButton();
+
+      // Hover first row and then open the row page
+      await tester.openFirstRowDetailPage();
+
+      await tester.hoverRowBanner();
+
+      await tester.openEmojiPicker();
+      await tester.switchToEmojiList();
+      await tester.tapEmoji('😀');
+
+      // After select the emoji, the EmojiButton will show up
+      await tester.tapButton(find.byType(EmojiButton));
+    });
+
+    testWidgets('update emoji in the row detail page', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      // Create a new grid
+      await tester.tapAddButton();
+      await tester.tapCreateGridButton();
+
+      // Hover first row and then open the row page
+      await tester.openFirstRowDetailPage();
+      await tester.hoverRowBanner();
+      await tester.openEmojiPicker();
+      await tester.switchToEmojiList();
+      await tester.tapEmoji('😀');
+
+      // Update existing selected emoji
+      await tester.tapButton(find.byType(EmojiButton));
+      await tester.switchToEmojiList();
+      await tester.tapEmoji('😅');
+
+      // The emoji already displayed in the row banner
+      final emojiText = find.byWidgetPredicate(
+        (widget) => widget is FlowyText && widget.title == '😅',
+      );
+
+      // The number of emoji should be two. One in the row displayed in the grid
+      // one in the row detail page.
+      expect(emojiText, findsNWidgets(2));
+    });
+
+    testWidgets('remove emoji in the row detail page', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      // Create a new grid
+      await tester.tapAddButton();
+      await tester.tapCreateGridButton();
+
+      // Hover first row and then open the row page
+      await tester.openFirstRowDetailPage();
+      await tester.hoverRowBanner();
+      await tester.openEmojiPicker();
+      await tester.switchToEmojiList();
+      await tester.tapEmoji('😀');
+
+      // Remove the emoji
+      await tester.tapButton(find.byType(RemoveEmojiButton));
+      final emojiText = find.byWidgetPredicate(
+        (widget) => widget is FlowyText && widget.title == '😀',
+      );
+      expect(emojiText, findsNothing);
+    });
+
+    testWidgets('create list of fields in row detail page', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      // Create a new grid
+      await tester.tapAddButton();
+      await tester.tapCreateGridButton();
+
+      // Hover first row and then open the row page
+      await tester.openFirstRowDetailPage();
+
+      for (final fieldType in [
+        FieldType.Checklist,
+        FieldType.DateTime,
+        FieldType.Number,
+        FieldType.URL,
+        FieldType.MultiSelect,
+        FieldType.LastEditedTime,
+        FieldType.CreatedTime,
+        FieldType.Checkbox,
+      ]) {
+        await tester.tapRowDetailPageCreatePropertyButton();
+        await tester.renameField(fieldType.name);
+
+        // Open the type option menu
+        await tester.tapTypeOptionButton();
+
+        await tester.selectFieldType(fieldType);
+        await tester.dismissFieldEditor();
+
+        // After update the field type, the cells should be updated
+        await tester.findCellByFieldType(fieldType);
+        await tester.scrollRowDetailByOffset(const Offset(0, -50));
+      }
+    });
+
+    testWidgets('check document is exist in row detail page', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      // Create a new grid
+      await tester.tapAddButton();
+      await tester.tapCreateGridButton();
+
+      // Hover first row and then open the row page
+      await tester.openFirstRowDetailPage();
+
+      // Each row detail page should have a document
+      await tester.assertDocumentExistInRowDetailPage();
+    });
+
+    testWidgets('update the content of the document and re-open it',
+        (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      // Create a new grid
+      await tester.tapAddButton();
+      await tester.tapCreateGridButton();
+
+      // Hover first row and then open the row page
+      await tester.openFirstRowDetailPage();
+
+      // Wait for the document to be loaded
+      await tester.wait(500);
+
+      // Focus on the editor
+      final textBlock = find.byType(TextBlockComponentWidget);
+      await tester.tapAt(tester.getCenter(textBlock));
+
+      // Input some text
+      const inputText = 'Hello world';
+      await tester.ime.insertText(inputText);
+      expect(
+        find.textContaining(inputText, findRichText: true),
+        findsOneWidget,
+      );
+
+      // Tap outside to dismiss the field
+      await tester.tapAt(Offset.zero);
+      await tester.pumpAndSettle();
+
+      // Re-open the document
+      await tester.openFirstRowDetailPage();
+      expect(
+        find.textContaining(inputText, findRichText: true),
+        findsOneWidget,
+      );
+    });
+
+    testWidgets('delete row in row detail page', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      // Create a new grid
+      await tester.tapAddButton();
+      await tester.tapCreateGridButton();
+
+      // Hover first row and then open the row page
+      await tester.openFirstRowDetailPage();
+
+      await tester.tapRowDetailPageDeleteRowButton();
+      await tester.tapEscButton();
+
+      await tester.assertNumberOfRowsInGridPage(2);
+    });
+
+    testWidgets('duplicate row in row detail page', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      // Create a new grid
+      await tester.tapAddButton();
+      await tester.tapCreateGridButton();
+
+      // Hover first row and then open the row page
+      await tester.openFirstRowDetailPage();
+
+      await tester.tapRowDetailPageDuplicateRowButton();
+      await tester.tapEscButton();
+
+      await tester.assertNumberOfRowsInGridPage(4);
+    });
+  });
+}

+ 85 - 0
frontend/appflowy_flutter/integration_test/database_row_test.dart

@@ -0,0 +1,85 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+
+import 'util/database_test_op.dart';
+import 'util/util.dart';
+
+void main() {
+  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+  group('grid', () {
+    const location = 'appflowy';
+
+    setUp(() async {
+      await TestFolder.cleanTestLocation(location);
+      await TestFolder.setTestLocation(location);
+    });
+
+    tearDown(() async {
+      await TestFolder.cleanTestLocation(location);
+    });
+
+    tearDownAll(() async {
+      await TestFolder.cleanTestLocation(null);
+    });
+
+    testWidgets('create row of the grid', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      await tester.tapAddButton();
+      await tester.tapCreateGridButton();
+      await tester.tapCreateRowButtonInGrid();
+
+      // The initial number of rows is 3
+      await tester.assertNumberOfRowsInGridPage(4);
+      await tester.pumpAndSettle();
+    });
+
+    testWidgets('create row from row menu of the grid', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      await tester.tapAddButton();
+      await tester.tapCreateGridButton();
+      await tester.hoverOnFirstRowOfGrid();
+
+      await tester.tapCreateRowButtonInRowMenuOfGrid();
+
+      // The initial number of rows is 3
+      await tester.assertNumberOfRowsInGridPage(4);
+      await tester.assertRowCountInGridPage(4);
+      await tester.pumpAndSettle();
+    });
+
+    testWidgets('delete row of the grid', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      await tester.tapAddButton();
+      await tester.tapCreateGridButton();
+      await tester.hoverOnFirstRowOfGrid();
+
+      // Open the row menu and then click the delete
+      await tester.tapRowMenuButtonInGrid();
+      await tester.tapDeleteOnRowMenu();
+
+      // The initial number of rows is 3
+      await tester.assertNumberOfRowsInGridPage(2);
+      await tester.assertRowCountInGridPage(2);
+      await tester.pumpAndSettle();
+    });
+
+    testWidgets('check number of row indicator in the initial grid',
+        (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      await tester.tapAddButton();
+      await tester.tapCreateGridButton();
+      await tester.assertRowCountInGridPage(3);
+
+      await tester.pumpAndSettle();
+    });
+  });
+}

+ 66 - 0
frontend/appflowy_flutter/integration_test/database_setting_test.dart

@@ -0,0 +1,66 @@
+import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+
+import 'util/database_test_op.dart';
+import 'util/util.dart';
+
+void main() {
+  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+  group('grid', () {
+    const location = 'appflowy';
+
+    setUp(() async {
+      await TestFolder.cleanTestLocation(location);
+      await TestFolder.setTestLocation(location);
+    });
+
+    tearDown(() async {
+      await TestFolder.cleanTestLocation(location);
+    });
+
+    tearDownAll(() async {
+      await TestFolder.cleanTestLocation(null);
+    });
+
+    testWidgets('update layout', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      await tester.tapAddButton();
+      await tester.tapCreateGridButton();
+
+      // open setting
+      await tester.tapDatabaseSettingButton();
+      // select the layout
+      await tester.tapDatabaseLayoutButton();
+      // select layout by board
+      await tester.selectDatabaseLayoutType(DatabaseLayoutPB.Board);
+      await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Board);
+
+      await tester.pumpAndSettle();
+    });
+
+    testWidgets('update layout multiple times', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      await tester.tapAddButton();
+      await tester.tapCreateGridButton();
+
+      // open setting
+      await tester.tapDatabaseSettingButton();
+      await tester.tapDatabaseLayoutButton();
+      await tester.selectDatabaseLayoutType(DatabaseLayoutPB.Board);
+      await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Board);
+
+      await tester.tapDatabaseSettingButton();
+      await tester.tapDatabaseLayoutButton();
+      await tester.selectDatabaseLayoutType(DatabaseLayoutPB.Calendar);
+      await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Calendar);
+
+      await tester.pumpAndSettle();
+    });
+  });
+}

+ 16 - 6
frontend/appflowy_flutter/integration_test/util/common_operations.dart

@@ -1,6 +1,7 @@
 import 'dart:ui';
 import 'package:appflowy_backend/log.dart';
 import 'package:appflowy/generated/locale_keys.g.dart';
+
 import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
 import 'package:appflowy/user/presentation/skip_log_in_screen.dart';
 import 'package:appflowy/workspace/presentation/home/menu/app/header/add_button.dart';
@@ -39,6 +40,13 @@ extension CommonOperations on WidgetTester {
     await tapButtonWithName(LocaleKeys.document_menuName.tr());
   }
 
+  /// Tap the create grid button.
+  ///
+  /// Must call [tapAddButton] first.
+  Future<void> tapCreateGridButton() async {
+    await tapButtonWithName(LocaleKeys.grid_menuName.tr());
+  }
+
   /// Tap the import button.
   ///
   /// Must call [tapAddButton] first.
@@ -105,12 +113,14 @@ extension CommonOperations on WidgetTester {
     Finder finder, {
     Offset? offset,
   }) async {
-    final gesture = await createGesture(kind: PointerDeviceKind.mouse);
-    await gesture.addPointer(location: Offset.zero);
-    addTearDown(gesture.removePointer);
-    await pump();
-    await gesture.moveTo(offset ?? getCenter(finder));
-    await pumpAndSettle();
+    try {
+      final gesture = await createGesture(kind: PointerDeviceKind.mouse);
+      await gesture.addPointer(location: Offset.zero);
+      addTearDown(gesture.removePointer);
+      await pump();
+      await gesture.moveTo(offset ?? getCenter(finder));
+      await pumpAndSettle();
+    } catch (_) {}
   }
 
   /// Hover on the page name.

+ 481 - 0
frontend/appflowy_flutter/integration_test/util/database_test_op.dart

@@ -0,0 +1,481 @@
+import 'dart:ui';
+
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/database_view/application/setting/setting_bloc.dart';
+import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart';
+import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart';
+import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_cell_action_sheet.dart';
+import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart';
+import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/grid_layout.dart';
+import 'package:appflowy/plugins/database_view/widgets/row/row_document.dart';
+import 'package:appflowy/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart';
+import 'package:appflowy/plugins/database_view/widgets/setting/database_setting.dart';
+import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra_ui/style_widget/icon_button.dart';
+import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
+import 'package:appflowy/plugins/database_view/grid/presentation/widgets/footer/grid_footer.dart';
+import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_cell.dart';
+import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.dart';
+import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
+import 'package:appflowy/plugins/database_view/grid/presentation/widgets/row/row.dart';
+import 'package:appflowy/plugins/database_view/widgets/row/accessory/cell_accessory.dart';
+import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart';
+import 'package:appflowy/plugins/database_view/widgets/row/row_action.dart';
+import 'package:appflowy/plugins/database_view/widgets/row/row_banner.dart';
+import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/emoji_picker/emoji_menu_item.dart';
+import 'package:flowy_infra_ui/style_widget/text.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
+import 'package:table_calendar/table_calendar.dart';
+
+import 'base.dart';
+import 'common_operations.dart';
+
+extension AppFlowyDatabaseTest on WidgetTester {
+  Future<void> hoverOnFirstRowOfGrid() async {
+    final findRow = find.byType(GridRow);
+    expect(findRow, findsWidgets);
+
+    final firstRow = findRow.first;
+    await hoverOnWidget(firstRow);
+  }
+
+  Future<void> editCell({
+    required int rowIndex,
+    required FieldType fieldType,
+    required String input,
+  }) async {
+    final findRow = find.byType(GridRow);
+    final findCell = finderForFieldType(fieldType);
+
+    final cell = find.descendant(
+      of: findRow.at(rowIndex),
+      matching: findCell,
+    );
+
+    expect(cell, findsOneWidget);
+    await enterText(cell, input);
+    await pumpAndSettle();
+  }
+
+  Future<void> tapCheckboxCellInGrid({
+    required int rowIndex,
+  }) async {
+    final findRow = find.byType(GridRow);
+    final findCell = finderForFieldType(FieldType.Checkbox);
+
+    final cell = find.descendant(
+      of: findRow.at(rowIndex),
+      matching: findCell,
+    );
+
+    final button = find.descendant(
+      of: cell,
+      matching: find.byType(FlowyIconButton),
+    );
+
+    expect(cell, findsOneWidget);
+    await tapButton(button);
+  }
+
+  Future<void> assertCheckboxCell({
+    required int rowIndex,
+    required bool isSelected,
+  }) async {
+    final findRow = find.byType(GridRow);
+    final findCell = finderForFieldType(FieldType.Checkbox);
+
+    final cell = find.descendant(
+      of: findRow.at(rowIndex),
+      matching: findCell,
+    );
+
+    var finder = find.byType(CheckboxCellUncheck);
+    if (isSelected) {
+      finder = find.byType(CheckboxCellCheck);
+    }
+
+    expect(
+      find.descendant(
+        of: cell,
+        matching: finder,
+      ),
+      findsOneWidget,
+    );
+  }
+
+  Future<void> tapCellInGrid({
+    required int rowIndex,
+    required FieldType fieldType,
+  }) async {
+    final findRow = find.byType(GridRow);
+    final findCell = finderForFieldType(fieldType);
+
+    final cell = find.descendant(
+      of: findRow.at(rowIndex),
+      matching: findCell,
+    );
+
+    expect(cell, findsOneWidget);
+    await tapButton(cell);
+  }
+
+  Future<void> assertCellContent({
+    required int rowIndex,
+    required FieldType fieldType,
+    required String content,
+  }) async {
+    final findRow = find.byType(GridRow);
+    final findCell = finderForFieldType(fieldType);
+    final cell = find.descendant(
+      of: findRow.at(rowIndex),
+      matching: findCell,
+    );
+
+    final findContent = find.descendant(
+      of: cell,
+      matching: find.text(content),
+    );
+
+    expect(findContent, findsOneWidget);
+  }
+
+  Future<void> selectDay({
+    required int content,
+  }) async {
+    final findCalendar = find.byType(TableCalendar);
+    final findDay = find.text(content.toString());
+
+    final finder = find.descendant(
+      of: findCalendar,
+      matching: findDay,
+    );
+
+    await tapButton(finder);
+  }
+
+  Future<void> openFirstRowDetailPage() async {
+    await hoverOnFirstRowOfGrid();
+
+    final expandButton = find.byType(PrimaryCellAccessory);
+    expect(expandButton, findsOneWidget);
+    await tapButton(expandButton);
+  }
+
+  Future<void> hoverRowBanner() async {
+    final banner = find.byType(RowBanner);
+    expect(banner, findsOneWidget);
+
+    await startGesture(
+      getTopLeft(banner),
+      kind: PointerDeviceKind.mouse,
+    );
+
+    await pumpAndSettle();
+  }
+
+  Future<void> openEmojiPicker() async {
+    await tapButton(find.byType(EmojiPickerButton));
+    await tapButton(find.byType(EmojiSelectionMenu));
+  }
+
+  /// Must call [openEmojiPicker] first
+  Future<void> switchToEmojiList() async {
+    final icon = find.byIcon(Icons.tag_faces);
+    await tapButton(icon);
+  }
+
+  Future<void> tapEmoji(String emoji) async {
+    final emojiWidget = find.text(emoji);
+    await tapButton(emojiWidget);
+  }
+
+  Future<void> scrollGridByOffset(Offset offset) async {
+    await drag(find.byType(GridPage), offset);
+    await pumpAndSettle();
+  }
+
+  Future<void> scrollRowDetailByOffset(Offset offset) async {
+    await drag(find.byType(RowDetailPage), offset);
+    await pumpAndSettle();
+  }
+
+  Future<void> scrollToRight(Finder find) async {
+    final size = getSize(find);
+    await drag(find, Offset(-size.width, 0));
+    await pumpAndSettle(const Duration(milliseconds: 500));
+  }
+
+  Future<void> tapNewPropertyButton() async {
+    await tapButtonWithName(LocaleKeys.grid_field_newProperty.tr());
+    await pumpAndSettle();
+  }
+
+  Future<void> tapGridFieldWithName(String name) async {
+    final field = find.byWidgetPredicate(
+      (widget) => widget is FieldCellButton && widget.field.name == name,
+    );
+    await tapButton(field);
+    await pumpAndSettle();
+  }
+
+  /// Should call [tapGridFieldWithName] first.
+  Future<void> tapEditPropertyButton() async {
+    await tapButtonWithName(LocaleKeys.grid_field_editProperty.tr());
+    await pumpAndSettle(const Duration(milliseconds: 200));
+  }
+
+  /// Should call [tapGridFieldWithName] first.
+  Future<void> tapDeletePropertyButton() async {
+    final field = find.byWidgetPredicate(
+      (widget) =>
+          widget is FieldActionCell && widget.action == FieldAction.delete,
+    );
+    await tapButton(field);
+  }
+
+  /// Should call [tapGridFieldWithName] first.
+  Future<void> tapDialogOkButton() async {
+    final field = find.byWidgetPredicate(
+      (widget) =>
+          widget is PrimaryTextButton &&
+          widget.label == LocaleKeys.button_OK.tr(),
+    );
+    await tapButton(field);
+  }
+
+  /// Should call [tapGridFieldWithName] first.
+  Future<void> tapDuplicatePropertyButton() async {
+    final field = find.byWidgetPredicate(
+      (widget) =>
+          widget is FieldActionCell && widget.action == FieldAction.duplicate,
+    );
+    await tapButton(field);
+  }
+
+  /// Should call [tapGridFieldWithName] first.
+  Future<void> tapHidePropertyButton() async {
+    final field = find.byWidgetPredicate(
+      (widget) =>
+          widget is FieldActionCell && widget.action == FieldAction.hide,
+    );
+    await tapButton(field);
+  }
+
+  Future<void> tapRowDetailPageCreatePropertyButton() async {
+    await tapButton(find.byType(CreateRowFieldButton));
+  }
+
+  Future<void> tapRowDetailPageDeleteRowButton() async {
+    await tapButton(find.byType(RowDetailPageDeleteButton));
+  }
+
+  Future<void> tapRowDetailPageDuplicateRowButton() async {
+    await tapButton(find.byType(RowDetailPageDuplicateButton));
+  }
+
+  Future<void> tapTypeOptionButton() async {
+    await tapButton(find.byType(SwitchFieldButton));
+  }
+
+  Future<void> tapEscButton() async {
+    await sendKeyEvent(LogicalKeyboardKey.escape);
+  }
+
+  /// Must call [tapTypeOptionButton] first.
+  Future<void> selectFieldType(FieldType fieldType) async {
+    final fieldTypeButton = find.byWidgetPredicate(
+      (widget) => widget is FlowyText && widget.title == fieldType.title(),
+    );
+    await tapButton(fieldTypeButton);
+  }
+
+  /// Each field has its own cell, so we can find the corresponding cell by
+  /// the field type after create a new field.
+  Future<void> findCellByFieldType(FieldType fieldType) async {
+    final finder = finderForFieldType(fieldType);
+    expect(finder, findsWidgets);
+  }
+
+  Future<void> assertNumberOfFieldsInGridPage(int num) async {
+    expect(find.byType(GridFieldCell), findsNWidgets(num));
+  }
+
+  Future<void> assertNumberOfRowsInGridPage(int num) async {
+    expect(find.byType(GridRow), findsNWidgets(num));
+  }
+
+  Future<void> assertDocumentExistInRowDetailPage() async {
+    expect(find.byType(RowDocument), findsOneWidget);
+  }
+
+  /// Check the field type of the [FieldCellButton] is the same as the name.
+  Future<void> assertFieldTypeWithFieldName(
+    String name,
+    FieldType fieldType,
+  ) async {
+    final field = find.byWidgetPredicate(
+      (widget) =>
+          widget is FieldCellButton &&
+          widget.field.fieldType == fieldType &&
+          widget.field.name == name,
+    );
+
+    expect(field, findsOneWidget);
+  }
+
+  Future<void> findFieldWithName(String name) async {
+    final field = find.byWidgetPredicate(
+      (widget) => widget is FieldCellButton && widget.field.name == name,
+    );
+    expect(field, findsOneWidget);
+  }
+
+  Future<void> noFieldWithName(String name) async {
+    final field = find.byWidgetPredicate(
+      (widget) => widget is FieldCellButton && widget.field.name == name,
+    );
+    expect(field, findsNothing);
+  }
+
+  Future<void> renameField(String newName) async {
+    final textField = find.byType(FieldNameTextField);
+    expect(textField, findsOneWidget);
+    await enterText(textField, newName);
+    await pumpAndSettle();
+  }
+
+  Future<void> dismissFieldEditor() async {
+    await sendKeyEvent(LogicalKeyboardKey.escape);
+    await sendKeyEvent(LogicalKeyboardKey.escape);
+    await sendKeyEvent(LogicalKeyboardKey.escape);
+    await pumpAndSettle();
+  }
+
+  Future<void> findFieldEditor(dynamic matcher) async {
+    final finder = find.byType(FieldEditor);
+    expect(finder, matcher);
+  }
+
+  Future<void> findDateEditor(dynamic matcher) async {
+    final finder = find.byType(DateCellEditor);
+    expect(finder, matcher);
+  }
+
+  Future<void> tapCreateRowButtonInGrid() async {
+    await tapButton(find.byType(GridAddRowButton));
+  }
+
+  Future<void> tapCreateRowButtonInRowMenuOfGrid() async {
+    await tapButton(find.byType(InsertRowButton));
+  }
+
+  Future<void> tapRowMenuButtonInGrid() async {
+    await tapButton(find.byType(RowMenuButton));
+  }
+
+  /// Should call [tapRowMenuButtonInGrid] first.
+  Future<void> tapDeleteOnRowMenu() async {
+    await tapButtonWithName(LocaleKeys.grid_row_delete.tr());
+  }
+
+  Future<void> assertRowCountInGridPage(int num) async {
+    final text = find.byWidgetPredicate(
+      (widget) => widget is FlowyText && widget.title == rowCountString(num),
+    );
+    expect(text, findsOneWidget);
+  }
+
+  Future<void> createField(FieldType fieldType, String name) async {
+    await scrollToRight(find.byType(GridPage));
+    await tapNewPropertyButton();
+    await renameField(name);
+    await tapTypeOptionButton();
+    await selectFieldType(fieldType);
+    await dismissFieldEditor();
+  }
+
+  Future<void> tapDatabaseSettingButton() async {
+    await tapButton(find.byType(SettingButton));
+  }
+
+  /// Should call [tapDatabaseSettingButton] first.
+  Future<void> tapDatabaseLayoutButton() async {
+    final findSettingItem = find.byType(DatabaseSettingItem);
+    final findLayoutButton = find.byWidgetPredicate(
+      (widget) =>
+          widget is FlowyText &&
+          widget.title == DatabaseSettingAction.showLayout.title(),
+    );
+
+    final button = find.descendant(
+      of: findSettingItem,
+      matching: findLayoutButton,
+    );
+
+    await tapButton(button);
+  }
+
+  Future<void> selectDatabaseLayoutType(DatabaseLayoutPB layout) async {
+    final findLayoutCell = find.byType(DatabaseViewLayoutCell);
+    final findText = find.byWidgetPredicate(
+      (widget) => widget is FlowyText && widget.title == layout.layoutName(),
+    );
+
+    final button = find.descendant(
+      of: findLayoutCell,
+      matching: findText,
+    );
+
+    await tapButton(button);
+  }
+
+  Future<void> assertCurrentDatabaseLayoutType(DatabaseLayoutPB layout) async {
+    expect(finderForDatabaseLayoutType(layout), findsOneWidget);
+  }
+}
+
+Finder finderForDatabaseLayoutType(DatabaseLayoutPB layout) {
+  switch (layout) {
+    case DatabaseLayoutPB.Board:
+      return find.byType(BoardPage);
+    case DatabaseLayoutPB.Calendar:
+      return find.byType(CalendarPage);
+    case DatabaseLayoutPB.Grid:
+      return find.byType(GridPage);
+    default:
+      throw Exception('Unknown database layout type: $layout');
+  }
+}
+
+Finder finderForFieldType(FieldType fieldType) {
+  switch (fieldType) {
+    case FieldType.Checkbox:
+      return find.byType(GridCheckboxCell);
+    case FieldType.DateTime:
+      return find.byType(GridDateCell);
+    case FieldType.LastEditedTime:
+    case FieldType.CreatedTime:
+      return find.byType(GridDateCell);
+    case FieldType.SingleSelect:
+      return find.byType(GridSingleSelectCell);
+    case FieldType.MultiSelect:
+      return find.byType(GridMultiSelectCell);
+    case FieldType.Checklist:
+      return find.byType(GridChecklistCell);
+    case FieldType.Number:
+      return find.byType(GridNumberCell);
+    case FieldType.RichText:
+      return find.byType(GridTextCell);
+    case FieldType.URL:
+      return find.byType(GridURLCell);
+    default:
+      throw Exception('Unknown field type: $fieldType');
+  }
+}

+ 54 - 0
frontend/appflowy_flutter/integration_test/util/ime.dart

@@ -0,0 +1,54 @@
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+extension IME on WidgetTester {
+  IMESimulator get ime => IMESimulator(this);
+}
+
+class IMESimulator {
+  IMESimulator(this.tester) {
+    client = findDeltaTextInputClient();
+  }
+
+  final WidgetTester tester;
+  late final DeltaTextInputClient client;
+
+  Future<void> insertText(String text) async {
+    for (final c in text.characters) {
+      await insertCharacter(c);
+    }
+  }
+
+  Future<void> insertCharacter(String character) async {
+    final value = client.currentTextEditingValue;
+    if (value == null) {
+      assert(false);
+      return;
+    }
+    final deltas = [
+      TextEditingDeltaInsertion(
+        textInserted: character,
+        oldText: value.text.replaceRange(
+          value.selection.start,
+          value.selection.end,
+          '',
+        ),
+        insertionOffset: value.selection.baseOffset,
+        selection: TextSelection.collapsed(
+          offset: value.selection.baseOffset + 1,
+        ),
+        composing: TextRange.empty,
+      ),
+    ];
+    client.updateEditingValueWithDeltas(deltas);
+    await tester.pumpAndSettle();
+  }
+
+  DeltaTextInputClient findDeltaTextInputClient() {
+    final finder = find.byType(KeyboardServiceWidget);
+    final KeyboardServiceWidgetState state = tester.state(finder);
+    return state.textInputService as DeltaTextInputClient;
+  }
+}

+ 44 - 25
frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller.dart

@@ -1,5 +1,6 @@
 import 'dart:async';
 import 'package:appflowy/plugins/database_view/application/field/field_listener.dart';
+import 'package:appflowy/plugins/database_view/application/row/row_meta_listener.dart';
 import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
 import 'package:appflowy_backend/log.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
@@ -22,39 +23,45 @@ import 'cell_service.dart';
 ///
 // ignore: must_be_immutable
 class CellController<T, D> extends Equatable {
-  final DatabaseCellContext cellContext;
+  DatabaseCellContext _cellContext;
   final CellCache _cellCache;
   final CellCacheKey _cacheKey;
   final FieldBackendService _fieldBackendSvc;
-  final SingleFieldListener _fieldListener;
   final CellDataLoader<T> _cellDataLoader;
   final CellDataPersistence<D> _cellDataPersistence;
 
   CellListener? _cellListener;
+  RowMetaListener? _rowMetaListener;
+  SingleFieldListener? _fieldListener;
   CellDataNotifier<T?>? _cellDataNotifier;
 
   VoidCallback? _onCellFieldChanged;
+  VoidCallback? _onRowMetaChanged;
   Timer? _loadDataOperation;
   Timer? _saveDataOperation;
 
-  String get viewId => cellContext.viewId;
+  String get viewId => _cellContext.viewId;
 
-  RowId get rowId => cellContext.rowId;
+  RowId get rowId => _cellContext.rowId;
 
-  String get fieldId => cellContext.fieldInfo.id;
+  String get fieldId => _cellContext.fieldInfo.id;
 
-  FieldInfo get fieldInfo => cellContext.fieldInfo;
+  FieldInfo get fieldInfo => _cellContext.fieldInfo;
 
-  FieldType get fieldType => cellContext.fieldInfo.fieldType;
+  FieldType get fieldType => _cellContext.fieldInfo.fieldType;
+
+  String? get emoji => _cellContext.emoji;
 
   CellController({
-    required this.cellContext,
+    required DatabaseCellContext cellContext,
     required CellCache cellCache,
     required CellDataLoader<T> cellDataLoader,
     required CellDataPersistence<D> cellDataPersistence,
-  })  : _cellCache = cellCache,
+  })  : _cellContext = cellContext,
+        _cellCache = cellCache,
         _cellDataLoader = cellDataLoader,
         _cellDataPersistence = cellDataPersistence,
+        _rowMetaListener = RowMetaListener(cellContext.rowId),
         _fieldListener = SingleFieldListener(fieldId: cellContext.fieldId),
         _fieldBackendSvc = FieldBackendService(
           viewId: cellContext.viewId,
@@ -84,20 +91,22 @@ class CellController<T, D> extends Equatable {
     );
 
     /// 2.Listen on the field event and load the cell data if needed.
-    _fieldListener.start(
-      onFieldChanged: (result) {
-        result.fold(
-          (fieldPB) {
-            /// reloadOnFieldChanged should be true if you need to load the data when the corresponding field is changed
-            /// For example:
-            ///   ¥12 -> $12
-            if (_cellDataLoader.reloadOnFieldChanged) {
-              _loadData();
-            }
-            _onCellFieldChanged?.call();
-          },
-          (err) => Log.error(err),
-        );
+    _fieldListener?.start(
+      onFieldChanged: (fieldPB) {
+        /// reloadOnFieldChanged should be true if you need to load the data when the corresponding field is changed
+        /// For example:
+        ///   ¥12 -> $12
+        if (_cellDataLoader.reloadOnFieldChanged) {
+          _loadData();
+        }
+        _onCellFieldChanged?.call();
+      },
+    );
+
+    _rowMetaListener?.start(
+      callback: (newRowMeta) {
+        _cellContext = _cellContext.copyWith(rowMeta: newRowMeta);
+        _onRowMetaChanged?.call();
       },
     );
   }
@@ -105,9 +114,11 @@ class CellController<T, D> extends Equatable {
   /// Listen on the cell content or field changes
   VoidCallback? startListening({
     required void Function(T?) onCellChanged,
+    VoidCallback? onRowMetaChanged,
     VoidCallback? onCellFieldChanged,
   }) {
     _onCellFieldChanged = onCellFieldChanged;
+    _onRowMetaChanged = onRowMetaChanged;
 
     /// Notify the listener, the cell data was changed.
     onCellChangedFn() => onCellChanged(_cellDataNotifier?.value);
@@ -186,18 +197,26 @@ class CellController<T, D> extends Equatable {
   }
 
   Future<void> dispose() async {
+    await _rowMetaListener?.stop();
+    _rowMetaListener = null;
+
     await _cellListener?.stop();
+    _cellListener = null;
+
+    await _fieldListener?.stop();
+    _fieldListener = null;
+
     _loadDataOperation?.cancel();
     _saveDataOperation?.cancel();
     _cellDataNotifier?.dispose();
-    await _fieldListener.stop();
     _cellDataNotifier = null;
+    _onRowMetaChanged = null;
   }
 
   @override
   List<Object> get props => [
         _cellCache.get(_cacheKey) ?? "",
-        cellContext.rowId + cellContext.fieldInfo.id
+        _cellContext.rowId + _cellContext.fieldInfo.id
       ];
 }
 

+ 8 - 2
frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_service.dart

@@ -2,6 +2,7 @@ import 'dart:async';
 import 'dart:collection';
 import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart';
 import 'package:dartz/dartz.dart';
@@ -52,18 +53,23 @@ class CellBackendService {
 class DatabaseCellContext with _$DatabaseCellContext {
   const factory DatabaseCellContext({
     required String viewId,
-    required RowId rowId,
+    required RowMetaPB rowMeta,
     required FieldInfo fieldInfo,
   }) = _DatabaseCellContext;
 
   // ignore: unused_element
   const DatabaseCellContext._();
 
+  String get rowId => rowMeta.id;
+
   String get fieldId => fieldInfo.id;
 
   FieldType get fieldType => fieldInfo.fieldType;
 
   ValueKey key() {
-    return ValueKey("$rowId$fieldId${fieldInfo.fieldType}");
+    return ValueKey("${rowMeta.id}$fieldId${fieldInfo.fieldType}");
   }
+
+  /// Only the primary field can have an emoji.
+  String? get emoji => fieldInfo.isPrimary ? rowMeta.icon : null;
 }

+ 23 - 19
frontend/appflowy_flutter/lib/plugins/database_view/application/cell/checklist_cell_service.dart

@@ -1,4 +1,3 @@
-import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
 import 'package:appflowy_backend/dispatch/dispatch.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/cell_entities.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.dart';
@@ -7,17 +6,23 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
 import 'package:dartz/dartz.dart';
 
 class ChecklistCellBackendService {
-  final DatabaseCellContext cellContext;
+  final String viewId;
+  final String fieldId;
+  final String rowId;
 
-  ChecklistCellBackendService({required this.cellContext});
+  ChecklistCellBackendService({
+    required this.viewId,
+    required this.fieldId,
+    required this.rowId,
+  });
 
   Future<Either<Unit, FlowyError>> create({
     required String name,
   }) {
     final payload = ChecklistCellDataChangesetPB.create()
-      ..viewId = cellContext.viewId
-      ..fieldId = cellContext.fieldInfo.id
-      ..rowId = cellContext.rowId
+      ..viewId = viewId
+      ..fieldId = fieldId
+      ..rowId = rowId
       ..insertOptions.add(name);
 
     return DatabaseEventUpdateChecklistCell(payload).send();
@@ -27,9 +32,9 @@ class ChecklistCellBackendService {
     required List<String> optionIds,
   }) {
     final payload = ChecklistCellDataChangesetPB.create()
-      ..viewId = cellContext.viewId
-      ..fieldId = cellContext.fieldInfo.id
-      ..rowId = cellContext.rowId
+      ..viewId = viewId
+      ..fieldId = fieldId
+      ..rowId = rowId
       ..deleteOptionIds.addAll(optionIds);
 
     return DatabaseEventUpdateChecklistCell(payload).send();
@@ -39,9 +44,9 @@ class ChecklistCellBackendService {
     required String optionId,
   }) {
     final payload = ChecklistCellDataChangesetPB.create()
-      ..viewId = cellContext.viewId
-      ..fieldId = cellContext.fieldInfo.id
-      ..rowId = cellContext.rowId
+      ..viewId = viewId
+      ..fieldId = fieldId
+      ..rowId = rowId
       ..selectedOptionIds.add(optionId);
 
     return DatabaseEventUpdateChecklistCell(payload).send();
@@ -51,9 +56,9 @@ class ChecklistCellBackendService {
     required SelectOptionPB option,
   }) {
     final payload = ChecklistCellDataChangesetPB.create()
-      ..viewId = cellContext.viewId
-      ..fieldId = cellContext.fieldInfo.id
-      ..rowId = cellContext.rowId
+      ..viewId = viewId
+      ..fieldId = fieldId
+      ..rowId = rowId
       ..updateOptions.add(option);
 
     return DatabaseEventUpdateChecklistCell(payload).send();
@@ -61,10 +66,9 @@ class ChecklistCellBackendService {
 
   Future<Either<ChecklistCellDataPB, FlowyError>> getCellData() {
     final payload = CellIdPB.create()
-      ..fieldId = cellContext.fieldInfo.id
-      ..viewId = cellContext.viewId
-      ..rowId = cellContext.rowId
-      ..rowId = cellContext.rowId;
+      ..viewId = viewId
+      ..fieldId = fieldId
+      ..rowId = rowId;
 
     return DatabaseEventGetChecklistCellData(payload).send();
   }

+ 8 - 7
frontend/appflowy_flutter/lib/plugins/database_view/application/cell/select_option_cell_service.dart

@@ -1,6 +1,4 @@
-import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
 import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_service.dart';
-import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
 import 'package:dartz/dartz.dart';
 import 'package:appflowy_backend/dispatch/dispatch.dart';
@@ -8,12 +6,15 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/cell_entities.pb.dart';
 
 class SelectOptionCellBackendService {
-  final DatabaseCellContext cellContext;
-  SelectOptionCellBackendService({required this.cellContext});
+  final String viewId;
+  final String fieldId;
+  final String rowId;
 
-  String get viewId => cellContext.viewId;
-  String get fieldId => cellContext.fieldInfo.id;
-  RowId get rowId => cellContext.rowId;
+  SelectOptionCellBackendService({
+    required this.viewId,
+    required this.fieldId,
+    required this.rowId,
+  });
 
   Future<Either<Unit, FlowyError>> create({
     required String name,

+ 9 - 9
frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart

@@ -160,7 +160,7 @@ class DatabaseController {
     });
   }
 
-  Future<Either<RowPB, FlowyError>> createRow({
+  Future<Either<RowMetaPB, FlowyError>> createRow({
     RowId? startRowId,
     String? groupId,
     void Function(RowDataBuilder builder)? withCells,
@@ -181,9 +181,9 @@ class DatabaseController {
   }
 
   Future<Either<Unit, FlowyError>> moveGroupRow({
-    required RowPB fromRow,
+    required RowMetaPB fromRow,
     required String groupId,
-    RowPB? toRow,
+    RowMetaPB? toRow,
   }) {
     return _databaseViewBackendSvc.moveGroupRow(
       fromRowId: fromRow.id,
@@ -193,12 +193,12 @@ class DatabaseController {
   }
 
   Future<Either<Unit, FlowyError>> moveRow({
-    required RowPB fromRow,
-    required RowPB toRow,
+    required String fromRowId,
+    required String toRowId,
   }) {
     return _databaseViewBackendSvc.moveRow(
-      fromRowId: fromRow.id,
-      toRowId: toRow.id,
+      fromRowId: fromRowId,
+      toRowId: toRowId,
     );
   }
 
@@ -269,8 +269,8 @@ class DatabaseController {
       onRowsDeleted: (ids) {
         _databaseCallbacks?.onRowsDeleted?.call(ids);
       },
-      onRowsUpdated: (ids) {
-        _databaseCallbacks?.onRowsUpdated?.call(ids);
+      onRowsUpdated: (ids, reason) {
+        _databaseCallbacks?.onRowsUpdated?.call(ids, reason);
       },
       onRowsCreated: (ids) {
         _databaseCallbacks?.onRowsCreated?.call(ids);

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/application/database_view_service.dart

@@ -30,7 +30,7 @@ class DatabaseViewBackendService {
     return DatabaseEventGetDatabase(payload).send();
   }
 
-  Future<Either<RowPB, FlowyError>> createRow({
+  Future<Either<RowMetaPB, FlowyError>> createRow({
     RowId? startRowId,
     String? groupId,
     Map<String, String>? cellDataByFieldId,

+ 4 - 1
frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart

@@ -13,7 +13,10 @@ typedef OnFiltersChanged = void Function(List<FilterInfo>);
 typedef OnDatabaseChanged = void Function(DatabasePB);
 
 typedef OnRowsCreated = void Function(List<RowId> ids);
-typedef OnRowsUpdated = void Function(List<RowId> ids);
+typedef OnRowsUpdated = void Function(
+  List<RowId> ids,
+  RowsChangedReason reason,
+);
 typedef OnRowsDeleted = void Function(List<RowId> ids);
 typedef OnNumOfRowsChanged = void Function(
   UnmodifiableListView<RowInfo> rows,

+ 2 - 6
frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_cell_bloc.dart

@@ -1,4 +1,3 @@
-import 'package:appflowy_backend/log.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
@@ -52,14 +51,11 @@ class FieldCellBloc extends Bloc<FieldCellEvent, FieldCellState> {
 
   void _startListening() {
     _fieldListener.start(
-      onFieldChanged: (result) {
+      onFieldChanged: (updatedField) {
         if (isClosed) {
           return;
         }
-        result.fold(
-          (field) => add(FieldCellEvent.didReceiveFieldUpdate(field)),
-          (err) => Log.error(err),
-        );
+        add(FieldCellEvent.didReceiveFieldUpdate(updatedField));
       },
     );
   }

+ 6 - 8
frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_listener.dart

@@ -1,4 +1,5 @@
 import 'package:appflowy/core/notification/grid_notification.dart';
+import 'package:appflowy_backend/log.dart';
 import 'package:dartz/dartz.dart';
 import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/notification.pb.dart';
@@ -7,12 +8,11 @@ import 'dart:async';
 import 'dart:typed_data';
 import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
 
-typedef UpdateFieldNotifiedValue = Either<FieldPB, FlowyError>;
+typedef UpdateFieldNotifiedValue = FieldPB;
 
 class SingleFieldListener {
   final String fieldId;
-  PublishNotifier<UpdateFieldNotifiedValue>? _updateFieldNotifier =
-      PublishNotifier();
+  void Function(UpdateFieldNotifiedValue)? _updateFieldNotifier;
   DatabaseNotificationListener? _listener;
 
   SingleFieldListener({required this.fieldId});
@@ -20,7 +20,7 @@ class SingleFieldListener {
   void start({
     required void Function(UpdateFieldNotifiedValue) onFieldChanged,
   }) {
-    _updateFieldNotifier?.addPublishListener(onFieldChanged);
+    _updateFieldNotifier = onFieldChanged;
     _listener = DatabaseNotificationListener(
       objectId: fieldId,
       handler: _handler,
@@ -34,9 +34,8 @@ class SingleFieldListener {
     switch (ty) {
       case DatabaseNotification.DidUpdateField:
         result.fold(
-          (payload) =>
-              _updateFieldNotifier?.value = left(FieldPB.fromBuffer(payload)),
-          (error) => _updateFieldNotifier?.value = right(error),
+          (payload) => _updateFieldNotifier?.call(FieldPB.fromBuffer(payload)),
+          (error) => Log.error(error),
         );
         break;
       default:
@@ -46,7 +45,6 @@ class SingleFieldListener {
 
   Future<void> stop() async {
     await _listener?.stop();
-    _updateFieldNotifier?.dispose();
     _updateFieldNotifier = null;
   }
 }

+ 8 - 0
frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_service.dart

@@ -99,6 +99,14 @@ class FieldBackendService {
       );
     });
   }
+
+  /// Returns the primary field of the view.
+  static Future<Either<FieldPB, FlowyError>> getPrimaryField({
+    required String viewId,
+  }) {
+    final payload = DatabaseViewIdPB.create()..value = viewId;
+    return DatabaseEventGetPrimaryField(payload).send();
+  }
 }
 
 @freezed

+ 163 - 0
frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_banner_bloc.dart

@@ -0,0 +1,163 @@
+import 'package:appflowy/plugins/database_view/application/field/field_listener.dart';
+import 'package:appflowy/plugins/database_view/application/field/field_service.dart';
+import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
+import 'package:appflowy/workspace/application/view/prelude.dart';
+import 'package:appflowy_backend/log.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+import 'row_meta_listener.dart';
+
+part 'row_banner_bloc.freezed.dart';
+
+class RowBannerBloc extends Bloc<RowBannerEvent, RowBannerState> {
+  final String viewId;
+  final RowBackendService _rowBackendSvc;
+  final RowMetaListener _metaListener;
+  SingleFieldListener? _fieldListener;
+
+  RowBannerBloc({
+    required this.viewId,
+    required RowMetaPB rowMeta,
+  })  : _rowBackendSvc = RowBackendService(viewId: viewId),
+        _metaListener = RowMetaListener(rowMeta.id),
+        super(RowBannerState.initial(rowMeta)) {
+    on<RowBannerEvent>(
+      (event, emit) async {
+        event.when(
+          initial: () async {
+            _loadPrimaryField();
+            await _listenRowMeteChanged();
+          },
+          didReceiveRowMeta: (RowMetaPB rowMeta) {
+            emit(
+              state.copyWith(
+                rowMeta: rowMeta,
+              ),
+            );
+          },
+          setCover: (String coverURL) {
+            _updateMeta(coverURL: coverURL);
+          },
+          setIcon: (String iconURL) {
+            _updateMeta(iconURL: iconURL);
+          },
+          didReceiveFieldUpdate: (updatedField) {
+            emit(
+              state.copyWith(
+                primaryField: updatedField,
+                loadingState: const LoadingState.finish(),
+              ),
+            );
+          },
+        );
+      },
+    );
+  }
+
+  @override
+  Future<void> close() async {
+    await _metaListener.stop();
+    await _fieldListener?.stop();
+    _fieldListener = null;
+
+    return super.close();
+  }
+
+  Future<void> _loadPrimaryField() async {
+    final fieldOrError =
+        await FieldBackendService.getPrimaryField(viewId: viewId);
+    fieldOrError.fold(
+      (primaryField) {
+        if (!isClosed) {
+          _fieldListener = SingleFieldListener(fieldId: primaryField.id);
+          _fieldListener?.start(
+            onFieldChanged: (updatedField) {
+              if (!isClosed) {
+                add(RowBannerEvent.didReceiveFieldUpdate(updatedField));
+              }
+            },
+          );
+          add(RowBannerEvent.didReceiveFieldUpdate(primaryField));
+        }
+      },
+      (r) => Log.error(r),
+    );
+  }
+
+  /// Listen the changes of the row meta and then update the banner
+  Future<void> _listenRowMeteChanged() async {
+    _metaListener.start(
+      callback: (rowMeta) {
+        add(RowBannerEvent.didReceiveRowMeta(rowMeta));
+      },
+    );
+  }
+
+  /// Update the meta of the row and the view
+  Future<void> _updateMeta({
+    String? iconURL,
+    String? coverURL,
+  }) async {
+    // Most of the time, the result is success, so we don't need to handle it.
+    await _rowBackendSvc
+        .updateMeta(
+      iconURL: iconURL,
+      coverURL: coverURL,
+      rowId: state.rowMeta.id,
+    )
+        .then((result) {
+      result.fold(
+        (l) => null,
+        (err) => Log.error(err),
+      );
+    });
+
+    // Set the icon and cover of the view
+    ViewBackendService.updateView(
+      viewId: viewId,
+      iconURL: iconURL,
+      coverURL: coverURL,
+    ).then((result) {
+      result.fold(
+        (l) => null,
+        (err) => Log.error(err),
+      );
+    });
+  }
+}
+
+@freezed
+class RowBannerEvent with _$RowBannerEvent {
+  const factory RowBannerEvent.initial() = _Initial;
+  const factory RowBannerEvent.didReceiveRowMeta(RowMetaPB rowMeta) =
+      _DidReceiveRowMeta;
+  const factory RowBannerEvent.didReceiveFieldUpdate(FieldPB field) =
+      _DidReceiveFieldUdate;
+  const factory RowBannerEvent.setIcon(String iconURL) = _SetIcon;
+  const factory RowBannerEvent.setCover(String coverURL) = _SetCover;
+}
+
+@freezed
+class RowBannerState with _$RowBannerState {
+  const factory RowBannerState({
+    ViewPB? view,
+    FieldPB? primaryField,
+    required RowMetaPB rowMeta,
+    required LoadingState loadingState,
+  }) = _RowBannerState;
+
+  factory RowBannerState.initial(RowMetaPB rowMetaPB) => RowBannerState(
+        rowMeta: rowMetaPB,
+        loadingState: const LoadingState.loading(),
+      );
+}
+
+@freezed
+class LoadingState with _$LoadingState {
+  const factory LoadingState.loading() = _Loading;
+  const factory LoadingState.finish() = _Finish;
+}

+ 51 - 48
frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart

@@ -38,17 +38,21 @@ class RowCache {
   final RowCacheDelegate _delegate;
   final RowChangesetNotifier _rowChangeReasonNotifier;
 
+  /// Returns a unmodifiable list of RowInfo
   UnmodifiableListView<RowInfo> get rowInfos {
     final visibleRows = [..._rowList.rows];
     return UnmodifiableListView(visibleRows);
   }
 
+  /// Returns a unmodifiable map of rowId to RowInfo
   UnmodifiableMapView<RowId, RowInfo> get rowByRowId {
     return UnmodifiableMapView(_rowList.rowInfoByRowId);
   }
 
   CellCache get cellCache => _cellCache;
 
+  RowsChangedReason get changeReason => _rowChangeReasonNotifier.reason;
+
   RowCache({
     required this.viewId,
     required RowFieldsDelegate fieldsDelegate,
@@ -70,7 +74,7 @@ class RowCache {
     return _rowList.get(rowId);
   }
 
-  void setInitialRows(List<RowPB> rows) {
+  void setInitialRows(List<RowMetaPB> rows) {
     for (final row in rows) {
       final rowInfo = buildGridRow(row);
       _rowList.add(rowInfo);
@@ -128,7 +132,7 @@ class RowCache {
   void _insertRows(List<InsertedRowPB> insertRows) {
     for (final insertedRow in insertRows) {
       final insertedIndex =
-          _rowList.insert(insertedRow.index, buildGridRow(insertedRow.row));
+          _rowList.insert(insertedRow.index, buildGridRow(insertedRow.rowMeta));
       if (insertedIndex != null) {
         _rowChangeReasonNotifier
             .receive(RowsChangedReason.insert(insertedIndex));
@@ -138,20 +142,23 @@ class RowCache {
 
   void _updateRows(List<UpdatedRowPB> updatedRows) {
     if (updatedRows.isEmpty) return;
-    final List<RowPB> rowPBs = [];
+    final List<RowMetaPB> updatedList = [];
     for (final updatedRow in updatedRows) {
       for (final fieldId in updatedRow.fieldIds) {
         final key = CellCacheKey(
           fieldId: fieldId,
-          rowId: updatedRow.row.id,
+          rowId: updatedRow.rowId,
         );
         _cellCache.remove(key);
       }
-      rowPBs.add(updatedRow.row);
+      if (updatedRow.hasRowMeta()) {
+        updatedList.add(updatedRow.rowMeta);
+      }
     }
 
     final updatedIndexs =
-        _rowList.updateRows(rowPBs, (rowPB) => buildGridRow(rowPB));
+        _rowList.updateRows(updatedList, (rowId) => buildGridRow(rowId));
+
     if (updatedIndexs.isNotEmpty) {
       _rowChangeReasonNotifier.receive(RowsChangedReason.update(updatedIndexs));
     }
@@ -169,7 +176,7 @@ class RowCache {
   void _showRows(List<InsertedRowPB> visibleRows) {
     for (final insertedRow in visibleRows) {
       final insertedIndex =
-          _rowList.insert(insertedRow.index, buildGridRow(insertedRow.row));
+          _rowList.insert(insertedRow.index, buildGridRow(insertedRow.rowMeta));
       if (insertedIndex != null) {
         _rowChangeReasonNotifier
             .receive(RowsChangedReason.insert(insertedIndex));
@@ -197,8 +204,9 @@ class RowCache {
         if (onCellUpdated != null) {
           final rowInfo = _rowList.get(rowId);
           if (rowInfo != null) {
-            final CellContextByFieldId cellDataMap =
-                _makeGridCells(rowId, rowInfo.rowPB);
+            final CellContextByFieldId cellDataMap = _makeGridCells(
+              rowInfo.rowMeta,
+            );
             onCellUpdated(cellDataMap, _rowChangeReasonNotifier.reason);
           }
         }
@@ -220,12 +228,12 @@ class RowCache {
     _rowChangeReasonNotifier.removeListener(callback);
   }
 
-  CellContextByFieldId loadGridCells(RowId rowId) {
-    final RowPB? data = _rowList.get(rowId)?.rowPB;
-    if (data == null) {
-      _loadRow(rowId);
+  CellContextByFieldId loadGridCells(RowMetaPB rowMeta) {
+    final rowInfo = _rowList.get(rowMeta.id);
+    if (rowInfo == null) {
+      _loadRow(rowMeta.id);
     }
-    return _makeGridCells(rowId, data);
+    return _makeGridCells(rowMeta);
   }
 
   Future<void> _loadRow(RowId rowId) async {
@@ -233,57 +241,51 @@ class RowCache {
       ..viewId = viewId
       ..rowId = rowId;
 
-    final result = await DatabaseEventGetRow(payload).send();
+    final result = await DatabaseEventGetRowMeta(payload).send();
     result.fold(
-      (optionRow) => _refreshRow(optionRow),
+      (rowMetaPB) {
+        final rowInfo = _rowList.get(rowMetaPB.id);
+        final rowIndex = _rowList.indexOfRow(rowMetaPB.id);
+        if (rowInfo != null && rowIndex != null) {
+          final updatedRowInfo = rowInfo.copyWith(rowMeta: rowMetaPB);
+          _rowList.remove(rowMetaPB.id);
+          _rowList.insert(rowIndex, updatedRowInfo);
+
+          final UpdatedIndexMap updatedIndexs = UpdatedIndexMap();
+          updatedIndexs[rowMetaPB.id] = UpdatedIndex(
+            index: rowIndex,
+            rowId: rowMetaPB.id,
+          );
+
+          _rowChangeReasonNotifier
+              .receive(RowsChangedReason.update(updatedIndexs));
+        }
+      },
       (err) => Log.error(err),
     );
   }
 
-  CellContextByFieldId _makeGridCells(RowId rowId, RowPB? row) {
+  CellContextByFieldId _makeGridCells(RowMetaPB rowMeta) {
     // ignore: prefer_collection_literals
-    final cellDataMap = CellContextByFieldId();
+    final cellContextMap = CellContextByFieldId();
     for (final field in _delegate.fields) {
       if (field.visibility) {
-        cellDataMap[field.id] = DatabaseCellContext(
-          rowId: rowId,
+        cellContextMap[field.id] = DatabaseCellContext(
+          rowMeta: rowMeta,
           viewId: viewId,
           fieldInfo: field,
         );
       }
     }
-    return cellDataMap;
-  }
-
-  void _refreshRow(OptionalRowPB optionRow) {
-    if (!optionRow.hasRow()) {
-      return;
-    }
-    final updatedRow = optionRow.row;
-    updatedRow.freeze();
-
-    final rowInfo = _rowList.get(updatedRow.id);
-    final rowIndex = _rowList.indexOfRow(updatedRow.id);
-    if (rowInfo != null && rowIndex != null) {
-      final updatedRowInfo = rowInfo.copyWith(rowPB: updatedRow);
-      _rowList.remove(updatedRow.id);
-      _rowList.insert(rowIndex, updatedRowInfo);
-
-      final UpdatedIndexMap updatedIndexs = UpdatedIndexMap();
-      updatedIndexs[rowInfo.rowPB.id] = UpdatedIndex(
-        index: rowIndex,
-        rowId: updatedRowInfo.rowPB.id,
-      );
-
-      _rowChangeReasonNotifier.receive(RowsChangedReason.update(updatedIndexs));
-    }
+    return cellContextMap;
   }
 
-  RowInfo buildGridRow(RowPB rowPB) {
+  RowInfo buildGridRow(RowMetaPB rowMetaPB) {
     return RowInfo(
       viewId: viewId,
       fields: _delegate.fields,
-      rowPB: rowPB,
+      rowId: rowMetaPB.id,
+      rowMeta: rowMetaPB,
     );
   }
 }
@@ -310,9 +312,10 @@ class RowChangesetNotifier extends ChangeNotifier {
 @unfreezed
 class RowInfo with _$RowInfo {
   factory RowInfo({
+    required String rowId,
     required String viewId,
     required UnmodifiableListView<FieldInfo> fields,
-    required RowPB rowPB,
+    required RowMetaPB rowMeta,
   }) = _RowInfo;
 }
 

+ 12 - 9
frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_data_controller.dart

@@ -1,12 +1,12 @@
+import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
 import 'package:flutter/material.dart';
 import '../cell/cell_service.dart';
 import 'row_cache.dart';
-import 'row_service.dart';
 
 typedef OnRowChanged = void Function(CellContextByFieldId, RowsChangedReason);
 
 class RowController {
-  final RowId rowId;
+  final RowMetaPB rowMeta;
   final String? groupId;
   final String viewId;
   final List<VoidCallback> _onRowChangedListeners = [];
@@ -14,24 +14,27 @@ class RowController {
 
   get cellCache => _rowCache.cellCache;
 
+  get rowId => rowMeta.id;
+
   RowController({
-    required this.rowId,
+    required this.rowMeta,
     required this.viewId,
     required RowCache rowCache,
     this.groupId,
   }) : _rowCache = rowCache;
 
   CellContextByFieldId loadData() {
-    return _rowCache.loadGridCells(rowId);
+    return _rowCache.loadGridCells(rowMeta);
   }
 
   void addListener({OnRowChanged? onRowChanged}) {
-    _onRowChangedListeners.add(
-      _rowCache.addListener(
-        rowId: rowId,
-        onCellUpdated: onRowChanged,
-      ),
+    final fn = _rowCache.addListener(
+      rowId: rowMeta.id,
+      onCellUpdated: onRowChanged,
     );
+
+    // Add the listener to the list so that we can remove it later.
+    _onRowChangedListeners.add(fn);
   }
 
   void dispose() {

+ 20 - 19
frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_list.dart

@@ -25,10 +25,9 @@ class RowList {
   }
 
   void add(RowInfo rowInfo) {
-    final rowId = rowInfo.rowPB.id;
+    final rowId = rowInfo.rowId;
     if (contains(rowId)) {
-      final index =
-          _rowInfos.indexWhere((element) => element.rowPB.id == rowId);
+      final index = _rowInfos.indexWhere((element) => element.rowId == rowId);
       _rowInfos.removeAt(index);
       _rowInfos.insert(index, rowInfo);
     } else {
@@ -38,7 +37,7 @@ class RowList {
   }
 
   InsertedIndex? insert(int index, RowInfo rowInfo) {
-    final rowId = rowInfo.rowPB.id;
+    final rowId = rowInfo.rowId;
     var insertedIndex = index;
     if (_rowInfos.length <= insertedIndex) {
       insertedIndex = _rowInfos.length;
@@ -62,7 +61,7 @@ class RowList {
     if (rowInfo != null) {
       final index = _rowInfos.indexOf(rowInfo);
       if (index != -1) {
-        rowInfoByRowId.remove(rowInfo.rowPB.id);
+        rowInfoByRowId.remove(rowInfo.rowId);
         _rowInfos.remove(rowInfo);
       }
       return DeletedIndex(index: index, rowInfo: rowInfo);
@@ -73,23 +72,23 @@ class RowList {
 
   InsertedIndexs insertRows(
     List<InsertedRowPB> insertedRows,
-    RowInfo Function(RowPB) builder,
+    RowInfo Function(RowMetaPB) builder,
   ) {
     final InsertedIndexs insertIndexs = [];
     for (final insertRow in insertedRows) {
-      final isContains = contains(insertRow.row.id);
+      final isContains = contains(insertRow.rowMeta.id);
 
       var index = insertRow.index;
       if (_rowInfos.length < index) {
         index = _rowInfos.length;
       }
-      insert(index, builder(insertRow.row));
+      insert(index, builder(insertRow.rowMeta));
 
       if (!isContains) {
         insertIndexs.add(
           InsertedIndex(
             index: index,
-            rowId: insertRow.row.id,
+            rowId: insertRow.rowMeta.id,
           ),
         );
       }
@@ -105,10 +104,10 @@ class RowList {
     };
 
     _rowInfos.asMap().forEach((index, RowInfo rowInfo) {
-      if (deletedRowByRowId[rowInfo.rowPB.id] == null) {
+      if (deletedRowByRowId[rowInfo.rowId] == null) {
         newRows.add(rowInfo);
       } else {
-        rowInfoByRowId.remove(rowInfo.rowPB.id);
+        rowInfoByRowId.remove(rowInfo.rowId);
         deletedIndex.add(DeletedIndex(index: index, rowInfo: rowInfo));
       }
     });
@@ -117,19 +116,21 @@ class RowList {
   }
 
   UpdatedIndexMap updateRows(
-    List<RowPB> updatedRows,
-    RowInfo Function(RowPB) builder,
+    List<RowMetaPB> rowMetas,
+    RowInfo Function(RowMetaPB) builder,
   ) {
     final UpdatedIndexMap updatedIndexs = UpdatedIndexMap();
-    for (final RowPB updatedRow in updatedRows) {
-      final rowId = updatedRow.id;
+    for (final rowMeta in rowMetas) {
       final index = _rowInfos.indexWhere(
-        (rowInfo) => rowInfo.rowPB.id == rowId,
+        (rowInfo) => rowInfo.rowId == rowMeta.id,
       );
       if (index != -1) {
-        final rowInfo = builder(updatedRow);
+        final rowInfo = builder(rowMeta);
         insert(index, rowInfo);
-        updatedIndexs[rowId] = UpdatedIndex(index: index, rowId: rowId);
+        updatedIndexs[rowMeta.id] = UpdatedIndex(
+          index: index,
+          rowId: rowMeta.id,
+        );
       }
     }
     return updatedIndexs;
@@ -148,7 +149,7 @@ class RowList {
 
   void moveRow(RowId rowId, int oldIndex, int newIndex) {
     final index = _rowInfos.indexWhere(
-      (rowInfo) => rowInfo.rowPB.id == rowId,
+      (rowInfo) => rowInfo.rowId == rowId,
     );
     if (index != -1) {
       final rowInfo = remove(rowId)!.rowInfo;

+ 49 - 0
frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_meta_listener.dart

@@ -0,0 +1,49 @@
+import 'dart:typed_data';
+
+import 'package:appflowy/core/notification/grid_notification.dart';
+import 'package:appflowy_backend/log.dart';
+import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
+import 'package:dartz/dartz.dart';
+
+typedef RowMetaCallback = void Function(RowMetaPB);
+
+class RowMetaListener {
+  final String rowId;
+  RowMetaCallback? _callback;
+  DatabaseNotificationListener? _listener;
+  RowMetaListener(this.rowId);
+
+  void start({required RowMetaCallback callback}) {
+    _callback = callback;
+    _listener = DatabaseNotificationListener(
+      objectId: rowId,
+      handler: _handler,
+    );
+  }
+
+  void _handler(
+    DatabaseNotification ty,
+    Either<Uint8List, FlowyError> result,
+  ) {
+    switch (ty) {
+      case DatabaseNotification.DidUpdateRowMeta:
+        result.fold(
+          (payload) {
+            if (_callback != null) {
+              _callback!(RowMetaPB.fromBuffer(payload));
+            }
+          },
+          (error) => Log.error(error),
+        );
+        break;
+      default:
+        break;
+    }
+  }
+
+  Future<void> stop() async {
+    await _listener?.stop();
+    _callback = null;
+  }
+}

+ 28 - 1
frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_service.dart

@@ -12,7 +12,7 @@ class RowBackendService {
     required this.viewId,
   });
 
-  Future<Either<RowPB, FlowyError>> createRow(RowId rowId) {
+  Future<Either<RowMetaPB, FlowyError>> createRowAfterRow(RowId rowId) {
     final payload = CreateRowPayloadPB.create()
       ..viewId = viewId
       ..startRowId = rowId;
@@ -28,6 +28,33 @@ class RowBackendService {
     return DatabaseEventGetRow(payload).send();
   }
 
+  Future<Either<RowMetaPB, FlowyError>> getRowMeta(RowId rowId) {
+    final payload = RowIdPB.create()
+      ..viewId = viewId
+      ..rowId = rowId;
+
+    return DatabaseEventGetRowMeta(payload).send();
+  }
+
+  Future<Either<Unit, FlowyError>> updateMeta({
+    required String rowId,
+    String? iconURL,
+    String? coverURL,
+  }) {
+    final payload = UpdateRowMetaChangesetPB.create()
+      ..viewId = viewId
+      ..id = rowId;
+
+    if (iconURL != null) {
+      payload.iconUrl = iconURL;
+    }
+    if (coverURL != null) {
+      payload.coverUrl = coverURL;
+    }
+
+    return DatabaseEventUpdateRowMeta(payload).send();
+  }
+
   Future<Either<Unit, FlowyError>> deleteRow(RowId rowId) {
     final payload = RowIdPB.create()
       ..viewId = viewId

+ 5 - 3
frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_cache.dart

@@ -65,14 +65,16 @@ class DatabaseViewCache {
             }
 
             if (changeset.updatedRows.isNotEmpty) {
-              _callbacks?.onRowsUpdated
-                  ?.call(changeset.updatedRows.map((e) => e.row.id).toList());
+              _callbacks?.onRowsUpdated?.call(
+                changeset.updatedRows.map((e) => e.rowId).toList(),
+                _rowCache.changeReason,
+              );
             }
 
             if (changeset.insertedRows.isNotEmpty) {
               _callbacks?.onRowsCreated?.call(
                 changeset.insertedRows
-                    .map((insertedRow) => insertedRow.row.id)
+                    .map((insertedRow) => insertedRow.rowMeta.id)
                     .toList(),
               );
             }

+ 9 - 9
frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart

@@ -156,7 +156,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
     );
   }
 
-  void _groupItemStartEditing(GroupPB group, RowPB row, bool isEdit) {
+  void _groupItemStartEditing(GroupPB group, RowMetaPB row, bool isEdit) {
     final fieldInfo = fieldController.getField(group.fieldId);
     if (fieldInfo == null) {
       Log.warn("fieldInfo should not be null");
@@ -302,12 +302,12 @@ class BoardEvent with _$BoardEvent {
   const factory BoardEvent.createHeaderRow(String groupId) = _CreateHeaderRow;
   const factory BoardEvent.didCreateRow(
     GroupPB group,
-    RowPB row,
+    RowMetaPB row,
     int? index,
   ) = _DidCreateRow;
   const factory BoardEvent.startEditingRow(
     GroupPB group,
-    RowPB row,
+    RowMetaPB row,
   ) = _StartEditRow;
   const factory BoardEvent.endEditingRow(RowId rowId) = _EndEditRow;
   const factory BoardEvent.didReceiveError(FlowyError error) = _DidReceiveError;
@@ -371,7 +371,7 @@ class GridFieldEquatable extends Equatable {
 }
 
 class GroupItem extends AppFlowyGroupItem {
-  final RowPB row;
+  final RowMetaPB row;
   final FieldInfo fieldInfo;
 
   GroupItem({
@@ -389,7 +389,7 @@ class GroupItem extends AppFlowyGroupItem {
 class GroupControllerDelegateImpl extends GroupControllerDelegate {
   final FieldController fieldController;
   final AppFlowyBoardController controller;
-  final void Function(String, RowPB, int?) onNewColumnItem;
+  final void Function(String, RowMetaPB, int?) onNewColumnItem;
 
   GroupControllerDelegateImpl({
     required this.controller,
@@ -398,7 +398,7 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
   });
 
   @override
-  void insertRow(GroupPB group, RowPB row, int? index) {
+  void insertRow(GroupPB group, RowMetaPB row, int? index) {
     final fieldInfo = fieldController.getField(group.fieldId);
     if (fieldInfo == null) {
       Log.warn("fieldInfo should not be null");
@@ -426,7 +426,7 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
   }
 
   @override
-  void updateRow(GroupPB group, RowPB row) {
+  void updateRow(GroupPB group, RowMetaPB row) {
     final fieldInfo = fieldController.getField(group.fieldId);
     if (fieldInfo == null) {
       Log.warn("fieldInfo should not be null");
@@ -442,7 +442,7 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
   }
 
   @override
-  void addNewRow(GroupPB group, RowPB row, int? index) {
+  void addNewRow(GroupPB group, RowMetaPB row, int? index) {
     final fieldInfo = fieldController.getField(group.fieldId);
     if (fieldInfo == null) {
       Log.warn("fieldInfo should not be null");
@@ -465,7 +465,7 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
 
 class BoardEditingRow {
   GroupPB group;
-  RowPB row;
+  RowMetaPB row;
   int? index;
 
   BoardEditingRow({

+ 9 - 9
frontend/appflowy_flutter/lib/plugins/database_view/board/application/group_controller.dart

@@ -12,9 +12,9 @@ typedef OnGroupError = void Function(FlowyError);
 
 abstract class GroupControllerDelegate {
   void removeRow(GroupPB group, RowId rowId);
-  void insertRow(GroupPB group, RowPB row, int? index);
-  void updateRow(GroupPB group, RowPB row);
-  void addNewRow(GroupPB group, RowPB row, int? index);
+  void insertRow(GroupPB group, RowMetaPB row, int? index);
+  void updateRow(GroupPB group, RowMetaPB row);
+  void addNewRow(GroupPB group, RowMetaPB row, int? index);
 }
 
 class GroupController {
@@ -28,7 +28,7 @@ class GroupController {
     required this.delegate,
   }) : _listener = SingleGroupListener(group);
 
-  RowPB? rowAtIndex(int index) {
+  RowMetaPB? rowAtIndex(int index) {
     if (index < group.rows.length) {
       return group.rows[index];
     } else {
@@ -36,7 +36,7 @@ class GroupController {
     }
   }
 
-  RowPB? lastRow() {
+  RowMetaPB? lastRow() {
     if (group.rows.isEmpty) return null;
     return group.rows.last;
   }
@@ -55,15 +55,15 @@ class GroupController {
               final index = insertedRow.hasIndex() ? insertedRow.index : null;
               if (insertedRow.hasIndex() &&
                   group.rows.length > insertedRow.index) {
-                group.rows.insert(insertedRow.index, insertedRow.row);
+                group.rows.insert(insertedRow.index, insertedRow.rowMeta);
               } else {
-                group.rows.add(insertedRow.row);
+                group.rows.add(insertedRow.rowMeta);
               }
 
               if (insertedRow.isNew) {
-                delegate.addNewRow(group, insertedRow.row, index);
+                delegate.addNewRow(group, insertedRow.rowMeta, index);
               } else {
-                delegate.insertRow(group, insertedRow.row, index);
+                delegate.insertRow(group, insertedRow.rowMeta, index);
               }
             }
 

+ 5 - 1
frontend/appflowy_flutter/lib/plugins/database_view/board/board.dart

@@ -45,8 +45,12 @@ class BoardPlugin extends Plugin {
   BoardPlugin({
     required ViewPB view,
     required PluginType pluginType,
+    bool listenOnViewChanged = false,
   })  : _pluginType = pluginType,
-        notifier = ViewPluginNotifier(view: view);
+        notifier = ViewPluginNotifier(
+          view: view,
+          listenOnViewChanged: listenOnViewChanged,
+        );
 
   @override
   PluginWidgetBuilder get widgetBuilder =>

+ 7 - 6
frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart

@@ -231,7 +231,7 @@ class _BoardContentState extends State<BoardContent> {
   ) {
     final groupItem = afGroupItem as GroupItem;
     final groupData = afGroupData.customData as GroupData;
-    final rowPB = groupItem.row;
+    final rowMeta = groupItem.row;
     final rowCache = context.read<BoardBloc>().getRowCache();
 
     /// Return placeholder widget if the rowCache is null.
@@ -255,7 +255,7 @@ class _BoardContentState extends State<BoardContent> {
       margin: config.cardPadding,
       decoration: _makeBoxDecoration(context),
       child: RowCard<String>(
-        row: rowPB,
+        rowMeta: rowMeta,
         viewId: viewId,
         rowCache: rowCache,
         cardData: groupData.group.groupId,
@@ -267,7 +267,7 @@ class _BoardContentState extends State<BoardContent> {
           viewId,
           groupData.group.groupId,
           fieldController,
-          rowPB,
+          rowMeta,
           rowCache,
           context,
         ),
@@ -305,18 +305,19 @@ class _BoardContentState extends State<BoardContent> {
     String viewId,
     String groupId,
     FieldController fieldController,
-    RowPB rowPB,
+    RowMetaPB rowMetaPB,
     RowCache rowCache,
     BuildContext context,
   ) {
     final rowInfo = RowInfo(
       viewId: viewId,
       fields: UnmodifiableListView(fieldController.fieldInfos),
-      rowPB: rowPB,
+      rowMeta: rowMetaPB,
+      rowId: rowMetaPB.id,
     );
 
     final dataController = RowController(
-      rowId: rowInfo.rowPB.id,
+      rowMeta: rowInfo.rowMeta,
       viewId: rowInfo.viewId,
       rowCache: rowCache,
       groupId: groupId,

+ 2 - 2
frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart

@@ -268,7 +268,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
 
     final eventData = CalendarDayEvent(
       event: eventPB,
-      eventId: eventPB.rowId,
+      eventId: eventPB.rowMeta.id,
       dateFieldId: eventPB.dateFieldId,
       date: date,
     );
@@ -310,7 +310,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
         }
         add(CalendarEvent.didDeleteEvents(rowIds));
       },
-      onRowsUpdated: (rowIds) async {
+      onRowsUpdated: (rowIds, reason) async {
         if (isClosed) {
           return;
         }

+ 5 - 1
frontend/appflowy_flutter/lib/plugins/database_view/calendar/calendar.dart

@@ -45,8 +45,12 @@ class CalendarPlugin extends Plugin {
   CalendarPlugin({
     required ViewPB view,
     required PluginType pluginType,
+    bool listenOnViewChanged = false,
   })  : _pluginType = pluginType,
-        notifier = ViewPluginNotifier(view: view);
+        notifier = ViewPluginNotifier(
+          view: view,
+          listenOnViewChanged: listenOnViewChanged,
+        );
 
   @override
   PluginWidgetBuilder get widgetBuilder =>

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_day.dart

@@ -301,7 +301,7 @@ class _EventCard extends StatelessWidget {
       // Add the key here to make sure the card is rebuilt when the cells
       // in this row are updated.
       key: ValueKey(event.eventId),
-      row: rowInfo!.rowPB,
+      rowMeta: rowInfo!.rowMeta,
       viewId: viewId,
       rowCache: rowCache,
       cardData: event.dateFieldId,

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart

@@ -243,7 +243,7 @@ void showEventDetails({
   required RowCache rowCache,
 }) {
   final dataController = RowController(
-    rowId: event.eventId,
+    rowMeta: event.event.rowMeta,
     viewId: viewId,
     rowCache: rowCache,
   );

+ 0 - 61
frontend/appflowy_flutter/lib/plugins/database_view/database_view.dart

@@ -1,61 +0,0 @@
-import 'package:appflowy/startup/plugin/plugin.dart';
-import 'package:appflowy/startup/startup.dart';
-import 'package:appflowy/workspace/application/view/view_ext.dart';
-import 'package:appflowy/workspace/application/view/view_listener.dart';
-import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
-import '../../workspace/presentation/home/home_stack.dart';
-
-/// [DatabaseViewPlugin] is used to build the grid, calendar, and board.
-/// It is a wrapper of the [Plugin] class. The underlying [Plugin] is
-/// determined by the [ViewPB.pluginType] field.
-///
-class DatabaseViewPlugin extends Plugin {
-  final ViewListener _viewListener;
-  ViewPB _view;
-  Plugin _innerPlugin;
-
-  DatabaseViewPlugin({
-    required ViewPB view,
-  })  : _view = view,
-        _innerPlugin = _makeInnerPlugin(view),
-        _viewListener = ViewListener(view: view) {
-    _listenOnLayoutChanged();
-  }
-
-  @override
-  PluginId get id => _innerPlugin.id;
-
-  @override
-  PluginType get pluginType => _innerPlugin.pluginType;
-
-  @override
-  PluginWidgetBuilder get widgetBuilder => _innerPlugin.widgetBuilder;
-
-  @override
-  void dispose() {
-    _viewListener.stop();
-    super.dispose();
-  }
-
-  void _listenOnLayoutChanged() {
-    _viewListener.start(
-      onViewUpdated: (result) {
-        result.fold(
-          (updatedView) {
-            if (_view.layout != updatedView.layout) {
-              _innerPlugin = _makeInnerPlugin(updatedView);
-
-              getIt<HomeStackManager>().setPlugin(_innerPlugin);
-            }
-            _view = updatedView;
-          },
-          (r) => null,
-        );
-      },
-    );
-  }
-}
-
-Plugin _makeInnerPlugin(ViewPB view) {
-  return makePlugin(pluginType: view.pluginType, data: view);
-}

+ 14 - 9
frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart

@@ -33,18 +33,18 @@ class GridBloc extends Bloc<GridEvent, GridState> {
             final rowService = RowBackendService(
               viewId: rowInfo.viewId,
             );
-            await rowService.deleteRow(rowInfo.rowPB.id);
+            await rowService.deleteRow(rowInfo.rowId);
           },
           moveRow: (int from, int to) {
             final List<RowInfo> rows = [...state.rowInfos];
 
-            final fromRow = rows[from].rowPB;
-            final toRow = rows[to].rowPB;
+            final fromRow = rows[from].rowId;
+            final toRow = rows[to].rowId;
 
             rows.insert(to, rows.removeAt(from));
             emit(state.copyWith(rowInfos: rows));
 
-            databaseController.moveRow(fromRow: fromRow, toRow: toRow);
+            databaseController.moveRow(fromRowId: fromRow, toRowId: toRow);
           },
           didReceiveGridUpdate: (grid) {
             emit(state.copyWith(grid: Some(grid)));
@@ -56,7 +56,7 @@ class GridBloc extends Bloc<GridEvent, GridState> {
               ),
             );
           },
-          didReceiveRowUpdate: (newRowInfos, reason) {
+          didLoadRows: (newRowInfos, reason) {
             emit(
               state.copyWith(
                 rowInfos: newRowInfos,
@@ -76,7 +76,7 @@ class GridBloc extends Bloc<GridEvent, GridState> {
     return super.close();
   }
 
-  RowCache? getRowCache(RowId rowId) {
+  RowCache getRowCache(RowId rowId) {
     return databaseController.rowCache;
   }
 
@@ -89,9 +89,14 @@ class GridBloc extends Bloc<GridEvent, GridState> {
       },
       onNumOfRowsChanged: (rowInfos, _, reason) {
         if (!isClosed) {
-          add(GridEvent.didReceiveRowUpdate(rowInfos, reason));
+          add(GridEvent.didLoadRows(rowInfos, reason));
         }
       },
+      onRowsUpdated: (rows, reason) {
+        add(
+          GridEvent.didLoadRows(databaseController.rowCache.rowInfos, reason),
+        );
+      },
       onFieldsChanged: (fields) {
         if (!isClosed) {
           add(GridEvent.didReceiveFieldUpdate(fields));
@@ -122,9 +127,9 @@ class GridEvent with _$GridEvent {
   const factory GridEvent.createRow() = _CreateRow;
   const factory GridEvent.deleteRow(RowInfo rowInfo) = _DeleteRow;
   const factory GridEvent.moveRow(int from, int to) = _MoveRow;
-  const factory GridEvent.didReceiveRowUpdate(
+  const factory GridEvent.didLoadRows(
     List<RowInfo> rows,
-    RowsChangedReason listState,
+    RowsChangedReason reason,
   ) = _DidReceiveRowUpdate;
   const factory GridEvent.didReceiveFieldUpdate(
     List<FieldInfo> fields,

+ 10 - 10
frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_action_sheet_bloc.dart

@@ -4,7 +4,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'package:dartz/dartz.dart';
 
-import '../../../application/row/row_cache.dart';
 import '../../../application/row/row_service.dart';
 
 part 'row_action_sheet_bloc.freezed.dart';
@@ -13,19 +12,20 @@ class RowActionSheetBloc
     extends Bloc<RowActionSheetEvent, RowActionSheetState> {
   final RowBackendService _rowService;
 
-  RowActionSheetBloc({required RowInfo rowInfo})
-      : _rowService = RowBackendService(viewId: rowInfo.viewId),
-        super(RowActionSheetState.initial(rowInfo)) {
+  RowActionSheetBloc({
+    required String viewId,
+    required RowId rowId,
+  })  : _rowService = RowBackendService(viewId: viewId),
+        super(RowActionSheetState.initial(rowId)) {
     on<RowActionSheetEvent>(
       (event, emit) async {
         await event.when(
           deleteRow: () async {
-            final result = await _rowService.deleteRow(state.rowData.rowPB.id);
+            final result = await _rowService.deleteRow(state.rowId);
             logResult(result);
           },
           duplicateRow: () async {
-            final result =
-                await _rowService.duplicateRow(rowId: state.rowData.rowPB.id);
+            final result = await _rowService.duplicateRow(rowId: state.rowId);
             logResult(result);
           },
         );
@@ -47,10 +47,10 @@ class RowActionSheetEvent with _$RowActionSheetEvent {
 @freezed
 class RowActionSheetState with _$RowActionSheetState {
   const factory RowActionSheetState({
-    required RowInfo rowData,
+    required RowId rowId,
   }) = _RowActionSheetState;
 
-  factory RowActionSheetState.initial(RowInfo rowData) => RowActionSheetState(
-        rowData: rowData,
+  factory RowActionSheetState.initial(RowId rowId) => RowActionSheetState(
+        rowId: rowId,
       );
 }

+ 7 - 7
frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_bloc.dart

@@ -15,13 +15,16 @@ part 'row_bloc.freezed.dart';
 class RowBloc extends Bloc<RowEvent, RowState> {
   final RowBackendService _rowBackendSvc;
   final RowController _dataController;
+  final String viewId;
+  final String rowId;
 
   RowBloc({
-    required RowInfo rowInfo,
+    required this.rowId,
+    required this.viewId,
     required RowController dataController,
-  })  : _rowBackendSvc = RowBackendService(viewId: rowInfo.viewId),
+  })  : _rowBackendSvc = RowBackendService(viewId: viewId),
         _dataController = dataController,
-        super(RowState.initial(rowInfo, dataController.loadData())) {
+        super(RowState.initial(dataController.loadData())) {
     on<RowEvent>(
       (event, emit) async {
         await event.when(
@@ -29,7 +32,7 @@ class RowBloc extends Bloc<RowEvent, RowState> {
             await _startListening();
           },
           createRow: () {
-            _rowBackendSvc.createRow(rowInfo.rowPB.id);
+            _rowBackendSvc.createRowAfterRow(rowId);
           },
           didReceiveCells: (cellByFieldId, reason) async {
             final cells = cellByFieldId.values
@@ -78,18 +81,15 @@ class RowEvent with _$RowEvent {
 @freezed
 class RowState with _$RowState {
   const factory RowState({
-    required RowInfo rowInfo,
     required CellContextByFieldId cellByFieldId,
     required UnmodifiableListView<GridCellEquatable> cells,
     RowsChangedReason? changeReason,
   }) = _RowState;
 
   factory RowState.initial(
-    RowInfo rowInfo,
     CellContextByFieldId cellByFieldId,
   ) =>
       RowState(
-        rowInfo: rowInfo,
         cellByFieldId: cellByFieldId,
         cells: UnmodifiableListView(
           cellByFieldId.values

+ 3 - 3
frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_detail_bloc.dart

@@ -27,7 +27,7 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
             }
           },
           didReceiveCellDatas: (cells) {
-            emit(state.copyWith(gridCells: cells));
+            emit(state.copyWith(cells: cells));
           },
           deleteField: (fieldId) {
             _fieldBackendService(fieldId).deleteField();
@@ -95,10 +95,10 @@ class RowDetailEvent with _$RowDetailEvent {
 @freezed
 class RowDetailState with _$RowDetailState {
   const factory RowDetailState({
-    required List<DatabaseCellContext> gridCells,
+    required List<DatabaseCellContext> cells,
   }) = _RowDetailState;
 
   factory RowDetailState.initial() => RowDetailState(
-        gridCells: List.empty(),
+        cells: List.empty(),
       );
 }

+ 126 - 0
frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_document_bloc.dart

@@ -0,0 +1,126 @@
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/workspace/application/view/view_service.dart';
+import 'package:appflowy_backend/log.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
+import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart';
+import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
+
+import '../../../application/row/row_service.dart';
+
+part 'row_document_bloc.freezed.dart';
+
+class RowDocumentBloc extends Bloc<RowDocumentEvent, RowDocumentState> {
+  final String rowId;
+  final RowBackendService _rowBackendSvc;
+
+  RowDocumentBloc({
+    required this.rowId,
+    required String viewId,
+  })  : _rowBackendSvc = RowBackendService(viewId: viewId),
+        super(RowDocumentState.initial()) {
+    on<RowDocumentEvent>(
+      (event, emit) async {
+        await event.when(
+          initial: () async {
+            _getRowDocumentView();
+          },
+          didReceiveRowDocument: (view) {
+            emit(
+              state.copyWith(
+                viewPB: view,
+                loadingState: const LoadingState.finish(),
+              ),
+            );
+          },
+          didReceiveError: (FlowyError error) {
+            emit(
+              state.copyWith(
+                loadingState: LoadingState.error(error),
+              ),
+            );
+          },
+        );
+      },
+    );
+  }
+
+  Future<void> _getRowDocumentView() async {
+    final rowDetailOrError = await _rowBackendSvc.getRowMeta(rowId);
+    rowDetailOrError.fold(
+      (RowMetaPB rowMeta) async {
+        final viewsOrError =
+            await ViewBackendService.getView(rowMeta.documentId);
+
+        if (isClosed) {
+          return;
+        }
+
+        viewsOrError.fold(
+          (view) => add(RowDocumentEvent.didReceiveRowDocument(view)),
+          (error) async {
+            if (error.code == ErrorCode.RecordNotFound.value) {
+              // By default, the document of the row is not exist. So creating a
+              // new document for the given document id of the row.
+              final documentView =
+                  await _createRowDocumentView(rowMeta.documentId);
+              if (documentView != null) {
+                add(RowDocumentEvent.didReceiveRowDocument(documentView));
+              }
+            } else {
+              add(RowDocumentEvent.didReceiveError(error));
+            }
+          },
+        );
+      },
+      (err) => Log.error('Failed to get row detail: $err'),
+    );
+  }
+
+  Future<ViewPB?> _createRowDocumentView(String viewId) async {
+    final result = await ViewBackendService.createOrphanView(
+      viewId: viewId,
+      name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
+      desc: '',
+      layoutType: ViewLayoutPB.Document,
+    );
+    return result.fold(
+      (view) => view,
+      (error) {
+        Log.error(error);
+        return null;
+      },
+    );
+  }
+}
+
+@freezed
+class RowDocumentEvent with _$RowDocumentEvent {
+  const factory RowDocumentEvent.initial() = _InitialRow;
+  const factory RowDocumentEvent.didReceiveRowDocument(ViewPB view) =
+      _DidReceiveRowDocument;
+  const factory RowDocumentEvent.didReceiveError(FlowyError error) =
+      _DidReceiveError;
+}
+
+@freezed
+class RowDocumentState with _$RowDocumentState {
+  const factory RowDocumentState({
+    ViewPB? viewPB,
+    required LoadingState loadingState,
+  }) = _RowDocumentState;
+
+  factory RowDocumentState.initial() => const RowDocumentState(
+        loadingState: LoadingState.loading(),
+      );
+}
+
+@freezed
+class LoadingState with _$LoadingState {
+  const factory LoadingState.loading() = _Loading;
+  const factory LoadingState.error(FlowyError error) = _Error;
+  const factory LoadingState.finish() = _Finish;
+}

+ 5 - 1
frontend/appflowy_flutter/lib/plugins/database_view/grid/grid.dart

@@ -45,8 +45,12 @@ class GridPlugin extends Plugin {
   GridPlugin({
     required ViewPB view,
     required PluginType pluginType,
+    bool listenOnViewChanged = false,
   })  : _pluginType = pluginType,
-        notifier = ViewPluginNotifier(view: view);
+        notifier = ViewPluginNotifier(
+          view: view,
+          listenOnViewChanged: listenOnViewChanged,
+        );
 
   @override
   PluginWidgetBuilder get widgetBuilder =>

+ 50 - 30
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart

@@ -1,5 +1,7 @@
 import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
 import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
+import 'package:appflowy_backend/log.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
 import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart';
@@ -78,7 +80,11 @@ class _GridPageState extends State<GridPage> {
             loading: (_) =>
                 const Center(child: CircularProgressIndicator.adaptive()),
             finish: (result) => result.successOrFail.fold(
-              (_) => const GridShortcuts(child: FlowyGrid()),
+              (_) => GridShortcuts(
+                child: FlowyGrid(
+                  viewId: widget.view.id,
+                ),
+              ),
               (err) => FlowyErrorPage(err.toString()),
             ),
           );
@@ -89,7 +95,9 @@ class _GridPageState extends State<GridPage> {
 }
 
 class FlowyGrid extends StatefulWidget {
+  final String viewId;
   const FlowyGrid({
+    required this.viewId,
     super.key,
   });
 
@@ -125,6 +133,7 @@ class _FlowyGridState extends State<FlowyGrid> {
           scrollController: _scrollController,
           contentWidth: contentWidth,
           child: _GridRows(
+            viewId: widget.viewId,
             verticalScrollController: _scrollController.verticalController,
           ),
         );
@@ -155,7 +164,9 @@ class _FlowyGridState extends State<FlowyGrid> {
 }
 
 class _GridRows extends StatelessWidget {
+  final String viewId;
   const _GridRows({
+    required this.viewId,
     required this.verticalScrollController,
   });
 
@@ -207,7 +218,7 @@ class _GridRows extends StatelessWidget {
                 final rowInfo = rowInfos[index];
                 return _renderRow(
                   context,
-                  rowInfo,
+                  rowInfo.rowId,
                   index: index,
                   isSortEnabled: sortState.sortInfos.isNotEmpty,
                   isFilterEnabled: filterState.filters.isNotEmpty,
@@ -223,38 +234,38 @@ class _GridRows extends StatelessWidget {
 
   Widget _renderRow(
     BuildContext context,
-    RowInfo rowInfo, {
+    RowId rowId, {
     int? index,
     bool isSortEnabled = false,
     bool isFilterEnabled = false,
     Animation<double>? animation,
   }) {
-    final rowCache = context.read<GridBloc>().getRowCache(
-          rowInfo.rowPB.id,
-        );
+    final rowCache = context.read<GridBloc>().getRowCache(rowId);
+    final rowMeta = rowCache.getRow(rowId)?.rowMeta;
 
-    /// Return placeholder widget if the rowCache is null.
-    if (rowCache == null) return const SizedBox.shrink();
+    /// Return placeholder widget if the rowMeta is null.
+    if (rowMeta == null) return const SizedBox.shrink();
 
     final fieldController =
         context.read<GridBloc>().databaseController.fieldController;
     final dataController = RowController(
-      rowId: rowInfo.rowPB.id,
-      viewId: rowInfo.viewId,
+      viewId: viewId,
+      rowMeta: rowMeta,
       rowCache: rowCache,
     );
 
     final child = GridRow(
-      key: ValueKey(rowInfo.rowPB.id),
+      key: ValueKey(rowMeta.id),
+      rowId: rowId,
+      viewId: viewId,
       index: index,
       isDraggable: !isSortEnabled && !isFilterEnabled,
-      rowInfo: rowInfo,
       dataController: dataController,
       cellBuilder: GridCellBuilder(cellCache: dataController.cellCache),
       openDetailPage: (context, cellBuilder) {
         _openRowDetailPage(
           context,
-          rowInfo,
+          rowId,
           fieldController,
           rowCache,
           cellBuilder,
@@ -274,26 +285,32 @@ class _GridRows extends StatelessWidget {
 
   void _openRowDetailPage(
     BuildContext context,
-    RowInfo rowInfo,
+    RowId rowId,
     FieldController fieldController,
     RowCache rowCache,
     GridCellBuilder cellBuilder,
   ) {
-    final dataController = RowController(
-      viewId: rowInfo.viewId,
-      rowId: rowInfo.rowPB.id,
-      rowCache: rowCache,
-    );
+    final rowMeta = rowCache.getRow(rowId)?.rowMeta;
+    // Most of the cases, the rowMeta should not be null.
+    if (rowMeta != null) {
+      final dataController = RowController(
+        viewId: viewId,
+        rowMeta: rowMeta,
+        rowCache: rowCache,
+      );
 
-    FlowyOverlay.show(
-      context: context,
-      builder: (BuildContext context) {
-        return RowDetailPage(
-          cellBuilder: cellBuilder,
-          rowController: dataController,
-        );
-      },
-    );
+      FlowyOverlay.show(
+        context: context,
+        builder: (BuildContext context) {
+          return RowDetailPage(
+            cellBuilder: cellBuilder,
+            rowController: dataController,
+          );
+        },
+      );
+    } else {
+      Log.warn('RowMeta is null for rowId: $rowId');
+    }
   }
 }
 
@@ -357,10 +374,9 @@ class _RowCountBadge extends StatelessWidget {
             mainAxisAlignment: MainAxisAlignment.start,
             children: [
               FlowyText.medium(
-                '${LocaleKeys.grid_row_count.tr()} : ',
+                rowCountString(rowCount),
                 color: Theme.of(context).hintColor,
               ),
-              FlowyText.medium(rowCount.toString()),
             ],
           ),
         );
@@ -368,3 +384,7 @@ class _RowCountBadge extends StatelessWidget {
     );
   }
 }
+
+String rowCountString(int count) {
+  return '${LocaleKeys.grid_row_count.tr()} : $count';
+}

+ 9 - 9
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_editor.dart

@@ -52,11 +52,11 @@ class _FieldEditorState extends State<FieldEditor> {
   @override
   Widget build(BuildContext context) {
     final List<Widget> children = [
-      _FieldNameTextField(popoverMutex: popoverMutex),
+      FieldNameTextField(popoverMutex: popoverMutex),
       if (widget.onDeleted != null) _addDeleteFieldButton(),
       if (widget.onHidden != null) _addHideFieldButton(),
       if (!widget.typeOptionLoader.field.isPrimary)
-        _FieldTypeOptionCell(popoverMutex: popoverMutex),
+        FieldTypeOptionCell(popoverMutex: popoverMutex),
     ];
     return BlocProvider(
       create: (context) {
@@ -116,10 +116,10 @@ class _FieldEditorState extends State<FieldEditor> {
   }
 }
 
-class _FieldTypeOptionCell extends StatelessWidget {
+class FieldTypeOptionCell extends StatelessWidget {
   final PopoverMutex popoverMutex;
 
-  const _FieldTypeOptionCell({
+  const FieldTypeOptionCell({
     Key? key,
     required this.popoverMutex,
   }) : super(key: key);
@@ -130,7 +130,7 @@ class _FieldTypeOptionCell extends StatelessWidget {
       buildWhen: (p, c) => p.field != c.field,
       builder: (context, state) {
         return state.field.fold(
-          () => const SizedBox(),
+          () => const SizedBox.shrink(),
           (fieldInfo) {
             final dataController =
                 context.read<FieldEditorBloc>().dataController;
@@ -145,18 +145,18 @@ class _FieldTypeOptionCell extends StatelessWidget {
   }
 }
 
-class _FieldNameTextField extends StatefulWidget {
+class FieldNameTextField extends StatefulWidget {
   final PopoverMutex popoverMutex;
-  const _FieldNameTextField({
+  const FieldNameTextField({
     required this.popoverMutex,
     Key? key,
   }) : super(key: key);
 
   @override
-  State<_FieldNameTextField> createState() => _FieldNameTextFieldState();
+  State<FieldNameTextField> createState() => _FieldNameTextFieldState();
 }
 
-class _FieldNameTextFieldState extends State<_FieldNameTextField> {
+class _FieldNameTextFieldState extends State<FieldNameTextField> {
   final textController = TextEditingController();
   FocusNode focusNode = FocusNode();
 

+ 3 - 3
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart

@@ -48,7 +48,7 @@ class FieldTypeOptionEditor extends StatelessWidget {
           );
 
           final List<Widget> children = [
-            _SwitchFieldButton(popoverMutex: popoverMutex),
+            SwitchFieldButton(popoverMutex: popoverMutex),
             if (typeOptionWidget != null) typeOptionWidget
           ];
 
@@ -73,9 +73,9 @@ class FieldTypeOptionEditor extends StatelessWidget {
   }
 }
 
-class _SwitchFieldButton extends StatelessWidget {
+class SwitchFieldButton extends StatelessWidget {
   final PopoverMutex popoverMutex;
-  const _SwitchFieldButton({
+  const SwitchFieldButton({
     required this.popoverMutex,
     Key? key,
   }) : super(key: key);

+ 9 - 4
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/action.dart

@@ -1,4 +1,4 @@
-import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
+import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
 import 'package:appflowy/plugins/database_view/grid/application/row/row_action_sheet_bloc.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:appflowy/generated/locale_keys.g.dart';
@@ -14,13 +14,18 @@ import 'package:flutter_bloc/flutter_bloc.dart';
 import '../../layout/sizes.dart';
 
 class RowActions extends StatelessWidget {
-  final RowInfo rowData;
-  const RowActions({required this.rowData, Key? key}) : super(key: key);
+  final String viewId;
+  final RowId rowId;
+  const RowActions({
+    required this.viewId,
+    required this.rowId,
+    Key? key,
+  }) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
     return BlocProvider(
-      create: (context) => RowActionSheetBloc(rowInfo: rowData),
+      create: (context) => RowActionSheetBloc(viewId: viewId, rowId: rowId),
       child: BlocBuilder<RowActionSheetBloc, RowActionSheetState>(
         builder: (context, state) {
           final cells = _RowAction.values

+ 24 - 15
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/row.dart

@@ -1,6 +1,6 @@
 import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
-import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
 import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart';
+import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
 import 'package:appflowy/plugins/database_view/grid/application/row/row_bloc.dart';
 import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
@@ -20,7 +20,8 @@ import "package:appflowy/generated/locale_keys.g.dart";
 import 'package:easy_localization/easy_localization.dart';
 
 class GridRow extends StatefulWidget {
-  final RowInfo rowInfo;
+  final RowId viewId;
+  final RowId rowId;
   final RowController dataController;
   final GridCellBuilder cellBuilder;
   final void Function(BuildContext, GridCellBuilder) openDetailPage;
@@ -30,7 +31,8 @@ class GridRow extends StatefulWidget {
 
   const GridRow({
     super.key,
-    required this.rowInfo,
+    required this.viewId,
+    required this.rowId,
     required this.dataController,
     required this.cellBuilder,
     required this.openDetailPage,
@@ -49,8 +51,9 @@ class _GridRowState extends State<GridRow> {
   void initState() {
     super.initState();
     _rowBloc = RowBloc(
-      rowInfo: widget.rowInfo,
+      rowId: widget.rowId,
       dataController: widget.dataController,
+      viewId: widget.viewId,
     );
     _rowBloc.add(const RowEvent.initial());
   }
@@ -61,7 +64,8 @@ class _GridRowState extends State<GridRow> {
       value: _rowBloc,
       child: _RowEnterRegion(
         child: BlocBuilder<RowBloc, RowState>(
-          buildWhen: (p, c) => p.rowInfo.rowPB.height != c.rowInfo.rowPB.height,
+          // The row need to rebuild when the cell count changes.
+          buildWhen: (p, c) => p.cellByFieldId.length != c.cellByFieldId.length,
           builder: (context, state) {
             final content = Expanded(
               child: RowContent(
@@ -126,7 +130,11 @@ class _RowLeadingState extends State<_RowLeading> {
       direction: PopoverDirection.rightWithCenterAligned,
       margin: const EdgeInsets.all(6),
       popupBuilder: (BuildContext popoverContext) {
-        return RowActions(rowData: context.read<RowBloc>().state.rowInfo);
+        final bloc = context.read<RowBloc>();
+        return RowActions(
+          viewId: bloc.viewId,
+          rowId: bloc.rowId,
+        );
       },
       child: Consumer<RegionStateNotifier>(
         builder: (context, state, _) {
@@ -143,11 +151,11 @@ class _RowLeadingState extends State<_RowLeading> {
     return Row(
       mainAxisAlignment: MainAxisAlignment.center,
       children: [
-        const _InsertButton(),
+        const InsertRowButton(),
         if (isDraggable) ...[
           ReorderableDragStartListener(
             index: widget.index!,
-            child: _MenuButton(
+            child: RowMenuButton(
               isDragEnabled: isDraggable,
               openMenu: () {
                 popoverController.show();
@@ -155,7 +163,7 @@ class _RowLeadingState extends State<_RowLeading> {
             ),
           ),
         ] else ...[
-          _MenuButton(
+          RowMenuButton(
             openMenu: () {
               popoverController.show();
             },
@@ -168,8 +176,8 @@ class _RowLeadingState extends State<_RowLeading> {
   bool get isDraggable => widget.index != null && widget.isDraggable;
 }
 
-class _InsertButton extends StatelessWidget {
-  const _InsertButton({Key? key}) : super(key: key);
+class InsertRowButton extends StatelessWidget {
+  const InsertRowButton({Key? key}) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
@@ -188,20 +196,21 @@ class _InsertButton extends StatelessWidget {
   }
 }
 
-class _MenuButton extends StatefulWidget {
+class RowMenuButton extends StatefulWidget {
   final VoidCallback openMenu;
   final bool isDragEnabled;
 
-  const _MenuButton({
+  const RowMenuButton({
     required this.openMenu,
     this.isDragEnabled = false,
+    super.key,
   });
 
   @override
-  State<_MenuButton> createState() => _MenuButtonState();
+  State<RowMenuButton> createState() => _RowMenuButtonState();
 }
 
-class _MenuButtonState extends State<_MenuButton> {
+class _RowMenuButtonState extends State<RowMenuButton> {
   @override
   Widget build(BuildContext context) {
     return FlowyIconButton(

+ 5 - 4
frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card.dart

@@ -17,7 +17,7 @@ import 'container/card_container.dart';
 
 /// Edit a database row with card style widget
 class RowCard<CustomCardData> extends StatefulWidget {
-  final RowPB row;
+  final RowMetaPB rowMeta;
   final String viewId;
   final String? groupingFieldId;
 
@@ -46,7 +46,7 @@ class RowCard<CustomCardData> extends StatefulWidget {
   final RowCardStyleConfiguration styleConfiguration;
 
   const RowCard({
-    required this.row,
+    required this.rowMeta,
     required this.viewId,
     this.groupingFieldId,
     required this.isEditing,
@@ -81,7 +81,7 @@ class _RowCardState<T> extends State<RowCard<T>> {
       viewId: widget.viewId,
       groupFieldId: widget.groupingFieldId,
       isEditing: widget.isEditing,
-      row: widget.row,
+      rowMeta: widget.rowMeta,
       rowCache: widget.rowCache,
     )..add(const RowCardEvent.initial());
 
@@ -178,7 +178,8 @@ class _RowCardState<T> extends State<RowCard<T>> {
         throw UnimplementedError();
       case AccessoryType.more:
         return RowActions(
-          rowData: context.read<CardBloc>().rowInfo(),
+          viewId: context.read<CardBloc>().viewId,
+          rowId: context.read<CardBloc>().rowMeta.id,
         );
     }
   }

+ 8 - 10
frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_bloc.dart

@@ -12,24 +12,24 @@ import '../../application/row/row_service.dart';
 part 'card_bloc.freezed.dart';
 
 class CardBloc extends Bloc<RowCardEvent, RowCardState> {
-  final RowPB row;
+  final RowMetaPB rowMeta;
   final String? groupFieldId;
   final RowBackendService _rowBackendSvc;
   final RowCache _rowCache;
   VoidCallback? _rowCallback;
+  final String viewId;
 
   CardBloc({
-    required this.row,
+    required this.rowMeta,
     required this.groupFieldId,
-    required String viewId,
+    required this.viewId,
     required RowCache rowCache,
     required bool isEditing,
   })  : _rowBackendSvc = RowBackendService(viewId: viewId),
         _rowCache = rowCache,
         super(
           RowCardState.initial(
-            row,
-            _makeCells(groupFieldId, rowCache.loadGridCells(row.id)),
+            _makeCells(groupFieldId, rowCache.loadGridCells(rowMeta)),
             isEditing,
           ),
         ) {
@@ -70,13 +70,14 @@ class CardBloc extends Bloc<RowCardEvent, RowCardState> {
       fields: UnmodifiableListView(
         state.cells.map((cell) => cell.fieldInfo).toList(),
       ),
-      rowPB: state.rowPB,
+      rowId: rowMeta.id,
+      rowMeta: rowMeta,
     );
   }
 
   Future<void> _startListening() async {
     _rowCallback = _rowCache.addListener(
-      rowId: row.id,
+      rowId: rowMeta.id,
       onCellUpdated: (cellMap, reason) {
         if (!isClosed) {
           final cells = _makeCells(groupFieldId, cellMap);
@@ -118,19 +119,16 @@ class RowCardEvent with _$RowCardEvent {
 @freezed
 class RowCardState with _$RowCardState {
   const factory RowCardState({
-    required RowPB rowPB,
     required List<DatabaseCellContext> cells,
     required bool isEditing,
     RowsChangedReason? changeReason,
   }) = _RowCardState;
 
   factory RowCardState.initial(
-    RowPB rowPB,
     List<DatabaseCellContext> cells,
     bool isEditing,
   ) =>
       RowCardState(
-        rowPB: rowPB,
         cells: cells,
         isEditing: isEditing,
       );

+ 7 - 15
frontend/appflowy_flutter/lib/plugins/database_view/widgets/database_view_widget.dart

@@ -1,4 +1,3 @@
-import 'package:appflowy/startup/plugin/plugin.dart';
 import 'package:appflowy/workspace/application/view/view_ext.dart';
 import 'package:appflowy/workspace/application/view/view_listener.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
@@ -47,26 +46,19 @@ class _DatabaseViewWidgetState extends State<DatabaseViewWidget> {
     return ValueListenableBuilder<ViewLayoutPB>(
       valueListenable: _layoutTypeChangeNotifier,
       builder: (_, __, ___) {
-        return makePlugin(pluginType: view.pluginType, data: view)
-            .widgetBuilder
-            .buildWidget();
+        return view.plugin().widgetBuilder.buildWidget();
       },
     );
   }
 
   void _listenOnViewUpdated() {
-    _listener = ViewListener(view: widget.view)
+    _listener = ViewListener(viewId: widget.view.id)
       ..start(
-        onViewUpdated: (result) {
-          result.fold(
-            (updatedView) {
-              if (mounted) {
-                view = updatedView;
-                _layoutTypeChangeNotifier.value = view.layout;
-              }
-            },
-            (r) => null,
-          );
+        onViewUpdated: (updatedView) {
+          if (mounted) {
+            view = updatedView;
+            _layoutTypeChangeNotifier.value = view.layout;
+          }
         },
       );
 

+ 1 - 0
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart

@@ -14,6 +14,7 @@ import 'cells/select_option_cell/select_option_cell.dart';
 import 'cells/text_cell/text_cell.dart';
 import 'cells/url_cell/url_cell.dart';
 
+/// Build the cell widget in Grid style.
 class GridCellBuilder {
   final CellCache cellCache;
   GridCellBuilder({

+ 7 - 0
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/cells.dart

@@ -0,0 +1,7 @@
+export 'checkbox_cell/checkbox_cell.dart';
+export 'checklist_cell/checklist_cell.dart';
+export 'date_cell/date_cell.dart';
+export 'number_cell/number_cell.dart';
+export 'select_option_cell/select_option_cell.dart';
+export 'text_cell/text_cell.dart';
+export 'url_cell/url_cell.dart';

+ 20 - 2
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checkbox_cell/checkbox_cell.dart

@@ -40,8 +40,8 @@ class _CheckboxCellState extends GridCellState<GridCheckboxCell> {
       child: BlocBuilder<CheckboxCellBloc, CheckboxCellState>(
         builder: (context, state) {
           final icon = state.isSelected
-              ? svgWidget('editor/editor_check')
-              : svgWidget('editor/editor_uncheck');
+              ? const CheckboxCellCheck()
+              : const CheckboxCellUncheck();
           return Align(
             alignment: Alignment.centerLeft,
             child: Padding(
@@ -82,3 +82,21 @@ class _CheckboxCellState extends GridCellState<GridCheckboxCell> {
     }
   }
 }
+
+class CheckboxCellCheck extends StatelessWidget {
+  const CheckboxCellCheck({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return svgWidget('editor/editor_check');
+  }
+}
+
+class CheckboxCellUncheck extends StatelessWidget {
+  const CheckboxCellUncheck({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return svgWidget('editor/editor_uncheck');
+  }
+}

+ 3 - 1
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_bloc.dart

@@ -16,7 +16,9 @@ class ChecklistCardCellBloc
   ChecklistCardCellBloc({
     required this.cellController,
   })  : _checklistCellSvc = ChecklistCellBackendService(
-          cellContext: cellController.cellContext,
+          viewId: cellController.viewId,
+          fieldId: cellController.fieldId,
+          rowId: cellController.rowId,
         ),
         super(ChecklistCellState.initial(cellController)) {
     on<ChecklistCellEvent>(

+ 3 - 1
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor_bloc.dart

@@ -19,7 +19,9 @@ class ChecklistCellEditorBloc
   ChecklistCellEditorBloc({
     required this.cellController,
   })  : _checklistCellService = ChecklistCellBackendService(
-          cellContext: cellController.cellContext,
+          viewId: cellController.viewId,
+          fieldId: cellController.fieldId,
+          rowId: cellController.rowId,
         ),
         super(ChecklistCellEditorState.initial(cellController)) {
     on<ChecklistCellEditorEvent>(

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_progress_bar.dart

@@ -22,7 +22,7 @@ class ChecklistProgressBar extends StatelessWidget {
       percent: percent,
       padding: EdgeInsets.zero,
       progressColor: percent < 1.0
-          ? SelectOptionColorPB.Blue.toColor(context)
+          ? SelectOptionColorPB.Purple.toColor(context)
           : SelectOptionColorPB.Green.toColor(context),
       backgroundColor: AFThemeExtension.of(context).progressBarBGColor,
       barRadius: const Radius.circular(5),

+ 3 - 1
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor_bloc.dart

@@ -17,7 +17,9 @@ class SelectOptionCellEditorBloc
   SelectOptionCellEditorBloc({
     required this.cellController,
   })  : _selectOptionService = SelectOptionCellBackendService(
-          cellContext: cellController.cellContext,
+          viewId: cellController.viewId,
+          fieldId: cellController.fieldId,
+          rowId: cellController.rowId,
         ),
         super(SelectOptionEditorState.initial(cellController)) {
     on<SelectOptionEditorEvent>(

+ 43 - 18
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/text_cell/text_cell.dart

@@ -1,6 +1,7 @@
 import 'dart:async';
 import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
 import 'package:appflowy/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import '../../../../grid/presentation/layout/sizes.dart';
@@ -10,17 +11,23 @@ class GridTextCellStyle extends GridCellStyle {
   String? placeholder;
   TextStyle? textStyle;
   bool? autofocus;
+  double emojiFontSize;
+  double emojiHPadding;
+  bool showEmoji;
 
   GridTextCellStyle({
     this.placeholder,
     this.textStyle,
     this.autofocus,
+    this.showEmoji = true,
+    this.emojiFontSize = 16,
+    this.emojiHPadding = 0,
   });
 }
 
 class GridTextCell extends GridCellWidget {
   final CellControllerBuilder cellControllerBuilder;
-  late final GridTextCellStyle? cellStyle;
+  late final GridTextCellStyle cellStyle;
   GridTextCell({
     required this.cellControllerBuilder,
     GridCellStyle? style,
@@ -29,7 +36,7 @@ class GridTextCell extends GridCellWidget {
     if (style != null) {
       cellStyle = (style as GridTextCellStyle);
     } else {
-      cellStyle = null;
+      cellStyle = GridTextCellStyle();
     }
   }
 
@@ -66,22 +73,40 @@ class _GridTextCellState extends GridFocusNodeCellState<GridTextCell> {
             left: GridSize.cellContentInsets.left,
             right: GridSize.cellContentInsets.right,
           ),
-          child: TextField(
-            controller: _controller,
-            focusNode: focusNode,
-            maxLines: null,
-            style: widget.cellStyle?.textStyle ??
-                Theme.of(context).textTheme.bodyMedium,
-            autofocus: widget.cellStyle?.autofocus ?? false,
-            decoration: InputDecoration(
-              contentPadding: EdgeInsets.only(
-                top: GridSize.cellContentInsets.top,
-                bottom: GridSize.cellContentInsets.bottom,
-              ),
-              border: InputBorder.none,
-              hintText: widget.cellStyle?.placeholder,
-              isDense: true,
-            ),
+          child: Row(
+            children: [
+              if (widget.cellStyle.showEmoji)
+                // Only build the emoji when it changes
+                BlocBuilder<TextCellBloc, TextCellState>(
+                  buildWhen: (p, c) => p.emoji != c.emoji,
+                  builder: (context, state) => Center(
+                    child: FlowyText(
+                      state.emoji,
+                      fontSize: widget.cellStyle.emojiFontSize,
+                    ),
+                  ),
+                ),
+              HSpace(widget.cellStyle.emojiHPadding),
+              Expanded(
+                child: TextField(
+                  controller: _controller,
+                  focusNode: focusNode,
+                  maxLines: null,
+                  style: widget.cellStyle.textStyle ??
+                      Theme.of(context).textTheme.bodyMedium,
+                  autofocus: widget.cellStyle.autofocus ?? false,
+                  decoration: InputDecoration(
+                    contentPadding: EdgeInsets.only(
+                      top: GridSize.cellContentInsets.top,
+                      bottom: GridSize.cellContentInsets.bottom,
+                    ),
+                    border: InputBorder.none,
+                    hintText: widget.cellStyle.placeholder,
+                    isDense: true,
+                  ),
+                ),
+              )
+            ],
           ),
         ),
       ),

+ 11 - 0
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart

@@ -26,6 +26,9 @@ class TextCellBloc extends Bloc<TextCellEvent, TextCellState> {
           didReceiveCellUpdate: (content) {
             emit(state.copyWith(content: content));
           },
+          didUpdateEmoji: (String emoji) {
+            emit(state.copyWith(emoji: emoji));
+          },
         );
       },
     );
@@ -48,6 +51,11 @@ class TextCellBloc extends Bloc<TextCellEvent, TextCellState> {
           add(TextCellEvent.didReceiveCellUpdate(cellContent ?? ""));
         }
       }),
+      onRowMetaChanged: () {
+        if (!isClosed) {
+          add(TextCellEvent.didUpdateEmoji(cellController.emoji ?? ""));
+        }
+      },
     );
   }
 }
@@ -58,15 +66,18 @@ class TextCellEvent with _$TextCellEvent {
   const factory TextCellEvent.didReceiveCellUpdate(String cellContent) =
       _DidReceiveCellUpdate;
   const factory TextCellEvent.updateText(String text) = _UpdateText;
+  const factory TextCellEvent.didUpdateEmoji(String emoji) = _UpdateEmoji;
 }
 
 @freezed
 class TextCellState with _$TextCellState {
   const factory TextCellState({
     required String content,
+    required String emoji,
   }) = _TextCellState;
 
   factory TextCellState.initial(TextCellController context) => TextCellState(
         content: context.getCellData() ?? "",
+        emoji: context.emoji ?? "",
       );
 }

+ 174 - 0
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_action.dart

@@ -0,0 +1,174 @@
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
+import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_service.dart';
+import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart';
+import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart';
+import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
+import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.dart';
+import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
+import 'package:appflowy_backend/log.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
+import 'package:appflowy_popover/appflowy_popover.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra/image.dart';
+import 'package:flowy_infra/theme_extension.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class RowActionList extends StatelessWidget {
+  final RowController rowController;
+  const RowActionList({
+    required String viewId,
+    required this.rowController,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      mainAxisSize: MainAxisSize.min,
+      children: [
+        Padding(
+          padding: const EdgeInsets.only(left: 10),
+          child: FlowyText(LocaleKeys.grid_row_action.tr()),
+        ),
+        const VSpace(15),
+        RowDetailPageDeleteButton(rowId: rowController.rowId),
+        RowDetailPageDuplicateButton(
+          rowId: rowController.rowId,
+          groupId: rowController.groupId,
+        ),
+      ],
+    );
+  }
+}
+
+class RowDetailPageDeleteButton extends StatelessWidget {
+  final String rowId;
+  const RowDetailPageDeleteButton({required this.rowId, Key? key})
+      : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return SizedBox(
+      height: GridSize.popoverItemHeight,
+      child: FlowyButton(
+        text: FlowyText.regular(LocaleKeys.grid_row_delete.tr()),
+        leftIcon: const FlowySvg(name: "home/trash"),
+        onTap: () {
+          context.read<RowDetailBloc>().add(RowDetailEvent.deleteRow(rowId));
+          FlowyOverlay.pop(context);
+        },
+      ),
+    );
+  }
+}
+
+class RowDetailPageDuplicateButton extends StatelessWidget {
+  final String rowId;
+  final String? groupId;
+  const RowDetailPageDuplicateButton({
+    required this.rowId,
+    this.groupId,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return SizedBox(
+      height: GridSize.popoverItemHeight,
+      child: FlowyButton(
+        text: FlowyText.regular(LocaleKeys.grid_row_duplicate.tr()),
+        leftIcon: const FlowySvg(name: "grid/duplicate"),
+        onTap: () {
+          context
+              .read<RowDetailBloc>()
+              .add(RowDetailEvent.duplicateRow(rowId, groupId));
+          FlowyOverlay.pop(context);
+        },
+      ),
+    );
+  }
+}
+
+class CreateRowFieldButton extends StatefulWidget {
+  final String viewId;
+
+  const CreateRowFieldButton({
+    required this.viewId,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  State<CreateRowFieldButton> createState() => _CreateRowFieldButtonState();
+}
+
+class _CreateRowFieldButtonState extends State<CreateRowFieldButton> {
+  late PopoverController popoverController;
+  late TypeOptionPB typeOption;
+
+  @override
+  void initState() {
+    popoverController = PopoverController();
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return AppFlowyPopover(
+      constraints: BoxConstraints.loose(const Size(240, 200)),
+      controller: popoverController,
+      direction: PopoverDirection.topWithLeftAligned,
+      triggerActions: PopoverTriggerFlags.none,
+      margin: EdgeInsets.zero,
+      child: SizedBox(
+        height: 40,
+        child: FlowyButton(
+          text: FlowyText.medium(
+            LocaleKeys.grid_field_newProperty.tr(),
+            color: AFThemeExtension.of(context).textColor,
+          ),
+          hoverColor: AFThemeExtension.of(context).lightGreyHover,
+          onTap: () async {
+            final result = await TypeOptionBackendService.createFieldTypeOption(
+              viewId: widget.viewId,
+            );
+            result.fold(
+              (l) {
+                typeOption = l;
+                popoverController.show();
+              },
+              (r) => Log.error("Failed to create field type option: $r"),
+            );
+          },
+          leftIcon: svgWidget(
+            "home/add",
+            color: AFThemeExtension.of(context).textColor,
+          ),
+        ),
+      ),
+      popupBuilder: (BuildContext popOverContext) {
+        return FieldEditor(
+          viewId: widget.viewId,
+          typeOptionLoader: FieldTypeOptionLoader(
+            viewId: widget.viewId,
+            field: typeOption.field_2,
+          ),
+          onDeleted: (fieldId) {
+            popoverController.close();
+            NavigatorAlertDialog(
+              title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
+              confirm: () {
+                context
+                    .read<RowDetailBloc>()
+                    .add(RowDetailEvent.deleteField(fieldId));
+              },
+            ).show(context);
+          },
+        );
+      },
+    );
+  }
+}

+ 268 - 0
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_banner.dart

@@ -0,0 +1,268 @@
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/database_view/application/row/row_banner_bloc.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/emoji_picker/emoji_picker.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
+import 'package:appflowy_popover/appflowy_popover.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+typedef RowBannerCellBuilder = Widget Function(String fieldId);
+
+class RowBanner extends StatefulWidget {
+  final String viewId;
+  final RowMetaPB rowMeta;
+  final RowBannerCellBuilder cellBuilder;
+  const RowBanner({
+    required this.viewId,
+    required this.rowMeta,
+    required this.cellBuilder,
+    super.key,
+  });
+
+  @override
+  State<RowBanner> createState() => _RowBannerState();
+}
+
+class _RowBannerState extends State<RowBanner> {
+  final _isHovering = ValueNotifier(false);
+  final popoverController = PopoverController();
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocProvider<RowBannerBloc>(
+      create: (context) => RowBannerBloc(
+        viewId: widget.viewId,
+        rowMeta: widget.rowMeta,
+      )..add(const RowBannerEvent.initial()),
+      child: MouseRegion(
+        onEnter: (event) => _isHovering.value = true,
+        onExit: (event) => _isHovering.value = false,
+        child: SizedBox(
+          height: 80,
+          child: Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              SizedBox(
+                height: 30,
+                child: _BannerAction(
+                  isHovering: _isHovering,
+                  popoverController: popoverController,
+                ),
+              ),
+              _BannerTitle(
+                cellBuilder: widget.cellBuilder,
+                popoverController: popoverController,
+              ),
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+}
+
+class _BannerAction extends StatelessWidget {
+  final ValueNotifier<bool> isHovering;
+  final PopoverController popoverController;
+  const _BannerAction({
+    required this.isHovering,
+    required this.popoverController,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return ValueListenableBuilder(
+      valueListenable: isHovering,
+      builder: (BuildContext context, bool value, Widget? child) {
+        if (value) {
+          return BlocBuilder<RowBannerBloc, RowBannerState>(
+            builder: (context, state) {
+              final children = <Widget>[];
+              final rowMeta = state.rowMeta;
+              if (rowMeta.icon.isEmpty) {
+                children.add(
+                  EmojiPickerButton(
+                    showEmojiPicker: () => popoverController.show(),
+                  ),
+                );
+              } else {
+                children.add(
+                  RemoveEmojiButton(
+                    onRemoved: () {
+                      context
+                          .read<RowBannerBloc>()
+                          .add(const RowBannerEvent.setIcon(''));
+                    },
+                  ),
+                );
+              }
+              return Row(
+                mainAxisSize: MainAxisSize.min,
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: children,
+              );
+            },
+          );
+        } else {
+          return const SizedBox(height: _kBannerActionHeight);
+        }
+      },
+    );
+  }
+}
+
+class _BannerTitle extends StatefulWidget {
+  final RowBannerCellBuilder cellBuilder;
+  final PopoverController popoverController;
+  const _BannerTitle({
+    required this.cellBuilder,
+    required this.popoverController,
+  });
+
+  @override
+  State<_BannerTitle> createState() => _BannerTitleState();
+}
+
+class _BannerTitleState extends State<_BannerTitle> {
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<RowBannerBloc, RowBannerState>(
+      builder: (context, state) {
+        final children = <Widget>[];
+
+        if (state.rowMeta.icon.isNotEmpty) {
+          children.add(
+            EmojiButton(
+              emoji: state.rowMeta.icon,
+              showEmojiPicker: () => widget.popoverController.show(),
+            ),
+          );
+        }
+
+        if (state.primaryField != null) {
+          children.add(
+            Expanded(
+              child: widget.cellBuilder(state.primaryField!.id),
+            ),
+          );
+        }
+
+        return AppFlowyPopover(
+          controller: widget.popoverController,
+          triggerActions: PopoverTriggerFlags.none,
+          direction: PopoverDirection.bottomWithLeftAligned,
+          popupBuilder: (popoverContext) => _buildEmojiPicker((emoji) {
+            context
+                .read<RowBannerBloc>()
+                .add(RowBannerEvent.setIcon(emoji.emoji));
+            widget.popoverController.close();
+          }),
+          child: Row(children: children),
+        );
+      },
+    );
+  }
+}
+
+typedef OnSubmittedEmoji = void Function(Emoji emoji);
+const _kBannerActionHeight = 40.0;
+
+class EmojiButton extends StatelessWidget {
+  final String emoji;
+  final VoidCallback showEmojiPicker;
+
+  const EmojiButton({
+    required this.emoji,
+    required this.showEmojiPicker,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return SizedBox(
+      height: _kBannerActionHeight,
+      width: _kBannerActionHeight,
+      child: FlowyButton(
+        margin: const EdgeInsets.all(4),
+        text: FlowyText.medium(
+          emoji,
+          fontSize: 30,
+          textAlign: TextAlign.center,
+        ),
+        onTap: showEmojiPicker,
+      ),
+    );
+  }
+}
+
+class EmojiPickerButton extends StatefulWidget {
+  final VoidCallback showEmojiPicker;
+  const EmojiPickerButton({
+    super.key,
+    required this.showEmojiPicker,
+  });
+
+  @override
+  State<EmojiPickerButton> createState() => _EmojiPickerButtonState();
+}
+
+class _EmojiPickerButtonState extends State<EmojiPickerButton> {
+  @override
+  Widget build(BuildContext context) {
+    return SizedBox(
+      height: 26,
+      width: 160,
+      child: FlowyButton(
+        text: FlowyText.medium(
+          LocaleKeys.document_plugins_cover_addIcon.tr(),
+        ),
+        leftIcon: const Icon(
+          Icons.emoji_emotions,
+          size: 16,
+        ),
+        onTap: widget.showEmojiPicker,
+      ),
+    );
+  }
+}
+
+class RemoveEmojiButton extends StatelessWidget {
+  final VoidCallback onRemoved;
+  RemoveEmojiButton({
+    super.key,
+    required this.onRemoved,
+  });
+
+  final popoverController = PopoverController();
+
+  @override
+  Widget build(BuildContext context) {
+    return SizedBox(
+      height: 26,
+      width: 160,
+      child: FlowyButton(
+        text: FlowyText.medium(
+          LocaleKeys.document_plugins_cover_removeIcon.tr(),
+        ),
+        leftIcon: const Icon(
+          Icons.emoji_emotions,
+          size: 16,
+        ),
+        onTap: onRemoved,
+      ),
+    );
+  }
+}
+
+Widget _buildEmojiPicker(OnSubmittedEmoji onSubmitted) {
+  return SizedBox(
+    height: 250,
+    child: EmojiSelectionMenu(
+      onSubmitted: onSubmitted,
+      onExit: () {},
+    ),
+  );
+}

+ 78 - 375
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart

@@ -1,31 +1,20 @@
+import 'package:appflowy/generated/locale_keys.g.dart';
 import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
-import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
-import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_service.dart';
 import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart';
 import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart';
-import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
-import 'package:appflowy_backend/log.dart';
+import 'package:appflowy/plugins/database_view/widgets/row/row_document.dart';
 import 'package:collection/collection.dart';
-import 'package:flowy_infra/theme_extension.dart';
-import 'package:flowy_infra/image.dart';
+import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 
-import 'package:easy_localization/easy_localization.dart';
-import 'package:appflowy/generated/locale_keys.g.dart';
-import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
-import 'package:appflowy_popover/appflowy_popover.dart';
 
-import '../../grid/presentation/layout/sizes.dart';
-import 'accessory/cell_accessory.dart';
 import 'cell_builder.dart';
-import 'cells/date_cell/date_cell.dart';
-import 'cells/select_option_cell/select_option_cell.dart';
 import 'cells/text_cell/text_cell.dart';
-import 'cells/url_cell/url_cell.dart';
-import '../../grid/presentation/widgets/header/field_cell.dart';
-import '../../grid/presentation/widgets/header/field_editor.dart';
+import 'row_action.dart';
+import 'row_banner.dart';
+import 'row_property.dart';
 
 class RowDetailPage extends StatefulWidget with FlowyOverlayDelegate {
   final RowController rowController;
@@ -46,6 +35,14 @@ class RowDetailPage extends StatefulWidget with FlowyOverlayDelegate {
 }
 
 class _RowDetailPageState extends State<RowDetailPage> {
+  final scrollController = ScrollController();
+
+  @override
+  void dispose() {
+    scrollController.dispose();
+    super.dispose();
+  }
+
   @override
   Widget build(BuildContext context) {
     return FlowyDialog(
@@ -55,43 +52,91 @@ class _RowDetailPageState extends State<RowDetailPage> {
             ..add(const RowDetailEvent.initial());
         },
         child: ListView(
+          controller: scrollController,
           children: [
-            // using ListView here for future expansion:
-            // - header and cover image
-            // - lower rich text area
+            _rowBanner(),
             IntrinsicHeight(child: _responsiveRowInfo()),
             const Divider(height: 1.0),
-            const SizedBox(height: 10),
+            const VSpace(10),
+            RowDocument(
+              viewId: widget.rowController.viewId,
+              rowId: widget.rowController.rowId,
+              scrollController: scrollController,
+            ),
           ],
         ),
       ),
     );
   }
 
+  Widget _rowBanner() {
+    return BlocBuilder<RowDetailBloc, RowDetailState>(
+      builder: (context, state) {
+        final paddingOffset = getHorizontalPadding(context);
+        return Padding(
+          padding: EdgeInsets.only(
+            left: paddingOffset,
+            right: paddingOffset,
+            top: 20,
+          ),
+          child: RowBanner(
+            rowMeta: widget.rowController.rowMeta,
+            viewId: widget.rowController.viewId,
+            cellBuilder: (fieldId) {
+              final fieldInfo = state.cells
+                  .firstWhereOrNull(
+                    (e) => e.fieldInfo.field.id == fieldId,
+                  )
+                  ?.fieldInfo;
+
+              if (fieldInfo != null) {
+                final style = GridTextCellStyle(
+                  placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
+                  textStyle: Theme.of(context).textTheme.titleLarge,
+                  showEmoji: false,
+                  autofocus: true,
+                );
+                final cellContext = DatabaseCellContext(
+                  viewId: widget.rowController.viewId,
+                  rowMeta: widget.rowController.rowMeta,
+                  fieldInfo: fieldInfo,
+                );
+                return widget.cellBuilder.build(cellContext, style: style);
+              } else {
+                return const SizedBox.shrink();
+              }
+            },
+          ),
+        );
+      },
+    );
+  }
+
   Widget _responsiveRowInfo() {
-    final rowDataColumn = _PropertyColumn(
+    final rowDataColumn = RowPropertyList(
       cellBuilder: widget.cellBuilder,
       viewId: widget.rowController.viewId,
     );
-    final rowOptionColumn = _RowOptionColumn(
+    final rowOptionColumn = RowActionList(
       viewId: widget.rowController.viewId,
       rowController: widget.rowController,
     );
+    final paddingOffset = getHorizontalPadding(context);
     if (MediaQuery.of(context).size.width > 800) {
       return Row(
         crossAxisAlignment: CrossAxisAlignment.start,
         children: [
           Flexible(
-            flex: 4,
+            flex: 3,
             child: Padding(
-              padding: const EdgeInsets.fromLTRB(50, 50, 20, 20),
+              padding: EdgeInsets.fromLTRB(paddingOffset, 0, 20, 20),
               child: rowDataColumn,
             ),
           ),
           const VerticalDivider(width: 1.0),
           Flexible(
             child: Padding(
-              padding: const EdgeInsets.fromLTRB(20, 50, 20, 20),
+              padding: EdgeInsets.fromLTRB(20, 0, paddingOffset, 0),
               child: rowOptionColumn,
             ),
           ),
@@ -103,12 +148,12 @@ class _RowDetailPageState extends State<RowDetailPage> {
         mainAxisSize: MainAxisSize.min,
         children: [
           Padding(
-            padding: const EdgeInsets.fromLTRB(20, 50, 20, 20),
+            padding: EdgeInsets.fromLTRB(paddingOffset, 0, 20, 20),
             child: rowDataColumn,
           ),
           const Divider(height: 1.0),
           Padding(
-            padding: const EdgeInsets.all(20),
+            padding: EdgeInsets.symmetric(horizontal: paddingOffset),
             child: rowOptionColumn,
           )
         ],
@@ -117,352 +162,10 @@ class _RowDetailPageState extends State<RowDetailPage> {
   }
 }
 
-class _PropertyColumn extends StatelessWidget {
-  final String viewId;
-  final GridCellBuilder cellBuilder;
-  const _PropertyColumn({
-    required this.viewId,
-    required this.cellBuilder,
-    Key? key,
-  }) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return BlocBuilder<RowDetailBloc, RowDetailState>(
-      buildWhen: (previous, current) => previous.gridCells != current.gridCells,
-      builder: (context, state) {
-        return Column(
-          mainAxisSize: MainAxisSize.min,
-          crossAxisAlignment: CrossAxisAlignment.start,
-          children: [
-            _RowTitle(
-              cellContext: state.gridCells
-                  .firstWhereOrNull((e) => e.fieldInfo.isPrimary),
-              cellBuilder: cellBuilder,
-            ),
-            const VSpace(20),
-            ...state.gridCells
-                .where((element) => !element.fieldInfo.isPrimary)
-                .map(
-                  (cell) => Padding(
-                    padding: const EdgeInsets.only(bottom: 4.0),
-                    child: _PropertyCell(
-                      cellContext: cell,
-                      cellBuilder: cellBuilder,
-                    ),
-                  ),
-                )
-                .toList(),
-            const VSpace(20),
-            _CreatePropertyButton(viewId: viewId),
-          ],
-        );
-      },
-    );
-  }
-}
-
-class _RowTitle extends StatelessWidget {
-  final DatabaseCellContext? cellContext;
-  final GridCellBuilder cellBuilder;
-  const _RowTitle({this.cellContext, required this.cellBuilder, Key? key})
-      : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    if (cellContext == null) {
-      return const SizedBox();
-    }
-    final style = GridTextCellStyle(
-      placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
-      textStyle: Theme.of(context).textTheme.titleLarge,
-      autofocus: true,
-    );
-    return cellBuilder.build(cellContext!, style: style);
-  }
-}
-
-class _CreatePropertyButton extends StatefulWidget {
-  final String viewId;
-
-  const _CreatePropertyButton({
-    required this.viewId,
-    Key? key,
-  }) : super(key: key);
-
-  @override
-  State<_CreatePropertyButton> createState() => _CreatePropertyButtonState();
-}
-
-class _CreatePropertyButtonState extends State<_CreatePropertyButton> {
-  late PopoverController popoverController;
-  late TypeOptionPB typeOption;
-
-  @override
-  void initState() {
-    popoverController = PopoverController();
-    super.initState();
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    return AppFlowyPopover(
-      constraints: BoxConstraints.loose(const Size(240, 200)),
-      controller: popoverController,
-      direction: PopoverDirection.topWithLeftAligned,
-      triggerActions: PopoverTriggerFlags.none,
-      margin: EdgeInsets.zero,
-      child: SizedBox(
-        height: 40,
-        child: FlowyButton(
-          text: FlowyText.medium(
-            LocaleKeys.grid_field_newProperty.tr(),
-            color: AFThemeExtension.of(context).textColor,
-          ),
-          hoverColor: AFThemeExtension.of(context).lightGreyHover,
-          onTap: () async {
-            final result = await TypeOptionBackendService.createFieldTypeOption(
-              viewId: widget.viewId,
-            );
-            result.fold(
-              (l) {
-                typeOption = l;
-                popoverController.show();
-              },
-              (r) => Log.error("Failed to create field type option: $r"),
-            );
-          },
-          leftIcon: svgWidget(
-            "home/add",
-            color: AFThemeExtension.of(context).textColor,
-          ),
-        ),
-      ),
-      popupBuilder: (BuildContext popOverContext) {
-        return FieldEditor(
-          viewId: widget.viewId,
-          typeOptionLoader: FieldTypeOptionLoader(
-            viewId: widget.viewId,
-            field: typeOption.field_2,
-          ),
-          onDeleted: (fieldId) {
-            popoverController.close();
-            NavigatorAlertDialog(
-              title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
-              confirm: () {
-                context
-                    .read<RowDetailBloc>()
-                    .add(RowDetailEvent.deleteField(fieldId));
-              },
-            ).show(context);
-          },
-        );
-      },
-    );
-  }
-}
-
-class _PropertyCell extends StatefulWidget {
-  final DatabaseCellContext cellContext;
-  final GridCellBuilder cellBuilder;
-  const _PropertyCell({
-    required this.cellContext,
-    required this.cellBuilder,
-    Key? key,
-  }) : super(key: key);
-
-  @override
-  State<StatefulWidget> createState() => _PropertyCellState();
-}
-
-class _PropertyCellState extends State<_PropertyCell> {
-  final PopoverController popover = PopoverController();
-
-  @override
-  Widget build(BuildContext context) {
-    final style = _customCellStyle(widget.cellContext.fieldType);
-    final cell = widget.cellBuilder.build(widget.cellContext, style: style);
-
-    final gesture = GestureDetector(
-      behavior: HitTestBehavior.translucent,
-      onTap: () => cell.beginFocus.notify(),
-      child: AccessoryHover(
-        contentPadding: const EdgeInsets.symmetric(horizontal: 3, vertical: 3),
-        child: cell,
-      ),
-    );
-
-    return IntrinsicHeight(
-      child: ConstrainedBox(
-        constraints: const BoxConstraints(minHeight: 30),
-        child: Row(
-          crossAxisAlignment: CrossAxisAlignment.stretch,
-          mainAxisAlignment: MainAxisAlignment.center,
-          children: [
-            AppFlowyPopover(
-              controller: popover,
-              constraints: BoxConstraints.loose(const Size(240, 600)),
-              margin: EdgeInsets.zero,
-              triggerActions: PopoverTriggerFlags.none,
-              popupBuilder: (popoverContext) => buildFieldEditor(),
-              child: SizedBox(
-                width: 150,
-                child: FieldCellButton(
-                  field: widget.cellContext.fieldInfo.field,
-                  onTap: () => popover.show(),
-                  radius: BorderRadius.circular(6),
-                ),
-              ),
-            ),
-            const HSpace(10),
-            Expanded(child: gesture),
-          ],
-        ),
-      ),
-    );
-  }
-
-  Widget buildFieldEditor() {
-    return FieldEditor(
-      viewId: widget.cellContext.viewId,
-      isGroupingField: widget.cellContext.fieldInfo.isGroupField,
-      typeOptionLoader: FieldTypeOptionLoader(
-        viewId: widget.cellContext.viewId,
-        field: widget.cellContext.fieldInfo.field,
-      ),
-      onHidden: (fieldId) {
-        popover.close();
-        context.read<RowDetailBloc>().add(RowDetailEvent.hideField(fieldId));
-      },
-      onDeleted: (fieldId) {
-        popover.close();
-
-        NavigatorAlertDialog(
-          title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
-          confirm: () {
-            context
-                .read<RowDetailBloc>()
-                .add(RowDetailEvent.deleteField(fieldId));
-          },
-        ).show(context);
-      },
-    );
-  }
-}
-
-GridCellStyle? _customCellStyle(FieldType fieldType) {
-  switch (fieldType) {
-    case FieldType.Checkbox:
-      return null;
-    case FieldType.DateTime:
-    case FieldType.LastEditedTime:
-    case FieldType.CreatedTime:
-      return DateCellStyle(
-        alignment: Alignment.centerLeft,
-      );
-    case FieldType.MultiSelect:
-      return SelectOptionCellStyle(
-        placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
-      );
-    case FieldType.Checklist:
-      return SelectOptionCellStyle(
-        placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
-      );
-    case FieldType.Number:
-      return null;
-    case FieldType.RichText:
-      return GridTextCellStyle(
-        placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
-      );
-    case FieldType.SingleSelect:
-      return SelectOptionCellStyle(
-        placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
-      );
-
-    case FieldType.URL:
-      return GridURLCellStyle(
-        placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
-        accessoryTypes: [
-          GridURLCellAccessoryType.copyURL,
-          GridURLCellAccessoryType.visitURL,
-        ],
-      );
-  }
-  throw UnimplementedError;
-}
-
-class _RowOptionColumn extends StatelessWidget {
-  final RowController rowController;
-  const _RowOptionColumn({
-    required String viewId,
-    required this.rowController,
-    Key? key,
-  }) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return Column(
-      crossAxisAlignment: CrossAxisAlignment.start,
-      mainAxisSize: MainAxisSize.min,
-      children: [
-        Padding(
-          padding: const EdgeInsets.only(left: 10),
-          child: FlowyText(LocaleKeys.grid_row_action.tr()),
-        ),
-        const VSpace(15),
-        _DeleteButton(rowId: rowController.rowId),
-        _DuplicateButton(
-          rowId: rowController.rowId,
-          groupId: rowController.groupId,
-        ),
-      ],
-    );
-  }
-}
-
-class _DeleteButton extends StatelessWidget {
-  final String rowId;
-  const _DeleteButton({required this.rowId, Key? key}) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return SizedBox(
-      height: GridSize.popoverItemHeight,
-      child: FlowyButton(
-        text: FlowyText.regular(LocaleKeys.grid_row_delete.tr()),
-        leftIcon: const FlowySvg(name: "home/trash"),
-        onTap: () {
-          context.read<RowDetailBloc>().add(RowDetailEvent.deleteRow(rowId));
-          FlowyOverlay.pop(context);
-        },
-      ),
-    );
-  }
-}
-
-class _DuplicateButton extends StatelessWidget {
-  final String rowId;
-  final String? groupId;
-  const _DuplicateButton({
-    required this.rowId,
-    this.groupId,
-    Key? key,
-  }) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return SizedBox(
-      height: GridSize.popoverItemHeight,
-      child: FlowyButton(
-        text: FlowyText.regular(LocaleKeys.grid_row_duplicate.tr()),
-        leftIcon: const FlowySvg(name: "grid/duplicate"),
-        onTap: () {
-          context
-              .read<RowDetailBloc>()
-              .add(RowDetailEvent.duplicateRow(rowId, groupId));
-          FlowyOverlay.pop(context);
-        },
-      ),
-    );
+double getHorizontalPadding(BuildContext context) {
+  if (MediaQuery.of(context).size.width > 800) {
+    return 50;
+  } else {
+    return 20;
   }
 }

+ 120 - 0
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_document.dart

@@ -0,0 +1,120 @@
+import 'package:appflowy/plugins/database_view/grid/application/row/row_document_bloc.dart';
+import 'package:appflowy/plugins/document/application/doc_bloc.dart';
+import 'package:appflowy/plugins/document/presentation/editor_page.dart';
+import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
+import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
+import 'package:flowy_infra_ui/widget/error_page.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class RowDocument extends StatelessWidget {
+  const RowDocument({
+    super.key,
+    required this.viewId,
+    required this.rowId,
+    required this.scrollController,
+  });
+
+  final String viewId;
+  final String rowId;
+  final ScrollController scrollController;
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocProvider<RowDocumentBloc>(
+      create: (context) => RowDocumentBloc(
+        viewId: viewId,
+        rowId: rowId,
+      )..add(
+          const RowDocumentEvent.initial(),
+        ),
+      child: BlocBuilder<RowDocumentBloc, RowDocumentState>(
+        builder: (context, state) {
+          return state.loadingState.when(
+            loading: () => const Center(
+              child: CircularProgressIndicator.adaptive(),
+            ),
+            error: (error) => FlowyErrorPage(
+              error.toString(),
+            ),
+            finish: () => RowEditor(
+              viewPB: state.viewPB!,
+              scrollController: scrollController,
+            ),
+          );
+        },
+      ),
+    );
+  }
+}
+
+class RowEditor extends StatefulWidget {
+  const RowEditor({
+    super.key,
+    required this.viewPB,
+    required this.scrollController,
+  });
+
+  final ViewPB viewPB;
+  final ScrollController scrollController;
+
+  @override
+  State<RowEditor> createState() => _RowEditorState();
+}
+
+class _RowEditorState extends State<RowEditor> {
+  late final DocumentBloc documentBloc;
+
+  @override
+  void initState() {
+    super.initState();
+    documentBloc = DocumentBloc(view: widget.viewPB)
+      ..add(const DocumentEvent.initial());
+  }
+
+  @override
+  dispose() {
+    documentBloc.close();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return MultiBlocProvider(
+      providers: [
+        BlocProvider(create: (_) => DocumentAppearanceCubit()),
+        BlocProvider.value(value: documentBloc),
+      ],
+      child: BlocBuilder<DocumentBloc, DocumentState>(
+        builder: (context, state) {
+          return state.loadingState.when(
+            loading: () => const Center(
+              child: CircularProgressIndicator.adaptive(),
+            ),
+            finish: (result) {
+              return result.fold(
+                (error) => FlowyErrorPage(
+                  error.toString(),
+                ),
+                (_) {
+                  final editorState = documentBloc.editorState;
+                  if (editorState == null) {
+                    return const SizedBox.shrink();
+                  }
+                  return IntrinsicHeight(
+                    child: AppFlowyEditorPage(
+                      shrinkWrap: true,
+                      autoFocus: false,
+                      editorState: editorState,
+                      scrollController: widget.scrollController,
+                    ),
+                  );
+                },
+              );
+            },
+          );
+        },
+      ),
+    );
+  }
+}

+ 192 - 0
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_property.dart

@@ -0,0 +1,192 @@
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
+import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
+import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart';
+import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_cell.dart';
+import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.dart';
+import 'package:appflowy/plugins/database_view/widgets/row/row_action.dart';
+import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
+import 'package:appflowy_popover/appflowy_popover.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'accessory/cell_accessory.dart';
+import 'cell_builder.dart';
+import 'cells/date_cell/date_cell.dart';
+import 'cells/select_option_cell/select_option_cell.dart';
+import 'cells/text_cell/text_cell.dart';
+import 'cells/url_cell/url_cell.dart';
+
+/// Display the row properties in a list. Only use this widget in the
+/// [RowDetailPage].
+///
+class RowPropertyList extends StatelessWidget {
+  final String viewId;
+  final GridCellBuilder cellBuilder;
+  const RowPropertyList({
+    required this.viewId,
+    required this.cellBuilder,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<RowDetailBloc, RowDetailState>(
+      buildWhen: (previous, current) => previous.cells != current.cells,
+      builder: (context, state) {
+        return Column(
+          mainAxisSize: MainAxisSize.min,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            // The rest of the fields are displayed in the order of the field
+            // list
+            ...state.cells
+                .where((element) => !element.fieldInfo.isPrimary)
+                .map(
+                  (cell) => _PropertyCell(
+                    cellContext: cell,
+                    cellBuilder: cellBuilder,
+                  ),
+                )
+                .toList(),
+            const VSpace(20),
+
+            // Create a new property(field) button
+            CreateRowFieldButton(viewId: viewId),
+          ],
+        );
+      },
+    );
+  }
+}
+
+class _PropertyCell extends StatefulWidget {
+  final DatabaseCellContext cellContext;
+  final GridCellBuilder cellBuilder;
+  const _PropertyCell({
+    required this.cellContext,
+    required this.cellBuilder,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _PropertyCellState();
+}
+
+class _PropertyCellState extends State<_PropertyCell> {
+  final PopoverController popover = PopoverController();
+
+  @override
+  Widget build(BuildContext context) {
+    final style = _customCellStyle(widget.cellContext.fieldType);
+    final cell = widget.cellBuilder.build(widget.cellContext, style: style);
+
+    final gesture = GestureDetector(
+      behavior: HitTestBehavior.translucent,
+      onTap: () => cell.beginFocus.notify(),
+      child: AccessoryHover(
+        contentPadding: const EdgeInsets.symmetric(horizontal: 3, vertical: 3),
+        child: cell,
+      ),
+    );
+
+    return IntrinsicHeight(
+      child: ConstrainedBox(
+        constraints: const BoxConstraints(minHeight: 30),
+        child: Row(
+          crossAxisAlignment: CrossAxisAlignment.stretch,
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: [
+            AppFlowyPopover(
+              controller: popover,
+              constraints: BoxConstraints.loose(const Size(240, 600)),
+              margin: EdgeInsets.zero,
+              triggerActions: PopoverTriggerFlags.none,
+              popupBuilder: (popoverContext) => buildFieldEditor(),
+              child: SizedBox(
+                width: 150,
+                child: FieldCellButton(
+                  field: widget.cellContext.fieldInfo.field,
+                  onTap: () => popover.show(),
+                  radius: BorderRadius.circular(6),
+                ),
+              ),
+            ),
+            Expanded(child: gesture),
+          ],
+        ),
+      ),
+    );
+  }
+
+  Widget buildFieldEditor() {
+    return FieldEditor(
+      viewId: widget.cellContext.viewId,
+      isGroupingField: widget.cellContext.fieldInfo.isGroupField,
+      typeOptionLoader: FieldTypeOptionLoader(
+        viewId: widget.cellContext.viewId,
+        field: widget.cellContext.fieldInfo.field,
+      ),
+      onHidden: (fieldId) {
+        popover.close();
+        context.read<RowDetailBloc>().add(RowDetailEvent.hideField(fieldId));
+      },
+      onDeleted: (fieldId) {
+        popover.close();
+
+        NavigatorAlertDialog(
+          title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
+          confirm: () {
+            context
+                .read<RowDetailBloc>()
+                .add(RowDetailEvent.deleteField(fieldId));
+          },
+        ).show(context);
+      },
+    );
+  }
+}
+
+GridCellStyle? _customCellStyle(FieldType fieldType) {
+  switch (fieldType) {
+    case FieldType.Checkbox:
+      return null;
+    case FieldType.DateTime:
+    case FieldType.LastEditedTime:
+    case FieldType.CreatedTime:
+      return DateCellStyle(
+        alignment: Alignment.centerLeft,
+      );
+    case FieldType.MultiSelect:
+      return SelectOptionCellStyle(
+        placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
+      );
+    case FieldType.Checklist:
+      return SelectOptionCellStyle(
+        placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
+      );
+    case FieldType.Number:
+      return null;
+    case FieldType.RichText:
+      return GridTextCellStyle(
+        placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
+      );
+    case FieldType.SingleSelect:
+      return SelectOptionCellStyle(
+        placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
+      );
+
+    case FieldType.URL:
+      return GridURLCellStyle(
+        placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
+        accessoryTypes: [
+          GridURLCellAccessoryType.copyURL,
+          GridURLCellAccessoryType.visitURL,
+        ],
+      );
+  }
+  throw UnimplementedError;
+}

+ 3 - 3
frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/database_setting.dart

@@ -23,7 +23,7 @@ class DatabaseSettingList extends StatelessWidget {
   Widget build(BuildContext context) {
     final cells = actionsForDatabaseLayout(databaseContoller.databaseLayout)
         .map((action) {
-      return _SettingItem(
+      return DatabaseSettingItem(
         action: action,
         onAction: (action) => onAction(action, databaseContoller),
       );
@@ -44,11 +44,11 @@ class DatabaseSettingList extends StatelessWidget {
   }
 }
 
-class _SettingItem extends StatelessWidget {
+class DatabaseSettingItem extends StatelessWidget {
   final DatabaseSettingAction action;
   final Function(DatabaseSettingAction) onAction;
 
-  const _SettingItem({
+  const DatabaseSettingItem({
     required this.action,
     required this.onAction,
     Key? key,

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart

@@ -23,7 +23,7 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
   DocumentBloc({
     required this.view,
   })  : _documentListener = DocumentListener(id: view.id),
-        _viewListener = ViewListener(view: view),
+        _viewListener = ViewListener(viewId: view.id),
         _documentService = DocumentService(),
         _trashService = TrashService(),
         super(DocumentState.initial()) {

+ 5 - 1
frontend/appflowy_flutter/lib/plugins/document/document.dart

@@ -48,8 +48,12 @@ class DocumentPlugin extends Plugin<int> {
   DocumentPlugin({
     required PluginType pluginType,
     required ViewPB view,
+    bool listenOnViewChanged = false,
     Key? key,
-  }) : notifier = ViewPluginNotifier(view: view) {
+  }) : notifier = ViewPluginNotifier(
+          view: view,
+          listenOnViewChanged: listenOnViewChanged,
+        ) {
     _pluginType = pluginType;
     _documentAppearanceCubit.fetch();
   }

+ 26 - 5
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart

@@ -14,17 +14,23 @@ class AppFlowyEditorPage extends StatefulWidget {
     super.key,
     required this.editorState,
     this.header,
+    this.shrinkWrap = false,
+    this.scrollController,
+    this.autoFocus,
   });
 
-  final EditorState editorState;
   final Widget? header;
+  final EditorState editorState;
+  final ScrollController? scrollController;
+  final bool shrinkWrap;
+  final bool? autoFocus;
 
   @override
   State<AppFlowyEditorPage> createState() => _AppFlowyEditorPageState();
 }
 
 class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
-  final scrollController = ScrollController();
+  late final ScrollController effectiveScrollController;
 
   final List<CommandShortcutEvent> commandShortcutEvents = [
     ...codeBlockCommands,
@@ -90,6 +96,20 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
       );
   DocumentBloc get documentBloc => context.read<DocumentBloc>();
 
+  @override
+  void initState() {
+    super.initState();
+    effectiveScrollController = widget.scrollController ?? ScrollController();
+  }
+
+  @override
+  void dispose() {
+    if (widget.scrollController == null) {
+      effectiveScrollController.dispose();
+    }
+    super.dispose();
+  }
+
   @override
   Widget build(BuildContext context) {
     final (bool autoFocus, Selection? selection) =
@@ -98,9 +118,10 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
     final editor = AppFlowyEditor.custom(
       editorState: widget.editorState,
       editable: true,
-      scrollController: scrollController,
+      shrinkWrap: widget.shrinkWrap,
+      scrollController: effectiveScrollController,
       // setup the auto focus parameters
-      autoFocus: autoFocus,
+      autoFocus: widget.autoFocus ?? autoFocus,
       focusedSelection: selection,
       // setup the theme
       editorStyle: styleCustomizer.style(),
@@ -122,7 +143,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
           style: styleCustomizer.floatingToolbarStyleBuilder(),
           items: toolbarItems,
           editorState: widget.editorState,
-          scrollController: scrollController,
+          scrollController: effectiveScrollController,
           child: editor,
         ),
       ),

+ 1 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart

@@ -61,6 +61,7 @@ extension InsertDatabase on EditorState {
     ).then((value) => value.swap().toOption().toNullable());
 
     // TODO(a-wallen): Show error dialog here.
+    // Maybe extend the FlowyErrorPage.
     if (ref == null) {
       return;
     }

+ 2 - 2
frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_button.dart

@@ -84,7 +84,7 @@ class ShareActionList extends StatefulWidget {
 @visibleForTesting
 class ShareActionListState extends State<ShareActionList> {
   late String name;
-  late final ViewListener viewListener = ViewListener(view: widget.view);
+  late final ViewListener viewListener = ViewListener(viewId: widget.view.id);
 
   @override
   void initState() {
@@ -134,7 +134,7 @@ class ShareActionListState extends State<ShareActionList> {
     name = widget.view.name;
     viewListener.start(
       onViewUpdated: (view) {
-        name = view.fold((l) => l.name, (r) => '');
+        name = view.name;
       },
     );
   }

+ 27 - 21
frontend/appflowy_flutter/lib/plugins/util.dart

@@ -1,10 +1,14 @@
 import 'package:appflowy/startup/plugin/plugin.dart';
+import 'package:appflowy/startup/startup.dart';
+import 'package:appflowy/workspace/application/view/view_ext.dart';
 import 'package:appflowy/workspace/application/view/view_listener.dart';
 import 'package:dartz/dartz.dart';
 import 'package:appflowy_backend/log.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
 import 'package:flutter/material.dart';
 
+import '../workspace/presentation/home/home_stack.dart';
+
 class ViewPluginNotifier extends PluginNotifier<Option<DeletedViewPB>> {
   final ViewListener? _viewListener;
   ViewPB view;
@@ -12,35 +16,37 @@ class ViewPluginNotifier extends PluginNotifier<Option<DeletedViewPB>> {
   @override
   final ValueNotifier<Option<DeletedViewPB>> isDeleted = ValueNotifier(none());
 
-  @override
-  final ValueNotifier<int> isDisplayChanged = ValueNotifier(0);
-
   ViewPluginNotifier({
     required this.view,
-  }) : _viewListener = ViewListener(view: view) {
-    _viewListener?.start(
-      onViewUpdated: (result) {
-        result.fold(
-          (updatedView) {
+    required bool listenOnViewChanged,
+  }) : _viewListener = ViewListener(viewId: view.id) {
+    if (listenOnViewChanged) {
+      _viewListener?.start(
+        onViewUpdated: (updatedView) {
+          // If the layout is changed, we need to create a new plugin for it.
+          if (view.layout != updatedView.layout) {
+            getIt<HomeStackManager>().setPlugin(
+              updatedView.plugin(
+                listenOnViewChanged: listenOnViewChanged,
+              ),
+            );
+          } else {
             view = updatedView;
-            isDisplayChanged.value = updatedView.hashCode;
-          },
-          (err) => Log.error(err),
-        );
-      },
-      onViewMoveToTrash: (result) {
-        result.fold(
-          (deletedView) => isDeleted.value = some(deletedView),
-          (err) => Log.error(err),
-        );
-      },
-    );
+          }
+        },
+        onViewMoveToTrash: (result) {
+          result.fold(
+            (deletedView) => isDeleted.value = some(deletedView),
+            (err) => Log.error(err),
+          );
+        },
+      );
+    }
   }
 
   @override
   void dispose() {
     isDeleted.dispose();
-    isDisplayChanged.dispose();
     _viewListener?.stop();
   }
 }

+ 0 - 5
frontend/appflowy_flutter/lib/startup/deps_resolver.dart

@@ -122,11 +122,6 @@ void _resolveFolderDeps(GetIt getIt) {
         WorkspaceListener(user: user, workspaceId: workspaceId),
   );
 
-  // ViewPB
-  getIt.registerFactoryParam<ViewListener, ViewPB, void>(
-    (view, _) => ViewListener(view: view),
-  );
-
   getIt.registerFactoryParam<ViewBloc, ViewPB, void>(
     (view, _) => ViewBloc(
       view: view,

+ 0 - 3
frontend/appflowy_flutter/lib/startup/plugin/plugin.dart

@@ -37,9 +37,6 @@ abstract class PluginNotifier<T> {
   /// Notify if the plugin get deleted
   ValueNotifier<T> get isDeleted;
 
-  /// Notify if the [PluginWidgetBuilder]'s content was changed
-  ValueNotifier<int> get isDisplayChanged;
-
   void dispose() {}
 }
 

+ 2 - 2
frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart

@@ -16,14 +16,14 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
   ViewBloc({
     required this.view,
   })  : viewBackendSvc = ViewBackendService(),
-        listener = ViewListener(view: view),
+        listener = ViewListener(viewId: view.id),
         super(ViewState.init(view)) {
     on<ViewEvent>((event, emit) async {
       await event.map(
         initial: (e) {
           listener.start(
             onViewUpdated: (result) {
-              add(ViewEvent.viewDidUpdate(result));
+              add(ViewEvent.viewDidUpdate(left(result)));
             },
           );
           emit(state);

+ 25 - 4
frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart

@@ -1,4 +1,7 @@
-import 'package:appflowy/plugins/database_view/database_view.dart';
+import 'package:appflowy/plugins/database_view/board/board.dart';
+import 'package:appflowy/plugins/database_view/calendar/calendar.dart';
+import 'package:appflowy/plugins/database_view/grid/grid.dart';
+import 'package:appflowy/plugins/document/document.dart';
 import 'package:appflowy/startup/plugin/plugin.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
@@ -56,14 +59,32 @@ extension ViewExtension on ViewPB {
     throw UnimplementedError;
   }
 
-  Plugin plugin() {
+  Plugin plugin({bool listenOnViewChanged = false}) {
     switch (layout) {
       case ViewLayoutPB.Board:
+        return BoardPlugin(
+          view: this,
+          pluginType: pluginType,
+          listenOnViewChanged: listenOnViewChanged,
+        );
       case ViewLayoutPB.Calendar:
+        return CalendarPlugin(
+          view: this,
+          pluginType: pluginType,
+          listenOnViewChanged: listenOnViewChanged,
+        );
       case ViewLayoutPB.Grid:
-        return DatabaseViewPlugin(view: this);
+        return GridPlugin(
+          view: this,
+          pluginType: pluginType,
+          listenOnViewChanged: listenOnViewChanged,
+        );
       case ViewLayoutPB.Document:
-        return makePlugin(pluginType: pluginType, data: this);
+        return DocumentPlugin(
+          view: this,
+          pluginType: pluginType,
+          listenOnViewChanged: listenOnViewChanged,
+        );
     }
     throw UnimplementedError;
   }

+ 31 - 43
frontend/appflowy_flutter/lib/workspace/application/view/view_listener.dart

@@ -1,18 +1,18 @@
 import 'dart:async';
 import 'dart:typed_data';
 import 'package:appflowy/core/notification/folder_notification.dart';
+import 'package:appflowy_backend/log.dart';
 import 'package:dartz/dartz.dart';
 import 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/notification.pb.dart';
 import 'package:appflowy_backend/rust_stream.dart';
-import 'package:flowy_infra/notifier.dart';
 
 // Delete the view from trash, which means the view was deleted permanently
 typedef DeleteViewNotifyValue = Either<ViewPB, FlowyError>;
 // The view get updated
-typedef UpdateViewNotifiedValue = Either<ViewPB, FlowyError>;
+typedef UpdateViewNotifiedValue = ViewPB;
 // Restore the view from trash
 typedef RestoreViewNotifiedValue = Either<ViewPB, FlowyError>;
 // Move the view to trash
@@ -20,15 +20,17 @@ typedef MoveToTrashNotifiedValue = Either<DeletedViewPB, FlowyError>;
 
 class ViewListener {
   StreamSubscription<SubscribeObject>? _subscription;
-  final _updatedViewNotifier = PublishNotifier<UpdateViewNotifiedValue>();
-  final _deletedNotifier = PublishNotifier<DeleteViewNotifyValue>();
-  final _restoredNotifier = PublishNotifier<RestoreViewNotifiedValue>();
-  final _moveToTrashNotifier = PublishNotifier<MoveToTrashNotifiedValue>();
+  void Function(UpdateViewNotifiedValue)? _updatedViewNotifier;
+  void Function(DeleteViewNotifyValue)? _deletedNotifier;
+  void Function(RestoreViewNotifiedValue)? _restoredNotifier;
+  void Function(MoveToTrashNotifiedValue)? _moveToTrashNotifier;
+  bool _isDisposed = false;
+
   FolderNotificationParser? _parser;
-  ViewPB view;
+  final String viewId;
 
   ViewListener({
-    required this.view,
+    required this.viewId,
   });
 
   void start({
@@ -37,32 +39,18 @@ class ViewListener {
     void Function(RestoreViewNotifiedValue)? onViewRestored,
     void Function(MoveToTrashNotifiedValue)? onViewMoveToTrash,
   }) {
-    if (onViewUpdated != null) {
-      _updatedViewNotifier.addListener(() {
-        onViewUpdated(_updatedViewNotifier.currentValue!);
-      });
-    }
-
-    if (onViewDeleted != null) {
-      _deletedNotifier.addListener(() {
-        onViewDeleted(_deletedNotifier.currentValue!);
-      });
+    if (_isDisposed) {
+      Log.warn("ViewListener is already disposed");
+      return;
     }
 
-    if (onViewRestored != null) {
-      _restoredNotifier.addListener(() {
-        onViewRestored(_restoredNotifier.currentValue!);
-      });
-    }
-
-    if (onViewMoveToTrash != null) {
-      _moveToTrashNotifier.addListener(() {
-        onViewMoveToTrash(_moveToTrashNotifier.currentValue!);
-      });
-    }
+    _updatedViewNotifier = onViewUpdated;
+    _deletedNotifier = onViewDeleted;
+    _restoredNotifier = onViewRestored;
+    _moveToTrashNotifier = onViewMoveToTrash;
 
     _parser = FolderNotificationParser(
-      id: view.id,
+      id: viewId,
       callback: (ty, result) {
         _handleObservableType(ty, result);
       },
@@ -81,30 +69,29 @@ class ViewListener {
         result.fold(
           (payload) {
             final view = ViewPB.fromBuffer(payload);
-            _updatedViewNotifier.value = left(view);
+            _updatedViewNotifier?.call(view);
           },
-          (error) => _updatedViewNotifier.value = right(error),
+          (error) => Log.error(error),
         );
         break;
       case FolderNotification.DidDeleteView:
         result.fold(
-          (payload) =>
-              _deletedNotifier.value = left(ViewPB.fromBuffer(payload)),
-          (error) => _deletedNotifier.value = right(error),
+          (payload) => _deletedNotifier?.call(left(ViewPB.fromBuffer(payload))),
+          (error) => _deletedNotifier?.call(right(error)),
         );
         break;
       case FolderNotification.DidRestoreView:
         result.fold(
           (payload) =>
-              _restoredNotifier.value = left(ViewPB.fromBuffer(payload)),
-          (error) => _restoredNotifier.value = right(error),
+              _restoredNotifier?.call(left(ViewPB.fromBuffer(payload))),
+          (error) => _restoredNotifier?.call(right(error)),
         );
         break;
       case FolderNotification.DidMoveViewToTrash:
         result.fold(
-          (payload) => _moveToTrashNotifier.value =
-              left(DeletedViewPB.fromBuffer(payload)),
-          (error) => _moveToTrashNotifier.value = right(error),
+          (payload) => _moveToTrashNotifier
+              ?.call(left(DeletedViewPB.fromBuffer(payload))),
+          (error) => _moveToTrashNotifier?.call(right(error)),
         );
         break;
       default:
@@ -113,10 +100,11 @@ class ViewListener {
   }
 
   Future<void> stop() async {
+    _isDisposed = true;
     _parser = null;
     await _subscription?.cancel();
-    _updatedViewNotifier.dispose();
-    _deletedNotifier.dispose();
-    _restoredNotifier.dispose();
+    _updatedViewNotifier = null;
+    _deletedNotifier = null;
+    _restoredNotifier = null;
   }
 }

+ 35 - 1
frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart

@@ -50,6 +50,29 @@ class ViewBackendService {
     return FolderEventCreateView(payload).send();
   }
 
+  /// The orphan view is meant to be a view that is not attached to any parent view. By default, this
+  /// view will not be shown in the view list unless it is attached to a parent view that is shown in
+  /// the view list.
+  static Future<Either<ViewPB, FlowyError>> createOrphanView({
+    required String viewId,
+    required ViewLayoutPB layoutType,
+    required String name,
+    String? desc,
+
+    /// The initial data should be a JSON that represent the DocumentDataPB.
+    /// Currently, only support create document with initial data.
+    List<int>? initialDataBytes,
+  }) {
+    final payload = CreateOrphanViewPayloadPB.create()
+      ..viewId = viewId
+      ..name = name
+      ..desc = desc ?? ""
+      ..layout = layoutType
+      ..initialData = initialDataBytes ?? [];
+
+    return FolderEventCreateOrphanView(payload).send();
+  }
+
   static Future<Either<ViewPB, FlowyError>> createDatabaseReferenceView({
     required String parentViewId,
     required String databaseId,
@@ -98,12 +121,23 @@ class ViewBackendService {
   static Future<Either<ViewPB, FlowyError>> updateView({
     required String viewId,
     String? name,
+    String? iconURL,
+    String? coverURL,
   }) {
     final payload = UpdateViewPayloadPB.create()..viewId = viewId;
 
     if (name != null) {
       payload.name = name;
     }
+
+    if (iconURL != null) {
+      payload.iconUrl = iconURL;
+    }
+
+    if (coverURL != null) {
+      payload.coverUrl = coverURL;
+    }
+
     return FolderEventUpdateView(payload).send();
   }
 
@@ -144,7 +178,7 @@ class ViewBackendService {
     });
   }
 
-  Future<Either<ViewPB, FlowyError>> getView(
+  static Future<Either<ViewPB, FlowyError>> getView(
     String viewID,
   ) async {
     final payload = ViewIdPB.create()..value = viewID;

+ 5 - 9
frontend/appflowy_flutter/lib/workspace/presentation/home/home_screen.dart

@@ -78,11 +78,9 @@ class _HomeScreenState extends State<HomeScreen> {
                     // All opened widgets that display on the home screen are in the form of plugins. There is a list of built-in plugins defined in the [PluginType] enum, including board, grid and trash.
                     if (getIt<HomeStackManager>().plugin.pluginType ==
                         PluginType.blank) {
-                      final plugin = makePlugin(
-                        pluginType: view.pluginType,
-                        data: view,
+                      getIt<HomeStackManager>().setPlugin(
+                        view.plugin(listenOnViewChanged: true),
                       );
-                      getIt<HomeStackManager>().setPlugin(plugin);
                       getIt<MenuSharedState>().latestOpenView = view;
                     }
                   }
@@ -282,12 +280,10 @@ class HomeScreenStackAdaptor extends HomeStackDelegate {
               lastView = views[index - 1];
             }
 
-            final plugin = makePlugin(
-              pluginType: lastView.pluginType,
-              data: lastView,
-            );
             getIt<MenuSharedState>().latestOpenView = lastView;
-            getIt<HomeStackManager>().setPlugin(plugin);
+            getIt<HomeStackManager>().setPlugin(
+              lastView.plugin(listenOnViewChanged: true),
+            );
           } else {
             getIt<MenuSharedState>().latestOpenView = null;
             getIt<HomeStackManager>().setPlugin(BlankPagePlugin());

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

@@ -121,14 +121,12 @@ class HomeStackNotifier extends ChangeNotifier {
   /// This is the only place where the plugin is set.
   /// No need compare the old plugin with the new plugin. Just set it.
   set plugin(Plugin newPlugin) {
-    _plugin.notifier?.isDisplayChanged.addListener(notifyListeners);
     _plugin.dispose();
 
     /// Set the plugin view as the latest view.
     FolderEventSetLatestView(ViewIdPB(value: newPlugin.id)).send();
 
     _plugin = newPlugin;
-    _plugin.notifier?.isDisplayChanged.removeListener(notifyListeners);
     notifyListeners();
   }
 

+ 3 - 1
frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/section/section.dart

@@ -27,7 +27,9 @@ class ViewSection extends StatelessWidget {
         listener: (context, state) {
           if (state.selectedView != null) {
             WidgetsBinding.instance.addPostFrameCallback((_) {
-              getIt<HomeStackManager>().setPlugin(state.selectedView!.plugin());
+              getIt<HomeStackManager>().setPlugin(
+                state.selectedView!.plugin(listenOnViewChanged: true),
+              );
             });
           }
         },

+ 8 - 14
frontend/appflowy_flutter/lib/workspace/presentation/widgets/left_bar_item.dart

@@ -1,6 +1,5 @@
 import 'package:appflowy/workspace/application/view/view_listener.dart';
 import 'package:appflowy/workspace/application/view/view_service.dart';
-import 'package:appflowy_backend/log.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
 import 'package:flutter/material.dart';
 
@@ -25,20 +24,15 @@ class _ViewLeftBarItemState extends State<ViewLeftBarItem> {
     super.initState();
     view = widget.view;
     _focusNode.addListener(_handleFocusChanged);
-    _viewListener = ViewListener(view: widget.view);
+    _viewListener = ViewListener(viewId: widget.view.id);
     _viewListener.start(
-      onViewUpdated: (result) {
-        result.fold(
-          (updatedView) {
-            if (mounted) {
-              setState(() {
-                view = updatedView;
-                _controller.text = view.name;
-              });
-            }
-          },
-          (err) => Log.error(err),
-        );
+      onViewUpdated: (updatedView) {
+        if (mounted) {
+          setState(() {
+            view = updatedView;
+            _controller.text = view.name;
+          });
+        }
       },
     );
     _controller.text = view.name;

+ 3 - 3
frontend/appflowy_flutter/pubspec.lock

@@ -53,11 +53,11 @@ packages:
     dependency: "direct main"
     description:
       path: "."
-      ref: "7fe5bb8"
-      resolved-ref: "7fe5bb85d455416ddbce4bbf2afed1c434466eeb"
+      ref: "23bc6d2"
+      resolved-ref: "23bc6d2f58ab7ab4ff21c507d53753de35094ec0"
       url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
     source: git
-    version: "1.0.0"
+    version: "1.0.2"
   appflowy_popover:
     dependency: "direct main"
     description:

+ 2 - 2
frontend/appflowy_flutter/pubspec.yaml

@@ -42,11 +42,11 @@ dependencies:
     git:
       url: https://github.com/AppFlowy-IO/appflowy-board.git
       ref: a183c57
-  # appflowy_editor: ^1.0.0
+  # appflowy_editor: ^1.0.2
   appflowy_editor:
     git:
       url: https://github.com/AppFlowy-IO/appflowy-editor.git
-      ref: 7fe5bb8
+      ref: 23bc6d2
   appflowy_popover:
     path: packages/appflowy_popover
 

+ 3 - 2
frontend/appflowy_flutter/test/bloc_test/board_test/util.dart

@@ -106,13 +106,14 @@ class BoardTestContext {
 
     final rowDataController = RowController(
       viewId: rowInfo.viewId,
-      rowId: rowInfo.rowPB.id,
+      rowMeta: rowInfo.rowMeta,
       rowCache: rowCache,
     );
 
     final rowBloc = RowBloc(
-      rowInfo: rowInfo,
+      viewId: rowInfo.viewId,
       dataController: rowDataController,
+      rowId: rowInfo.rowMeta.id,
     )..add(const RowEvent.initial());
     await gridResponseFuture();
 

+ 6 - 6
frontend/appflowy_flutter/test/bloc_test/grid_test/grid_bloc_test.dart

@@ -63,16 +63,16 @@ void main() {
       act: (bloc) async {
         await gridResponseFuture();
 
-        firstId = bloc.state.rowInfos[0].rowPB.id;
-        secondId = bloc.state.rowInfos[1].rowPB.id;
-        thirdId = bloc.state.rowInfos[2].rowPB.id;
+        firstId = bloc.state.rowInfos[0].rowId;
+        secondId = bloc.state.rowInfos[1].rowId;
+        thirdId = bloc.state.rowInfos[2].rowId;
 
         bloc.add(const GridEvent.moveRow(0, 2));
       },
       verify: (bloc) {
-        expect(secondId, bloc.state.rowInfos[0].rowPB.id);
-        expect(thirdId, bloc.state.rowInfos[1].rowPB.id);
-        expect(firstId, bloc.state.rowInfos[2].rowPB.id);
+        expect(secondId, bloc.state.rowInfos[0].rowId);
+        expect(thirdId, bloc.state.rowInfos[1].rowId);
+        expect(firstId, bloc.state.rowInfos[2].rowId);
       },
     );
   });

+ 4 - 3
frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart

@@ -35,7 +35,7 @@ class GridTestContext {
     return gridController.fieldController;
   }
 
-  Future<Either<RowPB, FlowyError>> createRow() async {
+  Future<Either<RowMetaPB, FlowyError>> createRow() async {
     return gridController.createRow();
   }
 
@@ -55,14 +55,15 @@ class GridTestContext {
     final rowCache = gridController.rowCache;
 
     final rowDataController = RowController(
-      rowId: rowInfo.rowPB.id,
+      rowMeta: rowInfo.rowMeta,
       viewId: rowInfo.viewId,
       rowCache: rowCache,
     );
 
     final rowBloc = RowBloc(
-      rowInfo: rowInfo,
+      viewId: rowInfo.viewId,
       dataController: rowDataController,
+      rowId: rowInfo.rowMeta.id,
     )..add(const RowEvent.initial());
     await gridResponseFuture();
 

+ 18 - 10
frontend/appflowy_tauri/src-tauri/Cargo.lock

@@ -99,7 +99,7 @@ checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
 [[package]]
 name = "appflowy-integrate"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
 dependencies = [
  "anyhow",
  "collab",
@@ -1024,7 +1024,7 @@ dependencies = [
 [[package]]
 name = "collab"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
 dependencies = [
  "anyhow",
  "bytes",
@@ -1042,7 +1042,7 @@ dependencies = [
 [[package]]
 name = "collab-client-ws"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
 dependencies = [
  "bytes",
  "collab-sync",
@@ -1060,7 +1060,7 @@ dependencies = [
 [[package]]
 name = "collab-database"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -1086,7 +1086,7 @@ dependencies = [
 [[package]]
 name = "collab-derive"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1098,7 +1098,7 @@ dependencies = [
 [[package]]
 name = "collab-document"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
 dependencies = [
  "anyhow",
  "collab",
@@ -1109,13 +1109,14 @@ dependencies = [
  "serde",
  "serde_json",
  "thiserror",
+ "tokio",
  "tracing",
 ]
 
 [[package]]
 name = "collab-folder"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
 dependencies = [
  "anyhow",
  "chrono",
@@ -1135,7 +1136,7 @@ dependencies = [
 [[package]]
 name = "collab-persistence"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
 dependencies = [
  "bincode",
  "chrono",
@@ -1155,7 +1156,7 @@ dependencies = [
 [[package]]
 name = "collab-plugins"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -1186,7 +1187,7 @@ dependencies = [
 [[package]]
 name = "collab-sync"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
 dependencies = [
  "bytes",
  "collab",
@@ -5006,6 +5007,12 @@ dependencies = [
  "digest 0.10.6",
 ]
 
+[[package]]
+name = "sha1_smol"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012"
+
 [[package]]
 name = "sha2"
 version = "0.10.6"
@@ -6206,6 +6213,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2"
 dependencies = [
  "getrandom 0.2.9",
+ "sha1_smol",
 ]
 
 [[package]]

+ 6 - 6
frontend/appflowy_tauri/src-tauri/Cargo.toml

@@ -34,12 +34,12 @@ default = ["custom-protocol"]
 custom-protocol = ["tauri/custom-protocol"]
 
 [patch.crates-io]
-collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4f5837" }
-collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4f5837" }
-collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4f5837" }
-collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4f5837" }
-collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4f5837" }
-appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4f5837" }
+collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9f7fc" }
+collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9f7fc" }
+collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9f7fc" }
+collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9f7fc" }
+collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9f7fc" }
+appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9f7fc" }
 
 #collab = { path = "../../AppFlowy-Collab/collab" }
 #collab-folder = { path = "../../AppFlowy-Collab/collab-folder" }

+ 16 - 9
frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/group/group_controller.ts

@@ -1,4 +1,11 @@
-import { DatabaseNotification, FlowyError, GroupPB, GroupRowsNotificationPB, RowPB } from '@/services/backend';
+import {
+  DatabaseNotification,
+  FlowyError,
+  GroupPB,
+  GroupRowsNotificationPB,
+  RowMetaPB,
+  RowPB,
+} from '@/services/backend';
 import { ChangeNotifier } from '$app/utils/change_notifier';
 import { None, Ok, Option, Result, Some } from 'ts-results';
 import { DatabaseNotificationObserver } from '../notifications/observer';
@@ -7,10 +14,10 @@ import { DatabaseBackendService } from '../database_bd_svc';
 
 export type GroupDataCallbacks = {
   onRemoveRow: (groupId: string, rowId: string) => void;
-  onInsertRow: (groupId: string, row: RowPB, index?: number) => void;
-  onUpdateRow: (groupId: string, row: RowPB) => void;
+  onInsertRow: (groupId: string, row: RowMetaPB, index?: number) => void;
+  onUpdateRow: (groupId: string, row: RowMetaPB) => void;
 
-  onCreateRow: (groupId: string, row: RowPB) => void;
+  onCreateRow: (groupId: string, row: RowMetaPB) => void;
 };
 
 export class DatabaseGroupController {
@@ -37,7 +44,7 @@ export class DatabaseGroupController {
     this.group = group;
   };
 
-  rowAtIndex = (index: number): Option<RowPB> => {
+  rowAtIndex = (index: number): Option<RowMetaPB> => {
     if (this.group.rows.length < index) {
       return None;
     }
@@ -59,16 +66,16 @@ export class DatabaseGroupController {
           changeset.inserted_rows.forEach((insertedRow) => {
             let index: number | undefined = insertedRow.index;
             if (insertedRow.has_index && this.group.rows.length > insertedRow.index) {
-              this.group.rows.splice(index, 0, insertedRow.row);
+              this.group.rows.splice(index, 0, insertedRow.row_meta);
             } else {
               index = undefined;
-              this.group.rows.push(insertedRow.row);
+              this.group.rows.push(insertedRow.row_meta);
             }
 
             if (insertedRow.is_new) {
-              this.callbacks?.onCreateRow(this.group.group_id, insertedRow.row);
+              this.callbacks?.onCreateRow(this.group.group_id, insertedRow.row_meta);
             } else {
-              this.callbacks?.onInsertRow(this.group.group_id, insertedRow.row, index);
+              this.callbacks?.onInsertRow(this.group.group_id, insertedRow.row_meta, index);
             }
           });
 

+ 12 - 15
frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/row/row_cache.ts

@@ -7,12 +7,13 @@ import {
   RowsChangePB,
   RowsVisibilityChangePB,
   ReorderSingleRowPB,
+  RowMetaPB,
 } from '@/services/backend';
 import { ChangeNotifier } from '$app/utils/change_notifier';
 import { FieldInfo } from '../field/field_controller';
 import { CellCache, CellCacheKey } from '../cell/cell_cache';
 import { CellIdentifier } from '../cell/cell_bd_svc';
-import { DatabaseEventGetRow } from '@/services/backend/events/flowy-database2';
+import { DatabaseEventGetRow, DatabaseEventGetRowMeta } from '@/services/backend/events/flowy-database2';
 import { None, Option, Some } from 'ts-results';
 import { Log } from '$app/utils/log';
 
@@ -75,7 +76,7 @@ export class RowCache {
     this.notifier.withChange(RowChangedReason.FieldDidChanged);
   };
 
-  initializeRows = (rows: RowPB[]) => {
+  initializeRows = (rows: RowMetaPB[]) => {
     rows.forEach((rowPB) => {
       this.rowList.push(this._toRowInfo(rowPB));
     });
@@ -106,11 +107,7 @@ export class RowCache {
     }
   };
 
-  private _refreshRow = (opRow: OptionalRowPB) => {
-    if (!opRow.has_row) {
-      return;
-    }
-    const updatedRow = opRow.row;
+  private _refreshRow = (updatedRow: RowMetaPB) => {
     const option = this.rowList.getRowWithIndex(updatedRow.id);
     if (option.some) {
       const { rowInfo, index } = option.val;
@@ -124,7 +121,7 @@ export class RowCache {
 
   private _loadRow = (rowId: string) => {
     const payload = RowIdPB.fromObject({ view_id: this.viewId, row_id: rowId });
-    return DatabaseEventGetRow(payload);
+    return DatabaseEventGetRowMeta(payload);
   };
 
   private _deleteRows = (rowIds: string[]) => {
@@ -138,7 +135,7 @@ export class RowCache {
 
   private _insertRows = (rows: InsertedRowPB[]) => {
     rows.forEach((insertedRow) => {
-      const rowInfo = this._toRowInfo(insertedRow.row);
+      const rowInfo = this._toRowInfo(insertedRow.row_meta);
       const insertedIndex = this.rowList.insert(insertedRow.index, rowInfo);
       if (insertedIndex !== undefined) {
         this.notifier.withChange(RowChangedReason.Insert, insertedIndex.rowId);
@@ -154,11 +151,11 @@ export class RowCache {
     const rowInfos: RowInfo[] = [];
     updatedRows.forEach((updatedRow) => {
       updatedRow.field_ids.forEach((fieldId) => {
-        const key = new CellCacheKey(fieldId, updatedRow.row.id);
+        const key = new CellCacheKey(fieldId, updatedRow.row_meta.id);
         this.cellCache.remove(key);
       });
 
-      rowInfos.push(this._toRowInfo(updatedRow.row));
+      rowInfos.push(this._toRowInfo(updatedRow.row_meta));
     });
 
     const updatedIndexs = this.rowList.insertRows(rowInfos);
@@ -178,7 +175,7 @@ export class RowCache {
 
   private _displayRows = (insertedRows: InsertedRowPB[]) => {
     insertedRows.forEach((insertedRow) => {
-      const insertedIndex = this.rowList.insert(insertedRow.index, this._toRowInfo(insertedRow.row));
+      const insertedIndex = this.rowList.insert(insertedRow.index, this._toRowInfo(insertedRow.row_meta));
 
       if (insertedIndex !== undefined) {
         this.notifier.withChange(RowChangedReason.Insert, insertedIndex.rowId);
@@ -190,7 +187,7 @@ export class RowCache {
     this.notifier.dispose();
   };
 
-  private _toRowInfo = (rowPB: RowPB) => {
+  private _toRowInfo = (rowPB: RowMetaPB) => {
     return new RowInfo(this.viewId, this.getFieldInfos(), rowPB);
   };
 
@@ -338,10 +335,10 @@ export class RowInfo {
   constructor(
     public readonly viewId: string,
     public readonly fieldInfos: readonly FieldInfo[],
-    public readonly row: RowPB
+    public readonly row: RowMetaPB
   ) {}
 
-  copyWith = (params: { row?: RowPB; fieldInfos?: readonly FieldInfo[] }) => {
+  copyWith = (params: { row?: RowMetaPB; fieldInfos?: readonly FieldInfo[] }) => {
     return new RowInfo(this.viewId, params.fieldInfos || this.fieldInfos, params.row || this.row);
   };
 }

+ 2 - 2
frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/database_view_cache.ts

@@ -1,7 +1,7 @@
 import { DatabaseViewRowsObserver } from './view_row_observer';
 import { RowCache, RowInfo } from '../row/row_cache';
 import { FieldController } from '../field/field_controller';
-import { RowPB } from '@/services/backend';
+import { RowMetaPB, RowPB } from '@/services/backend';
 
 export class DatabaseViewCache {
   private readonly rowsObserver: DatabaseViewRowsObserver;
@@ -20,7 +20,7 @@ export class DatabaseViewCache {
     });
   }
 
-  initializeWithRows = (rows: RowPB[]) => {
+  initializeWithRows = (rows: RowMetaPB[]) => {
     this.rowCache.initializeRows(rows);
   };
 

+ 18 - 10
frontend/rust-lib/Cargo.lock

@@ -85,7 +85,7 @@ checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4"
 [[package]]
 name = "appflowy-integrate"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
 dependencies = [
  "anyhow",
  "collab",
@@ -887,7 +887,7 @@ dependencies = [
 [[package]]
 name = "collab"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
 dependencies = [
  "anyhow",
  "bytes",
@@ -905,7 +905,7 @@ dependencies = [
 [[package]]
 name = "collab-client-ws"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
 dependencies = [
  "bytes",
  "collab-sync",
@@ -923,7 +923,7 @@ dependencies = [
 [[package]]
 name = "collab-database"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -949,7 +949,7 @@ dependencies = [
 [[package]]
 name = "collab-derive"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -961,7 +961,7 @@ dependencies = [
 [[package]]
 name = "collab-document"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
 dependencies = [
  "anyhow",
  "collab",
@@ -972,13 +972,14 @@ dependencies = [
  "serde",
  "serde_json",
  "thiserror",
+ "tokio",
  "tracing",
 ]
 
 [[package]]
 name = "collab-folder"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
 dependencies = [
  "anyhow",
  "chrono",
@@ -998,7 +999,7 @@ dependencies = [
 [[package]]
 name = "collab-persistence"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
 dependencies = [
  "bincode",
  "chrono",
@@ -1018,7 +1019,7 @@ dependencies = [
 [[package]]
 name = "collab-plugins"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -1049,7 +1050,7 @@ dependencies = [
 [[package]]
 name = "collab-sync"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
 dependencies = [
  "bytes",
  "collab",
@@ -4176,6 +4177,12 @@ dependencies = [
  "digest 0.10.6",
 ]
 
+[[package]]
+name = "sha1_smol"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012"
+
 [[package]]
 name = "sha2"
 version = "0.10.6"
@@ -5029,6 +5036,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2"
 dependencies = [
  "getrandom 0.2.9",
+ "sha1_smol",
 ]
 
 [[package]]

+ 5 - 5
frontend/rust-lib/Cargo.toml

@@ -33,11 +33,11 @@ opt-level = 3
 incremental = false
 
 [patch.crates-io]
-collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4f5837" }
-collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4f5837" }
-collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4f5837" }
-collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4f5837" }
-appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4f5837" }
+collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9f7fc" }
+collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9f7fc" }
+collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9f7fc" }
+collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9f7fc" }
+appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9f7fc" }
 
 #collab = { path = "../AppFlowy-Collab/collab" }
 #collab-folder = { path = "../AppFlowy-Collab/collab-folder" }

+ 2 - 1
frontend/rust-lib/flowy-database2/src/entities/calendar_entities.rs

@@ -2,6 +2,7 @@ use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
 use flowy_error::ErrorCode;
 
 use crate::entities::parser::NotEmptyStr;
+use crate::entities::RowMetaPB;
 use crate::services::setting::{CalendarLayout, CalendarLayoutSetting};
 
 use super::CellIdPB;
@@ -99,7 +100,7 @@ impl TryInto<CalendarEventRequestParams> for CalendarEventRequestPB {
 #[derive(Debug, Clone, Default, ProtoBuf)]
 pub struct CalendarEventPB {
   #[pb(index = 1)]
-  pub row_id: String,
+  pub row_meta: RowMetaPB,
 
   #[pb(index = 2)]
   pub date_field_id: String,

+ 2 - 2
frontend/rust-lib/flowy-database2/src/entities/database_entities.rs

@@ -6,7 +6,7 @@ use flowy_derive::ProtoBuf;
 use flowy_error::{ErrorCode, FlowyError};
 
 use crate::entities::parser::NotEmptyStr;
-use crate::entities::{DatabaseLayoutPB, FieldIdPB, RowPB};
+use crate::entities::{DatabaseLayoutPB, FieldIdPB, RowMetaPB};
 use crate::services::database::CreateDatabaseViewParams;
 
 /// [DatabasePB] describes how many fields and blocks the grid has
@@ -19,7 +19,7 @@ pub struct DatabasePB {
   pub fields: Vec<FieldIdPB>,
 
   #[pb(index = 3)]
-  pub rows: Vec<RowPB>,
+  pub rows: Vec<RowMetaPB>,
 
   #[pb(index = 4)]
   pub layout_type: DatabaseLayoutPB,

+ 7 - 3
frontend/rust-lib/flowy-database2/src/entities/group_entities/group.rs

@@ -4,7 +4,7 @@ use flowy_derive::ProtoBuf;
 use flowy_error::ErrorCode;
 
 use crate::entities::parser::NotEmptyStr;
-use crate::entities::{FieldType, RowPB};
+use crate::entities::{FieldType, RowMetaPB};
 use crate::services::group::{GroupChangeset, GroupData, GroupSetting};
 
 #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
@@ -79,7 +79,7 @@ pub struct GroupPB {
   pub group_name: String,
 
   #[pb(index = 4)]
-  pub rows: Vec<RowPB>,
+  pub rows: Vec<RowMetaPB>,
 
   #[pb(index = 5)]
   pub is_default: bool,
@@ -94,7 +94,11 @@ impl std::convert::From<GroupData> for GroupPB {
       field_id: group_data.field_id,
       group_id: group_data.id,
       group_name: group_data.name,
-      rows: group_data.rows.into_iter().map(RowPB::from).collect(),
+      rows: group_data
+        .rows
+        .into_iter()
+        .map(|row_detail| RowMetaPB::from(row_detail.meta))
+        .collect(),
       is_default: group_data.is_default,
       is_visible: group_data.is_visible,
     }

+ 4 - 4
frontend/rust-lib/flowy-database2/src/entities/group_entities/group_changeset.rs

@@ -4,7 +4,7 @@ use flowy_derive::ProtoBuf;
 use flowy_error::ErrorCode;
 
 use crate::entities::parser::NotEmptyStr;
-use crate::entities::{GroupPB, InsertedRowPB, RowPB};
+use crate::entities::{GroupPB, InsertedRowPB, RowMetaPB};
 
 #[derive(Debug, Default, ProtoBuf)]
 pub struct GroupRowsNotificationPB {
@@ -21,7 +21,7 @@ pub struct GroupRowsNotificationPB {
   pub deleted_rows: Vec<String>,
 
   #[pb(index = 5)]
-  pub updated_rows: Vec<RowPB>,
+  pub updated_rows: Vec<RowMetaPB>,
 }
 
 impl std::fmt::Display for GroupRowsNotificationPB {
@@ -29,7 +29,7 @@ impl std::fmt::Display for GroupRowsNotificationPB {
     for inserted_row in &self.inserted_rows {
       f.write_fmt(format_args!(
         "Insert: {} row at {:?}",
-        inserted_row.row.id, inserted_row.index
+        inserted_row.row_meta.id, inserted_row.index
       ))?;
     }
 
@@ -80,7 +80,7 @@ impl GroupRowsNotificationPB {
     }
   }
 
-  pub fn update(group_id: String, updated_rows: Vec<RowPB>) -> Self {
+  pub fn update(group_id: String, updated_rows: Vec<RowMetaPB>) -> Self {
     Self {
       group_id,
       updated_rows,

+ 164 - 16
frontend/rust-lib/flowy-database2/src/entities/row_entities.rs

@@ -1,6 +1,6 @@
 use std::collections::HashMap;
 
-use collab_database::rows::{Row, RowId};
+use collab_database::rows::{Row, RowId, RowMeta};
 use collab_database::views::RowOrder;
 
 use flowy_derive::ProtoBuf;
@@ -36,6 +36,7 @@ impl std::convert::From<Row> for RowPB {
     }
   }
 }
+
 impl From<RowOrder> for RowPB {
   fn from(data: RowOrder) -> Self {
     Self {
@@ -45,6 +46,153 @@ impl From<RowOrder> for RowPB {
   }
 }
 
+#[derive(Debug, Default, Clone, ProtoBuf)]
+pub struct RowMetaPB {
+  #[pb(index = 1)]
+  pub id: String,
+
+  #[pb(index = 2)]
+  pub document_id: String,
+
+  #[pb(index = 3, one_of)]
+  pub icon: Option<String>,
+
+  #[pb(index = 4, one_of)]
+  pub cover: Option<String>,
+}
+
+impl std::convert::From<&RowMeta> for RowMetaPB {
+  fn from(row_meta: &RowMeta) -> Self {
+    Self {
+      id: row_meta.row_id.clone(),
+      document_id: row_meta.document_id.clone(),
+      icon: row_meta.icon_url.clone(),
+      cover: row_meta.cover_url.clone(),
+    }
+  }
+}
+
+impl std::convert::From<RowMeta> for RowMetaPB {
+  fn from(row_meta: RowMeta) -> Self {
+    Self {
+      id: row_meta.row_id,
+      document_id: row_meta.document_id,
+      icon: row_meta.icon_url,
+      cover: row_meta.cover_url,
+    }
+  }
+}
+
+#[derive(Debug, Default, Clone, ProtoBuf)]
+pub struct UpdateRowMetaChangesetPB {
+  #[pb(index = 1)]
+  pub id: String,
+
+  #[pb(index = 2)]
+  pub view_id: String,
+
+  #[pb(index = 3, one_of)]
+  pub icon_url: Option<String>,
+
+  #[pb(index = 4, one_of)]
+  pub cover_url: Option<String>,
+}
+
+#[derive(Debug)]
+pub struct UpdateRowMetaParams {
+  pub id: String,
+  pub view_id: String,
+  pub icon_url: Option<String>,
+  pub cover_url: Option<String>,
+}
+
+impl TryInto<UpdateRowMetaParams> for UpdateRowMetaChangesetPB {
+  type Error = ErrorCode;
+
+  fn try_into(self) -> Result<UpdateRowMetaParams, Self::Error> {
+    let row_id = NotEmptyStr::parse(self.id)
+      .map_err(|_| ErrorCode::RowIdIsEmpty)?
+      .0;
+
+    let view_id = NotEmptyStr::parse(self.view_id)
+      .map_err(|_| ErrorCode::ViewIdIsInvalid)?
+      .0;
+    Ok(UpdateRowMetaParams {
+      id: row_id,
+      view_id,
+      icon_url: self.icon_url,
+      cover_url: self.cover_url,
+    })
+  }
+}
+
+#[derive(Debug, Default, Clone, ProtoBuf)]
+pub struct UpdateRowPayloadPB {
+  #[pb(index = 1)]
+  pub row_id: String,
+
+  #[pb(index = 2, one_of)]
+  pub insert_document: Option<bool>,
+
+  #[pb(index = 3, one_of)]
+  pub insert_comment: Option<RowCommentPayloadPB>,
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct UpdateRowParams {
+  pub row_id: String,
+  pub insert_comment: Option<RowCommentParams>,
+}
+
+impl TryInto<UpdateRowParams> for UpdateRowPayloadPB {
+  type Error = ErrorCode;
+
+  fn try_into(self) -> Result<UpdateRowParams, Self::Error> {
+    let row_id = NotEmptyStr::parse(self.row_id)
+      .map_err(|_| ErrorCode::RowIdIsEmpty)?
+      .0;
+    let insert_comment = self
+      .insert_comment
+      .map(|comment| comment.try_into())
+      .transpose()?;
+
+    Ok(UpdateRowParams {
+      row_id,
+      insert_comment,
+    })
+  }
+}
+
+#[derive(Debug, Default, Clone, ProtoBuf)]
+pub struct RowCommentPayloadPB {
+  #[pb(index = 1)]
+  pub uid: String,
+
+  #[pb(index = 2)]
+  pub comment: String,
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct RowCommentParams {
+  pub uid: String,
+  pub comment: String,
+}
+
+impl TryInto<RowCommentParams> for RowCommentPayloadPB {
+  type Error = ErrorCode;
+
+  fn try_into(self) -> Result<RowCommentParams, Self::Error> {
+    let uid = NotEmptyStr::parse(self.uid)
+      .map_err(|_| ErrorCode::RowIdIsEmpty)?
+      .0;
+    let comment = NotEmptyStr::parse(self.comment)
+      .map_err(|_| ErrorCode::RowIdIsEmpty)?
+      .0;
+
+    Ok(RowCommentParams { uid, comment })
+  }
+}
+
 #[derive(Debug, Default, ProtoBuf)]
 pub struct OptionalRowPB {
   #[pb(index = 1, one_of)]
@@ -66,7 +214,7 @@ impl std::convert::From<Vec<RowPB>> for RepeatedRowPB {
 #[derive(Debug, Clone, Default, ProtoBuf)]
 pub struct InsertedRowPB {
   #[pb(index = 1)]
-  pub row: RowPB,
+  pub row_meta: RowMetaPB,
 
   #[pb(index = 2, one_of)]
   pub index: Option<i32>,
@@ -76,9 +224,9 @@ pub struct InsertedRowPB {
 }
 
 impl InsertedRowPB {
-  pub fn new(row: RowPB) -> Self {
+  pub fn new(row_meta: RowMetaPB) -> Self {
     Self {
-      row,
+      row_meta,
       index: None,
       is_new: false,
     }
@@ -90,26 +238,20 @@ impl InsertedRowPB {
   }
 }
 
-impl std::convert::From<RowPB> for InsertedRowPB {
-  fn from(row: RowPB) -> Self {
+impl std::convert::From<RowMetaPB> for InsertedRowPB {
+  fn from(row_meta: RowMetaPB) -> Self {
     Self {
-      row,
+      row_meta,
       index: None,
       is_new: false,
     }
   }
 }
 
-impl std::convert::From<&Row> for InsertedRowPB {
-  fn from(row: &Row) -> Self {
-    Self::from(RowPB::from(row))
-  }
-}
-
 impl From<InsertedRow> for InsertedRowPB {
   fn from(data: InsertedRow) -> Self {
     Self {
-      row: data.row.into(),
+      row_meta: data.row_meta.into(),
       index: data.index,
       is_new: data.is_new,
     }
@@ -119,18 +261,24 @@ impl From<InsertedRow> for InsertedRowPB {
 #[derive(Debug, Clone, Default, ProtoBuf)]
 pub struct UpdatedRowPB {
   #[pb(index = 1)]
-  pub row: RowPB,
+  pub row_id: String,
 
   // Indicates the field ids of the cells that were updated in this row.
   #[pb(index = 2)]
   pub field_ids: Vec<String>,
+
+  /// The meta of row was updated if this is Some.
+  #[pb(index = 3, one_of)]
+  pub row_meta: Option<RowMetaPB>,
 }
 
 impl From<UpdatedRow> for UpdatedRowPB {
   fn from(data: UpdatedRow) -> Self {
+    let row_meta = data.row_meta.map(RowMetaPB::from);
     Self {
-      row: data.row.into(),
+      row_id: data.row_id,
       field_ids: data.field_ids,
+      row_meta,
     }
   }
 }

+ 53 - 2
frontend/rust-lib/flowy-database2/src/event_handler.rs

@@ -133,6 +133,34 @@ pub(crate) async fn get_fields_handler(
   data_result_ok(fields)
 }
 
+#[tracing::instrument(level = "trace", skip(data, manager), err)]
+pub(crate) async fn get_primary_field_handler(
+  data: AFPluginData<DatabaseViewIdPB>,
+  manager: AFPluginState<Arc<DatabaseManager2>>,
+) -> DataResult<FieldPB, FlowyError> {
+  let view_id = data.into_inner().value;
+  let database_editor = manager.get_database_with_view_id(&view_id).await?;
+  let mut fields = database_editor
+    .get_fields(&view_id, None)
+    .into_iter()
+    .filter(|field| field.is_primary)
+    .map(FieldPB::from)
+    .collect::<Vec<FieldPB>>();
+
+  if fields.is_empty() {
+    // The primary field should not be empty. Because it is created when the database is created.
+    // If it is empty, it must be a bug.
+    Err(FlowyError::record_not_found())
+  } else {
+    if fields.len() > 1 {
+      // The primary field should not be more than one. If it is more than one,
+      // it must be a bug.
+      tracing::error!("The primary field is more than one");
+    }
+    data_result_ok(fields.remove(0))
+  }
+}
+
 #[tracing::instrument(level = "trace", skip(data, manager), err)]
 pub(crate) async fn update_field_handler(
   data: AFPluginData<FieldChangesetPB>,
@@ -300,6 +328,29 @@ pub(crate) async fn get_row_handler(
   data_result_ok(OptionalRowPB { row })
 }
 
+pub(crate) async fn get_row_meta_handler(
+  data: AFPluginData<RowIdPB>,
+  manager: AFPluginState<Arc<DatabaseManager2>>,
+) -> DataResult<RowMetaPB, FlowyError> {
+  let params: RowIdParams = data.into_inner().try_into()?;
+  let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
+  match database_editor.get_row_meta(&params.view_id, &params.row_id) {
+    None => Err(FlowyError::record_not_found()),
+    Some(row) => data_result_ok(row),
+  }
+}
+
+pub(crate) async fn update_row_meta_handler(
+  data: AFPluginData<UpdateRowMetaChangesetPB>,
+  manager: AFPluginState<Arc<DatabaseManager2>>,
+) -> FlowyResult<()> {
+  let params: UpdateRowMetaParams = data.into_inner().try_into()?;
+  let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
+  let row_id = RowId::from(params.id.clone());
+  database_editor.update_row_meta(&row_id, params).await;
+  Ok(())
+}
+
 #[tracing::instrument(level = "debug", skip(data, manager), err)]
 pub(crate) async fn delete_row_handler(
   data: AFPluginData<RowIdPB>,
@@ -341,7 +392,7 @@ pub(crate) async fn move_row_handler(
 pub(crate) async fn create_row_handler(
   data: AFPluginData<CreateRowPayloadPB>,
   manager: AFPluginState<Arc<DatabaseManager2>>,
-) -> DataResult<RowPB, FlowyError> {
+) -> DataResult<RowMetaPB, FlowyError> {
   let params: CreateRowParams = data.into_inner().try_into()?;
   let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
   let fields = database_editor.get_fields(&params.view_id, None);
@@ -362,7 +413,7 @@ pub(crate) async fn create_row_handler(
     .await?
   {
     None => Err(FlowyError::internal().context("Create row fail")),
-    Some(row) => data_result_ok(RowPB::from(row)),
+    Some(row) => data_result_ok(RowMetaPB::from(row.meta)),
   }
 }
 

+ 13 - 1
frontend/rust-lib/flowy-database2/src/event_map.rs

@@ -22,6 +22,7 @@ pub fn init(database_manager: Arc<DatabaseManager2>) -> AFPlugin {
         .event(DatabaseEvent::DeleteAllSorts, delete_all_sorts_handler)
         // Field
         .event(DatabaseEvent::GetFields, get_fields_handler)
+        .event(DatabaseEvent::GetPrimaryField, get_primary_field_handler)
         .event(DatabaseEvent::UpdateField, update_field_handler)
         .event(DatabaseEvent::UpdateFieldTypeOption, update_field_type_option_handler)
         .event(DatabaseEvent::DeleteField, delete_field_handler)
@@ -33,6 +34,8 @@ pub fn init(database_manager: Arc<DatabaseManager2>) -> AFPlugin {
         // Row
         .event(DatabaseEvent::CreateRow, create_row_handler)
         .event(DatabaseEvent::GetRow, get_row_handler)
+        .event(DatabaseEvent::GetRowMeta, get_row_meta_handler)
+        .event(DatabaseEvent::UpdateRowMeta, update_row_meta_handler)
         .event(DatabaseEvent::DeleteRow, delete_row_handler)
         .event(DatabaseEvent::DuplicateRow, duplicate_row_handler)
         .event(DatabaseEvent::MoveRow, move_row_handler)
@@ -172,6 +175,9 @@ pub enum DatabaseEvent {
   #[event(input = "CreateFieldPayloadPB", output = "TypeOptionPB")]
   CreateTypeOption = 24,
 
+  #[event(input = "DatabaseViewIdPB", output = "FieldPB")]
+  GetPrimaryField = 25,
+
   /// [CreateSelectOption] event is used to create a new select option. Returns a [SelectOptionPB] if
   /// there are no errors.
   #[event(input = "CreateSelectOptionPayloadPB", output = "SelectOptionPB")]
@@ -195,7 +201,7 @@ pub enum DatabaseEvent {
   #[event(input = "RepeatedSelectOptionPayload")]
   DeleteSelectOption = 33,
 
-  #[event(input = "CreateRowPayloadPB", output = "RowPB")]
+  #[event(input = "CreateRowPayloadPB", output = "RowMetaPB")]
   CreateRow = 50,
 
   /// [GetRow] event is used to get the row data,[RowPB]. [OptionalRowPB] is a wrapper that enables
@@ -212,6 +218,12 @@ pub enum DatabaseEvent {
   #[event(input = "MoveRowPayloadPB")]
   MoveRow = 54,
 
+  #[event(input = "RowIdPB", output = "RowMetaPB")]
+  GetRowMeta = 55,
+
+  #[event(input = "UpdateRowMetaChangesetPB")]
+  UpdateRowMeta = 56,
+
   #[event(input = "CellIdPB", output = "CellPB")]
   GetCell = 70,
 

+ 2 - 0
frontend/rust-lib/flowy-database2/src/notification.rs

@@ -31,6 +31,8 @@ pub enum DatabaseNotification {
   DidReorderRows = 65,
   /// Trigger after editing the row that hit the sort rule
   DidReorderSingleRow = 66,
+  /// Trigger after updating the row meta
+  DidUpdateRowMeta = 67,
   /// Trigger when the settings of the database are changed
   DidUpdateSettings = 70,
   // Trigger when the layout setting of the database is updated

+ 102 - 41
frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs

@@ -14,19 +14,13 @@ use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult};
 use flowy_task::TaskDispatcher;
 use lib_infra::future::{to_fut, Fut};
 
-use crate::entities::{
-  CalendarEventPB, CellChangesetNotifyPB, CellPB, ChecklistCellDataPB, DatabaseFieldChangesetPB,
-  DatabasePB, DatabaseViewSettingPB, DeleteFilterParams, DeleteGroupParams, DeleteSortParams,
-  FieldChangesetParams, FieldIdPB, FieldPB, FieldType, GroupPB, IndexFieldPB, InsertedRowPB,
-  LayoutSettingParams, NoDateCalendarEventPB, RepeatedFilterPB, RepeatedGroupPB, RepeatedSortPB,
-  RowPB, RowsChangePB, SelectOptionCellDataPB, SelectOptionPB, UpdateFilterParams,
-  UpdateSortParams, UpdatedRowPB,
-};
+use crate::entities::*;
 use crate::notification::{send_notification, DatabaseNotification};
 use crate::services::cell::{
   apply_cell_changeset, get_cell_protobuf, AnyTypeCache, CellCache, ToCellChangeset,
 };
 use crate::services::database::util::database_view_setting_pb_from_view;
+use crate::services::database::{RowDetail, UpdatedRow};
 use crate::services::database_view::{DatabaseViewChanged, DatabaseViewData, DatabaseViews};
 use crate::services::field::checklist_type_option::{ChecklistCellChangeset, ChecklistCellData};
 use crate::services::field::{
@@ -376,8 +370,8 @@ impl DatabaseEditor {
 
   pub async fn move_row(&self, view_id: &str, from: RowId, to: RowId) {
     let database = self.database.lock();
-    if let (Some(row), Some(from_index), Some(to_index)) = (
-      database.get_row(&from),
+    if let (Some(row_meta), Some(from_index), Some(to_index)) = (
+      database.get_row_meta(&from),
       database.index_of_row(view_id, &from),
       database.index_of_row(view_id, &to),
     ) {
@@ -387,7 +381,7 @@ impl DatabaseEditor {
       drop(database);
 
       let delete_row_id = from.into_inner();
-      let insert_row = InsertedRowPB::from(&row).with_index(to_index as i32);
+      let insert_row = InsertedRowPB::new(RowMetaPB::from(&row_meta)).with_index(to_index as i32);
       let changes =
         RowsChangePB::from_move(view_id.to_string(), vec![delete_row_id], vec![insert_row]);
       send_notification(view_id, DatabaseNotification::DidUpdateViewRows)
@@ -401,7 +395,7 @@ impl DatabaseEditor {
     view_id: &str,
     group_id: Option<String>,
     mut params: CreateRowParams,
-  ) -> FlowyResult<Option<Row>> {
+  ) -> FlowyResult<Option<RowDetail>> {
     for view in self.database_views.editors().await {
       view.v_will_create_row(&mut params.cells, &group_id).await;
     }
@@ -409,11 +403,13 @@ impl DatabaseEditor {
     if let Some((index, row_order)) = result {
       tracing::trace!("create row: {:?} at {}", row_order, index);
       let row = self.database.lock().get_row(&row_order.id);
-      if let Some(row) = row {
+      let row_meta = self.database.lock().get_row_meta(&row_order.id);
+      if let (Some(row), Some(meta)) = (row, row_meta) {
+        let row_detail = RowDetail { row, meta };
         for view in self.database_views.editors().await {
-          view.v_did_create_row(&row, &group_id, index).await;
+          view.v_did_create_row(&row_detail, &group_id, index).await;
         }
-        return Ok(Some(row));
+        return Ok(Some(row_detail));
       }
     }
 
@@ -491,16 +487,42 @@ impl DatabaseEditor {
     Ok(())
   }
 
-  pub async fn get_rows(&self, view_id: &str) -> FlowyResult<Vec<Arc<Row>>> {
+  pub async fn get_rows(&self, view_id: &str) -> FlowyResult<Vec<Arc<RowDetail>>> {
     let view_editor = self.database_views.get_view_editor(view_id).await?;
     Ok(view_editor.v_get_rows().await)
   }
 
   pub fn get_row(&self, view_id: &str, row_id: &RowId) -> Option<Row> {
     if self.database.lock().views.is_row_exist(view_id, row_id) {
-      return None;
-    } else {
       self.database.lock().get_row(row_id)
+    } else {
+      None
+    }
+  }
+
+  pub fn get_row_meta(&self, view_id: &str, row_id: &RowId) -> Option<RowMetaPB> {
+    if self.database.lock().views.is_row_exist(view_id, row_id) {
+      let row_meta = self.database.lock().get_row_meta(row_id)?;
+      Some(RowMetaPB {
+        id: row_id.clone().into_inner(),
+        document_id: row_meta.document_id,
+        icon: row_meta.icon_url,
+        cover: row_meta.cover_url,
+      })
+    } else {
+      tracing::warn!("the row:{} is exist in view:{}", row_id.as_str(), view_id);
+      None
+    }
+  }
+
+  pub fn get_row_detail(&self, view_id: &str, row_id: &RowId) -> Option<RowDetail> {
+    if self.database.lock().views.is_row_exist(view_id, row_id) {
+      let meta = self.database.lock().get_row_meta(row_id)?;
+      let row = self.database.lock().get_row(row_id)?;
+      Some(RowDetail { row, meta })
+    } else {
+      tracing::warn!("the row:{} is exist in view:{}", row_id.as_str(), view_id);
+      None
     }
   }
 
@@ -514,6 +536,28 @@ impl DatabaseEditor {
     }
   }
 
+  #[tracing::instrument(level = "trace", skip_all)]
+  pub async fn update_row_meta(&self, row_id: &RowId, changeset: UpdateRowMetaParams) {
+    self.database.lock().update_row_meta(row_id, |meta_update| {
+      meta_update
+        .insert_cover_if_not_none(changeset.cover_url)
+        .insert_icon_if_not_none(changeset.icon_url);
+    });
+
+    // Use the temporary row meta to get rid of the lock that not implement the `Send` or 'Sync' trait.
+    let row_meta = self.database.lock().get_row_meta(row_id);
+    if let Some(row_meta) = row_meta {
+      for view in self.database_views.editors().await {
+        view.v_did_update_row_meta(row_id, &row_meta).await;
+      }
+
+      // Notifies the client that the row meta has been updated.
+      send_notification(row_id.as_str(), DatabaseNotification::DidUpdateRowMeta)
+        .payload(RowMetaPB::from(&row_meta))
+        .send();
+    }
+  }
+
   pub async fn get_cell(&self, field_id: &str, row_id: &RowId) -> Option<Cell> {
     let database = self.database.lock();
     let field = database.fields.get_field(field_id)?;
@@ -630,7 +674,7 @@ impl DatabaseEditor {
     new_cell: Cell,
   ) -> FlowyResult<()> {
     // Get the old row before updating the cell. It would be better to get the old cell
-    let old_row = { self.database.lock().get_row(&row_id) };
+    let old_row = { self.get_row_detail(view_id, &row_id) };
 
     // Get all auto updated fields. It will be used to notify the frontend
     // that the fields have been updated.
@@ -642,19 +686,19 @@ impl DatabaseEditor {
       });
     });
 
-    let option_row = self.database.lock().get_row(&row_id);
-    if let Some(new_row) = option_row {
-      let updated_row = UpdatedRowPB {
-        row: RowPB::from(&new_row),
-        field_ids: vec![field_id.to_string()],
-      };
-      let changes = RowsChangePB::from_update(view_id.to_string(), updated_row);
+    let option_row = self.get_row_detail(view_id, &row_id);
+    if let Some(new_row_detail) = option_row {
+      let updated_row =
+        UpdatedRow::new(&new_row_detail.row.id).with_field_ids(vec![field_id.to_string()]);
+      let changes = RowsChangePB::from_update(view_id.to_string(), updated_row.into());
       send_notification(view_id, DatabaseNotification::DidUpdateViewRows)
         .payload(changes)
         .send();
 
       for view in self.database_views.editors().await {
-        view.v_did_update_row(&old_row, &new_row, field_id).await;
+        view
+          .v_did_update_row(&old_row, &new_row_detail, field_id)
+          .await;
       }
     }
 
@@ -854,23 +898,23 @@ impl DatabaseEditor {
     from_row: RowId,
     to_row: Option<RowId>,
   ) -> FlowyResult<()> {
-    let row = self.database.lock().get_row(&from_row);
-    match row {
+    let row_detail = self.get_row_detail(view_id, &from_row);
+    match row_detail {
       None => {
         tracing::warn!(
           "Move row between group failed, can not find the row:{}",
           from_row
         )
       },
-      Some(row) => {
-        let mut row_changeset = RowChangeset::new(row.id.clone());
+      Some(row_detail) => {
+        let mut row_changeset = RowChangeset::new(row_detail.row.id.clone());
         let view = self.database_views.get_view_editor(view_id).await?;
         view
-          .v_move_group_row(&row, &mut row_changeset, to_group, to_row)
+          .v_move_group_row(&row_detail, &mut row_changeset, to_group, to_row)
           .await;
 
         tracing::trace!("Row data changed: {:?}", row_changeset);
-        self.database.lock().update_row(&row.id, |row| {
+        self.database.lock().update_row(&row_detail.row.id, |row| {
           row.set_cells(Cells::from(row_changeset.cell_by_field_id.clone()));
         });
 
@@ -1012,8 +1056,8 @@ impl DatabaseEditor {
 
     let rows = rows
       .into_iter()
-      .map(|row| RowPB::from(row.as_ref()))
-      .collect::<Vec<RowPB>>();
+      .map(|row_detail| RowMetaPB::from(&row_detail.meta))
+      .collect::<Vec<RowMetaPB>>();
     Ok(DatabasePB {
       id: database_id,
       fields,
@@ -1150,20 +1194,37 @@ impl DatabaseViewData for DatabaseViewDataImpl {
     to_fut(async move { index })
   }
 
-  fn get_row(&self, view_id: &str, row_id: &RowId) -> Fut<Option<(usize, Arc<Row>)>> {
+  fn get_row(&self, view_id: &str, row_id: &RowId) -> Fut<Option<(usize, Arc<RowDetail>)>> {
     let index = self.database.lock().index_of_row(view_id, row_id);
     let row = self.database.lock().get_row(row_id);
+    let row_meta = self.database.lock().get_row_meta(row_id);
     to_fut(async move {
-      match (index, row) {
-        (Some(index), Some(row)) => Some((index, Arc::new(row))),
+      match (index, row, row_meta) {
+        (Some(index), Some(row), Some(row_meta)) => {
+          let row_detail = RowDetail {
+            row,
+            meta: row_meta,
+          };
+          Some((index, Arc::new(row_detail)))
+        },
         _ => None,
       }
     })
   }
 
-  fn get_rows(&self, view_id: &str) -> Fut<Vec<Arc<Row>>> {
-    let rows = self.database.lock().get_rows_for_view(view_id);
-    to_fut(async move { rows.into_iter().map(Arc::new).collect() })
+  fn get_rows(&self, view_id: &str) -> Fut<Vec<Arc<RowDetail>>> {
+    let database = self.database.lock();
+    let rows = database.get_rows_for_view(view_id);
+    let row_details = rows
+      .into_iter()
+      .flat_map(|row| {
+        database
+          .get_row_meta(&row.id)
+          .map(|meta| RowDetail { row, meta })
+      })
+      .collect::<Vec<RowDetail>>();
+
+    to_fut(async move { row_details.into_iter().map(Arc::new).collect() })
   }
 
   fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Fut<Vec<Arc<RowCell>>> {

Some files were not shown because too many files changed in this diff