瀏覽代碼

feat: switch database layout (#2677)

* chore: rename update at and create at

* chore: support switching view layout

* chore: implement ui

* chore: update layout type

* refactor: board/calendar/grid setting button

* chore: update UI after switch to other layout type

* fix: no date display in calendar

* chore: update patch

* chore: fix create ref view in document

* chore: fix flutter analyze

* ci: warnings

* chore: rename board and grid keys

* fix: calendar row event update

---------

Co-authored-by: Lucas.Xu <[email protected]>
Nathan.fooo 1 年之前
父節點
當前提交
33e0f8d26d
共有 98 個文件被更改,包括 1601 次插入1123 次删除
  1. 6 0
      frontend/appflowy_flutter/assets/images/grid/setting/database_layout.svg
  2. 5 3
      frontend/appflowy_flutter/assets/translations/en.json
  3. 4 3
      frontend/appflowy_flutter/lib/plugins/blank/blank.dart
  4. 29 26
      frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart
  5. 2 2
      frontend/appflowy_flutter/lib/plugins/database_view/application/database_view_service.dart
  6. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart
  7. 2 2
      frontend/appflowy_flutter/lib/plugins/database_view/application/layout/calendar_setting_listener.dart
  8. 56 0
      frontend/appflowy_flutter/lib/plugins/database_view/application/layout/layout_bloc.dart
  9. 50 0
      frontend/appflowy_flutter/lib/plugins/database_view/application/layout/layout_listener.dart
  10. 35 0
      frontend/appflowy_flutter/lib/plugins/database_view/application/layout/layout_service.dart
  11. 6 6
      frontend/appflowy_flutter/lib/plugins/database_view/application/layout/layout_setting_listener.dart
  12. 28 2
      frontend/appflowy_flutter/lib/plugins/database_view/application/setting/setting_bloc.dart
  13. 5 5
      frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_cache.dart
  14. 13 15
      frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart
  15. 5 4
      frontend/appflowy_flutter/lib/plugins/database_view/board/board.dart
  16. 1 9
      frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart
  17. 0 192
      frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/toolbar/board_setting.dart
  18. 5 70
      frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/toolbar/board_toolbar.dart
  19. 16 19
      frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart
  20. 5 4
      frontend/appflowy_flutter/lib/plugins/database_view/calendar/calendar.dart
  21. 2 2
      frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_setting.dart
  22. 20 9
      frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_toolbar.dart
  23. 54 0
      frontend/appflowy_flutter/lib/plugins/database_view/database_view.dart
  24. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart
  25. 5 4
      frontend/appflowy_flutter/lib/plugins/database_view/grid/grid.dart
  26. 1 5
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart
  27. 133 0
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/grid_layout.dart
  28. 16 58
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/grid_setting.dart
  29. 6 2
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/grid_toolbar.dart
  30. 0 102
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/setting_button.dart
  31. 6 6
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/field/grid_property.dart
  32. 2 2
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/group/database_group.dart
  33. 116 0
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/setting_button.dart
  34. 9 5
      frontend/appflowy_flutter/lib/plugins/document/document.dart
  35. 10 4
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart
  36. 16 17
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart
  37. 2 2
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart
  38. 2 2
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/board/board_node_widget.dart
  39. 7 4
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/board/board_view_menu_item.dart
  40. 2 2
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/grid/grid_node_widget.dart
  41. 7 4
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/grid/grid_view_menu_item.dart
  42. 3 3
      frontend/appflowy_flutter/lib/plugins/trash/trash.dart
  43. 12 4
      frontend/appflowy_flutter/lib/startup/plugin/plugin.dart
  44. 5 2
      frontend/appflowy_flutter/lib/startup/plugin/src/sandbox.dart
  45. 12 9
      frontend/appflowy_flutter/lib/workspace/application/app/app_bloc.dart
  46. 0 143
      frontend/appflowy_flutter/lib/workspace/application/app/app_service.dart
  47. 0 1
      frontend/appflowy_flutter/lib/workspace/application/app/prelude.dart
  48. 3 5
      frontend/appflowy_flutter/lib/workspace/application/menu/menu_view_section_bloc.dart
  49. 5 5
      frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart
  50. 10 2
      frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart
  51. 145 14
      frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart
  52. 1 1
      frontend/appflowy_flutter/lib/workspace/presentation/home/home_screen.dart
  53. 18 18
      frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart
  54. 3 3
      frontend/appflowy_flutter/lib/workspace/presentation/home/navigation.dart
  55. 1 3
      frontend/appflowy_flutter/lib/workspace/presentation/widgets/left_bar_item.dart
  56. 4 10
      frontend/appflowy_flutter/test/bloc_test/board_test/util.dart
  57. 0 5
      frontend/appflowy_flutter/test/bloc_test/grid_test/filter/create_filter_test.dart
  58. 4 10
      frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_util.dart
  59. 3 13
      frontend/appflowy_flutter/test/bloc_test/grid_test/grid_bloc_test.dart
  60. 4 10
      frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart
  61. 10 10
      frontend/appflowy_tauri/src-tauri/Cargo.lock
  62. 7 7
      frontend/appflowy_tauri/src-tauri/Cargo.toml
  63. 10 10
      frontend/rust-lib/Cargo.lock
  64. 5 5
      frontend/rust-lib/Cargo.toml
  65. 32 15
      frontend/rust-lib/flowy-core/src/deps_resolve/folder2_deps.rs
  66. 15 0
      frontend/rust-lib/flowy-database2/src/entities/calendar_entities.rs
  67. 30 7
      frontend/rust-lib/flowy-database2/src/entities/database_entities.rs
  68. 1 1
      frontend/rust-lib/flowy-database2/src/entities/row_entities.rs
  69. 20 10
      frontend/rust-lib/flowy-database2/src/entities/setting_entities.rs
  70. 35 7
      frontend/rust-lib/flowy-database2/src/event_handler.rs
  71. 14 4
      frontend/rust-lib/flowy-database2/src/event_map.rs
  72. 12 11
      frontend/rust-lib/flowy-database2/src/manager.rs
  73. 2 0
      frontend/rust-lib/flowy-database2/src/notification.rs
  74. 88 39
      frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs
  75. 8 1
      frontend/rust-lib/flowy-database2/src/services/database/entities.rs
  76. 7 6
      frontend/rust-lib/flowy-database2/src/services/database/util.rs
  77. 94 20
      frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs
  78. 27 29
      frontend/rust-lib/flowy-database2/src/services/database_view/view_group.rs
  79. 12 1
      frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option.rs
  80. 2 2
      frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs
  81. 4 3
      frontend/rust-lib/flowy-database2/src/services/group/controller.rs
  82. 10 2
      frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs
  83. 10 2
      frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/single_select_controller.rs
  84. 0 0
      frontend/rust-lib/flowy-database2/src/services/group/group_builder.rs
  85. 2 2
      frontend/rust-lib/flowy-database2/src/services/group/mod.rs
  86. 12 8
      frontend/rust-lib/flowy-database2/tests/database/cell_test/test.rs
  87. 31 22
      frontend/rust-lib/flowy-database2/tests/database/database_editor.rs
  88. 1 1
      frontend/rust-lib/flowy-database2/tests/database/filter_test/script.rs
  89. 25 2
      frontend/rust-lib/flowy-database2/tests/database/layout_test/script.rs
  90. 14 1
      frontend/rust-lib/flowy-database2/tests/database/layout_test/test.rs
  91. 77 0
      frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs
  92. 8 3
      frontend/rust-lib/flowy-folder2/src/entities/view.rs
  93. 1 1
      frontend/rust-lib/flowy-folder2/src/lib.rs
  94. 25 24
      frontend/rust-lib/flowy-folder2/src/manager.rs
  95. 1 1
      frontend/rust-lib/flowy-folder2/src/test_helper.rs
  96. 3 6
      frontend/rust-lib/flowy-folder2/src/user_default.rs
  97. 21 5
      frontend/rust-lib/flowy-folder2/src/view_operation.rs
  98. 1 0
      frontend/rust-lib/flowy-folder2/tests/workspace/script.rs

+ 6 - 0
frontend/appflowy_flutter/assets/images/grid/setting/database_layout.svg

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M3 6C3 3.79086 4.79086 2 7 2H17C19.2091 2 21 3.79086 21 6C21 8.20914 19.2091 10 17 10H7C4.79086 10 3 8.20914 3 6Z" stroke="#000000" stroke-width="2"/>
+<path d="M3 16C3 14.8954 3.89543 14 5 14H8C9.10457 14 10 14.8954 10 16V19C10 20.1046 9.10457 21 8 21H5C3.89543 21 3 20.1046 3 19V16Z" stroke="#000000" stroke-width="2"/>
+<path d="M14 17.5C14 15.567 15.567 14 17.5 14C19.433 14 21 15.567 21 17.5C21 19.433 19.433 21 17.5 21C15.567 21 14 19.433 14 17.5Z" stroke="#000000" stroke-width="2"/>
+</svg>

+ 5 - 3
frontend/appflowy_flutter/assets/translations/en.json

@@ -231,8 +231,9 @@
       "addFilter": "Add Filter",
       "deleteFilter": "Delete filter",
       "filterBy": "Filter by...",
-      "typeAValue": "Type a value...",
-      "layout": "Layout"
+      "typeAValue": "Type a value...", 
+      "layout": "Layout",
+      "databaseLayout": "Layout"
     },
     "textFilter": {
       "contains": "Contains",
@@ -439,7 +440,8 @@
       "firstDayOfWeek": "Start week on",
       "layoutDateField": "Layout calendar by",
       "noDateTitle": "No Date",
-      "noDateHint": "Unscheduled events will show up here"
+      "noDateHint": "Unscheduled events will show up here",
+      "clickToAdd": "Click to add to the calendar"
     }
   }
 }

+ 4 - 3
frontend/appflowy_flutter/lib/plugins/blank/blank.dart

