Bläddra i källkod

feat: create database view on same database (#2829)

* feat: create database view on same database

* feat: switch tag between views

* fix: calendar tool bar

* fix: set layout setting

* chore: update collab rev

* fix: board layout issue

* test: add integration tests

* test: add calendar start from day test
Nathan.fooo 1 år sedan
förälder
incheckning
e50d708c21
92 ändrade filer med 3009 tillägg och 1422 borttagningar
  1. 1 0
      frontend/appflowy_flutter/assets/translations/en.json
  2. 78 0
      frontend/appflowy_flutter/integration_test/database_calendar_test.dart
  3. 96 0
      frontend/appflowy_flutter/integration_test/database_view_test.dart
  4. 7 0
      frontend/appflowy_flutter/integration_test/runner.dart
  5. 1 0
      frontend/appflowy_flutter/integration_test/util/base.dart
  6. 10 1
      frontend/appflowy_flutter/integration_test/util/common_operations.dart
  7. 153 2
      frontend/appflowy_flutter/integration_test/util/database_test_op.dart
  8. 1 0
      frontend/appflowy_flutter/integration_test/util/expectation.dart
  9. 91 76
      frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart
  10. 5 2
      frontend/appflowy_flutter/lib/plugins/database_view/application/database_view_service.dart
  11. 2 0
      frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart
  12. 15 2
      frontend/appflowy_flutter/lib/plugins/database_view/application/layout/layout_service.dart
  13. 0 103
      frontend/appflowy_flutter/lib/plugins/database_view/application/setting/setting_bloc.dart
  14. 290 0
      frontend/appflowy_flutter/lib/plugins/database_view/application/tar_bar_bloc.dart
  15. 29 19
      frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_cache.dart
  16. 5 5
      frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart
  17. 2 59
      frontend/appflowy_flutter/lib/plugins/database_view/board/board.dart
  18. 59 36
      frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart
  19. 6 7
      frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/toolbar/board_setting_bar.dart
  20. 13 32
      frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart
  21. 167 0
      frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/unschedule_event_bloc.dart
  22. 2 59
      frontend/appflowy_flutter/lib/plugins/database_view/calendar/calendar.dart
  23. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_day.dart
  24. 52 9
      frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart
  25. 37 16
      frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart
  26. 178 0
      frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_setting_bar.dart
  27. 0 136
      frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_toolbar.dart
  28. 16 13
      frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_accessory_bloc.dart
  29. 45 10
      frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart
  30. 2 59
      frontend/appflowy_flutter/lib/plugins/database_view/grid/grid.dart
  31. 156 131
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart
  32. 0 90
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/accessory_menu.dart
  33. 38 25
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/filter_menu.dart
  34. 17 0
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/footer/grid_footer.dart
  35. 32 21
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/sort_menu.dart
  36. 1 30
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/grid_layout.dart
  37. 68 0
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/grid_setting_bar.dart
  38. 0 41
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/grid_toolbar.dart
  39. 98 0
      frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/setting_menu.dart
  40. 415 0
      frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tab_bar_view.dart
  41. 156 0
      frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tar_bar_add_button.dart
  42. 31 0
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/database_layout_ext.dart
  43. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/database_setting.dart
  44. 63 3
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/setting_button.dart
  45. 1 4
      frontend/appflowy_flutter/lib/plugins/document/document.dart
  46. 49 57
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart
  47. 1 1
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart
  48. 2 14
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart
  49. 11 27
      frontend/appflowy_flutter/lib/plugins/util.dart
  50. 22 22
      frontend/appflowy_flutter/lib/workspace/application/app/app_bloc.dart
  51. 0 61
      frontend/appflowy_flutter/lib/workspace/application/app/app_listener.dart
  52. 0 1
      frontend/appflowy_flutter/lib/workspace/application/app/prelude.dart
  53. 22 29
      frontend/appflowy_flutter/lib/workspace/application/menu/menu_view_section_bloc.dart
  54. 32 15
      frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart
  55. 12 0
      frontend/appflowy_flutter/lib/workspace/application/view/view_listener.dart
  56. 4 4
      frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart
  57. 1 1
      frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/header/header.dart
  58. 4 4
      frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/menu_app.dart
  59. 2 2
      frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/section/item.dart
  60. 4 3
      frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/section/section.dart
  61. 3 0
      frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart
  62. 6 2
      frontend/appflowy_flutter/test/bloc_test/board_test/create_card_test.dart
  63. 13 6
      frontend/appflowy_flutter/test/bloc_test/board_test/create_or_edit_field_test.dart
  64. 5 2
      frontend/appflowy_flutter/test/bloc_test/board_test/group_by_checkbox_field_test.dart
  65. 9 4
      frontend/appflowy_flutter/test/bloc_test/board_test/group_by_multi_select_field_test.dart
  66. 7 2
      frontend/appflowy_flutter/test/bloc_test/board_test/group_by_unsupport_field_test.dart
  67. 2 3
      frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_util.dart
  68. 1 3
      frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart
  69. 14 15
      frontend/appflowy_flutter/test/bloc_test/home_test/app_bloc_test.dart
  70. 4 8
      frontend/appflowy_flutter/test/bloc_test/home_test/create_page_test.dart
  71. 2 2
      frontend/appflowy_flutter/test/bloc_test/home_test/home_bloc_test.dart
  72. 6 7
      frontend/appflowy_flutter/test/bloc_test/home_test/trash_bloc_test.dart
  73. 4 13
      frontend/appflowy_flutter/test/bloc_test/home_test/view_bloc_test.dart
  74. 6 6
      frontend/appflowy_tauri/src-tauri/Cargo.toml
  75. 10 10
      frontend/rust-lib/Cargo.lock
  76. 5 5
      frontend/rust-lib/Cargo.toml
  77. 1 1
      frontend/rust-lib/flowy-core/src/deps_resolve/folder2_deps.rs
  78. 3 0
      frontend/rust-lib/flowy-database2/src/entities/calendar_entities.rs
  79. 3 0
      frontend/rust-lib/flowy-database2/src/entities/database_entities.rs
  80. 14 3
      frontend/rust-lib/flowy-database2/src/manager.rs
  81. 15 8
      frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs
  82. 115 0
      frontend/rust-lib/flowy-database2/src/services/database_view/layout_deps.rs
  83. 6 4
      frontend/rust-lib/flowy-database2/src/services/database_view/mod.rs
  84. 37 51
      frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs
  85. 1 1
      frontend/rust-lib/flowy-database2/src/services/database_view/views.rs
  86. 15 12
      frontend/rust-lib/flowy-database2/src/services/group/configuration.rs
  87. 5 4
      frontend/rust-lib/flowy-database2/src/services/share/csv/export.rs
  88. 10 7
      frontend/rust-lib/flowy-document2/tests/document/document_redo_undo_test.rs
  89. 15 0
      frontend/rust-lib/flowy-folder2/src/entities/view.rs
  90. 61 5
      frontend/rust-lib/flowy-folder2/src/manager.rs
  91. 3 3
      frontend/rust-lib/flowy-folder2/src/notification.rs
  92. 1 1
      frontend/rust-lib/flowy-test/tests/document/utils.rs

+ 1 - 0
frontend/appflowy_flutter/assets/translations/en.json

@@ -236,6 +236,7 @@
     }
   },
   "grid": {
+    "deleteView": "Are you sure you want to delete this view?",
     "settings": {
       "filter": "Filter",
       "sort": "Sort",

+ 78 - 0
frontend/appflowy_flutter/integration_test/database_calendar_test.dart

@@ -0,0 +1,78 @@
+import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
+import 'package:appflowy_backend/protobuf/flowy-folder2/view.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('database', () {
+    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 calendar layout', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      await tester.tapAddButton();
+      await tester.tapCreateCalendarButton();
+
+      // 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.Grid);
+      await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Grid);
+
+      await tester.pumpAndSettle();
+    });
+
+    testWidgets('calendar start from day setting', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      // Create calendar view
+      await tester.createNewPageWithName(ViewLayoutPB.Calendar, 'calendar');
+
+      // Open setting
+      await tester.tapDatabaseSettingButton();
+      await tester.tapCalendarLayoutSettingButton();
+
+      // select the first day of week is Monday
+      await tester.tapFirstDayOfWeek();
+      await tester.tapFirstDayOfWeekStartFromMonday();
+
+      // Open the other page and open the new calendar page again
+      await tester.openPage(readme);
+      await tester.pumpAndSettle(const Duration(milliseconds: 300));
+      await tester.openPage('calendar');
+
+      // Open setting again and check the start from Monday is selected
+      await tester.tapDatabaseSettingButton();
+      await tester.tapCalendarLayoutSettingButton();
+      await tester.tapFirstDayOfWeek();
+      tester.assertFirstDayOfWeekStartFromMonday();
+
+      await tester.pumpAndSettle();
+    });
+  });
+}

+ 96 - 0
frontend/appflowy_flutter/integration_test/database_view_test.dart

@@ -0,0 +1,96 @@
+import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
+import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.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('database', () {
+    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 linked view', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      await tester.tapAddButton();
+      await tester.tapCreateGridButton();
+
+      // Create board view
+      await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board);
+      tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board);
+
+      // Create grid view
+      await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.grid);
+      tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Grid);
+
+      // Create calendar view
+      await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.calendar);
+      tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Calendar);
+
+      await tester.pumpAndSettle();
+    });
+
+    testWidgets('rename and delete linked view', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      await tester.tapAddButton();
+      await tester.tapCreateGridButton();
+
+      // Create board view
+      await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board);
+      tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board);
+
+      // rename board view
+      await tester.renameLinkedView(
+        tester.findTabBarLinkViewByViewLayout(ViewLayoutPB.Board),
+        'new board',
+      );
+      final findBoard = tester.findTabBarLinkViewByViewName('new board');
+      expect(findBoard, findsOneWidget);
+
+      // delete the board
+      await tester.deleteDatebaseView(findBoard);
+      expect(tester.findTabBarLinkViewByViewName('new board'), findsNothing);
+
+      await tester.pumpAndSettle();
+    });
+
+    testWidgets('delete the last database view', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      await tester.tapAddButton();
+      await tester.tapCreateGridButton();
+
+      // Create board view
+      await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board);
+      tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board);
+
+      // delete the board
+      await tester.deleteDatebaseView(
+        tester.findTabBarLinkViewByViewLayout(ViewLayoutPB.Board),
+      );
+
+      await tester.pumpAndSettle();
+    });
+  });
+}

+ 7 - 0
frontend/appflowy_flutter/integration_test/runner.dart

@@ -13,6 +13,8 @@ import 'database_row_page_test.dart' as database_row_page_test;
 import 'database_row_test.dart' as database_row_test;
 import 'database_setting_test.dart' as database_setting_test;
 import 'database_filter_test.dart' as database_filter_test;
+import 'database_view_test.dart' as database_view_test;
+import 'database_calendar_test.dart' as database_calendar_test;
 
 /// The main task runner for all integration tests in AppFlowy.
 ///