@@ -28,16 +28,17 @@ class BlankPluginConfig implements PluginConfig {
 
 class BlankPagePlugin extends Plugin {
   @override
-  PluginDisplay get display => BlankPagePluginDisplay();
+  PluginWidgetBuilder get widgetBuilder => BlankPagePluginWidgetBuilder();
 
   @override
   PluginId get id => "BlankStack";
 
   @override
-  PluginType get ty => PluginType.blank;
+  PluginType get pluginType => PluginType.blank;
 }
 
-class BlankPagePluginDisplay extends PluginDisplay with NavigationItem {
+class BlankPagePluginWidgetBuilder extends PluginWidgetBuilder
+    with NavigationItem {
   @override
   Widget get leftBarItem => FlowyText.medium(LocaleKeys.blankPageTitle.tr());
 

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

@@ -39,18 +39,18 @@ class GroupCallbacks {
   });
 }
 
-class LayoutCallbacks {
-  final void Function(LayoutSettingPB) onLayoutChanged;
-  final void Function(LayoutSettingPB) onLoadLayout;
+class DatabaseLayoutSettingCallbacks {
+  final void Function(DatabaseLayoutSettingPB) onLayoutChanged;
+  final void Function(DatabaseLayoutSettingPB) onLoadLayout;
 
-  LayoutCallbacks({
+  DatabaseLayoutSettingCallbacks({
     required this.onLayoutChanged,
     required this.onLoadLayout,
   });
 }
 
 class CalendarLayoutCallbacks {
-  final void Function(LayoutSettingPB) onCalendarLayoutChanged;
+  final void Function(DatabaseLayoutSettingPB) onCalendarLayoutChanged;
 
   CalendarLayoutCallbacks({required this.onCalendarLayoutChanged});
 }
@@ -59,14 +59,14 @@ class DatabaseCallbacks {
   OnDatabaseChanged? onDatabaseChanged;
   OnFieldsChanged? onFieldsChanged;
   OnFiltersChanged? onFiltersChanged;
-  OnRowsChanged? onRowsChanged;
+  OnNumOfRowsChanged? onNumOfRowsChanged;
   OnRowsDeleted? onRowsDeleted;
   OnRowsUpdated? onRowsUpdated;
   OnRowsCreated? onRowsCreated;
 
   DatabaseCallbacks({
     this.onDatabaseChanged,
-    this.onRowsChanged,
+    this.onNumOfRowsChanged,
     this.onFieldsChanged,
     this.onFiltersChanged,
     this.onRowsUpdated,
@@ -79,13 +79,13 @@ class DatabaseController {
   final String viewId;
   final DatabaseViewBackendService _databaseViewBackendSvc;
   final FieldController fieldController;
+  DatabaseLayoutPB? databaseLayout;
   late DatabaseViewCache _viewCache;
-  final DatabaseLayoutPB layoutType;
 
   // Callbacks
   DatabaseCallbacks? _databaseCallbacks;
   GroupCallbacks? _groupCallbacks;
-  LayoutCallbacks? _layoutCallbacks;
+  DatabaseLayoutSettingCallbacks? _layoutCallbacks;
   CalendarLayoutCallbacks? _calendarLayoutCallbacks;
 
   // Getters
@@ -93,15 +93,15 @@ class DatabaseController {
 
   // Listener
   final DatabaseGroupListener groupListener;
-  final DatabaseLayoutListener layoutListener;
+  final DatabaseLayoutSettingListener layoutListener;
   final DatabaseCalendarLayoutListener calendarLayoutListener;
 
-  DatabaseController({required ViewPB view, required this.layoutType})
+  DatabaseController({required ViewPB view})
       : viewId = view.id,
         _databaseViewBackendSvc = DatabaseViewBackendService(viewId: view.id),
         fieldController = FieldController(viewId: view.id),
         groupListener = DatabaseGroupListener(view.id),
-        layoutListener = DatabaseLayoutListener(view.id),
+        layoutListener = DatabaseLayoutSettingListener(view.id),
         calendarLayoutListener = DatabaseCalendarLayoutListener(view.id) {
     _viewCache = DatabaseViewCache(
       viewId: viewId,
@@ -111,14 +111,11 @@ class DatabaseController {
     _listenOnFieldsChanged();
     _listenOnGroupChanged();
     _listenOnLayoutChanged();
-    if (layoutType == DatabaseLayoutPB.Calendar) {
-      _listenOnCalendarLayoutChanged();
-    }
   }
 
   void setListener({
     DatabaseCallbacks? onDatabaseChanged,
-    LayoutCallbacks? onLayoutChanged,
+    DatabaseLayoutSettingCallbacks? onLayoutChanged,
     GroupCallbacks? onGroupChanged,
     CalendarLayoutCallbacks? onCalendarLayoutChanged,
   }) {
@@ -132,6 +129,12 @@ class DatabaseController {
     return _databaseViewBackendSvc.openGrid().then((result) {
       return result.fold(
         (database) async {
+          databaseLayout = database.layoutType;
+
+          if (databaseLayout == DatabaseLayoutPB.Calendar) {
+            _listenOnCalendarLayoutChanged();
+          }
+
           _databaseCallbacks?.onDatabaseChanged?.call(database);
           _viewCache.rowCache.setInitialRows(database.rows);
           return await fieldController
@@ -242,20 +245,20 @@ class DatabaseController {
   }
 
   Future<void> _loadLayoutSetting() async {
-    _databaseViewBackendSvc.getLayoutSetting(layoutType).then((result) {
-      result.fold(
-        (l) {
-          _layoutCallbacks?.onLoadLayout(l);
-        },
-        (r) => Log.error(r),
-      );
-    });
+    if (databaseLayout != null) {
+      _databaseViewBackendSvc.getLayoutSetting(databaseLayout!).then((result) {
+        result.fold(
+          (l) => _layoutCallbacks?.onLoadLayout(l),
+          (r) => Log.error(r),
+        );
+      });
+    }
   }
 
   void _listenOnRowsChanged() {
     final callbacks = DatabaseViewCallbacks(
-      onRowsChanged: (rows, rowByRowId, reason) {
-        _databaseCallbacks?.onRowsChanged?.call(rows, rowByRowId, reason);
+      onNumOfRowsChanged: (rows, rowByRowId, reason) {
+        _databaseCallbacks?.onNumOfRowsChanged?.call(rows, rowByRowId, reason);
       },
       onRowsDeleted: (ids) {
         _databaseCallbacks?.onRowsDeleted?.call(ids);

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

@@ -97,10 +97,10 @@ class DatabaseViewBackendService {
     });
   }
 
-  Future<Either<LayoutSettingPB, FlowyError>> getLayoutSetting(
+  Future<Either<DatabaseLayoutSettingPB, FlowyError>> getLayoutSetting(
     DatabaseLayoutPB layoutType,
   ) {
-    final payload = DatabaseLayoutIdPB.create()
+    final payload = DatabaseLayoutMetaPB.create()
       ..viewId = viewId
       ..layout = layoutType;
     return DatabaseEventGetLayoutSetting(payload).send();

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

@@ -15,7 +15,7 @@ typedef OnDatabaseChanged = void Function(DatabasePB);
 typedef OnRowsCreated = void Function(List<RowId> ids);
 typedef OnRowsUpdated = void Function(List<RowId> ids);
 typedef OnRowsDeleted = void Function(List<RowId> ids);
-typedef OnRowsChanged = void Function(
+typedef OnNumOfRowsChanged = void Function(
   UnmodifiableListView<RowInfo> rows,
   UnmodifiableMapView<RowId, RowInfo> rowByRowId,
   RowsChangedReason reason,

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

@@ -6,7 +6,7 @@ import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
 import 'package:dartz/dartz.dart';
 
-typedef NewLayoutFieldValue = Either<LayoutSettingPB, FlowyError>;
+typedef NewLayoutFieldValue = Either<DatabaseLayoutSettingPB, FlowyError>;
 
 class DatabaseCalendarLayoutListener {
   final String viewId;
@@ -33,7 +33,7 @@ class DatabaseCalendarLayoutListener {
       case DatabaseNotification.DidSetNewLayoutField:
         result.fold(
           (payload) => _newLayoutFieldNotifier?.value =
-              left(LayoutSettingPB.fromBuffer(payload)),
+              left(DatabaseLayoutSettingPB.fromBuffer(payload)),
           (error) => _newLayoutFieldNotifier?.value = right(error),
         );
         break;

+ 56 - 0
frontend/appflowy_flutter/lib/plugins/database_view/application/layout/layout_bloc.dart

@@ -0,0 +1,56 @@
+import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+import 'layout_service.dart';
+part 'layout_bloc.freezed.dart';
+
+class DatabaseLayoutBloc
+    extends Bloc<DatabaseLayoutEvent, DatabaseLayoutState> {
+  final DatabaseLayoutBackendService layoutService;
+
+  DatabaseLayoutBloc({
+    required String viewId,
+    required DatabaseLayoutPB databaseLayout,
+  })  : layoutService = DatabaseLayoutBackendService(viewId),
+        super(DatabaseLayoutState.initial(viewId, databaseLayout)) {
+    on<DatabaseLayoutEvent>(
+      (event, emit) async {
+        event.when(
+          initial: () {},
+          updateLayout: (DatabaseLayoutPB layout) {
+            layoutService.updateLayout(
+              fieldId: viewId,
+              layout: layout,
+            );
+            emit(state.copyWith(databaseLayout: layout));
+          },
+        );
+      },
+    );
+  }
+}
+
+@freezed
+class DatabaseLayoutEvent with _$DatabaseLayoutEvent {
+  const factory DatabaseLayoutEvent.initial() = _Initial;
+  const factory DatabaseLayoutEvent.updateLayout(DatabaseLayoutPB layout) =
+      _UpdateLayout;
+}
+
+@freezed
+class DatabaseLayoutState with _$DatabaseLayoutState {
+  const factory DatabaseLayoutState({
+    required String viewId,
+    required DatabaseLayoutPB databaseLayout,
+  }) = _DatabaseLayoutState;
+
+  factory DatabaseLayoutState.initial(
+    String viewId,
+    DatabaseLayoutPB databaseLayout,
+  ) =>
+      DatabaseLayoutState(
+        viewId: viewId,
+        databaseLayout: databaseLayout,
+      );
+}

+ 50 - 0
frontend/appflowy_flutter/lib/plugins/database_view/application/layout/layout_listener.dart

@@ -0,0 +1,50 @@
+import 'dart:typed_data';
+
+import 'package:appflowy/core/notification/grid_notification.dart';
+import 'package:flowy_infra/notifier.dart';
+import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
+import 'package:dartz/dartz.dart';
+
+/// Listener for database layout changes.
+class DatabaseLayoutListener {
+  final String viewId;
+  PublishNotifier<Either<DatabaseLayoutPB, FlowyError>>? _layoutNotifier =
+      PublishNotifier();
+  DatabaseNotificationListener? _listener;
+  DatabaseLayoutListener(this.viewId);
+
+  void start({
+    required void Function(Either<DatabaseLayoutPB, FlowyError>)
+        onLayoutChanged,
+  }) {
+    _layoutNotifier?.addPublishListener(onLayoutChanged);
+    _listener = DatabaseNotificationListener(
+      objectId: viewId,
+      handler: _handler,
+    );
+  }
+
+  void _handler(
+    DatabaseNotification ty,
+    Either<Uint8List, FlowyError> result,
+  ) {
+    switch (ty) {
+      case DatabaseNotification.DidUpdateDatabaseLayout:
+        result.fold(
+          (payload) => _layoutNotifier?.value =
+              left(DatabaseLayoutMetaPB.fromBuffer(payload).layout),
+          (error) => _layoutNotifier?.value = right(error),
+        );
+        break;
+      default:
+        break;
+    }
+  }
+
+  Future<void> stop() async {
+    await _listener?.stop();
+    _layoutNotifier?.dispose();
+    _layoutNotifier = null;
+  }
+}

+ 35 - 0
frontend/appflowy_flutter/lib/plugins/database_view/application/layout/layout_service.dart

@@ -0,0 +1,35 @@
+import 'package:appflowy_backend/dispatch/dispatch.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
+import 'package:dartz/dartz.dart';
+
+class DatabaseLayoutBackendService {
+  final String viewId;
+
+  DatabaseLayoutBackendService(this.viewId);
+
+  Future<Either<ViewPB, FlowyError>> updateLayout({
+    required String fieldId,
+    required DatabaseLayoutPB layout,
+  }) {
+    var payload = UpdateViewPayloadPB.create()
+      ..viewId = viewId
+      ..layout = _viewLayoutFromDatabaseLayout(layout);
+
+    return FolderEventUpdateView(payload).send();
+  }
+}
+
+ViewLayoutPB _viewLayoutFromDatabaseLayout(DatabaseLayoutPB databaseLayout) {
+  switch (databaseLayout) {
+    case DatabaseLayoutPB.Board:
+      return ViewLayoutPB.Board;
+    case DatabaseLayoutPB.Calendar:
+      return ViewLayoutPB.Calendar;
+    case DatabaseLayoutPB.Grid:
+      return ViewLayoutPB.Grid;
+    default:
+      throw UnimplementedError;
+  }
+}

+ 6 - 6
frontend/appflowy_flutter/lib/plugins/database_view/application/layout/layout_setting_listener.dart

@@ -8,15 +8,15 @@ import 'package:dartz/dartz.dart';
 
 typedef LayoutSettingsValue<T> = Either<T, FlowyError>;
 
-class DatabaseLayoutListener {
+class DatabaseLayoutSettingListener {
   final String viewId;
-  PublishNotifier<LayoutSettingsValue<LayoutSettingPB>>? _settingNotifier =
-      PublishNotifier();
+  PublishNotifier<LayoutSettingsValue<DatabaseLayoutSettingPB>>?
+      _settingNotifier = PublishNotifier();
   DatabaseNotificationListener? _listener;
-  DatabaseLayoutListener(this.viewId);
+  DatabaseLayoutSettingListener(this.viewId);
 
   void start({
-    required void Function(LayoutSettingsValue<LayoutSettingPB>)
+    required void Function(LayoutSettingsValue<DatabaseLayoutSettingPB>)
         onLayoutChanged,
   }) {
     _settingNotifier?.addPublishListener(onLayoutChanged);
@@ -34,7 +34,7 @@ class DatabaseLayoutListener {
       case DatabaseNotification.DidUpdateLayoutSettings:
         result.fold(
           (payload) => _settingNotifier?.value =
-              left(LayoutSettingPB.fromBuffer(payload)),
+              left(DatabaseLayoutSettingPB.fromBuffer(payload)),
           (error) => _settingNotifier?.value = right(error),
         );
         break;

+ 28 - 2
frontend/appflowy_flutter/lib/plugins/database_view/application/setting/setting_bloc.dart

@@ -1,3 +1,5 @@
+import 'package:appflowy/generated/locale_keys.g.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';
@@ -40,7 +42,31 @@ class DatabaseSettingState with _$DatabaseSettingState {
 }
 
 enum DatabaseSettingAction {
-  showFilters,
-  sortBy,
   showProperties,
+  showLayout,
+  showGroup,
+}
+
+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';
+    }
+  }
+
+  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();
+    }
+  }
 }

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

@@ -9,21 +9,21 @@ import 'view_listener.dart';
 
 class DatabaseViewCallbacks {
   /// Will get called when number of rows were changed that includes
-  /// update/delete/insert rows. The [onRowsChanged] will return all
+  /// update/delete/insert rows. The [onNumOfRowsChanged] will return all
   /// the rows of the current database
-  final OnRowsChanged? onRowsChanged;
+  final OnNumOfRowsChanged? onNumOfRowsChanged;
 
   // Will get called when creating new rows
   final OnRowsCreated? onRowsCreated;
 
-  /// Will get called when number of rows were updated
+  /// Will get called when rows were updated
   final OnRowsUpdated? onRowsUpdated;
 
   /// Will get called when number of rows were deleted
   final OnRowsDeleted? onRowsDeleted;
 
   const DatabaseViewCallbacks({
-    this.onRowsChanged,
+    this.onNumOfRowsChanged,
     this.onRowsCreated,
     this.onRowsUpdated,
     this.onRowsDeleted,
@@ -101,7 +101,7 @@ class DatabaseViewCache {
     );
 
     _rowCache.onRowsChanged(
-      (reason) => _callbacks?.onRowsChanged?.call(
+      (reason) => _callbacks?.onNumOfRowsChanged?.call(
         rowInfos,
         _rowCache.rowByRowId,
         reason,

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

@@ -20,18 +20,17 @@ import 'group_controller.dart';
 part 'board_bloc.freezed.dart';
 
 class BoardBloc extends Bloc<BoardEvent, BoardState> {
-  final DatabaseController _databaseController;
+  final DatabaseController databaseController;
   late final AppFlowyBoardController boardController;
   final LinkedHashMap<String, GroupController> groupControllers =
       LinkedHashMap();
 
-  FieldController get fieldController => _databaseController.fieldController;
-  String get viewId => _databaseController.viewId;
+  FieldController get fieldController => databaseController.fieldController;
+  String get viewId => databaseController.viewId;
 
   BoardBloc({required ViewPB view})
-      : _databaseController = DatabaseController(
+      : databaseController = DatabaseController(
           view: view,
-          layoutType: DatabaseLayoutPB.Board,
         ),
         super(BoardState.initial(view.id)) {
     boardController = AppFlowyBoardController(
@@ -41,7 +40,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
         toGroupId,
         toIndex,
       ) {
-        _databaseController.moveGroup(
+        databaseController.moveGroup(
           fromGroupId: fromGroupId,
           toGroupId: toGroupId,
         );
@@ -54,7 +53,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
         final fromRow = groupControllers[groupId]?.rowAtIndex(fromIndex);
         final toRow = groupControllers[groupId]?.rowAtIndex(toIndex);
         if (fromRow != null) {
-          _databaseController.moveGroupRow(
+          databaseController.moveGroupRow(
             fromRow: fromRow,
             toRow: toRow,
             groupId: groupId,
@@ -70,7 +69,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
         final fromRow = groupControllers[fromGroupId]?.rowAtIndex(fromIndex);
         final toRow = groupControllers[toGroupId]?.rowAtIndex(toIndex);
         if (fromRow != null) {
-          _databaseController.moveGroupRow(
+          databaseController.moveGroupRow(
             fromRow: fromRow,
             toRow: toRow,
             groupId: toGroupId,
@@ -88,7 +87,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
           },
           createBottomRow: (groupId) async {
             final startRowId = groupControllers[groupId]?.lastRow()?.id;
-            final result = await _databaseController.createRow(
+            final result = await databaseController.createRow(
               groupId: groupId,
               startRowId: startRowId,
             );
@@ -98,8 +97,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
             );
           },
           createHeaderRow: (String groupId) async {
-            final result =
-                await _databaseController.createRow(groupId: groupId);
+            final result = await databaseController.createRow(groupId: groupId);
             result.fold(
               (_) {},
               (err) => Log.error(err),
@@ -170,7 +168,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
 
   @override
   Future<void> close() async {
-    await _databaseController.dispose();
+    await databaseController.dispose();
     for (final controller in groupControllers.values) {
       controller.dispose();
     }
@@ -198,7 +196,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
   }
 
   RowCache? getRowCache() {
-    return _databaseController.rowCache;
+    return databaseController.rowCache;
   }
 
   void _startListening() {
@@ -237,7 +235,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
       },
     );
 
-    _databaseController.setListener(
+    databaseController.setListener(
       onDatabaseChanged: onDatabaseChanged,
       onGroupChanged: onGroupChanged,
     );
@@ -256,7 +254,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
   }
 
   Future<void> _openGrid(Emitter<BoardState> emit) async {
-    final result = await _databaseController.open();
+    final result = await databaseController.open();
     result.fold(
       (grid) => emit(
         state.copyWith(loadingState: GridLoadingState.finish(left(unit))),

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

@@ -49,18 +49,19 @@ class BoardPlugin extends Plugin {
         notifier = ViewPluginNotifier(view: view);
 
   @override
-  PluginDisplay get display => GridPluginDisplay(notifier: notifier);
+  PluginWidgetBuilder get widgetBuilder =>
+      BoardPluginWidgetBuilder(notifier: notifier);
 
   @override
   PluginId get id => notifier.view.id;
 
   @override
-  PluginType get ty => _pluginType;
+  PluginType get pluginType => _pluginType;
 }
 
-class GridPluginDisplay extends PluginDisplay {
+class BoardPluginWidgetBuilder extends PluginWidgetBuilder {
   final ViewPluginNotifier notifier;
-  GridPluginDisplay({required this.notifier, Key? key});
+  BoardPluginWidgetBuilder({required this.notifier, Key? key});
 
   ViewPB get view => notifier.view;
 

+ 1 - 9
frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart

@@ -340,15 +340,7 @@ class _ToolbarBlocAdaptor extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     return BlocBuilder<BoardBloc, BoardState>(
-      builder: (context, state) {
-        final bloc = context.read<BoardBloc>();
-        final toolbarContext = BoardToolbarContext(
-          viewId: bloc.viewId,
-          fieldController: bloc.fieldController,
-        );
-
-        return BoardToolbar(toolbarContext: toolbarContext);
-      },
+      builder: (context, state) => const BoardToolbar(),
     );
   }
 }

+ 0 - 192
frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/toolbar/board_setting.dart

@@ -1,192 +0,0 @@
-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/board/application/toolbar/board_setting_bloc.dart';
-import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
-import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/grid_group.dart';
-import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/grid_property.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/style_widget/button.dart';
-import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
-import 'package:flowy_infra_ui/style_widget/text.dart';
-import 'package:flowy_infra_ui/widget/spacing.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_bloc/flutter_bloc.dart';
-import 'package:styled_widget/styled_widget.dart';
-
-import 'board_toolbar.dart';
-
-class BoardSettingContext {
-  final String viewId;
-  final FieldController fieldController;
-  BoardSettingContext({
-    required this.viewId,
-    required this.fieldController,
-  });
-
-  factory BoardSettingContext.from(BoardToolbarContext toolbarContext) =>
-      BoardSettingContext(
-        viewId: toolbarContext.viewId,
-        fieldController: toolbarContext.fieldController,
-      );
-}
-
-class BoardSettingList extends StatelessWidget {
-  final BoardSettingContext settingContext;
-  final Function(BoardSettingAction, BoardSettingContext) onAction;
-  const BoardSettingList({
-    required this.settingContext,
-    required this.onAction,
-    Key? key,
-  }) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return BlocProvider(
-      create: (context) => BoardSettingBloc(viewId: settingContext.viewId),
-      child: BlocListener<BoardSettingBloc, BoardSettingState>(
-        listenWhen: (previous, current) =>
-            previous.selectedAction != current.selectedAction,
-        listener: (context, state) {
-          state.selectedAction.foldLeft(null, (_, action) {
-            onAction(action, settingContext);
-          });
-        },
-        child: BlocBuilder<BoardSettingBloc, BoardSettingState>(
-          builder: (context, state) {
-            return _renderList();
-          },
-        ),
-      ),
-    );
-  }
-
-  Widget _renderList() {
-    final cells = BoardSettingAction.values.map((action) {
-      return _SettingItem(action: action);
-    }).toList();
-
-    return SizedBox(
-      width: 140,
-      child: ListView.separated(
-        shrinkWrap: true,
-        controller: ScrollController(),
-        itemCount: cells.length,
-        separatorBuilder: (context, index) {
-          return VSpace(GridSize.typeOptionSeparatorHeight);
-        },
-        physics: StyledScrollPhysics(),
-        itemBuilder: (BuildContext context, int index) {
-          return cells[index];
-        },
-      ),
-    );
-  }
-}
-
-class _SettingItem extends StatelessWidget {
-  final BoardSettingAction action;
-
-  const _SettingItem({
-    required this.action,
-    Key? key,
-  }) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    final isSelected = context
-        .read<BoardSettingBloc>()
-        .state
-        .selectedAction
-        .foldLeft(false, (_, selectedAction) => selectedAction == action);
-
-    return SizedBox(
-      height: 30,
-      child: FlowyButton(
-        hoverColor: AFThemeExtension.of(context).lightGreyHover,
-        isSelected: isSelected,
-        text: FlowyText.medium(
-          action.title(),
-          color: AFThemeExtension.of(context).textColor,
-        ),
-        onTap: () {
-          context
-              .read<BoardSettingBloc>()
-              .add(BoardSettingEvent.performAction(action));
-        },
-        leftIcon: svgWidget(
-          action.iconName(),
-          color: Theme.of(context).iconTheme.color,
-        ),
-      ),
-    );
-  }
-}
-
-extension _GridSettingExtension on BoardSettingAction {
-  String iconName() {
-    switch (this) {
-      case BoardSettingAction.properties:
-        return 'grid/setting/properties';
-      case BoardSettingAction.groups:
-        return 'grid/setting/group';
-    }
-  }
-
-  String title() {
-    switch (this) {
-      case BoardSettingAction.properties:
-        return LocaleKeys.grid_settings_Properties.tr();
-      case BoardSettingAction.groups:
-        return LocaleKeys.grid_settings_group.tr();
-    }
-  }
-}
-
-class BoardSettingListPopover extends StatefulWidget {
-  final PopoverController popoverController;
-  final BoardSettingContext settingContext;
-
-  const BoardSettingListPopover({
-    Key? key,
-    required this.popoverController,
-    required this.settingContext,
-  }) : super(key: key);
-
-  @override
-  State<StatefulWidget> createState() => _BoardSettingListPopoverState();
-}
-
-class _BoardSettingListPopoverState extends State<BoardSettingListPopover> {
-  BoardSettingAction? _action;
-
-  @override
-  Widget build(BuildContext context) {
-    if (_action != null) {
-      switch (_action!) {
-        case BoardSettingAction.groups:
-          return GridGroupList(
-            viewId: widget.settingContext.viewId,
-            fieldController: widget.settingContext.fieldController,
-            onDismissed: () {
-              widget.popoverController.close();
-            },
-          );
-        case BoardSettingAction.properties:
-          return GridPropertyList(
-            viewId: widget.settingContext.viewId,
-            fieldController: widget.settingContext.fieldController,
-          );
-      }
-    }
-
-    return BoardSettingList(
-      settingContext: widget.settingContext,
-      onAction: (action, settingContext) {
-        setState(() => _action = action);
-      },
-    ).padding(all: 6.0);
-  }
-}

+ 5 - 70
frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/toolbar/board_toolbar.dart

@@ -1,28 +1,10 @@
-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/presentation/layout/sizes.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:appflowy/plugins/database_view/board/application/board_bloc.dart';
+import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart';
 import 'package:flutter/material.dart';
-
-import 'board_setting.dart';
-
-class BoardToolbarContext {
-  final String viewId;
-  final FieldController fieldController;
-
-  BoardToolbarContext({
-    required this.viewId,
-    required this.fieldController,
-  });
-}
+import 'package:flutter_bloc/flutter_bloc.dart';
 
 class BoardToolbar extends StatelessWidget {
-  final BoardToolbarContext toolbarContext;
   const BoardToolbar({
-    required this.toolbarContext,
     Key? key,
   }) : super(key: key);
 
@@ -33,58 +15,11 @@ class BoardToolbar extends StatelessWidget {
       child: Row(
         children: [
           const Spacer(),
-          _SettingButton(
-            settingContext: BoardSettingContext.from(toolbarContext),
+          SettingButton(
+            databaseController: context.read<BoardBloc>().databaseController,
           ),
         ],
       ),
     );
   }
 }
-
-class _SettingButton extends StatefulWidget {
-  final BoardSettingContext settingContext;
-  const _SettingButton({required this.settingContext, Key? key})
-      : super(key: key);
-
-  @override
-  State<_SettingButton> createState() => _SettingButtonState();
-}
-
-class _SettingButtonState extends State<_SettingButton> {
-  late PopoverController popoverController;
-
-  @override
-  void initState() {
-    popoverController = PopoverController();
-    super.initState();
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    return AppFlowyPopover(
-      controller: popoverController,
-      direction: PopoverDirection.leftWithTopAligned,
-      offset: const Offset(-8, 0),
-      triggerActions: PopoverTriggerFlags.none,
-      constraints: BoxConstraints.loose(const Size(260, 400)),
-      margin: EdgeInsets.zero,
-      child: FlowyTextButton(
-        LocaleKeys.settings_title.tr(),
-        fontColor: AFThemeExtension.of(context).textColor,
-        fillColor: Colors.transparent,
-        hoverColor: AFThemeExtension.of(context).lightGreyHover,
-        padding: GridSize.typeOptionContentInsets,
-        onPressed: () {
-          popoverController.show();
-        },
-      ),
-      popupBuilder: (BuildContext popoverContext) {
-        return BoardSettingListPopover(
-          settingContext: widget.settingContext,
-          popoverController: popoverController,
-        );
-      },
-    );
-  }
-}

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

@@ -18,20 +18,17 @@ import '../../application/row/row_cache.dart';
 part 'calendar_bloc.freezed.dart';
 
 class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
-  final DatabaseController _databaseController;
+  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;
+  String get viewId => databaseController.viewId;
+  FieldController get fieldController => databaseController.fieldController;
+  CellCache get cellCache => databaseController.rowCache.cellCache;
+  RowCache get rowCache => databaseController.rowCache;
 
   CalendarBloc({required ViewPB view})
-      : _databaseController = DatabaseController(
-          view: view,
-          layoutType: DatabaseLayoutPB.Calendar,
-        ),
+      : databaseController = DatabaseController(view: view),
         super(CalendarState.initial()) {
     on<CalendarEvent>(
       (event, emit) async {
@@ -110,12 +107,12 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
 
   @override
   Future<void> close() async {
-    await _databaseController.dispose();
+    await databaseController.dispose();
     return super.close();
   }
 
   FieldInfo? _getCalendarFieldInfo(String fieldId) {
-    final fieldInfos = _databaseController.fieldController.fieldInfos;
+    final fieldInfos = databaseController.fieldController.fieldInfos;
     final index = fieldInfos.indexWhere(
       (element) => element.field.id == fieldId,
     );
@@ -127,7 +124,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
   }
 
   FieldInfo? _getTitleFieldInfo() {
-    final fieldInfos = _databaseController.fieldController.fieldInfos;
+    final fieldInfos = databaseController.fieldController.fieldInfos;
     final index = fieldInfos.indexWhere(
       (element) => element.field.isPrimary,
     );
@@ -139,7 +136,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
   }
 
   Future<void> _openDatabase(Emitter<CalendarState> emit) async {
-    final result = await _databaseController.open();
+    final result = await databaseController.open();
     result.fold(
       (database) => emit(
         state.copyWith(loadingState: DatabaseLoadingState.finish(left(unit))),
@@ -157,7 +154,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
         final dateField = _getCalendarFieldInfo(settings.fieldId);
         final titleField = _getTitleFieldInfo();
         if (dateField != null && titleField != null) {
-          final newRow = await _databaseController.createRow(
+          final newRow = await databaseController.createRow(
             withCells: (builder) {
               builder.insertDate(dateField, date);
               builder.insertText(titleField, title);
@@ -210,7 +207,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
   Future<void> _updateCalendarLayoutSetting(
     CalendarLayoutSettingPB layoutSetting,
   ) async {
-    return _databaseController.updateCalenderLayoutSetting(layoutSetting);
+    return databaseController.updateCalenderLayoutSetting(layoutSetting);
   }
 
   Future<CalendarEventData<CalendarDayEvent>?> _loadEvent(RowId rowId) async {
@@ -331,7 +328,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
       },
     );
 
-    final onLayoutChanged = LayoutCallbacks(
+    final onLayoutChanged = DatabaseLayoutSettingCallbacks(
       onLayoutChanged: _didReceiveLayoutSetting,
       onLoadLayout: _didReceiveLayoutSetting,
     );
@@ -340,14 +337,14 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
       onCalendarLayoutChanged: _didReceiveNewLayoutField,
     );
 
-    _databaseController.setListener(
+    databaseController.setListener(
       onDatabaseChanged: onDatabaseChanged,
       onLayoutChanged: onLayoutChanged,
       onCalendarLayoutChanged: onCalendarLayoutFieldChanged,
     );
   }
 
-  void _didReceiveLayoutSetting(LayoutSettingPB layoutSetting) {
+  void _didReceiveLayoutSetting(DatabaseLayoutSettingPB layoutSetting) {
     if (layoutSetting.hasCalendar()) {
       if (isClosed) {
         return;
@@ -356,7 +353,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
     }
   }
 
-  void _didReceiveNewLayoutField(LayoutSettingPB layoutSetting) {
+  void _didReceiveNewLayoutField(DatabaseLayoutSettingPB layoutSetting) {
     if (layoutSetting.hasCalendar()) {
       if (isClosed) return;
       add(CalendarEvent.didReceiveNewLayoutField(layoutSetting.calendar));

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

@@ -49,18 +49,19 @@ class CalendarPlugin extends Plugin {
         notifier = ViewPluginNotifier(view: view);
 
   @override
-  PluginDisplay get display => CalendarPluginDisplay(notifier: notifier);
+  PluginWidgetBuilder get widgetBuilder =>
+      CalendarPluginWidgetBuilder(notifier: notifier);
 
   @override
   PluginId get id => notifier.view.id;
 
   @override
-  PluginType get ty => _pluginType;
+  PluginType get pluginType => _pluginType;
 }
 
-class CalendarPluginDisplay extends PluginDisplay {
+class CalendarPluginWidgetBuilder extends PluginWidgetBuilder {
   final ViewPluginNotifier notifier;
-  CalendarPluginDisplay({required this.notifier, Key? key});
+  CalendarPluginWidgetBuilder({required this.notifier, Key? key});
 
   ViewPB get view => notifier.view;
 

+ 2 - 2
frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_setting.dart

@@ -2,7 +2,7 @@ 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/calendar/application/calendar_setting_bloc.dart';
 import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
-import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/grid_property.dart';
+import 'package:appflowy/plugins/database_view/widgets/field/grid_property.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra/image.dart';
@@ -41,7 +41,7 @@ class CalendarSetting extends StatelessWidget {
               state.selectedAction.foldLeft(null, (previous, action) => action);
           switch (action) {
             case CalendarSettingAction.properties:
-              return GridPropertyList(
+              return DatabasePropertyList(
                 viewId: settingContext.viewId,
                 fieldController: settingContext.fieldController,
               );

+ 20 - 9
frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_toolbar.dart

@@ -1,6 +1,7 @@
 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';
@@ -17,13 +18,15 @@ class CalendarToolbar extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    return const SizedBox(
+    return SizedBox(
       height: 40,
       child: Row(
         mainAxisAlignment: MainAxisAlignment.end,
         children: [
-          _UnscheduleEventsButton(),
-          _SettingButton(),
+          const _UnscheduleEventsButton(),
+          SettingButton(
+            databaseController: context.read<CalendarBloc>().databaseController,
+          ),
         ],
       ),
     );
@@ -115,12 +118,16 @@ class _UnscheduleEventsButtonState extends State<_UnscheduleEventsButton> {
           ),
           popupBuilder: (context) {
             final cells = <Widget>[
-              FlowyText.medium(
-                LocaleKeys.calendar_settings_noDateHint.tr(),
-                color: Theme.of(context).hintColor,
-                overflow: TextOverflow.ellipsis,
+              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(10),
+              const VSpace(6),
               ...unscheduledEvents.map(
                 (e) => _UnscheduledEventItem(
                   event: e,
@@ -164,7 +171,11 @@ class _UnscheduledEventItem extends StatelessWidget {
     return SizedBox(
       height: GridSize.popoverItemHeight,
       child: FlowyButton(
-        text: FlowyText.medium(event.title),
+        text: FlowyText.medium(
+          event.title.isEmpty
+              ? LocaleKeys.calendar_defaultNewCalendarTitle.tr()
+              : event.title,
+        ),
         onTap: onPressed,
       ),
     );

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

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

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

@@ -87,7 +87,7 @@ class GridBloc extends Bloc<GridEvent, GridState> {
           add(GridEvent.didReceiveGridUpdate(database));
         }
       },
-      onRowsChanged: (rowInfos, _, reason) {
+      onNumOfRowsChanged: (rowInfos, _, reason) {
         if (!isClosed) {
           add(GridEvent.didReceiveRowUpdate(rowInfos, reason));
         }

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

@@ -49,20 +49,21 @@ class GridPlugin extends Plugin {
         notifier = ViewPluginNotifier(view: view);
 
   @override
-  PluginDisplay get display => GridPluginDisplay(notifier: notifier);
+  PluginWidgetBuilder get widgetBuilder =>
+      GridPluginWidgetBuilder(notifier: notifier);
 
   @override
   PluginId get id => notifier.view.id;
 
   @override
-  PluginType get ty => _pluginType;
+  PluginType get pluginType => _pluginType;
 }
 
-class GridPluginDisplay extends PluginDisplay {
+class GridPluginWidgetBuilder extends PluginWidgetBuilder {
   final ViewPluginNotifier notifier;
   ViewPB get view => notifier.view;
 
-  GridPluginDisplay({required this.notifier, Key? key});
+  GridPluginWidgetBuilder({required this.notifier, Key? key});
 
   @override
   Widget get leftBarItem => ViewLeftBarItem(view: view);

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

@@ -1,6 +1,5 @@
 import 'package:appflowy/generated/locale_keys.g.dart';
 import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
-import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
 import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
@@ -36,10 +35,7 @@ class GridPage extends StatefulWidget {
     required this.view,
     this.onDeleted,
     Key? key,
-  })  : databaseController = DatabaseController(
-          view: view,
-          layoutType: DatabaseLayoutPB.Grid,
-        ),
+  })  : databaseController = DatabaseController(view: view),
         super(key: key);
 
   final ViewPB view;

+ 133 - 0
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/grid_layout.dart

@@ -0,0 +1,133 @@
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/database_view/application/layout/layout_bloc.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';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:styled_widget/styled_widget.dart';
+
+import '../../layout/sizes.dart';
+
+class DatabaseLayoutList extends StatefulWidget {
+  final String viewId;
+  final DatabaseLayoutPB currentLayout;
+  const DatabaseLayoutList({
+    required this.viewId,
+    required this.currentLayout,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _DatabaseLayoutListState();
+}
+
+class _DatabaseLayoutListState extends State<DatabaseLayoutList> {
+  @override
+  void initState() {
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocProvider(
+      create: (context) => DatabaseLayoutBloc(
+        viewId: widget.viewId,
+        databaseLayout: widget.currentLayout,
+      )..add(const DatabaseLayoutEvent.initial()),
+      child: BlocBuilder<DatabaseLayoutBloc, DatabaseLayoutState>(
+        builder: (context, state) {
+          final cells = DatabaseLayoutPB.values.map((layout) {
+            final isSelected = state.databaseLayout == layout;
+            return DatabaseViewLayoutCell(
+              databaseLayout: layout,
+              isSelected: isSelected,
+              onTap: (selectedLayout) {
+                context
+                    .read<DatabaseLayoutBloc>()
+                    .add(DatabaseLayoutEvent.updateLayout(selectedLayout));
+              },
+            );
+          }).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),
+          );
+        },
+      ),
+    );
+  }
+}
+
+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;
+  final void Function(DatabaseLayoutPB) onTap;
+  const DatabaseViewLayoutCell({
+    required this.databaseLayout,
+    required this.isSelected,
+    required this.onTap,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    Widget? checkmark;
+    if (isSelected) {
+      checkmark = svgWidget("grid/checkmark");
+    }
+
+    return SizedBox(
+      height: GridSize.popoverItemHeight,
+      child: FlowyButton(
+        hoverColor: AFThemeExtension.of(context).lightGreyHover,
+        text: FlowyText.medium(
+          databaseLayout.layoutName(),
+          color: AFThemeExtension.of(context).textColor,
+        ),
+        leftIcon: svgWidget(
+          databaseLayout.iconName(),
+          color: Theme.of(context).iconTheme.color,
+        ),
+        rightIcon: checkmark,
+        onTap: () => onTap(databaseLayout),
+      ).padding(horizontal: 6.0),
+    );
+  }
+}

+ 16 - 58
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/grid_setting.dart

@@ -1,6 +1,6 @@
-import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
+import 'package:appflowy/plugins/database_view/application/database_controller.dart';
 import 'package:appflowy/plugins/database_view/application/setting/setting_bloc.dart';
-import 'package:easy_localization/easy_localization.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra/theme_extension.dart';
 import 'package:flowy_infra_ui/style_widget/button.dart';
@@ -9,36 +9,29 @@ import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flowy_infra_ui/widget/spacing.dart';
 import 'package:flutter/material.dart';
 
-import 'package:appflowy/generated/locale_keys.g.dart';
 import '../../layout/sizes.dart';
 
-class GridSettingContext {
-  final String viewId;
-  final FieldController fieldController;
-
-  GridSettingContext({
-    required this.viewId,
-    required this.fieldController,
-  });
-}
-
-class GridSettingList extends StatelessWidget {
-  final GridSettingContext settingContext;
-  final Function(DatabaseSettingAction, GridSettingContext) onAction;
-  const GridSettingList({
-    required this.settingContext,
+class DatabaseSettingList extends StatelessWidget {
+  final DatabaseController databaseContoller;
+  final Function(DatabaseSettingAction, DatabaseController) onAction;
+  const DatabaseSettingList({
+    required this.databaseContoller,
     required this.onAction,
     Key? key,
   }) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
-    final cells = DatabaseSettingAction.values
-        .where((value) => value.enable())
-        .map((action) {
+    final cells = DatabaseSettingAction.values.where((element) {
+      if (element == DatabaseSettingAction.showGroup) {
+        return databaseContoller.databaseLayout == DatabaseLayoutPB.Board;
+      } else {
+        return true;
+      }
+    }).map((action) {
       return _SettingItem(
         action: action,
-        onAction: (action) => onAction(action, settingContext),
+        onAction: (action) => onAction(action, databaseContoller),
       );
     }).toList();
 
@@ -78,9 +71,7 @@ class _SettingItem extends StatelessWidget {
         hoverColor: AFThemeExtension.of(context).lightGreyHover,
         text: FlowyText.medium(
           action.title(),
-          color: action.enable()
-              ? AFThemeExtension.of(context).textColor
-              : Theme.of(context).disabledColor,
+          color: AFThemeExtension.of(context).textColor,
         ),
         onTap: () => onAction(action),
         leftIcon: svgWidget(
@@ -91,36 +82,3 @@ class _SettingItem extends StatelessWidget {
     );
   }
 }
-
-extension _GridSettingExtension on DatabaseSettingAction {
-  String iconName() {
-    switch (this) {
-      case DatabaseSettingAction.showFilters:
-        return 'grid/setting/filter';
-      case DatabaseSettingAction.sortBy:
-        return 'grid/setting/sort';
-      case DatabaseSettingAction.showProperties:
-        return 'grid/setting/properties';
-    }
-  }
-
-  String title() {
-    switch (this) {
-      case DatabaseSettingAction.showFilters:
-        return LocaleKeys.grid_settings_filter.tr();
-      case DatabaseSettingAction.sortBy:
-        return LocaleKeys.grid_settings_sortBy.tr();
-      case DatabaseSettingAction.showProperties:
-        return LocaleKeys.grid_settings_Properties.tr();
-    }
-  }
-
-  bool enable() {
-    switch (this) {
-      case DatabaseSettingAction.showProperties:
-        return true;
-      default:
-        return false;
-    }
-  }
-}

+ 6 - 2
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/grid_toolbar.dart

@@ -1,9 +1,11 @@
 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 'setting_button.dart';
+import '../../../../widgets/setting/setting_button.dart';
 import 'sort_button.dart';
 
 class GridToolbarContext {
@@ -29,7 +31,9 @@ class GridToolbar extends StatelessWidget {
           const Spacer(),
           const FilterButton(),
           const SortButton(),
-          const SettingButton(),
+          SettingButton(
+            databaseController: context.read<GridBloc>().databaseController,
+          ),
         ],
       ),
     );

+ 0 - 102
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/setting_button.dart

@@ -1,102 +0,0 @@
-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/grid/application/grid_bloc.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';
-import 'package:styled_widget/styled_widget.dart';
-
-import '../../layout/sizes.dart';
-import 'grid_property.dart';
-import 'grid_setting.dart';
-
-class SettingButton extends StatefulWidget {
-  const SettingButton({Key? key}) : super(key: key);
-
-  @override
-  State<SettingButton> createState() => _SettingButtonState();
-}
-
-class _SettingButtonState extends State<SettingButton> {
-  late PopoverController _popoverController;
-
-  @override
-  void initState() {
-    _popoverController = PopoverController();
-    super.initState();
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    return BlocSelector<GridBloc, GridState, GridSettingContext>(
-      selector: (state) {
-        final fieldController =
-            context.read<GridBloc>().databaseController.fieldController;
-        return GridSettingContext(
-          viewId: state.viewId,
-          fieldController: fieldController,
-        );
-      },
-      builder: (context, settingContext) {
-        return SizedBox(
-          height: 26,
-          child: AppFlowyPopover(
-            controller: _popoverController,
-            constraints: BoxConstraints.loose(const Size(260, 400)),
-            direction: PopoverDirection.bottomWithLeftAligned,
-            offset: const Offset(0, 8),
-            margin: EdgeInsets.zero,
-            triggerActions: PopoverTriggerFlags.none,
-            child: FlowyTextButton(
-              LocaleKeys.settings_title.tr(),
-              fontColor: AFThemeExtension.of(context).textColor,
-              fillColor: Colors.transparent,
-              hoverColor: AFThemeExtension.of(context).lightGreyHover,
-              padding: GridSize.typeOptionContentInsets,
-              onPressed: () => _popoverController.show(),
-            ),
-            popupBuilder: (BuildContext context) {
-              return _GridSettingListPopover(settingContext: settingContext);
-            },
-          ),
-        );
-      },
-    );
-  }
-}
-
-class _GridSettingListPopover extends StatefulWidget {
-  final GridSettingContext settingContext;
-
-  const _GridSettingListPopover({Key? key, required this.settingContext})
-      : super(key: key);
-
-  @override
-  State<StatefulWidget> createState() => _GridSettingListPopoverState();
-}
-
-class _GridSettingListPopoverState extends State<_GridSettingListPopover> {
-  DatabaseSettingAction? _action;
-
-  @override
-  Widget build(BuildContext context) {
-    if (_action == DatabaseSettingAction.showProperties) {
-      return GridPropertyList(
-        viewId: widget.settingContext.viewId,
-        fieldController: widget.settingContext.fieldController,
-      );
-    }
-
-    return GridSettingList(
-      settingContext: widget.settingContext,
-      onAction: (action, settingContext) {
-        setState(() {
-          _action = action;
-        });
-      },
-    ).padding(all: 6.0);
-  }
-}

+ 6 - 6
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/grid_property.dart → frontend/appflowy_flutter/lib/plugins/database_view/widgets/field/grid_property.dart

@@ -11,23 +11,23 @@ import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:styled_widget/styled_widget.dart';
 
-import '../../layout/sizes.dart';
-import '../header/field_editor.dart';
+import '../../grid/presentation/layout/sizes.dart';
+import '../../grid/presentation/widgets/header/field_editor.dart';
 
-class GridPropertyList extends StatefulWidget {
+class DatabasePropertyList extends StatefulWidget {
   final String viewId;
   final FieldController fieldController;
-  const GridPropertyList({
+  const DatabasePropertyList({
     required this.viewId,
     required this.fieldController,
     Key? key,
   }) : super(key: key);
 
   @override
-  State<StatefulWidget> createState() => _GridPropertyListState();
+  State<StatefulWidget> createState() => _DatabasePropertyListState();
 }
 
-class _GridPropertyListState extends State<GridPropertyList> {
+class _DatabasePropertyListState extends State<DatabasePropertyList> {
   late PopoverMutex _popoverMutex;
 
   @override

+ 2 - 2
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/grid_group.dart → frontend/appflowy_flutter/lib/plugins/database_view/widgets/group/database_group.dart

@@ -11,11 +11,11 @@ import 'package:flutter/material.dart';
 
 import 'package:flutter_bloc/flutter_bloc.dart';
 
-class GridGroupList extends StatelessWidget {
+class DatabaseGroupList extends StatelessWidget {
   final String viewId;
   final FieldController fieldController;
   final VoidCallback onDismissed;
-  const GridGroupList({
+  const DatabaseGroupList({
     required this.viewId,
     required this.fieldController,
     required this.onDismissed,

+ 116 - 0
frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/setting_button.dart

@@ -0,0 +1,116 @@
+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/widgets/group/database_group.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:styled_widget/styled_widget.dart';
+
+import '../../grid/presentation/layout/sizes.dart';
+import '../../grid/presentation/widgets/toolbar/grid_layout.dart';
+import '../field/grid_property.dart';
+import '../../grid/presentation/widgets/toolbar/grid_setting.dart';
+
+class SettingButton extends StatefulWidget {
+  final DatabaseController databaseController;
+  const SettingButton({
+    required this.databaseController,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  State<SettingButton> createState() => _SettingButtonState();
+}
+
+class _SettingButtonState extends State<SettingButton> {
+  late PopoverController _popoverController;
+
+  @override
+  void initState() {
+    _popoverController = PopoverController();
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return SizedBox(
+      height: 26,
+      child: 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: FlowyTextButton(
+          LocaleKeys.settings_title.tr(),
+          fontColor: AFThemeExtension.of(context).textColor,
+          fillColor: Colors.transparent,
+          hoverColor: AFThemeExtension.of(context).lightGreyHover,
+          padding: GridSize.typeOptionContentInsets,
+          onPressed: () => _popoverController.show(),
+        ),
+        popupBuilder: (BuildContext context) {
+          return _DatabaseSettingListPopover(
+            databaseController: widget.databaseController,
+          );
+        },
+      ),
+    );
+  }
+}
+
+class _DatabaseSettingListPopover extends StatefulWidget {
+  final DatabaseController databaseController;
+
+  const _DatabaseSettingListPopover({
+    required this.databaseController,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _DatabaseSettingListPopoverState();
+}
+
+class _DatabaseSettingListPopoverState
+    extends State<_DatabaseSettingListPopover> {
+  DatabaseSettingAction? _action;
+
+  @override
+  Widget build(BuildContext context) {
+    if (_action == null) {
+      return DatabaseSettingList(
+        databaseContoller: widget.databaseController,
+        onAction: (action, settingContext) {
+          setState(() {
+            _action = action;
+          });
+        },
+      ).padding(all: 6.0);
+    } else {
+      switch (_action!) {
+        case DatabaseSettingAction.showLayout:
+          return DatabaseLayoutList(
+            viewId: widget.databaseController.viewId,
+            currentLayout: widget.databaseController.databaseLayout!,
+          );
+        case DatabaseSettingAction.showGroup:
+          return DatabaseGroupList(
+            viewId: widget.databaseController.viewId,
+            fieldController: widget.databaseController.fieldController,
+            onDismissed: () {
+              // widget.popoverController.close();
+            },
+          );
+        case DatabaseSettingAction.showProperties:
+          return DatabasePropertyList(
+            viewId: widget.databaseController.viewId,
+            fieldController: widget.databaseController.fieldController,
+          );
+      }
+    }
+  }
+}

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

@@ -61,32 +61,36 @@ class DocumentPlugin extends Plugin<int> {
   }
 
   @override
-  PluginDisplay get display {
-    return DocumentPluginDisplay(
+  PluginWidgetBuilder get widgetBuilder {
+    return DocumentPluginWidgetBuilder(
       notifier: notifier,
       documentAppearanceCubit: _documentAppearanceCubit,
     );
   }
 
   @override
-  PluginType get ty => _pluginType;
+  PluginType get pluginType => _pluginType;
 
   @override
   PluginId get id => notifier.view.id;
 }
 
-class DocumentPluginDisplay extends PluginDisplay with NavigationItem {
+class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
+    with NavigationItem {
   final ViewPluginNotifier notifier;
   ViewPB get view => notifier.view;
   int? deletedViewIndex;
   DocumentAppearanceCubit documentAppearanceCubit;
 
-  DocumentPluginDisplay({
+  DocumentPluginWidgetBuilder({
     required this.notifier,
     required this.documentAppearanceCubit,
     Key? key,
   });
 
+  @override
+  EdgeInsets get contentPadding => EdgeInsets.zero;
+
   @override
   Widget buildWidget(PluginContext context) {
     notifier.isDeleted.addListener(() {

+ 10 - 4
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart

@@ -1,6 +1,6 @@
 import 'package:appflowy/plugins/document/presentation/editor_plugins/base/insert_page_command.dart';
 import 'package:appflowy/startup/startup.dart';
-import 'package:appflowy/workspace/application/app/app_service.dart';
+import 'package:appflowy/workspace/application/view/view_service.dart';
 import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
 import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
@@ -38,13 +38,18 @@ class _BuiltInPageWidgetState extends State<BuiltInPageWidget> {
   late Future<dartz.Either<FlowyError, ViewPB>> future;
   final focusNode = FocusNode();
 
-  String get appId => widget.node.attributes[DatabaseBlockKeys.kAppID];
-  String get viewId => widget.node.attributes[DatabaseBlockKeys.kViewID];
+  String get parentViewId => widget.node.attributes[DatabaseBlockKeys.parentID];
+  String get childViewId => widget.node.attributes[DatabaseBlockKeys.viewID];
 
   @override
   void initState() {
     super.initState();
-    future = AppBackendService().getChildView(viewId, appId).then(
+    future = ViewBackendService()
+        .getChildView(
+          parentViewId: parentViewId,
+          childViewId: childViewId,
+        )
+        .then(
           (value) => value.swap(),
         );
   }
@@ -153,6 +158,7 @@ class _BuiltInPageWidgetState extends State<BuiltInPageWidget> {
               switch (action.inner) {
                 case _ActionType.viewDatabase:
                   getIt<MenuSharedState>().latestOpenView = viewPB;
+
                   getIt<HomeStackManager>().setPlugin(viewPB.plugin());
                   break;
                 case _ActionType.delete:

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

@@ -2,20 +2,20 @@ import 'package:appflowy/generated/locale_keys.g.dart';
 import 'package:appflowy/plugins/database_view/application/database_view_service.dart';
 import 'package:appflowy/plugins/document/presentation/editor_plugins/board/board_node_widget.dart';
 import 'package:appflowy/plugins/document/presentation/editor_plugins/grid/grid_node_widget.dart';
+import 'package:appflowy/workspace/application/view/view_service.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
-import 'package:appflowy/workspace/application/app/app_service.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:easy_localization/easy_localization.dart';
 
 class DatabaseBlockKeys {
   const DatabaseBlockKeys._();
 
-  static const String kAppID = 'app_id';
-  static const String kViewID = 'view_id';
+  static const String parentID = 'parent_id';
+  static const String viewID = 'view_id';
 }
 
 extension InsertDatabase on EditorState {
-  Future<void> insertPage(ViewPB appPB, ViewPB viewPB) async {
+  Future<void> insertPage(ViewPB parentView, ViewPB childView) async {
     final selection = this.selection;
     if (selection == null || !selection.isCollapsed) {
       return;
@@ -26,23 +26,22 @@ extension InsertDatabase on EditorState {
     }
 
     // get the database that the view is associated with
-    final database = await DatabaseViewBackendService(viewId: viewPB.id)
+    final database = await DatabaseViewBackendService(viewId: childView.id)
         .openGrid()
         .then((value) => value.swap().toOption().toNullable());
+
     if (database == null) {
       throw StateError(
-        'The database associated with ${viewPB.id} could not be found while attempting to create a referenced ${viewPB.layout.name}.',
+        'The database associated with ${childView.id} could not be found while attempting to create a referenced ${childView.layout.name}.',
       );
     }
 
-    final prefix = _referencedDatabasePrefix(viewPB.layout);
-    final ref = await AppBackendService().createView(
-      parentViewId: appPB.id,
-      name: "$prefix ${viewPB.name}",
-      layoutType: viewPB.layout,
-      ext: {
-        'database_id': database.id,
-      },
+    final prefix = _referencedDatabasePrefix(childView.layout);
+    final ref = await ViewBackendService.createDatabaseReferenceView(
+      parentViewId: parentView.id,
+      name: "$prefix ${childView.name}",
+      layoutType: childView.layout,
+      databaseId: database.id,
     ).then((value) => value.swap().toOption().toNullable());
 
     // TODO(a-wallen): Show error dialog here.
@@ -54,10 +53,10 @@ extension InsertDatabase on EditorState {
     transaction.insertNode(
       selection.end.path,
       Node(
-        type: _convertPageType(viewPB),
+        type: _convertPageType(childView),
         attributes: {
-          DatabaseBlockKeys.kAppID: appPB.id,
-          DatabaseBlockKeys.kViewID: ref.id,
+          DatabaseBlockKeys.parentID: parentView.id,
+          DatabaseBlockKeys.viewID: ref.id,
         },
       ),
     );

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

@@ -1,5 +1,5 @@
 import 'package:appflowy/plugins/document/presentation/editor_plugins/base/insert_page_command.dart';
-import 'package:appflowy/workspace/application/app/app_service.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';
 import 'package:flowy_infra/image.dart';
@@ -69,7 +69,7 @@ class _LinkToPageMenuState extends State<LinkToPageMenu> {
   final Map<int, (ViewPB, ViewPB)> _items = {};
 
   Future<List<(ViewPB, List<ViewPB>)>> fetchItems() async {
-    final items = await AppBackendService().fetchViews(widget.layoutType);
+    final items = await ViewBackendService().fetchViews(widget.layoutType);
 
     int index = 0;
     for (final (app, children) in items) {

+ 2 - 2
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/board/board_node_widget.dart

@@ -37,8 +37,8 @@ class BoardBlockComponentBuilder extends BlockComponentBuilder {
   @override
   bool validate(Node node) =>
       node.children.isEmpty &&
-      node.attributes[DatabaseBlockKeys.kAppID] is String &&
-      node.attributes[DatabaseBlockKeys.kViewID] is String;
+      node.attributes[DatabaseBlockKeys.parentID] is String &&
+      node.attributes[DatabaseBlockKeys.viewID] is String;
 }
 
 class BoardBlockComponentWidget extends BlockComponentStatefulWidget {

+ 7 - 4
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/board/board_view_menu_item.dart

@@ -1,10 +1,10 @@
 import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.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';
 import 'package:appflowy/generated/locale_keys.g.dart';
 import 'package:appflowy/plugins/document/application/prelude.dart';
 import 'package:appflowy/plugins/document/presentation/editor_plugins/base/insert_page_command.dart';
-import 'package:appflowy/workspace/application/app/app_service.dart';
 import 'package:easy_localization/easy_localization.dart';
 
 SelectionMenuItem boardViewMenuItem(DocumentBloc documentBloc) =>
@@ -22,9 +22,9 @@ SelectionMenuItem boardViewMenuItem(DocumentBloc documentBloc) =>
         }
 
         final appId = documentBloc.view.parentViewId;
-        final service = AppBackendService();
+        final service = ViewBackendService();
 
-        final result = (await service.createView(
+        final result = (await ViewBackendService.createView(
           parentViewId: appId,
           name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
           layoutType: ViewLayoutPB.Board,
@@ -42,7 +42,10 @@ SelectionMenuItem boardViewMenuItem(DocumentBloc documentBloc) =>
           return;
         }
 
-        final view = (await service.getChildView(result.viewId, result.id))
+        final view = (await service.getChildView(
+          parentViewId: result.viewId,
+          childViewId: result.id,
+        ))
             .getLeftOrNull();
         // As this.
         if (view == null) {

+ 2 - 2
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/grid/grid_node_widget.dart

@@ -37,8 +37,8 @@ class GridBlockComponentBuilder extends BlockComponentBuilder {
   @override
   bool validate(Node node) =>
       node.children.isEmpty &&
-      node.attributes[DatabaseBlockKeys.kAppID] is String &&
-      node.attributes[DatabaseBlockKeys.kViewID] is String;
+      node.attributes[DatabaseBlockKeys.parentID] is String &&
+      node.attributes[DatabaseBlockKeys.viewID] is String;
 }
 
 class GridBlockComponentWidget extends BlockComponentStatefulWidget {

+ 7 - 4
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/grid/grid_view_menu_item.dart

@@ -2,7 +2,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
 import 'package:appflowy/plugins/document/application/doc_bloc.dart';
 import 'package:appflowy/plugins/document/presentation/editor_plugins/base/insert_page_command.dart';
 import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';
-import 'package:appflowy/workspace/application/app/app_service.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';
 import 'package:easy_localization/easy_localization.dart';
@@ -22,9 +22,9 @@ SelectionMenuItem gridViewMenuItem(DocumentBloc documentBloc) =>
         }
 
         final appId = documentBloc.view.parentViewId;
-        final service = AppBackendService();
+        final service = ViewBackendService();
 
-        final result = (await service.createView(
+        final result = (await ViewBackendService.createView(
           parentViewId: appId,
           name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
           layoutType: ViewLayoutPB.Grid,
@@ -42,7 +42,10 @@ SelectionMenuItem gridViewMenuItem(DocumentBloc documentBloc) =>
           return;
         }
 
-        final view = (await service.getChildView(result.viewId, result.id))
+        final view = (await service.getChildView(
+          parentViewId: result.viewId,
+          childViewId: result.id,
+        ))
             .getLeftOrNull();
         // As this.
         if (view == null) {

+ 3 - 3
frontend/appflowy_flutter/lib/plugins/trash/trash.dart

@@ -38,16 +38,16 @@ class TrashPlugin extends Plugin {
   TrashPlugin({required PluginType pluginType}) : _pluginType = pluginType;
 
   @override
-  PluginDisplay get display => TrashPluginDisplay();
+  PluginWidgetBuilder get widgetBuilder => TrashPluginDisplay();
 
   @override
   PluginId get id => "TrashStack";
 
   @override
-  PluginType get ty => _pluginType;
+  PluginType get pluginType => _pluginType;
 }
 
-class TrashPluginDisplay extends PluginDisplay {
+class TrashPluginDisplay extends PluginWidgetBuilder {
   @override
   Widget get leftBarItem => FlowyText.medium(LocaleKeys.trash_text.tr());
 

+ 12 - 4
frontend/appflowy_flutter/lib/startup/plugin/plugin.dart

@@ -22,11 +22,11 @@ typedef PluginId = String;
 abstract class Plugin<T> {
   PluginId get id;
 
-  PluginDisplay get display;
+  PluginWidgetBuilder get widgetBuilder;
 
   PluginNotifier? get notifier => null;
 
-  PluginType get ty;
+  PluginType get pluginType;
 
   void dispose() {
     notifier?.dispose();
@@ -37,7 +37,7 @@ abstract class PluginNotifier<T> {
   /// Notify if the plugin get deleted
   ValueNotifier<T> get isDeleted;
 
-  /// Notify if the [PluginDisplay]'s content was changed
+  /// Notify if the [PluginWidgetBuilder]'s content was changed
   ValueNotifier<int> get isDisplayChanged;
 
   void dispose() {}
@@ -50,8 +50,11 @@ abstract class PluginBuilder {
 
   String get menuIcon;
 
+  /// The type of this [Plugin]. Each [Plugin] should have a unique [PluginType]
   PluginType get pluginType;
 
+  /// The layoutType is used in the backend to determine the layout of the view.
+  /// Currrently, AppFlowy supports 4 layout types: Document, Grid, Board, Calendar.
   ViewLayoutPB? get layoutType => ViewLayoutPB.Document;
 }
 
@@ -60,9 +63,12 @@ abstract class PluginConfig {
   bool get creatable => true;
 }
 
-abstract class PluginDisplay with NavigationItem {
+abstract class PluginWidgetBuilder with NavigationItem {
   List<NavigationItem> get navigationItems;
 
+  EdgeInsets get contentPadding =>
+      const EdgeInsets.symmetric(horizontal: 40, vertical: 28);
+
   Widget buildWidget(PluginContext context);
 }
 
@@ -78,6 +84,8 @@ void registerPlugin({required PluginBuilder builder, PluginConfig? config}) {
       .registerPlugin(builder.pluginType, builder, config: config);
 }
 
+/// Make the correct plugin from the [pluginType] and [data]. If the plugin
+///  is not registered, it will return a blank plugin.
 Plugin makePlugin({required PluginType pluginType, dynamic data}) {
   final plugin = getIt<PluginSandbox>().buildPlugin(pluginType, data);
   return plugin;

+ 5 - 2
frontend/appflowy_flutter/lib/startup/plugin/src/sandbox.dart

@@ -1,5 +1,6 @@
 import 'dart:collection';
 
+import 'package:appflowy/plugins/blank/blank.dart';
 import 'package:flutter/services.dart';
 
 import '../plugin.dart';
@@ -28,9 +29,11 @@ class PluginSandbox {
     return index;
   }
 
+  /// Build a plugin from [data] with [pluginType]
+  /// If the [pluginType] is not registered, it will return a blank plugin
   Plugin buildPlugin(PluginType pluginType, dynamic data) {
-    final plugin = _pluginBuilders[pluginType]!.build(data);
-    return plugin;
+    final builder = _pluginBuilders[pluginType] ?? BlankPluginBuilder();
+    return builder.build(data);
   }
 
   void registerPlugin(

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

@@ -3,7 +3,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/app/app_service.dart';
+import 'package:appflowy/workspace/application/view/view_service.dart';
 import 'package:appflowy/workspace/presentation/home/menu/menu.dart';
 import 'package:expandable/expandable.dart';
 import 'package:appflowy_backend/log.dart';
@@ -17,11 +17,11 @@ import 'package:dartz/dartz.dart';
 part 'app_bloc.freezed.dart';
 
 class AppBloc extends Bloc<AppEvent, AppState> {
-  final AppBackendService appService;
+  final ViewBackendService appService;
   final AppListener appListener;
 
   AppBloc({required ViewPB view})
-      : appService = AppBackendService(),
+      : appService = ViewBackendService(),
         appListener = AppListener(viewId: view.id),
         super(AppState.initial(view)) {
     on<AppEvent>((event, emit) async {
@@ -77,8 +77,10 @@ class AppBloc extends Bloc<AppEvent, AppState> {
   }
 
   Future<void> _renameView(Rename e, Emitter<AppState> emit) async {
-    final result =
-        await appService.updateApp(appId: state.view.id, name: e.newName);
+    final result = await ViewBackendService.updateView(
+      viewId: state.view.id,
+      name: e.newName,
+    );
     result.fold(
       (l) => emit(state.copyWith(successOrFailure: left(unit))),
       (error) => emit(state.copyWith(successOrFailure: right(error))),
@@ -87,7 +89,7 @@ class AppBloc extends Bloc<AppEvent, AppState> {
 
 // Delete the current app
   Future<void> _deleteApp(Emitter<AppState> emit) async {
-    final result = await appService.delete(viewId: state.view.id);
+    final result = await ViewBackendService.delete(viewId: state.view.id);
     result.fold(
       (unit) => emit(state.copyWith(successOrFailure: left(unit))),
       (error) => emit(state.copyWith(successOrFailure: right(error))),
@@ -95,7 +97,7 @@ class AppBloc extends Bloc<AppEvent, AppState> {
   }
 
   Future<void> _deleteView(Emitter<AppState> emit, String viewId) async {
-    final result = await appService.deleteView(viewId: viewId);
+    final result = await ViewBackendService.deleteView(viewId: viewId);
     result.fold(
       (unit) => emit(state.copyWith(successOrFailure: left(unit))),
       (error) => emit(state.copyWith(successOrFailure: right(error))),
@@ -103,7 +105,7 @@ class AppBloc extends Bloc<AppEvent, AppState> {
   }
 
   Future<void> _createView(CreateView value, Emitter<AppState> emit) async {
-    final result = await appService.createView(
+    final result = await ViewBackendService.createView(
       parentViewId: state.view.id,
       name: value.name,
       desc: value.desc ?? "",
@@ -132,7 +134,8 @@ class AppBloc extends Bloc<AppEvent, AppState> {
   }
 
   Future<void> _loadViews(Emitter<AppState> emit) async {
-    final viewsOrFailed = await appService.getViews(viewId: state.view.id);
+    final viewsOrFailed =
+        await ViewBackendService.getViews(viewId: state.view.id);
     viewsOrFailed.fold(
       (views) => emit(state.copyWith(views: views)),
       (error) {

+ 0 - 143
frontend/appflowy_flutter/lib/workspace/application/app/app_service.dart

@@ -1,143 +0,0 @@
-import 'dart:async';
-
-import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart';
-import 'package:dartz/dartz.dart';
-import 'package:appflowy_backend/dispatch/dispatch.dart';
-import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
-import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
-
-class AppBackendService {
-  Future<Either<ViewPB, FlowyError>> createView({
-    required String parentViewId,
-    required String name,
-    String? desc,
-    required ViewLayoutPB layoutType,
-
-    /// The initial data should be a JSON that represent the DocumentDataPB.
-    /// Currently, only support create document with initial data.
-    List<int>? initialDataBytes,
-
-    /// The [ext] is used to pass through the custom configuration
-    /// to the backend.
-    /// Linking the view to the existing database, it needs to pass
-    /// the database id. For example: "database_id": "xxx"
-    ///
-    Map<String, String> ext = const {},
-  }) {
-    final payload = CreateViewPayloadPB.create()
-      ..parentViewId = parentViewId
-      ..name = name
-      ..desc = desc ?? ""
-      ..layout = layoutType
-      ..initialData = initialDataBytes ?? [];
-
-    if (ext.isNotEmpty) {
-      payload.ext.addAll(ext);
-    }
-
-    return FolderEventCreateView(payload).send();
-  }
-
-  Future<Either<List<ViewPB>, FlowyError>> getViews({required String viewId}) {
-    final payload = ViewIdPB.create()..value = viewId;
-
-    return FolderEventReadView(payload).send().then((result) {
-      return result.fold(
-        (app) => left(app.childViews),
-        (error) => right(error),
-      );
-    });
-  }
-
-  Future<Either<Unit, FlowyError>> delete({required String viewId}) {
-    final request = RepeatedViewIdPB.create()..items.add(viewId);
-    return FolderEventDeleteView(request).send();
-  }
-
-  Future<Either<Unit, FlowyError>> deleteView({required String viewId}) {
-    final request = RepeatedViewIdPB.create()..items.add(viewId);
-    return FolderEventDeleteView(request).send();
-  }
-
-  Future<Either<ViewPB, FlowyError>> updateApp({
-    required String appId,
-    String? name,
-  }) {
-    var payload = UpdateViewPayloadPB.create()..viewId = appId;
-
-    if (name != null) {
-      payload.name = name;
-    }
-    return FolderEventUpdateView(payload).send();
-  }
-
-  Future<Either<Unit, FlowyError>> moveView({
-    required String viewId,
-    required int fromIndex,
-    required int toIndex,
-  }) {
-    final payload = MoveFolderItemPayloadPB.create()
-      ..itemId = viewId
-      ..from = fromIndex
-      ..to = toIndex
-      ..ty = MoveFolderItemType.MoveView;
-
-    return FolderEventMoveItem(payload).send();
-  }
-
-  Future<List<(ViewPB, List<ViewPB>)>> fetchViews(
-    ViewLayoutPB layoutType,
-  ) async {
-    final result = <(ViewPB, List<ViewPB>)>[];
-    return FolderEventReadCurrentWorkspace().send().then((value) async {
-      final workspaces = value.getLeftOrNull<WorkspaceSettingPB>();
-      if (workspaces != null) {
-        final views = workspaces.workspace.views;
-        for (var view in views) {
-          final childViews = await getViews(viewId: view.id).then(
-            (value) => value
-                .getLeftOrNull<List<ViewPB>>()
-                ?.where((e) => e.layout == layoutType)
-                .toList(),
-          );
-          if (childViews != null && childViews.isNotEmpty) {
-            result.add((view, childViews));
-          }
-        }
-      }
-      return result;
-    });
-  }
-
-  Future<Either<ViewPB, FlowyError>> getView(
-    String viewID,
-  ) async {
-    final payload = ViewIdPB.create()..value = viewID;
-    return FolderEventReadView(payload).send();
-  }
-
-  Future<Either<ViewPB, FlowyError>> getChildView(
-    String viewID,
-    String childViewID,
-  ) async {
-    final payload = ViewIdPB.create()..value = viewID;
-    return FolderEventReadView(payload).send().then((result) {
-      return result.fold(
-        (app) => left(
-          app.childViews.firstWhere((e) => e.id == childViewID),
-        ),
-        (error) => right(error),
-      );
-    });
-  }
-}
-
-extension AppFlowy on Either {
-  T? getLeftOrNull<T>() {
-    if (isLeft()) {
-      final result = fold<T?>((l) => l, (r) => null);
-      return result;
-    }
-    return null;
-  }
-}

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

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

+ 3 - 5
frontend/appflowy_flutter/lib/workspace/application/menu/menu_view_section_bloc.dart

@@ -1,7 +1,7 @@
 import 'dart:async';
 
 import 'package:appflowy/workspace/application/app/app_bloc.dart';
-import 'package:appflowy/workspace/application/app/app_service.dart';
+import 'package:appflowy/workspace/application/view/view_service.dart';
 import 'package:appflowy_backend/log.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
@@ -13,12 +13,10 @@ class ViewSectionBloc extends Bloc<ViewSectionEvent, ViewSectionState> {
   void Function()? _viewsListener;
   void Function()? _selectedViewlistener;
   final AppViewDataContext _appViewData;
-  late final AppBackendService _appService;
 
   ViewSectionBloc({
     required AppViewDataContext appViewData,
-  })  : _appService = AppBackendService(),
-        _appViewData = appViewData,
+  })  : _appViewData = appViewData,
         super(ViewSectionState.initial(appViewData)) {
     on<ViewSectionEvent>((event, emit) async {
       await event.map(
@@ -69,7 +67,7 @@ class ViewSectionBloc extends Bloc<ViewSectionEvent, ViewSectionState> {
       views.insert(value.toIndex, views.removeAt(value.fromIndex));
       emit(state.copyWith(views: views));
 
-      final result = await _appService.moveView(
+      final result = await ViewBackendService.moveView(
         viewId: viewId,
         fromIndex: value.fromIndex,
         toIndex: value.toIndex,

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

@@ -9,13 +9,13 @@ import 'package:freezed_annotation/freezed_annotation.dart';
 part 'view_bloc.freezed.dart';
 
 class ViewBloc extends Bloc<ViewEvent, ViewState> {
-  final ViewService service;
+  final ViewBackendService viewBackendSvc;
   final ViewListener listener;
   final ViewPB view;
 
   ViewBloc({
     required this.view,
-  })  : service = ViewService(),
+  })  : viewBackendSvc = ViewBackendService(),
         listener = ViewListener(view: view),
         super(ViewState.init(view)) {
     on<ViewEvent>((event, emit) async {
@@ -42,7 +42,7 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
           );
         },
         rename: (e) async {
-          final result = await service.updateView(
+          final result = await ViewBackendService.updateView(
             viewId: view.id,
             name: e.newName,
           );
@@ -54,7 +54,7 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
           );
         },
         delete: (e) async {
-          final result = await service.delete(viewId: view.id);
+          final result = await ViewBackendService.delete(viewId: view.id);
           emit(
             result.fold(
               (l) => state.copyWith(successOrFailure: left(unit)),
@@ -63,7 +63,7 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
           );
         },
         duplicate: (e) async {
-          final result = await service.duplicate(view: view);
+          final result = await ViewBackendService.duplicate(view: view);
           emit(
             result.fold(
               (l) => state.copyWith(successOrFailure: left(unit)),

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

@@ -1,3 +1,4 @@
+import 'package:appflowy/plugins/database_view/database_view.dart';
 import 'package:appflowy/startup/plugin/plugin.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
@@ -56,7 +57,14 @@ extension ViewExtension on ViewPB {
   }
 
   Plugin plugin() {
-    final plugin = makePlugin(pluginType: pluginType, data: this);
-    return plugin;
+    switch (layout) {
+      case ViewLayoutPB.Board:
+      case ViewLayoutPB.Calendar:
+      case ViewLayoutPB.Grid:
+        return DatabaseViewPlugin(view: this);
+      case ViewLayoutPB.Document:
+        return makePlugin(pluginType: pluginType, data: this);
+    }
+    throw UnimplementedError;
   }
 }

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

@@ -1,34 +1,165 @@
 import 'dart:async';
+
+import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart';
 import 'package:dartz/dartz.dart';
 import 'package:appflowy_backend/dispatch/dispatch.dart';
-import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
 
-class ViewService {
-  Future<Either<ViewPB, FlowyError>> updateView({
-    required String viewId,
-    String? name,
+class ViewBackendService {
+  static Future<Either<ViewPB, FlowyError>> createView({
+    required ViewLayoutPB layoutType,
+    required String parentViewId,
+    required String name,
     String? desc,
+
+    /// The initial data should be a JSON that represent the DocumentDataPB.
+    /// Currently, only support create document with initial data.
+    List<int>? initialDataBytes,
+
+    /// The [ext] is used to pass through the custom configuration
+    /// to the backend.
+    /// Linking the view to the existing database, it needs to pass
+    /// the database id. For example: "database_id": "xxx"
+    ///
+    Map<String, String> ext = const {},
   }) {
-    final request = UpdateViewPayloadPB.create()..viewId = viewId;
+    final payload = CreateViewPayloadPB.create()
+      ..parentViewId = parentViewId
+      ..name = name
+      ..desc = desc ?? ""
+      ..layout = layoutType
+      ..initialData = initialDataBytes ?? [];
 
-    if (name != null) {
-      request.name = name;
+    if (ext.isNotEmpty) {
+      payload.ext.addAll(ext);
     }
 
-    if (desc != null) {
-      request.desc = desc;
-    }
+    return FolderEventCreateView(payload).send();
+  }
+
+  static Future<Either<ViewPB, FlowyError>> createDatabaseReferenceView({
+    required String parentViewId,
+    required String databaseId,
+    required ViewLayoutPB layoutType,
+    required String name,
+  }) {
+    return ViewBackendService.createView(
+      layoutType: layoutType,
+      parentViewId: parentViewId,
+      name: name,
+      ext: {
+        'database_id': databaseId,
+      },
+    );
+  }
+
+  static Future<Either<List<ViewPB>, FlowyError>> getViews({
+    required String viewId,
+  }) {
+    final payload = ViewIdPB.create()..value = viewId;
 
-    return FolderEventUpdateView(request).send();
+    return FolderEventReadView(payload).send().then((result) {
+      return result.fold(
+        (app) => left(app.childViews),
+        (error) => right(error),
+      );
+    });
   }
 
-  Future<Either<Unit, FlowyError>> delete({required String viewId}) {
+  static Future<Either<Unit, FlowyError>> delete({required String viewId}) {
     final request = RepeatedViewIdPB.create()..items.add(viewId);
     return FolderEventDeleteView(request).send();
   }
 
-  Future<Either<Unit, FlowyError>> duplicate({required ViewPB view}) {
+  static Future<Either<Unit, FlowyError>> deleteView({required String viewId}) {
+    final request = RepeatedViewIdPB.create()..items.add(viewId);
+    return FolderEventDeleteView(request).send();
+  }
+
+  static Future<Either<Unit, FlowyError>> duplicate({required ViewPB view}) {
     return FolderEventDuplicateView(view).send();
   }
+
+  static Future<Either<ViewPB, FlowyError>> updateView({
+    required String viewId,
+    String? name,
+  }) {
+    var payload = UpdateViewPayloadPB.create()..viewId = viewId;
+
+    if (name != null) {
+      payload.name = name;
+    }
+    return FolderEventUpdateView(payload).send();
+  }
+
+  static Future<Either<Unit, FlowyError>> moveView({
+    required String viewId,
+    required int fromIndex,
+    required int toIndex,
+  }) {
+    final payload = MoveFolderItemPayloadPB.create()
+      ..itemId = viewId
+      ..from = fromIndex
+      ..to = toIndex
+      ..ty = MoveFolderItemType.MoveView;
+
+    return FolderEventMoveItem(payload).send();
+  }
+
+  Future<List<(ViewPB, List<ViewPB>)>> fetchViews(
+    ViewLayoutPB layoutType,
+  ) async {
+    final result = <(ViewPB, List<ViewPB>)>[];
+    return FolderEventReadCurrentWorkspace().send().then((value) async {
+      final workspaces = value.getLeftOrNull<WorkspaceSettingPB>();
+      if (workspaces != null) {
+        final views = workspaces.workspace.views;
+        for (var view in views) {
+          final childViews = await getViews(viewId: view.id).then(
+            (value) => value
+                .getLeftOrNull<List<ViewPB>>()
+                ?.where((e) => e.layout == layoutType)
+                .toList(),
+          );
+          if (childViews != null && childViews.isNotEmpty) {
+            result.add((view, childViews));
+          }
+        }
+      }
+      return result;
+    });
+  }
+
+  Future<Either<ViewPB, FlowyError>> getView(
+    String viewID,
+  ) async {
+    final payload = ViewIdPB.create()..value = viewID;
+    return FolderEventReadView(payload).send();
+  }
+
+  Future<Either<ViewPB, FlowyError>> getChildView({
+    required String parentViewId,
+    required String childViewId,
+  }) async {
+    final payload = ViewIdPB.create()..value = parentViewId;
+    return FolderEventReadView(payload).send().then((result) {
+      return result.fold(
+        (app) => left(
+          app.childViews.firstWhere((e) => e.id == childViewId),
+        ),
+        (error) => right(error),
+      );
+    });
+  }
+}
+
+extension AppFlowy on Either {
+  T? getLeftOrNull<T>() {
+    if (isLeft()) {
+      final result = fold<T?>((l) => l, (r) => null);
+      return result;
+    }
+    return null;
+  }
 }

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

@@ -76,7 +76,7 @@ class _HomeScreenState extends State<HomeScreen> {
                   if (view != null) {
                     // Only open the last opened view if the [HomeStackManager] current opened plugin is blank and the last opened view is not null.
                     // All opened widgets that display on the home screen are in the form of plugins. There is a list of built-in plugins defined in the [PluginType] enum, including board, grid and trash.
-                    if (getIt<HomeStackManager>().plugin.ty ==
+                    if (getIt<HomeStackManager>().plugin.pluginType ==
                         PluginType.blank) {
                       final plugin = makePlugin(
                         pluginType: view.pluginType,

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

@@ -112,16 +112,14 @@ abstract mixin class NavigationItem {
 class HomeStackNotifier extends ChangeNotifier {
   Plugin _plugin;
 
-  Widget get titleWidget => _plugin.display.leftBarItem;
+  Widget get titleWidget => _plugin.widgetBuilder.leftBarItem;
 
   HomeStackNotifier({Plugin? plugin})
       : _plugin = plugin ?? makePlugin(pluginType: PluginType.blank);
 
+  /// This is the only place where the plugin is set.
+  /// No need compare the old plugin with the new plugin. Just set it.
   set plugin(Plugin newPlugin) {
-    if (newPlugin.id == _plugin.id) {
-      return;
-    }
-
     _plugin.notifier?.isDisplayChanged.addListener(notifyListeners);
     _plugin.dispose();
 
@@ -139,7 +137,7 @@ class HomeStackManager {
   HomeStackManager();
 
   Widget title() {
-    return _notifier.plugin.display.leftBarItem;
+    return _notifier.plugin.widgetBuilder.leftBarItem;
   }
 
   Plugin get plugin => _notifier.plugin;
@@ -172,20 +170,22 @@ class HomeStackManager {
       child: Consumer(
         builder: (_, HomeStackNotifier notifier, __) {
           return FadingIndexedStack(
-            index: getIt<PluginSandbox>().indexOf(notifier.plugin.ty),
+            index: getIt<PluginSandbox>().indexOf(notifier.plugin.pluginType),
             children: getIt<PluginSandbox>().supportPluginTypes.map(
               (pluginType) {
-                if (pluginType == notifier.plugin.ty) {
-                  final pluginWidget = notifier.plugin.display
-                      .buildWidget(PluginContext(onDeleted: onDeleted));
-                  if (pluginType == PluginType.editor) {
-                    return pluginWidget;
-                  }
-
-                  return pluginWidget.padding(horizontal: 40, vertical: 28);
+                if (pluginType == notifier.plugin.pluginType) {
+                  final builder = notifier.plugin.widgetBuilder;
+                  final pluginWidget = builder.buildWidget(
+                    PluginContext(onDeleted: onDeleted),
+                  );
+
+                  return Padding(
+                    padding: builder.contentPadding,
+                    child: pluginWidget,
+                  );
+                } else {
+                  return const BlankPage();
                 }
-
-                return const BlankPage();
               },
             ).toList(),
           );
@@ -219,7 +219,7 @@ class HomeTopBar extends StatelessWidget {
               value: Provider.of<HomeStackNotifier>(context, listen: false),
               child: Consumer(
                 builder: (_, HomeStackNotifier notifier, __) =>
-                    notifier.plugin.display.rightBarItem ??
+                    notifier.plugin.widgetBuilder.rightBarItem ??
                     const SizedBox.shrink(),
               ),
             ),

+ 3 - 3
frontend/appflowy_flutter/lib/workspace/presentation/home/navigation.dart

@@ -21,8 +21,8 @@ class NavigationNotifier with ChangeNotifier {
 
   void update(HomeStackNotifier notifier) {
     bool shouldNotify = false;
-    if (navigationItems != notifier.plugin.display.navigationItems) {
-      navigationItems = notifier.plugin.display.navigationItems;
+    if (navigationItems != notifier.plugin.widgetBuilder.navigationItems) {
+      navigationItems = notifier.plugin.widgetBuilder.navigationItems;
       shouldNotify = true;
     }
 
@@ -41,7 +41,7 @@ class FlowyNavigation extends StatelessWidget {
       create: (_) {
         final notifier = Provider.of<HomeStackNotifier>(context, listen: false);
         return NavigationNotifier(
-          navigationItems: notifier.plugin.display.navigationItems,
+          navigationItems: notifier.plugin.widgetBuilder.navigationItems,
         );
       },
       update: (_, notifier, controller) => controller!..update(notifier),

+ 1 - 3
frontend/appflowy_flutter/lib/workspace/presentation/widgets/left_bar_item.dart

@@ -17,7 +17,6 @@ class ViewLeftBarItem extends StatefulWidget {
 class _ViewLeftBarItemState extends State<ViewLeftBarItem> {
   final _controller = TextEditingController();
   final _focusNode = FocusNode();
-  late final ViewService _viewService;
   late final ViewListener _viewListener;
   late ViewPB view;
 
@@ -25,7 +24,6 @@ class _ViewLeftBarItemState extends State<ViewLeftBarItem> {
   void initState() {
     super.initState();
     view = widget.view;
-    _viewService = ViewService();
     _focusNode.addListener(_handleFocusChanged);
     _viewListener = ViewListener(view: widget.view);
     _viewListener.start(
@@ -86,7 +84,7 @@ class _ViewLeftBarItemState extends State<ViewLeftBarItem> {
     }
 
     if (_controller.text != view.name) {
-      _viewService.updateView(viewId: view.id, name: _controller.text);
+      ViewBackendService.updateView(viewId: view.id, name: _controller.text);
     }
   }
 }

+ 4 - 10
frontend/appflowy_flutter/test/bloc_test/board_test/util.dart

@@ -9,8 +9,7 @@ import 'package:appflowy/plugins/database_view/application/row/row_data_controll
 import 'package:appflowy/plugins/database_view/board/board.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/workspace/application/app/app_service.dart';
-import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
+import 'package:appflowy/workspace/application/view/view_service.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
 
@@ -30,21 +29,16 @@ class AppFlowyBoardTest {
   Future<BoardTestContext> createTestBoard() async {
     final app = await unitTest.createTestApp();
     final builder = BoardPluginBuilder();
-    return AppBackendService()
-        .createView(
+    return ViewBackendService.createView(
       parentViewId: app.id,
       name: "Test Board",
       layoutType: builder.layoutType!,
-    )
-        .then((result) {
+    ).then((result) {
       return result.fold(
         (view) async {
           final context = BoardTestContext(
             view,
-            DatabaseController(
-              view: view,
-              layoutType: DatabaseLayoutPB.Board,
-            ),
+            DatabaseController(view: view),
           );
           final result = await context._boardDataController.open();
           result.fold((l) => null, (r) => throw Exception(r));

+ 0 - 5
frontend/appflowy_flutter/test/bloc_test/grid_test/filter/create_filter_test.dart

@@ -2,7 +2,6 @@ import 'package:appflowy/plugins/database_view/application/filter/filter_service
 import 'package:appflowy/plugins/database_view/grid/application/grid_bloc.dart';
 import 'package:appflowy/plugins/database_view/application/database_controller.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_filter.pbenum.dart';
-import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/text_filter.pb.dart';
 import 'package:flutter_test/flutter_test.dart';
 
@@ -55,7 +54,6 @@ void main() {
     final service = FilterBackendService(viewId: context.gridView.id);
     final gridController = DatabaseController(
       view: context.gridView,
-      layoutType: DatabaseLayoutPB.Grid,
     );
     final gridBloc = GridBloc(
       view: context.gridView,
@@ -80,7 +78,6 @@ void main() {
     final service = FilterBackendService(viewId: context.gridView.id);
     final gridController = DatabaseController(
       view: context.gridView,
-      layoutType: DatabaseLayoutPB.Grid,
     );
     final gridBloc = GridBloc(
       view: context.gridView,
@@ -126,7 +123,6 @@ void main() {
     final service = FilterBackendService(viewId: context.gridView.id);
     final gridController = DatabaseController(
       view: context.gridView,
-      layoutType: DatabaseLayoutPB.Grid,
     );
     final gridBloc = GridBloc(
       view: context.gridView,
@@ -148,7 +144,6 @@ void main() {
     final service = FilterBackendService(viewId: context.gridView.id);
     final gridController = DatabaseController(
       view: context.gridView,
-      layoutType: DatabaseLayoutPB.Grid,
     );
     final gridBloc = GridBloc(
       view: context.gridView,

+ 4 - 10
frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_util.dart

@@ -1,28 +1,22 @@
 import 'package:appflowy/plugins/database_view/application/database_controller.dart';
 import 'package:appflowy/plugins/database_view/grid/grid.dart';
-import 'package:appflowy/workspace/application/app/app_service.dart';
-import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
+import 'package:appflowy/workspace/application/view/view_service.dart';
 
 import '../util.dart';
 
 Future<GridTestContext> createTestFilterGrid(AppFlowyGridTest gridTest) async {
   final app = await gridTest.unitTest.createTestApp();
   final builder = GridPluginBuilder();
-  final context = await AppBackendService()
-      .createView(
+  final context = await ViewBackendService.createView(
     parentViewId: app.id,
     name: "Filter Grid",
     layoutType: builder.layoutType!,
-  )
-      .then((result) {
+  ).then((result) {
     return result.fold(
       (view) async {
         final context = GridTestContext(
           view,
-          DatabaseController(
-            view: view,
-            layoutType: DatabaseLayoutPB.Grid,
-          ),
+          DatabaseController(view: view),
         );
         final result = await context.gridController.open();
 

+ 3 - 13
frontend/appflowy_flutter/test/bloc_test/grid_test/grid_bloc_test.dart

@@ -1,6 +1,5 @@
 import 'package:appflowy/plugins/database_view/grid/application/grid_bloc.dart';
 import 'package:appflowy/plugins/database_view/application/database_controller.dart';
-import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:bloc_test/bloc_test.dart';
 import 'util.dart';
@@ -23,10 +22,7 @@ void main() {
       "create a row",
       build: () => GridBloc(
         view: context.gridView,
-        databaseController: DatabaseController(
-          view: context.gridView,
-          layoutType: DatabaseLayoutPB.Grid,
-        ),
+        databaseController: DatabaseController(view: context.gridView),
       )..add(const GridEvent.initial()),
       act: (bloc) => bloc.add(const GridEvent.createRow()),
       wait: const Duration(milliseconds: 300),
@@ -39,10 +35,7 @@ void main() {
       "delete the last row",
       build: () => GridBloc(
         view: context.gridView,
-        databaseController: DatabaseController(
-          view: context.gridView,
-          layoutType: DatabaseLayoutPB.Grid,
-        ),
+        databaseController: DatabaseController(view: context.gridView),
       )..add(const GridEvent.initial()),
       act: (bloc) async {
         await gridResponseFuture();
@@ -65,10 +58,7 @@ void main() {
       'reorder rows',
       build: () => GridBloc(
         view: context.gridView,
-        databaseController: DatabaseController(
-          view: context.gridView,
-          layoutType: DatabaseLayoutPB.Grid,
-        ),
+        databaseController: DatabaseController(view: context.gridView),
       )..add(const GridEvent.initial()),
       act: (bloc) async {
         await gridResponseFuture();

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

@@ -9,9 +9,8 @@ import 'package:appflowy/plugins/database_view/application/row/row_data_controll
 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/app/app_service.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-database2/setting_entities.pbenum.dart';
 import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
@@ -170,21 +169,16 @@ class AppFlowyGridTest {
   Future<GridTestContext> createTestGrid() async {
     final app = await unitTest.createTestApp();
     final builder = GridPluginBuilder();
-    final context = await AppBackendService()
-        .createView(
+    final context = await ViewBackendService.createView(
       parentViewId: app.id,
       name: "Test Grid",
       layoutType: builder.layoutType!,
-    )
-        .then((result) {
+    ).then((result) {
       return result.fold(
         (view) async {
           final context = GridTestContext(
             view,
-            DatabaseController(
-              view: view,
-              layoutType: DatabaseLayoutPB.Grid,
-            ),
+            DatabaseController(view: view),
           );
           final result = await context.gridController.open();
           result.fold((l) => null, (r) => throw Exception(r));

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

@@ -99,7 +99,7 @@ checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
 [[package]]
 name = "appflowy-integrate"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bb6ab1#bb6ab1bada7b045e0edb2652017cd95795eb1309"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=12e373#12e373f972d893d5f34b18794e77d3b49783ddcf"
 dependencies = [
  "anyhow",
  "collab",
@@ -1024,7 +1024,7 @@ dependencies = [
 [[package]]
 name = "collab"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bb6ab1#bb6ab1bada7b045e0edb2652017cd95795eb1309"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=12e373#12e373f972d893d5f34b18794e77d3b49783ddcf"
 dependencies = [
  "anyhow",
  "bytes",
@@ -1042,7 +1042,7 @@ dependencies = [
 [[package]]
 name = "collab-client-ws"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bb6ab1#bb6ab1bada7b045e0edb2652017cd95795eb1309"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=12e373#12e373f972d893d5f34b18794e77d3b49783ddcf"
 dependencies = [
  "bytes",
  "collab-sync",
@@ -1060,7 +1060,7 @@ dependencies = [
 [[package]]
 name = "collab-database"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bb6ab1#bb6ab1bada7b045e0edb2652017cd95795eb1309"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=12e373#12e373f972d893d5f34b18794e77d3b49783ddcf"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -1086,7 +1086,7 @@ dependencies = [
 [[package]]
 name = "collab-derive"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bb6ab1#bb6ab1bada7b045e0edb2652017cd95795eb1309"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=12e373#12e373f972d893d5f34b18794e77d3b49783ddcf"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1098,7 +1098,7 @@ dependencies = [
 [[package]]
 name = "collab-document"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bb6ab1#bb6ab1bada7b045e0edb2652017cd95795eb1309"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=12e373#12e373f972d893d5f34b18794e77d3b49783ddcf"
 dependencies = [
  "anyhow",
  "collab",
@@ -1115,7 +1115,7 @@ dependencies = [
 [[package]]
 name = "collab-folder"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bb6ab1#bb6ab1bada7b045e0edb2652017cd95795eb1309"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=12e373#12e373f972d893d5f34b18794e77d3b49783ddcf"
 dependencies = [
  "anyhow",
  "collab",
@@ -1134,7 +1134,7 @@ dependencies = [
 [[package]]
 name = "collab-persistence"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bb6ab1#bb6ab1bada7b045e0edb2652017cd95795eb1309"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=12e373#12e373f972d893d5f34b18794e77d3b49783ddcf"
 dependencies = [
  "bincode",
  "chrono",
@@ -1154,7 +1154,7 @@ dependencies = [
 [[package]]
 name = "collab-plugins"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bb6ab1#bb6ab1bada7b045e0edb2652017cd95795eb1309"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=12e373#12e373f972d893d5f34b18794e77d3b49783ddcf"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -1184,7 +1184,7 @@ dependencies = [
 [[package]]
 name = "collab-sync"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bb6ab1#bb6ab1bada7b045e0edb2652017cd95795eb1309"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=12e373#12e373f972d893d5f34b18794e77d3b49783ddcf"
 dependencies = [
  "bytes",
  "collab",

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

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

+ 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=26438c#26438c77de5d4cad723370380da19763da536ac1"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=12e373#12e373f972d893d5f34b18794e77d3b49783ddcf"
 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=26438c#26438c77de5d4cad723370380da19763da536ac1"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=12e373#12e373f972d893d5f34b18794e77d3b49783ddcf"
 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=26438c#26438c77de5d4cad723370380da19763da536ac1"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=12e373#12e373f972d893d5f34b18794e77d3b49783ddcf"
 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=26438c#26438c77de5d4cad723370380da19763da536ac1"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=12e373#12e373f972d893d5f34b18794e77d3b49783ddcf"
 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=26438c#26438c77de5d4cad723370380da19763da536ac1"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=12e373#12e373f972d893d5f34b18794e77d3b49783ddcf"
 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=26438c#26438c77de5d4cad723370380da19763da536ac1"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=12e373#12e373f972d893d5f34b18794e77d3b49783ddcf"
 dependencies = [
  "anyhow",
  "collab",
@@ -978,7 +978,7 @@ dependencies = [
 [[package]]
 name = "collab-folder"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=26438c#26438c77de5d4cad723370380da19763da536ac1"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=12e373#12e373f972d893d5f34b18794e77d3b49783ddcf"
 dependencies = [
  "anyhow",
  "collab",
@@ -997,7 +997,7 @@ dependencies = [
 [[package]]
 name = "collab-persistence"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=26438c#26438c77de5d4cad723370380da19763da536ac1"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=12e373#12e373f972d893d5f34b18794e77d3b49783ddcf"
 dependencies = [
  "bincode",
  "chrono",
@@ -1017,7 +1017,7 @@ dependencies = [
 [[package]]
 name = "collab-plugins"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=26438c#26438c77de5d4cad723370380da19763da536ac1"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=12e373#12e373f972d893d5f34b18794e77d3b49783ddcf"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -1047,7 +1047,7 @@ dependencies = [
 [[package]]
 name = "collab-sync"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=26438c#26438c77de5d4cad723370380da19763da536ac1"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=12e373#12e373f972d893d5f34b18794e77d3b49783ddcf"
 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 = "26438c"  }
-collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "26438c"  }
-collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "26438c" }
-collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "26438c" }
-appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "26438c" }
+collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "12e373"  }
+collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "12e373"  }
+collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "12e373" }
+collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "12e373" }
+appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "12e373" }
 
 #collab = { path = "../AppFlowy-Collab/collab" }
 #collab-folder = { path = "../AppFlowy-Collab/collab-folder" }

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

@@ -18,7 +18,7 @@ use flowy_error::FlowyError;
 use flowy_folder2::deps::{FolderCloudService, FolderUser};
 use flowy_folder2::entities::ViewLayoutPB;
 use flowy_folder2::manager::Folder2Manager;
-use flowy_folder2::view_ext::{FolderOperationHandler, FolderOperationHandlers};
+use flowy_folder2::view_operation::{FolderOperationHandler, FolderOperationHandlers, View};
 use flowy_folder2::ViewLayout;
 use flowy_user::services::UserSession;
 use lib_dispatch::prelude::ToBytes;
@@ -111,7 +111,7 @@ impl FolderOperationHandler for DocumentFolderOperation {
     _name: &str,
     data: Vec<u8>,
     layout: ViewLayout,
-    _ext: HashMap<String, String>,
+    _meta: HashMap<String, String>,
   ) -> FutureResult<(), FlowyError> {
     debug_assert_eq!(layout, ViewLayout::Document);
     let view_id = view_id.to_string();
@@ -130,7 +130,6 @@ impl FolderOperationHandler for DocumentFolderOperation {
     view_id: &str,
     _name: &str,
     layout: ViewLayout,
-    _ext: HashMap<String, String>,
   ) -> FutureResult<(), FlowyError> {
     debug_assert_eq!(layout, ViewLayout::Document);
 
@@ -200,9 +199,9 @@ impl FolderOperationHandler for DatabaseFolderOperation {
     name: &str,
     data: Vec<u8>,
     layout: ViewLayout,
-    ext: HashMap<String, String>,
+    meta: HashMap<String, String>,
   ) -> FutureResult<(), FlowyError> {
-    match CreateDatabaseExtParams::from_map(ext) {
+    match CreateDatabaseExtParams::from_map(meta) {
       None => {
         let database_manager = self.0.clone();
         let view_id = view_id.to_string();
@@ -217,17 +216,11 @@ impl FolderOperationHandler for DatabaseFolderOperation {
         let database_manager = self.0.clone();
         let layout = layout_type_from_view_layout(layout.into());
         let name = name.to_string();
-        let target_view_id = view_id.to_string();
+        let database_view_id = view_id.to_string();
 
         FutureResult::new(async move {
           database_manager
-            .create_linked_view(
-              name,
-              layout,
-              params.database_id,
-              target_view_id,
-              params.duplicated_view_id,
-            )
+            .create_linked_view(name, layout, params.database_id, database_view_id)
             .await?;
           Ok(())
         })
@@ -245,7 +238,6 @@ impl FolderOperationHandler for DatabaseFolderOperation {
     view_id: &str,
     name: &str,
     layout: ViewLayout,
-    _meta: HashMap<String, String>,
   ) -> FutureResult<(), FlowyError> {
     let name = name.to_string();
     let database_manager = self.0.clone();
@@ -296,12 +288,37 @@ impl FolderOperationHandler for DatabaseFolderOperation {
       Ok(())
     })
   }
+
+  fn did_update_view(&self, old: &View, new: &View) -> FutureResult<(), FlowyError> {
+    let database_layout = match new.layout {
+      ViewLayout::Document => {
+        return FutureResult::new(async {
+          Err(FlowyError::internal().context("Can't handle document layout type"))
+        });
+      },
+      ViewLayout::Grid => DatabaseLayoutPB::Grid,
+      ViewLayout::Board => DatabaseLayoutPB::Board,
+      ViewLayout::Calendar => DatabaseLayoutPB::Calendar,
+    };
+
+    let database_manager = self.0.clone();
+    let view_id = new.id.clone();
+    if old.layout != new.layout {
+      FutureResult::new(async move {
+        database_manager
+          .update_database_layout(&view_id, database_layout)
+          .await?;
+        Ok(())
+      })
+    } else {
+      FutureResult::new(async move { Ok(()) })
+    }
+  }
 }
 
 #[derive(Debug, serde::Deserialize)]
 struct CreateDatabaseExtParams {
   database_id: String,
-  duplicated_view_id: Option<String>,
 }
 
 impl CreateDatabaseExtParams {

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

@@ -134,3 +134,18 @@ pub struct MoveCalendarEventPB {
   #[pb(index = 2)]
   pub timestamp: i64,
 }
+
+#[derive(Debug, Clone, Default, ProtoBuf)]
+pub struct NoDateCalendarEventPB {
+  #[pb(index = 1)]
+  pub row_id: String,
+
+  #[pb(index = 2)]
+  pub title: String,
+}
+
+#[derive(Debug, Clone, Default, ProtoBuf)]
+pub struct RepeatedNoDateCalendarEventPB {
+  #[pb(index = 1)]
+  pub items: Vec<NoDateCalendarEventPB>,
+}

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

@@ -3,10 +3,11 @@ use collab_database::user::DatabaseRecord;
 use collab_database::views::DatabaseLayout;
 
 use flowy_derive::ProtoBuf;
-use flowy_error::ErrorCode;
+use flowy_error::{ErrorCode, FlowyError};
 
 use crate::entities::parser::NotEmptyStr;
 use crate::entities::{DatabaseLayoutPB, FieldIdPB, RowPB};
+use crate::services::database::CreateDatabaseViewParams;
 
 /// [DatabasePB] describes how many fields and blocks the grid has
 #[derive(Debug, Clone, Default, ProtoBuf)]
@@ -19,12 +20,34 @@ pub struct DatabasePB {
 
   #[pb(index = 3)]
   pub rows: Vec<RowPB>,
+
+  #[pb(index = 4)]
+  pub layout_type: DatabaseLayoutPB,
 }
 
 #[derive(ProtoBuf, Default)]
-pub struct CreateDatabasePayloadPB {
+pub struct CreateDatabaseViewPayloadPB {
   #[pb(index = 1)]
   pub name: String,
+
+  #[pb(index = 2)]
+  pub view_id: String,
+
+  #[pb(index = 3)]
+  pub layout_type: DatabaseLayoutPB,
+}
+
+impl TryInto<CreateDatabaseViewParams> for CreateDatabaseViewPayloadPB {
+  type Error = FlowyError;
+
+  fn try_into(self) -> Result<CreateDatabaseViewParams, Self::Error> {
+    let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)?;
+    Ok(CreateDatabaseViewParams {
+      name: self.name,
+      view_id: view_id.0,
+      layout_type: self.layout_type.into(),
+    })
+  }
 }
 
 #[derive(Clone, ProtoBuf, Default, Debug)]
@@ -198,7 +221,7 @@ impl TryInto<DatabaseGroupIdParams> for DatabaseGroupIdPB {
   }
 }
 #[derive(Clone, ProtoBuf, Default, Debug)]
-pub struct DatabaseLayoutIdPB {
+pub struct DatabaseLayoutMetaPB {
   #[pb(index = 1)]
   pub view_id: String,
 
@@ -207,18 +230,18 @@ pub struct DatabaseLayoutIdPB {
 }
 
 #[derive(Clone, Debug)]
-pub struct DatabaseLayoutId {
+pub struct DatabaseLayoutMeta {
   pub view_id: String,
   pub layout: DatabaseLayout,
 }
 
-impl TryInto<DatabaseLayoutId> for DatabaseLayoutIdPB {
+impl TryInto<DatabaseLayoutMeta> for DatabaseLayoutMetaPB {
   type Error = ErrorCode;
 
-  fn try_into(self) -> Result<DatabaseLayoutId, Self::Error> {
+  fn try_into(self) -> Result<DatabaseLayoutMeta, Self::Error> {
     let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)?;
     let layout = self.layout.into();
-    Ok(DatabaseLayoutId {
+    Ok(DatabaseLayoutMeta {
       view_id: view_id.0,
       layout,
     })

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

@@ -121,7 +121,7 @@ pub struct UpdatedRowPB {
   #[pb(index = 1)]
   pub row: RowPB,
 
-  // represents as the cells that were updated in this row.
+  // Indicates the field ids of the cells that were updated in this row.
   #[pb(index = 2)]
   pub field_ids: Vec<String>,
 }

+ 20 - 10
frontend/rust-lib/flowy-database2/src/entities/setting_entities.rs

@@ -18,10 +18,10 @@ use crate::services::setting::CalendarLayoutSetting;
 #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
 pub struct DatabaseViewSettingPB {
   #[pb(index = 1)]
-  pub current_layout: DatabaseLayoutPB,
+  pub layout_type: DatabaseLayoutPB,
 
   #[pb(index = 2)]
-  pub layout_setting: LayoutSettingPB,
+  pub layout_setting: DatabaseLayoutSettingPB,
 
   #[pb(index = 3)]
   pub filters: RepeatedFilterPB,
@@ -72,8 +72,8 @@ pub struct DatabaseSettingChangesetPB {
   #[pb(index = 1)]
   pub view_id: String,
 
-  #[pb(index = 2)]
-  pub layout_type: DatabaseLayoutPB,
+  #[pb(index = 2, one_of)]
+  pub layout_type: Option<DatabaseLayoutPB>,
 
   #[pb(index = 3, one_of)]
   pub update_filter: Option<UpdateFilterPayloadPB>,
@@ -121,7 +121,7 @@ impl TryInto<DatabaseSettingChangesetParams> for DatabaseSettingChangesetPB {
 
     Ok(DatabaseSettingChangesetParams {
       view_id,
-      layout_type: self.layout_type.into(),
+      layout_type: self.layout_type.map(|ty| ty.into()),
       insert_filter,
       delete_filter,
       alert_sort,
@@ -132,7 +132,7 @@ impl TryInto<DatabaseSettingChangesetParams> for DatabaseSettingChangesetPB {
 
 pub struct DatabaseSettingChangesetParams {
   pub view_id: String,
-  pub layout_type: DatabaseLayout,
+  pub layout_type: Option<DatabaseLayout>,
   pub insert_filter: Option<UpdateFilterParams>,
   pub delete_filter: Option<DeleteFilterParams>,
   pub alert_sort: Option<UpdateSortParams>,
@@ -146,19 +146,24 @@ impl DatabaseSettingChangesetParams {
 }
 
 #[derive(Debug, Eq, PartialEq, Default, ProtoBuf, Clone)]
-pub struct LayoutSettingPB {
-  #[pb(index = 1, one_of)]
+pub struct DatabaseLayoutSettingPB {
+  #[pb(index = 1)]
+  pub layout_type: DatabaseLayoutPB,
+
+  #[pb(index = 2, one_of)]
   pub calendar: Option<CalendarLayoutSettingPB>,
 }
 
 #[derive(Debug, Clone, Default)]
 pub struct LayoutSettingParams {
+  pub layout_type: DatabaseLayout,
   pub calendar: Option<CalendarLayoutSetting>,
 }
 
-impl From<LayoutSettingParams> for LayoutSettingPB {
+impl From<LayoutSettingParams> for DatabaseLayoutSettingPB {
   fn from(data: LayoutSettingParams) -> Self {
     Self {
+      layout_type: data.layout_type.into(),
       calendar: data.calendar.map(|calendar| calendar.into()),
     }
   }
@@ -169,13 +174,17 @@ pub struct LayoutSettingChangesetPB {
   #[pb(index = 1)]
   pub view_id: String,
 
-  #[pb(index = 2, one_of)]
+  #[pb(index = 2)]
+  pub layout_type: DatabaseLayoutPB,
+
+  #[pb(index = 3, one_of)]
   pub calendar: Option<CalendarLayoutSettingPB>,
 }
 
 #[derive(Debug)]
 pub struct LayoutSettingChangeset {
   pub view_id: String,
+  pub layout_type: DatabaseLayout,
   pub calendar: Option<CalendarLayoutSetting>,
 }
 
@@ -189,6 +198,7 @@ impl TryInto<LayoutSettingChangeset> for LayoutSettingChangesetPB {
 
     Ok(LayoutSettingChangeset {
       view_id,
+      layout_type: self.layout_type.into(),
       calendar: self.calendar.map(|calendar| calendar.into()),
     })
   }

+ 35 - 7
frontend/rust-lib/flowy-database2/src/event_handler.rs

@@ -2,7 +2,6 @@ use collab_database::database::gen_row_id;
 use std::sync::Arc;
 
 use collab_database::rows::RowId;
-use collab_database::views::DatabaseLayout;
 use lib_infra::util::timestamp;
 
 use flowy_error::{FlowyError, FlowyResult};
@@ -25,7 +24,7 @@ pub(crate) async fn get_database_data_handler(
 ) -> DataResult<DatabasePB, FlowyError> {
   let view_id: DatabaseViewIdPB = data.into_inner();
   let database_editor = manager.get_database_with_view_id(view_id.as_ref()).await?;
-  let data = database_editor.get_database_data(view_id.as_ref()).await;
+  let data = database_editor.get_database_data(view_id.as_ref()).await?;
   data_result_ok(data)
 }
 
@@ -64,6 +63,12 @@ pub(crate) async fn update_database_setting_handler(
   if let Some(delete_sort) = params.delete_sort {
     editor.delete_sort(delete_sort).await?;
   }
+
+  if let Some(layout_type) = params.layout_type {
+    editor
+      .update_view_layout(&params.view_id, layout_type)
+      .await?;
+  }
   Ok(())
 }
 
@@ -626,24 +631,25 @@ pub(crate) async fn set_layout_setting_handler(
   let params: LayoutSettingChangeset = data.into_inner().try_into()?;
   let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
   let layout_params = LayoutSettingParams {
+    layout_type: params.layout_type,
     calendar: params.calendar,
   };
   database_editor
-    .set_layout_setting(&params.view_id, DatabaseLayout::Calendar, layout_params)
+    .set_layout_setting(&params.view_id, layout_params)
     .await;
   Ok(())
 }
 
 pub(crate) async fn get_layout_setting_handler(
-  data: AFPluginData<DatabaseLayoutIdPB>,
+  data: AFPluginData<DatabaseLayoutMetaPB>,
   manager: AFPluginState<Arc<DatabaseManager2>>,
-) -> DataResult<LayoutSettingPB, FlowyError> {
-  let params: DatabaseLayoutId = data.into_inner().try_into()?;
+) -> DataResult<DatabaseLayoutSettingPB, FlowyError> {
+  let params: DatabaseLayoutMeta = data.into_inner().try_into()?;
   let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
   let layout_setting_pb = database_editor
     .get_layout_setting(&params.view_id, params.layout)
     .await
-    .map(LayoutSettingPB::from)
+    .map(DatabaseLayoutSettingPB::from)
     .unwrap_or_default();
   data_result_ok(layout_setting_pb)
 }
@@ -661,6 +667,19 @@ pub(crate) async fn get_calendar_events_handler(
   data_result_ok(RepeatedCalendarEventPB { items: events })
 }
 
+#[tracing::instrument(level = "debug", skip(data, manager), err)]
+pub(crate) async fn get_no_date_calendar_events_handler(
+  data: AFPluginData<CalendarEventRequestPB>,
+  manager: AFPluginState<Arc<DatabaseManager2>>,
+) -> DataResult<RepeatedNoDateCalendarEventPB, FlowyError> {
+  let params: CalendarEventRequestParams = data.into_inner().try_into()?;
+  let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
+  let _events = database_editor
+    .get_all_no_date_calendar_events(&params.view_id)
+    .await;
+  todo!()
+}
+
 #[tracing::instrument(level = "debug", skip(data, manager), err)]
 pub(crate) async fn get_calendar_event_handler(
   data: AFPluginData<RowIdPB>,
@@ -699,3 +718,12 @@ pub(crate) async fn move_calendar_event_handler(
     .await?;
   Ok(())
 }
+
+#[tracing::instrument(level = "debug", skip_all, err)]
+pub(crate) async fn create_database_view(
+  _data: AFPluginData<CreateDatabaseViewPayloadPB>,
+  _manager: AFPluginState<Arc<DatabaseManager2>>,
+) -> FlowyResult<()> {
+  // let data: CreateDatabaseViewParams = data.into_inner().try_into()?;
+  Ok(())
+}

+ 14 - 4
frontend/rust-lib/flowy-database2/src/event_map.rs

@@ -60,12 +60,13 @@ pub fn init(database_manager: Arc<DatabaseManager2>) -> AFPlugin {
         .event(DatabaseEvent::GetDatabases, get_databases_handler)
         // Calendar
         .event(DatabaseEvent::GetAllCalendarEvents, get_calendar_events_handler)
+        .event(DatabaseEvent::GetNoDateCalendarEvents, get_no_date_calendar_events_handler)
         .event(DatabaseEvent::GetCalendarEvent, get_calendar_event_handler)
         .event(DatabaseEvent::MoveCalendarEvent, move_calendar_event_handler)
         // Layout setting
         .event(DatabaseEvent::SetLayoutSetting, set_layout_setting_handler)
         .event(DatabaseEvent::GetLayoutSetting, get_layout_setting_handler)
-  // import
+        .event(DatabaseEvent::CreateDatabaseView, create_database_view)
 }
 
 /// [DatabaseEvent] defines events that are used to interact with the Grid. You could check [this](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/backend/protobuf)
@@ -265,15 +266,24 @@ pub enum DatabaseEvent {
   #[event(input = "LayoutSettingChangesetPB")]
   SetLayoutSetting = 121,
 
-  #[event(input = "DatabaseLayoutIdPB", output = "LayoutSettingPB")]
+  #[event(input = "DatabaseLayoutMetaPB", output = "DatabaseLayoutSettingPB")]
   GetLayoutSetting = 122,
 
   #[event(input = "CalendarEventRequestPB", output = "RepeatedCalendarEventPB")]
   GetAllCalendarEvents = 123,
 
+  #[event(
+    input = "CalendarEventRequestPB",
+    output = "RepeatedNoDateCalendarEventPB"
+  )]
+  GetNoDateCalendarEvents = 124,
+
   #[event(input = "RowIdPB", output = "CalendarEventPB")]
-  GetCalendarEvent = 124,
+  GetCalendarEvent = 125,
 
   #[event(input = "MoveCalendarEventPB")]
-  MoveCalendarEvent = 125,
+  MoveCalendarEvent = 126,
+
+  #[event(input = "CreateDatabaseViewPayloadPB")]
+  CreateDatabaseView = 130,
 }

+ 12 - 11
frontend/rust-lib/flowy-database2/src/manager.rs

@@ -171,8 +171,7 @@ impl DatabaseManager2 {
     name: String,
     layout: DatabaseLayoutPB,
     database_id: String,
-    target_view_id: String,
-    duplicated_view_id: Option<String>,
+    database_view_id: String,
   ) -> FlowyResult<()> {
     self.with_user_database(
       Err(FlowyError::internal().context("Create database view failed")),
@@ -180,15 +179,8 @@ impl DatabaseManager2 {
         let database = user_database
           .get_database(&database_id)
           .ok_or_else(FlowyError::record_not_found)?;
-        match duplicated_view_id {
-          None => {
-            let params = CreateViewParams::new(database_id, target_view_id, name, layout.into());
-            database.create_linked_view(params)?;
-          },
-          Some(duplicated_view_id) => {
-            database.duplicate_linked_view(&duplicated_view_id);
-          },
-        }
+        let params = CreateViewParams::new(database_id, database_view_id, name, layout.into());
+        database.create_linked_view(params)?;
         Ok(())
       },
     )?;
@@ -228,6 +220,15 @@ impl DatabaseManager2 {
     database.export_csv(style).await
   }
 
+  pub async fn update_database_layout(
+    &self,
+    view_id: &str,
+    layout: DatabaseLayoutPB,
+  ) -> FlowyResult<()> {
+    let database = self.get_database_with_view_id(view_id).await?;
+    database.update_view_layout(view_id, layout.into()).await
+  }
+
   fn with_user_database<F, Output>(&self, default_value: Output, f: F) -> Output
   where
     F: FnOnce(&InnerUserDatabase) -> Output,

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

@@ -35,6 +35,8 @@ pub enum DatabaseNotification {
   DidUpdateLayoutSettings = 80,
   // Trigger when the layout field of the database is changed
   DidSetNewLayoutField = 81,
+  // Trigger when the layout of the database is changed
+  DidUpdateDatabaseLayout = 82,
 }
 
 impl std::default::Default for DatabaseNotification {

+ 88 - 39
frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs

@@ -18,8 +18,9 @@ use crate::entities::{
   CalendarEventPB, CellChangesetNotifyPB, CellPB, ChecklistCellDataPB, DatabaseFieldChangesetPB,
   DatabasePB, DatabaseViewSettingPB, DeleteFilterParams, DeleteGroupParams, DeleteSortParams,
   FieldChangesetParams, FieldIdPB, FieldPB, FieldType, GroupPB, IndexFieldPB, InsertedRowPB,
-  LayoutSettingParams, RepeatedFilterPB, RepeatedGroupPB, RepeatedSortPB, RowPB, RowsChangePB,
-  SelectOptionCellDataPB, SelectOptionPB, UpdateFilterParams, UpdateSortParams,
+  LayoutSettingParams, NoDateCalendarEventPB, RepeatedFilterPB, RepeatedGroupPB, RepeatedSortPB,
+  RowPB, RowsChangePB, SelectOptionCellDataPB, SelectOptionPB, UpdateFilterParams,
+  UpdateSortParams, UpdatedRowPB,
 };
 use crate::notification::{send_notification, DatabaseNotification};
 use crate::services::cell::{
@@ -38,6 +39,7 @@ use crate::services::filter::Filter;
 use crate::services::group::{
   default_group_setting, GroupSetting, GroupSettingChangeset, RowChangeset,
 };
+
 use crate::services::share::csv::{CSVExport, CSVFormat};
 use crate::services::sort::Sort;
 
@@ -76,6 +78,17 @@ impl DatabaseEditor {
 
   pub async fn close(&self) {}
 
+  pub async fn update_view_layout(
+    &self,
+    view_id: &str,
+    layout_type: DatabaseLayout,
+  ) -> FlowyResult<()> {
+    let view_editor = self.database_views.get_view_editor(view_id).await?;
+    view_editor.v_update_layout_type(layout_type).await?;
+
+    Ok(())
+  }
+
   pub async fn subscribe_view_changed(
     &self,
     view_id: &str,
@@ -442,7 +455,7 @@ impl DatabaseEditor {
     let (field, cell) = {
       let database = self.database.lock();
       let field = database.fields.get_field(field_id);
-      let cell = database.get_cell(field_id, &row_id);
+      let cell = database.get_cell(field_id, &row_id).cell;
       (field, cell)
     };
 
@@ -484,12 +497,7 @@ impl DatabaseEditor {
           Err(FlowyError::internal().context(msg))
         },
       }?;
-      (
-        field,
-        database
-          .get_cell(field_id, &row_id)
-          .map(|row_cell| row_cell.cell),
-      )
+      (field, database.get_cell(field_id, &row_id).cell)
     };
     let new_cell =
       apply_cell_changeset(cell_changeset, cell, &field, Some(self.cell_cache.clone()))?;
@@ -529,6 +537,15 @@ impl DatabaseEditor {
 
     let option_row = self.database.lock().get_row(&row_id);
     if let Some(new_row) = option_row {
+      let updated_row = UpdatedRowPB {
+        row: RowPB::from(&new_row),
+        field_ids: vec![field_id.to_string()],
+      };
+      let changes = RowsChangePB::from_update(view_id.to_string(), updated_row);
+      send_notification(view_id, DatabaseNotification::DidUpdateViewRows)
+        .payload(changes)
+        .send();
+
       for view in self.database_views.editors().await {
         view.v_did_update_row(&old_row, &new_row, field_id).await;
       }
@@ -646,10 +663,10 @@ impl DatabaseEditor {
     match field {
       None => SelectOptionCellDataPB::default(),
       Some(field) => {
-        let row_cell = self.database.lock().get_cell(field_id, &row_id);
-        let ids = match row_cell {
+        let cell = self.database.lock().get_cell(field_id, &row_id).cell;
+        let ids = match cell {
           None => SelectOptionIds::new(),
-          Some(row_cell) => SelectOptionIds::from(&row_cell.cell),
+          Some(cell) => SelectOptionIds::from(&cell),
         };
         match select_type_option_from_field(&field) {
           Ok(type_option) => type_option.get_selected_options(ids).into(),
@@ -661,9 +678,9 @@ impl DatabaseEditor {
 
   pub async fn get_checklist_option(&self, row_id: RowId, field_id: &str) -> ChecklistCellDataPB {
     let row_cell = self.database.lock().get_cell(field_id, &row_id);
-    let cell_data = match row_cell {
+    let cell_data = match row_cell.cell {
       None => ChecklistCellData::default(),
-      Some(row_cell) => ChecklistCellData::from(&row_cell.cell),
+      Some(cell) => ChecklistCellData::from(&cell),
     };
     ChecklistCellDataPB::from(cell_data)
   }
@@ -763,14 +780,9 @@ impl DatabaseEditor {
     Ok(())
   }
 
-  pub async fn set_layout_setting(
-    &self,
-    view_id: &str,
-    layout_ty: DatabaseLayout,
-    layout_setting: LayoutSettingParams,
-  ) {
+  pub async fn set_layout_setting(&self, view_id: &str, layout_setting: LayoutSettingParams) {
     if let Ok(view) = self.database_views.get_view_editor(view_id).await {
-      let _ = view.v_set_layout_settings(&layout_ty, layout_setting).await;
+      let _ = view.v_set_layout_settings(layout_setting).await;
     }
   }
 
@@ -795,6 +807,15 @@ impl DatabaseEditor {
     }
   }
 
+  #[tracing::instrument(level = "trace", skip_all)]
+  pub async fn get_all_no_date_calendar_events(
+    &self,
+    view_id: &str,
+  ) -> FlowyResult<Vec<NoDateCalendarEventPB>> {
+    let _database_view = self.database_views.get_view_editor(view_id).await?;
+    todo!()
+  }
+
   #[tracing::instrument(level = "trace", skip_all)]
   pub async fn get_calendar_event(&self, view_id: &str, row_id: RowId) -> Option<CalendarEventPB> {
     let view = self.database_views.get_view_editor(view_id).await.ok()?;
@@ -858,8 +879,13 @@ impl DatabaseEditor {
     Ok(database_view_setting_pb_from_view(view))
   }
 
-  pub async fn get_database_data(&self, view_id: &str) -> DatabasePB {
-    let rows = self.get_rows(view_id).await.unwrap_or_default();
+  pub async fn get_database_data(&self, view_id: &str) -> FlowyResult<DatabasePB> {
+    let database_view = self.database_views.get_view_editor(view_id).await?;
+    let view = database_view
+      .get_view()
+      .await
+      .ok_or(FlowyError::record_not_found())?;
+    let rows = database_view.v_get_rows().await;
     let (database_id, fields) = {
       let database = self.database.lock();
       let database_id = database.get_database_id();
@@ -876,11 +902,12 @@ impl DatabaseEditor {
       .into_iter()
       .map(|row| RowPB::from(row.as_ref()))
       .collect::<Vec<RowPB>>();
-    DatabasePB {
+    Ok(DatabasePB {
       id: database_id,
       fields,
       rows,
-    }
+      layout_type: view.layout.into(),
+    })
   }
 
   pub async fn export_csv(&self, style: CSVFormat) -> FlowyResult<String> {
@@ -946,7 +973,7 @@ struct DatabaseViewDataImpl {
 }
 
 impl DatabaseViewData for DatabaseViewDataImpl {
-  fn get_view_setting(&self, view_id: &str) -> Fut<Option<DatabaseView>> {
+  fn get_view(&self, view_id: &str) -> Fut<Option<DatabaseView>> {
     let view = self.database.lock().get_view(view_id);
     to_fut(async move { view })
   }
@@ -966,6 +993,26 @@ impl DatabaseViewData for DatabaseViewDataImpl {
     to_fut(async move { field })
   }
 
+  fn create_field(
+    &self,
+    view_id: &str,
+    name: &str,
+    field_type: FieldType,
+    type_option_data: TypeOptionData,
+  ) -> Fut<Field> {
+    let (_, field) = self.database.lock().create_default_field(
+      view_id,
+      name.to_string(),
+      field_type.clone().into(),
+      |field| {
+        field
+          .type_options
+          .insert(field_type.to_string(), type_option_data);
+      },
+    );
+    to_fut(async move { field })
+  }
+
   fn get_primary_field(&self) -> Fut<Option<Arc<Field>>> {
     let field = self
       .database
@@ -1002,18 +1049,13 @@ impl DatabaseViewData for DatabaseViewDataImpl {
     to_fut(async move { cells.into_iter().map(Arc::new).collect() })
   }
 
-  fn get_cell_in_row(&self, field_id: &str, row_id: &RowId) -> Fut<Option<Arc<RowCell>>> {
+  fn get_cell_in_row(&self, field_id: &str, row_id: &RowId) -> Fut<Arc<RowCell>> {
     let cell = self.database.lock().get_cell(field_id, row_id);
-    to_fut(async move { cell.map(Arc::new) })
+    to_fut(async move { Arc::new(cell) })
   }
 
   fn get_layout_for_view(&self, view_id: &str) -> DatabaseLayout {
-    self
-      .database
-      .lock()
-      .views
-      .get_view_layout(view_id)
-      .unwrap_or_default()
+    self.database.lock().views.get_database_view_layout(view_id)
   }
 
   fn get_group_setting(&self, view_id: &str) -> Vec<GroupSetting> {
@@ -1077,11 +1119,7 @@ impl DatabaseViewData for DatabaseViewDataImpl {
   }
 
   fn get_layout_setting(&self, view_id: &str, layout_ty: &DatabaseLayout) -> Option<LayoutSetting> {
-    self
-      .database
-      .lock()
-      .views
-      .get_layout_setting(view_id, layout_ty)
+    self.database.lock().get_layout_setting(view_id, layout_ty)
   }
 
   fn insert_layout_setting(
@@ -1096,6 +1134,17 @@ impl DatabaseViewData for DatabaseViewDataImpl {
       .insert_layout_setting(view_id, layout_ty, layout_setting);
   }
 
+  fn get_layout_type(&self, view_id: &str) -> DatabaseLayout {
+    self.database.lock().views.get_database_view_layout(view_id)
+  }
+
+  fn update_layout_type(&self, view_id: &str, layout_type: &DatabaseLayout) {
+    self
+      .database
+      .lock()
+      .update_layout_type(view_id, layout_type);
+  }
+
   fn get_task_scheduler(&self) -> Arc<RwLock<TaskDispatcher>> {
     self.task_scheduler.clone()
   }

+ 8 - 1
frontend/rust-lib/flowy-database2/src/services/database/entities.rs

@@ -1,5 +1,5 @@
 use collab_database::rows::RowId;
-use collab_database::views::RowOrder;
+use collab_database::views::{DatabaseLayout, RowOrder};
 
 #[derive(Debug, Clone)]
 pub enum DatabaseRowEvent {
@@ -25,3 +25,10 @@ pub struct UpdatedRow {
   // represents as the cells that were updated in this row.
   pub field_ids: Vec<String>,
 }
+
+#[derive(Debug, Clone)]
+pub struct CreateDatabaseViewParams {
+  pub name: String,
+  pub view_id: String,
+  pub layout_type: DatabaseLayout,
+}

+ 7 - 6
frontend/rust-lib/flowy-database2/src/services/database/util.rs

@@ -1,6 +1,6 @@
 use crate::entities::{
-  CalendarLayoutSettingPB, DatabaseLayoutPB, DatabaseViewSettingPB, FilterPB, GroupSettingPB,
-  LayoutSettingPB, SortPB,
+  CalendarLayoutSettingPB, DatabaseLayoutPB, DatabaseLayoutSettingPB, DatabaseViewSettingPB,
+  FilterPB, GroupSettingPB, SortPB,
 };
 use crate::services::filter::Filter;
 use crate::services::group::GroupSetting;
@@ -9,17 +9,18 @@ use crate::services::sort::Sort;
 use collab_database::views::DatabaseView;
 
 pub(crate) fn database_view_setting_pb_from_view(view: DatabaseView) -> DatabaseViewSettingPB {
+  let layout_type: DatabaseLayoutPB = view.layout.clone().into();
   let layout_setting = if let Some(layout_setting) = view.layout_settings.get(&view.layout) {
     let calendar_setting =
       CalendarLayoutSettingPB::from(CalendarLayoutSetting::from(layout_setting.clone()));
-    LayoutSettingPB {
+    DatabaseLayoutSettingPB {
+      layout_type: layout_type.clone(),
       calendar: Some(calendar_setting),
     }
   } else {
-    LayoutSettingPB::default()
+    DatabaseLayoutSettingPB::default()
   };
 
-  let current_layout: DatabaseLayoutPB = view.layout.into();
   let filters = view
     .filters
     .into_iter()
@@ -47,7 +48,7 @@ pub(crate) fn database_view_setting_pb_from_view(view: DatabaseView) -> Database
     .collect::<Vec<SortPB>>();
 
   DatabaseViewSettingPB {
-    current_layout,
+    layout_type,
     filters: filters.into(),
     group_settings: group_settings.into(),
     sorts: sorts.into(),

+ 94 - 20
frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs

@@ -3,7 +3,7 @@ use std::collections::HashMap;
 use std::sync::Arc;
 
 use collab_database::database::{gen_database_filter_id, gen_database_sort_id};
-use collab_database::fields::Field;
+use collab_database::fields::{Field, TypeOptionData};
 use collab_database::rows::{Cells, Row, RowCell, RowId};
 use collab_database::views::{DatabaseLayout, DatabaseView, LayoutSetting, RowOrder};
 use tokio::sync::{broadcast, RwLock};
@@ -13,9 +13,9 @@ use flowy_task::TaskDispatcher;
 use lib_infra::future::Fut;
 
 use crate::entities::{
-  CalendarEventPB, DeleteFilterParams, DeleteGroupParams, DeleteSortParams, FieldType,
-  GroupChangesPB, GroupPB, GroupRowsNotificationPB, InsertedRowPB, LayoutSettingPB,
-  LayoutSettingParams, RowPB, RowsChangePB, SortChangesetNotificationPB, SortPB,
+  CalendarEventPB, DatabaseLayoutMetaPB, DatabaseLayoutSettingPB, DeleteFilterParams,
+  DeleteGroupParams, DeleteSortParams, FieldType, GroupChangesPB, GroupPB, GroupRowsNotificationPB,
+  InsertedRowPB, LayoutSettingParams, RowPB, RowsChangePB, SortChangesetNotificationPB, SortPB,
   UpdateFilterParams, UpdateSortParams,
 };
 use crate::notification::{send_notification, DatabaseNotification};
@@ -31,7 +31,7 @@ use crate::services::database_view::{
   notify_did_update_setting, notify_did_update_sort, DatabaseViewChangedNotifier,
   DatabaseViewChangedReceiverRunner,
 };
-use crate::services::field::TypeOptionCellDataHandler;
+use crate::services::field::{DateTypeOption, TypeOptionCellDataHandler};
 use crate::services::filter::{
   Filter, FilterChangeset, FilterController, FilterType, UpdatedFilterType,
 };
@@ -42,13 +42,21 @@ use crate::services::setting::CalendarLayoutSetting;
 use crate::services::sort::{DeletedSortType, Sort, SortChangeset, SortController, SortType};
 
 pub trait DatabaseViewData: Send + Sync + 'static {
-  fn get_view_setting(&self, view_id: &str) -> Fut<Option<DatabaseView>>;
+  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>>>;
 
   /// Returns the field with the field_id
   fn get_field(&self, field_id: &str) -> Fut<Option<Arc<Field>>>;
 
+  fn create_field(
+    &self,
+    view_id: &str,
+    name: &str,
+    field_type: FieldType,
+    type_option_data: TypeOptionData,
+  ) -> Fut<Field>;
+
   fn get_primary_field(&self) -> Fut<Option<Arc<Field>>>;
 
   /// Returns the index of the row with row_id
@@ -62,7 +70,7 @@ pub trait DatabaseViewData: Send + Sync + 'static {
 
   fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Fut<Vec<Arc<RowCell>>>;
 
-  fn get_cell_in_row(&self, field_id: &str, row_id: &RowId) -> Fut<Option<Arc<RowCell>>>;
+  fn get_cell_in_row(&self, field_id: &str, row_id: &RowId) -> Fut<Arc<RowCell>>;
 
   fn get_layout_for_view(&self, view_id: &str) -> DatabaseLayout;
 
@@ -99,6 +107,12 @@ pub trait DatabaseViewData: Send + Sync + 'static {
     layout_setting: LayoutSetting,
   );
 
+  /// Return the database layout type for the view with given view_id
+  /// The default layout type is [DatabaseLayout::Grid]
+  fn get_layout_type(&self, view_id: &str) -> DatabaseLayout;
+
+  fn update_layout_type(&self, view_id: &str, layout_type: &DatabaseLayout);
+
   /// Returns a `TaskDispatcher` used to poll a `Task`
   fn get_task_scheduler(&self) -> Arc<RwLock<TaskDispatcher>>;
 
@@ -167,6 +181,10 @@ impl DatabaseViewEditor {
     self.filter_controller.close().await;
   }
 
+  pub async fn get_view(&self) -> Option<DatabaseView> {
+    self.delegate.get_view(&self.view_id).await
+  }
+
   pub async fn v_will_create_row(&self, cells: &mut Cells, group_id: &Option<String>) {
     if group_id.is_none() {
       return;
@@ -398,7 +416,7 @@ impl DatabaseViewEditor {
     if !is_grouping_field {
       self.v_update_grouping_field(field_id).await?;
 
-      if let Some(view) = self.delegate.get_view_setting(&self.view_id).await {
+      if let Some(view) = self.delegate.get_view(&self.view_id).await {
         let setting = database_view_setting_pb_from_view(view);
         notify_did_update_setting(&self.view_id, setting).await;
       }
@@ -571,16 +589,11 @@ impl DatabaseViewEditor {
       },
     }
 
-    tracing::debug!("{:?}", layout_setting);
     layout_setting
   }
 
   /// Update the calendar settings and send the notification to refresh the UI
-  pub async fn v_set_layout_settings(
-    &self,
-    _layout_ty: &DatabaseLayout,
-    params: LayoutSettingParams,
-  ) -> FlowyResult<()> {
+  pub async fn v_set_layout_settings(&self, params: LayoutSettingParams) -> FlowyResult<()> {
     // Maybe it needs no send notification to refresh the UI
     if let Some(new_calendar_setting) = params.calendar {
       if let Some(field) = self
@@ -593,16 +606,19 @@ impl DatabaseViewEditor {
           return Err(FlowyError::unexpect_calendar_field_type());
         }
 
-        let layout_ty = DatabaseLayout::Calendar;
-        let old_calender_setting = self.v_get_layout_settings(&layout_ty).await.calendar;
+        let old_calender_setting = self
+          .v_get_layout_settings(&params.layout_type)
+          .await
+          .calendar;
 
         self.delegate.insert_layout_setting(
           &self.view_id,
-          &layout_ty,
+          &params.layout_type,
           new_calendar_setting.clone().into(),
         );
         let new_field_id = new_calendar_setting.field_id.clone();
-        let layout_setting_pb: LayoutSettingPB = LayoutSettingParams {
+        let layout_setting_pb: DatabaseLayoutSettingPB = LayoutSettingParams {
+          layout_type: params.layout_type,
           calendar: Some(new_calendar_setting),
         }
         .into();
@@ -620,8 +636,6 @@ impl DatabaseViewEditor {
               .payload(layout_setting_pb)
               .send();
           }
-        } else {
-          tracing::warn!("Calendar setting should not be empty")
         }
       }
     }
@@ -788,6 +802,66 @@ impl DatabaseViewEditor {
     Some(events)
   }
 
+  #[tracing::instrument(level = "trace", skip_all)]
+  pub async fn v_update_layout_type(&self, 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(),
+    };
+
+    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?;
+        }
+      },
+    }
+
+    let payload = DatabaseLayoutMetaPB {
+      view_id: self.view_id.clone(),
+      layout: layout_type.into(),
+    };
+    send_notification(&self.view_id, DatabaseNotification::DidUpdateDatabaseLayout)
+      .payload(payload)
+      .send();
+
+    Ok(())
+  }
+
   pub async fn handle_row_event(&self, event: Cow<'_, DatabaseRowEvent>) {
     let changeset = match event.into_owned() {
       DatabaseRowEvent::InsertRow(row) => {

+ 27 - 29
frontend/rust-lib/flowy-database2/src/services/database_view/view_group.rs

@@ -52,10 +52,8 @@ pub async fn new_group_controller(
 
   let layout = delegate.get_layout_for_view(&view_id);
   // If the view is a board and the grouping field is empty, we need to find a new grouping field
-  if layout.is_board() {
-    if grouping_field.is_none() {
-      grouping_field = find_new_grouping_field(&fields, &layout);
-    }
+  if layout.is_board() && grouping_field.is_none() {
+    grouping_field = find_new_grouping_field(&fields, &layout);
   }
 
   if let Some(grouping_field) = grouping_field {
@@ -104,21 +102,20 @@ pub(crate) async fn get_cell_for_row(
   row_id: &RowId,
 ) -> Option<RowSingleCellData> {
   let field = delegate.get_field(field_id).await?;
-  let cell = delegate.get_cell_in_row(field_id, row_id).await?;
+  let row_cell = delegate.get_cell_in_row(field_id, row_id).await;
   let field_type = FieldType::from(field.field_type);
-
-  if let Some(handler) = delegate.get_type_option_cell_handler(&field, &field_type) {
-    return match handler.get_cell_data(&cell, &field_type, &field) {
-      Ok(cell_data) => Some(RowSingleCellData {
-        row_id: cell.row_id.clone(),
-        field_id: field.id.clone(),
-        field_type: field_type.clone(),
-        cell_data,
-      }),
-      Err(_) => None,
-    };
-  }
-  None
+  let handler = delegate.get_type_option_cell_handler(&field, &field_type)?;
+
+  let cell_data = match &row_cell.cell {
+    None => None,
+    Some(cell) => handler.get_cell_data(&cell, &field_type, &field).ok(),
+  };
+  Some(RowSingleCellData {
+    row_id: row_cell.row_id.clone(),
+    field_id: field.id.clone(),
+    field_type: field_type.clone(),
+    cell_data,
+  })
 }
 
 // Returns the list of cells corresponding to the given field.
@@ -133,17 +130,18 @@ pub(crate) async fn get_cells_for_field(
       let cells = delegate.get_cells_for_field(view_id, field_id).await;
       return cells
         .iter()
-        .flat_map(
-          |cell| match handler.get_cell_data(cell, &field_type, &field) {
-            Ok(cell_data) => Some(RowSingleCellData {
-              row_id: cell.row_id.clone(),
-              field_id: field.id.clone(),
-              field_type: field_type.clone(),
-              cell_data,
-            }),
-            Err(_) => None,
-          },
-        )
+        .map(|row_cell| {
+          let cell_data = match &row_cell.cell {
+            None => None,
+            Some(cell) => handler.get_cell_data(&cell, &field_type, &field).ok(),
+          };
+          RowSingleCellData {
+            row_id: row_cell.row_id.clone(),
+            field_id: field.id.clone(),
+            field_type: field_type.clone(),
+            cell_data,
+          }
+        })
         .collect();
     }
   }

+ 12 - 1
frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option.rs

@@ -19,7 +19,7 @@ use std::str::FromStr;
 /// The [DateTypeOption] is used by [FieldType::Date], [FieldType::LastEditedTime], and [FieldType::CreatedTime].
 /// So, storing the field type is necessary to distinguish the field type.
 /// Most of the cases, each [FieldType] has its own [TypeOption] implementation.
-#[derive(Clone, Default, Debug, Serialize, Deserialize)]
+#[derive(Clone, Debug, Serialize, Deserialize)]
 pub struct DateTypeOption {
   pub date_format: DateFormat,
   pub time_format: TimeFormat,
@@ -27,6 +27,17 @@ pub struct DateTypeOption {
   pub field_type: FieldType,
 }
 
+impl Default for DateTypeOption {
+  fn default() -> Self {
+    Self {
+      date_format: Default::default(),
+      time_format: Default::default(),
+      timezone_id: Default::default(),
+      field_type: FieldType::DateTime,
+    }
+  }
+}
+
 impl TypeOption for DateTypeOption {
   type CellData = DateCellData;
   type CellChangeset = DateCellChangeset;

+ 2 - 2
frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs

@@ -498,14 +498,14 @@ pub struct RowSingleCellData {
   pub row_id: RowId,
   pub field_id: String,
   pub field_type: FieldType,
-  pub cell_data: BoxCellData,
+  pub cell_data: Option<BoxCellData>,
 }
 
 macro_rules! into_cell_data {
   ($func_name:ident,$return_ty:ty) => {
     #[allow(dead_code)]
     pub fn $func_name(self) -> Option<$return_ty> {
-      self.cell_data.unbox_or_none()
+      self.cell_data?.unbox_or_none()
     }
   };
 }

+ 4 - 3
frontend/rust-lib/flowy-database2/src/services/group/controller.rs

@@ -254,6 +254,7 @@ where
           continue;
         }
       }
+
       match self.context.get_mut_no_status_group() {
         None => {},
         Some(no_status_group) => no_status_group.add_row((*row).clone()),
@@ -349,12 +350,12 @@ where
       deleted_group: None,
       row_changesets: vec![],
     };
-    let cell_rev = match context.row.cells.get(&self.grouping_field_id) {
-      Some(cell_rev) => Some(cell_rev.clone()),
+    let cell = match context.row.cells.get(&self.grouping_field_id) {
+      Some(cell) => Some(cell.clone()),
       None => self.placeholder_cell(),
     };
 
-    if let Some(cell) = cell_rev {
+    if let Some(cell) = cell {
       let cell_bytes = get_cell_protobuf(&cell, context.field, None);
       let cell_data = cell_bytes.parser::<P>()?;
       result.deleted_group = self.delete_group_when_move_row(context.row, &cell_data);

+ 10 - 2
frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs

@@ -1,4 +1,4 @@
-use crate::entities::{GroupRowsNotificationPB, SelectOptionCellDataPB};
+use crate::entities::{FieldType, GroupRowsNotificationPB, SelectOptionCellDataPB};
 use crate::services::cell::insert_select_option_cell;
 use crate::services::field::{MultiSelectTypeOption, SelectOptionCellDataParser};
 use crate::services::group::action::GroupCustomize;
@@ -10,7 +10,7 @@ use crate::services::group::{
   move_group_row, remove_select_option_row, GeneratedGroups, GroupContext,
 };
 use collab_database::fields::Field;
-use collab_database::rows::{Cells, Row};
+use collab_database::rows::{new_cell_builder, Cell, Cells, Row};
 use std::sync::Arc;
 
 use serde::{Deserialize, Serialize};
@@ -39,6 +39,14 @@ impl GroupCustomize for MultiSelectGroupController {
       .any(|option| option.id == content)
   }
 
+  fn placeholder_cell(&self) -> Option<Cell> {
+    Some(
+      new_cell_builder(FieldType::MultiSelect)
+        .insert_str_value("data", "")
+        .build(),
+    )
+  }
+
   fn add_or_remove_row_when_cell_changed(
     &mut self,
     row: &Row,

+ 10 - 2
frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/single_select_controller.rs

@@ -1,9 +1,9 @@
-use crate::entities::{GroupRowsNotificationPB, SelectOptionCellDataPB};
+use crate::entities::{FieldType, GroupRowsNotificationPB, SelectOptionCellDataPB};
 use crate::services::cell::insert_select_option_cell;
 use crate::services::field::{SelectOptionCellDataParser, SingleSelectTypeOption};
 use crate::services::group::action::GroupCustomize;
 use collab_database::fields::Field;
-use collab_database::rows::{Cells, Row};
+use collab_database::rows::{new_cell_builder, Cell, Cells, Row};
 use std::sync::Arc;
 
 use crate::services::group::controller::{
@@ -39,6 +39,14 @@ impl GroupCustomize for SingleSelectGroupController {
       .any(|option| option.id == content)
   }
 
+  fn placeholder_cell(&self) -> Option<Cell> {
+    Some(
+      new_cell_builder(FieldType::SingleSelect)
+        .insert_str_value("data", "")
+        .build(),
+    )
+  }
+
   fn add_or_remove_row_when_cell_changed(
     &mut self,
     row: &Row,

+ 0 - 0
frontend/rust-lib/flowy-database2/src/services/group/group_util.rs → frontend/rust-lib/flowy-database2/src/services/group/group_builder.rs


+ 2 - 2
frontend/rust-lib/flowy-database2/src/services/group/mod.rs

@@ -3,10 +3,10 @@ mod configuration;
 mod controller;
 mod controller_impls;
 mod entities;
-mod group_util;
+mod group_builder;
 
 pub(crate) use configuration::*;
 pub(crate) use controller::*;
 pub(crate) use controller_impls::*;
 pub(crate) use entities::*;
-pub(crate) use group_util::*;
+pub(crate) use group_builder::*;

+ 12 - 8
frontend/rust-lib/flowy-database2/tests/database/cell_test/test.rs

@@ -74,8 +74,8 @@ async fn text_cell_data_test() {
     .get_cells_for_field(&test.view_id, &text_field.id)
     .await;
 
-  for (i, cell) in cells.into_iter().enumerate() {
-    let text = StrCellData::from(cell.as_ref());
+  for (i, row_cell) in cells.into_iter().enumerate() {
+    let text = StrCellData::from(row_cell.cell.as_ref().unwrap());
     match i {
       0 => assert_eq!(text.as_str(), "A"),
       1 => assert_eq!(text.as_str(), ""),
@@ -97,10 +97,12 @@ async fn url_cell_data_test() {
     .get_cells_for_field(&test.view_id, &url_field.id)
     .await;
 
-  for (i, cell) in cells.into_iter().enumerate() {
-    let cell = URLCellData::from(cell.as_ref());
-    if i == 0 {
-      assert_eq!(cell.url.as_str(), "https://www.appflowy.io/");
+  for (i, row_cell) in cells.into_iter().enumerate() {
+    if let Some(cell) = row_cell.cell.as_ref() {
+      let cell = URLCellData::from(cell);
+      if i == 0 {
+        assert_eq!(cell.url.as_str(), "https://www.appflowy.io/");
+      }
     }
   }
 }
@@ -135,8 +137,10 @@ async fn update_updated_at_field_on_other_cell_update() {
     .get_cells_for_field(&test.view_id, &updated_at_field.id)
     .await;
   assert!(!cells.is_empty());
-  for (i, cell) in cells.into_iter().enumerate() {
-    let timestamp = DateCellData::from(cell.as_ref()).timestamp.unwrap();
+  for (i, row_cell) in cells.into_iter().enumerate() {
+    let timestamp = DateCellData::from(row_cell.cell.as_ref().unwrap())
+      .timestamp
+      .unwrap();
     println!(
       "{}, bf: {}, af: {}",
       timestamp, before_update_timestamp, after_update_timestamp

+ 31 - 22
frontend/rust-lib/flowy-database2/tests/database/database_editor.rs

@@ -6,7 +6,7 @@ use collab_database::fields::Field;
 use collab_database::rows::{CreateRowParams, Row, RowId};
 use strum::EnumCount;
 
-use flowy_database2::entities::{DatabaseLayoutPB, FieldType, FilterPB, RowPB, SelectOptionPB};
+use flowy_database2::entities::{FieldType, FilterPB, RowPB, SelectOptionPB};
 use flowy_database2::services::cell::{CellBuilder, ToCellChangeset};
 use flowy_database2::services::database::DatabaseEditor;
 use flowy_database2::services::field::checklist_type_option::{
@@ -21,7 +21,9 @@ use flowy_error::FlowyResult;
 use flowy_test::folder_event::ViewTest;
 use flowy_test::FlowyCoreTest;
 
-use crate::database::mock_data::{make_test_board, make_test_calendar, make_test_grid};
+use crate::database::mock_data::{
+  make_no_date_test_grid, make_test_board, make_test_calendar, make_test_grid,
+};
 
 pub struct DatabaseEditorTest {
   pub sdk: FlowyCoreTest,
@@ -36,35 +38,42 @@ pub struct DatabaseEditorTest {
 
 impl DatabaseEditorTest {
   pub async fn new_grid() -> Self {
-    Self::new(DatabaseLayoutPB::Grid).await
+    let sdk = FlowyCoreTest::new();
+    let _ = sdk.init_user().await;
+
+    let params = make_test_grid();
+    let view_test = ViewTest::new_grid_view(&sdk, params.to_json_bytes().unwrap()).await;
+    Self::new(sdk, view_test).await
   }
 
-  pub async fn new_board() -> Self {
-    Self::new(DatabaseLayoutPB::Board).await
+  pub async fn new_no_date_grid() -> Self {
+    let sdk = FlowyCoreTest::new();
+    let _ = sdk.init_user().await;
+
+    let params = make_no_date_test_grid();
+    let view_test = ViewTest::new_grid_view(&sdk, params.to_json_bytes().unwrap()).await;
+    Self::new(sdk, view_test).await
   }
 
-  pub async fn new_calendar() -> Self {
-    Self::new(DatabaseLayoutPB::Calendar).await
+  pub async fn new_board() -> Self {
+    let sdk = FlowyCoreTest::new();
+    let _ = sdk.init_user().await;
+
+    let params = make_test_board();
+    let view_test = ViewTest::new_grid_view(&sdk, params.to_json_bytes().unwrap()).await;
+    Self::new(sdk, view_test).await
   }
 
-  pub async fn new(layout: DatabaseLayoutPB) -> Self {
+  pub async fn new_calendar() -> Self {
     let sdk = FlowyCoreTest::new();
     let _ = sdk.init_user().await;
-    let test = match layout {
-      DatabaseLayoutPB::Grid => {
-        let params = make_test_grid();
-        ViewTest::new_grid_view(&sdk, params.to_json_bytes().unwrap()).await
-      },
-      DatabaseLayoutPB::Board => {
-        let data = make_test_board();
-        ViewTest::new_board_view(&sdk, data.to_json_bytes().unwrap()).await
-      },
-      DatabaseLayoutPB::Calendar => {
-        let data = make_test_calendar();
-        ViewTest::new_calendar_view(&sdk, data.to_json_bytes().unwrap()).await
-      },
-    };
 
+    let params = make_test_calendar();
+    let view_test = ViewTest::new_grid_view(&sdk, params.to_json_bytes().unwrap()).await;
+    Self::new(sdk, view_test).await
+  }
+
+  pub async fn new(sdk: FlowyCoreTest, test: ViewTest) -> Self {
     let editor = sdk
       .database_manager
       .get_database_with_view_id(&test.child_view.id)

+ 1 - 1
frontend/rust-lib/flowy-database2/tests/database/filter_test/script.rs

@@ -265,7 +265,7 @@ impl DatabaseFilterTest {
                 assert_eq!(expected_setting, setting);
             }
             FilterScript::AssertNumberOfVisibleRows { expected } => {
-                let grid = self.editor.get_database_data(&self.view_id).await;
+                let grid = self.editor.get_database_data(&self.view_id).await.unwrap();
                 assert_eq!(grid.rows.len(), expected);
             }
             FilterScript::Wait { millisecond } => {

+ 25 - 2
frontend/rust-lib/flowy-database2/tests/database/layout_test/script.rs

@@ -8,7 +8,9 @@ use crate::database::database_editor::DatabaseEditorTest;
 
 pub enum LayoutScript {
   AssertCalendarLayoutSetting { expected: CalendarLayoutSetting },
-  GetCalendarEvents,
+  AssertDefaultAllCalendarEvents,
+  AssertAllCalendarEventsCount { expected: usize },
+  UpdateDatabaseLayout { layout: DatabaseLayout },
 }
 
 pub struct DatabaseLayoutTest {
@@ -16,6 +18,11 @@ pub struct DatabaseLayoutTest {
 }
 
 impl DatabaseLayoutTest {
+  pub async fn new_no_date_grid() -> Self {
+    let database_test = DatabaseEditorTest::new_no_date_grid().await;
+    Self { database_test }
+  }
+
   pub async fn new_calendar() -> Self {
     let database_test = DatabaseEditorTest::new_calendar().await;
     Self { database_test }
@@ -33,6 +40,22 @@ impl DatabaseLayoutTest {
 
   pub async fn run_script(&mut self, script: LayoutScript) {
     match script {
+      LayoutScript::UpdateDatabaseLayout { layout } => {
+        self
+          .database_test
+          .editor
+          .update_view_layout(&self.database_test.view_id, layout)
+          .await
+          .unwrap();
+      },
+      LayoutScript::AssertAllCalendarEventsCount { expected } => {
+        let events = self
+          .database_test
+          .editor
+          .get_all_calendar_events(&self.database_test.view_id)
+          .await;
+        assert_eq!(events.len(), expected);
+      },
       LayoutScript::AssertCalendarLayoutSetting { expected } => {
         let view_id = self.database_test.view_id.clone();
         let layout_ty = DatabaseLayout::Calendar;
@@ -53,7 +76,7 @@ impl DatabaseLayoutTest {
         );
         assert_eq!(calendar_setting.show_weekends, expected.show_weekends);
       },
-      LayoutScript::GetCalendarEvents => {
+      LayoutScript::AssertDefaultAllCalendarEvents => {
         let events = self
           .database_test
           .editor

+ 14 - 1
frontend/rust-lib/flowy-database2/tests/database/layout_test/test.rs

@@ -1,3 +1,4 @@
+use collab_database::views::DatabaseLayout;
 use flowy_database2::services::setting::CalendarLayoutSetting;
 
 use crate::database::layout_test::script::DatabaseLayoutTest;
@@ -17,6 +18,18 @@ async fn calendar_initial_layout_setting_test() {
 #[tokio::test]
 async fn calendar_get_events_test() {
   let mut test = DatabaseLayoutTest::new_calendar().await;
-  let scripts = vec![GetCalendarEvents];
+  let scripts = vec![AssertDefaultAllCalendarEvents];
+  test.run_scripts(scripts).await;
+}
+
+#[tokio::test]
+async fn grid_to_calendar_layout_test() {
+  let mut test = DatabaseLayoutTest::new_no_date_grid().await;
+  let scripts = vec![
+    UpdateDatabaseLayout {
+      layout: DatabaseLayout::Calendar,
+    },
+    AssertAllCalendarEventsCount { expected: 3 },
+  ];
   test.run_scripts(scripts).await;
 }

+ 77 - 0
frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs

@@ -240,3 +240,80 @@ pub fn make_test_grid() -> DatabaseData {
 
   DatabaseData { view, fields, rows }
 }
+
+pub fn make_no_date_test_grid() -> DatabaseData {
+  let mut fields = vec![];
+  let mut rows = vec![];
+  // Iterate through the FieldType to create the corresponding Field.
+  for field_type in FieldType::iter() {
+    match field_type {
+      FieldType::RichText => {
+        let text_field = FieldBuilder::from_field_type(field_type.clone())
+          .name("Name")
+          .visibility(true)
+          .primary(true)
+          .build();
+        fields.push(text_field);
+      },
+      FieldType::Number => {
+        // Number
+        let mut type_option = NumberTypeOption::default();
+        type_option.set_format(NumberFormat::USD);
+
+        let number_field = FieldBuilder::new(field_type.clone(), type_option)
+          .name("Price")
+          .visibility(true)
+          .build();
+        fields.push(number_field);
+      },
+      _ => {},
+    }
+  }
+
+  for i in 0..3 {
+    let mut row_builder = TestRowBuilder::new(i.into(), &fields);
+    match i {
+      0 => {
+        for field_type in FieldType::iter() {
+          match field_type {
+            FieldType::RichText => row_builder.insert_text_cell("A"),
+            FieldType::Number => row_builder.insert_number_cell("1"),
+            _ => "".to_owned(),
+          };
+        }
+      },
+      1 => {
+        for field_type in FieldType::iter() {
+          match field_type {
+            FieldType::RichText => row_builder.insert_text_cell(""),
+            FieldType::Number => row_builder.insert_number_cell("2"),
+            _ => "".to_owned(),
+          };
+        }
+      },
+      2 => {
+        for field_type in FieldType::iter() {
+          match field_type {
+            FieldType::RichText => row_builder.insert_text_cell("C"),
+            FieldType::Number => row_builder.insert_number_cell("3"),
+            _ => "".to_owned(),
+          };
+        }
+      },
+      _ => {},
+    }
+
+    let row = row_builder.build();
+    rows.push(row);
+  }
+
+  let view = DatabaseView {
+    id: gen_database_view_id(),
+    database_id: gen_database_id(),
+    name: "".to_string(),
+    layout: DatabaseLayout::Grid,
+    ..Default::default()
+  };
+
+  DatabaseData { view, fields, rows }
+}

+ 8 - 3
frontend/rust-lib/flowy-folder2/src/entities/view.rs

@@ -1,5 +1,5 @@
 use crate::entities::parser::view::{ViewDesc, ViewIdentify, ViewName, ViewThumbnail};
-use crate::view_ext::gen_view_id;
+use crate::view_operation::gen_view_id;
 use collab_folder::core::{View, ViewLayout};
 use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
 use flowy_error::ErrorCode;
@@ -31,7 +31,7 @@ pub struct ViewPB {
 pub fn view_pb_without_child_views(view: View) -> ViewPB {
   ViewPB {
     id: view.id,
-    parent_view_id: view.bid,
+    parent_view_id: view.parent_view_id,
     name: view.name,
     create_time: view.created_at,
     child_views: Default::default(),
@@ -43,7 +43,7 @@ pub fn view_pb_without_child_views(view: View) -> ViewPB {
 pub fn view_pb_with_child_views(view: View, child_views: Vec<View>) -> ViewPB {
   ViewPB {
     id: view.id,
-    parent_view_id: view.bid,
+    parent_view_id: view.parent_view_id,
     name: view.name,
     create_time: view.created_at,
     child_views: child_views
@@ -219,6 +219,9 @@ pub struct UpdateViewPayloadPB {
 
   #[pb(index = 4, one_of)]
   pub thumbnail: Option<String>,
+
+  #[pb(index = 5, one_of)]
+  pub layout: Option<ViewLayoutPB>,
 }
 
 #[derive(Clone, Debug)]
@@ -227,6 +230,7 @@ pub struct UpdateViewParams {
   pub name: Option<String>,
   pub desc: Option<String>,
   pub thumbnail: Option<String>,
+  pub layout: Option<ViewLayout>,
 }
 
 impl TryInto<UpdateViewParams> for UpdateViewPayloadPB {
@@ -255,6 +259,7 @@ impl TryInto<UpdateViewParams> for UpdateViewPayloadPB {
       name,
       desc,
       thumbnail,
+      layout: self.layout.map(|ty| ty.into()),
     })
   }
 }

+ 1 - 1
frontend/rust-lib/flowy-folder2/src/lib.rs

@@ -5,7 +5,7 @@ pub mod manager;
 mod notification;
 pub mod protobuf;
 mod user_default;
-pub mod view_ext;
+pub mod view_operation;
 
 pub mod deps;
 mod share;

+ 25 - 24
frontend/rust-lib/flowy-folder2/src/manager.rs

@@ -29,7 +29,9 @@ use crate::notification::{
 };
 use crate::share::ImportParams;
 use crate::user_default::DefaultFolderBuilder;
-use crate::view_ext::{create_view, gen_view_id, FolderOperationHandler, FolderOperationHandlers};
+use crate::view_operation::{
+  create_view, gen_view_id, FolderOperationHandler, FolderOperationHandlers,
+};
 
 pub struct Folder2Manager {
   mutex_folder: Arc<MutexFolder>,
@@ -200,18 +202,12 @@ impl Folder2Manager {
     let view_layout: ViewLayout = params.layout.clone().into();
     let handler = self.get_handler(&view_layout)?;
     let user_id = self.user.user_id()?;
-    let ext = params.meta.clone();
+    let meta = params.meta.clone();
     match params.initial_data.is_empty() {
       true => {
         tracing::trace!("Create view with build-in data");
         handler
-          .create_built_in_view(
-            user_id,
-            &params.view_id,
-            &params.name,
-            view_layout.clone(),
-            ext,
-          )
+          .create_built_in_view(user_id, &params.view_id, &params.name, view_layout.clone())
           .await?;
       },
       false => {
@@ -223,7 +219,7 @@ impl Folder2Manager {
             &params.name,
             params.initial_data.clone(),
             view_layout.clone(),
-            ext,
+            meta,
           )
           .await?;
       },
@@ -233,7 +229,7 @@ impl Folder2Manager {
       folder.insert_view(view.clone());
     });
 
-    notify_parent_view_did_change(self.mutex_folder.clone(), vec![view.bid.clone()]);
+    notify_parent_view_did_change(self.mutex_folder.clone(), vec![view.parent_view_id.clone()]);
     Ok(view)
   }
 
@@ -336,7 +332,7 @@ impl Folder2Manager {
     match view {
       None => tracing::error!("Couldn't find the view. It should not be empty"),
       Some(view) => {
-        notify_parent_view_did_change(self.mutex_folder.clone(), vec![view.bid]);
+        notify_parent_view_did_change(self.mutex_folder.clone(), vec![view.parent_view_id]);
       },
     }
     Ok(())
@@ -350,18 +346,23 @@ impl Folder2Manager {
 
   #[tracing::instrument(level = "trace", skip(self), err)]
   pub async fn update_view_with_params(&self, params: UpdateViewParams) -> FlowyResult<()> {
-    let _ = self
-      .mutex_folder
-      .lock()
-      .as_ref()
-      .ok_or_else(folder_not_init_error)?
-      .views
-      .update_view(&params.view_id, |update| {
+    let value = self.with_folder(None, |folder| {
+      let old_view = folder.views.get_view(&params.view_id);
+      let new_view = folder.views.update_view(&params.view_id, |update| {
         update
           .set_name_if_not_none(params.name)
           .set_desc_if_not_none(params.desc)
+          .set_layout_if_not_none(params.layout)
           .done()
       });
+      Some((old_view, new_view))
+    });
+
+    if let Some((Some(old_view), Some(new_view))) = value {
+      if let Ok(handler) = self.get_handler(&old_view.layout) {
+        handler.did_update_view(&old_view, &new_view).await?;
+      }
+    }
 
     if let Ok(view_pb) = self.get_view(&params.view_id).await {
       notify_parent_view_did_change(
@@ -388,7 +389,7 @@ impl Folder2Manager {
     //   meta.insert("database_id".to_string(), database_id);
     // }
     let duplicate_params = CreateViewParams {
-      parent_view_id: view.bid.clone(),
+      parent_view_id: view.parent_view_id.clone(),
       name: format!("{} (copy)", &view.name),
       desc: view.desc,
       layout: view.layout.into(),
@@ -501,7 +502,7 @@ impl Folder2Manager {
     self.with_folder((), |folder| {
       folder.insert_view(view.clone());
     });
-    notify_parent_view_did_change(self.mutex_folder.clone(), vec![view.bid.clone()]);
+    notify_parent_view_did_change(self.mutex_folder.clone(), vec![view.parent_view_id.clone()]);
     Ok(view)
   }
 
@@ -529,11 +530,11 @@ 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_parent_view_did_change(folder.clone(), vec![view.bid]);
+            notify_parent_view_did_change(folder.clone(), vec![view.parent_view_id]);
           },
           ViewChange::DidDeleteView { views: _ } => {},
           ViewChange::DidUpdate { view } => {
-            notify_parent_view_did_change(folder.clone(), vec![view.bid]);
+            notify_parent_view_did_change(folder.clone(), vec![view.parent_view_id]);
           },
         };
       }
@@ -580,7 +581,7 @@ fn listen_on_trash_change(mut rx: TrashChangeReceiver, weak_mutex_folder: &Weak<
         if let Some(folder) = folder.lock().as_ref() {
           let views = folder.views.get_views(&ids);
           for view in views {
-            unique_ids.insert(view.bid);
+            unique_ids.insert(view.parent_view_id);
           }
 
           let repeated_trash: RepeatedTrashPB = folder.trash.get_all_trash().into();

+ 1 - 1
frontend/rust-lib/flowy-folder2/src/test_helper.rs

@@ -1,6 +1,6 @@
 use crate::entities::{CreateViewParams, ViewLayoutPB};
 use crate::manager::Folder2Manager;
-use crate::view_ext::gen_view_id;
+use crate::view_operation::gen_view_id;
 use std::collections::HashMap;
 
 #[cfg(feature = "test_helper")]

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

@@ -1,11 +1,9 @@
-use std::collections::HashMap;
-
 use chrono::Utc;
 use collab_folder::core::{FolderData, RepeatedView, View, ViewIdentifier, ViewLayout, Workspace};
 use nanoid::nanoid;
 
 use crate::entities::{view_pb_with_child_views, WorkspacePB};
-use crate::view_ext::{gen_view_id, FolderOperationHandlers};
+use crate::view_operation::{gen_view_id, FolderOperationHandlers};
 
 pub struct DefaultFolderBuilder();
 impl DefaultFolderBuilder {
@@ -21,7 +19,7 @@ impl DefaultFolderBuilder {
     let child_view_layout = ViewLayout::Document;
     let child_view = View {
       id: child_view_id.clone(),
-      bid: view_id.clone(),
+      parent_view_id: view_id.clone(),
       name: "Read me".to_string(),
       desc: "".to_string(),
       created_at: time,
@@ -39,14 +37,13 @@ impl DefaultFolderBuilder {
         &child_view.id,
         &child_view.name,
         child_view_layout.clone(),
-        HashMap::default(),
       )
       .await
       .unwrap();
 
     let view = View {
       id: view_id,
-      bid: workspace_id.clone(),
+      parent_view_id: workspace_id.clone(),
       name: "⭐️ Getting started".to_string(),
       desc: "".to_string(),
       children: RepeatedView::new(vec![ViewIdentifier {

+ 21 - 5
frontend/rust-lib/flowy-folder2/src/view_ext.rs → frontend/rust-lib/flowy-folder2/src/view_operation.rs

@@ -1,6 +1,6 @@
 use crate::entities::{CreateViewParams, ViewLayoutPB};
 use bytes::Bytes;
-use collab_folder::core::{View, ViewLayout};
+use collab_folder::core::ViewLayout;
 use flowy_error::FlowyError;
 use lib_infra::future::FutureResult;
 use lib_infra::util::timestamp;
@@ -9,6 +9,7 @@ use std::collections::HashMap;
 use std::sync::Arc;
 
 pub type ViewData = Bytes;
+pub use collab_folder::core::View;
 
 /// The handler will be used to handler the folder operation for a specific
 /// view layout. Each [ViewLayout] will have a handler. So when creating a new
@@ -22,7 +23,18 @@ pub trait FolderOperationHandler {
   /// Returns the [ViewData] that can be used to create the same view.
   fn duplicate_view(&self, view_id: &str) -> FutureResult<ViewData, FlowyError>;
 
-  /// Create a view with custom data
+  /// Create a view with the data.
+  ///
+  /// # Arguments
+  ///
+  /// * `user_id`: the user id
+  /// * `view_id`: the view id
+  /// * `name`: the name of the view
+  /// * `data`: initial data of the view. The data should be parsed by the [FolderOperationHandler]
+  /// implementation. For example, the data of the database will be [DatabaseData].
+  /// * `layout`: the layout of the view
+  /// * `meta`: use to carry extra information. For example, the database view will use this
+  /// to carry the reference database id.
   fn create_view_with_view_data(
     &self,
     user_id: i64,
@@ -30,7 +42,7 @@ pub trait FolderOperationHandler {
     name: &str,
     data: Vec<u8>,
     layout: ViewLayout,
-    ext: HashMap<String, String>,
+    meta: HashMap<String, String>,
   ) -> FutureResult<(), FlowyError>;
 
   /// Create a view with the pre-defined data.
@@ -42,7 +54,6 @@ pub trait FolderOperationHandler {
     view_id: &str,
     name: &str,
     layout: ViewLayout,
-    meta: HashMap<String, String>,
   ) -> FutureResult<(), FlowyError>;
 
   /// Create a view by importing data
@@ -60,6 +71,11 @@ pub trait FolderOperationHandler {
     name: &str,
     path: String,
   ) -> FutureResult<(), FlowyError>;
+
+  /// Called when the view is updated. The handler is the `old` registered handler.
+  fn did_update_view(&self, _old: &View, _new: &View) -> FutureResult<(), FlowyError> {
+    FutureResult::new(async move { Ok(()) })
+  }
 }
 
 pub type FolderOperationHandlers =
@@ -80,7 +96,7 @@ pub(crate) fn create_view(params: CreateViewParams, layout: ViewLayout) -> View
   let time = timestamp();
   View {
     id: params.view_id,
-    bid: params.parent_view_id,
+    parent_view_id: params.parent_view_id,
     name: params.name,
     desc: params.desc,
     children: Default::default(),

+ 1 - 0
frontend/rust-lib/flowy-folder2/tests/workspace/script.rs

@@ -274,6 +274,7 @@ pub async fn update_view(
     name,
     desc,
     thumbnail: None,
+    layout: None,
   };
   EventBuilder::new(sdk.clone())
     .event(UpdateView)