@@ -29,6 +31,8 @@ void main() {
   share_markdown_test.main();
   import_files_test.main();
   document_with_database_test.main();
+
+  // Database integration tests
   database_cell_test.main();
   database_field_test.main();
   database_share_test.main();
@@ -36,6 +40,9 @@ void main() {
   database_row_test.main();
   database_setting_test.main();
   database_filter_test.main();
+  database_view_test.main();
+  database_calendar_test.main();
+
   // board_test.main();
   // empty_document_test.main();
   // smart_menu_test.main();

+ 1 - 0
frontend/appflowy_flutter/integration_test/util/base.dart

@@ -87,6 +87,7 @@ extension AppFlowyTestBase on WidgetTester {
   }) async {
     await tap(
       finder,
+      buttons: buttons,
       warnIfMissed: warnIfMissed,
     );
     await pumpAndSettle(Duration(milliseconds: milliseconds));

+ 10 - 1
frontend/appflowy_flutter/integration_test/util/common_operations.dart

@@ -47,6 +47,13 @@ extension CommonOperations on WidgetTester {
     await tapButtonWithName(LocaleKeys.grid_menuName.tr());
   }
 
+  /// Tap the create grid button.
+  ///
+  /// Must call [tapAddButton] first.
+  Future<void> tapCreateCalendarButton() async {
+    await tapButtonWithName(LocaleKeys.calendar_menuName.tr());
+  }
+
   /// Tap the import button.
   ///
   /// Must call [tapAddButton] first.
@@ -142,7 +149,9 @@ extension CommonOperations on WidgetTester {
 
   /// open the page with given name.
   Future<void> openPage(String name) async {
-    await tapButton(findPageName(name));
+    final finder = findPageName(name);
+    expect(finder, findsOneWidget);
+    await tapButton(finder);
   }
 
   /// Tap the ... button beside the page name.

+ 153 - 2
frontend/appflowy_flutter/integration_test/util/database_test_op.dart

@@ -1,10 +1,9 @@
 import 'dart:io';
-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/calendar/presentation/toolbar/calendar_layout_setting.dart';
 import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/checkbox.dart';
 import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/checklist/checklist.dart';
 import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart';
@@ -19,6 +18,9 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/
 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/filter_button.dart';
 import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/grid_layout.dart';
+import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart';
+import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart';
+import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart';
 import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart';
 import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor.dart';
 import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/text_field.dart';
@@ -29,10 +31,13 @@ import 'package:appflowy/plugins/database_view/widgets/setting/database_setting.
 import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart';
 import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
+import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra_ui/style_widget/icon_button.dart';
 import 'package:flowy_infra_ui/style_widget/text_field.dart';
+import 'package:flowy_infra_ui/style_widget/text_input.dart';
 import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
+import 'package:flutter/gestures.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_test/flutter_test.dart';
@@ -728,6 +733,152 @@ extension AppFlowyDatabaseTest on WidgetTester {
     await tapButton(button);
   }
 
+  Future<void> tapCalendarLayoutSettingButton() async {
+    final findSettingItem = find.byType(DatabaseSettingItem);
+    final findLayoutButton = find.byWidgetPredicate(
+      (widget) =>
+          widget is FlowyText &&
+          widget.text == DatabaseSettingAction.showCalendarLayout.title(),
+    );
+
+    final button = find.descendant(
+      of: findSettingItem,
+      matching: findLayoutButton,
+    );
+
+    await tapButton(button);
+  }
+
+  Future<void> tapFirstDayOfWeek() async {
+    await tapButton(find.byType(FirstDayOfWeek));
+  }
+
+  Future<void> tapFirstDayOfWeekStartFromSunday() async {
+    final finder = find.byWidgetPredicate(
+      (widget) => widget is StartFromButton && widget.dayIndex == 0,
+    );
+    await tapButton(finder);
+  }
+
+  Future<void> tapFirstDayOfWeekStartFromMonday() async {
+    final finder = find.byWidgetPredicate(
+      (widget) => widget is StartFromButton && widget.dayIndex == 1,
+    );
+    await tapButton(finder);
+
+    // Dismiss the popover overlay in cause of obscure the tapButton
+    // in the next test case.
+    await sendKeyEvent(LogicalKeyboardKey.escape);
+    await pumpAndSettle(const Duration(milliseconds: 200));
+  }
+
+  void assertFirstDayOfWeekStartFromMonday() {
+    final finder = find.byWidgetPredicate(
+      (widget) =>
+          widget is StartFromButton &&
+          widget.dayIndex == 1 &&
+          widget.isSelected == true,
+    );
+    expect(finder, findsOneWidget);
+  }
+
+  void assertFirstDayOfWeekStartFromSunday() {
+    final finder = find.byWidgetPredicate(
+      (widget) =>
+          widget is StartFromButton &&
+          widget.dayIndex == 0 &&
+          widget.isSelected == true,
+    );
+    expect(finder, findsOneWidget);
+  }
+
+  Future<void> tapCreateLinkedDatabaseViewButton(AddButtonAction action) async {
+    final findAddButton = find.byType(AddDatabaseViewButton);
+    await tapButton(findAddButton);
+
+    final findCreateButton = find.byWidgetPredicate(
+      (widget) =>
+          widget is TarBarAddButtonActionCell && widget.action == action,
+    );
+    await tapButton(findCreateButton);
+  }
+
+  Finder findTabBarLinkViewByViewLayout(ViewLayoutPB layout) {
+    return find.byWidgetPredicate(
+      (widget) => widget is TabBarItemButton && widget.view.layout == layout,
+    );
+  }
+
+  Finder findTabBarLinkViewByViewName(String name) {
+    return find.byWidgetPredicate(
+      (widget) => widget is TabBarItemButton && widget.view.name == name,
+    );
+  }
+
+  Future<void> renameLinkedView(Finder linkedView, String name) async {
+    await tap(linkedView, buttons: kSecondaryButton);
+    await pumpAndSettle();
+
+    await tapButton(
+      find.byWidgetPredicate(
+        (widget) =>
+            widget is ActionCellWidget &&
+            widget.action == TabBarViewAction.rename,
+      ),
+    );
+
+    await enterText(
+      find.descendant(
+        of: find.byType(FlowyFormTextInput),
+        matching: find.byType(TextFormField),
+      ),
+      name,
+    );
+
+    final field = find.byWidgetPredicate(
+      (widget) =>
+          widget is PrimaryTextButton &&
+          widget.label == LocaleKeys.button_OK.tr(),
+    );
+    await tapButton(field);
+  }
+
+  Future<void> deleteDatebaseView(Finder linkedView) async {
+    await tap(linkedView, buttons: kSecondaryButton);
+    await pumpAndSettle();
+
+    await tapButton(
+      find.byWidgetPredicate(
+        (widget) =>
+            widget is ActionCellWidget &&
+            widget.action == TabBarViewAction.delete,
+      ),
+    );
+
+    final okButton = find.byWidgetPredicate(
+      (widget) =>
+          widget is PrimaryTextButton &&
+          widget.label == LocaleKeys.button_OK.tr(),
+    );
+    await tapButton(okButton);
+  }
+
+  Future<void> assertCurrentDatabaseTagIs(DatabaseLayoutPB layout) async {
+    switch (layout) {
+      case DatabaseLayoutPB.Board:
+        expect(find.byType(BoardPage), findsOneWidget);
+        break;
+      case DatabaseLayoutPB.Calendar:
+        expect(find.byType(CalendarPage), findsOneWidget);
+        break;
+      case DatabaseLayoutPB.Grid:
+        expect(find.byType(GridPage), findsOneWidget);
+        break;
+      default:
+        throw Exception('Unknown database layout type: $layout');
+    }
+  }
+
   Future<void> selectDatabaseLayoutType(DatabaseLayoutPB layout) async {
     final findLayoutCell = find.byType(DatabaseViewLayoutCell);
     final findText = find.byWidgetPredicate(

+ 1 - 0
frontend/appflowy_flutter/integration_test/util/expectation.dart

@@ -82,6 +82,7 @@ extension Expectation on WidgetTester {
   Finder findPageName(String name) {
     return find.byWidgetPredicate(
       (widget) => widget is ViewSectionItem && widget.view.name == name,
+      skipOffstage: false,
     );
   }
 }

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

@@ -1,5 +1,4 @@
 import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
-import 'package:appflowy/plugins/database_view/application/layout/calendar_setting_listener.dart';
 import 'package:appflowy/plugins/database_view/application/view/view_cache.dart';
 import 'package:appflowy_backend/log.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart';
@@ -16,6 +15,7 @@ import 'dart:async';
 import 'package:dartz/dartz.dart';
 import 'database_view_service.dart';
 import 'defines.dart';
+import 'layout/layout_service.dart';
 import 'layout/layout_setting_listener.dart';
 import 'row/row_cache.dart';
 import 'group/group_listener.dart';
@@ -50,16 +50,11 @@ class DatabaseLayoutSettingCallbacks {
   });
 }
 
-class CalendarLayoutCallbacks {
-  final void Function(DatabaseLayoutSettingPB) onCalendarLayoutChanged;
-
-  CalendarLayoutCallbacks({required this.onCalendarLayoutChanged});
-}
-
 class DatabaseCallbacks {
   OnDatabaseChanged? onDatabaseChanged;
   OnFieldsChanged? onFieldsChanged;
   OnFiltersChanged? onFiltersChanged;
+  OnSortsChanged? onSortsChanged;
   OnNumOfRowsChanged? onNumOfRowsChanged;
   OnRowsDeleted? onRowsDeleted;
   OnRowsUpdated? onRowsUpdated;
@@ -70,6 +65,7 @@ class DatabaseCallbacks {
     this.onNumOfRowsChanged,
     this.onFieldsChanged,
     this.onFiltersChanged,
+    this.onSortsChanged,
     this.onRowsUpdated,
     this.onRowsDeleted,
     this.onRowsCreated,
@@ -80,15 +76,14 @@ class DatabaseController {
   final String viewId;
   final DatabaseViewBackendService _databaseViewBackendSvc;
   final FieldController fieldController;
-  DatabaseLayoutPB? databaseLayout;
+  DatabaseLayoutPB databaseLayout;
   DatabaseLayoutSettingPB? databaseLayoutSetting;
   late DatabaseViewCache _viewCache;
 
   // Callbacks
-  DatabaseCallbacks? _databaseCallbacks;
-  GroupCallbacks? _groupCallbacks;
-  DatabaseLayoutSettingCallbacks? _layoutCallbacks;
-  CalendarLayoutCallbacks? _calendarLayoutCallbacks;
+  final List<DatabaseCallbacks> _databaseCallbacks = [];
+  final List<GroupCallbacks> _groupCallbacks = [];
+  final List<DatabaseLayoutSettingCallbacks> _layoutCallbacks = [];
 
   // Getters
   RowCache get rowCache => _viewCache.rowCache;
@@ -96,15 +91,14 @@ class DatabaseController {
   // Listener
   final DatabaseGroupListener _groupListener;
   final DatabaseLayoutSettingListener _layoutListener;
-  final DatabaseCalendarLayoutListener _calendarLayoutListener;
 
   DatabaseController({required ViewPB view})
       : viewId = view.id,
         _databaseViewBackendSvc = DatabaseViewBackendService(viewId: view.id),
         fieldController = FieldController(viewId: view.id),
         _groupListener = DatabaseGroupListener(view.id),
-        _layoutListener = DatabaseLayoutSettingListener(view.id),
-        _calendarLayoutListener = DatabaseCalendarLayoutListener(view.id) {
+        databaseLayout = databaseLayoutFromViewLayout(view.layout),
+        _layoutListener = DatabaseLayoutSettingListener(view.id) {
     _viewCache = DatabaseViewCache(
       viewId: viewId,
       fieldController: fieldController,
@@ -115,29 +109,30 @@ class DatabaseController {
     _listenOnLayoutChanged();
   }
 
-  void setListener({
+  void addListener({
     DatabaseCallbacks? onDatabaseChanged,
     DatabaseLayoutSettingCallbacks? onLayoutChanged,
     GroupCallbacks? onGroupChanged,
-    CalendarLayoutCallbacks? onCalendarLayoutChanged,
   }) {
-    _layoutCallbacks = onLayoutChanged;
-    _databaseCallbacks = onDatabaseChanged;
-    _groupCallbacks = onGroupChanged;
-    _calendarLayoutCallbacks = onCalendarLayoutChanged;
+    if (onLayoutChanged != null) {
+      _layoutCallbacks.add(onLayoutChanged);
+    }
+
+    if (onDatabaseChanged != null) {
+      _databaseCallbacks.add(onDatabaseChanged);
+    }
+
+    if (onGroupChanged != null) {
+      _groupCallbacks.add(onGroupChanged);
+    }
   }
 
   Future<Either<Unit, FlowyError>> open() async {
-    return _databaseViewBackendSvc.openGrid().then((result) {
+    return _databaseViewBackendSvc.openDatabase().then((result) {
       return result.fold(
         (DatabasePB database) async {
           databaseLayout = database.layoutType;
 
-          // Listen on layout changed if database layout is calendar
-          if (databaseLayout == DatabaseLayoutPB.Calendar) {
-            _listenOnCalendarLayoutChanged();
-          }
-
           // Load the actual database field data.
           final fieldsOrFail = await fieldController.loadFields(
             fieldIds: database.fields,
@@ -146,7 +141,9 @@ class DatabaseController {
             (fields) {
               // Notify the database is changed after the fields are loaded.
               // The database won't can't be used until the fields are loaded.
-              _databaseCallbacks?.onDatabaseChanged?.call(database);
+              for (final callback in _databaseCallbacks) {
+                callback.onDatabaseChanged?.call(database);
+              }
               _viewCache.rowCache.setInitialRows(database.rows);
               return Future(() async {
                 await _loadGroups();
@@ -217,11 +214,14 @@ class DatabaseController {
     );
   }
 
-  Future<void> updateCalenderLayoutSetting(
-    CalendarLayoutSettingPB layoutSetting,
+  Future<void> updateLayoutSetting(
+    CalendarLayoutSettingPB calendarlLayoutSetting,
   ) async {
     await _databaseViewBackendSvc
-        .updateLayoutSetting(calendarLayoutSetting: layoutSetting)
+        .updateLayoutSetting(
+      calendarLayoutSetting: calendarlLayoutSetting,
+      layoutType: databaseLayout,
+    )
         .then((result) {
       result.fold((l) => null, (r) => Log.error(r));
     });
@@ -232,10 +232,9 @@ class DatabaseController {
     await fieldController.dispose();
     await _groupListener.stop();
     await _viewCache.dispose();
-    _databaseCallbacks = null;
-    _groupCallbacks = null;
-    _layoutCallbacks = null;
-    _calendarLayoutCallbacks = null;
+    _databaseCallbacks.clear();
+    _groupCallbacks.clear();
+    _layoutCallbacks.clear();
   }
 
   Future<void> _loadGroups() async {
@@ -243,7 +242,9 @@ class DatabaseController {
     return Future(
       () => result.fold(
         (groups) {
-          _groupCallbacks?.onGroupByField?.call(groups.items);
+          for (final callback in _groupCallbacks) {
+            callback.onGroupByField?.call(groups.items);
+          }
         },
         (err) => Log.error(err),
       ),
@@ -251,46 +252,63 @@ class DatabaseController {
   }
 
   Future<void> _loadLayoutSetting() async {
-    if (databaseLayout != null) {
-      _databaseViewBackendSvc.getLayoutSetting(databaseLayout!).then((result) {
-        result.fold(
-          (newDatabaseLayoutSetting) {
-            databaseLayoutSetting = newDatabaseLayoutSetting;
-            databaseLayoutSetting?.freeze();
-
-            _layoutCallbacks?.onLoadLayout(newDatabaseLayoutSetting);
-          },
-          (r) => Log.error(r),
-        );
-      });
-    }
+    _databaseViewBackendSvc.getLayoutSetting(databaseLayout).then((result) {
+      result.fold(
+        (newDatabaseLayoutSetting) {
+          databaseLayoutSetting = newDatabaseLayoutSetting;
+          databaseLayoutSetting?.freeze();
+
+          for (final callback in _layoutCallbacks) {
+            callback.onLoadLayout(newDatabaseLayoutSetting);
+          }
+        },
+        (r) => Log.error(r),
+      );
+    });
   }
 
   void _listenOnRowsChanged() {
     final callbacks = DatabaseViewCallbacks(
       onNumOfRowsChanged: (rows, rowByRowId, reason) {
-        _databaseCallbacks?.onNumOfRowsChanged?.call(rows, rowByRowId, reason);
+        for (final callback in _databaseCallbacks) {
+          callback.onNumOfRowsChanged?.call(rows, rowByRowId, reason);
+        }
       },
       onRowsDeleted: (ids) {
-        _databaseCallbacks?.onRowsDeleted?.call(ids);
+        for (final callback in _databaseCallbacks) {
+          callback.onRowsDeleted?.call(ids);
+        }
       },
       onRowsUpdated: (ids, reason) {
-        _databaseCallbacks?.onRowsUpdated?.call(ids, reason);
+        for (final callback in _databaseCallbacks) {
+          callback.onRowsUpdated?.call(ids, reason);
+        }
       },
       onRowsCreated: (ids) {
-        _databaseCallbacks?.onRowsCreated?.call(ids);
+        for (final callback in _databaseCallbacks) {
+          callback.onRowsCreated?.call(ids);
+        }
       },
     );
-    _viewCache.setListener(callbacks);
+    _viewCache.addListener(callbacks);
   }
 
   void _listenOnFieldsChanged() {
     fieldController.addListener(
       onReceiveFields: (fields) {
-        _databaseCallbacks?.onFieldsChanged?.call(UnmodifiableListView(fields));
+        for (final callback in _databaseCallbacks) {
+          callback.onFieldsChanged?.call(UnmodifiableListView(fields));
+        }
+      },
+      onSorts: (sorts) {
+        for (final callback in _databaseCallbacks) {
+          callback.onSortsChanged?.call(sorts);
+        }
       },
       onFilters: (filters) {
-        _databaseCallbacks?.onFiltersChanged?.call(filters);
+        for (final callback in _databaseCallbacks) {
+          callback.onFiltersChanged?.call(filters);
+        }
       },
     );
   }
@@ -301,15 +319,21 @@ class DatabaseController {
         result.fold(
           (changeset) {
             if (changeset.updateGroups.isNotEmpty) {
-              _groupCallbacks?.onUpdateGroup?.call(changeset.updateGroups);
+              for (final callback in _groupCallbacks) {
+                callback.onUpdateGroup?.call(changeset.updateGroups);
+              }
             }
 
             if (changeset.deletedGroups.isNotEmpty) {
-              _groupCallbacks?.onDeleteGroup?.call(changeset.deletedGroups);
+              for (final callback in _groupCallbacks) {
+                callback.onDeleteGroup?.call(changeset.deletedGroups);
+              }
             }
 
             for (final insertedGroup in changeset.insertedGroups) {
-              _groupCallbacks?.onInsertGroup?.call(insertedGroup);
+              for (final callback in _groupCallbacks) {
+                callback.onInsertGroup?.call(insertedGroup);
+              }
             }
           },
           (r) => Log.error(r),
@@ -318,7 +342,9 @@ class DatabaseController {
       onGroupByNewField: (result) {
         result.fold(
           (groups) {
-            _groupCallbacks?.onGroupByField?.call(groups);
+            for (final callback in _groupCallbacks) {
+              callback.onGroupByField?.call(groups);
+            }
           },
           (r) => Log.error(r),
         );
@@ -330,24 +356,13 @@ class DatabaseController {
     _layoutListener.start(
       onLayoutChanged: (result) {
         result.fold(
-          (newDatabaseLayoutSetting) {
-            databaseLayoutSetting = newDatabaseLayoutSetting;
+          (newLayout) {
+            databaseLayoutSetting = newLayout;
             databaseLayoutSetting?.freeze();
 
-            _layoutCallbacks?.onLayoutChanged(newDatabaseLayoutSetting);
-          },
-          (r) => Log.error(r),
-        );
-      },
-    );
-  }
-
-  void _listenOnCalendarLayoutChanged() {
-    _calendarLayoutListener.start(
-      onCalendarLayoutChanged: (result) {
-        result.fold(
-          (l) {
-            _calendarLayoutCallbacks?.onCalendarLayoutChanged(l);
+            for (final callback in _layoutCallbacks) {
+              callback.onLayoutChanged(newLayout);
+            }
           },
           (r) => Log.error(r),
         );

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

@@ -25,7 +25,7 @@ class DatabaseViewBackendService {
         .then((value) => value.leftMap((l) => l.value));
   }
 
-  Future<Either<DatabasePB, FlowyError>> openGrid() async {
+  Future<Either<DatabasePB, FlowyError>> openDatabase() async {
     final payload = DatabaseViewIdPB(value: viewId);
     return DatabaseEventGetDatabase(payload).send();
   }
@@ -113,9 +113,12 @@ class DatabaseViewBackendService {
   }
 
   Future<Either<Unit, FlowyError>> updateLayoutSetting({
+    required DatabaseLayoutPB layoutType,
     CalendarLayoutSettingPB? calendarLayoutSetting,
   }) {
-    final payload = LayoutSettingChangesetPB.create()..viewId = viewId;
+    final payload = LayoutSettingChangesetPB.create()
+      ..viewId = viewId
+      ..layoutType = layoutType;
     if (calendarLayoutSetting != null) {
       payload.calendar = calendarLayoutSetting;
     }

+ 2 - 0
frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart

@@ -1,5 +1,6 @@
 import 'dart:collection';
 
+import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_info.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
 
@@ -10,6 +11,7 @@ import 'row/row_service.dart';
 
 typedef OnFieldsChanged = void Function(UnmodifiableListView<FieldInfo>);
 typedef OnFiltersChanged = void Function(List<FilterInfo>);
+typedef OnSortsChanged = void Function(List<SortInfo>);
 typedef OnDatabaseChanged = void Function(DatabasePB);
 
 typedef OnRowsCreated = void Function(List<RowId> ids);

+ 15 - 2
frontend/appflowy_flutter/lib/plugins/database_view/application/layout/layout_service.dart

@@ -15,13 +15,13 @@ class DatabaseLayoutBackendService {
   }) {
     final payload = UpdateViewPayloadPB.create()
       ..viewId = viewId
-      ..layout = _viewLayoutFromDatabaseLayout(layout);
+      ..layout = viewLayoutFromDatabaseLayout(layout);
 
     return FolderEventUpdateView(payload).send();
   }
 }
 
-ViewLayoutPB _viewLayoutFromDatabaseLayout(DatabaseLayoutPB databaseLayout) {
+ViewLayoutPB viewLayoutFromDatabaseLayout(DatabaseLayoutPB databaseLayout) {
   switch (databaseLayout) {
     case DatabaseLayoutPB.Board:
       return ViewLayoutPB.Board;
@@ -33,3 +33,16 @@ ViewLayoutPB _viewLayoutFromDatabaseLayout(DatabaseLayoutPB databaseLayout) {
       throw UnimplementedError;
   }
 }
+
+DatabaseLayoutPB databaseLayoutFromViewLayout(ViewLayoutPB viewLayout) {
+  switch (viewLayout) {
+    case ViewLayoutPB.Board:
+      return DatabaseLayoutPB.Board;
+    case ViewLayoutPB.Calendar:
+      return DatabaseLayoutPB.Calendar;
+    case ViewLayoutPB.Grid:
+      return DatabaseLayoutPB.Grid;
+    default:
+      throw UnimplementedError;
+  }
+}

+ 0 - 103
frontend/appflowy_flutter/lib/plugins/database_view/application/setting/setting_bloc.dart

@@ -1,103 +0,0 @@
-import 'package:appflowy/generated/locale_keys.g.dart';
-import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
-import 'package:easy_localization/easy_localization.dart';
-import 'package:flutter_bloc/flutter_bloc.dart';
-import 'package:freezed_annotation/freezed_annotation.dart';
-import 'package:dartz/dartz.dart';
-
-part 'setting_bloc.freezed.dart';
-
-class DatabaseSettingBloc
-    extends Bloc<DatabaseSettingEvent, DatabaseSettingState> {
-  final String viewId;
-  DatabaseSettingBloc({required this.viewId})
-      : super(DatabaseSettingState.initial()) {
-    on<DatabaseSettingEvent>(
-      (event, emit) async {
-        event.map(
-          performAction: (_PerformAction value) {
-            emit(state.copyWith(selectedAction: Some(value.action)));
-          },
-        );
-      },
-    );
-  }
-}
-
-@freezed
-class DatabaseSettingEvent with _$DatabaseSettingEvent {
-  const factory DatabaseSettingEvent.performAction(
-    DatabaseSettingAction action,
-  ) = _PerformAction;
-}
-
-@freezed
-class DatabaseSettingState with _$DatabaseSettingState {
-  const factory DatabaseSettingState({
-    required Option<DatabaseSettingAction> selectedAction,
-  }) = _DatabaseSettingState;
-
-  factory DatabaseSettingState.initial() => DatabaseSettingState(
-        selectedAction: none(),
-      );
-}
-
-enum DatabaseSettingAction {
-  showProperties,
-  showLayout,
-  showGroup,
-  showCalendarLayout,
-}
-
-extension DatabaseSettingActionExtension on DatabaseSettingAction {
-  String iconName() {
-    switch (this) {
-      case DatabaseSettingAction.showProperties:
-        return 'grid/setting/properties';
-      case DatabaseSettingAction.showLayout:
-        return 'grid/setting/database_layout';
-      case DatabaseSettingAction.showGroup:
-        return 'grid/setting/group';
-      case DatabaseSettingAction.showCalendarLayout:
-        return 'grid/setting/calendar_layout';
-    }
-  }
-
-  String title() {
-    switch (this) {
-      case DatabaseSettingAction.showProperties:
-        return LocaleKeys.grid_settings_Properties.tr();
-      case DatabaseSettingAction.showLayout:
-        return LocaleKeys.grid_settings_databaseLayout.tr();
-      case DatabaseSettingAction.showGroup:
-        return LocaleKeys.grid_settings_group.tr();
-      case DatabaseSettingAction.showCalendarLayout:
-        return LocaleKeys.calendar_settings_name.tr();
-    }
-  }
-}
-
-/// Returns the list of actions that should be shown for the given database layout.
-List<DatabaseSettingAction> actionsForDatabaseLayout(DatabaseLayoutPB? layout) {
-  switch (layout) {
-    case DatabaseLayoutPB.Board:
-      return [
-        DatabaseSettingAction.showProperties,
-        DatabaseSettingAction.showLayout,
-        DatabaseSettingAction.showGroup,
-      ];
-    case DatabaseLayoutPB.Calendar:
-      return [
-        DatabaseSettingAction.showProperties,
-        DatabaseSettingAction.showLayout,
-        DatabaseSettingAction.showCalendarLayout,
-      ];
-    case DatabaseLayoutPB.Grid:
-      return [
-        DatabaseSettingAction.showProperties,
-        DatabaseSettingAction.showLayout,
-      ];
-    default:
-      return [];
-  }
-}

+ 290 - 0
frontend/appflowy_flutter/lib/plugins/database_view/application/tar_bar_bloc.dart

@@ -0,0 +1,290 @@
+import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart';
+import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart';
+import 'package:appflowy/workspace/application/view/prelude.dart';
+import 'package:appflowy/workspace/application/view/view_ext.dart';
+import 'package:appflowy_backend/log.dart';
+import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
+import 'package:equatable/equatable.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+import 'database_controller.dart';
+import 'database_view_service.dart';
+
+part 'tar_bar_bloc.freezed.dart';
+
+class GridTabBarBloc extends Bloc<GridTabBarEvent, GridTabBarState> {
+  GridTabBarBloc({
+    bool isInlineView = false,
+    required ViewPB view,
+  }) : super(GridTabBarState.initial(view)) {
+    on<GridTabBarEvent>(
+      (event, emit) async {
+        event.when(
+          initial: () {
+            _listenInlineViewChanged();
+            _loadChildView();
+          },
+          didLoadChildViews: (List<ViewPB> childViews) {
+            emit(
+              state.copyWith(
+                tabBars: [
+                  ...state.tabBars,
+                  ...childViews.map(
+                    (newChildView) => TarBar(view: newChildView),
+                  ),
+                ],
+                tabBarControllerByViewId: _extendsTabBarController(childViews),
+              ),
+            );
+          },
+          selectView: (String viewId) {
+            final index =
+                state.tabBars.indexWhere((element) => element.viewId == viewId);
+            if (index != -1) {
+              emit(
+                state.copyWith(selectedIndex: index),
+              );
+            }
+          },
+          createView: (action) {
+            _createLinkedView(action.name, action.layoutType);
+          },
+          deleteView: (String viewId) async {
+            final result = await ViewBackendService.delete(viewId: viewId);
+            result.fold(
+              (l) {},
+              (r) => Log.error(r),
+            );
+          },
+          renameView: (String viewId, String newName) {
+            ViewBackendService.updateView(viewId: viewId, name: newName);
+          },
+          didUpdateChildViews: (updatePB) async {
+            if (updatePB.createChildViews.isNotEmpty) {
+              final allTabBars = [
+                ...state.tabBars,
+                ...updatePB.createChildViews.map((e) => TarBar(view: e))
+              ];
+              emit(
+                state.copyWith(
+                  tabBars: allTabBars,
+                  selectedIndex: state.tabBars.length,
+                  tabBarControllerByViewId:
+                      _extendsTabBarController(updatePB.createChildViews),
+                ),
+              );
+            }
+
+            if (updatePB.deleteChildViews.isNotEmpty) {
+              final allTabBars = [...state.tabBars];
+              final tabBarControllerByViewId = {
+                ...state.tabBarControllerByViewId
+              };
+              var newSelectedIndex = state.selectedIndex;
+              for (final viewId in updatePB.deleteChildViews) {
+                final index = allTabBars.indexWhere(
+                  (element) => element.viewId == viewId,
+                );
+                if (index != -1) {
+                  final tarBar = allTabBars.removeAt(index);
+                  // Dispose the controller when the tab is removed.
+                  final controller =
+                      tabBarControllerByViewId.remove(tarBar.viewId);
+                  controller?.dispose();
+                }
+
+                if (index == state.selectedIndex) {
+                  if (index > 0 && allTabBars.isNotEmpty) {
+                    newSelectedIndex = index - 1;
+                  }
+                }
+              }
+              emit(
+                state.copyWith(
+                  tabBars: allTabBars,
+                  selectedIndex: newSelectedIndex,
+                  tabBarControllerByViewId: tabBarControllerByViewId,
+                ),
+              );
+            }
+          },
+          viewDidUpdate: (ViewPB updatedView) {
+            final index = state.tabBars.indexWhere(
+              (element) => element.viewId == updatedView.id,
+            );
+            if (index != -1) {
+              final allTabBars = [...state.tabBars];
+              final updatedTabBar = TarBar(view: updatedView);
+              allTabBars[index] = updatedTabBar;
+              emit(state.copyWith(tabBars: allTabBars));
+            }
+          },
+        );
+      },
+    );
+  }
+
+  @override
+  Future<void> close() async {
+    for (final tabBar in state.tabBars) {
+      await state.tabBarControllerByViewId[tabBar.viewId]?.dispose();
+    }
+    return super.close();
+  }
+
+  void _listenInlineViewChanged() {
+    final controller = state.tabBarControllerByViewId[state.parentView.id];
+    controller?.onViewUpdated = (newView) {
+      add(GridTabBarEvent.viewDidUpdate(newView));
+    };
+
+    // Only listen the child view changes when the parent view is inline.
+    controller?.onViewChildViewChanged = (update) {
+      add(GridTabBarEvent.didUpdateChildViews(update));
+    };
+  }
+
+  /// Create tab bar controllers for the new views and return the updated map.
+  Map<String, DatabaseTarBarController> _extendsTabBarController(
+    List<ViewPB> newViews,
+  ) {
+    final tabBarControllerByViewId = {...state.tabBarControllerByViewId};
+    for (final view in newViews) {
+      final controller = DatabaseTarBarController(view: view);
+      controller.onViewUpdated = (newView) {
+        add(GridTabBarEvent.viewDidUpdate(newView));
+      };
+
+      tabBarControllerByViewId[view.id] = controller;
+    }
+    return tabBarControllerByViewId;
+  }
+
+  Future<void> _createLinkedView(String name, ViewLayoutPB layoutType) async {
+    final viewId = state.parentView.id;
+    final databaseIdOrError =
+        await DatabaseViewBackendService(viewId: viewId).getDatabaseId();
+    databaseIdOrError.fold(
+      (databaseId) async {
+        final linkedViewOrError =
+            await ViewBackendService.createDatabaseLinkedView(
+          parentViewId: viewId,
+          databaseId: databaseId,
+          layoutType: layoutType,
+          name: name,
+        );
+
+        linkedViewOrError.fold(
+          (linkedView) {},
+          (err) => Log.error(err),
+        );
+      },
+      (r) => Log.error(r),
+    );
+  }
+
+  Future<void> _loadChildView() async {
+    ViewBackendService.getChildViews(viewId: state.parentView.id)
+        .then((viewsOrFail) {
+      if (isClosed) {
+        return;
+      }
+      viewsOrFail.fold(
+        (views) => add(GridTabBarEvent.didLoadChildViews(views)),
+        (err) => Log.error(err),
+      );
+    });
+  }
+}
+
+@freezed
+class GridTabBarEvent with _$GridTabBarEvent {
+  const factory GridTabBarEvent.initial() = _Initial;
+  const factory GridTabBarEvent.didLoadChildViews(
+    List<ViewPB> childViews,
+  ) = _DidLoadChildViews;
+  const factory GridTabBarEvent.selectView(String viewId) = _DidSelectView;
+  const factory GridTabBarEvent.createView(AddButtonAction action) =
+      _CreateView;
+  const factory GridTabBarEvent.renameView(String viewId, String newName) =
+      _RenameView;
+  const factory GridTabBarEvent.deleteView(String viewId) = _DeleteView;
+  const factory GridTabBarEvent.didUpdateChildViews(
+    ChildViewUpdatePB updatePB,
+  ) = _DidUpdateChildViews;
+  const factory GridTabBarEvent.viewDidUpdate(ViewPB view) = _ViewDidUpdate;
+}
+
+@freezed
+class GridTabBarState with _$GridTabBarState {
+  const factory GridTabBarState({
+    required ViewPB parentView,
+    required int selectedIndex,
+    required List<TarBar> tabBars,
+    required Map<String, DatabaseTarBarController> tabBarControllerByViewId,
+  }) = _GridTabBarState;
+
+  factory GridTabBarState.initial(ViewPB view) {
+    final tabBar = TarBar(view: view);
+    return GridTabBarState(
+      parentView: view,
+      selectedIndex: 0,
+      tabBars: [tabBar],
+      tabBarControllerByViewId: {
+        view.id: DatabaseTarBarController(
+          view: view,
+        )
+      },
+    );
+  }
+}
+
+class TarBar extends Equatable {
+  final ViewPB view;
+  final DatabaseTabBarItemBuilder _builder;
+
+  String get viewId => view.id;
+  DatabaseTabBarItemBuilder get builder => _builder;
+  ViewLayoutPB get layout => view.layout;
+
+  TarBar({
+    required this.view,
+  }) : _builder = view.tarBarItem();
+
+  @override
+  List<Object?> get props => [view.hashCode];
+}
+
+typedef OnViewUpdated = void Function(ViewPB newView);
+typedef OnViewChildViewChanged = void Function(
+  ChildViewUpdatePB childViewUpdate,
+);
+
+class DatabaseTarBarController {
+  ViewPB view;
+  final DatabaseController controller;
+  final ViewListener viewListener;
+  OnViewUpdated? onViewUpdated;
+  OnViewChildViewChanged? onViewChildViewChanged;
+
+  DatabaseTarBarController({
+    required this.view,
+  })  : controller = DatabaseController(view: view),
+        viewListener = ViewListener(viewId: view.id) {
+    viewListener.start(
+      onViewChildViewsUpdated: (update) {
+        onViewChildViewChanged?.call(update);
+      },
+      onViewUpdated: (newView) {
+        view = newView;
+        onViewUpdated?.call(newView);
+      },
+    );
+  }
+
+  Future<void> dispose() async {
+    await viewListener.stop();
+    await controller.dispose();
+  }
+}

+ 29 - 19
frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_cache.dart

@@ -35,7 +35,7 @@ class DatabaseViewCache {
   final String viewId;
   late RowCache _rowCache;
   final DatabaseViewListener _databaseViewListener;
-  DatabaseViewCallbacks? _callbacks;
+  final List<DatabaseViewCallbacks> _callbacks = [];
 
   UnmodifiableListView<RowInfo> get rowInfos => _rowCache.rowInfos;
   RowCache get rowCache => _rowCache;
@@ -61,22 +61,28 @@ class DatabaseViewCache {
             _rowCache.applyRowsChanged(changeset);
 
             if (changeset.deletedRows.isNotEmpty) {
-              _callbacks?.onRowsDeleted?.call(changeset.deletedRows);
+              for (final callback in _callbacks) {
+                callback.onRowsDeleted?.call(changeset.deletedRows);
+              }
             }
 
             if (changeset.updatedRows.isNotEmpty) {
-              _callbacks?.onRowsUpdated?.call(
-                changeset.updatedRows.map((e) => e.rowId).toList(),
-                _rowCache.changeReason,
-              );
+              for (final callback in _callbacks) {
+                callback.onRowsUpdated?.call(
+                  changeset.updatedRows.map((e) => e.rowId).toList(),
+                  _rowCache.changeReason,
+                );
+              }
             }
 
             if (changeset.insertedRows.isNotEmpty) {
-              _callbacks?.onRowsCreated?.call(
-                changeset.insertedRows
-                    .map((insertedRow) => insertedRow.rowMeta.id)
-                    .toList(),
-              );
+              for (final callback in _callbacks) {
+                callback.onRowsCreated?.call(
+                  changeset.insertedRows
+                      .map((insertedRow) => insertedRow.rowMeta.id)
+                      .toList(),
+                );
+              }
             }
           },
           (err) => Log.error(err),
@@ -103,21 +109,25 @@ class DatabaseViewCache {
     );
 
     _rowCache.onRowsChanged(
-      (reason) => _callbacks?.onNumOfRowsChanged?.call(
-        rowInfos,
-        _rowCache.rowByRowId,
-        reason,
-      ),
+      (reason) {
+        for (final callback in _callbacks) {
+          callback.onNumOfRowsChanged?.call(
+            rowInfos,
+            _rowCache.rowByRowId,
+            reason,
+          );
+        }
+      },
     );
   }
 
   Future<void> dispose() async {
     await _databaseViewListener.stop();
     await _rowCache.dispose();
-    _callbacks = null;
+    _callbacks.clear();
   }
 
-  void setListener(DatabaseViewCallbacks callbacks) {
-    _callbacks = callbacks;
+  void addListener(DatabaseViewCallbacks callbacks) {
+    _callbacks.add(callbacks);
   }
 }

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

@@ -28,9 +28,10 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
   FieldController get fieldController => databaseController.fieldController;
   String get viewId => databaseController.viewId;
 
-  BoardBloc({required ViewPB view})
-      : databaseController = DatabaseController(view: view),
-        super(BoardState.initial(view.id)) {
+  BoardBloc({
+    required ViewPB view,
+    required this.databaseController,
+  }) : super(BoardState.initial(view.id)) {
     boardController = AppFlowyBoardController(
       onMoveGroup: (
         fromGroupId,
@@ -166,7 +167,6 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
 
   @override
   Future<void> close() async {
-    await databaseController.dispose();
     for (final controller in groupControllers.values) {
       controller.dispose();
     }
@@ -233,7 +233,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
       },
     );
 
-    databaseController.setListener(
+    databaseController.addListener(
       onDatabaseChanged: onDatabaseChanged,
       onGroupChanged: onGroupChanged,
     );

+ 2 - 59
frontend/appflowy_flutter/lib/plugins/database_view/board/board.dart

@@ -1,19 +1,14 @@
 import 'package:appflowy/generated/locale_keys.g.dart';
-import 'package:appflowy/plugins/util.dart';
+import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart';
 import 'package:appflowy/startup/plugin/plugin.dart';
-import 'package:appflowy/workspace/presentation/home/home_stack.dart';
-import 'package:appflowy/workspace/presentation/widgets/left_bar_item.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
 import 'package:easy_localization/easy_localization.dart';
-import 'package:flutter/material.dart';
-
-import 'presentation/board_page.dart';
 
 class BoardPluginBuilder implements PluginBuilder {
   @override
   Plugin build(dynamic data) {
     if (data is ViewPB) {
-      return BoardPlugin(pluginType: pluginType, view: data);
+      return DatabaseTabBarViewPlugin(pluginType: pluginType, view: data);
     } else {
       throw FlowyPluginException.invalidData;
     }
@@ -36,55 +31,3 @@ class BoardPluginConfig implements PluginConfig {
   @override
   bool get creatable => true;
 }
-
-class BoardPlugin extends Plugin {
-  @override
-  final ViewPluginNotifier notifier;
-  final PluginType _pluginType;
-
-  BoardPlugin({
-    required ViewPB view,
-    required PluginType pluginType,
-    bool listenOnViewChanged = false,
-  })  : _pluginType = pluginType,
-        notifier = ViewPluginNotifier(
-          view: view,
-          listenOnViewChanged: listenOnViewChanged,
-        );
-
-  @override
-  PluginWidgetBuilder get widgetBuilder =>
-      BoardPluginWidgetBuilder(notifier: notifier);
-
-  @override
-  PluginId get id => notifier.view.id;
-
-  @override
-  PluginType get pluginType => _pluginType;
-}
-
-class BoardPluginWidgetBuilder extends PluginWidgetBuilder {
-  final ViewPluginNotifier notifier;
-  BoardPluginWidgetBuilder({required this.notifier, Key? key});
-
-  ViewPB get view => notifier.view;
-
-  @override
-  Widget get leftBarItem => ViewLeftBarItem(view: view);
-
-  @override
-  Widget buildWidget({PluginContext? context}) {
-    notifier.isDeleted.addListener(() {
-      notifier.isDeleted.value.fold(() => null, (deletedView) {
-        if (deletedView.hasIndex()) {
-          context?.onDeleted(view, deletedView.index);
-        }
-      });
-    });
-
-    return BoardPage(key: ValueKey(view.id), view: view);
-  }
-
-  @override
-  List<NavigationItem> get navigationItems => [this];
-}

+ 59 - 36
frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart

@@ -3,9 +3,11 @@
 import 'dart:collection';
 
 import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/database_view/application/database_controller.dart';
 import 'package:appflowy/plugins/database_view/application/field/field_controller.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/tar_bar/tab_bar_view.dart';
 import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
@@ -24,11 +26,48 @@ import '../../widgets/card/card_cell_builder.dart';
 import '../../widgets/row/cell_builder.dart';
 import '../application/board_bloc.dart';
 import '../../widgets/card/card.dart';
-import 'toolbar/board_toolbar.dart';
+import 'toolbar/board_setting_bar.dart';
+
+class BoardPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
+  @override
+  Widget content(
+    BuildContext context,
+    ViewPB view,
+    DatabaseController controller,
+  ) {
+    return BoardPage(
+      key: _makeValueKey(controller),
+      view: view,
+      databaseController: controller,
+    );
+  }
+
+  @override
+  Widget settingBar(BuildContext context, DatabaseController controller) {
+    return BoardSettingBar(
+      key: _makeValueKey(controller),
+      databaseController: controller,
+    );
+  }
+
+  @override
+  Widget settingBarExtension(
+    BuildContext context,
+    DatabaseController controller,
+  ) {
+    return SizedBox.fromSize();
+  }
+
+  ValueKey _makeValueKey(DatabaseController controller) {
+    return ValueKey(controller.viewId);
+  }
+}
 
 class BoardPage extends StatelessWidget {
+  final DatabaseController databaseController;
   BoardPage({
     required this.view,
+    required this.databaseController,
     Key? key,
     this.onEditStateChanged,
   }) : super(key: ValueKey(view.id));
@@ -41,8 +80,10 @@ class BoardPage extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     return BlocProvider(
-      create: (context) =>
-          BoardBloc(view: view)..add(const BoardEvent.initial()),
+      create: (context) => BoardBloc(
+        view: view,
+        databaseController: databaseController,
+      )..add(const BoardEvent.initial()),
       child: BlocBuilder<BoardBloc, BoardState>(
         buildWhen: (p, c) => p.loadingState != c.loadingState,
         builder: (context, state) {
@@ -110,14 +151,9 @@ class _BoardContentState extends State<BoardContent> {
       child: BlocBuilder<BoardBloc, BoardState>(
         buildWhen: (previous, current) => previous.groupIds != current.groupIds,
         builder: (context, state) {
-          final column = Column(
-            crossAxisAlignment: CrossAxisAlignment.start,
-            children: [const _ToolbarBlocAdaptor(), _buildBoard(context)],
-          );
-
           return Padding(
             padding: const EdgeInsets.symmetric(horizontal: 20),
-            child: column,
+            child: _buildBoard(context),
           );
         },
       ),
@@ -125,22 +161,20 @@ class _BoardContentState extends State<BoardContent> {
   }
 
   Widget _buildBoard(BuildContext context) {
-    return Expanded(
-      child: AppFlowyBoard(
-        boardScrollController: scrollManager,
-        scrollController: ScrollController(),
-        controller: context.read<BoardBloc>().boardController,
-        headerBuilder: _buildHeader,
-        footerBuilder: _buildFooter,
-        cardBuilder: (_, column, columnItem) => _buildCard(
-          context,
-          column,
-          columnItem,
-        ),
-        groupConstraints: const BoxConstraints.tightFor(width: 300),
-        config: AppFlowyBoardConfig(
-          groupBackgroundColor: Theme.of(context).colorScheme.surfaceVariant,
-        ),
+    return AppFlowyBoard(
+      boardScrollController: scrollManager,
+      scrollController: ScrollController(),
+      controller: context.read<BoardBloc>().boardController,
+      headerBuilder: _buildHeader,
+      footerBuilder: _buildFooter,
+      cardBuilder: (_, column, columnItem) => _buildCard(
+        context,
+        column,
+        columnItem,
+      ),
+      groupConstraints: const BoxConstraints.tightFor(width: 300),
+      config: AppFlowyBoardConfig(
+        groupBackgroundColor: Theme.of(context).colorScheme.surfaceVariant,
       ),
     );
   }
@@ -335,17 +369,6 @@ class _BoardContentState extends State<BoardContent> {
   }
 }
 
-class _ToolbarBlocAdaptor extends StatelessWidget {
-  const _ToolbarBlocAdaptor({Key? key}) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return BlocBuilder<BoardBloc, BoardState>(
-      builder: (context, state) => const BoardToolbar(),
-    );
-  }
-}
-
 Widget? _buildHeaderIcon(GroupData customData) {
   Widget? widget;
   switch (customData.fieldType) {

+ 6 - 7
frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/toolbar/board_toolbar.dart → frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/toolbar/board_setting_bar.dart

@@ -1,10 +1,11 @@
-import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart';
+import 'package:appflowy/plugins/database_view/application/database_controller.dart';
 import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter_bloc/flutter_bloc.dart';
 
-class BoardToolbar extends StatelessWidget {
-  const BoardToolbar({
+class BoardSettingBar extends StatelessWidget {
+  final DatabaseController databaseController;
+  const BoardSettingBar({
+    required this.databaseController,
     Key? key,
   }) : super(key: key);
 
@@ -15,9 +16,7 @@ class BoardToolbar extends StatelessWidget {
       child: Row(
         children: [
           const Spacer(),
-          SettingButton(
-            databaseController: context.read<BoardBloc>().databaseController,
-          ),
+          SettingButton(databaseController: databaseController),
         ],
       ),
     );

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

@@ -27,9 +27,8 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
   CellCache get cellCache => databaseController.rowCache.cellCache;
   RowCache get rowCache => databaseController.rowCache;
 
-  CalendarBloc({required ViewPB view})
-      : databaseController = DatabaseController(view: view),
-        super(CalendarState.initial()) {
+  CalendarBloc({required ViewPB view, required this.databaseController})
+      : super(CalendarState.initial()) {
     on<CalendarEvent>(
       (event, emit) async {
         await event.when(
@@ -39,6 +38,12 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
             _loadAllEvents();
           },
           didReceiveCalendarSettings: (CalendarLayoutSettingPB settings) {
+            // If the field id changed, reload all events
+            state.settings.fold(() => null, (oldSetting) {
+              if (oldSetting.fieldId != settings.fieldId) {
+                _loadAllEvents();
+              }
+            });
             emit(state.copyWith(settings: Some(settings)));
           },
           didReceiveDatabaseUpdate: (DatabasePB database) {
@@ -53,10 +58,6 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
               ),
             );
           },
-          didReceiveNewLayoutField: (CalendarLayoutSettingPB layoutSettings) {
-            _loadAllEvents();
-            emit(state.copyWith(settings: Some(layoutSettings)));
-          },
           createEvent: (DateTime date, String title) async {
             await _createEvent(date, title);
           },
@@ -105,12 +106,6 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
     );
   }
 
-  @override
-  Future<void> close() async {
-    await databaseController.dispose();
-    return super.close();
-  }
-
   FieldInfo? _getCalendarFieldInfo(String fieldId) {
     final fieldInfos = databaseController.fieldController.fieldInfos;
     final index = fieldInfos.indexWhere(
@@ -149,7 +144,9 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
 
   Future<void> _createEvent(DateTime date, String title) async {
     return state.settings.fold(
-      () => null,
+      () {
+        Log.warn('Calendar settings not found');
+      },
       (settings) async {
         final dateField = _getCalendarFieldInfo(settings.fieldId);
         final titleField = _getTitleFieldInfo();
@@ -207,7 +204,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
   Future<void> _updateCalendarLayoutSetting(
     CalendarLayoutSettingPB layoutSetting,
   ) async {
-    return databaseController.updateCalenderLayoutSetting(layoutSetting);
+    return databaseController.updateLayoutSetting(layoutSetting);
   }
 
   Future<CalendarEventData<CalendarDayEvent>?> _loadEvent(RowId rowId) async {
@@ -333,14 +330,9 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
       onLoadLayout: _didReceiveLayoutSetting,
     );
 
-    final onCalendarLayoutFieldChanged = CalendarLayoutCallbacks(
-      onCalendarLayoutChanged: _didReceiveNewLayoutField,
-    );
-
-    databaseController.setListener(
+    databaseController.addListener(
       onDatabaseChanged: onDatabaseChanged,
       onLayoutChanged: onLayoutChanged,
-      onCalendarLayoutChanged: onCalendarLayoutFieldChanged,
     );
   }
 
@@ -353,13 +345,6 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
     }
   }
 
-  void _didReceiveNewLayoutField(DatabaseLayoutSettingPB layoutSetting) {
-    if (layoutSetting.hasCalendar()) {
-      if (isClosed) return;
-      add(CalendarEvent.didReceiveNewLayoutField(layoutSetting.calendar));
-    }
-  }
-
   bool isEventDayChanged(CalendarEventData<CalendarDayEvent> event) {
     final index = state.allEvents.indexWhere(
       (element) => element.event!.eventId == event.event!.eventId,
@@ -426,10 +411,6 @@ class CalendarEvent with _$CalendarEvent {
 
   const factory CalendarEvent.didReceiveDatabaseUpdate(DatabasePB database) =
       _ReceiveDatabaseUpdate;
-
-  const factory CalendarEvent.didReceiveNewLayoutField(
-    CalendarLayoutSettingPB layoutSettings,
-  ) = _DidReceiveNewLayoutField;
 }
 
 @freezed

+ 167 - 0
frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/unschedule_event_bloc.dart

@@ -0,0 +1,167 @@
+import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
+import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
+import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
+import 'package:appflowy_backend/dispatch/dispatch.dart';
+import 'package:appflowy_backend/log.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
+import 'package:dartz/dartz.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+import '../../application/database_controller.dart';
+import '../../application/row/row_cache.dart';
+
+part 'unschedule_event_bloc.freezed.dart';
+
+class UnscheduleEventsBloc
+    extends Bloc<UnscheduleEventsEvent, UnscheduleEventsState> {
+  final DatabaseController databaseController;
+  Map<String, FieldInfo> fieldInfoByFieldId = {};
+
+  // Getters
+  String get viewId => databaseController.viewId;
+  FieldController get fieldController => databaseController.fieldController;
+  CellCache get cellCache => databaseController.rowCache.cellCache;
+  RowCache get rowCache => databaseController.rowCache;
+
+  UnscheduleEventsBloc({
+    required this.databaseController,
+  }) : super(UnscheduleEventsState.initial()) {
+    on<UnscheduleEventsEvent>(
+      (event, emit) async {
+        await event.when(
+          initial: () async {
+            _startListening();
+            _loadAllEvents();
+          },
+          didLoadAllEvents: (events) {
+            emit(
+              state.copyWith(
+                allEvents: events,
+                unscheduleEvents:
+                    events.where((element) => !element.isScheduled).toList(),
+              ),
+            );
+          },
+          didDeleteEvents: (List<RowId> deletedRowIds) {
+            final events = [...state.allEvents];
+            events.retainWhere(
+              (element) => !deletedRowIds.contains(element.rowMeta.id),
+            );
+            emit(
+              state.copyWith(
+                allEvents: events,
+                unscheduleEvents:
+                    events.where((element) => !element.isScheduled).toList(),
+              ),
+            );
+          },
+          didReceiveEvent: (CalendarEventPB event) {
+            emit(
+              state.copyWith(
+                allEvents: [...state.allEvents, event],
+              ),
+            );
+          },
+        );
+      },
+    );
+  }
+
+  Future<CalendarEventPB?> _loadEvent(
+    RowId rowId,
+  ) async {
+    final payload = RowIdPB(viewId: viewId, rowId: rowId);
+    return DatabaseEventGetCalendarEvent(payload).send().then(
+          (result) => result.fold(
+            (eventPB) => eventPB,
+            (r) {
+              Log.error(r);
+              return null;
+            },
+          ),
+        );
+  }
+
+  Future<void> _loadAllEvents() async {
+    final payload = CalendarEventRequestPB.create()..viewId = viewId;
+    DatabaseEventGetAllCalendarEvents(payload).send().then((result) {
+      result.fold(
+        (events) {
+          if (!isClosed) {
+            add(UnscheduleEventsEvent.didLoadAllEvents(events.items));
+          }
+        },
+        (r) => Log.error(r),
+      );
+    });
+  }
+
+  void _startListening() {
+    final onDatabaseChanged = DatabaseCallbacks(
+      onRowsCreated: (rowIds) async {
+        if (isClosed) {
+          return;
+        }
+        for (final id in rowIds) {
+          final event = await _loadEvent(id);
+          if (event != null && !isClosed) {
+            add(UnscheduleEventsEvent.didReceiveEvent(event));
+          }
+        }
+      },
+      onRowsDeleted: (rowIds) {
+        if (isClosed) {
+          return;
+        }
+        add(UnscheduleEventsEvent.didDeleteEvents(rowIds));
+      },
+      onRowsUpdated: (rowIds, reason) async {
+        if (isClosed) {
+          return;
+        }
+        for (final id in rowIds) {
+          final event = await _loadEvent(id);
+          if (event != null) {
+            add(UnscheduleEventsEvent.didDeleteEvents([id]));
+            add(UnscheduleEventsEvent.didReceiveEvent(event));
+          }
+        }
+      },
+    );
+
+    databaseController.addListener(onDatabaseChanged: onDatabaseChanged);
+  }
+}
+
+@freezed
+class UnscheduleEventsEvent with _$UnscheduleEventsEvent {
+  const factory UnscheduleEventsEvent.initial() = _InitialCalendar;
+
+  // Called after loading all the current evnets
+  const factory UnscheduleEventsEvent.didLoadAllEvents(
+    List<CalendarEventPB> events,
+  ) = _ReceiveUnscheduleEventsEvents;
+
+  const factory UnscheduleEventsEvent.didDeleteEvents(List<RowId> rowIds) =
+      _DidDeleteEvents;
+
+  const factory UnscheduleEventsEvent.didReceiveEvent(
+    CalendarEventPB event,
+  ) = _DidReceiveEvent;
+}
+
+@freezed
+class UnscheduleEventsState with _$UnscheduleEventsState {
+  const factory UnscheduleEventsState({
+    required Option<DatabasePB> database,
+    required List<CalendarEventPB> allEvents,
+    required List<CalendarEventPB> unscheduleEvents,
+  }) = _UnscheduleEventsState;
+
+  factory UnscheduleEventsState.initial() => UnscheduleEventsState(
+        database: none(),
+        allEvents: [],
+        unscheduleEvents: [],
+      );
+}

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

@@ -1,19 +1,14 @@
 import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart';
 import 'package:appflowy/startup/plugin/plugin.dart';
-import 'package:appflowy/workspace/presentation/home/home_stack.dart';
-import 'package:appflowy/workspace/presentation/widgets/left_bar_item.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
-import 'package:flutter/material.dart';
-
-import '../../util.dart';
-import 'presentation/calendar_page.dart';
 
 class CalendarPluginBuilder extends PluginBuilder {
   @override
   Plugin build(dynamic data) {
     if (data is ViewPB) {
-      return CalendarPlugin(pluginType: pluginType, view: data);
+      return DatabaseTabBarViewPlugin(pluginType: pluginType, view: data);
     } else {
       throw FlowyPluginException.invalidData;
     }
@@ -36,55 +31,3 @@ class CalendarPluginConfig implements PluginConfig {
   @override
   bool get creatable => true;
 }
-
-class CalendarPlugin extends Plugin {
-  @override
-  final ViewPluginNotifier notifier;
-  final PluginType _pluginType;
-
-  CalendarPlugin({
-    required ViewPB view,
-    required PluginType pluginType,
-    bool listenOnViewChanged = false,
-  })  : _pluginType = pluginType,
-        notifier = ViewPluginNotifier(
-          view: view,
-          listenOnViewChanged: listenOnViewChanged,
-        );
-
-  @override
-  PluginWidgetBuilder get widgetBuilder =>
-      CalendarPluginWidgetBuilder(notifier: notifier);
-
-  @override
-  PluginId get id => notifier.view.id;
-
-  @override
-  PluginType get pluginType => _pluginType;
-}
-
-class CalendarPluginWidgetBuilder extends PluginWidgetBuilder {
-  final ViewPluginNotifier notifier;
-  CalendarPluginWidgetBuilder({required this.notifier, Key? key});
-
-  ViewPB get view => notifier.view;
-
-  @override
-  Widget get leftBarItem => ViewLeftBarItem(view: view);
-
-  @override
-  Widget buildWidget({PluginContext? context}) {
-    notifier.isDeleted.addListener(() {
-      notifier.isDeleted.value.fold(() => null, (deletedView) {
-        if (deletedView.hasIndex()) {
-          context?.onDeleted(view, deletedView.index);
-        }
-      });
-    });
-
-    return CalendarPage(key: ValueKey(view.id), view: view);
-  }
-
-  @override
-  List<NavigationItem> get navigationItems => [this];
-}

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

@@ -309,7 +309,7 @@ class _EventCard extends StatelessWidget {
       cellBuilder: cellBuilder,
       openCard: (context) => showEventDetails(
         context: context,
-        event: event,
+        event: event.event,
         viewId: viewId,
         rowCache: rowCache,
       ),

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

@@ -1,5 +1,8 @@
 import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/database_view/application/database_controller.dart';
 import 'package:appflowy/plugins/database_view/calendar/application/calendar_bloc.dart';
+import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
 import 'package:calendar_view/calendar_view.dart';
 import 'package:easy_localization/easy_localization.dart';
@@ -15,11 +18,51 @@ import '../../widgets/row/cell_builder.dart';
 import '../../widgets/row/row_detail.dart';
 import 'calendar_day.dart';
 import 'layout/sizes.dart';
-import 'toolbar/calendar_toolbar.dart';
+import 'toolbar/calendar_setting_bar.dart';
+
+class CalendarPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
+  @override
+  Widget content(
+    BuildContext context,
+    ViewPB view,
+    DatabaseController controller,
+  ) {
+    return CalendarPage(
+      key: _makeValueKey(controller),
+      view: view,
+      databaseController: controller,
+    );
+  }
+
+  @override
+  Widget settingBar(BuildContext context, DatabaseController controller) {
+    return CalendarSettingBar(
+      key: _makeValueKey(controller),
+      databaseController: controller,
+    );
+  }
+
+  @override
+  Widget settingBarExtension(
+    BuildContext context,
+    DatabaseController controller,
+  ) {
+    return SizedBox.fromSize();
+  }
+
+  ValueKey _makeValueKey(DatabaseController controller) {
+    return ValueKey(controller.viewId);
+  }
+}
 
 class CalendarPage extends StatefulWidget {
   final ViewPB view;
-  const CalendarPage({required this.view, super.key});
+  final DatabaseController databaseController;
+  const CalendarPage({
+    required this.view,
+    required this.databaseController,
+    super.key,
+  });
 
   @override
   State<CalendarPage> createState() => _CalendarPageState();
@@ -33,8 +76,10 @@ class _CalendarPageState extends State<CalendarPage> {
   @override
   void initState() {
     _calendarState = GlobalKey<MonthViewState>();
-    _calendarBloc = CalendarBloc(view: widget.view)
-      ..add(const CalendarEvent.initial());
+    _calendarBloc = CalendarBloc(
+      view: widget.view,
+      databaseController: widget.databaseController,
+    )..add(const CalendarEvent.initial());
 
     super.initState();
   }
@@ -79,7 +124,7 @@ class _CalendarPageState extends State<CalendarPage> {
                 if (state.editingEvent != null) {
                   showEventDetails(
                     context: context,
-                    event: state.editingEvent!.event!,
+                    event: state.editingEvent!.event!.event,
                     viewId: widget.view.id,
                     rowCache: _calendarBloc.rowCache,
                   );
@@ -115,8 +160,6 @@ class _CalendarPageState extends State<CalendarPage> {
             builder: (context, state) {
               return Column(
                 children: [
-                  // const _ToolbarBlocAdaptor(),
-                  const CalendarToolbar(),
                   _buildCalendar(
                     _eventController,
                     state.settings
@@ -238,12 +281,12 @@ class _CalendarPageState extends State<CalendarPage> {
 
 void showEventDetails({
   required BuildContext context,
-  required CalendarDayEvent event,
+  required CalendarEventPB event,
   required String viewId,
   required RowCache rowCache,
 }) {
   final dataController = RowController(
-    rowMeta: event.event.rowMeta,
+    rowMeta: event.rowMeta,
     viewId: viewId,
     rowCache: rowCache,
   );

+ 37 - 16
frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart

@@ -351,21 +351,16 @@ class FirstDayOfWeek extends StatelessWidget {
         final symbols =
             DateFormat.EEEE(context.locale.toLanguageTag()).dateSymbols;
         // starts from sunday
-        final items = symbols.WEEKDAYS.asMap().entries.map((entry) {
-          final index = entry.key;
-          final string = entry.value;
-          return SizedBox(
-            height: GridSize.popoverItemHeight,
-            child: FlowyButton(
-              text: FlowyText.medium(string),
-              onTap: () {
-                onUpdated(index);
-                popoverMutex.close();
-              },
-              rightIcon: firstDayOfWeek == index
-                  ? const FlowySvg(name: 'grid/checkmark')
-                  : null,
-            ),
+        const len = 2;
+        final items = symbols.WEEKDAYS.take(len).indexed.map((entry) {
+          return StartFromButton(
+            title: entry.$2,
+            dayIndex: entry.$1,
+            isSelected: firstDayOfWeek == entry.$1,
+            onTap: (index) {
+              onUpdated(index);
+              popoverMutex.close();
+            },
           );
         }).toList();
 
@@ -376,7 +371,7 @@ class FirstDayOfWeek extends StatelessWidget {
             itemBuilder: (context, index) => items[index],
             separatorBuilder: (context, index) =>
                 VSpace(GridSize.typeOptionSeparatorHeight),
-            itemCount: 2,
+            itemCount: len,
           ),
         );
       },
@@ -426,3 +421,29 @@ enum CalendarLayoutSettingAction {
   showWeekNumber,
   showTimeLine,
 }
+
+class StartFromButton extends StatelessWidget {
+  final int dayIndex;
+  final String title;
+  final bool isSelected;
+  final void Function(int) onTap;
+  const StartFromButton({
+    required this.title,
+    required this.dayIndex,
+    required this.onTap,
+    required this.isSelected,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return SizedBox(
+      height: GridSize.popoverItemHeight,
+      child: FlowyButton(
+        text: FlowyText.medium(title),
+        onTap: () => onTap(dayIndex),
+        rightIcon: isSelected ? const FlowySvg(name: 'grid/checkmark') : null,
+      ),
+    );
+  }
+}

+ 178 - 0
frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_setting_bar.dart

@@ -0,0 +1,178 @@
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/database_view/application/database_controller.dart';
+import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
+import 'package:appflowy/plugins/database_view/calendar/application/unschedule_event_bloc.dart';
+import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart';
+import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
+import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart';
+import 'package:appflowy_popover/appflowy_popover.dart';
+import 'package:easy_localization/easy_localization.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 CalendarSettingBar extends StatelessWidget {
+  final DatabaseController databaseController;
+  const CalendarSettingBar({
+    required this.databaseController,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return SizedBox(
+      height: 40,
+      child: Row(
+        mainAxisAlignment: MainAxisAlignment.end,
+        children: [
+          UnscheduleEventsButton(databaseController: databaseController),
+          SettingButton(
+            databaseController: databaseController,
+          ),
+        ],
+      ),
+    );
+  }
+}
+
+class UnscheduleEventsButton extends StatefulWidget {
+  final DatabaseController databaseController;
+  const UnscheduleEventsButton({
+    required this.databaseController,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  State<UnscheduleEventsButton> createState() => _UnscheduleEventsButtonState();
+}
+
+class _UnscheduleEventsButtonState extends State<UnscheduleEventsButton> {
+  late final PopoverController _popoverController;
+  late final UnscheduleEventsBloc _bloc;
+
+  @override
+  void initState() {
+    super.initState();
+    _bloc = UnscheduleEventsBloc(databaseController: widget.databaseController)
+      ..add(const UnscheduleEventsEvent.initial());
+    _popoverController = PopoverController();
+  }
+
+  @override
+  dispose() {
+    _bloc.close();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return AppFlowyPopover(
+      direction: PopoverDirection.bottomWithCenterAligned,
+      controller: _popoverController,
+      offset: const Offset(0, 8),
+      constraints: const BoxConstraints(maxWidth: 300, maxHeight: 600),
+      child: BlocProvider.value(
+        value: _bloc,
+        child: BlocBuilder<UnscheduleEventsBloc, UnscheduleEventsState>(
+          buildWhen: (previous, current) =>
+              previous.unscheduleEvents.length !=
+              current.unscheduleEvents.length,
+          builder: (context, state) {
+            return FlowyTextButton(
+              "${LocaleKeys.calendar_settings_noDateTitle.tr()} (${state.unscheduleEvents.length})",
+              fillColor: Colors.transparent,
+              hoverColor: AFThemeExtension.of(context).lightGreyHover,
+              padding: GridSize.typeOptionContentInsets,
+            );
+          },
+        ),
+      ),
+      popupBuilder: (context) {
+        return UnscheduleEventsList(
+          viewId: _bloc.viewId,
+          rowCache: _bloc.rowCache,
+          controller: _popoverController,
+          unscheduleEvents: _bloc.state.unscheduleEvents,
+        );
+      },
+    );
+  }
+}
+
+class UnscheduleEventsList extends StatelessWidget {
+  final String viewId;
+  final RowCache rowCache;
+  final PopoverController controller;
+  final List<CalendarEventPB> unscheduleEvents;
+  const UnscheduleEventsList({
+    required this.viewId,
+    required this.controller,
+    required this.unscheduleEvents,
+    required this.rowCache,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    final cells = <Widget>[
+      Padding(
+        padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
+        child: FlowyText.medium(
+          LocaleKeys.calendar_settings_clickToAdd.tr(),
+          color: Theme.of(context).hintColor,
+          overflow: TextOverflow.ellipsis,
+        ),
+      ),
+      const VSpace(6),
+      ...unscheduleEvents.map(
+        (e) => UnscheduledEventCell(
+          event: e,
+          onPressed: () {
+            showEventDetails(
+              context: context,
+              event: e,
+              viewId: viewId,
+              rowCache: rowCache,
+            );
+            controller.close();
+          },
+        ),
+      )
+    ];
+
+    return ListView.separated(
+      itemBuilder: (context, index) => cells[index],
+      itemCount: cells.length,
+      separatorBuilder: (context, index) =>
+          VSpace(GridSize.typeOptionSeparatorHeight),
+      shrinkWrap: true,
+    );
+  }
+}
+
+class UnscheduledEventCell extends StatelessWidget {
+  final CalendarEventPB event;
+  final VoidCallback onPressed;
+  const UnscheduledEventCell({
+    required this.event,
+    required this.onPressed,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return SizedBox(
+      height: GridSize.popoverItemHeight,
+      child: FlowyButton(
+        text: FlowyText.medium(
+          event.title.isEmpty
+              ? LocaleKeys.calendar_defaultNewCalendarTitle.tr()
+              : event.title,
+        ),
+        onTap: onPressed,
+      ),
+    );
+  }
+}

+ 0 - 136
frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_toolbar.dart

@@ -1,136 +0,0 @@
-import 'package:appflowy/generated/locale_keys.g.dart';
-import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart';
-import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
-import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart';
-import 'package:appflowy_popover/appflowy_popover.dart';
-import 'package:calendar_view/calendar_view.dart';
-import 'package:easy_localization/easy_localization.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';
-
-import '../../application/calendar_bloc.dart';
-
-class CalendarToolbar extends StatelessWidget {
-  const CalendarToolbar({super.key});
-
-  @override
-  Widget build(BuildContext context) {
-    return SizedBox(
-      height: 40,
-      child: Row(
-        mainAxisAlignment: MainAxisAlignment.end,
-        children: [
-          const _UnscheduleEventsButton(),
-          SettingButton(
-            databaseController: context.read<CalendarBloc>().databaseController,
-          ),
-        ],
-      ),
-    );
-  }
-}
-
-class _UnscheduleEventsButton extends StatefulWidget {
-  const _UnscheduleEventsButton({Key? key}) : super(key: key);
-
-  @override
-  State<_UnscheduleEventsButton> createState() =>
-      _UnscheduleEventsButtonState();
-}
-
-class _UnscheduleEventsButtonState extends State<_UnscheduleEventsButton> {
-  late final PopoverController _controller;
-
-  @override
-  void initState() {
-    super.initState();
-    _controller = PopoverController();
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    return BlocBuilder<CalendarBloc, CalendarState>(
-      builder: (context, state) {
-        final unscheduledEvents = state.allEvents
-            .where((e) => e.date == DateTime.fromMillisecondsSinceEpoch(0))
-            .toList();
-        final viewId = context.read<CalendarBloc>().viewId;
-        final rowCache = context.read<CalendarBloc>().rowCache;
-        return AppFlowyPopover(
-          direction: PopoverDirection.bottomWithCenterAligned,
-          controller: _controller,
-          offset: const Offset(0, 8),
-          constraints: const BoxConstraints(maxWidth: 300, maxHeight: 600),
-          child: FlowyTextButton(
-            "${LocaleKeys.calendar_settings_noDateTitle.tr()} (${unscheduledEvents.length})",
-            fillColor: Colors.transparent,
-            hoverColor: AFThemeExtension.of(context).lightGreyHover,
-            padding: GridSize.typeOptionContentInsets,
-          ),
-          popupBuilder: (context) {
-            final cells = <Widget>[
-              Padding(
-                padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
-                child: FlowyText.medium(
-                  // LocaleKeys.calendar_settings_noDateHint.tr(),
-                  LocaleKeys.calendar_settings_clickToAdd.tr(),
-                  color: Theme.of(context).hintColor,
-                  overflow: TextOverflow.ellipsis,
-                ),
-              ),
-              const VSpace(6),
-              ...unscheduledEvents.map(
-                (e) => _UnscheduledEventItem(
-                  event: e,
-                  onPressed: () {
-                    showEventDetails(
-                      context: context,
-                      event: e.event!,
-                      viewId: viewId,
-                      rowCache: rowCache,
-                    );
-                    _controller.close();
-                  },
-                ),
-              )
-            ];
-            return ListView.separated(
-              itemBuilder: (context, index) => cells[index],
-              itemCount: cells.length,
-              separatorBuilder: (context, index) =>
-                  VSpace(GridSize.typeOptionSeparatorHeight),
-              shrinkWrap: true,
-            );
-          },
-        );
-      },
-    );
-  }
-}
-
-class _UnscheduledEventItem extends StatelessWidget {
-  final CalendarEventData<CalendarDayEvent> event;
-  final VoidCallback onPressed;
-  const _UnscheduledEventItem({
-    required this.event,
-    required this.onPressed,
-    Key? key,
-  }) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return SizedBox(
-      height: GridSize.popoverItemHeight,
-      child: FlowyButton(
-        text: FlowyText.medium(
-          event.title.isEmpty
-              ? LocaleKeys.calendar_defaultNewCalendarTitle.tr()
-              : event.title,
-        ),
-        onTap: onPressed,
-      ),
-    );
-  }
-}

+ 16 - 13
frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_accessory_bloc.dart

@@ -3,17 +3,17 @@ import 'package:freezed_annotation/freezed_annotation.dart';
 
 part 'grid_accessory_bloc.freezed.dart';
 
-class GridAccessoryMenuBloc
-    extends Bloc<GridAccessoryMenuEvent, GridAccessoryMenuState> {
+class DatabaseViewSettingExtensionBloc extends Bloc<
+    DatabaseViewSettingExtensionEvent, DatabaseViewSettingExtensionState> {
   final String viewId;
 
-  GridAccessoryMenuBloc({required this.viewId})
+  DatabaseViewSettingExtensionBloc({required this.viewId})
       : super(
-          GridAccessoryMenuState.initial(
+          DatabaseViewSettingExtensionState.initial(
             viewId,
           ),
         ) {
-    on<GridAccessoryMenuEvent>(
+    on<DatabaseViewSettingExtensionEvent>(
       (event, emit) async {
         event.when(
           initial: () {},
@@ -27,22 +27,25 @@ class GridAccessoryMenuBloc
 }
 
 @freezed
-class GridAccessoryMenuEvent with _$GridAccessoryMenuEvent {
-  const factory GridAccessoryMenuEvent.initial() = _Initial;
-  const factory GridAccessoryMenuEvent.toggleMenu() = _MenuVisibleChange;
+class DatabaseViewSettingExtensionEvent
+    with _$DatabaseViewSettingExtensionEvent {
+  const factory DatabaseViewSettingExtensionEvent.initial() = _Initial;
+  const factory DatabaseViewSettingExtensionEvent.toggleMenu() =
+      _MenuVisibleChange;
 }
 
 @freezed
-class GridAccessoryMenuState with _$GridAccessoryMenuState {
-  const factory GridAccessoryMenuState({
+class DatabaseViewSettingExtensionState
+    with _$DatabaseViewSettingExtensionState {
+  const factory DatabaseViewSettingExtensionState({
     required String viewId,
     required bool isVisible,
-  }) = _GridAccessoryMenuState;
+  }) = _DatabaseViewSettingExtensionState;
 
-  factory GridAccessoryMenuState.initial(
+  factory DatabaseViewSettingExtensionState.initial(
     String viewId,
   ) =>
-      GridAccessoryMenuState(
+      DatabaseViewSettingExtensionState(
         viewId: viewId,
         isVisible: false,
       );

+ 45 - 10
frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart

@@ -1,6 +1,8 @@
 import 'dart:async';
 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/presentation/widgets/filter/filter_info.dart';
+import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_info.dart';
 import 'package:dartz/dartz.dart';
 import 'package:equatable/equatable.dart';
 import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
@@ -65,17 +67,27 @@ class GridBloc extends Bloc<GridEvent, GridState> {
               ),
             );
           },
+          didReceveFilters: (List<FilterInfo> filters) {
+            emit(
+              state.copyWith(
+                reorderable: filters.isEmpty && state.sorts.isEmpty,
+                filters: filters,
+              ),
+            );
+          },
+          didReceveSorts: (List<SortInfo> sorts) {
+            emit(
+              state.copyWith(
+                reorderable: sorts.isEmpty && state.filters.isEmpty,
+                sorts: sorts,
+              ),
+            );
+          },
         );
       },
     );
   }
 
-  @override
-  Future<void> close() async {
-    await databaseController.dispose();
-    return super.close();
-  }
-
   RowCache getRowCache(RowId rowId) {
     return databaseController.rowCache;
   }
@@ -93,17 +105,29 @@ class GridBloc extends Bloc<GridEvent, GridState> {
         }
       },
       onRowsUpdated: (rows, reason) {
-        add(
-          GridEvent.didLoadRows(databaseController.rowCache.rowInfos, reason),
-        );
+        if (!isClosed) {
+          add(
+            GridEvent.didLoadRows(databaseController.rowCache.rowInfos, reason),
+          );
+        }
       },
       onFieldsChanged: (fields) {
         if (!isClosed) {
           add(GridEvent.didReceiveFieldUpdate(fields));
         }
       },
+      onFiltersChanged: (filters) {
+        if (!isClosed) {
+          add(GridEvent.didReceveFilters(filters));
+        }
+      },
+      onSortsChanged: (sorts) {
+        if (!isClosed) {
+          add(GridEvent.didReceveSorts(sorts));
+        }
+      },
     );
-    databaseController.setListener(onDatabaseChanged: onDatabaseChanged);
+    databaseController.addListener(onDatabaseChanged: onDatabaseChanged);
   }
 
   Future<void> _openGrid(Emitter<GridState> emit) async {
@@ -138,6 +162,11 @@ class GridEvent with _$GridEvent {
   const factory GridEvent.didReceiveGridUpdate(
     DatabasePB grid,
   ) = _DidReceiveGridUpdate;
+
+  const factory GridEvent.didReceveFilters(List<FilterInfo> filters) =
+      _DidReceiveFilters;
+  const factory GridEvent.didReceveSorts(List<SortInfo> sorts) =
+      _DidReceiveSorts;
 }
 
 @freezed
@@ -149,7 +178,10 @@ class GridState with _$GridState {
     required List<RowInfo> rowInfos,
     required int rowCount,
     required GridLoadingState loadingState,
+    required bool reorderable,
     required RowsChangedReason reason,
+    required List<SortInfo> sorts,
+    required List<FilterInfo> filters,
   }) = _GridState;
 
   factory GridState.initial(String viewId) => GridState(
@@ -158,8 +190,11 @@ class GridState with _$GridState {
         rowCount: 0,
         grid: none(),
         viewId: viewId,
+        reorderable: true,
         loadingState: const _Loading(),
         reason: const InitialListState(),
+        filters: [],
+        sorts: [],
       );
 }
 

+ 2 - 59
frontend/appflowy_flutter/lib/plugins/database_view/grid/grid.dart

@@ -1,19 +1,14 @@
 import 'package:appflowy/generated/locale_keys.g.dart';
-import 'package:appflowy/plugins/util.dart';
+import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart';
 import 'package:appflowy/startup/plugin/plugin.dart';
-import 'package:appflowy/workspace/presentation/home/home_stack.dart';
-import 'package:appflowy/workspace/presentation/widgets/left_bar_item.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
-import 'package:flutter/material.dart';
-
-import 'presentation/grid_page.dart';
 
 class GridPluginBuilder implements PluginBuilder {
   @override
   Plugin build(dynamic data) {
     if (data is ViewPB) {
-      return GridPlugin(pluginType: pluginType, view: data);
+      return DatabaseTabBarViewPlugin(pluginType: pluginType, view: data);
     } else {
       throw FlowyPluginException.invalidData;
     }
@@ -36,55 +31,3 @@ class GridPluginConfig implements PluginConfig {
   @override
   bool get creatable => true;
 }
-
-class GridPlugin extends Plugin {
-  @override
-  final ViewPluginNotifier notifier;
-  final PluginType _pluginType;
-
-  GridPlugin({
-    required ViewPB view,
-    required PluginType pluginType,
-    bool listenOnViewChanged = false,
-  })  : _pluginType = pluginType,
-        notifier = ViewPluginNotifier(
-          view: view,
-          listenOnViewChanged: listenOnViewChanged,
-        );
-
-  @override
-  PluginWidgetBuilder get widgetBuilder =>
-      GridPluginWidgetBuilder(notifier: notifier);
-
-  @override
-  PluginId get id => notifier.view.id;
-
-  @override
-  PluginType get pluginType => _pluginType;
-}
-
-class GridPluginWidgetBuilder extends PluginWidgetBuilder {
-  final ViewPluginNotifier notifier;
-  ViewPB get view => notifier.view;
-
-  GridPluginWidgetBuilder({required this.notifier, Key? key});
-
-  @override
-  Widget get leftBarItem => ViewLeftBarItem(view: view);
-
-  @override
-  Widget buildWidget({PluginContext? context}) {
-    notifier.isDeleted.addListener(() {
-      notifier.isDeleted.value.fold(() => null, (deletedView) {
-        if (deletedView.hasIndex()) {
-          context?.onDeleted(view, deletedView.index);
-        }
-      });
-    });
-
-    return GridPage(key: ValueKey(view.id), view: view);
-  }
-
-  @override
-  List<NavigationItem> get navigationItems => [this];
-}

+ 156 - 131
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/grid/presentation/widgets/toolbar/grid_setting_bar.dart';
+import 'package:appflowy/plugins/database_view/tar_bar/setting_menu.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';
@@ -15,25 +17,77 @@ import 'package:linked_scroll_controller/linked_scroll_controller.dart';
 import '../../application/field/field_controller.dart';
 import '../../application/row/row_cache.dart';
 import '../../application/row/row_data_controller.dart';
-import '../../application/setting/setting_bloc.dart';
-import '../application/filter/filter_menu_bloc.dart';
 import '../application/grid_bloc.dart';
 import '../../application/database_controller.dart';
-import '../application/sort/sort_menu_bloc.dart';
 import 'grid_scroll.dart';
+import '../../tar_bar/tab_bar_view.dart';
 import 'layout/layout.dart';
 import 'layout/sizes.dart';
-import 'widgets/accessory_menu.dart';
 import 'widgets/row/row.dart';
 import 'widgets/footer/grid_footer.dart';
 import 'widgets/header/grid_header.dart';
 import '../../widgets/row/row_detail.dart';
 import 'widgets/shortcuts.dart';
-import 'widgets/toolbar/grid_toolbar.dart';
+
+class ToggleExtensionNotifier extends ChangeNotifier {
+  bool _isToggled = false;
+
+  get isToggled => _isToggled;
+
+  void toggle() {
+    _isToggled = !_isToggled;
+    notifyListeners();
+  }
+}
+
+class GridPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
+  final _toggleExtension = ToggleExtensionNotifier();
+
+  @override
+  Widget content(
+    BuildContext context,
+    ViewPB view,
+    DatabaseController controller,
+  ) {
+    return GridPage(
+      key: _makeValueKey(controller),
+      view: view,
+      databaseController: controller,
+    );
+  }
+
+  @override
+  Widget settingBar(BuildContext context, DatabaseController controller) {
+    return GridSettingBar(
+      key: _makeValueKey(controller),
+      controller: controller,
+      toggleExtension: _toggleExtension,
+    );
+  }
+
+  @override
+  Widget settingBarExtension(
+    BuildContext context,
+    DatabaseController controller,
+  ) {
+    return DatabaseViewSettingExtension(
+      key: _makeValueKey(controller),
+      viewId: controller.viewId,
+      databaseController: controller,
+      toggleExtension: _toggleExtension,
+    );
+  }
+
+  ValueKey _makeValueKey(DatabaseController controller) {
+    return ValueKey(controller.viewId);
+  }
+}
 
 class GridPage extends StatefulWidget {
+  final DatabaseController databaseController;
   const GridPage({
     required this.view,
+    required this.databaseController,
     this.onDeleted,
     Key? key,
   }) : super(key: key);
@@ -46,12 +100,9 @@ class GridPage extends StatefulWidget {
 }
 
 class _GridPageState extends State<GridPage> {
-  late DatabaseController databaseController;
-
   @override
   void initState() {
     super.initState();
-    databaseController = DatabaseController(view: widget.view);
   }
 
   @override
@@ -61,24 +112,9 @@ class _GridPageState extends State<GridPage> {
         BlocProvider<GridBloc>(
           create: (context) => GridBloc(
             view: widget.view,
-            databaseController: databaseController,
+            databaseController: widget.databaseController,
           )..add(const GridEvent.initial()),
         ),
-        BlocProvider<GridFilterMenuBloc>(
-          create: (context) => GridFilterMenuBloc(
-            viewId: widget.view.id,
-            fieldController: databaseController.fieldController,
-          )..add(const GridFilterMenuEvent.initial()),
-        ),
-        BlocProvider<SortMenuBloc>(
-          create: (context) => SortMenuBloc(
-            viewId: widget.view.id,
-            fieldController: databaseController.fieldController,
-          )..add(const SortMenuEvent.initial()),
-        ),
-        BlocProvider<DatabaseSettingBloc>(
-          create: (context) => DatabaseSettingBloc(viewId: widget.view.id),
-        ),
       ],
       child: BlocBuilder<GridBloc, GridState>(
         builder: (context, state) {
@@ -87,9 +123,7 @@ class _GridPageState extends State<GridPage> {
                 const Center(child: CircularProgressIndicator.adaptive()),
             finish: (result) => result.successOrFail.fold(
               (_) => GridShortcuts(
-                child: FlowyGrid(
-                  viewId: widget.view.id,
-                ),
+                child: GridPageContent(view: widget.view),
               ),
               (err) => FlowyErrorPage(err.toString()),
             ),
@@ -100,18 +134,18 @@ class _GridPageState extends State<GridPage> {
   }
 }
 
-class FlowyGrid extends StatefulWidget {
-  final String viewId;
-  const FlowyGrid({
-    required this.viewId,
+class GridPageContent extends StatefulWidget {
+  final ViewPB view;
+  const GridPageContent({
+    required this.view,
     super.key,
   });
 
   @override
-  State<FlowyGrid> createState() => _FlowyGridState();
+  State<GridPageContent> createState() => _GridPageContentState();
 }
 
-class _FlowyGridState extends State<FlowyGrid> {
+class _GridPageContentState extends State<GridPageContent> {
   final _scrollController = GridScrollController(
     scrollGroupController: LinkedScrollControllerGroup(),
   );
@@ -135,106 +169,114 @@ class _FlowyGridState extends State<FlowyGrid> {
       buildWhen: (previous, current) => previous.fields != current.fields,
       builder: (context, state) {
         final contentWidth = GridLayout.headerWidth(state.fields.value);
-        final child = _WrapScrollView(
-          scrollController: _scrollController,
-          contentWidth: contentWidth,
-          child: _GridRows(
-            viewId: widget.viewId,
-            verticalScrollController: _scrollController.verticalController,
-          ),
-        );
 
         return Column(
           crossAxisAlignment: CrossAxisAlignment.start,
           children: [
-            const GridToolbar(),
-            GridAccessoryMenu(viewId: state.viewId),
-            _gridHeader(context, state.viewId),
-            Flexible(child: child),
-            const _RowCountBadge(),
+            _GridHeader(headerScrollController: headerScrollController),
+            _GridRows(
+              viewId: state.viewId,
+              contentWidth: contentWidth,
+              scrollController: _scrollController,
+            ),
+            const _GridFooter(),
           ],
         );
       },
     );
   }
+}
 
-  Widget _gridHeader(BuildContext context, String viewId) {
-    final fieldController =
-        context.read<GridBloc>().databaseController.fieldController;
-    return GridHeaderSliverAdaptor(
-      viewId: viewId,
-      fieldController: fieldController,
-      anchorScrollController: headerScrollController,
+class _GridHeader extends StatelessWidget {
+  final ScrollController headerScrollController;
+  const _GridHeader({required this.headerScrollController});
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<GridBloc, GridState>(
+      builder: (context, state) {
+        return GridHeaderSliverAdaptor(
+          viewId: state.viewId,
+          fieldController:
+              context.read<GridBloc>().databaseController.fieldController,
+          anchorScrollController: headerScrollController,
+        );
+      },
     );
   }
 }
 
 class _GridRows extends StatelessWidget {
   final String viewId;
+  final double contentWidth;
+  final GridScrollController scrollController;
+
   const _GridRows({
     required this.viewId,
-    required this.verticalScrollController,
+    required this.contentWidth,
+    required this.scrollController,
   });
 
-  final ScrollController verticalScrollController;
-
   @override
   Widget build(BuildContext context) {
-    final filterState = context.watch<GridFilterMenuBloc>().state;
-    final sortState = context.watch<SortMenuBloc>().state;
-
-    return BlocBuilder<GridBloc, GridState>(
-      buildWhen: (previous, current) => current.reason.maybeWhen(
-        reorderRows: () => true,
-        reorderSingleRow: (reorderRow, rowInfo) => true,
-        delete: (item) => true,
-        insert: (item) => true,
-        orElse: () => false,
-      ),
-      builder: (context, state) {
-        final rowInfos = state.rowInfos;
-        final behavior = ScrollConfiguration.of(context).copyWith(
-          scrollbars: false,
-        );
-        return ScrollConfiguration(
-          behavior: behavior,
-          child: ReorderableListView.builder(
-            /// TODO(Xazin): Resolve inconsistent scrollbar behavior
-            ///  This is a workaround related to
-            ///  https://github.com/flutter/flutter/issues/25652
-            cacheExtent: 5000,
-            scrollController: verticalScrollController,
-            buildDefaultDragHandles: false,
-            proxyDecorator: (child, index, animation) => Material(
-              color: Colors.white.withOpacity(.1),
-              child: Opacity(opacity: .5, child: child),
-            ),
-            onReorder: (fromIndex, newIndex) {
-              final toIndex = newIndex > fromIndex ? newIndex - 1 : newIndex;
-              if (fromIndex == toIndex) {
-                return;
-              }
-              context
-                  .read<GridBloc>()
-                  .add(GridEvent.moveRow(fromIndex, toIndex));
-            },
-            itemCount: rowInfos.length + 1, // the extra item is the footer
-            itemBuilder: (context, index) {
-              if (index < rowInfos.length) {
-                final rowInfo = rowInfos[index];
-                return _renderRow(
-                  context,
-                  rowInfo.rowId,
-                  index: index,
-                  isSortEnabled: sortState.sortInfos.isNotEmpty,
-                  isFilterEnabled: filterState.filters.isNotEmpty,
-                );
-              }
-              return const _GridFooter(key: Key('gridFooter'));
-            },
+    return Flexible(
+      child: _WrapScrollView(
+        scrollController: scrollController,
+        contentWidth: contentWidth,
+        child: BlocBuilder<GridBloc, GridState>(
+          buildWhen: (previous, current) => current.reason.maybeWhen(
+            reorderRows: () => true,
+            reorderSingleRow: (reorderRow, rowInfo) => true,
+            delete: (item) => true,
+            insert: (item) => true,
+            orElse: () => false,
           ),
-        );
-      },
+          builder: (context, state) {
+            final rowInfos = state.rowInfos;
+            final behavior = ScrollConfiguration.of(context).copyWith(
+              scrollbars: false,
+            );
+            return ScrollConfiguration(
+              behavior: behavior,
+              child: ReorderableListView.builder(
+                /// TODO(Xazin): Resolve inconsistent scrollbar behavior
+                ///  This is a workaround related to
+                ///  https://github.com/flutter/flutter/issues/25652
+                cacheExtent: 5000,
+                scrollController: scrollController.verticalController,
+                buildDefaultDragHandles: false,
+                proxyDecorator: (child, index, animation) => Material(
+                  color: Colors.white.withOpacity(.1),
+                  child: Opacity(opacity: .5, child: child),
+                ),
+                onReorder: (fromIndex, newIndex) {
+                  final toIndex =
+                      newIndex > fromIndex ? newIndex - 1 : newIndex;
+                  if (fromIndex == toIndex) {
+                    return;
+                  }
+                  context
+                      .read<GridBloc>()
+                      .add(GridEvent.moveRow(fromIndex, toIndex));
+                },
+                itemCount: rowInfos.length + 1, // the extra item is the footer
+                itemBuilder: (context, index) {
+                  if (index < rowInfos.length) {
+                    final rowInfo = rowInfos[index];
+                    return _renderRow(
+                      context,
+                      rowInfo.rowId,
+                      isDraggable: state.reorderable,
+                      index: index,
+                    );
+                  }
+                  return const GridRowBottomBar(key: Key('gridFooter'));
+                },
+              ),
+            );
+          },
+        ),
+      ),
     );
   }
 
@@ -242,8 +284,7 @@ class _GridRows extends StatelessWidget {
     BuildContext context,
     RowId rowId, {
     int? index,
-    bool isSortEnabled = false,
-    bool isFilterEnabled = false,
+    required bool isDraggable,
     Animation<double>? animation,
   }) {
     final rowCache = context.read<GridBloc>().getRowCache(rowId);
@@ -265,7 +306,7 @@ class _GridRows extends StatelessWidget {
       rowId: rowId,
       viewId: viewId,
       index: index,
-      isDraggable: !isSortEnabled && !isFilterEnabled,
+      isDraggable: isDraggable,
       dataController: dataController,
       cellBuilder: GridCellBuilder(cellCache: dataController.cellCache),
       openDetailPage: (context, cellBuilder) {
@@ -320,22 +361,6 @@ class _GridRows extends StatelessWidget {
   }
 }
 
-class _GridFooter extends StatelessWidget {
-  const _GridFooter({
-    super.key,
-  });
-
-  @override
-  Widget build(BuildContext context) {
-    return Container(
-      padding: GridSize.footerContentInsets,
-      height: GridSize.footerHeight,
-      margin: const EdgeInsets.only(bottom: 200),
-      child: const GridAddRowButton(),
-    );
-  }
-}
-
 class _WrapScrollView extends StatelessWidget {
   const _WrapScrollView({
     required this.contentWidth,
@@ -366,8 +391,8 @@ class _WrapScrollView extends StatelessWidget {
   }
 }
 
-class _RowCountBadge extends StatelessWidget {
-  const _RowCountBadge();
+class _GridFooter extends StatelessWidget {
+  const _GridFooter();
 
   @override
   Widget build(BuildContext context) {

+ 0 - 90
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/accessory_menu.dart

@@ -1,90 +0,0 @@
-import 'package:appflowy/plugins/database_view/grid/application/filter/filter_menu_bloc.dart';
-import 'package:appflowy/plugins/database_view/grid/application/grid_accessory_bloc.dart';
-import 'package:appflowy/plugins/database_view/grid/application/sort/sort_menu_bloc.dart';
-import 'package:flowy_infra/theme_extension.dart';
-import 'package:flowy_infra_ui/widget/spacing.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_bloc/flutter_bloc.dart';
-
-import '../layout/sizes.dart';
-import 'filter/filter_menu.dart';
-import 'sort/sort_menu.dart';
-
-class GridAccessoryMenu extends StatelessWidget {
-  final String viewId;
-  const GridAccessoryMenu({required this.viewId, Key? key}) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return BlocProvider(
-      create: (context) => GridAccessoryMenuBloc(viewId: viewId),
-      child: MultiBlocListener(
-        listeners: [
-          BlocListener<GridFilterMenuBloc, GridFilterMenuState>(
-            listenWhen: (p, c) => p.isVisible != c.isVisible,
-            listener: (context, state) => context
-                .read<GridAccessoryMenuBloc>()
-                .add(const GridAccessoryMenuEvent.toggleMenu()),
-          ),
-          BlocListener<SortMenuBloc, SortMenuState>(
-            listenWhen: (p, c) => p.isVisible != c.isVisible,
-            listener: (context, state) => context
-                .read<GridAccessoryMenuBloc>()
-                .add(const GridAccessoryMenuEvent.toggleMenu()),
-          ),
-        ],
-        child: BlocBuilder<GridAccessoryMenuBloc, GridAccessoryMenuState>(
-          builder: (context, state) {
-            if (state.isVisible) {
-              return const _AccessoryMenu();
-            } else {
-              return const SizedBox();
-            }
-          },
-        ),
-      ),
-    );
-  }
-}
-
-class _AccessoryMenu extends StatelessWidget {
-  const _AccessoryMenu({Key? key}) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return BlocBuilder<GridAccessoryMenuBloc, GridAccessoryMenuState>(
-      builder: (context, state) {
-        return _wrapPadding(
-          Column(
-            children: [
-              Divider(
-                height: 1.0,
-                color: AFThemeExtension.of(context).toggleOffFill,
-              ),
-              const VSpace(6),
-              const IntrinsicHeight(
-                child: Row(
-                  children: [
-                    SortMenu(),
-                    HSpace(6),
-                    FilterMenu(),
-                  ],
-                ),
-              ),
-            ],
-          ),
-        );
-      },
-    );
-  }
-
-  Widget _wrapPadding(Widget child) {
-    return Padding(
-      padding: EdgeInsets.symmetric(
-        horizontal: GridSize.leadingHeaderPadding,
-        vertical: 6,
-      ),
-      child: child,
-    );
-  }
-}

+ 38 - 25
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/filter_menu.dart

@@ -1,4 +1,5 @@
 import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
 import 'package:appflowy/plugins/database_view/grid/application/filter/filter_menu_bloc.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:easy_localization/easy_localization.dart';
@@ -12,37 +13,49 @@ import 'create_filter_list.dart';
 import 'filter_menu_item.dart';
 
 class FilterMenu extends StatelessWidget {
-  const FilterMenu({Key? key}) : super(key: key);
+  final FieldController fieldController;
+  const FilterMenu({
+    required this.fieldController,
+    Key? key,
+  }) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
-    return BlocBuilder<GridFilterMenuBloc, GridFilterMenuState>(
-      builder: (context, state) {
-        final List<Widget> children = [];
-        children.addAll(
-          state.filters
-              .map((filterInfo) => FilterMenuItem(filterInfo: filterInfo))
-              .toList(),
-        );
+    return BlocProvider<GridFilterMenuBloc>(
+      create: (context) => GridFilterMenuBloc(
+        viewId: fieldController.viewId,
+        fieldController: fieldController,
+      )..add(
+          const GridFilterMenuEvent.initial(),
+        ),
+      child: BlocBuilder<GridFilterMenuBloc, GridFilterMenuState>(
+        builder: (context, state) {
+          final List<Widget> children = [];
+          children.addAll(
+            state.filters
+                .map((filterInfo) => FilterMenuItem(filterInfo: filterInfo))
+                .toList(),
+          );
 
-        if (state.creatableFields.isNotEmpty) {
-          children.add(AddFilterButton(viewId: state.viewId));
-        }
+          if (state.creatableFields.isNotEmpty) {
+            children.add(AddFilterButton(viewId: state.viewId));
+          }
 
-        return Expanded(
-          child: Row(
-            children: [
-              Expanded(
-                child: Wrap(
-                  spacing: 6,
-                  runSpacing: 4,
-                  children: children,
+          return Expanded(
+            child: Row(
+              children: [
+                Expanded(
+                  child: Wrap(
+                    spacing: 6,
+                    runSpacing: 4,
+                    children: children,
+                  ),
                 ),
-              ),
-            ],
-          ),
-        );
-      },
+              ],
+            ),
+          );
+        },
+      ),
     );
   }
 }

+ 17 - 0
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/footer/grid_footer.dart

@@ -1,5 +1,6 @@
 import 'package:appflowy/generated/locale_keys.g.dart';
 import 'package:appflowy/plugins/database_view/grid/application/grid_bloc.dart';
+import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra/theme_extension.dart';
 import 'package:flowy_infra/image.dart';
@@ -27,3 +28,19 @@ class GridAddRowButton extends StatelessWidget {
     );
   }
 }
+
+class GridRowBottomBar extends StatelessWidget {
+  const GridRowBottomBar({
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      padding: GridSize.footerContentInsets,
+      height: GridSize.footerHeight,
+      margin: const EdgeInsets.only(bottom: 200),
+      child: const GridAddRowButton(),
+    );
+  }
+}

+ 32 - 21
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/sort_menu.dart

@@ -1,3 +1,4 @@
+import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
 import 'package:appflowy/plugins/database_view/grid/application/sort/sort_menu_bloc.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -13,30 +14,40 @@ import 'sort_editor.dart';
 import 'sort_info.dart';
 
 class SortMenu extends StatelessWidget {
-  const SortMenu({Key? key}) : super(key: key);
+  final FieldController fieldController;
+  const SortMenu({
+    required this.fieldController,
+    Key? key,
+  }) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
-    return BlocBuilder<SortMenuBloc, SortMenuState>(
-      builder: (context, state) {
-        if (state.sortInfos.isNotEmpty) {
-          return AppFlowyPopover(
-            controller: PopoverController(),
-            constraints: BoxConstraints.loose(const Size(340, 200)),
-            direction: PopoverDirection.bottomWithLeftAligned,
-            popupBuilder: (BuildContext popoverContext) {
-              return SortEditor(
-                viewId: state.viewId,
-                fieldController: context.read<SortMenuBloc>().fieldController,
-                sortInfos: state.sortInfos,
-              );
-            },
-            child: SortChoiceChip(sortInfos: state.sortInfos),
-          );
-        } else {
-          return const SizedBox();
-        }
-      },
+    return BlocProvider<SortMenuBloc>(
+      create: (context) => SortMenuBloc(
+        viewId: fieldController.viewId,
+        fieldController: fieldController,
+      )..add(const SortMenuEvent.initial()),
+      child: BlocBuilder<SortMenuBloc, SortMenuState>(
+        builder: (context, state) {
+          if (state.sortInfos.isNotEmpty) {
+            return AppFlowyPopover(
+              controller: PopoverController(),
+              constraints: BoxConstraints.loose(const Size(340, 200)),
+              direction: PopoverDirection.bottomWithLeftAligned,
+              popupBuilder: (BuildContext popoverContext) {
+                return SortEditor(
+                  viewId: state.viewId,
+                  fieldController: context.read<SortMenuBloc>().fieldController,
+                  sortInfos: state.sortInfos,
+                );
+              },
+              child: SortChoiceChip(sortInfos: state.sortInfos),
+            );
+          } else {
+            return const SizedBox();
+          }
+        },
+      ),
     );
   }
 }

+ 1 - 30
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/grid_layout.dart

@@ -1,7 +1,6 @@
-import 'package:appflowy/generated/locale_keys.g.dart';
 import 'package:appflowy/plugins/database_view/application/layout/layout_bloc.dart';
+import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.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';
@@ -67,34 +66,6 @@ class _DatabaseLayoutListState extends State<DatabaseLayoutList> {
   }
 }
 
-extension DatabaseLayoutExtension on DatabaseLayoutPB {
-  String layoutName() {
-    switch (this) {
-      case DatabaseLayoutPB.Board:
-        return LocaleKeys.board_menuName.tr();
-      case DatabaseLayoutPB.Calendar:
-        return LocaleKeys.calendar_menuName.tr();
-      case DatabaseLayoutPB.Grid:
-        return LocaleKeys.grid_menuName.tr();
-      default:
-        return "";
-    }
-  }
-
-  String iconName() {
-    switch (this) {
-      case DatabaseLayoutPB.Board:
-        return 'editor/board';
-      case DatabaseLayoutPB.Calendar:
-        return "editor/grid";
-      case DatabaseLayoutPB.Grid:
-        return "editor/grid";
-      default:
-        return "";
-    }
-  }
-}
-
 class DatabaseViewLayoutCell extends StatelessWidget {
   final bool isSelected;
   final DatabaseLayoutPB databaseLayout;

+ 68 - 0
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/grid_setting_bar.dart

@@ -0,0 +1,68 @@
+import 'package:appflowy/plugins/database_view/application/database_controller.dart';
+import 'package:appflowy/plugins/database_view/grid/application/filter/filter_menu_bloc.dart';
+import 'package:appflowy/plugins/database_view/grid/application/sort/sort_menu_bloc.dart';
+import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
+import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
+import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'filter_button.dart';
+import 'sort_button.dart';
+
+class GridSettingBar extends StatelessWidget {
+  final DatabaseController controller;
+  final ToggleExtensionNotifier toggleExtension;
+  const GridSettingBar({
+    required this.controller,
+    required this.toggleExtension,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return MultiBlocProvider(
+      providers: [
+        BlocProvider<GridFilterMenuBloc>(
+          create: (context) => GridFilterMenuBloc(
+            viewId: controller.viewId,
+            fieldController: controller.fieldController,
+          )..add(const GridFilterMenuEvent.initial()),
+        ),
+        BlocProvider<SortMenuBloc>(
+          create: (context) => SortMenuBloc(
+            viewId: controller.viewId,
+            fieldController: controller.fieldController,
+          )..add(const SortMenuEvent.initial()),
+        ),
+      ],
+      child: MultiBlocListener(
+        listeners: [
+          BlocListener<GridFilterMenuBloc, GridFilterMenuState>(
+            listenWhen: (p, c) => p.isVisible != c.isVisible,
+            listener: (context, state) => toggleExtension.toggle(),
+          ),
+          BlocListener<SortMenuBloc, SortMenuState>(
+            listenWhen: (p, c) => p.isVisible != c.isVisible,
+            listener: (context, state) => toggleExtension.toggle(),
+          ),
+        ],
+        child: SizedBox(
+          height: 40,
+          child: Row(
+            mainAxisAlignment: MainAxisAlignment.center,
+            children: [
+              SizedBox(width: GridSize.leadingHeaderPadding),
+              const Spacer(),
+              const FilterButton(),
+              const SortButton(),
+              SettingButton(
+                databaseController: controller,
+              ),
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+}

+ 0 - 41
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/grid_toolbar.dart

@@ -1,41 +0,0 @@
-import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
-import 'package:appflowy/plugins/database_view/grid/application/grid_bloc.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_bloc/flutter_bloc.dart';
-
-import '../../layout/sizes.dart';
-import 'filter_button.dart';
-import '../../../../widgets/setting/setting_button.dart';
-import 'sort_button.dart';
-
-class GridToolbarContext {
-  final String viewId;
-  final FieldController fieldController;
-  GridToolbarContext({
-    required this.viewId,
-    required this.fieldController,
-  });
-}
-
-class GridToolbar extends StatelessWidget {
-  const GridToolbar({Key? key}) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return SizedBox(
-      height: 40,
-      child: Row(
-        mainAxisAlignment: MainAxisAlignment.center,
-        children: [
-          SizedBox(width: GridSize.leadingHeaderPadding),
-          const Spacer(),
-          const FilterButton(),
-          const SortButton(),
-          SettingButton(
-            databaseController: context.read<GridBloc>().databaseController,
-          ),
-        ],
-      ),
-    );
-  }
-}

+ 98 - 0
frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/setting_menu.dart

@@ -0,0 +1,98 @@
+import 'package:appflowy/plugins/database_view/application/database_controller.dart';
+import 'package:appflowy/plugins/database_view/grid/application/grid_accessory_bloc.dart';
+import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
+import 'package:flowy_infra/theme_extension.dart';
+import 'package:flowy_infra_ui/widget/spacing.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:provider/provider.dart';
+
+import '../application/field/field_controller.dart';
+import '../grid/presentation/layout/sizes.dart';
+import '../grid/presentation/widgets/filter/filter_menu.dart';
+import '../grid/presentation/widgets/sort/sort_menu.dart';
+
+class DatabaseViewSettingExtension extends StatelessWidget {
+  final String viewId;
+  final DatabaseController databaseController;
+  final ToggleExtensionNotifier toggleExtension;
+  const DatabaseViewSettingExtension({
+    required this.viewId,
+    required this.databaseController,
+    required this.toggleExtension,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return ChangeNotifierProvider.value(
+      value: toggleExtension,
+      child: Consumer<ToggleExtensionNotifier>(
+        builder: (context, value, child) {
+          if (value.isToggled) {
+            return BlocProvider(
+              create: (context) =>
+                  DatabaseViewSettingExtensionBloc(viewId: viewId),
+              child: _DatabaseViewSettingContent(
+                fieldController: databaseController.fieldController,
+              ),
+            );
+          } else {
+            return const SizedBox();
+          }
+        },
+      ),
+    );
+  }
+}
+
+class _DatabaseViewSettingContent extends StatelessWidget {
+  final FieldController fieldController;
+  const _DatabaseViewSettingContent({
+    required this.fieldController,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<DatabaseViewSettingExtensionBloc,
+        DatabaseViewSettingExtensionState>(
+      builder: (context, state) {
+        final children = <Widget>[
+          Divider(
+            height: 1.0,
+            color: AFThemeExtension.of(context).toggleOffFill,
+          ),
+          const VSpace(6),
+          IntrinsicHeight(
+            child: Row(
+              children: [
+                SortMenu(
+                  fieldController: fieldController,
+                ),
+                const HSpace(6),
+                FilterMenu(
+                  fieldController: fieldController,
+                ),
+              ],
+            ),
+          )
+        ];
+
+        return _wrapPadding(
+          Column(children: children),
+        );
+      },
+    );
+  }
+
+  Widget _wrapPadding(Widget child) {
+    return Padding(
+      padding: EdgeInsets.symmetric(
+        horizontal: GridSize.leadingHeaderPadding,
+        vertical: 6,
+      ),
+      child: child,
+    );
+  }
+}

+ 415 - 0
frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tab_bar_view.dart

@@ -0,0 +1,415 @@
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/database_view/application/tar_bar_bloc.dart';
+import 'package:appflowy/plugins/util.dart';
+import 'package:appflowy/startup/plugin/plugin.dart';
+import 'package:appflowy/workspace/application/view/view_ext.dart';
+import 'package:appflowy/workspace/presentation/home/home_stack.dart';
+import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
+import 'package:appflowy/workspace/presentation/widgets/left_bar_item.dart';
+import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
+import 'package:appflowy_backend/protobuf/flowy-folder2/view.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/size.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';
+
+import '../application/database_controller.dart';
+import '../grid/presentation/layout/sizes.dart';
+import 'tar_bar_add_button.dart';
+
+abstract class DatabaseTabBarItemBuilder {
+  const DatabaseTabBarItemBuilder();
+
+  /// Returns the content of the tab bar item. The content is shown when the tab
+  /// bar item is selected. It can be any kind of database view.
+  Widget content(
+    BuildContext context,
+    ViewPB view,
+    DatabaseController controller,
+  );
+
+  /// Returns the setting bar of the tab bar item. The setting bar is shown on the
+  /// top right conner when the tab bar item is selected.
+  Widget settingBar(
+    BuildContext context,
+    DatabaseController controller,
+  );
+
+  Widget settingBarExtension(
+    BuildContext context,
+    DatabaseController controller,
+  );
+}
+
+class DatabaseTabBarView extends StatefulWidget {
+  final ViewPB view;
+  const DatabaseTabBarView({
+    required this.view,
+    super.key,
+  });
+
+  @override
+  State<DatabaseTabBarView> createState() => _DatabaseTabBarViewState();
+}
+
+class _DatabaseTabBarViewState extends State<DatabaseTabBarView> {
+  PageController? _pageController;
+
+  @override
+  void initState() {
+    super.initState();
+    _pageController = PageController(
+      initialPage: 0,
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocProvider<GridTabBarBloc>(
+      create: (context) => GridTabBarBloc(view: widget.view)
+        ..add(
+          const GridTabBarEvent.initial(),
+        ),
+      child: MultiBlocListener(
+        listeners: [
+          BlocListener<GridTabBarBloc, GridTabBarState>(
+            listenWhen: (p, c) => p.selectedIndex != c.selectedIndex,
+            listener: (context, state) {
+              _pageController?.animateToPage(
+                state.selectedIndex,
+                duration: const Duration(milliseconds: 300),
+                curve: Curves.ease,
+              );
+            },
+          ),
+        ],
+        child: Column(
+          children: [
+            Row(
+              children: [
+                BlocBuilder<GridTabBarBloc, GridTabBarState>(
+                  builder: (context, state) {
+                    return const Flexible(
+                      child: Padding(
+                        padding: EdgeInsets.only(left: 50),
+                        child: DatabaseTabBar(),
+                      ),
+                    );
+                  },
+                ),
+                BlocBuilder<GridTabBarBloc, GridTabBarState>(
+                  builder: (context, state) {
+                    return SizedBox(
+                      width: 300,
+                      child: Padding(
+                        padding: const EdgeInsets.only(right: 50),
+                        child: pageSettingBarFromState(state),
+                      ),
+                    );
+                  },
+                ),
+              ],
+            ),
+            BlocBuilder<GridTabBarBloc, GridTabBarState>(
+              builder: (context, state) {
+                return pageSettingBarExtensionFromState(state);
+              },
+            ),
+            Expanded(
+              child: BlocBuilder<GridTabBarBloc, GridTabBarState>(
+                builder: (context, state) {
+                  return PageView(
+                    pageSnapping: false,
+                    physics: const NeverScrollableScrollPhysics(),
+                    controller: _pageController,
+                    children: pageContentFromState(state),
+                  );
+                },
+              ),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+
+  List<Widget> pageContentFromState(GridTabBarState state) {
+    return state.tabBars.map((tabBar) {
+      final controller =
+          state.tabBarControllerByViewId[tabBar.viewId]!.controller;
+      return tabBar.builder.content(
+        context,
+        tabBar.view,
+        controller,
+      );
+    }).toList();
+  }
+
+  Widget pageSettingBarFromState(GridTabBarState state) {
+    if (state.tabBars.length < state.selectedIndex) {
+      return const SizedBox.shrink();
+    }
+    final tarBar = state.tabBars[state.selectedIndex];
+    final controller =
+        state.tabBarControllerByViewId[tarBar.viewId]!.controller;
+    return tarBar.builder.settingBar(
+      context,
+      controller,
+    );
+  }
+
+  Widget pageSettingBarExtensionFromState(GridTabBarState state) {
+    if (state.tabBars.length < state.selectedIndex) {
+      return const SizedBox.shrink();
+    }
+    final tarBar = state.tabBars[state.selectedIndex];
+    final controller =
+        state.tabBarControllerByViewId[tarBar.viewId]!.controller;
+    return tarBar.builder.settingBarExtension(
+      context,
+      controller,
+    );
+  }
+}
+
+class DatabaseTabBarViewPlugin extends Plugin {
+  @override
+  final ViewPluginNotifier notifier;
+  final PluginType _pluginType;
+
+  DatabaseTabBarViewPlugin({
+    required ViewPB view,
+    required PluginType pluginType,
+  })  : _pluginType = pluginType,
+        notifier = ViewPluginNotifier(view: view);
+
+  @override
+  PluginWidgetBuilder get widgetBuilder => DatabasePluginWidgetBuilder(
+        notifier: notifier,
+      );
+
+  @override
+  PluginId get id => notifier.view.id;
+
+  @override
+  PluginType get pluginType => _pluginType;
+}
+
+class DatabasePluginWidgetBuilder extends PluginWidgetBuilder {
+  final ViewPluginNotifier notifier;
+
+  DatabasePluginWidgetBuilder({
+    required this.notifier,
+    Key? key,
+  });
+
+  @override
+  Widget get leftBarItem => ViewLeftBarItem(view: notifier.view);
+
+  @override
+  Widget buildWidget({PluginContext? context}) {
+    notifier.isDeleted.addListener(() {
+      notifier.isDeleted.value.fold(() => null, (deletedView) {
+        if (deletedView.hasIndex()) {
+          context?.onDeleted(notifier.view, deletedView.index);
+        }
+      });
+    });
+    return DatabaseTabBarView(
+      key: ValueKey(notifier.view.id),
+      view: notifier.view,
+    );
+  }
+
+  @override
+  List<NavigationItem> get navigationItems => [this];
+}
+
+class DatabaseTabBar extends StatefulWidget {
+  const DatabaseTabBar({super.key});
+
+  @override
+  State<DatabaseTabBar> createState() => _DatabaseTabBarState();
+}
+
+class _DatabaseTabBarState extends State<DatabaseTabBar> {
+  final _scrollController = ScrollController();
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<GridTabBarBloc, GridTabBarState>(
+      builder: (context, state) {
+        final children = state.tabBars.indexed.map((indexed) {
+          final isSelected = state.selectedIndex == indexed.$1;
+          final tabBar = indexed.$2;
+          return DatabaseTabBarItem(
+            key: ValueKey(tabBar.viewId),
+            view: tabBar.view,
+            isSelected: isSelected,
+            onTap: (selectedView) {
+              context.read<GridTabBarBloc>().add(
+                    GridTabBarEvent.selectView(selectedView.id),
+                  );
+            },
+          );
+        }).toList();
+
+        return Row(
+          children: [
+            Flexible(
+              child: SingleChildScrollView(
+                controller: _scrollController,
+                scrollDirection: Axis.horizontal,
+                child: IntrinsicWidth(
+                  child: Row(children: children),
+                ),
+              ),
+            ),
+            AddDatabaseViewButton(
+              onTap: (action) async {
+                context.read<GridTabBarBloc>().add(
+                      GridTabBarEvent.createView(action),
+                    );
+              },
+            ),
+          ],
+        );
+      },
+    );
+  }
+}
+
+class DatabaseTabBarItem extends StatelessWidget {
+  final bool isSelected;
+  final ViewPB view;
+  final Function(ViewPB) onTap;
+  const DatabaseTabBarItem({
+    required this.view,
+    required this.isSelected,
+    required this.onTap,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return ConstrainedBox(
+      constraints: const BoxConstraints(minWidth: 80, maxWidth: 160),
+      child: IntrinsicWidth(
+        child: Column(
+          children: [
+            TabBarItemButton(
+              view: view,
+              onTap: () => onTap(view),
+            ),
+            if (isSelected)
+              Divider(
+                height: 1,
+                thickness: 2,
+                color: Theme.of(context).colorScheme.secondary,
+              ),
+          ],
+        ),
+      ),
+    );
+  }
+}
+
+class TabBarItemButton extends StatelessWidget {
+  final ViewPB view;
+  final VoidCallback onTap;
+  const TabBarItemButton({
+    required this.view,
+    required this.onTap,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return PopoverActionList<TabBarViewAction>(
+      direction: PopoverDirection.bottomWithCenterAligned,
+      actions: TabBarViewAction.values,
+      buildChild: (controller) {
+        return FlowyButton(
+          radius: Corners.s5Border,
+          hoverColor: AFThemeExtension.of(context).greyHover,
+          onTap: onTap,
+          onSecondaryTap: () {
+            controller.show();
+          },
+          text: FlowyText.medium(
+            view.name,
+            maxLines: 1,
+            textAlign: TextAlign.center,
+            overflow: TextOverflow.ellipsis,
+          ),
+          margin: GridSize.cellContentInsets,
+          leftIcon: svgWidget(
+            view.iconName,
+            color: Theme.of(context).iconTheme.color,
+          ),
+        );
+      },
+      onSelected: (action, controller) {
+        switch (action) {
+          case TabBarViewAction.rename:
+            NavigatorTextFieldDialog(
+              title: LocaleKeys.menuAppHeader_renameDialog.tr(),
+              value: view.name,
+              confirm: (newValue) {
+                context.read<GridTabBarBloc>().add(
+                      GridTabBarEvent.renameView(view.id, newValue),
+                    );
+              },
+            ).show(context);
+            break;
+          case TabBarViewAction.delete:
+            NavigatorAlertDialog(
+              title: LocaleKeys.grid_deleteView.tr(),
+              confirm: () {
+                context.read<GridTabBarBloc>().add(
+                      GridTabBarEvent.deleteView(view.id),
+                    );
+              },
+            ).show(context);
+
+            break;
+        }
+        controller.close();
+      },
+    );
+  }
+}
+
+enum TabBarViewAction implements ActionCell {
+  rename,
+  delete;
+
+  @override
+  String get name {
+    switch (this) {
+      case TabBarViewAction.rename:
+        return LocaleKeys.disclosureAction_rename.tr();
+      case TabBarViewAction.delete:
+        return LocaleKeys.disclosureAction_delete.tr();
+    }
+  }
+
+  Widget icon(Color iconColor) {
+    switch (this) {
+      case TabBarViewAction.rename:
+        return const FlowySvg(name: 'editor/edit');
+      case TabBarViewAction.delete:
+        return const FlowySvg(name: 'editor/delete');
+    }
+  }
+
+  @override
+  Widget? leftIcon(Color iconColor) => icon(iconColor);
+
+  @override
+  Widget? rightIcon(Color iconColor) => null;
+}

+ 156 - 0
frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tar_bar_add_button.dart

@@ -0,0 +1,156 @@
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
+import 'package:appflowy_backend/protobuf/flowy-folder2/view.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:flowy_infra_ui/style_widget/extension.dart';
+import 'package:flutter/material.dart';
+
+class AddDatabaseViewButton extends StatefulWidget {
+  final Function(AddButtonAction) onTap;
+  const AddDatabaseViewButton({
+    required this.onTap,
+    super.key,
+  });
+
+  @override
+  State<AddDatabaseViewButton> createState() => _AddDatabaseViewButtonState();
+}
+
+class _AddDatabaseViewButtonState extends State<AddDatabaseViewButton> {
+  final popoverController = PopoverController();
+
+  @override
+  Widget build(BuildContext context) {
+    return AppFlowyPopover(
+      controller: popoverController,
+      constraints: BoxConstraints.loose(const Size(200, 400)),
+      direction: PopoverDirection.bottomWithLeftAligned,
+      offset: const Offset(0, 8),
+      margin: EdgeInsets.zero,
+      triggerActions: PopoverTriggerFlags.none,
+      child: FlowyIconButton(
+        iconPadding: const EdgeInsets.all(4),
+        hoverColor: AFThemeExtension.of(context).greyHover,
+        onPressed: () => popoverController.show(),
+        icon: svgWidget(
+          'home/add',
+          color: Theme.of(context).colorScheme.tertiary,
+        ),
+      ),
+      popupBuilder: (BuildContext context) {
+        return TarBarAddButtonAction(
+          onTap: (action) {
+            popoverController.close();
+            widget.onTap(action);
+          },
+        );
+      },
+    );
+  }
+}
+
+class TarBarAddButtonAction extends StatelessWidget {
+  final Function(AddButtonAction) onTap;
+  const TarBarAddButtonAction({
+    required this.onTap,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    final cells = AddButtonAction.values.map((layout) {
+      return TarBarAddButtonActionCell(
+        action: layout,
+        onTap: onTap,
+      );
+    }).toList();
+
+    return ListView.separated(
+      controller: ScrollController(),
+      shrinkWrap: true,
+      itemCount: cells.length,
+      itemBuilder: (BuildContext context, int index) => cells[index],
+      separatorBuilder: (BuildContext context, int index) =>
+          VSpace(GridSize.typeOptionSeparatorHeight),
+      padding: const EdgeInsets.symmetric(vertical: 6.0),
+    );
+  }
+}
+
+class TarBarAddButtonActionCell extends StatelessWidget {
+  final AddButtonAction action;
+  final void Function(AddButtonAction) onTap;
+  const TarBarAddButtonActionCell({
+    required this.action,
+    required this.onTap,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return SizedBox(
+      height: GridSize.popoverItemHeight,
+      child: FlowyButton(
+        hoverColor: AFThemeExtension.of(context).lightGreyHover,
+        text: FlowyText.medium(
+          action.title,
+          color: AFThemeExtension.of(context).textColor,
+        ),
+        leftIcon: svgWidget(
+          action.iconName,
+          color: Theme.of(context).iconTheme.color,
+        ),
+        onTap: () => onTap(action),
+      ).padding(horizontal: 6.0),
+    );
+  }
+}
+
+enum AddButtonAction {
+  grid,
+  calendar,
+  board;
+
+  String get title {
+    switch (this) {
+      case AddButtonAction.board:
+        return LocaleKeys.board_menuName.tr();
+      case AddButtonAction.calendar:
+        return LocaleKeys.calendar_menuName.tr();
+      case AddButtonAction.grid:
+        return LocaleKeys.grid_menuName.tr();
+      default:
+        return "";
+    }
+  }
+
+  ViewLayoutPB get layoutType {
+    switch (this) {
+      case AddButtonAction.board:
+        return ViewLayoutPB.Board;
+      case AddButtonAction.calendar:
+        return ViewLayoutPB.Calendar;
+      case AddButtonAction.grid:
+        return ViewLayoutPB.Grid;
+      default:
+        return ViewLayoutPB.Grid;
+    }
+  }
+
+  String get iconName {
+    switch (this) {
+      case AddButtonAction.board:
+        return 'editor/board';
+      case AddButtonAction.calendar:
+        return "editor/grid";
+      case AddButtonAction.grid:
+        return "editor/grid";
+      default:
+        return "";
+    }
+  }
+}

+ 31 - 0
frontend/appflowy_flutter/lib/plugins/database_view/widgets/database_layout_ext.dart

@@ -0,0 +1,31 @@
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
+import 'package:easy_localization/easy_localization.dart';
+
+extension DatabaseLayoutExtension on DatabaseLayoutPB {
+  String layoutName() {
+    switch (this) {
+      case DatabaseLayoutPB.Board:
+        return LocaleKeys.board_menuName.tr();
+      case DatabaseLayoutPB.Calendar:
+        return LocaleKeys.calendar_menuName.tr();
+      case DatabaseLayoutPB.Grid:
+        return LocaleKeys.grid_menuName.tr();
+      default:
+        return "";
+    }
+  }
+
+  String iconName() {
+    switch (this) {
+      case DatabaseLayoutPB.Board:
+        return 'editor/board';
+      case DatabaseLayoutPB.Calendar:
+        return "editor/grid";
+      case DatabaseLayoutPB.Grid:
+        return "editor/grid";
+      default:
+        return "";
+    }
+  }
+}

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

@@ -1,5 +1,4 @@
 import 'package:appflowy/plugins/database_view/application/database_controller.dart';
-import 'package:appflowy/plugins/database_view/application/setting/setting_bloc.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra/theme_extension.dart';
 import 'package:flowy_infra_ui/style_widget/button.dart';
@@ -9,6 +8,7 @@ import 'package:flowy_infra_ui/widget/spacing.dart';
 import 'package:flutter/material.dart';
 
 import '../../grid/presentation/layout/sizes.dart';
+import 'setting_button.dart';
 
 class DatabaseSettingList extends StatelessWidget {
   final DatabaseController databaseContoller;

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

@@ -1,9 +1,9 @@
 import 'package:appflowy/generated/locale_keys.g.dart';
 import 'package:appflowy/plugins/database_view/application/database_controller.dart';
-import 'package:appflowy/plugins/database_view/application/setting/setting_bloc.dart';
 import 'package:appflowy/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart';
 import 'package:appflowy/plugins/database_view/widgets/group/database_group.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra/theme_extension.dart';
@@ -97,7 +97,7 @@ class _DatabaseSettingListPopoverState
         case DatabaseSettingAction.showLayout:
           return DatabaseLayoutList(
             viewId: widget.databaseController.viewId,
-            currentLayout: widget.databaseController.databaseLayout!,
+            currentLayout: widget.databaseController.databaseLayout,
           );
         case DatabaseSettingAction.showGroup:
           return DatabaseGroupList(
@@ -132,7 +132,7 @@ class ICalendarSettingImpl extends ICalendarSetting {
 
   @override
   void updateLayoutSettings(CalendarLayoutSettingPB layoutSettings) {
-    _databaseController.updateCalenderLayoutSetting(layoutSettings);
+    _databaseController.updateLayoutSetting(layoutSettings);
   }
 
   @override
@@ -140,3 +140,63 @@ class ICalendarSettingImpl extends ICalendarSetting {
     return _databaseController.databaseLayoutSetting?.calendar;
   }
 }
+
+enum DatabaseSettingAction {
+  showProperties,
+  showLayout,
+  showGroup,
+  showCalendarLayout,
+}
+
+extension DatabaseSettingActionExtension on DatabaseSettingAction {
+  String iconName() {
+    switch (this) {
+      case DatabaseSettingAction.showProperties:
+        return 'grid/setting/properties';
+      case DatabaseSettingAction.showLayout:
+        return 'grid/setting/database_layout';
+      case DatabaseSettingAction.showGroup:
+        return 'grid/setting/group';
+      case DatabaseSettingAction.showCalendarLayout:
+        return 'grid/setting/calendar_layout';
+    }
+  }
+
+  String title() {
+    switch (this) {
+      case DatabaseSettingAction.showProperties:
+        return LocaleKeys.grid_settings_Properties.tr();
+      case DatabaseSettingAction.showLayout:
+        return LocaleKeys.grid_settings_databaseLayout.tr();
+      case DatabaseSettingAction.showGroup:
+        return LocaleKeys.grid_settings_group.tr();
+      case DatabaseSettingAction.showCalendarLayout:
+        return LocaleKeys.calendar_settings_name.tr();
+    }
+  }
+}
+
+/// Returns the list of actions that should be shown for the given database layout.
+List<DatabaseSettingAction> actionsForDatabaseLayout(DatabaseLayoutPB? layout) {
+  switch (layout) {
+    case DatabaseLayoutPB.Board:
+      return [
+        DatabaseSettingAction.showProperties,
+        DatabaseSettingAction.showLayout,
+        DatabaseSettingAction.showGroup,
+      ];
+    case DatabaseLayoutPB.Calendar:
+      return [
+        DatabaseSettingAction.showProperties,
+        DatabaseSettingAction.showLayout,
+        DatabaseSettingAction.showCalendarLayout,
+      ];
+    case DatabaseLayoutPB.Grid:
+      return [
+        DatabaseSettingAction.showProperties,
+        DatabaseSettingAction.showLayout,
+      ];
+    default:
+      return [];
+  }
+}

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

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

+ 49 - 57
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart

@@ -91,10 +91,12 @@ class _BuiltInPageWidgetState extends State<BuiltInPageWidget> {
       onExit: (_) => widget.editorState.service.scrollService?.enable(),
       child: SizedBox(
         height: 400,
-        child: Stack(
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.start,
+          crossAxisAlignment: CrossAxisAlignment.start,
           children: [
             _buildMenu(context, viewPB),
-            _buildPage(context, viewPB),
+            Expanded(child: _buildPage(context, viewPB)),
           ],
         ),
       ),
@@ -114,68 +116,58 @@ class _BuiltInPageWidgetState extends State<BuiltInPageWidget> {
   }
 
   Widget _buildMenu(BuildContext context, ViewPB viewPB) {
-    return Positioned(
-      top: 5,
-      left: 5,
-      child: Row(
-        mainAxisSize: MainAxisSize.min,
-        mainAxisAlignment: MainAxisAlignment.center,
-        children: [
-          // information
-          FlowyIconButton(
-            tooltipText: LocaleKeys.tooltip_referencePage.tr(
-              namedArgs: {'name': viewPB.layout.name},
-            ),
+    return Row(
+      mainAxisSize: MainAxisSize.min,
+      mainAxisAlignment: MainAxisAlignment.center,
+      children: [
+        // information
+        FlowyIconButton(
+          tooltipText: LocaleKeys.tooltip_referencePage.tr(
+            namedArgs: {'name': viewPB.layout.name},
+          ),
+          width: 24,
+          height: 24,
+          iconPadding: const EdgeInsets.all(3),
+          icon: svgWidget(
+            'common/information',
+            color: Theme.of(context).iconTheme.color,
+          ),
+        ),
+        // setting
+        const Space(7, 0),
+        PopoverActionList<_ActionWrapper>(
+          direction: PopoverDirection.bottomWithCenterAligned,
+          actions: _ActionType.values
+              .map((action) => _ActionWrapper(action))
+              .toList(),
+          buildChild: (controller) => FlowyIconButton(
+            tooltipText: LocaleKeys.tooltip_openMenu.tr(),
             width: 24,
             height: 24,
             iconPadding: const EdgeInsets.all(3),
             icon: svgWidget(
-              'common/information',
+              'common/settings',
               color: Theme.of(context).iconTheme.color,
             ),
+            onPressed: () => controller.show(),
           ),
-          // Name
-          const Space(7, 0),
-          FlowyText.medium(
-            viewPB.name,
-            fontSize: 16.0,
-          ),
-          // setting
-          const Space(7, 0),
-          PopoverActionList<_ActionWrapper>(
-            direction: PopoverDirection.bottomWithCenterAligned,
-            actions: _ActionType.values
-                .map((action) => _ActionWrapper(action))
-                .toList(),
-            buildChild: (controller) => FlowyIconButton(
-              tooltipText: LocaleKeys.tooltip_openMenu.tr(),
-              width: 24,
-              height: 24,
-              iconPadding: const EdgeInsets.all(3),
-              icon: svgWidget(
-                'common/settings',
-                color: Theme.of(context).iconTheme.color,
-              ),
-              onPressed: () => controller.show(),
-            ),
-            onSelected: (action, controller) async {
-              switch (action.inner) {
-                case _ActionType.viewDatabase:
-                  getIt<MenuSharedState>().latestOpenView = viewPB;
-
-                  getIt<HomeStackManager>().setPlugin(viewPB.plugin());
-                  break;
-                case _ActionType.delete:
-                  final transaction = widget.editorState.transaction;
-                  transaction.deleteNode(widget.node);
-                  widget.editorState.apply(transaction);
-                  break;
-              }
-              controller.close();
-            },
-          )
-        ],
-      ),
+          onSelected: (action, controller) async {
+            switch (action.inner) {
+              case _ActionType.viewDatabase:
+                getIt<MenuSharedState>().latestOpenView = viewPB;
+
+                getIt<HomeStackManager>().setPlugin(viewPB.plugin());
+                break;
+              case _ActionType.delete:
+                final transaction = widget.editorState.transaction;
+                transaction.deleteNode(widget.node);
+                widget.editorState.apply(transaction);
+                break;
+            }
+            controller.close();
+          },
+        )
+      ],
     );
   }
 

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

@@ -53,7 +53,7 @@ extension InsertDatabase on EditorState {
     }
 
     final prefix = _referencedDatabasePrefix(childView.layout);
-    final ref = await ViewBackendService.createDatabaseReferenceView(
+    final ref = await ViewBackendService.createDatabaseLinkedView(
       parentViewId: childView.id,
       name: "$prefix ${childView.name}",
       layoutType: childView.layout,

+ 2 - 14
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart

@@ -1,4 +1,5 @@
 import 'package:appflowy/plugins/document/presentation/editor_plugins/base/insert_page_command.dart';
+import 'package:appflowy/workspace/application/view/view_ext.dart';
 import 'package:appflowy/workspace/application/view/view_service.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
@@ -212,7 +213,7 @@ class _LinkToPageMenuState extends State<LinkToPageMenu> {
                     FlowyButton(
                       isSelected: index == _selectedIndex,
                       leftIcon: svgWidget(
-                        _iconName(value),
+                        value.iconName,
                         color: Theme.of(context).iconTheme.color,
                       ),
                       text: FlowyText.regular(value.name),
@@ -238,19 +239,6 @@ class _LinkToPageMenuState extends State<LinkToPageMenu> {
       future: items,
     );
   }
-
-  String _iconName(ViewPB viewPB) {
-    switch (viewPB.layout) {
-      case ViewLayoutPB.Grid:
-        return 'editor/grid';
-      case ViewLayoutPB.Board:
-        return 'editor/board';
-      case ViewLayoutPB.Calendar:
-        return 'editor/calendar';
-      default:
-        throw Exception('Unknown layout type');
-    }
-  }
 }
 
 extension on ViewLayoutPB {

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

@@ -1,14 +1,10 @@
 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;
@@ -18,30 +14,18 @@ class ViewPluginNotifier extends PluginNotifier<Option<DeletedViewPB>> {
 
   ViewPluginNotifier({
     required this.view,
-    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;
-          }
-        },
-        onViewMoveToTrash: (result) {
-          result.fold(
-            (deletedView) => isDeleted.value = some(deletedView),
-            (err) => Log.error(err),
-          );
-        },
-      );
-    }
+    _viewListener?.start(
+      onViewUpdated: (updatedView) {
+        view = updatedView;
+      },
+      onViewMoveToTrash: (result) {
+        result.fold(
+          (deletedView) => isDeleted.value = some(deletedView),
+          (err) => Log.error(err),
+        );
+      },
+    );
   }
 
   @override

+ 22 - 22
frontend/appflowy_flutter/lib/workspace/application/app/app_bloc.dart

@@ -1,9 +1,7 @@
 import 'dart:collection';
 
-import 'package:appflowy/startup/plugin/plugin.dart';
 import 'package:appflowy/startup/startup.dart';
-import 'package:appflowy/workspace/application/app/app_listener.dart';
-import 'package:appflowy/workspace/application/view/view_service.dart';
+import 'package:appflowy/workspace/application/view/prelude.dart';
 import 'package:appflowy/workspace/presentation/home/menu/menu.dart';
 import 'package:expandable/expandable.dart';
 import 'package:appflowy_backend/log.dart';
@@ -18,11 +16,11 @@ part 'app_bloc.freezed.dart';
 
 class AppBloc extends Bloc<AppEvent, AppState> {
   final ViewBackendService appService;
-  final AppListener appListener;
+  final ViewListener viewListener;
 
   AppBloc({required ViewPB view})
       : appService = ViewBackendService(),
-        appListener = AppListener(viewId: view.id),
+        viewListener = ViewListener(viewId: view.id),
         super(AppState.initial(view)) {
     on<AppEvent>((event, emit) async {
       await event.map(
@@ -47,10 +45,10 @@ class AppBloc extends Bloc<AppEvent, AppState> {
         },
         appDidUpdate: (e) async {
           final latestCreatedView = state.latestCreatedView;
-          final views = e.app.childViews;
+          final views = e.view.childViews;
           AppState newState = state.copyWith(
             views: views,
-            view: e.app,
+            view: e.view,
           );
           if (latestCreatedView != null) {
             final index = views
@@ -67,8 +65,8 @@ class AppBloc extends Bloc<AppEvent, AppState> {
   }
 
   void _startListening() {
-    appListener.start(
-      onAppUpdated: (app) {
+    viewListener.start(
+      onViewUpdated: (app) {
         if (!isClosed) {
           add(AppEvent.appDidUpdate(app));
         }
@@ -110,7 +108,7 @@ class AppBloc extends Bloc<AppEvent, AppState> {
       parentViewId: state.view.id,
       name: value.name,
       desc: value.desc ?? "",
-      layoutType: value.pluginBuilder.layoutType!,
+      layoutType: value.layoutType,
       initialDataBytes: value.initialDataBytes,
       ext: value.ext ?? {},
       openAfterCreate: true,
@@ -131,13 +129,13 @@ class AppBloc extends Bloc<AppEvent, AppState> {
 
   @override
   Future<void> close() async {
-    await appListener.stop();
+    await viewListener.stop();
     return super.close();
   }
 
   Future<void> _loadViews(Emitter<AppState> emit) async {
     final viewsOrFailed =
-        await ViewBackendService.getViews(viewId: state.view.id);
+        await ViewBackendService.getChildViews(viewId: state.view.id);
     viewsOrFailed.fold(
       (views) => emit(state.copyWith(views: views)),
       (error) {
@@ -153,7 +151,7 @@ class AppEvent with _$AppEvent {
   const factory AppEvent.initial() = Initial;
   const factory AppEvent.createView(
     String name,
-    PluginBuilder pluginBuilder, {
+    ViewLayoutPB layoutType, {
     String? desc,
 
     /// ~~The initial data should be the JSON of the document~~
@@ -172,7 +170,7 @@ class AppEvent with _$AppEvent {
   const factory AppEvent.delete() = DeleteApp;
   const factory AppEvent.deleteView(String viewId) = DeleteView;
   const factory AppEvent.rename(String newName) = Rename;
-  const factory AppEvent.appDidUpdate(ViewPB app) = AppDidUpdate;
+  const factory AppEvent.appDidUpdate(ViewPB view) = AppDidUpdate;
 }
 
 @freezed
@@ -191,7 +189,7 @@ class AppState with _$AppState {
       );
 }
 
-class AppViewDataContext extends ChangeNotifier {
+class ViewDataContext extends ChangeNotifier {
   final String viewId;
   final ValueNotifier<List<ViewPB>> _viewsNotifier = ValueNotifier([]);
   final ValueNotifier<ViewPB?> _selectedViewNotifier = ValueNotifier(null);
@@ -199,7 +197,7 @@ class AppViewDataContext extends ChangeNotifier {
   ExpandableController expandController =
       ExpandableController(initialExpanded: false);
 
-  AppViewDataContext({required this.viewId}) {
+  ViewDataContext({required this.viewId}) {
     _setLatestView(getIt<MenuSharedState>().latestOpenView);
     _menuSharedStateListener =
         getIt<MenuSharedState>().addLatestViewListener((view) {
@@ -207,7 +205,7 @@ class AppViewDataContext extends ChangeNotifier {
     });
   }
 
-  VoidCallback addSelectedViewChangeListener(void Function(ViewPB?) callback) {
+  VoidCallback onViewSelected(void Function(ViewPB?) callback) {
     listener() {
       callback(_selectedViewNotifier.value);
     }
@@ -216,7 +214,7 @@ class AppViewDataContext extends ChangeNotifier {
     return listener;
   }
 
-  void removeSelectedViewListener(VoidCallback listener) {
+  void removeOnViewSelectedListener(VoidCallback listener) {
     _selectedViewNotifier.removeListener(listener);
   }
 
@@ -235,7 +233,6 @@ class AppViewDataContext extends ChangeNotifier {
   set views(List<ViewPB> views) {
     if (_viewsNotifier.value != views) {
       _viewsNotifier.value = views;
-      _expandIfNeed();
       notifyListeners();
     }
   }
@@ -243,7 +240,7 @@ class AppViewDataContext extends ChangeNotifier {
   UnmodifiableListView<ViewPB> get views =>
       UnmodifiableListView(_viewsNotifier.value);
 
-  VoidCallback addViewsChangeListener(
+  VoidCallback onViewsChanged(
     void Function(UnmodifiableListView<ViewPB>) callback,
   ) {
     listener() {
@@ -254,7 +251,7 @@ class AppViewDataContext extends ChangeNotifier {
     return listener;
   }
 
-  void removeViewsListener(VoidCallback listener) {
+  void removeOnViewChangedListener(VoidCallback listener) {
     _viewsNotifier.removeListener(listener);
   }
 
@@ -263,7 +260,10 @@ class AppViewDataContext extends ChangeNotifier {
       return;
     }
 
-    if (!_viewsNotifier.value.contains(_selectedViewNotifier.value)) {
+    if (!_viewsNotifier.value
+        .map((e) => e.id)
+        .toList()
+        .contains(_selectedViewNotifier.value?.id)) {
       return;
     }
 

+ 0 - 61
frontend/appflowy_flutter/lib/workspace/application/app/app_listener.dart

@@ -1,61 +0,0 @@
-import 'dart:async';
-import 'dart:typed_data';
-import 'package:appflowy/core/notification/folder_notification.dart';
-import 'package:dartz/dartz.dart';
-import 'package:appflowy_backend/log.dart';
-import 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart';
-import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
-import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
-import 'package:appflowy_backend/protobuf/flowy-folder2/notification.pb.dart';
-import 'package:appflowy_backend/rust_stream.dart';
-
-typedef AppDidUpdateCallback = void Function(ViewPB app);
-typedef ViewsDidChangeCallback = void Function(
-  Either<List<ViewPB>, FlowyError> viewsOrFailed,
-);
-
-class AppListener {
-  StreamSubscription<SubscribeObject>? _subscription;
-  AppDidUpdateCallback? _updated;
-  FolderNotificationParser? _parser;
-  String viewId;
-
-  AppListener({
-    required this.viewId,
-  });
-
-  void start({AppDidUpdateCallback? onAppUpdated}) {
-    _updated = onAppUpdated;
-    _parser = FolderNotificationParser(id: viewId, callback: _handleCallback);
-    _subscription =
-        RustStreamReceiver.listen((observable) => _parser?.parse(observable));
-  }
-
-  void _handleCallback(
-    FolderNotification ty,
-    Either<Uint8List, FlowyError> result,
-  ) {
-    switch (ty) {
-      case FolderNotification.DidUpdateView:
-      case FolderNotification.DidUpdateChildViews:
-        if (_updated != null) {
-          result.fold(
-            (payload) {
-              final app = ViewPB.fromBuffer(payload);
-              _updated!(app);
-            },
-            (error) => Log.error(error),
-          );
-        }
-        break;
-      default:
-        break;
-    }
-  }
-
-  Future<void> stop() async {
-    _parser = null;
-    await _subscription?.cancel();
-    _updated = null;
-  }
-}

+ 0 - 1
frontend/appflowy_flutter/lib/workspace/application/app/prelude.dart

@@ -1,2 +1 @@
 export 'app_bloc.dart';
-export 'app_listener.dart';

+ 22 - 29
frontend/appflowy_flutter/lib/workspace/application/menu/menu_view_section_bloc.dart

@@ -12,65 +12,58 @@ part 'menu_view_section_bloc.freezed.dart';
 class ViewSectionBloc extends Bloc<ViewSectionEvent, ViewSectionState> {
   void Function()? _viewsListener;
   void Function()? _selectedViewlistener;
-  final AppViewDataContext _appViewData;
+  final ViewDataContext _appViewData;
 
   ViewSectionBloc({
-    required AppViewDataContext appViewData,
+    required ViewDataContext appViewData,
   })  : _appViewData = appViewData,
         super(ViewSectionState.initial(appViewData)) {
     on<ViewSectionEvent>((event, emit) async {
-      await event.map(
-        initial: (e) async {
+      await event.when(
+        initial: () async {
           _startListening();
         },
-        setSelectedView: (_SetSelectedView value) {
-          _setSelectView(value, emit);
+        setSelectedView: (view) {
+          emit(state.copyWith(selectedView: view));
         },
-        didReceiveViewUpdated: (_DidReceiveViewUpdated value) {
-          emit(state.copyWith(views: value.views));
+        didReceiveViewUpdated: (views) {
+          emit(state.copyWith(views: views));
         },
-        moveView: (_MoveView value) async {
-          _moveView(value, emit);
+        moveView: (fromIndex, toIndex) async {
+          _moveView(fromIndex, toIndex, emit);
         },
       );
     });
   }
 
   void _startListening() {
-    _viewsListener = _appViewData.addViewsChangeListener((views) {
+    _viewsListener = _appViewData.onViewsChanged((views) {
       if (!isClosed) {
         add(ViewSectionEvent.didReceiveViewUpdated(views));
       }
     });
-    _selectedViewlistener = _appViewData.addSelectedViewChangeListener((view) {
+    _selectedViewlistener = _appViewData.onViewSelected((view) {
       if (!isClosed) {
         add(ViewSectionEvent.setSelectedView(view));
       }
     });
   }
 
-  void _setSelectView(_SetSelectedView value, Emitter<ViewSectionState> emit) {
-    if (state.views.contains(value.view)) {
-      emit(state.copyWith(selectedView: value.view));
-    } else {
-      emit(state.copyWith(selectedView: null));
-    }
-  }
-
   Future<void> _moveView(
-    _MoveView value,
+    int fromIndex,
+    int toIndex,
     Emitter<ViewSectionState> emit,
   ) async {
-    if (value.fromIndex < state.views.length) {
-      final viewId = state.views[value.fromIndex].id;
+    if (fromIndex < state.views.length) {
+      final viewId = state.views[fromIndex].id;
       final views = List<ViewPB>.from(state.views);
-      views.insert(value.toIndex, views.removeAt(value.fromIndex));
+      views.insert(toIndex, views.removeAt(fromIndex));
       emit(state.copyWith(views: views));
 
       final result = await ViewBackendService.moveView(
         viewId: viewId,
-        fromIndex: value.fromIndex,
-        toIndex: value.toIndex,
+        fromIndex: fromIndex,
+        toIndex: toIndex,
       );
       result.fold((l) => null, (err) => Log.error(err));
     }
@@ -79,11 +72,11 @@ class ViewSectionBloc extends Bloc<ViewSectionEvent, ViewSectionState> {
   @override
   Future<void> close() async {
     if (_selectedViewlistener != null) {
-      _appViewData.removeSelectedViewListener(_selectedViewlistener!);
+      _appViewData.removeOnViewSelectedListener(_selectedViewlistener!);
     }
 
     if (_viewsListener != null) {
-      _appViewData.removeViewsListener(_viewsListener!);
+      _appViewData.removeOnViewChangedListener(_viewsListener!);
     }
 
     return super.close();
@@ -108,7 +101,7 @@ class ViewSectionState with _$ViewSectionState {
     ViewPB? selectedView,
   }) = _ViewSectionState;
 
-  factory ViewSectionState.initial(AppViewDataContext appViewData) =>
+  factory ViewSectionState.initial(ViewDataContext appViewData) =>
       ViewSectionState(
         views: appViewData.views,
         selectedView: appViewData.selectedView,

+ 32 - 15
frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart

@@ -1,6 +1,7 @@
-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/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/grid_page.dart';
+import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart';
 import 'package:appflowy/plugins/document/document.dart';
 import 'package:appflowy/startup/plugin/plugin.dart';
 import 'package:flowy_infra/image.dart';
@@ -62,22 +63,11 @@ extension ViewExtension on ViewPB {
   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 GridPlugin(
+        return DatabaseTabBarViewPlugin(
           view: this,
           pluginType: pluginType,
-          listenOnViewChanged: listenOnViewChanged,
         );
       case ViewLayoutPB.Document:
         return DocumentPlugin(
@@ -88,4 +78,31 @@ extension ViewExtension on ViewPB {
     }
     throw UnimplementedError;
   }
+
+  DatabaseTabBarItemBuilder tarBarItem() {
+    switch (layout) {
+      case ViewLayoutPB.Board:
+        return BoardPageTabBarBuilderImpl();
+      case ViewLayoutPB.Calendar:
+        return CalendarPageTabBarBuilderImpl();
+      case ViewLayoutPB.Grid:
+        return GridPageTabBarBuilderImpl();
+      case ViewLayoutPB.Document:
+        throw UnimplementedError;
+    }
+    throw UnimplementedError;
+  }
+
+  String get iconName {
+    switch (layout) {
+      case ViewLayoutPB.Grid:
+        return 'editor/grid';
+      case ViewLayoutPB.Board:
+        return 'editor/board';
+      case ViewLayoutPB.Calendar:
+        return 'editor/calendar';
+      default:
+        throw Exception('Unknown layout type');
+    }
+  }
 }

+ 12 - 0
frontend/appflowy_flutter/lib/workspace/application/view/view_listener.dart

@@ -21,6 +21,7 @@ typedef MoveToTrashNotifiedValue = Either<DeletedViewPB, FlowyError>;
 class ViewListener {
   StreamSubscription<SubscribeObject>? _subscription;
   void Function(UpdateViewNotifiedValue)? _updatedViewNotifier;
+  void Function(ChildViewUpdatePB)? _updateViewChildViewsNotifier;
   void Function(DeleteViewNotifyValue)? _deletedNotifier;
   void Function(RestoreViewNotifiedValue)? _restoredNotifier;
   void Function(MoveToTrashNotifiedValue)? _moveToTrashNotifier;
@@ -35,6 +36,7 @@ class ViewListener {
 
   void start({
     void Function(UpdateViewNotifiedValue)? onViewUpdated,
+    void Function(ChildViewUpdatePB)? onViewChildViewsUpdated,
     void Function(DeleteViewNotifyValue)? onViewDeleted,
     void Function(RestoreViewNotifiedValue)? onViewRestored,
     void Function(MoveToTrashNotifiedValue)? onViewMoveToTrash,
@@ -48,6 +50,7 @@ class ViewListener {
     _deletedNotifier = onViewDeleted;
     _restoredNotifier = onViewRestored;
     _moveToTrashNotifier = onViewMoveToTrash;
+    _updateViewChildViewsNotifier = onViewChildViewsUpdated;
 
     _parser = FolderNotificationParser(
       id: viewId,
@@ -74,6 +77,15 @@ class ViewListener {
           (error) => Log.error(error),
         );
         break;
+      case FolderNotification.DidUpdateChildViews:
+        result.fold(
+          (payload) {
+            final pb = ChildViewUpdatePB.fromBuffer(payload);
+            _updateViewChildViewsNotifier?.call(pb);
+          },
+          (error) => Log.error(error),
+        );
+        break;
       case FolderNotification.DidDeleteView:
         result.fold(
           (payload) => _deletedNotifier?.call(left(ViewPB.fromBuffer(payload))),

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

@@ -73,7 +73,7 @@ class ViewBackendService {
     return FolderEventCreateOrphanView(payload).send();
   }
 
-  static Future<Either<ViewPB, FlowyError>> createDatabaseReferenceView({
+  static Future<Either<ViewPB, FlowyError>> createDatabaseLinkedView({
     required String parentViewId,
     required String databaseId,
     required ViewLayoutPB layoutType,
@@ -91,14 +91,14 @@ class ViewBackendService {
   }
 
   /// Returns a list of views that are the children of the given [viewId].
-  static Future<Either<List<ViewPB>, FlowyError>> getViews({
+  static Future<Either<List<ViewPB>, FlowyError>> getChildViews({
     required String viewId,
   }) {
     final payload = ViewIdPB.create()..value = viewId;
 
     return FolderEventReadView(payload).send().then((result) {
       return result.fold(
-        (app) => left(app.childViews),
+        (view) => left(view.childViews),
         (error) => right(error),
       );
     });
@@ -163,7 +163,7 @@ class ViewBackendService {
       if (workspaces != null) {
         final views = workspaces.workspace.views;
         for (final view in views) {
-          final childViews = await getViews(viewId: view.id).then(
+          final childViews = await getChildViews(viewId: view.id).then(
             (value) => value
                 .getLeftOrNull<List<ViewPB>>()
                 ?.where((e) => e.layout == layoutType)

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

@@ -113,7 +113,7 @@ class MenuAppHeader extends StatelessWidget {
           context.read<AppBloc>().add(
                 AppEvent.createView(
                   name ?? LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
-                  pluginBuilder,
+                  pluginBuilder.layoutType!,
                   initialDataBytes: initialDataBytes,
                   openAfterCreated: openAfterCreated,
                 ),

+ 4 - 4
frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/menu_app.dart

@@ -17,11 +17,11 @@ class MenuApp extends StatefulWidget {
 }
 
 class _MenuAppState extends State<MenuApp> {
-  late AppViewDataContext viewDataContext;
+  late ViewDataContext viewDataContext;
 
   @override
   void initState() {
-    viewDataContext = AppViewDataContext(viewId: widget.view.id);
+    viewDataContext = ViewDataContext(viewId: widget.view.id);
     super.initState();
   }
 
@@ -56,7 +56,7 @@ class _MenuAppState extends State<MenuApp> {
           builder: (context, state) {
             return ChangeNotifierProvider.value(
               value: viewDataContext,
-              child: Consumer<AppViewDataContext>(
+              child: Consumer<ViewDataContext>(
                 builder: (context, viewDataContext, _) {
                   return expandableWrapper(context, viewDataContext);
                 },
@@ -70,7 +70,7 @@ class _MenuAppState extends State<MenuApp> {
 
   ExpandableNotifier expandableWrapper(
     BuildContext context,
-    AppViewDataContext viewDataContext,
+    ViewDataContext viewDataContext,
   ) {
     return ExpandableNotifier(
       controller: viewDataContext.expandController,

+ 2 - 2
frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/section/item.dart

@@ -24,12 +24,12 @@ class ViewSectionItem extends StatelessWidget {
   final ViewPB view;
   final void Function(ViewPB) onSelected;
 
-  ViewSectionItem({
+  const ViewSectionItem({
     Key? key,
     required this.view,
     required this.isSelected,
     required this.onSelected,
-  }) : super(key: ValueKey('$view.hashCode/$isSelected'));
+  }) : super(key: key);
 
   @override
   Widget build(BuildContext context) {

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

@@ -11,7 +11,7 @@ import 'package:reorderables/reorderables.dart';
 import 'item.dart';
 
 class ViewSection extends StatelessWidget {
-  final AppViewDataContext appViewData;
+  final ViewDataContext appViewData;
   const ViewSection({Key? key, required this.appViewData}) : super(key: key);
 
   @override
@@ -47,10 +47,11 @@ class ViewSection extends StatelessWidget {
     ViewSectionState state,
   ) {
     final children = state.views.map((view) {
+      final isSelected = _isViewSelected(state, view.id);
       return ViewSectionItem(
-        key: ValueKey(view.id),
         view: view,
-        isSelected: _isViewSelected(state, view.id),
+        key: ValueKey('$view.hashCode/$isSelected'),
+        isSelected: isSelected,
         onSelected: (view) => getIt<MenuSharedState>().latestOpenView = view,
       );
     }).toList();

+ 3 - 0
frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart

@@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
 class FlowyButton extends StatelessWidget {
   final Widget text;
   final VoidCallback? onTap;
+  final VoidCallback? onSecondaryTap;
   final void Function(bool)? onHover;
   final EdgeInsets? margin;
   final Widget? leftIcon;
@@ -25,6 +26,7 @@ class FlowyButton extends StatelessWidget {
     Key? key,
     required this.text,
     this.onTap,
+    this.onSecondaryTap,
     this.onHover,
     this.margin,
     this.leftIcon,
@@ -45,6 +47,7 @@ class FlowyButton extends StatelessWidget {
       return GestureDetector(
         behavior: HitTestBehavior.opaque,
         onTap: onTap,
+        onSecondaryTap: onSecondaryTap,
         child: FlowyHover(
           style: HoverStyle(
             borderRadius: radius ?? Corners.s6Border,

+ 6 - 2
frontend/appflowy_flutter/test/bloc_test/board_test/create_card_test.dart

@@ -1,3 +1,4 @@
+import 'package:appflowy/plugins/database_view/application/database_controller.dart';
 import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart';
 import 'package:flutter_test/flutter_test.dart';
 
@@ -12,8 +13,11 @@ void main() {
 
   test('create kanban baord card', () async {
     final context = await boardTest.createTestBoard();
-    final boardBloc = BoardBloc(view: context.gridView)
-      ..add(const BoardEvent.initial());
+    final databaseController = DatabaseController(view: context.gridView);
+    final boardBloc = BoardBloc(
+      view: context.gridView,
+      databaseController: databaseController,
+    )..add(const BoardEvent.initial());
     await boardResponseFuture();
 
     final groupId = boardBloc.state.groupIds.first;

+ 13 - 6
frontend/appflowy_flutter/test/bloc_test/board_test/create_or_edit_field_test.dart

@@ -1,3 +1,4 @@
+import 'package:appflowy/plugins/database_view/application/database_controller.dart';
 import 'package:appflowy/plugins/database_view/application/field/field_editor_bloc.dart';
 import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
 import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart';
@@ -14,8 +15,10 @@ void main() {
 
   test('create build-in kanban board test', () async {
     final context = await boardTest.createTestBoard();
-    final boardBloc = BoardBloc(view: context.gridView)
-      ..add(const BoardEvent.initial());
+    final boardBloc = BoardBloc(
+      view: context.gridView,
+      databaseController: DatabaseController(view: context.gridView),
+    )..add(const BoardEvent.initial());
     await boardResponseFuture();
 
     assert(boardBloc.groupControllers.values.length == 4);
@@ -24,8 +27,10 @@ void main() {
 
   test('edit kanban board field name test', () async {
     final context = await boardTest.createTestBoard();
-    final boardBloc = BoardBloc(view: context.gridView)
-      ..add(const BoardEvent.initial());
+    final boardBloc = BoardBloc(
+      view: context.gridView,
+      databaseController: DatabaseController(view: context.gridView),
+    )..add(const BoardEvent.initial());
     await boardResponseFuture();
 
     final fieldInfo = context.singleSelectFieldContext();
@@ -58,8 +63,10 @@ void main() {
 
   test('create a new field in kanban board test', () async {
     final context = await boardTest.createTestBoard();
-    final boardBloc = BoardBloc(view: context.gridView)
-      ..add(const BoardEvent.initial());
+    final boardBloc = BoardBloc(
+      view: context.gridView,
+      databaseController: DatabaseController(view: context.gridView),
+    )..add(const BoardEvent.initial());
     await boardResponseFuture();
 
     await context.createField(FieldType.Checkbox);

+ 5 - 2
frontend/appflowy_flutter/test/bloc_test/board_test/group_by_checkbox_field_test.dart

@@ -1,3 +1,4 @@
+import 'package:appflowy/plugins/database_view/application/database_controller.dart';
 import 'package:appflowy/plugins/database_view/application/setting/group_bloc.dart';
 import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
@@ -15,8 +16,10 @@ void main() {
   // Group by checkbox field
   test('group by checkbox field test', () async {
     final context = await boardTest.createTestBoard();
-    final boardBloc = BoardBloc(view: context.gridView)
-      ..add(const BoardEvent.initial());
+    final boardBloc = BoardBloc(
+      view: context.gridView,
+      databaseController: DatabaseController(view: context.gridView),
+    )..add(const BoardEvent.initial());
     await boardResponseFuture();
 
     // assert the initial values

+ 9 - 4
frontend/appflowy_flutter/test/bloc_test/board_test/group_by_multi_select_field_test.dart

@@ -1,4 +1,5 @@
 import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
+import 'package:appflowy/plugins/database_view/application/database_controller.dart';
 import 'package:appflowy/plugins/database_view/application/setting/group_bloc.dart';
 import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart';
 import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor_bloc.dart';
@@ -39,8 +40,10 @@ void main() {
     await boardResponseFuture();
 
     //assert only have the 'No status' group
-    final boardBloc = BoardBloc(view: context.gridView)
-      ..add(const BoardEvent.initial());
+    final boardBloc = BoardBloc(
+      view: context.gridView,
+      databaseController: DatabaseController(view: context.gridView),
+    )..add(const BoardEvent.initial());
     await boardResponseFuture();
     assert(
       boardBloc.groupControllers.values.length == 1,
@@ -92,8 +95,10 @@ void main() {
     await boardResponseFuture();
 
     // assert there are only three group
-    final boardBloc = BoardBloc(view: context.gridView)
-      ..add(const BoardEvent.initial());
+    final boardBloc = BoardBloc(
+      view: context.gridView,
+      databaseController: DatabaseController(view: context.gridView),
+    )..add(const BoardEvent.initial());
     await boardResponseFuture();
     assert(
       boardBloc.groupControllers.values.length == 3,

+ 7 - 2
frontend/appflowy_flutter/test/bloc_test/board_test/group_by_unsupport_field_test.dart

@@ -1,3 +1,4 @@
+import 'package:appflowy/plugins/database_view/application/database_controller.dart';
 import 'package:appflowy/plugins/database_view/application/field/field_editor_bloc.dart';
 import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart';
 import 'package:bloc_test/bloc_test.dart';
@@ -39,8 +40,12 @@ void main() {
     );
     blocTest<BoardBloc, BoardState>(
       'assert the number of groups is 1',
-      build: () =>
-          BoardBloc(view: context.gridView)..add(const BoardEvent.initial()),
+      build: () => BoardBloc(
+        view: context.gridView,
+        databaseController: DatabaseController(view: context.gridView),
+      )..add(
+          const BoardEvent.initial(),
+        ),
       wait: boardResponseDuration(),
       verify: (bloc) {
         assert(

+ 2 - 3
frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_util.dart

@@ -1,16 +1,15 @@
 import 'package:appflowy/plugins/database_view/application/database_controller.dart';
-import 'package:appflowy/plugins/database_view/grid/grid.dart';
 import 'package:appflowy/workspace/application/view/view_service.dart';
+import 'package:appflowy_backend/protobuf/flowy-folder2/view.pbenum.dart';
 
 import '../util.dart';
 
 Future<GridTestContext> createTestFilterGrid(AppFlowyGridTest gridTest) async {
   final app = await gridTest.unitTest.createTestApp();
-  final builder = GridPluginBuilder();
   final context = await ViewBackendService.createView(
     parentViewId: app.id,
     name: "Filter Grid",
-    layoutType: builder.layoutType!,
+    layoutType: ViewLayoutPB.Grid,
     openAfterCreate: true,
   ).then((result) {
     return result.fold(

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

@@ -9,7 +9,6 @@ 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/database_controller.dart';
 import 'package:appflowy/plugins/database_view/grid/application/row/row_bloc.dart';
-import 'package:appflowy/plugins/database_view/grid/grid.dart';
 import 'package:appflowy/workspace/application/view/view_service.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart';
@@ -172,11 +171,10 @@ class AppFlowyGridTest {
 
   Future<GridTestContext> createTestGrid() async {
     final app = await unitTest.createTestApp();
-    final builder = GridPluginBuilder();
     final context = await ViewBackendService.createView(
       parentViewId: app.id,
       name: "Test Grid",
-      layoutType: builder.layoutType!,
+      layoutType: ViewLayoutPB.Grid,
       openAfterCreate: true,
     ).then((result) {
       return result.fold(

+ 14 - 15
frontend/appflowy_flutter/test/bloc_test/home_test/app_bloc_test.dart

@@ -1,9 +1,8 @@
-import 'package:appflowy/plugins/database_view/grid/grid.dart';
 import 'package:appflowy/plugins/document/application/doc_bloc.dart';
-import 'package:appflowy/plugins/document/document.dart';
 import 'package:appflowy/workspace/application/app/app_bloc.dart';
 import 'package:appflowy/workspace/application/menu/menu_view_section_bloc.dart';
 import 'package:appflowy_backend/dispatch/dispatch.dart';
+import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
 import 'package:flutter_test/flutter_test.dart';
 import '../../util.dart';
 
@@ -41,11 +40,11 @@ void main() {
     final bloc = AppBloc(view: app)..add(const AppEvent.initial());
     await blocResponseFuture();
 
-    bloc.add(AppEvent.createView("1", DocumentPluginBuilder()));
+    bloc.add(const AppEvent.createView("1", ViewLayoutPB.Document));
     await blocResponseFuture();
-    bloc.add(AppEvent.createView("2", DocumentPluginBuilder()));
+    bloc.add(const AppEvent.createView("2", ViewLayoutPB.Document));
     await blocResponseFuture();
-    bloc.add(AppEvent.createView("3", DocumentPluginBuilder()));
+    bloc.add(const AppEvent.createView("3", ViewLayoutPB.Document));
     await blocResponseFuture();
 
     assert(bloc.state.views[0].name == '1');
@@ -58,15 +57,15 @@ void main() {
     final bloc = AppBloc(view: app)..add(const AppEvent.initial());
     await blocResponseFuture();
 
-    bloc.add(AppEvent.createView("1", DocumentPluginBuilder()));
+    bloc.add(const AppEvent.createView("1", ViewLayoutPB.Document));
     await blocResponseFuture();
-    bloc.add(AppEvent.createView("2", DocumentPluginBuilder()));
+    bloc.add(const AppEvent.createView("2", ViewLayoutPB.Document));
     await blocResponseFuture();
-    bloc.add(AppEvent.createView("3", DocumentPluginBuilder()));
+    bloc.add(const AppEvent.createView("3", ViewLayoutPB.Document));
     await blocResponseFuture();
     assert(bloc.state.views.length == 3);
 
-    final appViewData = AppViewDataContext(viewId: app.id);
+    final appViewData = ViewDataContext(viewId: app.id);
     appViewData.views = bloc.state.views;
 
     final viewSectionBloc = ViewSectionBloc(
@@ -91,14 +90,14 @@ void main() {
       "assert initial latest create view is null after initialize",
     );
 
-    bloc.add(AppEvent.createView("1", DocumentPluginBuilder()));
+    bloc.add(const AppEvent.createView("1", ViewLayoutPB.Document));
     await blocResponseFuture();
     assert(
       bloc.state.latestCreatedView!.id == bloc.state.views.last.id,
       "create a view and assert the latest create view is this view",
     );
 
-    bloc.add(AppEvent.createView("2", DocumentPluginBuilder()));
+    bloc.add(const AppEvent.createView("2", ViewLayoutPB.Document));
     await blocResponseFuture();
     assert(
       bloc.state.latestCreatedView!.id == bloc.state.views.last.id,
@@ -111,12 +110,12 @@ void main() {
     final bloc = AppBloc(view: app)..add(const AppEvent.initial());
     await blocResponseFuture();
 
-    bloc.add(AppEvent.createView("document 1", DocumentPluginBuilder()));
+    bloc.add(const AppEvent.createView("document 1", ViewLayoutPB.Document));
     await blocResponseFuture();
     final document1 = bloc.state.latestCreatedView;
     assert(document1!.name == "document 1");
 
-    bloc.add(AppEvent.createView("document 2", DocumentPluginBuilder()));
+    bloc.add(const AppEvent.createView("document 2", ViewLayoutPB.Document));
     await blocResponseFuture();
     final document2 = bloc.state.latestCreatedView;
     assert(document2!.name == "document 2");
@@ -138,12 +137,12 @@ void main() {
     final bloc = AppBloc(view: app)..add(const AppEvent.initial());
     await blocResponseFuture();
 
-    bloc.add(AppEvent.createView("document 1", DocumentPluginBuilder()));
+    bloc.add(const AppEvent.createView("document 1", ViewLayoutPB.Document));
     await blocResponseFuture();
     final document = bloc.state.latestCreatedView;
     assert(document!.name == "document 1");
 
-    bloc.add(AppEvent.createView("grid 2", GridPluginBuilder()));
+    bloc.add(const AppEvent.createView("grid 2", ViewLayoutPB.Grid));
     await blocResponseFuture();
     final grid = bloc.state.latestCreatedView;
     assert(grid!.name == "grid 2");

+ 4 - 8
frontend/appflowy_flutter/test/bloc_test/home_test/create_page_test.dart

@@ -1,7 +1,3 @@
-import 'package:appflowy/plugins/database_view/calendar/calendar.dart';
-import 'package:appflowy/plugins/database_view/board/board.dart';
-import 'package:appflowy/plugins/database_view/grid/grid.dart';
-import 'package:appflowy/plugins/document/document.dart';
 import 'package:appflowy/workspace/application/app/app_bloc.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
 import 'package:flutter_test/flutter_test.dart';
@@ -18,7 +14,7 @@ void main() {
     final bloc = AppBloc(view: app)..add(const AppEvent.initial());
     await blocResponseFuture();
 
-    bloc.add(AppEvent.createView("Test document", DocumentPluginBuilder()));
+    bloc.add(const AppEvent.createView("Test document", ViewLayoutPB.Document));
     await blocResponseFuture();
 
     assert(bloc.state.views.length == 1);
@@ -31,7 +27,7 @@ void main() {
     final bloc = AppBloc(view: app)..add(const AppEvent.initial());
     await blocResponseFuture();
 
-    bloc.add(AppEvent.createView("Test grid", GridPluginBuilder()));
+    bloc.add(const AppEvent.createView("Test grid", ViewLayoutPB.Grid));
     await blocResponseFuture();
 
     assert(bloc.state.views.length == 1);
@@ -44,7 +40,7 @@ void main() {
     final bloc = AppBloc(view: app)..add(const AppEvent.initial());
     await blocResponseFuture();
 
-    bloc.add(AppEvent.createView("Test board", BoardPluginBuilder()));
+    bloc.add(const AppEvent.createView("Test board", ViewLayoutPB.Board));
     await blocResponseFuture();
 
     assert(bloc.state.views.length == 1);
@@ -57,7 +53,7 @@ void main() {
     final bloc = AppBloc(view: app)..add(const AppEvent.initial());
     await blocResponseFuture();
 
-    bloc.add(AppEvent.createView("Test calendar", CalendarPluginBuilder()));
+    bloc.add(const AppEvent.createView("Test calendar", ViewLayoutPB.Calendar));
     await blocResponseFuture();
 
     assert(bloc.state.views.length == 1);

+ 2 - 2
frontend/appflowy_flutter/test/bloc_test/home_test/home_bloc_test.dart

@@ -1,5 +1,4 @@
 import 'package:appflowy/plugins/document/application/doc_bloc.dart';
-import 'package:appflowy/plugins/document/document.dart';
 import 'package:appflowy/workspace/application/app/app_bloc.dart';
 import 'package:appflowy/workspace/application/home/home_bloc.dart';
 import 'package:appflowy_backend/dispatch/dispatch.dart';
@@ -41,7 +40,8 @@ void main() {
     final appBloc = AppBloc(view: app)..add(const AppEvent.initial());
     assert(appBloc.state.latestCreatedView == null);
 
-    appBloc.add(AppEvent.createView("New document", DocumentPluginBuilder()));
+    appBloc
+        .add(const AppEvent.createView("New document", ViewLayoutPB.Document));
     await blocResponseFuture();
 
     assert(appBloc.state.latestCreatedView != null);

+ 6 - 7
frontend/appflowy_flutter/test/bloc_test/home_test/trash_bloc_test.dart

@@ -1,4 +1,3 @@
-import 'package:appflowy/plugins/document/document.dart';
 import 'package:appflowy/plugins/trash/application/trash_bloc.dart';
 import 'package:appflowy/workspace/application/app/app_bloc.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
@@ -20,25 +19,25 @@ class TrashTestContext {
     await blocResponseFuture();
 
     appBloc.add(
-      AppEvent.createView(
+      const AppEvent.createView(
         "Document 1",
-        DocumentPluginBuilder(),
+        ViewLayoutPB.Document,
       ),
     );
     await blocResponseFuture();
 
     appBloc.add(
-      AppEvent.createView(
+      const AppEvent.createView(
         "Document 2",
-        DocumentPluginBuilder(),
+        ViewLayoutPB.Document,
       ),
     );
     await blocResponseFuture();
 
     appBloc.add(
-      AppEvent.createView(
+      const AppEvent.createView(
         "Document 3",
-        DocumentPluginBuilder(),
+        ViewLayoutPB.Document,
       ),
     );
     await blocResponseFuture();

+ 4 - 13
frontend/appflowy_flutter/test/bloc_test/home_test/view_bloc_test.dart

@@ -1,6 +1,6 @@
-import 'package:appflowy/plugins/document/document.dart';
 import 'package:appflowy/workspace/application/app/app_bloc.dart';
 import 'package:appflowy/workspace/application/view/view_bloc.dart';
+import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
 import 'package:flutter_test/flutter_test.dart';
 
 import '../../util.dart';
@@ -16,10 +16,7 @@ void main() {
 
     final appBloc = AppBloc(view: app)..add(const AppEvent.initial());
     appBloc.add(
-      AppEvent.createView(
-        "Test document",
-        DocumentPluginBuilder(),
-      ),
+      const AppEvent.createView("Test document", ViewLayoutPB.Document),
     );
 
     await blocResponseFuture();
@@ -38,10 +35,7 @@ void main() {
     await blocResponseFuture();
 
     appBloc.add(
-      AppEvent.createView(
-        "Test document",
-        DocumentPluginBuilder(),
-      ),
+      const AppEvent.createView("Test document", ViewLayoutPB.Document),
     );
     await blocResponseFuture();
 
@@ -61,10 +55,7 @@ void main() {
     await blocResponseFuture();
 
     appBloc.add(
-      AppEvent.createView(
-        "Test document",
-        DocumentPluginBuilder(),
-      ),
+      const AppEvent.createView("Test document", ViewLayoutPB.Document),
     );
     await blocResponseFuture();
     expect(appBloc.state.views.length, 1);

+ 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 = "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 = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "06e942" }
+collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "06e942" }
+collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "06e942" }
+collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "06e942" }
+collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "06e942" }
+appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "06e942" }
 
 #collab = { path = "../../AppFlowy-Collab/collab" }
 #collab-folder = { path = "../../AppFlowy-Collab/collab-folder" }

+ 10 - 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=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c"
 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=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c"
 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=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c"
 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=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c"
 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=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c"
 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=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c"
 dependencies = [
  "anyhow",
  "collab",
@@ -979,7 +979,7 @@ dependencies = [
 [[package]]
 name = "collab-folder"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c"
 dependencies = [
  "anyhow",
  "chrono",
@@ -999,7 +999,7 @@ dependencies = [
 [[package]]
 name = "collab-persistence"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c"
 dependencies = [
  "bincode",
  "chrono",
@@ -1019,7 +1019,7 @@ dependencies = [
 [[package]]
 name = "collab-plugins"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -1050,7 +1050,7 @@ dependencies = [
 [[package]]
 name = "collab-sync"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c"
 dependencies = [
  "bytes",
  "collab",

+ 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 = "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 = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "06e942" }
+collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "06e942" }
+collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "06e942" }
+collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "06e942" }
+appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "06e942" }
 
 #collab = { path = "../AppFlowy-Collab/collab" }
 #collab-folder = { path = "../AppFlowy-Collab/collab-folder" }

+ 1 - 1
frontend/rust-lib/flowy-core/src/deps_resolve/folder2_deps.rs

@@ -276,7 +276,7 @@ impl FolderOperationHandler for DatabaseFolderOperation {
 
         FutureResult::new(async move {
           database_manager
-            .create_linked_view(name, layout, params.database_id, database_view_id)
+            .create_linked_view(name, layout.into(), params.database_id, database_view_id)
             .await?;
           Ok(())
         })

+ 3 - 0
frontend/rust-lib/flowy-database2/src/entities/calendar_entities.rs

@@ -110,6 +110,9 @@ pub struct CalendarEventPB {
 
   #[pb(index = 4)]
   pub timestamp: i64,
+
+  #[pb(index = 5)]
+  pub is_scheduled: bool,
 }
 
 #[derive(Debug, Clone, Default, ProtoBuf)]

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

@@ -23,6 +23,9 @@ pub struct DatabasePB {
 
   #[pb(index = 4)]
   pub layout_type: DatabaseLayoutPB,
+
+  #[pb(index = 5)]
+  pub is_linked: bool,
 }
 
 #[derive(ProtoBuf, Default)]

+ 14 - 3
frontend/rust-lib/flowy-database2/src/manager.rs

@@ -7,7 +7,7 @@ use appflowy_integrate::{CollabPersistenceConfig, RocksCollabDB};
 use collab::core::collab::MutexCollab;
 use collab_database::database::DatabaseData;
 use collab_database::user::{DatabaseCollabBuilder, UserDatabase as InnerUserDatabase};
-use collab_database::views::{CreateDatabaseParams, CreateViewParams};
+use collab_database::views::{CreateDatabaseParams, CreateViewParams, DatabaseLayout};
 use parking_lot::Mutex;
 use tokio::sync::RwLock;
 
@@ -16,6 +16,7 @@ use flowy_task::TaskDispatcher;
 
 use crate::entities::{DatabaseDescriptionPB, DatabaseLayoutPB, RepeatedDatabaseDescriptionPB};
 use crate::services::database::{DatabaseEditor, MutexDatabase};
+use crate::services::database_view::DatabaseLayoutDepsResolver;
 use crate::services::share::csv::{CSVFormat, CSVImporter, ImportResult};
 
 pub trait DatabaseUser2: Send + Sync {
@@ -179,18 +180,28 @@ impl DatabaseManager2 {
     Ok(())
   }
 
+  /// A linked view is a view that is linked to existing database.
   #[tracing::instrument(level = "trace", skip(self), err)]
   pub async fn create_linked_view(
     &self,
     name: String,
-    layout: DatabaseLayoutPB,
+    layout: DatabaseLayout,
     database_id: String,
     database_view_id: String,
   ) -> FlowyResult<()> {
     self.with_user_database(
       Err(FlowyError::internal().context("Create database view failed")),
       |user_database| {
-        let params = CreateViewParams::new(database_id, database_view_id, name, layout.into());
+        let mut params = CreateViewParams::new(database_id.clone(), database_view_id, name, layout);
+        if let Some(database) = user_database.get_database(&database_id) {
+          if let Some((field, layout_setting)) = DatabaseLayoutDepsResolver::new(database, layout)
+            .resolve_deps_when_create_database_linked_view()
+          {
+            params = params
+              .with_deps_fields(vec![field])
+              .with_layout_setting(layout_setting);
+          }
+        };
         user_database.create_database_linked_view(params)?;
         Ok(())
       },

+ 15 - 8
frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs

@@ -206,7 +206,7 @@ impl DatabaseEditor {
         .map(|field| field.id)
         .collect()
     });
-    database.get_fields(view_id, Some(field_ids))
+    database.get_fields_in_view(view_id, Some(field_ids))
   }
 
   pub async fn update_field(&self, params: FieldChangesetParams) -> FlowyResult<()> {
@@ -442,7 +442,7 @@ impl DatabaseEditor {
       self
         .database
         .lock()
-        .create_default_field(view_id, name, field_type.into(), |field| {
+        .create_field_with_mut(view_id, name, field_type.into(), |field| {
           field
             .type_options
             .insert(field_type.to_string(), type_option_data.clone());
@@ -932,11 +932,12 @@ impl DatabaseEditor {
 
   pub async fn group_by_field(&self, view_id: &str, field_id: &str) -> FlowyResult<()> {
     let view = self.database_views.get_view_editor(view_id).await?;
-    view.v_update_grouping_field(field_id).await?;
+    view.v_grouping_by_field(field_id).await?;
     Ok(())
   }
 
   pub async fn set_layout_setting(&self, view_id: &str, layout_setting: LayoutSettingParams) {
+    tracing::trace!("set_layout_setting: {:?}", layout_setting);
     if let Ok(view) = self.database_views.get_view_editor(view_id).await {
       let _ = view.v_set_layout_settings(layout_setting).await;
     };
@@ -1042,7 +1043,7 @@ impl DatabaseEditor {
       .await
       .ok_or_else(FlowyError::record_not_found)?;
     let rows = database_view.v_get_rows().await;
-    let (database_id, fields) = {
+    let (database_id, fields, is_linked) = {
       let database = self.database.lock();
       let database_id = database.get_database_id();
       let fields = database
@@ -1051,7 +1052,8 @@ impl DatabaseEditor {
         .into_iter()
         .map(FieldIdPB::from)
         .collect();
-      (database_id, fields)
+      let is_linked = database.is_inline_view(view_id);
+      (database_id, fields, is_linked)
     };
 
     let rows = rows
@@ -1063,6 +1065,7 @@ impl DatabaseEditor {
       fields,
       rows,
       layout_type: view.layout.into(),
+      is_linked,
     })
   }
 
@@ -1082,7 +1085,7 @@ impl DatabaseEditor {
     self
       .database
       .lock()
-      .get_fields(view_id, None)
+      .get_fields_in_view(view_id, None)
       .into_iter()
       .filter(|f| FieldType::from(f.field_type).is_auto_update())
       .collect::<Vec<Field>>()
@@ -1139,13 +1142,17 @@ struct DatabaseViewDataImpl {
 }
 
 impl DatabaseViewData for DatabaseViewDataImpl {
+  fn get_database(&self) -> Arc<InnerDatabase> {
+    self.database.lock().clone()
+  }
+
   fn get_view(&self, view_id: &str) -> Fut<Option<DatabaseView>> {
     let view = self.database.lock().get_view(view_id);
     to_fut(async move { view })
   }
 
   fn get_fields(&self, view_id: &str, field_ids: Option<Vec<String>>) -> Fut<Vec<Arc<Field>>> {
-    let fields = self.database.lock().get_fields(view_id, field_ids);
+    let fields = self.database.lock().get_fields_in_view(view_id, field_ids);
     to_fut(async move { fields.into_iter().map(Arc::new).collect() })
   }
 
@@ -1166,7 +1173,7 @@ impl DatabaseViewData for DatabaseViewDataImpl {
     field_type: FieldType,
     type_option_data: TypeOptionData,
   ) -> Fut<Field> {
-    let (_, field) = self.database.lock().create_default_field(
+    let (_, field) = self.database.lock().create_field_with_mut(
       view_id,
       name.to_string(),
       field_type.clone().into(),

+ 115 - 0
frontend/rust-lib/flowy-database2/src/services/database_view/layout_deps.rs

@@ -0,0 +1,115 @@
+use std::sync::Arc;
+
+use collab_database::database::{gen_field_id, Database};
+use collab_database::fields::Field;
+use collab_database::views::{DatabaseLayout, LayoutSetting};
+
+use crate::entities::FieldType;
+use crate::services::field::DateTypeOption;
+use crate::services::setting::CalendarLayoutSetting;
+
+/// When creating a database, we need to resolve the dependencies of the views. Different database
+/// view has different dependencies. For example, a calendar view depends on a date field.
+pub struct DatabaseLayoutDepsResolver {
+  pub database: Arc<Database>,
+  /// The new database layout.
+  pub database_layout: DatabaseLayout,
+}
+
+impl DatabaseLayoutDepsResolver {
+  pub fn new(database: Arc<Database>, database_layout: DatabaseLayout) -> Self {
+    Self {
+      database,
+      database_layout,
+    }
+  }
+
+  pub fn resolve_deps_when_create_database_linked_view(&self) -> Option<(Field, LayoutSetting)> {
+    match self.database_layout {
+      DatabaseLayout::Grid => None,
+      DatabaseLayout::Board => None,
+      DatabaseLayout::Calendar => {
+        let field = self.create_date_field();
+        let layout_setting: LayoutSetting = CalendarLayoutSetting::new(field.id.clone()).into();
+        Some((field, layout_setting))
+      },
+    }
+  }
+
+  /// If the new layout type is a calendar and there is not date field in the database, it will add
+  /// a new date field to the database and create the corresponding layout setting.
+  pub fn resolve_deps_when_update_layout_type(&self, view_id: &str) {
+    let fields = self.database.get_fields(None);
+    // Insert the layout setting if it's not exist
+    match &self.database_layout {
+      DatabaseLayout::Grid => {},
+      DatabaseLayout::Board => {},
+      DatabaseLayout::Calendar => {
+        let date_field_id = match fields
+          .into_iter()
+          .find(|field| FieldType::from(field.field_type) == FieldType::DateTime)
+        {
+          None => {
+            tracing::trace!("Create a new date field after layout type change");
+            let field = self.create_date_field();
+            let field_id = field.id.clone();
+            self.database.create_field(field);
+            field_id
+          },
+          Some(date_field) => date_field.id,
+        };
+        self.create_calendar_layout_setting_if_need(view_id, &date_field_id);
+      },
+    }
+  }
+
+  fn create_calendar_layout_setting_if_need(&self, view_id: &str, field_id: &str) {
+    if self
+      .database
+      .get_layout_setting::<CalendarLayoutSetting>(view_id, &self.database_layout)
+      .is_none()
+    {
+      let layout_setting = CalendarLayoutSetting::new(field_id.to_string());
+      self
+        .database
+        .insert_layout_setting(view_id, &self.database_layout, layout_setting);
+    }
+  }
+
+  fn create_date_field(&self) -> Field {
+    let field_type = FieldType::DateTime;
+    let default_date_type_option = DateTypeOption::default();
+    let field_id = gen_field_id();
+    Field::new(
+      field_id,
+      "Date".to_string(),
+      field_type.clone().into(),
+      false,
+    )
+    .with_type_option_data(field_type, default_date_type_option.into())
+  }
+}
+
+// pub async fn v_get_layout_settings(&self, layout_ty: &DatabaseLayout) -> LayoutSettingParams {
+//   let mut layout_setting = LayoutSettingParams::default();
+//   match layout_ty {
+//     DatabaseLayout::Grid => {},
+//     DatabaseLayout::Board => {},
+//     DatabaseLayout::Calendar => {
+//       if let Some(value) = self.delegate.get_layout_setting(&self.view_id, layout_ty) {
+//         let calendar_setting = CalendarLayoutSetting::from(value);
+//         // Check the field exist or not
+//         if let Some(field) = self.delegate.get_field(&calendar_setting.field_id).await {
+//           let field_type = FieldType::from(field.field_type);
+//
+//           // Check the type of field is Datetime or not
+//           if field_type == FieldType::DateTime {
+//             layout_setting.calendar = Some(calendar_setting);
+//           }
+//         }
+//       }
+//     },
+//   }
+//
+//   layout_setting
+// }

+ 6 - 4
frontend/rust-lib/flowy-database2/src/services/database_view/mod.rs

@@ -1,3 +1,9 @@
+pub use layout_deps::*;
+pub use notifier::*;
+pub use view_editor::*;
+pub use views::*;
+
+mod layout_deps;
 mod notifier;
 mod view_editor;
 mod view_filter;
@@ -5,7 +11,3 @@ mod view_group;
 mod view_sort;
 mod views;
 // mod trait_impl;
-
-pub use notifier::*;
-pub use view_editor::*;
-pub use views::*;

+ 37 - 51
frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs

@@ -2,7 +2,7 @@ use std::borrow::Cow;
 use std::collections::HashMap;
 use std::sync::Arc;
 
-use collab_database::database::{gen_database_filter_id, gen_database_sort_id};
+use collab_database::database::{gen_database_filter_id, gen_database_sort_id, Database};
 use collab_database::fields::{Field, TypeOptionData};
 use collab_database::rows::{Cells, Row, RowCell, RowId, RowMeta};
 use collab_database::views::{DatabaseLayout, DatabaseView, LayoutSetting};
@@ -30,10 +30,10 @@ use crate::services::database_view::view_group::{
 use crate::services::database_view::view_sort::make_sort_controller;
 use crate::services::database_view::{
   notify_did_update_filter, notify_did_update_group_rows, notify_did_update_num_of_groups,
-  notify_did_update_setting, notify_did_update_sort, DatabaseViewChangedNotifier,
-  DatabaseViewChangedReceiverRunner,
+  notify_did_update_setting, notify_did_update_sort, DatabaseLayoutDepsResolver,
+  DatabaseViewChangedNotifier, DatabaseViewChangedReceiverRunner,
 };
-use crate::services::field::{DateTypeOption, TypeOptionCellDataHandler};
+use crate::services::field::TypeOptionCellDataHandler;
 use crate::services::filter::{
   Filter, FilterChangeset, FilterController, FilterType, UpdatedFilterType,
 };
@@ -44,6 +44,8 @@ use crate::services::setting::CalendarLayoutSetting;
 use crate::services::sort::{DeletedSortType, Sort, SortChangeset, SortController, SortType};
 
 pub trait DatabaseViewData: Send + Sync + 'static {
+  fn get_database(&self) -> Arc<Database>;
+
   fn get_view(&self, view_id: &str) -> Fut<Option<DatabaseView>>;
   /// If the field_ids is None, then it will return all the field revisions
   fn get_fields(&self, view_id: &str, field_ids: Option<Vec<String>>) -> Fut<Vec<Arc<Field>>>;
@@ -438,7 +440,7 @@ impl DatabaseViewEditor {
   pub async fn v_initialize_new_group(&self, field_id: &str) -> FlowyResult<()> {
     let is_grouping_field = self.is_grouping_field(field_id).await;
     if !is_grouping_field {
-      self.v_update_grouping_field(field_id).await?;
+      self.v_grouping_by_field(field_id).await?;
 
       if let Some(view) = self.delegate.get_view(&self.view_id).await {
         let setting = database_view_setting_pb_from_view(view);
@@ -607,7 +609,11 @@ impl DatabaseViewEditor {
             // Check the type of field is Datetime or not
             if field_type == FieldType::DateTime {
               layout_setting.calendar = Some(calendar_setting);
+            } else {
+              tracing::warn!("The field of calendar setting is not datetime type")
             }
+          } else {
+            tracing::warn!("The field of calendar setting is not exist");
           }
         }
       },
@@ -713,7 +719,7 @@ impl DatabaseViewEditor {
 
   /// Called when a grouping field is updated.
   #[tracing::instrument(level = "debug", skip_all, err)]
-  pub async fn v_update_grouping_field(&self, field_id: &str) -> FlowyResult<()> {
+  pub async fn v_grouping_by_field(&self, field_id: &str) -> FlowyResult<()> {
     if let Some(field) = self.delegate.get_field(field_id).await {
       let new_group_controller =
         new_group_controller_with_field(self.view_id.clone(), self.delegate.clone(), field).await?;
@@ -770,12 +776,23 @@ impl DatabaseViewEditor {
       date_field_id: date_field.id.clone(),
       title,
       timestamp,
+      is_scheduled: timestamp != 0,
     })
   }
 
   pub async fn v_get_all_calendar_events(&self) -> Option<Vec<CalendarEventPB>> {
     let layout_ty = DatabaseLayout::Calendar;
-    let calendar_setting = self.v_get_layout_settings(&layout_ty).await.calendar?;
+    let calendar_setting = match self.v_get_layout_settings(&layout_ty).await.calendar {
+      None => {
+        // When create a new calendar view, the calendar setting should be created
+        tracing::error!(
+          "Calendar layout setting not found in database view:{}",
+          self.view_id
+        );
+        return None;
+      },
+      Some(calendar_setting) => calendar_setting,
+    };
 
     // Text
     let primary_field = self.delegate.get_primary_field().await?;
@@ -822,6 +839,7 @@ impl DatabaseViewEditor {
         date_field_id: calendar_setting.field_id.clone(),
         title,
         timestamp,
+        is_scheduled: timestamp != 0,
       };
       events.push(event);
     }
@@ -829,57 +847,25 @@ impl DatabaseViewEditor {
   }
 
   #[tracing::instrument(level = "trace", skip_all)]
-  pub async fn v_update_layout_type(&self, layout_type: DatabaseLayout) -> FlowyResult<()> {
+  pub async fn v_update_layout_type(&self, new_layout_type: DatabaseLayout) -> FlowyResult<()> {
     self
       .delegate
-      .update_layout_type(&self.view_id, &layout_type);
-
-    // Update the layout type in the database might add a new field to the database. If the new
-    // layout type is a calendar and there is not date field in the database, it will add a new
-    // date field to the database and create the corresponding layout setting.
-    //
-    let fields = self.delegate.get_fields(&self.view_id, None).await;
-    let date_field_id = match fields
-      .into_iter()
-      .find(|field| FieldType::from(field.field_type) == FieldType::DateTime)
-    {
-      None => {
-        tracing::trace!("Create a new date field after layout type change");
-        let default_date_type_option = DateTypeOption::default();
-        let field = self
-          .delegate
-          .create_field(
-            &self.view_id,
-            "Date",
-            FieldType::DateTime,
-            default_date_type_option.into(),
-          )
-          .await;
-        field.id
-      },
-      Some(date_field) => date_field.id.clone(),
-    };
+      .update_layout_type(&self.view_id, &new_layout_type);
 
-    let layout_setting = self.v_get_layout_settings(&layout_type).await;
-    match layout_type {
-      DatabaseLayout::Grid => {},
-      DatabaseLayout::Board => {},
-      DatabaseLayout::Calendar => {
-        if layout_setting.calendar.is_none() {
-          let layout_setting = CalendarLayoutSetting::new(date_field_id.clone());
-          self
-            .v_set_layout_settings(LayoutSettingParams {
-              layout_type,
-              calendar: Some(layout_setting),
-            })
-            .await?;
-        }
-      },
+    // using the {} brackets to denote the lifetime of the resolver. Because the DatabaseLayoutDepsResolver
+    // is not sync and send, so we can't pass it to the async block.
+    {
+      let resolver = DatabaseLayoutDepsResolver::new(self.delegate.get_database(), new_layout_type);
+      resolver.resolve_deps_when_update_layout_type(&self.view_id);
     }
 
+    // initialize the group controller if the current layout support grouping
+    *self.group_controller.write().await =
+      new_group_controller(self.view_id.clone(), self.delegate.clone()).await?;
+
     let payload = DatabaseLayoutMetaPB {
       view_id: self.view_id.clone(),
-      layout: layout_type.into(),
+      layout: new_layout_type.into(),
     };
     send_notification(&self.view_id, DatabaseNotification::DidUpdateDatabaseLayout)
       .payload(payload)

+ 1 - 1
frontend/rust-lib/flowy-database2/src/services/database_view/views.rs

@@ -94,7 +94,7 @@ impl DatabaseViews {
     // If the id of the grouping field is equal to the updated field's id, then we need to
     // update the group setting
     if view_editor.is_grouping_field(field_id).await {
-      view_editor.v_update_grouping_field(field_id).await?;
+      view_editor.v_grouping_by_field(field_id).await?;
     }
     view_editor
       .v_did_update_field_type_option(field_id, old_field)

+ 15 - 12
frontend/rust-lib/flowy-database2/src/services/group/configuration.rs

@@ -1,18 +1,21 @@
+use std::collections::HashMap;
+use std::fmt::Formatter;
+use std::marker::PhantomData;
+use std::sync::Arc;
+
+use collab_database::fields::Field;
+use indexmap::IndexMap;
+use serde::de::DeserializeOwned;
+use serde::Serialize;
+
+use flowy_error::{FlowyError, FlowyResult};
+use lib_infra::future::Fut;
+
 use crate::entities::{GroupChangesPB, GroupPB, InsertedGroupPB};
 use crate::services::field::RowSingleCellData;
 use crate::services::group::{
   default_group_setting, GeneratedGroups, Group, GroupChangeset, GroupData, GroupSetting,
 };
-use collab_database::fields::Field;
-use flowy_error::{FlowyError, FlowyResult};
-use indexmap::IndexMap;
-use lib_infra::future::Fut;
-use serde::de::DeserializeOwned;
-use serde::Serialize;
-use std::collections::HashMap;
-use std::fmt::Formatter;
-use std::marker::PhantomData;
-use std::sync::Arc;
 
 pub trait GroupSettingReader: Send + Sync + 'static {
   fn get_group_setting(&self, view_id: &str) -> Fut<Option<Arc<GroupSetting>>>;
@@ -361,10 +364,10 @@ where
     })?;
 
     if let Some(group) = update_group {
-      self.group_by_id.get_mut(&group.id).map(|group_data| {
+      if let Some(group_data) = self.group_by_id.get_mut(&group.id) {
         group_data.name = group.name.clone();
         group_data.is_visible = group.visible;
-      });
+      };
     }
     Ok(())
   }

+ 5 - 4
frontend/rust-lib/flowy-database2/src/services/share/csv/export.rs

@@ -1,9 +1,10 @@
-use crate::entities::FieldType;
-use crate::services::cell::stringify_cell_data;
 use collab_database::database::Database;
+use indexmap::IndexMap;
 
 use flowy_error::{FlowyError, FlowyResult};
-use indexmap::IndexMap;
+
+use crate::entities::FieldType;
+use crate::services::cell::stringify_cell_data;
 
 #[derive(Debug, Clone, Copy)]
 pub enum CSVFormat {
@@ -20,7 +21,7 @@ impl CSVExport {
   pub fn export_database(&self, database: &Database, style: CSVFormat) -> FlowyResult<String> {
     let mut wtr = csv::Writer::from_writer(vec![]);
     let inline_view_id = database.get_inline_view_id();
-    let fields = database.get_fields(&inline_view_id, None);
+    let fields = database.get_fields_in_view(&inline_view_id, None);
 
     // Write fields
     let field_records = fields

+ 10 - 7
frontend/rust-lib/flowy-document2/tests/document/document_redo_undo_test.rs

@@ -1,10 +1,13 @@
-use crate::document::util::{default_collab_builder, gen_document_id, gen_id, FakeUser};
+use std::collections::HashMap;
+use std::sync::Arc;
+
 use collab_document::blocks::{Block, BlockAction, BlockActionPayload, BlockActionType};
+
 use flowy_document2::document_block_keys::PARAGRAPH_BLOCK_TYPE;
 use flowy_document2::document_data::default_document_data;
 use flowy_document2::manager::DocumentManager;
-use std::collections::HashMap;
-use std::sync::Arc;
+
+use crate::document::util::{default_collab_builder, gen_document_id, gen_id, FakeUser};
 
 #[tokio::test]
 async fn undo_redo_test() {
@@ -38,22 +41,22 @@ async fn undo_redo_test() {
     action: BlockActionType::Insert,
     payload: BlockActionPayload {
       block: text_block,
-      parent_id: Some(page_id.clone()),
+      parent_id: Some(page_id),
       prev_id: None,
     },
   };
   document.apply_action(vec![insert_text_action]);
 
   let can_undo = document.can_undo();
-  assert_eq!(can_undo, true);
+  assert!(can_undo);
   // undo the insert
   let undo = document.undo();
-  assert_eq!(undo, true);
+  assert!(undo);
   assert_eq!(document.get_block(&text_block_id), None);
 
   let can_redo = document.can_redo();
   assert!(can_redo);
   // redo the insert
   let redo = document.redo();
-  assert_eq!(redo, true);
+  assert!(redo);
 }

+ 15 - 0
frontend/rust-lib/flowy-folder2/src/entities/view.rs

@@ -10,6 +10,21 @@ use flowy_error::ErrorCode;
 use crate::entities::parser::view::{ViewDesc, ViewIdentify, ViewName, ViewThumbnail};
 use crate::view_operation::gen_view_id;
 
+#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
+pub struct ChildViewUpdatePB {
+  #[pb(index = 1)]
+  pub parent_view_id: String,
+
+  #[pb(index = 2)]
+  pub create_child_views: Vec<ViewPB>,
+
+  #[pb(index = 3)]
+  pub delete_child_views: Vec<String>,
+
+  #[pb(index = 4)]
+  pub update_child_views: Vec<ViewPB>,
+}
+
 #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
 pub struct ViewPB {
   #[pb(index = 1)]

+ 61 - 5
frontend/rust-lib/flowy-folder2/src/manager.rs

@@ -18,8 +18,9 @@ use flowy_error::{ErrorCode, FlowyError, FlowyResult};
 
 use crate::deps::{FolderCloudService, FolderUser};
 use crate::entities::{
-  view_pb_with_child_views, CreateViewParams, CreateWorkspaceParams, DeletedViewPB,
-  RepeatedTrashPB, RepeatedViewPB, RepeatedWorkspacePB, UpdateViewParams, ViewPB, WorkspacePB,
+  view_pb_with_child_views, view_pb_without_child_views, ChildViewUpdatePB, CreateViewParams,
+  CreateWorkspaceParams, DeletedViewPB, RepeatedTrashPB, RepeatedViewPB, RepeatedWorkspacePB,
+  UpdateViewParams, ViewPB, WorkspacePB,
 };
 use crate::notification::{
   send_notification, send_workspace_notification, send_workspace_setting_notification,
@@ -257,7 +258,6 @@ impl Folder2Manager {
       folder.insert_view(view.clone());
     });
 
-    notify_parent_view_did_change(self.mutex_folder.clone(), vec![view.parent_view_id.clone()]);
     Ok(view)
   }
 
@@ -332,6 +332,7 @@ impl Folder2Manager {
   #[tracing::instrument(level = "debug", skip(self), err)]
   pub async fn move_view_to_trash(&self, view_id: &str) -> FlowyResult<()> {
     self.with_folder((), |folder| {
+      let view = folder.views.get_view(view_id);
       folder.add_trash(vec![view_id.to_string()]);
 
       // notify the parent view that the view is moved to trash
@@ -341,6 +342,13 @@ impl Folder2Manager {
           index: None,
         })
         .send();
+
+      if let Some(view) = view {
+        notify_child_views_changed(
+          view_pb_without_child_views(view),
+          ChildViewChangeReason::DidDeleteView,
+        );
+      }
     });
 
     Ok(())
@@ -415,6 +423,7 @@ impl Folder2Manager {
           .set_cover_url_if_not_none(params.cover_url)
           .done()
       });
+
       Some((old_view, new_view))
     });
 
@@ -632,10 +641,25 @@ fn listen_on_view_change(mut rx: ViewChangeReceiver, weak_mutex_folder: &Weak<Mu
         tracing::trace!("Did receive view change: {:?}", value);
         match value {
           ViewChange::DidCreateView { view } => {
+            notify_child_views_changed(
+              view_pb_without_child_views(view.clone()),
+              ChildViewChangeReason::DidCreateView,
+            );
             notify_parent_view_did_change(folder.clone(), vec![view.parent_view_id]);
           },
-          ViewChange::DidDeleteView { views: _ } => {},
+          ViewChange::DidDeleteView { views } => {
+            for view in views {
+              notify_child_views_changed(
+                view_pb_without_child_views(view),
+                ChildViewChangeReason::DidDeleteView,
+              );
+            }
+          },
           ViewChange::DidUpdate { view } => {
+            notify_child_views_changed(
+              view_pb_without_child_views(view.clone()),
+              ChildViewChangeReason::DidUpdateView,
+            );
             notify_parent_view_did_change(folder.clone(), vec![view.parent_view_id]);
           },
         };
@@ -764,7 +788,7 @@ fn notify_parent_view_did_change<T: AsRef<str>>(
 
       // Post the notification
       let parent_view_pb = view_pb_with_child_views(parent_view, child_views);
-      send_notification(parent_view_id, FolderNotification::DidUpdateChildViews)
+      send_notification(parent_view_id, FolderNotification::DidUpdateView)
         .payload(parent_view_pb)
         .send();
     }
@@ -773,6 +797,38 @@ fn notify_parent_view_did_change<T: AsRef<str>>(
   None
 }
 
+pub enum ChildViewChangeReason {
+  DidCreateView,
+  DidDeleteView,
+  DidUpdateView,
+}
+
+/// Notify the the list of parent view ids that its child views were changed.
+#[tracing::instrument(level = "debug", skip_all)]
+fn notify_child_views_changed(view_pb: ViewPB, reason: ChildViewChangeReason) {
+  let parent_view_id = view_pb.parent_view_id.clone();
+  let mut payload = ChildViewUpdatePB {
+    parent_view_id: view_pb.parent_view_id.clone(),
+    ..Default::default()
+  };
+
+  match reason {
+    ChildViewChangeReason::DidCreateView => {
+      payload.create_child_views.push(view_pb);
+    },
+    ChildViewChangeReason::DidDeleteView => {
+      payload.delete_child_views.push(view_pb.id);
+    },
+    ChildViewChangeReason::DidUpdateView => {
+      payload.update_child_views.push(view_pb);
+    },
+  }
+
+  send_notification(&parent_view_id, FolderNotification::DidUpdateChildViews)
+    .payload(payload)
+    .send();
+}
+
 fn folder_not_init_error() -> FlowyError {
   FlowyError::internal().context("Folder not initialized")
 }

+ 3 - 3
frontend/rust-lib/flowy-folder2/src/notification.rs

@@ -1,9 +1,11 @@
-use crate::entities::{view_pb_without_child_views, WorkspacePB, WorkspaceSettingPB};
 use collab_folder::core::{View, Workspace};
+
 use flowy_derive::ProtoBuf_Enum;
 use flowy_notification::NotificationBuilder;
 use lib_dispatch::prelude::ToBytes;
 
+use crate::entities::{view_pb_without_child_views, WorkspacePB, WorkspaceSettingPB};
+
 const OBSERVABLE_CATEGORY: &str = "Workspace";
 
 #[derive(ProtoBuf_Enum, Debug, Default)]
@@ -18,9 +20,7 @@ pub(crate) enum FolderNotification {
   DidUpdateWorkspaceViews = 3,
   /// Trigger when the settings of the workspace are changed. The changes including the latest visiting view, etc
   DidUpdateWorkspaceSetting = 4,
-
   DidUpdateView = 29,
-  /// Trigger when the properties including rename,update description of the view are changed
   DidUpdateChildViews = 30,
   /// Trigger after deleting the view
   DidDeleteView = 31,

+ 1 - 1
frontend/rust-lib/flowy-test/tests/document/utils.rs

@@ -41,7 +41,7 @@ pub fn gen_insert_block_action(document: OpenDocumentData) -> BlockActionPB {
   let data = block.data.clone();
   let new_block_id = gen_id();
   let new_block = BlockPB {
-    id: new_block_id.clone(),
+    id: new_block_id,
     ty: block.ty.clone(),
     data,
     parent_id: page_id.clone(),