Просмотр исходного кода

Feat/calendar with backend data (#1930)

* chore: enable calendar view

* chore: update database group listener

* refactor: remove board data controller

* refactor: remove group backend service

* refactor: remove calendar controller

* chore: create default calendar setting

* chore: send calednar setting notifications

* refactor: rename the card in kanban board, prepare to reuse in calendar

* refactor: support custom card cell

* chore: return calendar events

* refactor: remove groupId in card, make card more generic

* chore: display cell

* chore: display three cards in calendar

* chore: create calendar card

* refactor: create row with data

* chore: support create event

* ci: fix tauri build

* chore: disable create calendar
Nathan.fooo 2 лет назад
Родитель
Сommit
7106195d8a
92 измененных файлов с 2400 добавлено и 1298 удалено
  1. 2 1
      frontend/appflowy_flutter/assets/translations/en.json
  2. 3 3
      frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_cache.dart
  3. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller.dart
  4. 290 0
      frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart
  5. 67 11
      frontend/appflowy_flutter/lib/plugins/database_view/application/database_service.dart
  6. 15 15
      frontend/appflowy_flutter/lib/plugins/database_view/application/group/group_listener.dart
  7. 51 0
      frontend/appflowy_flutter/lib/plugins/database_view/application/layout/layout_setting_listener.dart
  8. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart
  9. 6 4
      frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_data_controller.dart
  10. 0 51
      frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_service.dart
  11. 47 66
      frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart
  12. 0 144
      frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_data_controller.dart
  13. 0 39
      frontend/appflowy_flutter/lib/plugins/database_view/board/application/card/card_data_controller.dart
  14. 49 3
      frontend/appflowy_flutter/lib/plugins/database_view/board/application/group_controller.dart
  15. 0 51
      frontend/appflowy_flutter/lib/plugins/database_view/board/application/group_listener.dart
  16. 24 14
      frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart
  17. 178 54
      frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart
  18. 0 115
      frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_data_controller.dart
  19. 0 65
      frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_listener.dart
  20. 1 2
      frontend/appflowy_flutter/lib/plugins/database_view/calendar/calendar.dart
  21. 251 32
      frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart
  22. 3 2
      frontend/appflowy_flutter/lib/plugins/database_view/grid/application/cell/checklist_cell_bloc.dart
  23. 6 5
      frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart
  24. 0 83
      frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_data_controller.dart
  25. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_detail_bloc.dart
  26. 10 4
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart
  27. 3 3
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/cell/checklist_cell/checklist_cell.dart
  28. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/row_detail.dart
  29. 16 16
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/checkbox_card_cell_bloc.dart
  30. 14 14
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/date_card_cell_bloc.dart
  31. 15 15
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/number_card_cell_bloc.dart
  32. 15 15
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/select_option_card_cell_bloc.dart
  33. 16 16
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/text_card_cell_bloc.dart
  34. 15 15
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/url_card_cell_bloc.dart
  35. 8 8
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/board_number_cell.dart
  36. 38 28
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card.dart
  37. 23 17
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_bloc.dart
  38. 32 37
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart
  39. 32 7
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/card_cell.dart
  40. 13 13
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/checkbox_card_cell.dart
  41. 9 8
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/checklist_card_cell.dart
  42. 12 13
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/date_card_cell.dart
  43. 68 0
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/number_card_cell.dart
  44. 26 26
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/select_option_card_cell.dart
  45. 20 22
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/text_card_cell.dart
  46. 12 13
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/url_card_cell.dart
  47. 0 0
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/container/accessory.dart
  48. 0 0
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/container/card_container.dart
  49. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/define.dart
  50. 2 2
      frontend/appflowy_flutter/lib/plugins/document/application/doc_service.dart
  51. 3 0
      frontend/appflowy_flutter/macos/Runner.xcodeproj/project.pbxproj
  52. 13 6
      frontend/appflowy_flutter/test/bloc_test/board_test/util.dart
  53. 18 5
      frontend/appflowy_flutter/test/bloc_test/grid_test/filter/create_filter_test.dart
  54. 9 3
      frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_util.dart
  55. 10 3
      frontend/appflowy_flutter/test/bloc_test/grid_test/grid_bloc_test.dart
  56. 15 5
      frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart
  57. 4 11
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_bd_svc.ts
  58. 1 1
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/group/group_controller.ts
  59. 6 1
      frontend/rust-lib/flowy-client-sync/src/client_database/database_builder.rs
  60. 2 2
      frontend/rust-lib/flowy-client-sync/src/client_database/database_revision_pad.rs
  61. 9 5
      frontend/rust-lib/flowy-client-sync/src/client_database/database_view_revision_pad.rs
  62. 100 49
      frontend/rust-lib/flowy-database/src/entities/calendar_entities.rs
  63. 9 1
      frontend/rust-lib/flowy-database/src/entities/database_entities.rs
  64. 1 36
      frontend/rust-lib/flowy-database/src/entities/group_entities/group.rs
  65. 26 5
      frontend/rust-lib/flowy-database/src/entities/row_entities.rs
  66. 59 6
      frontend/rust-lib/flowy-database/src/entities/setting_entities.rs
  67. 47 25
      frontend/rust-lib/flowy-database/src/event_handler.rs
  68. 19 11
      frontend/rust-lib/flowy-database/src/event_map.rs
  69. 5 1
      frontend/rust-lib/flowy-database/src/manager.rs
  70. 6 2
      frontend/rust-lib/flowy-database/src/notification.rs
  71. 58 3
      frontend/rust-lib/flowy-database/src/services/database/database_editor.rs
  72. 10 0
      frontend/rust-lib/flowy-database/src/services/database/trait_impl.rs
  73. 201 18
      frontend/rust-lib/flowy-database/src/services/database_view/editor.rs
  74. 23 3
      frontend/rust-lib/flowy-database/src/services/database_view/editor_manager.rs
  75. 4 4
      frontend/rust-lib/flowy-database/src/services/database_view/trait_impl.rs
  76. 4 0
      frontend/rust-lib/flowy-database/src/services/field/type_options/checkbox_type_option/checkbox_type_option_entities.rs
  77. 51 4
      frontend/rust-lib/flowy-database/src/services/row/row_builder.rs
  78. 9 2
      frontend/rust-lib/flowy-database/src/util.rs
  79. 2 2
      frontend/rust-lib/flowy-database/tests/database/block_test/script.rs
  80. 0 3
      frontend/rust-lib/flowy-database/tests/database/block_test/util.rs
  81. 4 4
      frontend/rust-lib/flowy-database/tests/database/database_editor.rs
  82. 2 2
      frontend/rust-lib/flowy-database/tests/database/group_test/script.rs
  83. 2 0
      frontend/rust-lib/flowy-database/tests/database/layout_test/mod.rs
  84. 86 0
      frontend/rust-lib/flowy-database/tests/database/layout_test/script.rs
  85. 21 0
      frontend/rust-lib/flowy-database/tests/database/layout_test/test.rs
  86. 100 2
      frontend/rust-lib/flowy-database/tests/database/mock_data/calendar_mock_data.rs
  87. 1 0
      frontend/rust-lib/flowy-database/tests/database/mod.rs
  88. 3 0
      frontend/rust-lib/flowy-error/src/code.rs
  89. 4 0
      frontend/rust-lib/flowy-error/src/errors.rs
  90. 36 1
      shared-lib/database-model/src/database_rev.rs
  91. 11 6
      shared-lib/database-model/src/view_rev.rs
  92. 9 9
      shared-lib/flowy-sync/src/errors.rs

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

@@ -372,6 +372,7 @@
   },
   "calendar": {
     "menuName": "Calendar",
+    "defaultNewCalendarTitle": "Untitled",
     "navigation": {
       "today": "Today",
       "jumpToday": "Jump to Today",
@@ -379,4 +380,4 @@
       "nextMonth": "Next Month"
     }
   }
-}
+}

+ 3 - 3
frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_cache.dart

@@ -2,9 +2,9 @@ part of 'cell_service.dart';
 
 typedef CellByFieldId = LinkedHashMap<String, CellIdentifier>;
 
-class GridBaseCell {
+class DatabaseCell {
   dynamic object;
-  GridBaseCell({
+  DatabaseCell({
     required this.object,
   });
 }
@@ -44,7 +44,7 @@ class CellCache {
     }
   }
 
-  void insert<T extends GridBaseCell>(CellCacheKey key, T value) {
+  void insert<T extends DatabaseCell>(CellCacheKey key, T value) {
     var map = _cellDataByFieldId[key.fieldId];
     if (map == null) {
       _cellDataByFieldId[key.fieldId] = {};

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

@@ -170,7 +170,7 @@ class CellController<T, D> extends Equatable {
     _loadDataOperation = Timer(const Duration(milliseconds: 10), () {
       _cellDataLoader.loadData().then((data) {
         if (data != null) {
-          _cellCache.insert(_cacheKey, GridBaseCell(object: data));
+          _cellCache.insert(_cacheKey, DatabaseCell(object: data));
         } else {
           _cellCache.remove(_cacheKey);
         }

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

@@ -0,0 +1,290 @@
+import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
+import 'package:appflowy/plugins/database_view/application/view/view_cache.dart';
+import 'package:appflowy_backend/log.dart';
+import 'package:appflowy_backend/protobuf/flowy-database/calendar_entities.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pbenum.dart';
+import 'package:appflowy_backend/protobuf/flowy-database/group.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-database/group_changeset.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-database/row_entities.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-database/setting_entities.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
+import 'package:collection/collection.dart';
+import 'dart:async';
+import 'package:dartz/dartz.dart';
+import 'database_service.dart';
+import 'defines.dart';
+import 'layout/layout_setting_listener.dart';
+import 'row/row_cache.dart';
+import 'group/group_listener.dart';
+
+typedef OnRowsChanged = void Function(
+  List<RowInfo> rowInfos,
+  RowsChangedReason,
+);
+
+typedef OnGroupByField = void Function(List<GroupPB>);
+typedef OnUpdateGroup = void Function(List<GroupPB>);
+typedef OnDeleteGroup = void Function(List<String>);
+typedef OnInsertGroup = void Function(InsertedGroupPB);
+
+class GroupCallbacks {
+  final OnGroupByField? onGroupByField;
+  final OnUpdateGroup? onUpdateGroup;
+  final OnDeleteGroup? onDeleteGroup;
+  final OnInsertGroup? onInsertGroup;
+
+  GroupCallbacks({
+    this.onGroupByField,
+    this.onUpdateGroup,
+    this.onDeleteGroup,
+    this.onInsertGroup,
+  });
+}
+
+class LayoutCallbacks {
+  final void Function(LayoutSettingPB) onLayoutChanged;
+  final void Function(LayoutSettingPB) onLoadLayout;
+
+  LayoutCallbacks({
+    required this.onLayoutChanged,
+    required this.onLoadLayout,
+  });
+}
+
+class DatabaseCallbacks {
+  OnDatabaseChanged? onDatabaseChanged;
+  OnRowsChanged? onRowsChanged;
+  OnFieldsChanged? onFieldsChanged;
+  OnFiltersChanged? onFiltersChanged;
+  DatabaseCallbacks({
+    this.onDatabaseChanged,
+    this.onRowsChanged,
+    this.onFieldsChanged,
+    this.onFiltersChanged,
+  });
+}
+
+class DatabaseController {
+  final String viewId;
+  final DatabaseBackendService _databaseBackendSvc;
+  final FieldController fieldController;
+  late DatabaseViewCache _viewCache;
+  final LayoutTypePB layoutType;
+
+  // Callbacks
+  DatabaseCallbacks? _databaseCallbacks;
+  GroupCallbacks? _groupCallbacks;
+  LayoutCallbacks? _layoutCallbacks;
+
+  // Getters
+  List<RowInfo> get rowInfos => _viewCache.rowInfos;
+  RowCache get rowCache => _viewCache.rowCache;
+
+  // Listener
+  final DatabaseGroupListener groupListener;
+  final DatabaseLayoutListener layoutListener;
+
+  DatabaseController({required ViewPB view, required this.layoutType})
+      : viewId = view.id,
+        _databaseBackendSvc = DatabaseBackendService(viewId: view.id),
+        fieldController = FieldController(viewId: view.id),
+        groupListener = DatabaseGroupListener(view.id),
+        layoutListener = DatabaseLayoutListener(view.id) {
+    _viewCache = DatabaseViewCache(
+      viewId: viewId,
+      fieldController: fieldController,
+    );
+    _listenOnRowsChanged();
+    _listenOnFieldsChanged();
+    _listenOnGroupChanged();
+    _listenOnLayoutChanged();
+  }
+
+  void addListener({
+    DatabaseCallbacks? onDatabaseChanged,
+    LayoutCallbacks? onLayoutChanged,
+    GroupCallbacks? onGroupChanged,
+  }) {
+    _layoutCallbacks = onLayoutChanged;
+    _databaseCallbacks = onDatabaseChanged;
+    _groupCallbacks = onGroupChanged;
+  }
+
+  Future<Either<Unit, FlowyError>> open() async {
+    return _databaseBackendSvc.openGrid().then((result) {
+      return result.fold(
+        (database) async {
+          _databaseCallbacks?.onDatabaseChanged?.call(database);
+          _viewCache.rowCache.setInitialRows(database.rows);
+          return await fieldController
+              .loadFields(
+            fieldIds: database.fields,
+          )
+              .then(
+            (result) {
+              return result.fold(
+                (l) => Future(() async {
+                  await _loadGroups();
+                  await _loadLayoutSetting();
+                  return left(l);
+                }),
+                (err) => right(err),
+              );
+            },
+          );
+        },
+        (err) => right(err),
+      );
+    });
+  }
+
+  Future<Either<RowPB, FlowyError>> createRow({
+    String? startRowId,
+    String? groupId,
+    void Function(RowDataBuilder builder)? withCells,
+  }) {
+    Map<String, String>? cellDataByFieldId;
+
+    if (withCells != null) {
+      final rowBuilder = RowDataBuilder();
+      withCells(rowBuilder);
+      cellDataByFieldId = rowBuilder.build();
+    }
+
+    return _databaseBackendSvc.createRow(
+      startRowId: startRowId,
+      groupId: groupId,
+      cellDataByFieldId: cellDataByFieldId,
+    );
+  }
+
+  Future<Either<Unit, FlowyError>> moveRow(RowPB fromRow,
+      {RowPB? toRow, String? groupId}) {
+    return _databaseBackendSvc.moveRow(
+      fromRowId: fromRow.id,
+      toGroupId: groupId,
+      toRowId: toRow?.id,
+    );
+  }
+
+  Future<Either<Unit, FlowyError>> moveGroup(
+      {required String fromGroupId, required String toGroupId}) {
+    return _databaseBackendSvc.moveGroup(
+      fromGroupId: fromGroupId,
+      toGroupId: toGroupId,
+    );
+  }
+
+  Future<void> updateCalenderLayoutSetting(
+      CalendarLayoutSettingsPB layoutSetting) async {
+    await _databaseBackendSvc
+        .updateLayoutSetting(calendarLayoutSetting: layoutSetting)
+        .then((result) {
+      result.fold((l) => null, (r) => Log.error(r));
+    });
+  }
+
+  Future<void> dispose() async {
+    await _databaseBackendSvc.closeView();
+    await fieldController.dispose();
+    await groupListener.stop();
+  }
+
+  Future<void> _loadGroups() async {
+    final result = await _databaseBackendSvc.loadGroups();
+    return Future(
+      () => result.fold(
+        (groups) {
+          _groupCallbacks?.onGroupByField?.call(groups.items);
+        },
+        (err) => Log.error(err),
+      ),
+    );
+  }
+
+  Future<void> _loadLayoutSetting() async {
+    _databaseBackendSvc.getLayoutSetting(layoutType).then((result) {
+      result.fold(
+        (l) {
+          _layoutCallbacks?.onLoadLayout(l);
+        },
+        (r) => Log.error(r),
+      );
+    });
+  }
+
+  void _listenOnRowsChanged() {
+    _viewCache.addListener(onRowsChanged: (reason) {
+      _databaseCallbacks?.onRowsChanged?.call(rowInfos, reason);
+    });
+  }
+
+  void _listenOnFieldsChanged() {
+    fieldController.addListener(
+      onReceiveFields: (fields) {
+        _databaseCallbacks?.onFieldsChanged?.call(UnmodifiableListView(fields));
+      },
+      onFilters: (filters) {
+        _databaseCallbacks?.onFiltersChanged?.call(filters);
+      },
+    );
+  }
+
+  void _listenOnGroupChanged() {
+    groupListener.start(
+      onNumOfGroupsChanged: (result) {
+        result.fold((changeset) {
+          if (changeset.updateGroups.isNotEmpty) {
+            _groupCallbacks?.onUpdateGroup?.call(changeset.updateGroups);
+          }
+
+          if (changeset.deletedGroups.isNotEmpty) {
+            _groupCallbacks?.onDeleteGroup?.call(changeset.deletedGroups);
+          }
+
+          for (final insertedGroup in changeset.insertedGroups) {
+            _groupCallbacks?.onInsertGroup?.call(insertedGroup);
+          }
+        }, (r) => Log.error(r));
+      },
+      onGroupByNewField: (result) {
+        result.fold((groups) {
+          _groupCallbacks?.onGroupByField?.call(groups);
+        }, (r) => Log.error(r));
+      },
+    );
+  }
+
+  void _listenOnLayoutChanged() {
+    layoutListener.start(onLayoutChanged: (result) {
+      result.fold((l) {
+        _layoutCallbacks?.onLayoutChanged(l);
+      }, (r) => Log.error(r));
+    });
+  }
+}
+
+class RowDataBuilder {
+  final _cellDataByFieldId = <String, String>{};
+
+  void insertText(FieldInfo fieldInfo, String text) {
+    assert(fieldInfo.fieldType == FieldType.RichText);
+    _cellDataByFieldId[fieldInfo.field.id] = text;
+  }
+
+  void insertNumber(FieldInfo fieldInfo, int num) {
+    assert(fieldInfo.fieldType == FieldType.Number);
+    _cellDataByFieldId[fieldInfo.field.id] = num.toString();
+  }
+
+  void insertDate(FieldInfo fieldInfo, DateTime date) {
+    assert(fieldInfo.fieldType == FieldType.DateTime);
+    final timestamp = (date.millisecondsSinceEpoch ~/ 1000);
+    _cellDataByFieldId[fieldInfo.field.id] = timestamp.toString();
+  }
+
+  Map<String, String> build() {
+    return _cellDataByFieldId;
+  }
+}

+ 67 - 11
frontend/appflowy_flutter/lib/plugins/database_view/application/database_service.dart

@@ -1,4 +1,7 @@
+import 'package:appflowy_backend/protobuf/flowy-database/calendar_entities.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-database/database_entities.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-database/group_changeset.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-database/setting_entities.pb.dart';
 import 'package:dartz/dartz.dart';
 import 'package:appflowy_backend/dispatch/dispatch.dart';
 import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
@@ -20,25 +23,56 @@ class DatabaseBackendService {
     return DatabaseEventGetDatabase(payload).send();
   }
 
-  Future<Either<RowPB, FlowyError>> createRow({Option<String>? startRowId}) {
+  Future<Either<RowPB, FlowyError>> createRow({
+    String? startRowId,
+    String? groupId,
+    Map<String, String>? cellDataByFieldId,
+  }) {
     var payload = CreateRowPayloadPB.create()..viewId = viewId;
-    startRowId?.fold(() => null, (id) => payload.startRowId = id);
+    if (startRowId != null) {
+      payload.startRowId = startRowId;
+    }
+
+    if (groupId != null) {
+      payload.groupId = groupId;
+    }
+
+    if (cellDataByFieldId != null && cellDataByFieldId.isNotEmpty) {
+      payload.data = RowDataPB(cellDataByFieldId: cellDataByFieldId);
+    }
+
     return DatabaseEventCreateRow(payload).send();
   }
 
-  Future<Either<RowPB, FlowyError>> createBoardCard(
-    String groupId,
-    String? startRowId,
-  ) {
-    CreateBoardCardPayloadPB payload = CreateBoardCardPayloadPB.create()
+  Future<Either<Unit, FlowyError>> moveRow({
+    required String fromRowId,
+    required String? toGroupId,
+    required String? toRowId,
+  }) {
+    var payload = MoveGroupRowPayloadPB.create()
       ..viewId = viewId
-      ..groupId = groupId;
+      ..fromRowId = fromRowId;
+    if (toGroupId != null) {
+      payload.toGroupId = toGroupId;
+    }
 
-    if (startRowId != null) {
-      payload.startRowId = startRowId;
+    if (toRowId != null) {
+      payload.toRowId = toRowId;
     }
 
-    return DatabaseEventCreateBoardCard(payload).send();
+    return DatabaseEventMoveGroupRow(payload).send();
+  }
+
+  Future<Either<Unit, FlowyError>> moveGroup({
+    required String fromGroupId,
+    required String toGroupId,
+  }) {
+    final payload = MoveGroupPayloadPB.create()
+      ..viewId = viewId
+      ..fromGroupId = fromGroupId
+      ..toGroupId = toGroupId;
+
+    return DatabaseEventMoveGroup(payload).send();
   }
 
   Future<Either<List<FieldPB>, FlowyError>> getFields(
@@ -53,6 +87,28 @@ class DatabaseBackendService {
     });
   }
 
+  Future<Either<LayoutSettingPB, FlowyError>> getLayoutSetting(
+      LayoutTypePB layoutType) {
+    final payload = DatabaseLayoutIdPB.create()
+      ..viewId = viewId
+      ..layout = layoutType;
+    return DatabaseEventGetLayoutSetting(payload).send();
+  }
+
+  Future<Either<Unit, FlowyError>> updateLayoutSetting(
+      {CalendarLayoutSettingsPB? calendarLayoutSetting}) {
+    final layoutSetting = LayoutSettingPB.create();
+    if (calendarLayoutSetting != null) {
+      layoutSetting.calendar = calendarLayoutSetting;
+    }
+
+    final payload = UpdateLayoutSettingPB.create()
+      ..viewId = viewId
+      ..layoutSetting = layoutSetting;
+
+    return DatabaseEventSetLayoutSetting(payload).send();
+  }
+
   Future<Either<Unit, FlowyError>> closeView() {
     final request = ViewIdPB(value: viewId);
     return FolderEventCloseView(request).send();

+ 15 - 15
frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_listener.dart → frontend/appflowy_flutter/lib/plugins/database_view/application/group/group_listener.dart

@@ -11,20 +11,20 @@ import 'package:appflowy_backend/protobuf/flowy-database/group_changeset.pb.dart
 typedef GroupUpdateValue = Either<GroupChangesetPB, FlowyError>;
 typedef GroupByNewFieldValue = Either<List<GroupPB>, FlowyError>;
 
-class BoardListener {
+class DatabaseGroupListener {
   final String viewId;
-  PublishNotifier<GroupUpdateValue>? _groupUpdateNotifier = PublishNotifier();
-  PublishNotifier<GroupByNewFieldValue>? _groupByNewFieldNotifier =
+  PublishNotifier<GroupUpdateValue>? _numOfGroupsNotifier = PublishNotifier();
+  PublishNotifier<GroupByNewFieldValue>? _groupByFieldNotifier =
       PublishNotifier();
   DatabaseNotificationListener? _listener;
-  BoardListener(this.viewId);
+  DatabaseGroupListener(this.viewId);
 
   void start({
-    required void Function(GroupUpdateValue) onBoardChanged,
+    required void Function(GroupUpdateValue) onNumOfGroupsChanged,
     required void Function(GroupByNewFieldValue) onGroupByNewField,
   }) {
-    _groupUpdateNotifier?.addPublishListener(onBoardChanged);
-    _groupByNewFieldNotifier?.addPublishListener(onGroupByNewField);
+    _numOfGroupsNotifier?.addPublishListener(onNumOfGroupsChanged);
+    _groupByFieldNotifier?.addPublishListener(onGroupByNewField);
     _listener = DatabaseNotificationListener(
       objectId: viewId,
       handler: _handler,
@@ -38,16 +38,16 @@ class BoardListener {
     switch (ty) {
       case DatabaseNotification.DidUpdateGroups:
         result.fold(
-          (payload) => _groupUpdateNotifier?.value =
+          (payload) => _numOfGroupsNotifier?.value =
               left(GroupChangesetPB.fromBuffer(payload)),
-          (error) => _groupUpdateNotifier?.value = right(error),
+          (error) => _numOfGroupsNotifier?.value = right(error),
         );
         break;
       case DatabaseNotification.DidGroupByField:
         result.fold(
-          (payload) => _groupByNewFieldNotifier?.value =
+          (payload) => _groupByFieldNotifier?.value =
               left(GroupChangesetPB.fromBuffer(payload).initialGroups),
-          (error) => _groupByNewFieldNotifier?.value = right(error),
+          (error) => _groupByFieldNotifier?.value = right(error),
         );
         break;
       default:
@@ -57,10 +57,10 @@ class BoardListener {
 
   Future<void> stop() async {
     await _listener?.stop();
-    _groupUpdateNotifier?.dispose();
-    _groupUpdateNotifier = null;
+    _numOfGroupsNotifier?.dispose();
+    _numOfGroupsNotifier = null;
 
-    _groupByNewFieldNotifier?.dispose();
-    _groupByNewFieldNotifier = null;
+    _groupByFieldNotifier?.dispose();
+    _groupByFieldNotifier = null;
   }
 }

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

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

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

@@ -61,7 +61,7 @@ class RowCache {
     });
   }
 
-  void initializeRows(List<RowPB> rows) {
+  void setInitialRows(List<RowPB> rows) {
     for (final row in rows) {
       final rowInfo = buildGridRow(row);
       _rowList.add(rowInfo);

+ 6 - 4
frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_data_controller.dart

@@ -5,24 +5,26 @@ import 'row_cache.dart';
 typedef OnRowChanged = void Function(CellByFieldId, RowsChangedReason);
 
 class RowDataController {
-  final RowInfo rowInfo;
+  final String rowId;
+  final String viewId;
   final List<VoidCallback> _onRowChangedListeners = [];
   final RowCache _rowCache;
 
   get cellCache => _rowCache.cellCache;
 
   RowDataController({
-    required this.rowInfo,
+    required this.rowId,
+    required this.viewId,
     required RowCache rowCache,
   }) : _rowCache = rowCache;
 
   CellByFieldId loadData() {
-    return _rowCache.loadGridCells(rowInfo.rowPB.id);
+    return _rowCache.loadGridCells(rowId);
   }
 
   void addListener({OnRowChanged? onRowChanged}) {
     _onRowChangedListeners.add(_rowCache.addListener(
-      rowId: rowInfo.rowPB.id,
+      rowId: rowId,
       onCellUpdated: onRowChanged,
     ));
   }

+ 0 - 51
frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_service.dart

@@ -1,8 +1,6 @@
-import 'package:appflowy_backend/protobuf/flowy-database/database_entities.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-database/group_changeset.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-database/row_entities.pb.dart';
 
 class RowBackendService {
@@ -44,52 +42,3 @@ class RowBackendService {
     return DatabaseEventDuplicateRow(payload).send();
   }
 }
-
-class GroupBackendService {
-  final String viewId;
-
-  GroupBackendService({
-    required this.viewId,
-  });
-
-  Future<Either<Unit, FlowyError>> moveRow({
-    required String fromRowId,
-    required String toRowId,
-  }) {
-    var payload = MoveRowPayloadPB.create()
-      ..viewId = viewId
-      ..fromRowId = fromRowId
-      ..toRowId = toRowId;
-
-    return DatabaseEventMoveRow(payload).send();
-  }
-
-  Future<Either<Unit, FlowyError>> moveGroupRow({
-    required String fromRowId,
-    required String toGroupId,
-    required String? toRowId,
-  }) {
-    var payload = MoveGroupRowPayloadPB.create()
-      ..viewId = viewId
-      ..fromRowId = fromRowId
-      ..toGroupId = toGroupId;
-
-    if (toRowId != null) {
-      payload.toRowId = toRowId;
-    }
-
-    return DatabaseEventMoveGroupRow(payload).send();
-  }
-
-  Future<Either<Unit, FlowyError>> moveGroup({
-    required String fromGroupId,
-    required String toGroupId,
-  }) {
-    final payload = MoveGroupPayloadPB.create()
-      ..viewId = viewId
-      ..fromGroupId = fromGroupId
-      ..toGroupId = toGroupId;
-
-    return DatabaseEventMoveGroup(payload).send();
-  }
-}

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

@@ -13,25 +13,25 @@ import 'package:freezed_annotation/freezed_annotation.dart';
 
 import '../../application/field/field_controller.dart';
 import '../../application/row/row_cache.dart';
-import '../../application/row/row_service.dart';
-import 'board_data_controller.dart';
+import '../../application/database_controller.dart';
 import 'group_controller.dart';
 
 part 'board_bloc.freezed.dart';
 
 class BoardBloc extends Bloc<BoardEvent, BoardState> {
-  final BoardDataController _boardDataController;
+  final DatabaseController _databaseController;
   late final AppFlowyBoardController boardController;
-  final GroupBackendService _groupBackendSvc;
   final LinkedHashMap<String, GroupController> groupControllers =
       LinkedHashMap();
 
-  FieldController get fieldController => _boardDataController.fieldController;
-  String get viewId => _boardDataController.viewId;
+  FieldController get fieldController => _databaseController.fieldController;
+  String get viewId => _databaseController.viewId;
 
   BoardBloc({required ViewPB view})
-      : _groupBackendSvc = GroupBackendService(viewId: view.id),
-        _boardDataController = BoardDataController(view: view),
+      : _databaseController = DatabaseController(
+          view: view,
+          layoutType: LayoutTypePB.Board,
+        ),
         super(BoardState.initial(view.id)) {
     boardController = AppFlowyBoardController(
       onMoveGroup: (
@@ -40,7 +40,10 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
         toGroupId,
         toIndex,
       ) {
-        _moveGroup(fromGroupId, toGroupId);
+        _databaseController.moveGroup(
+          fromGroupId: fromGroupId,
+          toGroupId: toGroupId,
+        );
       },
       onMoveGroupItem: (
         groupId,
@@ -49,7 +52,13 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
       ) {
         final fromRow = groupControllers[groupId]?.rowAtIndex(fromIndex);
         final toRow = groupControllers[groupId]?.rowAtIndex(toIndex);
-        _moveRow(fromRow, groupId, toRow);
+        if (fromRow != null) {
+          _databaseController.moveRow(
+            fromRow,
+            toRow: toRow,
+            groupId: groupId,
+          );
+        }
       },
       onMoveGroupItemToGroup: (
         fromGroupId,
@@ -59,7 +68,13 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
       ) {
         final fromRow = groupControllers[fromGroupId]?.rowAtIndex(fromIndex);
         final toRow = groupControllers[toGroupId]?.rowAtIndex(toIndex);
-        _moveRow(fromRow, toGroupId, toRow);
+        if (fromRow != null) {
+          _databaseController.moveRow(
+            fromRow,
+            toRow: toRow,
+            groupId: toGroupId,
+          );
+        }
       },
     );
 
@@ -72,8 +87,8 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
           },
           createBottomRow: (groupId) async {
             final startRowId = groupControllers[groupId]?.lastRow()?.id;
-            final result = await _boardDataController.createBoardCard(
-              groupId,
+            final result = await _databaseController.createRow(
+              groupId: groupId,
               startRowId: startRowId,
             );
             result.fold(
@@ -82,7 +97,8 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
             );
           },
           createHeaderRow: (String groupId) async {
-            final result = await _boardDataController.createBoardCard(groupId);
+            final result =
+                await _databaseController.createRow(groupId: groupId);
             result.fold(
               (_) {},
               (err) => Log.error(err),
@@ -141,44 +157,11 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
     }
 
     boardController.enableGroupDragging(!isEdit);
-    // boardController.updateGroupItem(
-    //   group.groupId,
-    //   GroupItem(
-    //     row: row,
-    //     fieldInfo: fieldInfo,
-    //     isDraggable: !isEdit,
-    //   ),
-    // );
-  }
-
-  void _moveRow(RowPB? fromRow, String columnId, RowPB? toRow) {
-    if (fromRow != null) {
-      _groupBackendSvc
-          .moveGroupRow(
-        fromRowId: fromRow.id,
-        toGroupId: columnId,
-        toRowId: toRow?.id,
-      )
-          .then((result) {
-        result.fold((l) => null, (r) => add(BoardEvent.didReceiveError(r)));
-      });
-    }
-  }
-
-  void _moveGroup(String fromGroupId, String toGroupId) {
-    _groupBackendSvc
-        .moveGroup(
-      fromGroupId: fromGroupId,
-      toGroupId: toGroupId,
-    )
-        .then((result) {
-      result.fold((l) => null, (r) => add(BoardEvent.didReceiveError(r)));
-    });
   }
 
   @override
   Future<void> close() async {
-    await _boardDataController.dispose();
+    await _databaseController.dispose();
     for (final controller in groupControllers.values) {
       controller.dispose();
     }
@@ -204,34 +187,36 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
   }
 
   RowCache? getRowCache(String blockId) {
-    return _boardDataController.rowCache;
+    return _databaseController.rowCache;
   }
 
   void _startListening() {
-    _boardDataController.addListener(
-      onDatabaseChanged: (grid) {
+    final onDatabaseChanged = DatabaseCallbacks(
+      onDatabaseChanged: (database) {
         if (!isClosed) {
-          add(BoardEvent.didReceiveGridUpdate(grid));
+          add(BoardEvent.didReceiveGridUpdate(database));
         }
       },
-      didLoadGroups: (groups) {
+    );
+    final onGroupChanged = GroupCallbacks(
+      onGroupByField: (groups) {
         if (isClosed) return;
         initializeGroups(groups);
         add(BoardEvent.didReceiveGroups(groups));
       },
-      onDeletedGroup: (groupIds) {
+      onDeleteGroup: (groupIds) {
         if (isClosed) return;
         boardController.removeGroups(groupIds);
       },
-      onInsertedGroup: (insertedGroup) {
+      onInsertGroup: (insertGroups) {
         if (isClosed) return;
-        final group = insertedGroup.group;
+        final group = insertGroups.group;
         final newGroup = initializeGroupData(group);
         final controller = initializeGroupController(group);
         groupControllers[controller.group.groupId] = (controller);
         boardController.addGroup(newGroup);
       },
-      onUpdatedGroup: (updatedGroups) {
+      onUpdateGroup: (updatedGroups) {
         if (isClosed) return;
         for (final group in updatedGroups) {
           final columnController =
@@ -239,15 +224,11 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
           columnController?.updateGroupName(group.desc);
         }
       },
-      onError: (err) {
-        Log.error(err);
-      },
-      onResetGroups: (groups) {
-        if (isClosed) return;
+    );
 
-        initializeGroups(groups);
-        add(BoardEvent.didReceiveGroups(groups));
-      },
+    _databaseController.addListener(
+      onDatabaseChanged: onDatabaseChanged,
+      onGroupChanged: onGroupChanged,
     );
   }
 
@@ -264,7 +245,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
   }
 
   Future<void> _openGrid(Emitter<BoardState> emit) async {
-    final result = await _boardDataController.openGrid();
+    final result = await _databaseController.open();
     result.fold(
       (grid) => emit(
         state.copyWith(loadingState: GridLoadingState.finish(left(unit))),

+ 0 - 144
frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_data_controller.dart

@@ -1,144 +0,0 @@
-import 'dart:collection';
-
-import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
-import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
-import 'dart:async';
-import 'package:dartz/dartz.dart';
-import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart';
-
-import '../../application/database_service.dart';
-import '../../application/defines.dart';
-import '../../application/field/field_controller.dart';
-import '../../application/row/row_cache.dart';
-import '../../application/view/view_cache.dart';
-import 'board_listener.dart';
-
-typedef DidLoadGroups = void Function(List<GroupPB>);
-typedef OnUpdatedGroup = void Function(List<GroupPB>);
-typedef OnDeletedGroup = void Function(List<String>);
-typedef OnInsertedGroup = void Function(InsertedGroupPB);
-typedef OnResetGroups = void Function(List<GroupPB>);
-
-class BoardDataController {
-  final String viewId;
-  final DatabaseBackendService _databaseSvc;
-  final FieldController fieldController;
-  final BoardListener _listener;
-  late DatabaseViewCache _viewCache;
-
-  OnFieldsChanged? _onFieldsChanged;
-  OnDatabaseChanged? _onDatabaseChanged;
-  DidLoadGroups? _didLoadGroup;
-  OnRowsChanged? _onRowsChanged;
-  OnError? _onError;
-
-  List<RowInfo> get rowInfos => _viewCache.rowInfos;
-  RowCache get rowCache => _viewCache.rowCache;
-
-  BoardDataController({required ViewPB view})
-      : viewId = view.id,
-        _listener = BoardListener(view.id),
-        _databaseSvc = DatabaseBackendService(viewId: view.id),
-        fieldController = FieldController(viewId: view.id) {
-    //
-    _viewCache = DatabaseViewCache(
-      viewId: view.id,
-      fieldController: fieldController,
-    );
-    _viewCache.addListener(onRowsChanged: (reason) {
-      _onRowsChanged?.call(rowInfos, reason);
-    });
-  }
-
-  void addListener({
-    required OnDatabaseChanged onDatabaseChanged,
-    OnFieldsChanged? onFieldsChanged,
-    required DidLoadGroups didLoadGroups,
-    OnRowsChanged? onRowsChanged,
-    required OnUpdatedGroup onUpdatedGroup,
-    required OnDeletedGroup onDeletedGroup,
-    required OnInsertedGroup onInsertedGroup,
-    required OnResetGroups onResetGroups,
-    required OnError? onError,
-  }) {
-    _onDatabaseChanged = onDatabaseChanged;
-    _onFieldsChanged = onFieldsChanged;
-    _didLoadGroup = didLoadGroups;
-    _onRowsChanged = onRowsChanged;
-    _onError = onError;
-
-    fieldController.addListener(onReceiveFields: (fields) {
-      _onFieldsChanged?.call(UnmodifiableListView(fields));
-    });
-
-    _listener.start(
-      onBoardChanged: (result) {
-        result.fold(
-          (changeset) {
-            if (changeset.updateGroups.isNotEmpty) {
-              onUpdatedGroup.call(changeset.updateGroups);
-            }
-
-            if (changeset.deletedGroups.isNotEmpty) {
-              onDeletedGroup.call(changeset.deletedGroups);
-            }
-
-            for (final insertedGroup in changeset.insertedGroups) {
-              onInsertedGroup.call(insertedGroup);
-            }
-          },
-          (e) => _onError?.call(e),
-        );
-      },
-      onGroupByNewField: (result) {
-        result.fold(
-          (groups) => onResetGroups(groups),
-          (e) => _onError?.call(e),
-        );
-      },
-    );
-  }
-
-  Future<Either<Unit, FlowyError>> openGrid() async {
-    final result = await _databaseSvc.openGrid();
-    return result.fold(
-      (grid) async {
-        _onDatabaseChanged?.call(grid);
-        return fieldController.loadFields(fieldIds: grid.fields).then((result) {
-          return result.fold(
-            (l) => Future(() async {
-              await _loadGroups();
-              _viewCache.rowCache.initializeRows(grid.rows);
-              return left(l);
-            }),
-            (err) => right(err),
-          );
-        });
-      },
-      (err) => right(err),
-    );
-  }
-
-  Future<Either<RowPB, FlowyError>> createBoardCard(String groupId,
-      {String? startRowId}) {
-    return _databaseSvc.createBoardCard(groupId, startRowId);
-  }
-
-  Future<void> dispose() async {
-    await _viewCache.dispose();
-    await _databaseSvc.closeView();
-    await fieldController.dispose();
-  }
-
-  Future<void> _loadGroups() async {
-    final result = await _databaseSvc.loadGroups();
-    return Future(
-      () => result.fold(
-        (groups) {
-          _didLoadGroup?.call(groups.items);
-        },
-        (err) => _onError?.call(err),
-      ),
-    );
-  }
-}

+ 0 - 39
frontend/appflowy_flutter/lib/plugins/database_view/board/application/card/card_data_controller.dart

@@ -1,39 +0,0 @@
-import 'package:appflowy_backend/protobuf/flowy-database/row_entities.pb.dart';
-import 'package:flutter/foundation.dart';
-
-import '../../../application/cell/cell_service.dart';
-import '../../../application/row/row_cache.dart';
-import '../../presentation/card/card_cell_builder.dart';
-
-typedef OnCardChanged = void Function(CellByFieldId, RowsChangedReason);
-
-class CardDataController extends BoardCellBuilderDelegate {
-  final RowPB rowPB;
-  final RowCache _rowCache;
-  final List<VoidCallback> _onCardChangedListeners = [];
-
-  CardDataController({
-    required this.rowPB,
-    required RowCache rowCache,
-  }) : _rowCache = rowCache;
-
-  CellByFieldId loadData() {
-    return _rowCache.loadGridCells(rowPB.id);
-  }
-
-  void addListener({OnCardChanged? onRowChanged}) {
-    _onCardChangedListeners.add(_rowCache.addListener(
-      rowId: rowPB.id,
-      onCellUpdated: onRowChanged,
-    ));
-  }
-
-  void dispose() {
-    for (final fn in _onCardChangedListeners) {
-      _rowCache.removeRowListener(fn);
-    }
-  }
-
-  @override
-  CellCache get cellCache => _rowCache.cellCache;
-}

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

@@ -1,7 +1,11 @@
 import 'package:appflowy_backend/log.dart';
 import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart';
-import 'group_listener.dart';
+import 'dart:typed_data';
+
+import 'package:appflowy/core/grid_notification.dart';
+import 'package:flowy_infra/notifier.dart';
+import 'package:dartz/dartz.dart';
 
 typedef OnGroupError = void Function(FlowyError);
 
@@ -14,14 +18,14 @@ abstract class GroupControllerDelegate {
 
 class GroupController {
   final GroupPB group;
-  final GroupListener _listener;
+  final SingleGroupListener _listener;
   final GroupControllerDelegate delegate;
 
   GroupController({
     required String viewId,
     required this.group,
     required this.delegate,
-  }) : _listener = GroupListener(group);
+  }) : _listener = SingleGroupListener(group);
 
   RowPB? rowAtIndex(int index) {
     if (index < group.rows.length) {
@@ -81,3 +85,45 @@ class GroupController {
     _listener.stop();
   }
 }
+
+typedef UpdateGroupNotifiedValue = Either<GroupRowsNotificationPB, FlowyError>;
+
+class SingleGroupListener {
+  final GroupPB group;
+  PublishNotifier<UpdateGroupNotifiedValue>? _groupNotifier = PublishNotifier();
+  DatabaseNotificationListener? _listener;
+  SingleGroupListener(this.group);
+
+  void start({
+    required void Function(UpdateGroupNotifiedValue) onGroupChanged,
+  }) {
+    _groupNotifier?.addPublishListener(onGroupChanged);
+    _listener = DatabaseNotificationListener(
+      objectId: group.groupId,
+      handler: _handler,
+    );
+  }
+
+  void _handler(
+    DatabaseNotification ty,
+    Either<Uint8List, FlowyError> result,
+  ) {
+    switch (ty) {
+      case DatabaseNotification.DidUpdateGroupRow:
+        result.fold(
+          (payload) => _groupNotifier?.value =
+              left(GroupRowsNotificationPB.fromBuffer(payload)),
+          (error) => _groupNotifier?.value = right(error),
+        );
+        break;
+      default:
+        break;
+    }
+  }
+
+  Future<void> stop() async {
+    await _listener?.stop();
+    _groupNotifier?.dispose();
+    _groupNotifier = null;
+  }
+}

+ 0 - 51
frontend/appflowy_flutter/lib/plugins/database_view/board/application/group_listener.dart

@@ -1,51 +0,0 @@
-import 'dart:typed_data';
-
-import 'package:appflowy/core/grid_notification.dart';
-import 'package:flowy_infra/notifier.dart';
-import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
-import 'package:appflowy_backend/protobuf/flowy-database/notification.pb.dart';
-import 'package:appflowy_backend/protobuf/flowy-database/group.pb.dart';
-import 'package:dartz/dartz.dart';
-import 'package:appflowy_backend/protobuf/flowy-database/group_changeset.pb.dart';
-
-typedef UpdateGroupNotifiedValue = Either<GroupRowsNotificationPB, FlowyError>;
-
-class GroupListener {
-  final GroupPB group;
-  PublishNotifier<UpdateGroupNotifiedValue>? _groupNotifier = PublishNotifier();
-  DatabaseNotificationListener? _listener;
-  GroupListener(this.group);
-
-  void start({
-    required void Function(UpdateGroupNotifiedValue) onGroupChanged,
-  }) {
-    _groupNotifier?.addPublishListener(onGroupChanged);
-    _listener = DatabaseNotificationListener(
-      objectId: group.groupId,
-      handler: _handler,
-    );
-  }
-
-  void _handler(
-    DatabaseNotification ty,
-    Either<Uint8List, FlowyError> result,
-  ) {
-    switch (ty) {
-      case DatabaseNotification.DidUpdateGroupRow:
-        result.fold(
-          (payload) => _groupNotifier?.value =
-              left(GroupRowsNotificationPB.fromBuffer(payload)),
-          (error) => _groupNotifier?.value = right(error),
-        );
-        break;
-      default:
-        break;
-    }
-  }
-
-  Future<void> stop() async {
-    await _listener?.stop();
-    _groupNotifier?.dispose();
-    _groupNotifier = null;
-  }
-}

+ 24 - 14
frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart

@@ -17,13 +17,13 @@ import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flowy_infra_ui/widget/error_page.dart';
-import 'package:flutter/material.dart';
+import 'package:flutter/material.dart' hide Card;
 import 'package:flutter_bloc/flutter_bloc.dart';
 
+import '../../widgets/card/cells/card_cell.dart';
+import '../../widgets/card/card_cell_builder.dart';
 import '../application/board_bloc.dart';
-import '../application/card/card_data_controller.dart';
-import 'card/card.dart';
-import 'card/card_cell_builder.dart';
+import '../../widgets/card/card.dart';
 import 'toolbar/board_toolbar.dart';
 
 class BoardPage extends StatelessWidget {
@@ -78,6 +78,7 @@ class BoardContent extends StatefulWidget {
 
 class _BoardContentState extends State<BoardContent> {
   late AppFlowyBoardScrollController scrollManager;
+  final cardConfiguration = CardConfiguration<String>();
 
   final config = AppFlowyBoardConfig(
     groupBackgroundColor: HexColor.fromHex('#F7F8FC'),
@@ -86,6 +87,16 @@ class _BoardContentState extends State<BoardContent> {
   @override
   void initState() {
     scrollManager = AppFlowyBoardScrollController();
+    cardConfiguration.addSelectOptionHook((options, groupId) {
+      // The cell should hide if the option id is equal to the groupId.
+      final isInGroup =
+          options.where((element) => element.id == groupId).isNotEmpty;
+      if (isInGroup || options.isEmpty) {
+        return const SizedBox();
+      }
+      return null;
+    });
+
     super.initState();
   }
 
@@ -225,15 +236,11 @@ class _BoardContentState extends State<BoardContent> {
 
     /// Return placeholder widget if the rowCache is null.
     if (rowCache == null) return SizedBox(key: ObjectKey(groupItem));
-
+    final cellCache = rowCache.cellCache;
     final fieldController = context.read<BoardBloc>().fieldController;
     final viewId = context.read<BoardBloc>().viewId;
-    final cardController = CardDataController(
-      rowCache: rowCache,
-      rowPB: rowPB,
-    );
 
-    final cellBuilder = BoardCellBuilder(cardController);
+    final cellBuilder = CardCellBuilder<String>(cellCache);
     bool isEditing = false;
     context.read<BoardBloc>().state.editingRow.fold(
       () => null,
@@ -247,13 +254,15 @@ class _BoardContentState extends State<BoardContent> {
       key: ValueKey(groupItemId),
       margin: config.cardPadding,
       decoration: _makeBoxDecoration(context),
-      child: BoardCard(
+      child: Card<String>(
+        row: rowPB,
         viewId: viewId,
-        groupId: groupData.group.groupId,
+        rowCache: rowCache,
+        cardData: groupData.group.groupId,
         fieldId: groupItem.fieldInfo.id,
         isEditing: isEditing,
         cellBuilder: cellBuilder,
-        dataController: cardController,
+        configuration: cardConfiguration,
         openCard: (context) => _openCard(
           viewId,
           fieldController,
@@ -304,7 +313,8 @@ class _BoardContentState extends State<BoardContent> {
     );
 
     final dataController = RowDataController(
-      rowInfo: rowInfo,
+      rowId: rowInfo.rowPB.id,
+      viewId: rowInfo.viewId,
       rowCache: rowCache,
     );
 

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

@@ -1,5 +1,6 @@
+import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
 import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
-import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
+import 'package:appflowy_backend/dispatch/dispatch.dart';
 import 'package:appflowy_backend/log.dart';
 import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
@@ -9,20 +10,24 @@ import 'package:dartz/dartz.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 
-import 'calendar_data_controller.dart';
+import '../../application/database_controller.dart';
+import '../../application/row/row_cache.dart';
 
 part 'calendar_bloc.freezed.dart';
 
 class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
-  final CalendarDataController _databaseDataController;
-  final EventController calendarEventsController = EventController();
+  final DatabaseController _databaseController;
 
-  FieldController get fieldController =>
-      _databaseDataController.fieldController;
-  String get databaseId => _databaseDataController.databaseId;
+  // Getters
+  String get viewId => _databaseController.viewId;
+  CellCache get cellCache => _databaseController.rowCache.cellCache;
+  RowCache get rowCache => _databaseController.rowCache;
 
   CalendarBloc({required ViewPB view})
-      : _databaseDataController = CalendarDataController(view: view),
+      : _databaseController = DatabaseController(
+          view: view,
+          layoutType: LayoutTypePB.Calendar,
+        ),
         super(CalendarState.initial(view.id)) {
     on<CalendarEvent>(
       (event, emit) async {
@@ -30,23 +35,57 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
           initial: () async {
             _startListening();
             await _openDatabase(emit);
+            _loadAllEvents();
           },
-          didReceiveCalendarSettings: (CalendarSettingsPB settings) {
+          didReceiveCalendarSettings: (CalendarLayoutSettingsPB settings) {
             emit(state.copyWith(settings: Some(settings)));
           },
           didReceiveDatabaseUpdate: (DatabasePB database) {
             emit(state.copyWith(database: Some(database)));
           },
-          didReceiveError: (FlowyError error) {
-            emit(state.copyWith(noneOrError: Some(error)));
+          didLoadAllEvents: (events) {
+            emit(state.copyWith(events: events));
+          },
+          createEvent: (DateTime date, String title) async {
+            await _createEvent(date, title);
+          },
+          didReceiveEvent: (CalendarEventData<CalendarCardData> newEvent) {
+            emit(state.copyWith(events: [...state.events, newEvent]));
+          },
+          didUpdateFieldInfos: (Map<String, FieldInfo> fieldInfoByFieldId) {
+            emit(state.copyWith(fieldInfoByFieldId: fieldInfoByFieldId));
           },
         );
       },
     );
   }
 
+  FieldInfo? _getCalendarFieldInfo(String fieldId) {
+    final fieldInfos = _databaseController.fieldController.fieldInfos;
+    final index = fieldInfos.indexWhere(
+      (element) => element.field.id == fieldId,
+    );
+    if (index != -1) {
+      return fieldInfos[index];
+    } else {
+      return null;
+    }
+  }
+
+  FieldInfo? _getTitleFieldInfo() {
+    final fieldInfos = _databaseController.fieldController.fieldInfos;
+    final index = fieldInfos.indexWhere(
+      (element) => element.field.isPrimary,
+    );
+    if (index != -1) {
+      return fieldInfos[index];
+    } else {
+      return null;
+    }
+  }
+
   Future<void> _openDatabase(Emitter<CalendarState> emit) async {
-    final result = await _databaseDataController.openDatabase();
+    final result = await _databaseController.open();
     result.fold(
       (database) => emit(
         state.copyWith(loadingState: DatabaseLoadingState.finish(left(unit))),
@@ -57,60 +96,144 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
     );
   }
 
-  RowCache? getRowCache(String blockId) {
-    return _databaseDataController.rowCache;
+  Future<void> _createEvent(DateTime date, String title) async {
+    state.settings.fold(
+      () => null,
+      (settings) async {
+        final dateField = _getCalendarFieldInfo(settings.layoutFieldId);
+        final titleField = _getTitleFieldInfo();
+        if (dateField != null && titleField != null) {
+          final result = await _databaseController.createRow(
+            withCells: (builder) {
+              builder.insertDate(dateField, date);
+              builder.insertText(titleField, title);
+            },
+          );
+
+          result.fold(
+            (newRow) => _loadEvent(newRow.id),
+            (err) => Log.error(err),
+          );
+        }
+      },
+    );
+  }
+
+  Future<void> _loadEvent(String rowId) async {
+    final payload = RowIdPB(viewId: viewId, rowId: rowId);
+    DatabaseEventGetCalendarEvent(payload).send().then((result) {
+      result.fold(
+        (eventPB) {
+          final calendarEvent = _calendarEventDataFromEventPB(eventPB);
+          if (calendarEvent != null) {
+            add(CalendarEvent.didReceiveEvent(calendarEvent));
+          }
+        },
+        (r) => Log.error(r),
+      );
+    });
+  }
+
+  Future<void> _loadAllEvents() async {
+    final payload = CalendarEventRequestPB.create()..viewId = viewId;
+    DatabaseEventGetAllCalendarEvents(payload).send().then((result) {
+      result.fold(
+        (events) {
+          if (!isClosed) {
+            final calendarEvents = <CalendarEventData<CalendarCardData>>[];
+            for (final eventPB in events.items) {
+              final calendarEvent = _calendarEventDataFromEventPB(eventPB);
+              if (calendarEvent != null) {
+                calendarEvents.add(calendarEvent);
+              }
+            }
+
+            add(CalendarEvent.didLoadAllEvents(calendarEvents));
+          }
+        },
+        (r) => Log.error(r),
+      );
+    });
+  }
+
+  CalendarEventData<CalendarCardData>? _calendarEventDataFromEventPB(
+      CalendarEventPB eventPB) {
+    final fieldInfo = state.fieldInfoByFieldId[eventPB.titleFieldId];
+    if (fieldInfo != null) {
+      final cellId = CellIdentifier(
+        viewId: viewId,
+        rowId: eventPB.rowId,
+        fieldInfo: fieldInfo,
+      );
+
+      final eventData = CalendarCardData(
+        event: eventPB,
+        cellId: cellId,
+      );
+
+      final date = DateTime.fromMillisecondsSinceEpoch(
+        eventPB.timestamp.toInt() * 1000,
+      );
+      return CalendarEventData(
+        title: eventPB.title,
+        date: date,
+        event: eventData,
+      );
+    } else {
+      return null;
+    }
   }
 
   void _startListening() {
-    _databaseDataController.addListener(
+    final onDatabaseChanged = DatabaseCallbacks(
       onDatabaseChanged: (database) {
-        if (!isClosed) return;
-
-        add(CalendarEvent.didReceiveDatabaseUpdate(database));
-      },
-      onSettingsChanged: (CalendarSettingsPB settings) {
         if (isClosed) return;
-        add(CalendarEvent.didReceiveCalendarSettings(settings));
       },
-      onArrangeWithNewField: (field) {
+      onFieldsChanged: (fieldInfos) {
         if (isClosed) return;
-        _initializeEvents(field);
-        // add(CalendarEvent.)
-      },
-      onError: (err) {
-        Log.error(err);
+        final fieldInfoByFieldId = {
+          for (var fieldInfo in fieldInfos) fieldInfo.field.id: fieldInfo
+        };
+        add(CalendarEvent.didUpdateFieldInfos(fieldInfoByFieldId));
       },
     );
-  }
-
-  void _initializeEvents(FieldPB dateField) {
-    calendarEventsController.removeWhere((element) => true);
-
-    const events = <CalendarEventData<CalendarData>>[];
 
-    // final List<CalendarEventData<CalendarData>> events = rows.map((row) {
-    // final event = CalendarEventData(
-    //   title: "",
-    //   date: row -> dateField -> value,
-    //   event: row,
-    // );
+    final onLayoutChanged = LayoutCallbacks(
+      onLayoutChanged: _didReceiveLayoutSetting,
+      onLoadLayout: _didReceiveLayoutSetting,
+    );
 
-    // return event;
-    // }).toList();
+    _databaseController.addListener(
+      onDatabaseChanged: onDatabaseChanged,
+      onLayoutChanged: onLayoutChanged,
+    );
+  }
 
-    calendarEventsController.addAll(events);
+  void _didReceiveLayoutSetting(LayoutSettingPB layoutSetting) {
+    if (layoutSetting.hasCalendar()) {
+      if (isClosed) return;
+      add(CalendarEvent.didReceiveCalendarSettings(layoutSetting.calendar));
+    }
   }
 }
 
+typedef Events = List<CalendarEventData<CalendarCardData>>;
+
 @freezed
 class CalendarEvent with _$CalendarEvent {
   const factory CalendarEvent.initial() = _InitialCalendar;
   const factory CalendarEvent.didReceiveCalendarSettings(
-      CalendarSettingsPB settings) = _DidReceiveCalendarSettings;
-  const factory CalendarEvent.didReceiveError(FlowyError error) =
-      _DidReceiveError;
+      CalendarLayoutSettingsPB settings) = _ReceiveCalendarSettings;
+  const factory CalendarEvent.didLoadAllEvents(Events events) =
+      _ReceiveCalendarEvents;
+  const factory CalendarEvent.didReceiveEvent(
+      CalendarEventData<CalendarCardData> event) = _ReceiveEvent;
+  const factory CalendarEvent.didUpdateFieldInfos(
+      Map<String, FieldInfo> fieldInfoByFieldId) = _DidUpdateFieldInfos;
+  const factory CalendarEvent.createEvent(DateTime date, String title) =
+      _CreateEvent;
   const factory CalendarEvent.didReceiveDatabaseUpdate(DatabasePB database) =
-      _DidReceiveDatabaseUpdate;
+      _ReceiveDatabaseUpdate;
 }
 
 @freezed
@@ -118,9 +241,9 @@ class CalendarState with _$CalendarState {
   const factory CalendarState({
     required String databaseId,
     required Option<DatabasePB> database,
-    required Option<FieldPB> dateField,
-    required Option<List<RowInfo>> unscheduledRows,
-    required Option<CalendarSettingsPB> settings,
+    required Events events,
+    required Map<String, FieldInfo> fieldInfoByFieldId,
+    required Option<CalendarLayoutSettingsPB> settings,
     required DatabaseLoadingState loadingState,
     required Option<FlowyError> noneOrError,
   }) = _CalendarState;
@@ -128,8 +251,8 @@ class CalendarState with _$CalendarState {
   factory CalendarState.initial(String databaseId) => CalendarState(
         database: none(),
         databaseId: databaseId,
-        dateField: none(),
-        unscheduledRows: none(),
+        fieldInfoByFieldId: {},
+        events: [],
         settings: none(),
         noneOrError: none(),
         loadingState: const _Loading(),
@@ -153,7 +276,8 @@ class CalendarEditingRow {
   });
 }
 
-class CalendarData {
-  final RowInfo rowInfo;
-  CalendarData(this.rowInfo);
+class CalendarCardData {
+  final CalendarEventPB event;
+  final CellIdentifier cellId;
+  CalendarCardData({required this.cellId, required this.event});
 }

+ 0 - 115
frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_data_controller.dart

@@ -1,115 +0,0 @@
-import 'dart:async';
-import 'dart:collection';
-
-import 'package:appflowy/plugins/database_view/application/database_service.dart';
-import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
-import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
-import 'package:appflowy/plugins/database_view/application/view/view_cache.dart';
-import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
-import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
-import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart';
-import 'package:dartz/dartz.dart';
-
-import 'calendar_listener.dart';
-
-typedef OnFieldsChanged = void Function(UnmodifiableListView<FieldInfo>);
-typedef OnDatabaseChanged = void Function(DatabasePB);
-typedef OnSettingsChanged = void Function(CalendarSettingsPB);
-typedef OnArrangeWithNewField = void Function(FieldPB);
-
-typedef OnRowsChanged = void Function(List<RowInfo>, RowsChangedReason);
-typedef OnError = void Function(FlowyError);
-
-class CalendarDataController {
-  final String databaseId;
-  final DatabaseBackendService _databaseBackendSvc;
-  final FieldController fieldController;
-  final CalendarListener _listener;
-  late DatabaseViewCache _viewCache;
-
-  OnFieldsChanged? _onFieldsChanged;
-  OnDatabaseChanged? _onDatabaseChanged;
-  OnRowsChanged? _onRowsChanged;
-  OnSettingsChanged? _onSettingsChanged;
-  OnArrangeWithNewField? _onArrangeWithNewField;
-  OnError? _onError;
-
-  List<RowInfo> get rowInfos => _viewCache.rowInfos;
-  RowCache get rowCache => _viewCache.rowCache;
-
-  CalendarDataController({required ViewPB view})
-      : databaseId = view.id,
-        _listener = CalendarListener(view.id),
-        _databaseBackendSvc = DatabaseBackendService(viewId: view.id),
-        fieldController = FieldController(viewId: view.id) {
-    _viewCache = DatabaseViewCache(
-      viewId: view.id,
-      fieldController: fieldController,
-    );
-    _viewCache.addListener(onRowsChanged: (reason) {
-      _onRowsChanged?.call(rowInfos, reason);
-    });
-  }
-
-  void addListener({
-    required OnDatabaseChanged onDatabaseChanged,
-    OnFieldsChanged? onFieldsChanged,
-    OnRowsChanged? onRowsChanged,
-    required OnSettingsChanged? onSettingsChanged,
-    required OnArrangeWithNewField? onArrangeWithNewField,
-    required OnError? onError,
-  }) {
-    _onDatabaseChanged = onDatabaseChanged;
-    _onFieldsChanged = onFieldsChanged;
-    _onRowsChanged = onRowsChanged;
-    _onSettingsChanged = onSettingsChanged;
-    _onArrangeWithNewField = onArrangeWithNewField;
-    _onError = onError;
-
-    fieldController.addListener(onReceiveFields: (fields) {
-      _onFieldsChanged?.call(UnmodifiableListView(fields));
-    });
-
-    _listener.start(
-      onCalendarSettingsChanged: (result) {
-        result.fold(
-          (settings) => _onSettingsChanged?.call(settings),
-          (e) => _onError?.call(e),
-        );
-      },
-      onArrangeWithNewField: (result) {
-        result.fold(
-          (settings) => _onArrangeWithNewField?.call(settings),
-          (e) => _onError?.call(e),
-        );
-      },
-    );
-  }
-
-  Future<Either<Unit, FlowyError>> openDatabase() async {
-    final result = await _databaseBackendSvc.openGrid();
-    return result.fold(
-      (database) async {
-        _onDatabaseChanged?.call(database);
-        return fieldController
-            .loadFields(fieldIds: database.fields)
-            .then((result) {
-          return result.fold(
-            (l) => Future(() async {
-              _viewCache.rowCache.initializeRows(database.rows);
-              return left(l);
-            }),
-            (err) => right(err),
-          );
-        });
-      },
-      (err) => right(err),
-    );
-  }
-
-  Future<void> dispose() async {
-    await _viewCache.dispose();
-    await _databaseBackendSvc.closeView();
-    await fieldController.dispose();
-  }
-}

+ 0 - 65
frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_listener.dart

@@ -1,65 +0,0 @@
-import 'dart:typed_data';
-
-import 'package:appflowy/core/grid_notification.dart';
-import 'package:flowy_infra/notifier.dart';
-import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart';
-import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart';
-import 'package:dartz/dartz.dart';
-
-typedef CalendarSettingsValue = Either<CalendarSettingsPB, FlowyError>;
-typedef ArrangeWithNewField = Either<FieldPB, FlowyError>;
-
-class CalendarListener {
-  final String viewId;
-  PublishNotifier<CalendarSettingsValue>? _calendarSettingsNotifier =
-      PublishNotifier();
-  PublishNotifier<ArrangeWithNewField>? _arrangeWithNewFieldNotifier =
-      PublishNotifier();
-  DatabaseNotificationListener? _listener;
-  CalendarListener(this.viewId);
-
-  void start({
-    required void Function(CalendarSettingsValue) onCalendarSettingsChanged,
-    required void Function(ArrangeWithNewField) onArrangeWithNewField,
-  }) {
-    _calendarSettingsNotifier?.addPublishListener(onCalendarSettingsChanged);
-    _arrangeWithNewFieldNotifier?.addPublishListener(onArrangeWithNewField);
-    _listener = DatabaseNotificationListener(
-      objectId: viewId,
-      handler: _handler,
-    );
-  }
-
-  void _handler(
-    DatabaseNotification ty,
-    Either<Uint8List, FlowyError> result,
-  ) {
-    switch (ty) {
-      case DatabaseNotification.DidUpdateCalendarSettings:
-        result.fold(
-          (payload) => _calendarSettingsNotifier?.value =
-              left(CalendarSettingsPB.fromBuffer(payload)),
-          (error) => _calendarSettingsNotifier?.value = right(error),
-        );
-        break;
-      case DatabaseNotification.DidArrangeCalendarWithNewField:
-        result.fold(
-          (payload) => _arrangeWithNewFieldNotifier?.value =
-              left(FieldPB.fromBuffer(payload)),
-          (error) => _arrangeWithNewFieldNotifier?.value = right(error),
-        );
-        break;
-      default:
-        break;
-    }
-  }
-
-  Future<void> stop() async {
-    await _listener?.stop();
-    _calendarSettingsNotifier?.dispose();
-    _calendarSettingsNotifier = null;
-
-    _arrangeWithNewFieldNotifier?.dispose();
-    _arrangeWithNewFieldNotifier = null;
-  }
-}

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

@@ -77,8 +77,7 @@ class CalendarPluginDisplay extends PluginDisplay {
       });
     });
 
-    return CalendarPage(key: ValueKey(view.id));
-    // return CalendarPage(key: ValueKey(view.id), view: view);
+    return CalendarPage(key: ValueKey(view.id), view: view);
   }
 
   @override

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

@@ -1,56 +1,85 @@
 import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart';
+import 'package:appflowy/plugins/database_view/calendar/application/calendar_bloc.dart';
+import 'package:appflowy/plugins/database_view/grid/presentation/widgets/cell/cell_builder.dart';
+import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart';
+import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
 import 'package:calendar_view/calendar_view.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra/size.dart';
 import 'package:flowy_infra/theme_extension.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flowy_infra_ui/style_widget/button.dart';
+import 'package:flowy_infra_ui/style_widget/hover.dart';
 import 'package:flowy_infra_ui/style_widget/icon_button.dart';
-import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flutter/material.dart';
-import 'package:styled_widget/styled_widget.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:provider/provider.dart';
 
 import '../../grid/presentation/layout/sizes.dart';
+import '../../grid/presentation/widgets/row/row_detail.dart';
 import 'layout/sizes.dart';
 import 'toolbar/calendar_toolbar.dart';
 
-class CalendarPage extends StatelessWidget {
-  const CalendarPage({super.key});
+class CalendarPage extends StatefulWidget {
+  final ViewPB view;
+  const CalendarPage({required this.view, super.key});
 
   @override
-  Widget build(BuildContext context) {
-    return const CalendarContent();
-  }
-}
-
-class CalendarContent extends StatefulWidget {
-  const CalendarContent({super.key});
-
-  @override
-  State<CalendarContent> createState() => _CalendarContentState();
+  State<CalendarPage> createState() => _CalendarPageState();
 }
 
-class _CalendarContentState extends State<CalendarContent> {
-  late EventController _eventController;
+class _CalendarPageState extends State<CalendarPage> {
+  final _eventController = EventController<CalendarCardData>();
   GlobalKey<MonthViewState>? _calendarState;
+  late CalendarBloc _calendarBloc;
 
   @override
   void initState() {
-    _eventController = EventController();
     _calendarState = GlobalKey<MonthViewState>();
+    _calendarBloc = CalendarBloc(view: widget.view)
+      ..add(const CalendarEvent.initial());
+
     super.initState();
   }
 
+  @override
+  void dispose() {
+    _calendarBloc.close();
+    super.dispose();
+  }
+
   @override
   Widget build(BuildContext context) {
     return CalendarControllerProvider(
       controller: _eventController,
-      child: Column(
-        children: [
-          // const _ToolbarBlocAdaptor(),
-          _toolbar(),
-          _buildCalendar(_eventController),
+      child: MultiBlocProvider(
+        providers: [
+          BlocProvider<CalendarBloc>.value(
+            value: _calendarBloc,
+          )
         ],
+        child: BlocListener<CalendarBloc, CalendarState>(
+          listenWhen: (previous, current) => previous.events != current.events,
+          listener: (context, state) {
+            if (state.events.isNotEmpty) {
+              _eventController.removeWhere((element) => true);
+              _eventController.addAll(state.events);
+            }
+          },
+          child: BlocBuilder<CalendarBloc, CalendarState>(
+            builder: (context, state) {
+              return Column(
+                children: [
+                  // const _ToolbarBlocAdaptor(),
+                  _toolbar(),
+                  _buildCalendar(_eventController),
+                ],
+              );
+            },
+          ),
+        ),
       ),
     );
   }
@@ -125,9 +154,190 @@ class _CalendarContentState extends State<CalendarContent> {
     );
   }
 
-  Widget _calendarDayBuilder(date, event, isToday, isInMonth) {
+  Widget _calendarDayBuilder(
+    DateTime date,
+    List<CalendarEventData<CalendarCardData>> calenderEvents,
+    isToday,
+    isInMonth,
+  ) {
+    final builder = CardCellBuilder(_calendarBloc.cellCache);
+    final cells = calenderEvents.map((value) => value.event!).map((event) {
+      final child = builder.buildCell(cellId: event.cellId);
+
+      return FlowyHover(
+        child: GestureDetector(
+          onTap: () {
+            final dataController = RowDataController(
+              rowId: event.cellId.rowId,
+              viewId: widget.view.id,
+              rowCache: _calendarBloc.rowCache,
+            );
+
+            FlowyOverlay.show(
+              context: context,
+              builder: (BuildContext context) {
+                return RowDetailPage(
+                  cellBuilder:
+                      GridCellBuilder(cellCache: _calendarBloc.cellCache),
+                  dataController: dataController,
+                );
+              },
+            );
+          },
+          child: Container(
+            padding: const EdgeInsets.symmetric(horizontal: 8),
+            child: child,
+          ),
+        ),
+      );
+    }).toList();
+
+    return _CalendarCard(
+      isToday: isToday,
+      isInMonth: isInMonth,
+      date: date,
+      children: cells,
+      onCreateEvent: (date) {
+        _calendarBloc.add(
+          CalendarEvent.createEvent(
+            date,
+            LocaleKeys.calendar_defaultNewCalendarTitle.tr(),
+          ),
+        );
+      },
+    );
+  }
+}
+
+class _CalendarCard extends StatelessWidget {
+  final bool isToday;
+  final bool isInMonth;
+  final DateTime date;
+  final List<Widget> children;
+  final void Function(DateTime) onCreateEvent;
+
+  const _CalendarCard({
+    required this.isToday,
+    required this.isInMonth,
+    required this.date,
+    required this.children,
+    required this.onCreateEvent,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    Color backgroundColor = Theme.of(context).colorScheme.surface;
+    if (!isInMonth) {
+      backgroundColor = AFThemeExtension.of(context).lightGreyHover;
+    }
+
+    return ChangeNotifierProvider(
+      create: (_) => _CardEnterNotifier(),
+      builder: ((context, child) {
+        return Container(
+          color: backgroundColor,
+          child: MouseRegion(
+            cursor: SystemMouseCursors.click,
+            onEnter: (p) => notifyEnter(context, true),
+            onExit: (p) => notifyEnter(context, false),
+            child: Padding(
+              padding: const EdgeInsets.all(8.0),
+              child: Column(
+                children: [
+                  _Header(
+                    date: date,
+                    isInMonth: isInMonth,
+                    isToday: isToday,
+                    onCreate: () => onCreateEvent(date),
+                  ),
+                  ...children
+                ],
+              ),
+            ),
+          ),
+        );
+      }),
+    );
+  }
+
+  notifyEnter(BuildContext context, bool isEnter) {
+    Provider.of<_CardEnterNotifier>(
+      context,
+      listen: false,
+    ).onEnter = isEnter;
+  }
+}
+
+class _Header extends StatelessWidget {
+  final bool isToday;
+  final bool isInMonth;
+  final DateTime date;
+  final VoidCallback onCreate;
+  const _Header({
+    required this.isToday,
+    required this.isInMonth,
+    required this.date,
+    required this.onCreate,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Consumer<_CardEnterNotifier>(
+      builder: (context, notifier, _) {
+        final badge = _DayBadge(
+          isToday: isToday,
+          isInMonth: isInMonth,
+          date: date,
+        );
+        return Row(
+          children: [
+            if (notifier.onEnter) _NewEventButton(onClick: onCreate),
+            const Spacer(),
+            badge,
+          ],
+        );
+      },
+    );
+  }
+}
+
+class _NewEventButton extends StatelessWidget {
+  final VoidCallback onClick;
+  const _NewEventButton({
+    required this.onClick,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return FlowyIconButton(
+      onPressed: onClick,
+      iconPadding: EdgeInsets.zero,
+      icon: svgWidget(
+        "home/add",
+        color: Theme.of(context).colorScheme.onSurface,
+      ),
+      width: 22,
+    );
+  }
+}
+
+class _DayBadge extends StatelessWidget {
+  final bool isToday;
+  final bool isInMonth;
+  final DateTime date;
+  const _DayBadge({
+    required this.isToday,
+    required this.isInMonth,
+    required this.date,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
     Color dayTextColor = Theme.of(context).colorScheme.onSurface;
-    Color cellBackgroundColor = Theme.of(context).colorScheme.surface;
     String dayString = date.day == 1
         ? DateFormat('MMM d', context.locale.toLanguageTag()).format(date)
         : date.day.toString();
@@ -137,8 +347,8 @@ class _CalendarContentState extends State<CalendarContent> {
     }
     if (!isInMonth) {
       dayTextColor = Theme.of(context).disabledColor;
-      cellBackgroundColor = AFThemeExtension.of(context).lightGreyHover;
     }
+
     Widget day = Container(
       decoration: BoxDecoration(
         color: isToday ? Theme.of(context).colorScheme.primary : null,
@@ -151,12 +361,21 @@ class _CalendarContentState extends State<CalendarContent> {
       ),
     );
 
-    return Container(
-      color: cellBackgroundColor,
-      child: Align(
-        alignment: Alignment.topRight,
-        child: day.padding(all: 6.0),
-      ),
-    );
+    return day;
   }
 }
+
+class _CardEnterNotifier extends ChangeNotifier {
+  bool _onEnter = false;
+
+  _CardEnterNotifier();
+
+  set onEnter(bool value) {
+    if (_onEnter != value) {
+      _onEnter = value;
+      notifyListeners();
+    }
+  }
+
+  bool get onEnter => _onEnter;
+}

+ 3 - 2
frontend/appflowy_flutter/lib/plugins/database_view/grid/application/cell/checklist_cell_bloc.dart

@@ -8,11 +8,12 @@ import 'checklist_cell_editor_bloc.dart';
 import 'select_option_service.dart';
 part 'checklist_cell_bloc.freezed.dart';
 
-class ChecklistCellBloc extends Bloc<ChecklistCellEvent, ChecklistCellState> {
+class ChecklistCardCellBloc
+    extends Bloc<ChecklistCellEvent, ChecklistCellState> {
   final ChecklistCellController cellController;
   final SelectOptionBackendService _selectOptionSvc;
   void Function()? _onCellChangedFn;
-  ChecklistCellBloc({
+  ChecklistCardCellBloc({
     required this.cellController,
   })  : _selectOptionSvc =
             SelectOptionBackendService(cellId: cellController.cellId),

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

@@ -9,7 +9,7 @@ import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import '../../application/field/field_controller.dart';
-import 'grid_data_controller.dart';
+import '../../application/database_controller.dart';
 import 'dart:collection';
 
 part 'grid_bloc.freezed.dart';
@@ -66,10 +66,10 @@ class GridBloc extends Bloc<GridEvent, GridState> {
   }
 
   void _startListening() {
-    databaseController.addListener(
-      onGridChanged: (grid) {
+    final onDatabaseChanged = DatabaseCallbacks(
+      onDatabaseChanged: (database) {
         if (!isClosed) {
-          add(GridEvent.didReceiveGridUpdate(grid));
+          add(GridEvent.didReceiveGridUpdate(database));
         }
       },
       onRowsChanged: (rowInfos, reason) {
@@ -83,10 +83,11 @@ class GridBloc extends Bloc<GridEvent, GridState> {
         }
       },
     );
+    databaseController.addListener(onDatabaseChanged: onDatabaseChanged);
   }
 
   Future<void> _openGrid(Emitter<GridState> emit) async {
-    final result = await databaseController.openGrid();
+    final result = await databaseController.open();
     result.fold(
       (grid) {
         emit(

+ 0 - 83
frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_data_controller.dart

@@ -1,83 +0,0 @@
-import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
-import 'package:appflowy/plugins/database_view/application/view/view_cache.dart';
-import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
-import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
-import 'package:collection/collection.dart';
-import 'dart:async';
-import 'package:dartz/dartz.dart';
-import '../../application/database_service.dart';
-import '../../application/defines.dart';
-import '../../application/row/row_cache.dart';
-
-typedef OnRowsChanged = void Function(
-  List<RowInfo> rowInfos,
-  RowsChangedReason,
-);
-typedef ListenOnRowChangedCondition = bool Function();
-
-class DatabaseController {
-  final String viewId;
-  final DatabaseBackendService _databaseBackendSvc;
-  final FieldController fieldController;
-  late DatabaseViewCache _viewCache;
-
-  OnRowsChanged? _onRowChanged;
-  OnDatabaseChanged? _onGridChanged;
-  List<RowInfo> get rowInfos => _viewCache.rowInfos;
-  RowCache get rowCache => _viewCache.rowCache;
-
-  DatabaseController({required ViewPB view})
-      : viewId = view.id,
-        _databaseBackendSvc = DatabaseBackendService(viewId: view.id),
-        fieldController = FieldController(viewId: view.id) {
-    _viewCache = DatabaseViewCache(
-      viewId: viewId,
-      fieldController: fieldController,
-    );
-    _viewCache.addListener(onRowsChanged: (reason) {
-      _onRowChanged?.call(rowInfos, reason);
-    });
-  }
-
-  void addListener({
-    OnDatabaseChanged? onGridChanged,
-    OnRowsChanged? onRowsChanged,
-    OnFieldsChanged? onFieldsChanged,
-    OnFiltersChanged? onFiltersChanged,
-  }) {
-    _onGridChanged = onGridChanged;
-    _onRowChanged = onRowsChanged;
-
-    fieldController.addListener(
-      onReceiveFields: (fields) {
-        onFieldsChanged?.call(UnmodifiableListView(fields));
-      },
-      onFilters: onFiltersChanged,
-    );
-  }
-
-  Future<Either<Unit, FlowyError>> openGrid() async {
-    return _databaseBackendSvc.openGrid().then((result) {
-      return result.fold(
-        (grid) async {
-          _onGridChanged?.call(grid);
-          _viewCache.rowCache.initializeRows(grid.rows);
-          final result = await fieldController.loadFields(
-            fieldIds: grid.fields,
-          );
-          return result;
-        },
-        (err) => right(err),
-      );
-    });
-  }
-
-  Future<void> createRow() async {
-    await _databaseBackendSvc.createRow();
-  }
-
-  Future<void> dispose() async {
-    await _databaseBackendSvc.closeView();
-    await fieldController.dispose();
-  }
-}

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

@@ -27,7 +27,7 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
           },
           deleteField: (_DeleteField value) {
             final fieldService = FieldBackendService(
-              viewId: dataController.rowInfo.viewId,
+              viewId: dataController.viewId,
               fieldId: value.fieldId,
             );
             fieldService.deleteField();

+ 10 - 4
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart

@@ -1,4 +1,5 @@
 import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy_backend/protobuf/flowy-database/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';
@@ -16,7 +17,7 @@ import '../../application/row/row_data_controller.dart';
 import '../../application/setting/setting_bloc.dart';
 import '../application/filter/filter_menu_bloc.dart';
 import '../application/grid_bloc.dart';
-import '../application/grid_data_controller.dart';
+import '../../application/database_controller.dart';
 import '../application/sort/sort_menu_bloc.dart';
 import 'grid_scroll.dart';
 import 'layout/layout.dart';
@@ -35,7 +36,10 @@ class GridPage extends StatefulWidget {
     required this.view,
     this.onDeleted,
     Key? key,
-  })  : databaseController = DatabaseController(view: view),
+  })  : databaseController = DatabaseController(
+          view: view,
+          layoutType: LayoutTypePB.Grid,
+        ),
         super(key: key);
 
   final ViewPB view;
@@ -276,7 +280,8 @@ class _GridRowsState extends State<_GridRows> {
     final fieldController =
         context.read<GridBloc>().databaseController.fieldController;
     final dataController = RowDataController(
-      rowInfo: rowInfo,
+      rowId: rowInfo.rowPB.id,
+      viewId: rowInfo.viewId,
       rowCache: rowCache,
     );
 
@@ -308,7 +313,8 @@ class _GridRowsState extends State<_GridRows> {
     GridCellBuilder cellBuilder,
   ) {
     final dataController = RowDataController(
-      rowInfo: rowInfo,
+      viewId: rowInfo.viewId,
+      rowId: rowInfo.rowPB.id,
       rowCache: rowCache,
     );
 

+ 3 - 3
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/cell/checklist_cell/checklist_cell.dart

@@ -20,7 +20,7 @@ class GridChecklistCell extends GridCellWidget {
 }
 
 class GridChecklistCellState extends GridCellState<GridChecklistCell> {
-  late ChecklistCellBloc _cellBloc;
+  late ChecklistCardCellBloc _cellBloc;
   late final PopoverController _popover;
 
   @override
@@ -28,7 +28,7 @@ class GridChecklistCellState extends GridCellState<GridChecklistCell> {
     _popover = PopoverController();
     final cellController =
         widget.cellControllerBuilder.build() as ChecklistCellController;
-    _cellBloc = ChecklistCellBloc(cellController: cellController);
+    _cellBloc = ChecklistCardCellBloc(cellController: cellController);
     _cellBloc.add(const ChecklistCellEvent.initial());
     super.initState();
   }
@@ -54,7 +54,7 @@ class GridChecklistCellState extends GridCellState<GridChecklistCell> {
         onClose: () => widget.onCellEditing.value = false,
         child: Padding(
           padding: GridSize.cellContentInsets,
-          child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
+          child: BlocBuilder<ChecklistCardCellBloc, ChecklistCellState>(
             builder: (context, state) =>
                 ChecklistProgressBar(percent: state.percent),
           ),

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/row_detail.dart

@@ -66,7 +66,7 @@ class _RowDetailPageState extends State<RowDetailPage> {
               Expanded(
                 child: _PropertyList(
                   cellBuilder: widget.cellBuilder,
-                  viewId: widget.dataController.rowInfo.viewId,
+                  viewId: widget.dataController.viewId,
                 ),
               ),
             ],

+ 16 - 16
frontend/appflowy_flutter/lib/plugins/database_view/board/application/card/board_checkbox_cell_bloc.dart → frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/checkbox_card_cell_bloc.dart

@@ -3,16 +3,16 @@ import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
 import '../../../application/cell/cell_controller_builder.dart';
 
-part 'board_checkbox_cell_bloc.freezed.dart';
+part 'checkbox_card_cell_bloc.freezed.dart';
 
-class BoardCheckboxCellBloc
-    extends Bloc<BoardCheckboxCellEvent, BoardCheckboxCellState> {
+class CheckboxCardCellBloc
+    extends Bloc<CheckboxCardCellEvent, CheckboxCardCellState> {
   final CheckboxCellController cellController;
   void Function()? _onCellChangedFn;
-  BoardCheckboxCellBloc({
+  CheckboxCardCellBloc({
     required this.cellController,
-  }) : super(BoardCheckboxCellState.initial(cellController)) {
-    on<BoardCheckboxCellEvent>(
+  }) : super(CheckboxCardCellState.initial(cellController)) {
+    on<CheckboxCardCellEvent>(
       (event, emit) async {
         await event.when(
           initial: () async {
@@ -43,7 +43,7 @@ class BoardCheckboxCellBloc
     _onCellChangedFn = cellController.startListening(
       onCellChanged: ((cellContent) {
         if (!isClosed) {
-          add(BoardCheckboxCellEvent.didReceiveCellUpdate(cellContent ?? ""));
+          add(CheckboxCardCellEvent.didReceiveCellUpdate(cellContent ?? ""));
         }
       }),
     );
@@ -51,21 +51,21 @@ class BoardCheckboxCellBloc
 }
 
 @freezed
-class BoardCheckboxCellEvent with _$BoardCheckboxCellEvent {
-  const factory BoardCheckboxCellEvent.initial() = _InitialCell;
-  const factory BoardCheckboxCellEvent.select() = _Selected;
-  const factory BoardCheckboxCellEvent.didReceiveCellUpdate(
-      String cellContent) = _DidReceiveCellUpdate;
+class CheckboxCardCellEvent with _$CheckboxCardCellEvent {
+  const factory CheckboxCardCellEvent.initial() = _InitialCell;
+  const factory CheckboxCardCellEvent.select() = _Selected;
+  const factory CheckboxCardCellEvent.didReceiveCellUpdate(String cellContent) =
+      _DidReceiveCellUpdate;
 }
 
 @freezed
-class BoardCheckboxCellState with _$BoardCheckboxCellState {
-  const factory BoardCheckboxCellState({
+class CheckboxCardCellState with _$CheckboxCardCellState {
+  const factory CheckboxCardCellState({
     required bool isSelected,
   }) = _CheckboxCellState;
 
-  factory BoardCheckboxCellState.initial(TextCellController context) {
-    return BoardCheckboxCellState(
+  factory CheckboxCardCellState.initial(TextCellController context) {
+    return CheckboxCardCellState(
         isSelected: _isSelected(context.getCellData()));
   }
 }

+ 14 - 14
frontend/appflowy_flutter/lib/plugins/database_view/board/application/card/board_date_cell_bloc.dart → frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/date_card_cell_bloc.dart

@@ -5,15 +5,15 @@ import 'dart:async';
 
 import '../../../application/cell/cell_controller_builder.dart';
 import '../../../application/field/field_controller.dart';
-part 'board_date_cell_bloc.freezed.dart';
+part 'date_card_cell_bloc.freezed.dart';
 
-class BoardDateCellBloc extends Bloc<BoardDateCellEvent, BoardDateCellState> {
+class DateCardCellBloc extends Bloc<DateCardCellEvent, DateCardCellState> {
   final DateCellController cellController;
   void Function()? _onCellChangedFn;
 
-  BoardDateCellBloc({required this.cellController})
-      : super(BoardDateCellState.initial(cellController)) {
-    on<BoardDateCellEvent>(
+  DateCardCellBloc({required this.cellController})
+      : super(DateCardCellState.initial(cellController)) {
+    on<DateCardCellEvent>(
       (event, emit) async {
         event.when(
           initial: () => _startListening(),
@@ -40,7 +40,7 @@ class BoardDateCellBloc extends Bloc<BoardDateCellEvent, BoardDateCellState> {
     _onCellChangedFn = cellController.startListening(
       onCellChanged: ((data) {
         if (!isClosed) {
-          add(BoardDateCellEvent.didReceiveCellUpdate(data));
+          add(DateCardCellEvent.didReceiveCellUpdate(data));
         }
       }),
     );
@@ -48,24 +48,24 @@ class BoardDateCellBloc extends Bloc<BoardDateCellEvent, BoardDateCellState> {
 }
 
 @freezed
-class BoardDateCellEvent with _$BoardDateCellEvent {
-  const factory BoardDateCellEvent.initial() = _InitialCell;
-  const factory BoardDateCellEvent.didReceiveCellUpdate(DateCellDataPB? data) =
+class DateCardCellEvent with _$DateCardCellEvent {
+  const factory DateCardCellEvent.initial() = _InitialCell;
+  const factory DateCardCellEvent.didReceiveCellUpdate(DateCellDataPB? data) =
       _DidReceiveCellUpdate;
 }
 
 @freezed
-class BoardDateCellState with _$BoardDateCellState {
-  const factory BoardDateCellState({
+class DateCardCellState with _$DateCardCellState {
+  const factory DateCardCellState({
     required DateCellDataPB? data,
     required String dateStr,
     required FieldInfo fieldInfo,
-  }) = _BoardDateCellState;
+  }) = _DateCardCellState;
 
-  factory BoardDateCellState.initial(DateCellController context) {
+  factory DateCardCellState.initial(DateCellController context) {
     final cellData = context.getCellData();
 
-    return BoardDateCellState(
+    return DateCardCellState(
       fieldInfo: context.fieldInfo,
       data: cellData,
       dateStr: _dateStrFromCellData(cellData),

+ 15 - 15
frontend/appflowy_flutter/lib/plugins/database_view/board/application/card/board_number_cell_bloc.dart → frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/number_card_cell_bloc.dart

@@ -4,16 +4,16 @@ import 'dart:async';
 
 import '../../../application/cell/cell_controller_builder.dart';
 
-part 'board_number_cell_bloc.freezed.dart';
+part 'number_card_cell_bloc.freezed.dart';
 
-class BoardNumberCellBloc
-    extends Bloc<BoardNumberCellEvent, BoardNumberCellState> {
+class NumberCardCellBloc
+    extends Bloc<NumberCardCellEvent, NumberCardCellState> {
   final NumberCellController cellController;
   void Function()? _onCellChangedFn;
-  BoardNumberCellBloc({
+  NumberCardCellBloc({
     required this.cellController,
-  }) : super(BoardNumberCellState.initial(cellController)) {
-    on<BoardNumberCellEvent>(
+  }) : super(NumberCardCellState.initial(cellController)) {
+    on<NumberCardCellEvent>(
       (event, emit) async {
         await event.when(
           initial: () async {
@@ -41,7 +41,7 @@ class BoardNumberCellBloc
     _onCellChangedFn = cellController.startListening(
       onCellChanged: ((cellContent) {
         if (!isClosed) {
-          add(BoardNumberCellEvent.didReceiveCellUpdate(cellContent ?? ""));
+          add(NumberCardCellEvent.didReceiveCellUpdate(cellContent ?? ""));
         }
       }),
     );
@@ -49,20 +49,20 @@ class BoardNumberCellBloc
 }
 
 @freezed
-class BoardNumberCellEvent with _$BoardNumberCellEvent {
-  const factory BoardNumberCellEvent.initial() = _InitialCell;
-  const factory BoardNumberCellEvent.didReceiveCellUpdate(String cellContent) =
+class NumberCardCellEvent with _$NumberCardCellEvent {
+  const factory NumberCardCellEvent.initial() = _InitialCell;
+  const factory NumberCardCellEvent.didReceiveCellUpdate(String cellContent) =
       _DidReceiveCellUpdate;
 }
 
 @freezed
-class BoardNumberCellState with _$BoardNumberCellState {
-  const factory BoardNumberCellState({
+class NumberCardCellState with _$NumberCardCellState {
+  const factory NumberCardCellState({
     required String content,
-  }) = _BoardNumberCellState;
+  }) = _NumberCardCellState;
 
-  factory BoardNumberCellState.initial(TextCellController context) =>
-      BoardNumberCellState(
+  factory NumberCardCellState.initial(TextCellController context) =>
+      NumberCardCellState(
         content: context.getCellData() ?? "",
       );
 }

+ 15 - 15
frontend/appflowy_flutter/lib/plugins/database_view/board/application/card/board_select_option_cell_bloc.dart → frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/select_option_card_cell_bloc.dart

@@ -4,17 +4,17 @@ import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.d
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 
-part 'board_select_option_cell_bloc.freezed.dart';
+part 'select_option_card_cell_bloc.freezed.dart';
 
-class BoardSelectOptionCellBloc
-    extends Bloc<BoardSelectOptionCellEvent, BoardSelectOptionCellState> {
+class SelectOptionCardCellBloc
+    extends Bloc<SelectOptionCardCellEvent, SelectOptionCardCellState> {
   final SelectOptionCellController cellController;
   void Function()? _onCellChangedFn;
 
-  BoardSelectOptionCellBloc({
+  SelectOptionCardCellBloc({
     required this.cellController,
-  }) : super(BoardSelectOptionCellState.initial(cellController)) {
-    on<BoardSelectOptionCellEvent>(
+  }) : super(SelectOptionCardCellState.initial(cellController)) {
+    on<SelectOptionCardCellEvent>(
       (event, emit) async {
         await event.when(
           initial: () async {
@@ -42,7 +42,7 @@ class BoardSelectOptionCellBloc
     _onCellChangedFn = cellController.startListening(
       onCellChanged: ((selectOptionContext) {
         if (!isClosed) {
-          add(BoardSelectOptionCellEvent.didReceiveOptions(
+          add(SelectOptionCardCellEvent.didReceiveOptions(
             selectOptionContext?.selectOptions ?? [],
           ));
         }
@@ -52,23 +52,23 @@ class BoardSelectOptionCellBloc
 }
 
 @freezed
-class BoardSelectOptionCellEvent with _$BoardSelectOptionCellEvent {
-  const factory BoardSelectOptionCellEvent.initial() = _InitialCell;
-  const factory BoardSelectOptionCellEvent.didReceiveOptions(
+class SelectOptionCardCellEvent with _$SelectOptionCardCellEvent {
+  const factory SelectOptionCardCellEvent.initial() = _InitialCell;
+  const factory SelectOptionCardCellEvent.didReceiveOptions(
     List<SelectOptionPB> selectedOptions,
   ) = _DidReceiveOptions;
 }
 
 @freezed
-class BoardSelectOptionCellState with _$BoardSelectOptionCellState {
-  const factory BoardSelectOptionCellState({
+class SelectOptionCardCellState with _$SelectOptionCardCellState {
+  const factory SelectOptionCardCellState({
     required List<SelectOptionPB> selectedOptions,
-  }) = _BoardSelectOptionCellState;
+  }) = _SelectOptionCardCellState;
 
-  factory BoardSelectOptionCellState.initial(
+  factory SelectOptionCardCellState.initial(
       SelectOptionCellController context) {
     final data = context.getCellData();
-    return BoardSelectOptionCellState(
+    return SelectOptionCardCellState(
       selectedOptions: data?.selectOptions ?? [],
     );
   }

+ 16 - 16
frontend/appflowy_flutter/lib/plugins/database_view/board/application/card/board_text_cell_bloc.dart → frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/text_card_cell_bloc.dart

@@ -3,15 +3,15 @@ import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
 
-part 'board_text_cell_bloc.freezed.dart';
+part 'text_card_cell_bloc.freezed.dart';
 
-class BoardTextCellBloc extends Bloc<BoardTextCellEvent, BoardTextCellState> {
+class TextCardCellBloc extends Bloc<TextCardCellEvent, TextCardCellState> {
   final TextCellController cellController;
   void Function()? _onCellChangedFn;
-  BoardTextCellBloc({
+  TextCardCellBloc({
     required this.cellController,
-  }) : super(BoardTextCellState.initial(cellController)) {
-    on<BoardTextCellEvent>(
+  }) : super(TextCardCellState.initial(cellController)) {
+    on<TextCardCellEvent>(
       (event, emit) async {
         await event.when(
           initial: () async {
@@ -48,7 +48,7 @@ class BoardTextCellBloc extends Bloc<BoardTextCellEvent, BoardTextCellState> {
     _onCellChangedFn = cellController.startListening(
       onCellChanged: ((cellContent) {
         if (!isClosed) {
-          add(BoardTextCellEvent.didReceiveCellUpdate(cellContent ?? ""));
+          add(TextCardCellEvent.didReceiveCellUpdate(cellContent ?? ""));
         }
       }),
     );
@@ -56,23 +56,23 @@ class BoardTextCellBloc extends Bloc<BoardTextCellEvent, BoardTextCellState> {
 }
 
 @freezed
-class BoardTextCellEvent with _$BoardTextCellEvent {
-  const factory BoardTextCellEvent.initial() = _InitialCell;
-  const factory BoardTextCellEvent.updateText(String text) = _UpdateContent;
-  const factory BoardTextCellEvent.enableEdit(bool enabled) = _EnableEdit;
-  const factory BoardTextCellEvent.didReceiveCellUpdate(String cellContent) =
+class TextCardCellEvent with _$TextCardCellEvent {
+  const factory TextCardCellEvent.initial() = _InitialCell;
+  const factory TextCardCellEvent.updateText(String text) = _UpdateContent;
+  const factory TextCardCellEvent.enableEdit(bool enabled) = _EnableEdit;
+  const factory TextCardCellEvent.didReceiveCellUpdate(String cellContent) =
       _DidReceiveCellUpdate;
 }
 
 @freezed
-class BoardTextCellState with _$BoardTextCellState {
-  const factory BoardTextCellState({
+class TextCardCellState with _$TextCardCellState {
+  const factory TextCardCellState({
     required String content,
     required bool enableEdit,
-  }) = _BoardTextCellState;
+  }) = _TextCardCellState;
 
-  factory BoardTextCellState.initial(TextCellController context) =>
-      BoardTextCellState(
+  factory TextCardCellState.initial(TextCellController context) =>
+      TextCardCellState(
         content: context.getCellData() ?? "",
         enableEdit: false,
       );

+ 15 - 15
frontend/appflowy_flutter/lib/plugins/database_view/board/application/card/board_url_cell_bloc.dart → frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/url_card_cell_bloc.dart

@@ -4,15 +4,15 @@ import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
 
-part 'board_url_cell_bloc.freezed.dart';
+part 'url_card_cell_bloc.freezed.dart';
 
-class BoardURLCellBloc extends Bloc<BoardURLCellEvent, BoardURLCellState> {
+class URLCardCellBloc extends Bloc<URLCardCellEvent, URLCardCellState> {
   final URLCellController cellController;
   void Function()? _onCellChangedFn;
-  BoardURLCellBloc({
+  URLCardCellBloc({
     required this.cellController,
-  }) : super(BoardURLCellState.initial(cellController)) {
-    on<BoardURLCellEvent>(
+  }) : super(URLCardCellState.initial(cellController)) {
+    on<URLCardCellEvent>(
       (event, emit) async {
         event.when(
           initial: () {
@@ -46,7 +46,7 @@ class BoardURLCellBloc extends Bloc<BoardURLCellEvent, BoardURLCellState> {
     _onCellChangedFn = cellController.startListening(
       onCellChanged: ((cellData) {
         if (!isClosed) {
-          add(BoardURLCellEvent.didReceiveCellUpdate(cellData));
+          add(URLCardCellEvent.didReceiveCellUpdate(cellData));
         }
       }),
     );
@@ -54,23 +54,23 @@ class BoardURLCellBloc extends Bloc<BoardURLCellEvent, BoardURLCellState> {
 }
 
 @freezed
-class BoardURLCellEvent with _$BoardURLCellEvent {
-  const factory BoardURLCellEvent.initial() = _InitialCell;
-  const factory BoardURLCellEvent.updateURL(String url) = _UpdateURL;
-  const factory BoardURLCellEvent.didReceiveCellUpdate(URLCellDataPB? cell) =
+class URLCardCellEvent with _$URLCardCellEvent {
+  const factory URLCardCellEvent.initial() = _InitialCell;
+  const factory URLCardCellEvent.updateURL(String url) = _UpdateURL;
+  const factory URLCardCellEvent.didReceiveCellUpdate(URLCellDataPB? cell) =
       _DidReceiveCellUpdate;
 }
 
 @freezed
-class BoardURLCellState with _$BoardURLCellState {
-  const factory BoardURLCellState({
+class URLCardCellState with _$URLCardCellState {
+  const factory URLCardCellState({
     required String content,
     required String url,
-  }) = _BoardURLCellState;
+  }) = _URLCardCellState;
 
-  factory BoardURLCellState.initial(URLCellController context) {
+  factory URLCardCellState.initial(URLCellController context) {
     final cellData = context.getCellData();
-    return BoardURLCellState(
+    return URLCardCellState(
       content: cellData?.content ?? "",
       url: cellData?.url ?? "",
     );

+ 8 - 8
frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/card/board_number_cell.dart → frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/board_number_cell.dart

@@ -2,7 +2,7 @@ import 'package:appflowy/plugins/database_view/application/cell/cell_controller_
 import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
-import '../../application/card/board_number_cell_bloc.dart';
+import 'bloc/number_card_cell_bloc.dart';
 import 'define.dart';
 
 class BoardNumberCell extends StatefulWidget {
@@ -16,19 +16,19 @@ class BoardNumberCell extends StatefulWidget {
   }) : super(key: key);
 
   @override
-  State<BoardNumberCell> createState() => _BoardNumberCellState();
+  State<BoardNumberCell> createState() => _NumberCardCellState();
 }
 
-class _BoardNumberCellState extends State<BoardNumberCell> {
-  late BoardNumberCellBloc _cellBloc;
+class _NumberCardCellState extends State<BoardNumberCell> {
+  late NumberCardCellBloc _cellBloc;
 
   @override
   void initState() {
     final cellController =
         widget.cellControllerBuilder.build() as NumberCellController;
 
-    _cellBloc = BoardNumberCellBloc(cellController: cellController)
-      ..add(const BoardNumberCellEvent.initial());
+    _cellBloc = NumberCardCellBloc(cellController: cellController)
+      ..add(const NumberCardCellEvent.initial());
     super.initState();
   }
 
@@ -36,7 +36,7 @@ class _BoardNumberCellState extends State<BoardNumberCell> {
   Widget build(BuildContext context) {
     return BlocProvider.value(
       value: _cellBloc,
-      child: BlocBuilder<BoardNumberCellBloc, BoardNumberCellState>(
+      child: BlocBuilder<NumberCardCellBloc, NumberCardCellState>(
         buildWhen: (previous, current) => previous.content != current.content,
         builder: (context, state) {
           if (state.content.isEmpty) {
@@ -46,7 +46,7 @@ class _BoardNumberCellState extends State<BoardNumberCell> {
               alignment: Alignment.centerLeft,
               child: Padding(
                 padding: EdgeInsets.symmetric(
-                  vertical: BoardSizes.cardCellVPadding,
+                  vertical: CardSizes.cardCellVPadding,
                 ),
                 child: FlowyText.medium(
                   state.content,

+ 38 - 28
frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/card/card.dart → frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card.dart

@@ -1,47 +1,52 @@
-import 'package:appflowy/plugins/database_view/board/application/card/card_bloc.dart';
-import 'package:appflowy/plugins/database_view/board/application/card/card_data_controller.dart';
+import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
 import 'package:appflowy/plugins/database_view/grid/presentation/widgets/row/row_action_sheet.dart';
+import 'package:appflowy_backend/protobuf/flowy-database/row_entities.pb.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
-import 'board_cell.dart';
+import 'card_bloc.dart';
+import 'cells/card_cell.dart';
 import 'card_cell_builder.dart';
 import 'container/accessory.dart';
 import 'container/card_container.dart';
 
-class BoardCard extends StatefulWidget {
+class Card<CustomCardData> extends StatefulWidget {
+  final RowPB row;
   final String viewId;
-  final String groupId;
   final String fieldId;
+  final CustomCardData? cardData;
   final bool isEditing;
-  final CardDataController dataController;
-  final BoardCellBuilder cellBuilder;
+  final RowCache rowCache;
+  final CardCellBuilder<CustomCardData> cellBuilder;
   final void Function(BuildContext) openCard;
   final VoidCallback onStartEditing;
   final VoidCallback onEndEditing;
+  final CardConfiguration<CustomCardData>? configuration;
 
-  const BoardCard({
+  const Card({
+    required this.row,
     required this.viewId,
-    required this.groupId,
     required this.fieldId,
     required this.isEditing,
-    required this.dataController,
+    required this.rowCache,
     required this.cellBuilder,
     required this.openCard,
     required this.onStartEditing,
     required this.onEndEditing,
+    this.cardData,
+    this.configuration,
     Key? key,
   }) : super(key: key);
 
   @override
-  State<BoardCard> createState() => _BoardCardState();
+  State<Card<CustomCardData>> createState() => _CardState<CustomCardData>();
 }
 
-class _BoardCardState extends State<BoardCard> {
-  late BoardCardBloc _cardBloc;
+class _CardState<T> extends State<Card<T>> {
+  late CardBloc _cardBloc;
   late EditableRowNotifier rowNotifier;
   late PopoverController popoverController;
   AccessoryType? accessoryType;
@@ -49,11 +54,12 @@ class _BoardCardState extends State<BoardCard> {
   @override
   void initState() {
     rowNotifier = EditableRowNotifier(isEditing: widget.isEditing);
-    _cardBloc = BoardCardBloc(
+    _cardBloc = CardBloc(
       viewId: widget.viewId,
       groupFieldId: widget.fieldId,
-      dataController: widget.dataController,
       isEditing: widget.isEditing,
+      row: widget.row,
+      rowCache: widget.rowCache,
     )..add(const BoardCardEvent.initial());
 
     rowNotifier.isEditing.addListener(() {
@@ -75,7 +81,7 @@ class _BoardCardState extends State<BoardCard> {
   Widget build(BuildContext context) {
     return BlocProvider.value(
       value: _cardBloc,
-      child: BlocBuilder<BoardCardBloc, BoardCardState>(
+      child: BlocBuilder<CardBloc, BoardCardState>(
         buildWhen: (previous, current) {
           // Rebuild when:
           // 1.If the length of the cells is not the same
@@ -110,11 +116,12 @@ class _BoardCardState extends State<BoardCard> {
               },
               openAccessory: _handleOpenAccessory,
               openCard: (context) => widget.openCard(context),
-              child: _CellColumn(
-                groupId: widget.groupId,
+              child: _CardContent<T>(
                 rowNotifier: rowNotifier,
                 cellBuilder: widget.cellBuilder,
                 cells: state.cells,
+                cardConfiguration: widget.configuration,
+                cardData: widget.cardData,
               ),
             ),
           );
@@ -143,7 +150,7 @@ class _BoardCardState extends State<BoardCard> {
         throw UnimplementedError();
       case AccessoryType.more:
         return GridRowActionSheet(
-          rowData: context.read<BoardCardBloc>().rowInfo(),
+          rowData: context.read<CardBloc>().rowInfo(),
         );
     }
   }
@@ -156,16 +163,18 @@ class _BoardCardState extends State<BoardCard> {
   }
 }
 
-class _CellColumn extends StatelessWidget {
-  final String groupId;
-  final BoardCellBuilder cellBuilder;
+class _CardContent<CustomCardData> extends StatelessWidget {
+  final CardCellBuilder<CustomCardData> cellBuilder;
   final EditableRowNotifier rowNotifier;
   final List<BoardCellEquatable> cells;
-  const _CellColumn({
-    required this.groupId,
+  final CardConfiguration<CustomCardData>? cardConfiguration;
+  final CustomCardData? cardData;
+  const _CardContent({
     required this.rowNotifier,
     required this.cellBuilder,
     required this.cells,
+    required this.cardData,
+    this.cardConfiguration,
     Key? key,
   }) : super(key: key);
 
@@ -188,7 +197,7 @@ class _CellColumn extends StatelessWidget {
     cells.asMap().forEach(
       (int index, BoardCellEquatable cell) {
         final isEditing = index == 0 ? rowNotifier.isEditing.value : false;
-        final cellNotifier = EditableCellNotifier(isEditing: isEditing);
+        final cellNotifier = EditableCardNotifier(isEditing: isEditing);
 
         if (index == 0) {
           // Only use the first cell to receive user's input when click the edit
@@ -200,9 +209,10 @@ class _CellColumn extends StatelessWidget {
           key: cell.identifier.key(),
           padding: const EdgeInsets.only(left: 4, right: 4),
           child: cellBuilder.buildCell(
-            groupId,
-            cell.identifier,
-            cellNotifier,
+            cellId: cell.identifier,
+            cellNotifier: cellNotifier,
+            cardConfiguration: cardConfiguration,
+            cardData: cardData,
           ),
         );
 

+ 23 - 17
frontend/appflowy_flutter/lib/plugins/database_view/board/application/card/card_bloc.dart → frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_bloc.dart

@@ -1,34 +1,36 @@
 import 'dart:collection';
 import 'package:equatable/equatable.dart';
 import 'package:appflowy_backend/protobuf/flowy-database/row_entities.pb.dart';
+import 'package:flutter/foundation.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
-import '../../../application/cell/cell_service.dart';
-import '../../../application/row/row_cache.dart';
-import '../../../application/row/row_service.dart';
-import 'card_data_controller.dart';
+
+import '../../application/cell/cell_service.dart';
+import '../../application/row/row_cache.dart';
+import '../../application/row/row_service.dart';
 
 part 'card_bloc.freezed.dart';
 
-class BoardCardBloc extends Bloc<BoardCardEvent, BoardCardState> {
+class CardBloc extends Bloc<BoardCardEvent, BoardCardState> {
+  final RowPB row;
   final String groupFieldId;
   final RowBackendService _rowBackendSvc;
-  final CardDataController _dataController;
+  final RowCache _rowCache;
+  VoidCallback? _rowCallback;
 
-  BoardCardBloc({
+  CardBloc({
+    required this.row,
     required this.groupFieldId,
     required String viewId,
-    required CardDataController dataController,
+    required RowCache rowCache,
     required bool isEditing,
-  })  : _rowBackendSvc = RowBackendService(
-          viewId: viewId,
-        ),
-        _dataController = dataController,
+  })  : _rowBackendSvc = RowBackendService(viewId: viewId),
+        _rowCache = rowCache,
         super(
           BoardCardState.initial(
-            dataController.rowPB,
-            _makeCells(groupFieldId, dataController.loadData()),
+            row,
+            _makeCells(groupFieldId, rowCache.loadGridCells(row.id)),
             isEditing,
           ),
         ) {
@@ -54,7 +56,10 @@ class BoardCardBloc extends Bloc<BoardCardEvent, BoardCardState> {
 
   @override
   Future<void> close() async {
-    _dataController.dispose();
+    if (_rowCallback != null) {
+      _rowCache.removeRowListener(_rowCallback!);
+      _rowCallback = null;
+    }
     return super.close();
   }
 
@@ -69,8 +74,9 @@ class BoardCardBloc extends Bloc<BoardCardEvent, BoardCardState> {
   }
 
   Future<void> _startListening() async {
-    _dataController.addListener(
-      onRowChanged: (cellMap, reason) {
+    _rowCallback = _rowCache.addListener(
+      rowId: row.id,
+      onCellUpdated: (cellMap, reason) {
         if (!isClosed) {
           final cells = _makeCells(groupFieldId, cellMap);
           add(BoardCardEvent.didReceiveCells(cells, reason));

+ 32 - 37
frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/card/card_cell_builder.dart → frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart

@@ -2,83 +2,78 @@ import 'package:appflowy/plugins/database_view/application/cell/cell_controller_
 import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart';
 import 'package:flutter/material.dart';
 
-import '../../../application/cell/cell_service.dart';
-import 'board_cell.dart';
-import 'board_checkbox_cell.dart';
-import 'board_checklist_cell.dart';
-import 'board_date_cell.dart';
-import 'board_number_cell.dart';
-import 'board_select_option_cell.dart';
-import 'board_text_cell.dart';
-import 'board_url_cell.dart';
+import '../../application/cell/cell_service.dart';
+import 'cells/card_cell.dart';
+import 'cells/checkbox_card_cell.dart';
+import 'cells/checklist_card_cell.dart';
+import 'cells/date_card_cell.dart';
+import 'cells/number_card_cell.dart';
+import 'cells/select_option_card_cell.dart';
+import 'cells/text_card_cell.dart';
+import 'cells/url_card_cell.dart';
 
-abstract class BoardCellBuilderDelegate {
-  CellCache get cellCache;
-}
-
-class BoardCellBuilder {
-  final BoardCellBuilderDelegate delegate;
+// T represents as the Generic card data
+class CardCellBuilder<CustomCardData> {
+  final CellCache cellCache;
 
-  BoardCellBuilder(this.delegate);
+  CardCellBuilder(this.cellCache);
 
-  Widget buildCell(
-    String groupId,
-    CellIdentifier cellId,
-    EditableCellNotifier cellNotifier,
-  ) {
+  Widget buildCell({
+    CustomCardData? cardData,
+    required CellIdentifier cellId,
+    EditableCardNotifier? cellNotifier,
+    CardConfiguration<CustomCardData>? cardConfiguration,
+  }) {
     final cellControllerBuilder = CellControllerBuilder(
       cellId: cellId,
-      cellCache: delegate.cellCache,
+      cellCache: cellCache,
     );
 
     final key = cellId.key();
     switch (cellId.fieldType) {
       case FieldType.Checkbox:
-        return BoardCheckboxCell(
-          groupId: groupId,
+        return CheckboxCardCell(
           cellControllerBuilder: cellControllerBuilder,
           key: key,
         );
       case FieldType.DateTime:
-        return BoardDateCell(
-          groupId: groupId,
+        return DateCardCell(
           cellControllerBuilder: cellControllerBuilder,
           key: key,
         );
       case FieldType.SingleSelect:
-        return BoardSelectOptionCell(
-          groupId: groupId,
+        return SelectOptionCardCell<CustomCardData>(
+          renderHook: cardConfiguration?.renderHook[FieldType.SingleSelect],
           cellControllerBuilder: cellControllerBuilder,
+          cardData: cardData,
           key: key,
         );
       case FieldType.MultiSelect:
-        return BoardSelectOptionCell(
-          groupId: groupId,
+        return SelectOptionCardCell<CustomCardData>(
+          renderHook: cardConfiguration?.renderHook[FieldType.MultiSelect],
           cellControllerBuilder: cellControllerBuilder,
+          cardData: cardData,
           editableNotifier: cellNotifier,
           key: key,
         );
       case FieldType.Checklist:
-        return BoardChecklistCell(
+        return ChecklistCardCell(
           cellControllerBuilder: cellControllerBuilder,
           key: key,
         );
       case FieldType.Number:
-        return BoardNumberCell(
-          groupId: groupId,
+        return NumberCardCell(
           cellControllerBuilder: cellControllerBuilder,
           key: key,
         );
       case FieldType.RichText:
-        return BoardTextCell(
-          groupId: groupId,
+        return TextCardCell(
           cellControllerBuilder: cellControllerBuilder,
           editableNotifier: cellNotifier,
           key: key,
         );
       case FieldType.URL:
-        return BoardUrlCell(
-          groupId: groupId,
+        return URLCardCell(
           cellControllerBuilder: cellControllerBuilder,
           key: key,
         );

+ 32 - 7
frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/card/board_cell.dart → frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/card_cell.dart

@@ -1,14 +1,39 @@
 import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
+import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pbenum.dart';
+import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart';
 import 'package:flutter/material.dart';
 
-abstract class FocusableBoardCell {
-  set becomeFocus(bool isFocus);
+typedef CellRenderHook<C, T> = Widget? Function(C cellData, T cardData);
+typedef RenderHookByFieldType<C> = Map<FieldType, CellRenderHook<dynamic, C>>;
+
+class CardConfiguration<CustomCardData> {
+  final RenderHookByFieldType<CustomCardData> renderHook = {};
+  CardConfiguration();
+
+  void addSelectOptionHook(
+    CellRenderHook<List<SelectOptionPB>, CustomCardData> hook,
+  ) {
+    selectOptionHook(cellData, cardData) {
+      if (cellData is List<SelectOptionPB>) {
+        hook(cellData, cardData);
+      }
+    }
+
+    renderHook[FieldType.SingleSelect] = selectOptionHook;
+    renderHook[FieldType.MultiSelect] = selectOptionHook;
+  }
+}
+
+abstract class CardCell<T> extends StatefulWidget {
+  final T? cardData;
+
+  const CardCell({super.key, this.cardData});
 }
 
-class EditableCellNotifier {
+class EditableCardNotifier {
   final ValueNotifier<bool> isCellEditing;
 
-  EditableCellNotifier({bool isEditing = false})
+  EditableCardNotifier({bool isEditing = false})
       : isCellEditing = ValueNotifier(isEditing);
 
   void dispose() {
@@ -17,7 +42,7 @@ class EditableCellNotifier {
 }
 
 class EditableRowNotifier {
-  final Map<EditableCellId, EditableCellNotifier> _cells = {};
+  final Map<EditableCellId, EditableCardNotifier> _cells = {};
   final ValueNotifier<bool> isEditing;
 
   EditableRowNotifier({required bool isEditing})
@@ -25,7 +50,7 @@ class EditableRowNotifier {
 
   void bindCell(
     CellIdentifier cellIdentifier,
-    EditableCellNotifier notifier,
+    EditableCardNotifier notifier,
   ) {
     assert(
       _cells.values.isEmpty,
@@ -80,7 +105,7 @@ abstract class EditableCell {
   // the row notifier receive its cells event. For example: begin editing the
   // cell or end editing the cell.
   //
-  EditableCellNotifier? get editableNotifier;
+  EditableCardNotifier? get editableNotifier;
 }
 
 class EditableCellId {

+ 13 - 13
frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/card/board_checkbox_cell.dart → frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/checkbox_card_cell.dart

@@ -1,33 +1,33 @@
 import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
-import 'package:appflowy/plugins/database_view/board/application/card/board_checkbox_cell_bloc.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra_ui/style_widget/icon_button.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
-class BoardCheckboxCell extends StatefulWidget {
-  final String groupId;
+import '../bloc/checkbox_card_cell_bloc.dart';
+import 'card_cell.dart';
+
+class CheckboxCardCell extends CardCell {
   final CellControllerBuilder cellControllerBuilder;
 
-  const BoardCheckboxCell({
-    required this.groupId,
+  const CheckboxCardCell({
     required this.cellControllerBuilder,
     Key? key,
   }) : super(key: key);
 
   @override
-  State<BoardCheckboxCell> createState() => _BoardCheckboxCellState();
+  State<CheckboxCardCell> createState() => _CheckboxCardCellState();
 }
 
-class _BoardCheckboxCellState extends State<BoardCheckboxCell> {
-  late BoardCheckboxCellBloc _cellBloc;
+class _CheckboxCardCellState extends State<CheckboxCardCell> {
+  late CheckboxCardCellBloc _cellBloc;
 
   @override
   void initState() {
     final cellController =
         widget.cellControllerBuilder.build() as CheckboxCellController;
-    _cellBloc = BoardCheckboxCellBloc(cellController: cellController);
-    _cellBloc.add(const BoardCheckboxCellEvent.initial());
+    _cellBloc = CheckboxCardCellBloc(cellController: cellController);
+    _cellBloc.add(const CheckboxCardCellEvent.initial());
     super.initState();
   }
 
@@ -35,7 +35,7 @@ class _BoardCheckboxCellState extends State<BoardCheckboxCell> {
   Widget build(BuildContext context) {
     return BlocProvider.value(
       value: _cellBloc,
-      child: BlocBuilder<BoardCheckboxCellBloc, BoardCheckboxCellState>(
+      child: BlocBuilder<CheckboxCardCellBloc, CheckboxCardCellState>(
         buildWhen: (previous, current) =>
             previous.isSelected != current.isSelected,
         builder: (context, state) {
@@ -49,8 +49,8 @@ class _BoardCheckboxCellState extends State<BoardCheckboxCell> {
               icon: icon,
               width: 20,
               onPressed: () => context
-                  .read<BoardCheckboxCellBloc>()
-                  .add(const BoardCheckboxCellEvent.select()),
+                  .read<CheckboxCardCellBloc>()
+                  .add(const CheckboxCardCellEvent.select()),
             ),
           );
         },

+ 9 - 8
frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/card/board_checklist_cell.dart → frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/checklist_card_cell.dart

@@ -1,27 +1,28 @@
 import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
+import 'package:appflowy/plugins/database_view/grid/presentation/widgets/cell/checklist_cell/checklist_progress_bar.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
 import '../../../grid/application/cell/checklist_cell_bloc.dart';
-import '../../../grid/presentation/widgets/cell/checklist_cell/checklist_progress_bar.dart';
+import 'card_cell.dart';
 
-class BoardChecklistCell extends StatefulWidget {
+class ChecklistCardCell extends CardCell {
   final CellControllerBuilder cellControllerBuilder;
-  const BoardChecklistCell({required this.cellControllerBuilder, Key? key})
+  const ChecklistCardCell({required this.cellControllerBuilder, Key? key})
       : super(key: key);
 
   @override
-  State<BoardChecklistCell> createState() => _BoardChecklistCellState();
+  State<ChecklistCardCell> createState() => _ChecklistCardCellState();
 }
 
-class _BoardChecklistCellState extends State<BoardChecklistCell> {
-  late ChecklistCellBloc _cellBloc;
+class _ChecklistCardCellState extends State<ChecklistCardCell> {
+  late ChecklistCardCellBloc _cellBloc;
 
   @override
   void initState() {
     final cellController =
         widget.cellControllerBuilder.build() as ChecklistCellController;
-    _cellBloc = ChecklistCellBloc(cellController: cellController);
+    _cellBloc = ChecklistCardCellBloc(cellController: cellController);
     _cellBloc.add(const ChecklistCellEvent.initial());
     super.initState();
   }
@@ -30,7 +31,7 @@ class _BoardChecklistCellState extends State<BoardChecklistCell> {
   Widget build(BuildContext context) {
     return BlocProvider.value(
       value: _cellBloc,
-      child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
+      child: BlocBuilder<ChecklistCardCellBloc, ChecklistCellState>(
         builder: (context, state) =>
             ChecklistProgressBar(percent: state.percent),
       ),

+ 12 - 13
frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/card/board_date_cell.dart → frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/date_card_cell.dart

@@ -1,35 +1,34 @@
 import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
-import 'package:appflowy/plugins/database_view/board/application/card/board_date_cell_bloc.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
-import 'define.dart';
+import '../bloc/date_card_cell_bloc.dart';
+import '../define.dart';
+import 'card_cell.dart';
 
-class BoardDateCell extends StatefulWidget {
-  final String groupId;
+class DateCardCell extends CardCell {
   final CellControllerBuilder cellControllerBuilder;
 
-  const BoardDateCell({
-    required this.groupId,
+  const DateCardCell({
     required this.cellControllerBuilder,
     Key? key,
   }) : super(key: key);
 
   @override
-  State<BoardDateCell> createState() => _BoardDateCellState();
+  State<DateCardCell> createState() => _DateCardCellState();
 }
 
-class _BoardDateCellState extends State<BoardDateCell> {
-  late BoardDateCellBloc _cellBloc;
+class _DateCardCellState extends State<DateCardCell> {
+  late DateCardCellBloc _cellBloc;
 
   @override
   void initState() {
     final cellController =
         widget.cellControllerBuilder.build() as DateCellController;
 
-    _cellBloc = BoardDateCellBloc(cellController: cellController)
-      ..add(const BoardDateCellEvent.initial());
+    _cellBloc = DateCardCellBloc(cellController: cellController)
+      ..add(const DateCardCellEvent.initial());
     super.initState();
   }
 
@@ -37,7 +36,7 @@ class _BoardDateCellState extends State<BoardDateCell> {
   Widget build(BuildContext context) {
     return BlocProvider.value(
       value: _cellBloc,
-      child: BlocBuilder<BoardDateCellBloc, BoardDateCellState>(
+      child: BlocBuilder<DateCardCellBloc, DateCardCellState>(
         buildWhen: (previous, current) => previous.dateStr != current.dateStr,
         builder: (context, state) {
           if (state.dateStr.isEmpty) {
@@ -47,7 +46,7 @@ class _BoardDateCellState extends State<BoardDateCell> {
               alignment: Alignment.centerLeft,
               child: Padding(
                 padding: EdgeInsets.symmetric(
-                  vertical: BoardSizes.cardCellVPadding,
+                  vertical: CardSizes.cardCellVPadding,
                 ),
                 child: FlowyText.regular(
                   state.dateStr,

+ 68 - 0
frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/number_card_cell.dart

@@ -0,0 +1,68 @@
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
+import 'package:flowy_infra_ui/style_widget/text.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import '../bloc/number_card_cell_bloc.dart';
+import '../define.dart';
+import 'card_cell.dart';
+
+class NumberCardCell extends CardCell {
+  final CellControllerBuilder cellControllerBuilder;
+
+  const NumberCardCell({
+    required this.cellControllerBuilder,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  State<NumberCardCell> createState() => _NumberCardCellState();
+}
+
+class _NumberCardCellState extends State<NumberCardCell> {
+  late NumberCardCellBloc _cellBloc;
+
+  @override
+  void initState() {
+    final cellController =
+        widget.cellControllerBuilder.build() as NumberCellController;
+
+    _cellBloc = NumberCardCellBloc(cellController: cellController)
+      ..add(const NumberCardCellEvent.initial());
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocProvider.value(
+      value: _cellBloc,
+      child: BlocBuilder<NumberCardCellBloc, NumberCardCellState>(
+        buildWhen: (previous, current) => previous.content != current.content,
+        builder: (context, state) {
+          if (state.content.isEmpty) {
+            return const SizedBox();
+          } else {
+            return Align(
+              alignment: Alignment.centerLeft,
+              child: Padding(
+                padding: EdgeInsets.symmetric(
+                  vertical: CardSizes.cardCellVPadding,
+                ),
+                child: FlowyText.medium(
+                  state.content,
+                  fontSize: 14,
+                ),
+              ),
+            );
+          }
+        },
+      ),
+    );
+  }
+
+  @override
+  Future<void> dispose() async {
+    _cellBloc.close();
+    super.dispose();
+  }
+}

+ 26 - 26
frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/card/board_select_option_cell.dart → frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/select_option_card_cell.dart

@@ -1,32 +1,35 @@
 import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
+import 'package:appflowy/plugins/database_view/grid/presentation/widgets/cell/select_option_cell/extension.dart';
+import 'package:appflowy/plugins/database_view/grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart';
+import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
-import '../../../grid/presentation/widgets/cell/select_option_cell/extension.dart';
-import '../../../grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart';
-import '../../application/card/board_select_option_cell_bloc.dart';
-import 'board_cell.dart';
+import '../bloc/select_option_card_cell_bloc.dart';
+import 'card_cell.dart';
 
-class BoardSelectOptionCell extends StatefulWidget with EditableCell {
-  final String groupId;
+class SelectOptionCardCell<T> extends CardCell<T> with EditableCell {
   final CellControllerBuilder cellControllerBuilder;
+  final CellRenderHook<List<SelectOptionPB>, T>? renderHook;
+
   @override
-  final EditableCellNotifier? editableNotifier;
+  final EditableCardNotifier? editableNotifier;
 
-  const BoardSelectOptionCell({
-    required this.groupId,
+  SelectOptionCardCell({
     required this.cellControllerBuilder,
+    required T? cardData,
+    this.renderHook,
     this.editableNotifier,
     Key? key,
-  }) : super(key: key);
+  }) : super(key: key, cardData: cardData);
 
   @override
-  State<BoardSelectOptionCell> createState() => _BoardSelectOptionCellState();
+  State<SelectOptionCardCell> createState() => _SelectOptionCardCellState();
 }
 
-class _BoardSelectOptionCellState extends State<BoardSelectOptionCell> {
-  late BoardSelectOptionCellBloc _cellBloc;
+class _SelectOptionCardCellState extends State<SelectOptionCardCell> {
+  late SelectOptionCardCellBloc _cellBloc;
   late PopoverController _popover;
 
   @override
@@ -34,8 +37,8 @@ class _BoardSelectOptionCellState extends State<BoardSelectOptionCell> {
     _popover = PopoverController();
     final cellController =
         widget.cellControllerBuilder.build() as SelectOptionCellController;
-    _cellBloc = BoardSelectOptionCellBloc(cellController: cellController)
-      ..add(const BoardSelectOptionCellEvent.initial());
+    _cellBloc = SelectOptionCardCellBloc(cellController: cellController)
+      ..add(const SelectOptionCardCellEvent.initial());
     super.initState();
   }
 
@@ -43,12 +46,17 @@ class _BoardSelectOptionCellState extends State<BoardSelectOptionCell> {
   Widget build(BuildContext context) {
     return BlocProvider.value(
       value: _cellBloc,
-      child: BlocBuilder<BoardSelectOptionCellBloc, BoardSelectOptionCellState>(
+      child: BlocBuilder<SelectOptionCardCellBloc, SelectOptionCardCellState>(
           buildWhen: (previous, current) {
         return previous.selectedOptions != current.selectedOptions;
       }, builder: (context, state) {
-        // Returns SizedBox if the content of the cell is empty
-        if (_isEmpty(state)) return const SizedBox();
+        Widget? custom = widget.renderHook?.call(
+          state.selectedOptions,
+          widget.cardData,
+        );
+        if (custom != null) {
+          return custom;
+        }
 
         final children = state.selectedOptions.map(
           (option) {
@@ -73,14 +81,6 @@ class _BoardSelectOptionCellState extends State<BoardSelectOptionCell> {
     );
   }
 
-  bool _isEmpty(BoardSelectOptionCellState state) {
-    // The cell should hide if the option id is equal to the groupId.
-    final isInGroup = state.selectedOptions
-        .where((element) => element.id == widget.groupId)
-        .isNotEmpty;
-    return isInGroup || state.selectedOptions.isEmpty;
-  }
-
   Widget _wrapPopover(Widget child) {
     final constraints = BoxConstraints.loose(Size(
       SelectOptionCellEditor.editorPanelWidth,

+ 20 - 22
frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/card/board_text_cell.dart → frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/text_card_cell.dart

@@ -5,29 +5,27 @@ import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:textstyle_extensions/textstyle_extensions.dart';
-import '../../application/card/board_text_cell_bloc.dart';
-import 'board_cell.dart';
-import 'define.dart';
+import '../bloc/text_card_cell_bloc.dart';
+import '../define.dart';
+import 'card_cell.dart';
 
-class BoardTextCell extends StatefulWidget with EditableCell {
-  final String groupId;
+class TextCardCell extends CardCell with EditableCell {
   @override
-  final EditableCellNotifier? editableNotifier;
+  final EditableCardNotifier? editableNotifier;
   final CellControllerBuilder cellControllerBuilder;
 
-  const BoardTextCell({
-    required this.groupId,
+  const TextCardCell({
     required this.cellControllerBuilder,
     this.editableNotifier,
     Key? key,
   }) : super(key: key);
 
   @override
-  State<BoardTextCell> createState() => _BoardTextCellState();
+  State<TextCardCell> createState() => _TextCardCellState();
 }
 
-class _BoardTextCellState extends State<BoardTextCell> {
-  late BoardTextCellBloc _cellBloc;
+class _TextCardCellState extends State<TextCardCell> {
+  late TextCardCellBloc _cellBloc;
   late TextEditingController _controller;
   bool focusWhenInit = false;
   final focusNode = SingleListenerFocusNode();
@@ -36,8 +34,8 @@ class _BoardTextCellState extends State<BoardTextCell> {
   void initState() {
     final cellController =
         widget.cellControllerBuilder.build() as TextCellController;
-    _cellBloc = BoardTextCellBloc(cellController: cellController)
-      ..add(const BoardTextCellEvent.initial());
+    _cellBloc = TextCardCellBloc(cellController: cellController)
+      ..add(const TextCardCellEvent.initial());
     _controller = TextEditingController(text: _cellBloc.state.content);
     focusWhenInit = widget.editableNotifier?.isCellEditing.value ?? false;
     if (focusWhenInit) {
@@ -51,7 +49,7 @@ class _BoardTextCellState extends State<BoardTextCell> {
       if (!focusNode.hasFocus) {
         focusWhenInit = false;
         widget.editableNotifier?.isCellEditing.value = false;
-        _cellBloc.add(const BoardTextCellEvent.enableEdit(false));
+        _cellBloc.add(const TextCardCellEvent.enableEdit(false));
       }
     });
     _bindEditableNotifier();
@@ -68,12 +66,12 @@ class _BoardTextCellState extends State<BoardTextCell> {
           focusNode.requestFocus();
         });
       }
-      _cellBloc.add(BoardTextCellEvent.enableEdit(isEditing));
+      _cellBloc.add(TextCardCellEvent.enableEdit(isEditing));
     });
   }
 
   @override
-  void didUpdateWidget(covariant BoardTextCell oldWidget) {
+  void didUpdateWidget(covariant TextCardCell oldWidget) {
     _bindEditableNotifier();
     super.didUpdateWidget(oldWidget);
   }
@@ -82,13 +80,13 @@ class _BoardTextCellState extends State<BoardTextCell> {
   Widget build(BuildContext context) {
     return BlocProvider.value(
       value: _cellBloc,
-      child: BlocListener<BoardTextCellBloc, BoardTextCellState>(
+      child: BlocListener<TextCardCellBloc, TextCardCellState>(
         listener: (context, state) {
           if (_controller.text != state.content) {
             _controller.text = state.content;
           }
         },
-        child: BlocBuilder<BoardTextCellBloc, BoardTextCellState>(
+        child: BlocBuilder<TextCardCellBloc, TextCardCellState>(
           buildWhen: (previous, current) {
             if (previous.content != current.content &&
                 _controller.text == current.content &&
@@ -120,7 +118,7 @@ class _BoardTextCellState extends State<BoardTextCell> {
   }
 
   Future<void> focusChanged() async {
-    _cellBloc.add(BoardTextCellEvent.updateText(_controller.text));
+    _cellBloc.add(TextCardCellEvent.updateText(_controller.text));
   }
 
   @override
@@ -131,10 +129,10 @@ class _BoardTextCellState extends State<BoardTextCell> {
     super.dispose();
   }
 
-  Widget _buildText(BoardTextCellState state) {
+  Widget _buildText(TextCardCellState state) {
     return Padding(
       padding: EdgeInsets.symmetric(
-        vertical: BoardSizes.cardCellVPadding,
+        vertical: CardSizes.cardCellVPadding,
       ),
       child: FlowyText.medium(
         state.content,
@@ -156,7 +154,7 @@ class _BoardTextCellState extends State<BoardTextCell> {
         decoration: InputDecoration(
           // Magic number 4 makes the textField take up the same space as FlowyText
           contentPadding: EdgeInsets.symmetric(
-            vertical: BoardSizes.cardCellVPadding + 4,
+            vertical: CardSizes.cardCellVPadding + 4,
           ),
           border: InputBorder.none,
           isDense: true,

+ 12 - 13
frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/card/board_url_cell.dart → frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/url_card_cell.dart

@@ -4,32 +4,31 @@ import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:textstyle_extensions/textstyle_extensions.dart';
 
-import '../../application/card/board_url_cell_bloc.dart';
-import 'define.dart';
+import '../bloc/url_card_cell_bloc.dart';
+import '../define.dart';
+import 'card_cell.dart';
 
-class BoardUrlCell extends StatefulWidget {
-  final String groupId;
+class URLCardCell extends CardCell {
   final CellControllerBuilder cellControllerBuilder;
 
-  const BoardUrlCell({
-    required this.groupId,
+  const URLCardCell({
     required this.cellControllerBuilder,
     Key? key,
   }) : super(key: key);
 
   @override
-  State<BoardUrlCell> createState() => _BoardUrlCellState();
+  State<URLCardCell> createState() => _URLCardCellState();
 }
 
-class _BoardUrlCellState extends State<BoardUrlCell> {
-  late BoardURLCellBloc _cellBloc;
+class _URLCardCellState extends State<URLCardCell> {
+  late URLCardCellBloc _cellBloc;
 
   @override
   void initState() {
     final cellController =
         widget.cellControllerBuilder.build() as URLCellController;
-    _cellBloc = BoardURLCellBloc(cellController: cellController);
-    _cellBloc.add(const BoardURLCellEvent.initial());
+    _cellBloc = URLCardCellBloc(cellController: cellController);
+    _cellBloc.add(const URLCardCellEvent.initial());
     super.initState();
   }
 
@@ -37,7 +36,7 @@ class _BoardUrlCellState extends State<BoardUrlCell> {
   Widget build(BuildContext context) {
     return BlocProvider.value(
       value: _cellBloc,
-      child: BlocBuilder<BoardURLCellBloc, BoardURLCellState>(
+      child: BlocBuilder<URLCardCellBloc, URLCardCellState>(
         buildWhen: (previous, current) => previous.content != current.content,
         builder: (context, state) {
           if (state.content.isEmpty) {
@@ -47,7 +46,7 @@ class _BoardUrlCellState extends State<BoardUrlCell> {
               alignment: Alignment.centerLeft,
               child: Padding(
                 padding: EdgeInsets.symmetric(
-                  vertical: BoardSizes.cardCellVPadding,
+                  vertical: CardSizes.cardCellVPadding,
                 ),
                 child: RichText(
                   textAlign: TextAlign.left,

+ 0 - 0
frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/card/container/accessory.dart → frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/container/accessory.dart


+ 0 - 0
frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/card/container/card_container.dart → frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/container/card_container.dart


+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/card/define.dart → frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/define.dart

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

+ 2 - 2
frontend/appflowy_flutter/lib/plugins/document/application/doc_service.dart

@@ -36,7 +36,7 @@ class DocumentService {
   }
 
   Future<Either<Unit, FlowyError>> closeDocument({required String docId}) {
-    final request = ViewIdPB(value: docId);
-    return FolderEventCloseView(request).send();
+    final payload = ViewIdPB(value: docId);
+    return FolderEventCloseView(payload).send();
   }
 }

+ 3 - 0
frontend/appflowy_flutter/macos/Runner.xcodeproj/project.pbxproj

@@ -416,6 +416,7 @@
 			isa = XCBuildConfiguration;
 			baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
 			buildSettings = {
+				ARCHS = arm64;
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				CLANG_ENABLE_MODULES = YES;
 				CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
@@ -549,6 +550,7 @@
 			isa = XCBuildConfiguration;
 			baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
 			buildSettings = {
+				ARCHS = arm64;
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				CLANG_ENABLE_MODULES = YES;
 				CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
@@ -573,6 +575,7 @@
 			isa = XCBuildConfiguration;
 			baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
 			buildSettings = {
+				ARCHS = arm64;
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				CLANG_ENABLE_MODULES = YES;
 				CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;

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

@@ -6,10 +6,11 @@ import 'package:appflowy/plugins/database_view/application/field/field_service.d
 import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
 import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
 import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart';
-import 'package:appflowy/plugins/database_view/board/application/board_data_controller.dart';
 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-database/setting_entities.pbenum.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart';
 
@@ -38,9 +39,14 @@ class AppFlowyBoardTest {
         .then((result) {
       return result.fold(
         (view) async {
-          final context =
-              BoardTestContext(view, BoardDataController(view: view));
-          final result = await context._boardDataController.openGrid();
+          final context = BoardTestContext(
+            view,
+            DatabaseController(
+              view: view,
+              layoutType: LayoutTypePB.Board,
+            ),
+          );
+          final result = await context._boardDataController.open();
           result.fold((l) => null, (r) => throw Exception(r));
           return context;
         },
@@ -62,7 +68,7 @@ Duration boardResponseDuration({int milliseconds = 200}) {
 
 class BoardTestContext {
   final ViewPB gridView;
-  final BoardDataController _boardDataController;
+  final DatabaseController _boardDataController;
 
   BoardTestContext(this.gridView, this._boardDataController);
 
@@ -108,7 +114,8 @@ class BoardTestContext {
     final rowCache = _boardDataController.rowCache;
 
     final rowDataController = RowDataController(
-      rowInfo: rowInfo,
+      viewId: rowInfo.viewId,
+      rowId: rowInfo.rowPB.id,
       rowCache: rowCache,
     );
 

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

@@ -1,7 +1,8 @@
 import 'package:appflowy/plugins/database_view/application/filter/filter_service.dart';
 import 'package:appflowy/plugins/database_view/grid/application/grid_bloc.dart';
-import 'package:appflowy/plugins/database_view/grid/application/grid_data_controller.dart';
+import 'package:appflowy/plugins/database_view/application/database_controller.dart';
 import 'package:appflowy_backend/protobuf/flowy-database/checkbox_filter.pbenum.dart';
+import 'package:appflowy_backend/protobuf/flowy-database/setting_entities.pbenum.dart';
 import 'package:appflowy_backend/protobuf/flowy-database/text_filter.pb.dart';
 import 'package:flutter_test/flutter_test.dart';
 
@@ -50,7 +51,10 @@ void main() {
   test('filter rows with condition: text is empty', () async {
     final context = await gridTest.createTestGrid();
     final service = FilterBackendService(viewId: context.gridView.id);
-    final gridController = DatabaseController(view: context.gridView);
+    final gridController = DatabaseController(
+      view: context.gridView,
+      layoutType: LayoutTypePB.Grid,
+    );
     final gridBloc = GridBloc(
       view: context.gridView,
       databaseController: gridController,
@@ -71,7 +75,10 @@ void main() {
       () async {
     final context = await gridTest.createTestGrid();
     final service = FilterBackendService(viewId: context.gridView.id);
-    final gridController = DatabaseController(view: context.gridView);
+    final gridController = DatabaseController(
+      view: context.gridView,
+      layoutType: LayoutTypePB.Grid,
+    );
     final gridBloc = GridBloc(
       view: context.gridView,
       databaseController: gridController,
@@ -112,7 +119,10 @@ void main() {
     final context = await gridTest.createTestGrid();
     final checkboxField = context.checkboxFieldContext();
     final service = FilterBackendService(viewId: context.gridView.id);
-    final gridController = DatabaseController(view: context.gridView);
+    final gridController = DatabaseController(
+      view: context.gridView,
+      layoutType: LayoutTypePB.Grid,
+    );
     final gridBloc = GridBloc(
       view: context.gridView,
       databaseController: gridController,
@@ -131,7 +141,10 @@ void main() {
     final context = await gridTest.createTestGrid();
     final checkboxField = context.checkboxFieldContext();
     final service = FilterBackendService(viewId: context.gridView.id);
-    final gridController = DatabaseController(view: context.gridView);
+    final gridController = DatabaseController(
+      view: context.gridView,
+      layoutType: LayoutTypePB.Grid,
+    );
     final gridBloc = GridBloc(
       view: context.gridView,
       databaseController: gridController,

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

@@ -1,6 +1,7 @@
-import 'package:appflowy/plugins/database_view/grid/application/grid_data_controller.dart';
+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-database/setting_entities.pbenum.dart';
 
 import '../util.dart';
 
@@ -16,8 +17,13 @@ Future<GridTestContext> createTestFilterGrid(AppFlowyGridTest gridTest) async {
       .then((result) {
     return result.fold(
       (view) async {
-        final context = GridTestContext(view, DatabaseController(view: view));
-        final result = await context.gridController.openGrid();
+        final context = GridTestContext(
+            view,
+            DatabaseController(
+              view: view,
+              layoutType: LayoutTypePB.Grid,
+            ));
+        final result = await context.gridController.open();
 
         await editCells(context);
         await gridResponseFuture(milliseconds: 500);

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

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

+ 15 - 5
frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart

@@ -6,12 +6,16 @@ import 'package:appflowy/plugins/database_view/application/field/field_service.d
 import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
 import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
 import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart';
-import 'package:appflowy/plugins/database_view/grid/application/grid_data_controller.dart';
+import 'package:appflowy/plugins/database_view/application/database_controller.dart';
 import 'package:appflowy/plugins/database_view/grid/application/row/row_bloc.dart';
 import 'package:appflowy/plugins/database_view/grid/grid.dart';
 import 'package:appflowy/workspace/application/app/app_service.dart';
+import 'package:appflowy_backend/protobuf/flowy-database/row_entities.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-database/setting_entities.pbenum.dart';
+import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart';
+import 'package:dartz/dartz.dart';
 
 import '../../util.dart';
 
@@ -31,7 +35,7 @@ class GridTestContext {
     return gridController.fieldController;
   }
 
-  Future<void> createRow() async {
+  Future<Either<RowPB, FlowyError>> createRow() async {
     return gridController.createRow();
   }
 
@@ -69,7 +73,8 @@ class GridTestContext {
     final rowCache = gridController.rowCache;
 
     final rowDataController = RowDataController(
-      rowInfo: rowInfo,
+      rowId: rowInfo.rowPB.id,
+      viewId: rowInfo.viewId,
       rowCache: rowCache,
     );
 
@@ -169,8 +174,13 @@ class AppFlowyGridTest {
         .then((result) {
       return result.fold(
         (view) async {
-          final context = GridTestContext(view, DatabaseController(view: view));
-          final result = await context.gridController.openGrid();
+          final context = GridTestContext(
+              view,
+              DatabaseController(
+                view: view,
+                layoutType: LayoutTypePB.Grid,
+              ));
+          final result = await context.gridController.open();
           result.fold((l) => null, (r) => throw Exception(r));
           return context;
         },

+ 4 - 11
frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_bd_svc.ts

@@ -1,6 +1,4 @@
 import {
-  CreateBoardCardPayloadPB,
-  DatabaseEventCreateBoardCard,
   DatabaseEventCreateRow,
   DatabaseEventGetDatabase,
   DatabaseEventGetFields,
@@ -41,17 +39,12 @@ export class DatabaseBackendService {
     return FolderEventCloseView(payload);
   };
 
-  createRow = async (rowId?: string) => {
+  createRow = async (rowId?: string, groupId?: string) => {
     const payload = CreateRowPayloadPB.fromObject({ view_id: this.viewId, start_row_id: rowId ?? undefined });
-    return DatabaseEventCreateRow(payload);
-  };
-
-  createGroupRow = async (groupId: string, startRowId?: string) => {
-    const payload = CreateBoardCardPayloadPB.fromObject({ view_id: this.viewId, group_id: groupId });
-    if (startRowId !== undefined) {
-      payload.start_row_id = startRowId;
+    if (groupId !== undefined) {
+      payload.group_id = groupId;
     }
-    return DatabaseEventCreateBoardCard(payload);
+    return DatabaseEventCreateRow(payload);
   };
 
   moveRow = (rowId: string, groupId?: string) => {

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

@@ -94,7 +94,7 @@ export class DatabaseGroupController {
   };
 
   createRow = async () => {
-    return this.databaseBackendSvc.createGroupRow(this.group.group_id);
+    return this.databaseBackendSvc.createRow(this.group.group_id);
   };
 
   subscribe = (callbacks: GroupDataCallbacks) => {

+ 6 - 1
frontend/rust-lib/flowy-client-sync/src/client_database/database_builder.rs

@@ -1,7 +1,7 @@
 use crate::errors::{SyncError, SyncResult};
 use database_model::{
   BuildDatabaseContext, DatabaseBlockMetaRevision, DatabaseBlockRevision, FieldRevision,
-  RowRevision,
+  LayoutSetting, RowRevision,
 };
 use std::sync::Arc;
 
@@ -30,6 +30,7 @@ impl DatabaseBuilder {
   pub fn new() -> Self {
     Self::default()
   }
+
   pub fn add_field(&mut self, field: FieldRevision) {
     self.build_context.field_revs.push(Arc::new(field));
   }
@@ -54,6 +55,10 @@ impl DatabaseBuilder {
     &self.build_context.block_metas.first().unwrap().block_id
   }
 
+  pub fn set_layout_setting(&mut self, layout_setting: LayoutSetting) {
+    self.build_context.layout_setting = layout_setting;
+  }
+
   pub fn build(self) -> BuildDatabaseContext {
     self.build_context
   }

+ 2 - 2
frontend/rust-lib/flowy-client-sync/src/client_database/database_revision_pad.rs

@@ -176,8 +176,8 @@ impl DatabaseRevisionPad {
     T: Into<FieldTypeRevision>,
   {
     let new_field_type = new_field_type.into();
-    self.modify_database(|grid_meta| {
-      match grid_meta
+    self.modify_database(|database_rev| {
+      match database_rev
         .fields
         .iter_mut()
         .find(|field_rev| field_rev.id == field_id)

+ 9 - 5
frontend/rust-lib/flowy-client-sync/src/client_database/database_view_revision_pad.rs

@@ -312,7 +312,7 @@ impl DatabaseViewRevisionPad {
   }
 
   /// updates the settings for the given layout type
-  pub fn update_layout_setting<T>(
+  pub fn set_layout_setting<T>(
     &mut self,
     layout: &LayoutRevision,
     settings: &T,
@@ -364,14 +364,18 @@ pub struct DatabaseViewRevisionChangeset {
   pub md5: String,
 }
 
-pub fn make_database_view_rev_json_str(grid_revision: &DatabaseViewRevision) -> SyncResult<String> {
-  let json = serde_json::to_string(grid_revision).map_err(|err| {
+pub fn make_database_view_rev_json_str(
+  database_view_rev: &DatabaseViewRevision,
+) -> SyncResult<String> {
+  let json = serde_json::to_string(database_view_rev).map_err(|err| {
     internal_sync_error(format!("Serialize grid view to json str failed. {:?}", err))
   })?;
   Ok(json)
 }
 
-pub fn make_database_view_operations(grid_view: &DatabaseViewRevision) -> DatabaseViewOperations {
-  let json = serde_json::to_string(grid_view).unwrap();
+pub fn make_database_view_operations(
+  database_view_rev: &DatabaseViewRevision,
+) -> DatabaseViewOperations {
+  let json = serde_json::to_string(database_view_rev).unwrap();
   DatabaseViewOperationsBuilder::new().insert(&json).build()
 }

+ 100 - 49
frontend/rust-lib/flowy-database/src/entities/calendar_entities.rs

@@ -1,17 +1,15 @@
 use crate::entities::parser::NotEmptyStr;
+use database_model::{CalendarLayout, CalendarLayoutSetting};
 use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
 use flowy_error::ErrorCode;
-use serde::{Deserialize, Serialize};
-use serde_repr::{Deserialize_repr, Serialize_repr};
-use std::convert::TryInto;
 
 #[derive(Debug, Clone, Eq, PartialEq, Default, ProtoBuf)]
-pub struct CalendarSettingsPB {
+pub struct CalendarLayoutSettingsPB {
   #[pb(index = 1)]
-  pub view_id: String,
+  pub layout_field_id: String,
 
   #[pb(index = 2)]
-  pub layout_ty: CalendarLayout,
+  pub layout_ty: CalendarLayoutPB,
 
   #[pb(index = 3)]
   pub first_day_of_week: i32,
@@ -23,51 +21,23 @@ pub struct CalendarSettingsPB {
   pub show_week_numbers: bool,
 }
 
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct CalendarSettingsParams {
-  pub(crate) view_id: String,
-  layout_ty: CalendarLayout,
-  first_day_of_week: i32,
-  show_weekends: bool,
-  show_week_numbers: bool,
-}
-
-const DEFAULT_FIRST_DAY_OF_WEEK: i32 = 0;
-const DEFAULT_SHOW_WEEKENDS: bool = true;
-const DEFAULT_SHOW_WEEK_NUMBERS: bool = true;
-
-impl CalendarSettingsParams {
-  pub fn default_with(view_id: String) -> Self {
-    CalendarSettingsParams {
-      view_id,
-      layout_ty: CalendarLayout::default(),
-      first_day_of_week: DEFAULT_FIRST_DAY_OF_WEEK,
-      show_weekends: DEFAULT_SHOW_WEEKENDS,
-      show_week_numbers: DEFAULT_SHOW_WEEK_NUMBERS,
+impl std::convert::From<CalendarLayoutSettingsPB> for CalendarLayoutSetting {
+  fn from(pb: CalendarLayoutSettingsPB) -> Self {
+    CalendarLayoutSetting {
+      layout_ty: pb.layout_ty.into(),
+      first_day_of_week: pb.first_day_of_week,
+      show_weekends: pb.show_weekends,
+      show_week_numbers: pb.show_week_numbers,
+      layout_field_id: pb.layout_field_id,
     }
   }
 }
 
-impl TryInto<CalendarSettingsParams> for CalendarSettingsPB {
-  type Error = ErrorCode;
-
-  fn try_into(self) -> Result<CalendarSettingsParams, Self::Error> {
-    let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::ViewIdIsInvalid)?;
-    Ok(CalendarSettingsParams {
-      view_id: view_id.0,
-      layout_ty: self.layout_ty,
-      first_day_of_week: self.first_day_of_week,
-      show_weekends: self.show_weekends,
-      show_week_numbers: self.show_week_numbers,
-    })
-  }
-}
-
-impl std::convert::From<CalendarSettingsParams> for CalendarSettingsPB {
-  fn from(params: CalendarSettingsParams) -> Self {
-    CalendarSettingsPB {
-      view_id: params.view_id,
-      layout_ty: params.layout_ty,
+impl std::convert::From<CalendarLayoutSetting> for CalendarLayoutSettingsPB {
+  fn from(params: CalendarLayoutSetting) -> Self {
+    CalendarLayoutSettingsPB {
+      layout_field_id: params.layout_field_id,
+      layout_ty: params.layout_ty.into(),
       first_day_of_week: params.first_day_of_week,
       show_weekends: params.show_weekends,
       show_week_numbers: params.show_week_numbers,
@@ -75,11 +45,92 @@ impl std::convert::From<CalendarSettingsParams> for CalendarSettingsPB {
   }
 }
 
-#[derive(Debug, Clone, Eq, PartialEq, Default, ProtoBuf_Enum, Serialize_repr, Deserialize_repr)]
+#[derive(Debug, Clone, Eq, PartialEq, Default, ProtoBuf_Enum)]
 #[repr(u8)]
-pub enum CalendarLayout {
+pub enum CalendarLayoutPB {
   #[default]
   MonthLayout = 0,
   WeekLayout = 1,
   DayLayout = 2,
 }
+
+impl std::convert::From<CalendarLayoutPB> for CalendarLayout {
+  fn from(pb: CalendarLayoutPB) -> Self {
+    match pb {
+      CalendarLayoutPB::MonthLayout => CalendarLayout::MonthLayout,
+      CalendarLayoutPB::WeekLayout => CalendarLayout::WeekLayout,
+      CalendarLayoutPB::DayLayout => CalendarLayout::DayLayout,
+    }
+  }
+}
+impl std::convert::From<CalendarLayout> for CalendarLayoutPB {
+  fn from(layout: CalendarLayout) -> Self {
+    match layout {
+      CalendarLayout::MonthLayout => CalendarLayoutPB::MonthLayout,
+      CalendarLayout::WeekLayout => CalendarLayoutPB::WeekLayout,
+      CalendarLayout::DayLayout => CalendarLayoutPB::DayLayout,
+    }
+  }
+}
+
+#[derive(Debug, Clone, Default, ProtoBuf)]
+pub struct CalendarEventRequestPB {
+  #[pb(index = 1)]
+  pub view_id: String,
+
+  // Currently, requesting the events within the specified month
+  // is not supported
+  #[pb(index = 2)]
+  pub month: String,
+}
+
+#[derive(Debug, Clone, Default)]
+pub struct CalendarEventRequestParams {
+  pub view_id: String,
+  pub month: String,
+}
+
+impl TryInto<CalendarEventRequestParams> for CalendarEventRequestPB {
+  type Error = ErrorCode;
+
+  fn try_into(self) -> Result<CalendarEventRequestParams, Self::Error> {
+    let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::ViewIdIsInvalid)?;
+    Ok(CalendarEventRequestParams {
+      view_id: view_id.0,
+      month: self.month,
+    })
+  }
+}
+
+#[derive(Debug, Clone, Default, ProtoBuf)]
+pub struct CalendarEventPB {
+  #[pb(index = 1)]
+  pub row_id: String,
+
+  #[pb(index = 2)]
+  pub title_field_id: String,
+
+  #[pb(index = 3)]
+  pub title: String,
+
+  #[pb(index = 4)]
+  pub timestamp: i64,
+}
+
+#[derive(Debug, Clone, Default, ProtoBuf)]
+pub struct RepeatedCalendarEventPB {
+  #[pb(index = 1)]
+  pub items: Vec<CalendarEventPB>,
+}
+
+#[derive(Debug, Clone, Default, ProtoBuf)]
+pub struct MoveCalendarEventPB {
+  #[pb(index = 1)]
+  pub row_id: String,
+
+  #[pb(index = 2)]
+  pub field_id: String,
+
+  #[pb(index = 3)]
+  pub timestamp: i64,
+}

+ 9 - 1
frontend/rust-lib/flowy-database/src/entities/database_entities.rs

@@ -1,5 +1,5 @@
 use crate::entities::parser::NotEmptyStr;
-use crate::entities::{FieldIdPB, RowPB};
+use crate::entities::{FieldIdPB, LayoutTypePB, RowPB};
 use flowy_derive::ProtoBuf;
 use flowy_error::ErrorCode;
 
@@ -195,3 +195,11 @@ impl TryInto<DatabaseGroupIdParams> for DatabaseGroupIdPB {
     })
   }
 }
+#[derive(Clone, ProtoBuf, Default, Debug)]
+pub struct DatabaseLayoutIdPB {
+  #[pb(index = 1)]
+  pub view_id: String,
+
+  #[pb(index = 2)]
+  pub layout: LayoutTypePB,
+}

+ 1 - 36
frontend/rust-lib/flowy-database/src/entities/group_entities/group.rs

@@ -1,5 +1,5 @@
 use crate::entities::parser::NotEmptyStr;
-use crate::entities::{CreateRowParams, FieldType, LayoutTypePB, RowPB};
+use crate::entities::{FieldType, RowPB};
 use crate::services::group::Group;
 use database_model::{FieldTypeRevision, GroupConfigurationRevision};
 use flowy_derive::ProtoBuf;
@@ -7,41 +7,6 @@ use flowy_error::ErrorCode;
 use std::convert::TryInto;
 use std::sync::Arc;
 
-#[derive(ProtoBuf, Debug, Default, Clone)]
-pub struct CreateBoardCardPayloadPB {
-  #[pb(index = 1)]
-  pub view_id: String,
-
-  #[pb(index = 2)]
-  pub group_id: String,
-
-  #[pb(index = 3, one_of)]
-  pub start_row_id: Option<String>,
-}
-
-impl TryInto<CreateRowParams> for CreateBoardCardPayloadPB {
-  type Error = ErrorCode;
-
-  fn try_into(self) -> Result<CreateRowParams, Self::Error> {
-    let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseIdIsEmpty)?;
-    let group_id = NotEmptyStr::parse(self.group_id).map_err(|_| ErrorCode::GroupIdIsEmpty)?;
-    let start_row_id = match self.start_row_id {
-      None => None,
-      Some(start_row_id) => Some(
-        NotEmptyStr::parse(start_row_id)
-          .map_err(|_| ErrorCode::RowIdIsEmpty)?
-          .0,
-      ),
-    };
-    Ok(CreateRowParams {
-      view_id: view_id.0,
-      start_row_id,
-      group_id: Some(group_id.0),
-      layout: LayoutTypePB::Board,
-    })
-  }
-}
-
 #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
 pub struct GroupConfigurationPB {
   #[pb(index = 1)]

+ 26 - 5
frontend/rust-lib/flowy-database/src/entities/row_entities.rs

@@ -1,8 +1,9 @@
 use crate::entities::parser::NotEmptyStr;
-use crate::entities::LayoutTypePB;
+
 use database_model::RowRevision;
 use flowy_derive::ProtoBuf;
 use flowy_error::ErrorCode;
+use std::collections::HashMap;
 use std::sync::Arc;
 
 /// [RowPB] Describes a row. Has the id of the parent Block. Has the metadata of the row.
@@ -177,6 +178,18 @@ pub struct CreateRowPayloadPB {
 
   #[pb(index = 2, one_of)]
   pub start_row_id: Option<String>,
+
+  #[pb(index = 3, one_of)]
+  pub group_id: Option<String>,
+
+  #[pb(index = 4, one_of)]
+  pub data: Option<RowDataPB>,
+}
+
+#[derive(ProtoBuf, Default)]
+pub struct RowDataPB {
+  #[pb(index = 1)]
+  pub cell_data_by_field_id: HashMap<String, String>,
 }
 
 #[derive(Default)]
@@ -184,7 +197,7 @@ pub struct CreateRowParams {
   pub view_id: String,
   pub start_row_id: Option<String>,
   pub group_id: Option<String>,
-  pub layout: LayoutTypePB,
+  pub cell_data_by_field_id: Option<HashMap<String, String>>,
 }
 
 impl TryInto<CreateRowParams> for CreateRowPayloadPB {
@@ -192,12 +205,20 @@ impl TryInto<CreateRowParams> for CreateRowPayloadPB {
 
   fn try_into(self) -> Result<CreateRowParams, Self::Error> {
     let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::ViewIdIsInvalid)?;
+    let start_row_id = match self.start_row_id {
+      None => None,
+      Some(start_row_id) => Some(
+        NotEmptyStr::parse(start_row_id)
+          .map_err(|_| ErrorCode::RowIdIsEmpty)?
+          .0,
+      ),
+    };
 
     Ok(CreateRowParams {
       view_id: view_id.0,
-      start_row_id: self.start_row_id,
-      group_id: None,
-      layout: LayoutTypePB::Grid,
+      start_row_id,
+      group_id: self.group_id,
+      cell_data_by_field_id: self.data.map(|data| data.cell_data_by_field_id),
     })
   }
 }

+ 59 - 6
frontend/rust-lib/flowy-database/src/entities/setting_entities.rs

@@ -1,11 +1,11 @@
 use crate::entities::parser::NotEmptyStr;
 use crate::entities::{
-  AlterFilterParams, AlterFilterPayloadPB, AlterSortParams, AlterSortPayloadPB, CalendarSettingsPB,
-  DeleteFilterParams, DeleteFilterPayloadPB, DeleteGroupParams, DeleteGroupPayloadPB,
-  DeleteSortParams, DeleteSortPayloadPB, InsertGroupParams, InsertGroupPayloadPB, RepeatedFilterPB,
-  RepeatedGroupConfigurationPB, RepeatedSortPB,
+  AlterFilterParams, AlterFilterPayloadPB, AlterSortParams, AlterSortPayloadPB,
+  CalendarLayoutSettingsPB, DeleteFilterParams, DeleteFilterPayloadPB, DeleteGroupParams,
+  DeleteGroupPayloadPB, DeleteSortParams, DeleteSortPayloadPB, InsertGroupParams,
+  InsertGroupPayloadPB, RepeatedFilterPB, RepeatedGroupConfigurationPB, RepeatedSortPB,
 };
-use database_model::LayoutRevision;
+use database_model::{CalendarLayoutSetting, LayoutRevision};
 use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
 use flowy_error::ErrorCode;
 use std::convert::TryInto;
@@ -159,10 +159,42 @@ impl DatabaseSettingChangesetParams {
   }
 }
 
+#[derive(Debug, Eq, PartialEq, Default, ProtoBuf, Clone)]
+pub struct UpdateLayoutSettingPB {
+  #[pb(index = 1)]
+  pub view_id: String,
+
+  #[pb(index = 2)]
+  pub layout_setting: LayoutSettingPB,
+}
+
+#[derive(Debug)]
+pub struct UpdateLayoutSettingParams {
+  pub view_id: String,
+  pub layout_setting: LayoutSettingParams,
+}
+
+impl TryInto<UpdateLayoutSettingParams> for UpdateLayoutSettingPB {
+  type Error = ErrorCode;
+
+  fn try_into(self) -> Result<UpdateLayoutSettingParams, Self::Error> {
+    let view_id = NotEmptyStr::parse(self.view_id)
+      .map_err(|_| ErrorCode::ViewIdIsInvalid)?
+      .0;
+
+    let layout_setting: LayoutSettingParams = self.layout_setting.into();
+
+    Ok(UpdateLayoutSettingParams {
+      view_id,
+      layout_setting,
+    })
+  }
+}
+
 #[derive(Debug, Eq, PartialEq, Default, ProtoBuf, Clone)]
 pub struct LayoutSettingPB {
   #[pb(index = 1, one_of)]
-  pub calendar: Option<CalendarSettingsPB>,
+  pub calendar: Option<CalendarLayoutSettingsPB>,
 }
 
 impl LayoutSettingPB {
@@ -170,3 +202,24 @@ impl LayoutSettingPB {
     Self::default()
   }
 }
+
+impl std::convert::From<LayoutSettingParams> for LayoutSettingPB {
+  fn from(params: LayoutSettingParams) -> Self {
+    Self {
+      calendar: params.calendar.map(|calender| calender.into()),
+    }
+  }
+}
+
+impl std::convert::From<LayoutSettingPB> for LayoutSettingParams {
+  fn from(params: LayoutSettingPB) -> Self {
+    Self {
+      calendar: params.calendar.map(|calender| calender.into()),
+    }
+  }
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct LayoutSettingParams {
+  pub calendar: Option<CalendarLayoutSetting>,
+}

+ 47 - 25
frontend/rust-lib/flowy-database/src/event_handler.rs

@@ -329,7 +329,7 @@ pub(crate) async fn move_row_handler(
 }
 
 #[tracing::instrument(level = "debug", skip(data, manager), err)]
-pub(crate) async fn create_table_row_handler(
+pub(crate) async fn create_row_handler(
   data: AFPluginData<CreateRowPayloadPB>,
   manager: AFPluginState<Arc<DatabaseManager>>,
 ) -> DataResult<RowPB, FlowyError> {
@@ -549,17 +549,6 @@ pub(crate) async fn get_group_handler(
   data_result_ok(group)
 }
 
-#[tracing::instrument(level = "debug", skip(data, manager), err)]
-pub(crate) async fn create_board_card_handler(
-  data: AFPluginData<CreateBoardCardPayloadPB>,
-  manager: AFPluginState<Arc<DatabaseManager>>,
-) -> DataResult<RowPB, FlowyError> {
-  let params: CreateRowParams = data.into_inner().try_into()?;
-  let editor = manager.get_database_editor(params.view_id.as_ref()).await?;
-  let row = editor.create_row(params).await?;
-  data_result_ok(row)
-}
-
 #[tracing::instrument(level = "debug", skip(data, manager), err)]
 pub(crate) async fn move_group_handler(
   data: AFPluginData<MoveGroupPayloadPB>,
@@ -599,23 +588,56 @@ pub(crate) async fn get_databases_handler(
 }
 
 #[tracing::instrument(level = "debug", skip(data, manager), err)]
-pub(crate) async fn set_calendar_setting_handler(
-  data: AFPluginData<CalendarSettingsPB>,
+pub(crate) async fn set_layout_setting_handler(
+  data: AFPluginData<UpdateLayoutSettingPB>,
   manager: AFPluginState<Arc<DatabaseManager>>,
 ) -> FlowyResult<()> {
-  let params: CalendarSettingsParams = data.into_inner().try_into()?;
-  let _ = manager.get_database_editor(params.view_id.as_ref()).await?;
-  //TODO(nathan):
-  todo!("nathan: depends on the main branch refactoring")
+  let params: UpdateLayoutSettingParams = data.into_inner().try_into()?;
+  let database_editor = manager.get_database_editor(params.view_id.as_ref()).await?;
+  database_editor
+    .set_layout_setting(&params.view_id, params.layout_setting)
+    .await?;
+  Ok(())
 }
 
 #[tracing::instrument(level = "debug", skip(data, manager), err)]
-pub(crate) async fn get_calendar_setting_handler(
-  data: AFPluginData<DatabaseViewIdPB>,
+pub(crate) async fn get_layout_setting_handler(
+  data: AFPluginData<DatabaseLayoutIdPB>,
+  manager: AFPluginState<Arc<DatabaseManager>>,
+) -> DataResult<LayoutSettingPB, FlowyError> {
+  let params = data.into_inner();
+  let database_editor = manager.get_database_editor(&params.view_id).await?;
+  let layout_setting = database_editor
+    .get_layout_setting(&params.view_id, params.layout)
+    .await?;
+  data_result_ok(layout_setting.into())
+}
+
+#[tracing::instrument(level = "debug", skip(data, manager), err)]
+pub(crate) async fn get_calendar_events_handler(
+  data: AFPluginData<CalendarEventRequestPB>,
   manager: AFPluginState<Arc<DatabaseManager>>,
-) -> FlowyResult<()> {
-  let view_id = data.into_inner().value;
-  let _ = manager.get_database_editor(view_id.as_ref()).await?;
-  //TODO(nathan):
-  todo!("nathan: depends on the main branch refactoring")
+) -> DataResult<RepeatedCalendarEventPB, FlowyError> {
+  let params: CalendarEventRequestParams = data.into_inner().try_into()?;
+  let database_editor = manager.get_database_editor(&params.view_id).await?;
+  let events = database_editor
+    .get_all_calendar_events(&params.view_id)
+    .await;
+  data_result_ok(RepeatedCalendarEventPB { items: events })
+}
+
+#[tracing::instrument(level = "debug", skip(data, manager), err)]
+pub(crate) async fn get_calendar_event_handler(
+  data: AFPluginData<RowIdPB>,
+  manager: AFPluginState<Arc<DatabaseManager>>,
+) -> DataResult<CalendarEventPB, FlowyError> {
+  let params: RowIdParams = data.into_inner().try_into()?;
+  let database_editor = manager.get_database_editor(&params.view_id).await?;
+  let event = database_editor
+    .get_calendar_event(&params.view_id, &params.row_id)
+    .await;
+  match event {
+    None => Err(FlowyError::record_not_found()),
+    Some(event) => data_result_ok(event),
+  }
 }

+ 19 - 11
frontend/rust-lib/flowy-database/src/event_map.rs

@@ -28,7 +28,7 @@ pub fn init(database_manager: Arc<DatabaseManager>) -> AFPlugin {
         .event(DatabaseEvent::GetTypeOption, get_field_type_option_data_handler)
         .event(DatabaseEvent::CreateTypeOption, create_field_type_option_data_handler)
         // Row
-        .event(DatabaseEvent::CreateRow, create_table_row_handler)
+        .event(DatabaseEvent::CreateRow, create_row_handler)
         .event(DatabaseEvent::GetRow, get_row_handler)
         .event(DatabaseEvent::DeleteRow, delete_row_handler)
         .event(DatabaseEvent::DuplicateRow, duplicate_row_handler)
@@ -44,7 +44,6 @@ pub fn init(database_manager: Arc<DatabaseManager>) -> AFPlugin {
         // Date
         .event(DatabaseEvent::UpdateDateCell, update_date_cell_handler)
         // Group
-        .event(DatabaseEvent::CreateBoardCard, create_board_card_handler)
         .event(DatabaseEvent::MoveGroup, move_group_handler)
         .event(DatabaseEvent::MoveGroupRow, move_group_row_handler)
         .event(DatabaseEvent::GetGroups, get_groups_handler)
@@ -52,8 +51,11 @@ pub fn init(database_manager: Arc<DatabaseManager>) -> AFPlugin {
         // Database
         .event(DatabaseEvent::GetDatabases, get_databases_handler)
         // Calendar
-        .event(DatabaseEvent::SetCalenderSetting, set_calendar_setting_handler)
-        .event(DatabaseEvent::GetCalendarSetting, get_calendar_setting_handler);
+        .event(DatabaseEvent::GetAllCalendarEvents, get_calendar_events_handler)
+        .event(DatabaseEvent::GetCalendarEvent, get_calendar_event_handler)
+        // Layout setting
+        .event(DatabaseEvent::SetLayoutSetting, set_layout_setting_handler)
+        .event(DatabaseEvent::GetLayoutSetting, get_layout_setting_handler);
 
   plugin
 }
@@ -227,9 +229,6 @@ pub enum DatabaseEvent {
   #[event(input = "DatabaseGroupIdPB", output = "GroupPB")]
   GetGroup = 101,
 
-  #[event(input = "CreateBoardCardPayloadPB", output = "RowPB")]
-  CreateBoardCard = 110,
-
   #[event(input = "MoveGroupPayloadPB")]
   MoveGroup = 111,
 
@@ -242,9 +241,18 @@ pub enum DatabaseEvent {
   #[event(output = "RepeatedDatabaseDescPB")]
   GetDatabases = 114,
 
-  #[event(input = "CalendarSettingsPB")]
-  SetCalenderSetting = 115,
+  #[event(input = "UpdateLayoutSettingPB")]
+  SetLayoutSetting = 115,
+
+  #[event(input = "DatabaseLayoutIdPB", output = "LayoutSettingPB")]
+  GetLayoutSetting = 116,
+
+  #[event(input = "CalendarEventRequestPB", output = "RepeatedCalendarEventPB")]
+  GetAllCalendarEvents = 117,
+
+  #[event(input = "RowIdPB", output = "CalendarEventPB")]
+  GetCalendarEvent = 118,
 
-  #[event()]
-  GetCalendarSetting = 116,
+  #[event(input = "MoveCalendarEventPB")]
+  MoveCalendarEvent = 119,
 }

+ 5 - 1
frontend/rust-lib/flowy-database/src/manager.rs

@@ -371,6 +371,7 @@ pub async fn create_new_database(
     block_metas,
     blocks,
     database_view_data,
+    layout_setting,
   } = build_context;
 
   for block_meta_data in &blocks {
@@ -405,11 +406,14 @@ pub async fn create_new_database(
 
   // Create database view
   tracing::trace!("Create new database view: {}", view_id);
-  let database_view_rev = if database_view_data.is_empty() {
+  let mut database_view_rev = if database_view_data.is_empty() {
     DatabaseViewRevision::new(database_id, view_id.to_owned(), true, name, layout.into())
   } else {
     DatabaseViewRevision::from_json(database_view_data)?
   };
+
+  tracing::trace!("Initial calendar layout setting: {:?}", layout_setting);
+  database_view_rev.layout_settings = layout_setting;
   let database_view_ops = make_database_view_operations(&database_view_rev);
   let database_view_bytes = database_view_ops.json_bytes();
   let revision = Revision::initial_revision(view_id, database_view_bytes);

+ 6 - 2
frontend/rust-lib/flowy-database/src/notification.rs

@@ -31,8 +31,12 @@ pub enum DatabaseNotification {
   DidReorderSingleRow = 66,
   /// Trigger when the settings of the database are changed
   DidUpdateSettings = 70,
-  DidUpdateCalendarSettings = 80,
-  DidArrangeCalendarWithNewField = 81,
+  // Trigger when the layout setting of the database is updated
+  DidUpdateLayoutSettings = 80,
+  // Trigger when the layout field of the database is changed
+  DidSetNewLayoutField = 81,
+
+  DidArrangeCalendarWithNewField = 82,
 }
 
 impl std::default::Default for DatabaseNotification {

+ 58 - 3
frontend/rust-lib/flowy-database/src/services/database/database_editor.rs

@@ -425,7 +425,9 @@ impl DatabaseEditor {
   }
 
   pub async fn create_row(&self, params: CreateRowParams) -> FlowyResult<RowPB> {
-    let mut row_rev = self.create_row_rev().await?;
+    let mut row_rev = self
+      .create_row_rev(params.cell_data_by_field_id.clone())
+      .await?;
 
     self
       .database_views
@@ -915,6 +917,7 @@ impl DatabaseEditor {
       field_revs: duplicated_fields.into_iter().map(Arc::new).collect(),
       block_metas: duplicated_blocks,
       blocks: blocks_meta_data,
+      layout_setting: Default::default(),
       database_view_data,
     })
   }
@@ -929,12 +932,64 @@ impl DatabaseEditor {
     self.database_views.get_group(view_id, group_id).await
   }
 
-  async fn create_row_rev(&self) -> FlowyResult<RowRevision> {
+  pub async fn get_layout_setting<T: Into<LayoutRevision>>(
+    &self,
+    view_id: &str,
+    layout_ty: T,
+  ) -> FlowyResult<LayoutSettingParams> {
+    let layout_ty = layout_ty.into();
+    self
+      .database_views
+      .get_layout_setting(view_id, &layout_ty)
+      .await
+  }
+
+  pub async fn set_layout_setting(
+    &self,
+    view_id: &str,
+    layout_setting: LayoutSettingParams,
+  ) -> FlowyResult<()> {
+    self
+      .database_views
+      .set_layout_setting(view_id, layout_setting)
+      .await
+  }
+
+  pub async fn get_all_calendar_events(&self, view_id: &str) -> Vec<CalendarEventPB> {
+    match self.database_views.get_view_editor(view_id).await {
+      Ok(view_editor) => view_editor
+        .v_get_all_calendar_events()
+        .await
+        .unwrap_or_default(),
+      Err(err) => {
+        tracing::error!("Get calendar event failed: {}", err);
+        vec![]
+      },
+    }
+  }
+
+  #[tracing::instrument(level = "trace", skip(self))]
+  pub async fn get_calendar_event(&self, view_id: &str, row_id: &str) -> Option<CalendarEventPB> {
+    let view_editor = self.database_views.get_view_editor(view_id).await.ok()?;
+    view_editor.v_get_calendar_event(row_id).await
+  }
+
+  async fn create_row_rev(
+    &self,
+    cell_data_by_field_id: Option<HashMap<String, String>>,
+  ) -> FlowyResult<RowRevision> {
     let field_revs = self.database_pad.read().await.get_field_revs(None)?;
     let block_id = self.block_id().await?;
 
     // insert empty row below the row whose id is upper_row_id
-    let row_rev = RowRevisionBuilder::new(&block_id, field_revs).build();
+    let builder = match cell_data_by_field_id {
+      None => RowRevisionBuilder::new(&block_id, field_revs),
+      Some(cell_data_by_field_id) => {
+        RowRevisionBuilder::new_with_data(&block_id, field_revs, cell_data_by_field_id)
+      },
+    };
+
+    let row_rev = builder.build();
     Ok(row_rev)
   }
 

+ 10 - 0
frontend/rust-lib/flowy-database/src/services/database/trait_impl.rs

@@ -43,6 +43,16 @@ impl DatabaseViewData for DatabaseViewDataImpl {
     to_fut(async move { Some(pad.read().await.get_field_rev(&field_id)?.1.clone()) })
   }
 
+  fn get_primary_field_rev(&self) -> Fut<Option<Arc<FieldRevision>>> {
+    let pad = self.pad.clone();
+    to_fut(async move {
+      let field_revs = pad.read().await.get_field_revs(None).ok()?;
+      field_revs
+        .into_iter()
+        .find(|field_rev| field_rev.is_primary)
+    })
+  }
+
   fn index_of_row(&self, row_id: &str) -> Fut<Option<usize>> {
     let block_manager = self.blocks.clone();
     let row_id = row_id.to_owned();

+ 201 - 18
frontend/rust-lib/flowy-database/src/services/database_view/editor.rs

@@ -18,8 +18,9 @@ use crate::services::sort::{
   DeletedSortType, SortChangeset, SortController, SortTaskHandler, SortType,
 };
 use database_model::{
-  gen_database_filter_id, gen_database_id, gen_database_sort_id, FieldRevision, FieldTypeRevision,
-  FilterRevision, LayoutRevision, RowChangeset, RowRevision, SortRevision,
+  gen_database_filter_id, gen_database_id, gen_database_sort_id, CalendarLayoutSetting,
+  FieldRevision, FieldTypeRevision, FilterRevision, LayoutRevision, RowChangeset, RowRevision,
+  SortRevision,
 };
 use flowy_client_sync::client_database::{
   make_database_view_operations, DatabaseViewRevisionChangeset, DatabaseViewRevisionPad,
@@ -32,6 +33,7 @@ use lib_infra::future::Fut;
 use nanoid::nanoid;
 use revision_model::Revision;
 use std::borrow::Cow;
+use std::collections::HashMap;
 use std::future::Future;
 use std::sync::Arc;
 use tokio::sync::{broadcast, RwLock};
@@ -43,6 +45,8 @@ pub trait DatabaseViewData: Send + Sync + 'static {
   /// Returns the field with the field_id
   fn get_field_rev(&self, field_id: &str) -> Fut<Option<Arc<FieldRevision>>>;
 
+  fn get_primary_field_rev(&self) -> Fut<Option<Arc<FieldRevision>>>;
+
   /// Returns the index of the row with row_id
   fn index_of_row(&self, row_id: &str) -> Fut<Option<usize>>;
 
@@ -661,25 +665,84 @@ impl DatabaseViewEditor {
   }
 
   /// Returns the current calendar settings
-  pub async fn v_get_calendar_settings(&self) -> FlowyResult<CalendarSettingsParams> {
-    let settings = self
-      .pad
-      .read()
-      .await
-      .get_layout_setting(&LayoutRevision::Calendar)
-      .unwrap_or_else(|| CalendarSettingsParams::default_with(self.view_id.to_string()));
-    Ok(settings)
+  #[tracing::instrument(level = "debug", skip(self), err)]
+  pub async fn v_get_layout_settings(
+    &self,
+    layout_ty: &LayoutRevision,
+  ) -> FlowyResult<LayoutSettingParams> {
+    let mut layout_setting = LayoutSettingParams::default();
+    match layout_ty {
+      LayoutRevision::Grid => {},
+      LayoutRevision::Board => {},
+      LayoutRevision::Calendar => {
+        if let Some(calendar) = self
+          .pad
+          .read()
+          .await
+          .get_layout_setting::<CalendarLayoutSetting>(layout_ty)
+        {
+          // Check the field exist or not
+          if let Some(field_rev) = self.delegate.get_field_rev(&calendar.layout_field_id).await {
+            let field_type: FieldType = field_rev.ty.into();
+
+            // Check the type of field is Datetime or not
+            if field_type == FieldType::DateTime {
+              layout_setting.calendar = Some(calendar);
+            }
+          }
+        }
+      },
+    }
+
+    tracing::debug!("{:?}", layout_setting);
+    Ok(layout_setting)
   }
 
   /// Update the calendar settings and send the notification to refresh the UI
-  pub async fn v_update_calendar_settings(
-    &self,
-    params: CalendarSettingsParams,
-  ) -> FlowyResult<()> {
+  pub async fn v_set_layout_settings(&self, params: LayoutSettingParams) -> FlowyResult<()> {
     // Maybe it needs no send notification to refresh the UI
-    self
-      .modify(|pad| Ok(pad.update_layout_setting(&LayoutRevision::Calendar, &params)?))
-      .await?;
+    if let Some(new_calendar_setting) = params.calendar {
+      if let Some(field_rev) = self
+        .delegate
+        .get_field_rev(&new_calendar_setting.layout_field_id)
+        .await
+      {
+        let field_type: FieldType = field_rev.ty.into();
+        if field_type != FieldType::DateTime {
+          return Err(FlowyError::unexpect_calendar_field_type());
+        }
+
+        let layout_ty = LayoutRevision::Calendar;
+        let old_calender_setting = self.v_get_layout_settings(&layout_ty).await?.calendar;
+        self
+          .modify(|pad| Ok(pad.set_layout_setting(&layout_ty, &new_calendar_setting)?))
+          .await?;
+
+        let new_field_id = new_calendar_setting.layout_field_id.clone();
+        let layout_setting_pb: LayoutSettingPB = LayoutSettingParams {
+          calendar: Some(new_calendar_setting),
+        }
+        .into();
+
+        if let Some(old_calendar_setting) = old_calender_setting {
+          // compare the new layout field id is equal to old layout field id
+          // if not equal, send the  DidSetNewLayoutField notification
+          // if equal, send the  DidUpdateLayoutSettings notification
+          if old_calendar_setting.layout_field_id != new_field_id {
+            send_notification(&self.view_id, DatabaseNotification::DidSetNewLayoutField)
+              .payload(layout_setting_pb)
+              .send();
+          } else {
+            send_notification(&self.view_id, DatabaseNotification::DidUpdateLayoutSettings)
+              .payload(layout_setting_pb)
+              .send();
+          }
+        } else {
+          tracing::warn!("Calendar setting should not be empty")
+        }
+      }
+    }
+
     Ok(())
   }
 
@@ -772,6 +835,100 @@ impl DatabaseViewEditor {
     get_cells_for_field(self.delegate.clone(), field_id).await
   }
 
+  pub async fn v_get_calendar_event(&self, row_id: &str) -> Option<CalendarEventPB> {
+    let layout_ty = LayoutRevision::Calendar;
+    let calendar_setting = self
+      .v_get_layout_settings(&layout_ty)
+      .await
+      .ok()?
+      .calendar?;
+
+    // Text
+    let primary_field = self.delegate.get_primary_field_rev().await?;
+    let text_cell = get_cell_for_row(self.delegate.clone(), &primary_field.id, row_id).await?;
+
+    // Date
+    let date_field = self
+      .delegate
+      .get_field_rev(&calendar_setting.layout_field_id)
+      .await?;
+
+    let date_cell = get_cell_for_row(self.delegate.clone(), &date_field.id, row_id).await?;
+    let title = text_cell
+      .into_text_field_cell_data()
+      .unwrap_or_default()
+      .into();
+
+    let timestamp = date_cell
+      .into_date_field_cell_data()
+      .unwrap_or_default()
+      .into();
+
+    Some(CalendarEventPB {
+      row_id: row_id.to_string(),
+      title_field_id: primary_field.id.clone(),
+      title,
+      timestamp,
+    })
+  }
+
+  pub async fn v_get_all_calendar_events(&self) -> Option<Vec<CalendarEventPB>> {
+    let layout_ty = LayoutRevision::Calendar;
+    let calendar_setting = self
+      .v_get_layout_settings(&layout_ty)
+      .await
+      .ok()?
+      .calendar?;
+
+    // Text
+    let primary_field = self.delegate.get_primary_field_rev().await?;
+    let text_cells = self.v_get_cells_for_field(&primary_field.id).await.ok()?;
+
+    // Date
+    let timestamp_by_row_id = self
+      .v_get_cells_for_field(&calendar_setting.layout_field_id)
+      .await
+      .ok()?
+      .into_iter()
+      .map(|date_cell| {
+        let row_id = date_cell.row_id.clone();
+
+        // timestamp
+        let timestamp = date_cell
+          .into_date_field_cell_data()
+          .map(|date_cell_data| date_cell_data.0.unwrap_or_default())
+          .unwrap_or_default();
+
+        (row_id, timestamp)
+      })
+      .collect::<HashMap<String, i64>>();
+
+    let mut events: Vec<CalendarEventPB> = vec![];
+    for text_cell in text_cells {
+      let title_field_id = text_cell.field_id.clone();
+      let row_id = text_cell.row_id.clone();
+      let timestamp = timestamp_by_row_id
+        .get(&row_id)
+        .cloned()
+        .unwrap_or_default();
+
+      let title = text_cell
+        .into_text_field_cell_data()
+        .unwrap_or_default()
+        .into();
+
+      let event = CalendarEventPB {
+        row_id,
+        title_field_id,
+        title,
+        timestamp,
+      };
+      events.push(event);
+    }
+
+    Some(events)
+  }
+
   async fn notify_did_update_setting(&self) {
     let setting = self.v_get_setting().await;
     send_notification(&self.view_id, DatabaseNotification::DidUpdateSettings)
@@ -851,12 +1008,38 @@ impl DatabaseViewEditor {
     }
   }
 }
-/// Returns the list of cells corresponding to the given field.
+
+pub(crate) async fn get_cell_for_row(
+  delegate: Arc<dyn DatabaseViewData>,
+  field_id: &str,
+  row_id: &str,
+) -> Option<RowSingleCellData> {
+  let (_, row_rev) = delegate.get_row_rev(row_id).await?;
+  let mut cells = get_cells_for_field_in_rows(delegate, field_id, vec![row_rev])
+    .await
+    .ok()?;
+  if cells.is_empty() {
+    None
+  } else {
+    assert_eq!(cells.len(), 1);
+    Some(cells.remove(0))
+  }
+}
+
+// Returns the list of cells corresponding to the given field.
 pub(crate) async fn get_cells_for_field(
   delegate: Arc<dyn DatabaseViewData>,
   field_id: &str,
 ) -> FlowyResult<Vec<RowSingleCellData>> {
   let row_revs = delegate.get_row_revs(None).await;
+  get_cells_for_field_in_rows(delegate, field_id, row_revs).await
+}
+
+pub(crate) async fn get_cells_for_field_in_rows(
+  delegate: Arc<dyn DatabaseViewData>,
+  field_id: &str,
+  row_revs: Vec<Arc<RowRevision>>,
+) -> FlowyResult<Vec<RowSingleCellData>> {
   let field_rev = delegate.get_field_rev(field_id).await.unwrap();
   let field_type: FieldType = field_rev.ty.into();
   let mut cells = vec![];

+ 23 - 3
frontend/rust-lib/flowy-database/src/services/database_view/editor_manager.rs

@@ -1,8 +1,8 @@
 #![allow(clippy::while_let_loop)]
 use crate::entities::{
   AlterFilterParams, AlterSortParams, CreateRowParams, DatabaseViewSettingPB, DeleteFilterParams,
-  DeleteGroupParams, DeleteSortParams, GroupPB, InsertGroupParams, MoveGroupParams,
-  RepeatedGroupPB, RowPB,
+  DeleteGroupParams, DeleteSortParams, GroupPB, InsertGroupParams, LayoutSettingParams,
+  MoveGroupParams, RepeatedGroupPB, RowPB,
 };
 use crate::manager::DatabaseUser;
 use crate::services::cell::AtomicCellDataCache;
@@ -16,7 +16,9 @@ use crate::services::filter::FilterType;
 use crate::services::persistence::rev_sqlite::{
   SQLiteDatabaseRevisionSnapshotPersistence, SQLiteDatabaseViewRevisionPersistence,
 };
-use database_model::{FieldRevision, FilterRevision, RowChangeset, RowRevision, SortRevision};
+use database_model::{
+  FieldRevision, FilterRevision, LayoutRevision, RowChangeset, RowRevision, SortRevision,
+};
 use flowy_client_sync::client_database::DatabaseViewRevisionPad;
 use flowy_error::FlowyResult;
 use flowy_revision::{RevisionManager, RevisionPersistence, RevisionPersistenceConfiguration};
@@ -207,6 +209,24 @@ impl DatabaseViews {
     view_editor.v_get_group(group_id).await
   }
 
+  pub async fn get_layout_setting(
+    &self,
+    view_id: &str,
+    layout_ty: &LayoutRevision,
+  ) -> FlowyResult<LayoutSettingParams> {
+    let view_editor = self.get_view_editor(view_id).await?;
+    view_editor.v_get_layout_settings(layout_ty).await
+  }
+
+  pub async fn set_layout_setting(
+    &self,
+    view_id: &str,
+    layout_setting: LayoutSettingParams,
+  ) -> FlowyResult<()> {
+    let view_editor = self.get_view_editor(view_id).await?;
+    view_editor.v_set_layout_settings(layout_setting).await
+  }
+
   pub async fn insert_or_update_group(&self, params: InsertGroupParams) -> FlowyResult<()> {
     let view_editor = self.get_view_editor(&params.view_id).await?;
     view_editor.v_initialize_new_group(params).await

+ 4 - 4
frontend/rust-lib/flowy-database/src/services/database_view/trait_impl.rs

@@ -1,4 +1,4 @@
-use crate::entities::{CalendarSettingsParams, DatabaseViewSettingPB, LayoutSettingPB};
+use crate::entities::{DatabaseViewSettingPB, LayoutSettingPB};
 use crate::services::database_view::{get_cells_for_field, DatabaseViewData};
 use crate::services::field::RowSingleCellData;
 use crate::services::filter::{FilterController, FilterDelegate, FilterType};
@@ -7,8 +7,8 @@ use crate::services::row::DatabaseBlockRowRevision;
 use crate::services::sort::{SortDelegate, SortType};
 use bytes::Bytes;
 use database_model::{
-  FieldRevision, FieldTypeRevision, FilterRevision, GroupConfigurationRevision, LayoutRevision,
-  RowRevision, SortRevision,
+  CalendarLayoutSetting, FieldRevision, FieldTypeRevision, FilterRevision,
+  GroupConfigurationRevision, LayoutRevision, RowRevision, SortRevision,
 };
 use flowy_client_sync::client_database::{DatabaseViewRevisionChangeset, DatabaseViewRevisionPad};
 use flowy_client_sync::make_operations_from_revisions;
@@ -153,7 +153,7 @@ pub fn make_database_view_setting(
     LayoutRevision::Board => {},
     LayoutRevision::Calendar => {
       layout_settings.calendar = view_pad
-        .get_layout_setting::<CalendarSettingsParams>(&layout_type)
+        .get_layout_setting::<CalendarLayoutSetting>(&layout_type)
         .map(|params| params.into());
     },
   }

+ 4 - 0
frontend/rust-lib/flowy-database/src/services/field/type_options/checkbox_type_option/checkbox_type_option_entities.rs

@@ -11,6 +11,10 @@ pub const UNCHECK: &str = "No";
 pub struct CheckboxCellData(String);
 
 impl CheckboxCellData {
+  pub fn into_inner(self) -> bool {
+    self.is_check()
+  }
+
   pub fn is_check(&self) -> bool {
     self.0 == CHECK
   }

+ 51 - 4
frontend/rust-lib/flowy-database/src/services/row/row_builder.rs

@@ -1,8 +1,10 @@
 use crate::services::cell::{
   insert_checkbox_cell, insert_date_cell, insert_number_cell, insert_select_option_cell,
-  insert_text_cell, insert_url_cell,
+  insert_text_cell, insert_url_cell, FromCellString,
 };
 
+use crate::entities::FieldType;
+use crate::services::field::{CheckboxCellData, SelectOptionIds};
 use database_model::{gen_row_id, CellRevision, FieldRevision, RowRevision, DEFAULT_ROW_HEIGHT};
 use indexmap::IndexMap;
 use std::collections::HashMap;
@@ -16,7 +18,15 @@ pub struct RowRevisionBuilder {
 
 impl RowRevisionBuilder {
   pub fn new(block_id: &str, fields: Vec<Arc<FieldRevision>>) -> Self {
-    let field_rev_map = fields
+    Self::new_with_data(block_id, fields, Default::default())
+  }
+
+  pub fn new_with_data(
+    block_id: &str,
+    field_revs: Vec<Arc<FieldRevision>>,
+    cell_data_by_field_id: HashMap<String, String>,
+  ) -> Self {
+    let field_rev_map = field_revs
       .iter()
       .map(|field| (field.id.clone(), field.clone()))
       .collect::<HashMap<String, Arc<FieldRevision>>>();
@@ -29,12 +39,49 @@ impl RowRevisionBuilder {
     };
 
     let block_id = block_id.to_string();
-
-    Self {
+    let mut builder = Self {
       block_id,
       field_rev_map,
       payload,
+    };
+
+    for (field_id, cell_data) in cell_data_by_field_id {
+      if let Some(field_rev) = builder.field_rev_map.get(&field_id) {
+        let field_type: FieldType = field_rev.ty.into();
+        match field_type {
+          FieldType::RichText => builder.insert_text_cell(&field_id, cell_data),
+          FieldType::Number => {
+            if let Ok(num) = cell_data.parse::<i64>() {
+              builder.insert_date_cell(&field_id, num)
+            }
+          },
+          FieldType::DateTime => {
+            if let Ok(timestamp) = cell_data.parse::<i64>() {
+              builder.insert_date_cell(&field_id, timestamp)
+            }
+          },
+          FieldType::MultiSelect | FieldType::SingleSelect => {
+            if let Ok(ids) = SelectOptionIds::from_cell_str(&cell_data) {
+              builder.insert_select_option_cell(&field_id, ids.into_inner());
+            }
+          },
+          FieldType::Checkbox => {
+            if let Ok(value) = CheckboxCellData::from_cell_str(&cell_data) {
+              builder.insert_checkbox_cell(&field_id, value.into_inner());
+            }
+          },
+          FieldType::URL => {
+            builder.insert_url_cell(&field_id, cell_data);
+          },
+          FieldType::Checklist => {
+            if let Ok(ids) = SelectOptionIds::from_cell_str(&cell_data) {
+              builder.insert_select_option_cell(&field_id, ids.into_inner());
+            }
+          },
+        }
+      }
     }
+    builder
   }
 
   pub fn insert_text_cell(&mut self, field_id: &str, data: String) {

+ 9 - 2
frontend/rust-lib/flowy-database/src/util.rs

@@ -1,7 +1,7 @@
 use crate::entities::FieldType;
 use crate::services::field::*;
 use crate::services::row::RowRevisionBuilder;
-use database_model::BuildDatabaseContext;
+use database_model::{BuildDatabaseContext, CalendarLayoutSetting, LayoutRevision, LayoutSetting};
 use flowy_client_sync::client_database::DatabaseBuilder;
 
 pub fn make_default_grid() -> BuildDatabaseContext {
@@ -80,7 +80,7 @@ pub fn make_default_calendar() -> BuildDatabaseContext {
   let mut database_builder = DatabaseBuilder::new();
   // text
   let text_field = FieldBuilder::new(RichTextTypeOptionBuilder::default())
-    .name("Description")
+    .name("Title")
     .visibility(true)
     .primary(true)
     .build();
@@ -92,6 +92,7 @@ pub fn make_default_calendar() -> BuildDatabaseContext {
     .name("Date")
     .visibility(true)
     .build();
+  let date_field_id = date_field.id.clone();
   database_builder.add_field(date_field);
 
   // single select
@@ -101,6 +102,12 @@ pub fn make_default_calendar() -> BuildDatabaseContext {
     .visibility(true)
     .build();
   database_builder.add_field(multi_select_field);
+
+  let calendar_layout_setting = CalendarLayoutSetting::new(date_field_id);
+  let mut layout_setting = LayoutSetting::new();
+  let calendar_setting = serde_json::to_string(&calendar_layout_setting).unwrap();
+  layout_setting.insert(LayoutRevision::Calendar, calendar_setting);
+  database_builder.set_layout_setting(layout_setting);
   database_builder.build()
 }
 

+ 2 - 2
frontend/rust-lib/flowy-database/tests/database/block_test/script.rs

@@ -4,7 +4,7 @@ use crate::database::database_editor::DatabaseEditorTest;
 use database_model::{
   DatabaseBlockMetaRevision, DatabaseBlockMetaRevisionChangeset, RowChangeset, RowRevision,
 };
-use flowy_database::entities::{CellIdParams, CreateRowParams, FieldType, LayoutTypePB, RowPB};
+use flowy_database::entities::{CellIdParams, CreateRowParams, FieldType, RowPB};
 use flowy_database::services::field::*;
 use flowy_database::services::row::DatabaseBlockRow;
 use std::collections::HashMap;
@@ -81,7 +81,7 @@ impl DatabaseRowTest {
           view_id: self.editor.database_id.clone(),
           start_row_id: None,
           group_id: None,
-          layout: LayoutTypePB::Grid,
+          cell_data_by_field_id: None,
         };
         let row_order = self.editor.create_row(params).await.unwrap();
         self

+ 0 - 3
frontend/rust-lib/flowy-database/tests/database/block_test/util.rs

@@ -7,8 +7,6 @@ use flowy_database::services::field::{
 use flowy_database::services::row::RowRevisionBuilder;
 use std::sync::Arc;
 
-use strum::EnumCount;
-
 pub struct DatabaseRowTestBuilder {
   field_revs: Vec<Arc<FieldRevision>>,
   inner_builder: RowRevisionBuilder,
@@ -16,7 +14,6 @@ pub struct DatabaseRowTestBuilder {
 
 impl DatabaseRowTestBuilder {
   pub fn new(block_id: String, field_revs: Vec<Arc<FieldRevision>>) -> Self {
-    assert_eq!(field_revs.len(), FieldType::COUNT);
     let inner_builder = RowRevisionBuilder::new(&block_id, field_revs.clone());
     Self {
       field_revs,

+ 4 - 4
frontend/rust-lib/flowy-database/tests/database/database_editor.rs

@@ -33,6 +33,10 @@ impl DatabaseEditorTest {
     Self::new(LayoutTypePB::Board).await
   }
 
+  pub async fn new_calendar() -> Self {
+    Self::new(LayoutTypePB::Calendar).await
+  }
+
   pub async fn new(layout: LayoutTypePB) -> Self {
     let sdk = FlowySDKTest::default();
     let _ = sdk.init_user().await;
@@ -64,10 +68,6 @@ impl DatabaseEditorTest {
     let row_pbs = editor.get_all_row_revs(&test.view.id).await.unwrap();
     assert_eq!(block_meta_revs.len(), 1);
 
-    // It seems like you should add the field in the make_test_grid() function.
-    // Because we assert the initialize count of the fields is equal to FieldType::COUNT.
-    assert_eq!(field_revs.len(), FieldType::COUNT);
-
     let view_id = test.view.id;
     let app_id = test.app.id;
     Self {

+ 2 - 2
frontend/rust-lib/flowy-database/tests/database/group_test/script.rs

@@ -1,7 +1,7 @@
 use crate::database::database_editor::DatabaseEditorTest;
 use database_model::{FieldRevision, RowChangeset};
 use flowy_database::entities::{
-  CreateRowParams, FieldType, GroupPB, LayoutTypePB, MoveGroupParams, MoveGroupRowParams, RowPB,
+  CreateRowParams, FieldType, GroupPB, MoveGroupParams, MoveGroupRowParams, RowPB,
 };
 use flowy_database::services::cell::{
   delete_select_option_cell, insert_select_option_cell, insert_url_cell,
@@ -130,7 +130,7 @@ impl DatabaseGroupTest {
           view_id: self.view_id.clone(),
           start_row_id: None,
           group_id: Some(group.group_id.clone()),
-          layout: LayoutTypePB::Board,
+          cell_data_by_field_id: None,
         };
         let _ = self.editor.create_row(params).await.unwrap();
       },

+ 2 - 0
frontend/rust-lib/flowy-database/tests/database/layout_test/mod.rs

@@ -0,0 +1,2 @@
+mod script;
+mod test;

+ 86 - 0
frontend/rust-lib/flowy-database/tests/database/layout_test/script.rs

@@ -0,0 +1,86 @@
+use crate::database::database_editor::DatabaseEditorTest;
+use database_model::{CalendarLayoutSetting, FieldRevision, LayoutRevision};
+use flowy_database::entities::FieldType;
+use std::sync::Arc;
+
+pub enum LayoutScript {
+  AssertCalendarLayoutSetting { expected: CalendarLayoutSetting },
+  GetCalendarEvents,
+}
+
+pub struct DatabaseLayoutTest {
+  database_test: DatabaseEditorTest,
+}
+
+impl DatabaseLayoutTest {
+  pub async fn new_calendar() -> Self {
+    let database_test = DatabaseEditorTest::new_calendar().await;
+    Self { database_test }
+  }
+
+  pub async fn run_scripts(&mut self, scripts: Vec<LayoutScript>) {
+    for script in scripts {
+      self.run_script(script).await;
+    }
+  }
+
+  pub async fn get_first_date_field(&self) -> Arc<FieldRevision> {
+    self
+      .database_test
+      .get_first_field_rev(FieldType::DateTime)
+      .clone()
+  }
+
+  pub async fn run_script(&mut self, script: LayoutScript) {
+    match script {
+      LayoutScript::AssertCalendarLayoutSetting { expected } => {
+        let view_id = self.database_test.view_id.clone();
+        let layout_ty = LayoutRevision::Calendar;
+
+        let calendar_setting = self
+          .database_test
+          .editor
+          .get_layout_setting(&view_id, layout_ty)
+          .await
+          .unwrap()
+          .calendar
+          .unwrap();
+
+        assert_eq!(calendar_setting.layout_ty, expected.layout_ty);
+        assert_eq!(
+          calendar_setting.first_day_of_week,
+          expected.first_day_of_week
+        );
+        assert_eq!(calendar_setting.show_weekends, expected.show_weekends);
+      },
+      LayoutScript::GetCalendarEvents => {
+        let events = self
+          .database_test
+          .editor
+          .get_all_calendar_events(&self.database_test.view_id)
+          .await;
+        assert_eq!(events.len(), 5);
+
+        for (index, event) in events.into_iter().enumerate() {
+          if index == 0 {
+            assert_eq!(event.title, "A");
+            assert_eq!(event.timestamp, 1678090778);
+          }
+
+          if index == 1 {
+            assert_eq!(event.title, "B");
+            assert_eq!(event.timestamp, 1677917978);
+          }
+          if index == 2 {
+            assert_eq!(event.title, "C");
+            assert_eq!(event.timestamp, 1679213978);
+          }
+          if index == 4 {
+            assert_eq!(event.title, "E");
+            assert_eq!(event.timestamp, 1678695578);
+          }
+        }
+      },
+    }
+  }
+}

+ 21 - 0
frontend/rust-lib/flowy-database/tests/database/layout_test/test.rs

@@ -0,0 +1,21 @@
+use crate::database::layout_test::script::DatabaseLayoutTest;
+use crate::database::layout_test::script::LayoutScript::*;
+use database_model::CalendarLayoutSetting;
+
+#[tokio::test]
+async fn calendar_initial_layout_setting_test() {
+  let mut test = DatabaseLayoutTest::new_calendar().await;
+  let date_field = test.get_first_date_field().await;
+  let default_calendar_setting = CalendarLayoutSetting::new(date_field.id.clone());
+  let scripts = vec![AssertCalendarLayoutSetting {
+    expected: default_calendar_setting,
+  }];
+  test.run_scripts(scripts).await;
+}
+
+#[tokio::test]
+async fn calendar_get_events_test() {
+  let mut test = DatabaseLayoutTest::new_calendar().await;
+  let scripts = vec![GetCalendarEvents];
+  test.run_scripts(scripts).await;
+}

+ 100 - 2
frontend/rust-lib/flowy-database/tests/database/mock_data/calendar_mock_data.rs

@@ -1,6 +1,104 @@
-use database_model::BuildDatabaseContext;
+use crate::database::block_test::util::DatabaseRowTestBuilder;
+use database_model::{BuildDatabaseContext, CalendarLayoutSetting, LayoutRevision, LayoutSetting};
+use flowy_client_sync::client_database::DatabaseBuilder;
+use flowy_database::entities::FieldType;
+use flowy_database::services::field::{
+  DateTypeOptionBuilder, FieldBuilder, MultiSelectTypeOptionBuilder, RichTextTypeOptionBuilder,
+};
+
+use strum::IntoEnumIterator;
 
 // Calendar unit test mock data
 pub fn make_test_calendar() -> BuildDatabaseContext {
-  todo!()
+  let mut database_builder = DatabaseBuilder::new();
+  // text
+  let text_field = FieldBuilder::new(RichTextTypeOptionBuilder::default())
+    .name("Title")
+    .visibility(true)
+    .primary(true)
+    .build();
+  let _text_field_id = text_field.id.clone();
+  database_builder.add_field(text_field);
+
+  // date
+  let date_type_option = DateTypeOptionBuilder::default();
+  let date_field = FieldBuilder::new(date_type_option)
+    .name("Date")
+    .visibility(true)
+    .build();
+  let date_field_id = date_field.id.clone();
+  database_builder.add_field(date_field);
+
+  // single select
+  let multi_select_type_option = MultiSelectTypeOptionBuilder::default();
+  let multi_select_field = FieldBuilder::new(multi_select_type_option)
+    .name("Tags")
+    .visibility(true)
+    .build();
+  database_builder.add_field(multi_select_field);
+
+  let calendar_layout_setting = CalendarLayoutSetting::new(date_field_id);
+  let mut layout_setting = LayoutSetting::new();
+  let calendar_setting = serde_json::to_string(&calendar_layout_setting).unwrap();
+  layout_setting.insert(LayoutRevision::Calendar, calendar_setting);
+  database_builder.set_layout_setting(layout_setting);
+
+  for i in 0..5 {
+    let block_id = database_builder.block_id().to_owned();
+    let field_revs = database_builder.field_revs().clone();
+    let mut row_builder = DatabaseRowTestBuilder::new(block_id.clone(), field_revs);
+    match i {
+      0 => {
+        for field_type in FieldType::iter() {
+          match field_type {
+            FieldType::RichText => row_builder.insert_text_cell("A"),
+            FieldType::DateTime => row_builder.insert_date_cell("1678090778"),
+            _ => "".to_owned(),
+          };
+        }
+      },
+      1 => {
+        for field_type in FieldType::iter() {
+          match field_type {
+            FieldType::RichText => row_builder.insert_text_cell("B"),
+            FieldType::DateTime => row_builder.insert_date_cell("1677917978"),
+            _ => "".to_owned(),
+          };
+        }
+      },
+      2 => {
+        for field_type in FieldType::iter() {
+          match field_type {
+            FieldType::RichText => row_builder.insert_text_cell("C"),
+            FieldType::DateTime => row_builder.insert_date_cell("1679213978"),
+            _ => "".to_owned(),
+          };
+        }
+      },
+      3 => {
+        for field_type in FieldType::iter() {
+          match field_type {
+            FieldType::RichText => row_builder.insert_text_cell("D"),
+            FieldType::DateTime => row_builder.insert_date_cell("1678695578"),
+            _ => "".to_owned(),
+          };
+        }
+      },
+      4 => {
+        for field_type in FieldType::iter() {
+          match field_type {
+            FieldType::RichText => row_builder.insert_text_cell("E"),
+            FieldType::DateTime => row_builder.insert_date_cell("1678695578"),
+            _ => "".to_owned(),
+          };
+        }
+      },
+      _ => {},
+    }
+
+    let row_rev = row_builder.build();
+    database_builder.add_row(row_rev);
+  }
+
+  database_builder.build()
 }

+ 1 - 0
frontend/rust-lib/flowy-database/tests/database/mod.rs

@@ -5,6 +5,7 @@ mod database_ref_test;
 mod field_test;
 mod filter_test;
 mod group_test;
+mod layout_test;
 mod snapshot_test;
 mod sort_test;
 

+ 3 - 0
frontend/rust-lib/flowy-error/src/code.rs

@@ -186,6 +186,9 @@ pub enum ErrorCode {
 
   #[error("Payload should not be empty")]
   UnexpectedEmptyPayload = 60,
+
+  #[error("Only the date type can be used in calendar")]
+  UnexpectedCalendarFieldType = 61,
 }
 
 impl ErrorCode {

+ 4 - 0
frontend/rust-lib/flowy-error/src/errors.rs

@@ -82,6 +82,10 @@ impl FlowyError {
   static_flowy_error!(field_record_not_found, ErrorCode::FieldRecordNotFound);
   static_flowy_error!(payload_none, ErrorCode::UnexpectedEmptyPayload);
   static_flowy_error!(http, ErrorCode::HttpError);
+  static_flowy_error!(
+    unexpect_calendar_field_type,
+    ErrorCode::UnexpectedCalendarFieldType
+  );
 }
 
 impl std::convert::From<ErrorCode> for FlowyError {

+ 36 - 1
shared-lib/database-model/src/database_rev.rs

@@ -1,8 +1,9 @@
-use crate::DatabaseBlockRevision;
+use crate::{DatabaseBlockRevision, LayoutSetting};
 use bytes::Bytes;
 use indexmap::IndexMap;
 use nanoid::nanoid;
 use serde::{Deserialize, Serialize};
+use serde_repr::{Deserialize_repr, Serialize_repr};
 use std::sync::Arc;
 
 pub fn gen_database_id() -> String {
@@ -195,6 +196,7 @@ pub struct BuildDatabaseContext {
   pub field_revs: Vec<Arc<FieldRevision>>,
   pub block_metas: Vec<DatabaseBlockMetaRevision>,
   pub blocks: Vec<DatabaseBlockRevision>,
+  pub layout_setting: LayoutSetting,
 
   // String in JSON format. It can be deserialized into [GridViewRevision]
   pub database_view_data: String,
@@ -223,3 +225,36 @@ impl std::convert::TryFrom<Bytes> for BuildDatabaseContext {
 }
 
 pub type FieldTypeRevision = u8;
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct CalendarLayoutSetting {
+  pub layout_ty: CalendarLayout,
+  pub first_day_of_week: i32,
+  pub show_weekends: bool,
+  pub show_week_numbers: bool,
+  pub layout_field_id: String,
+}
+
+impl CalendarLayoutSetting {
+  pub fn new(layout_field_id: String) -> Self {
+    CalendarLayoutSetting {
+      layout_ty: CalendarLayout::default(),
+      first_day_of_week: DEFAULT_FIRST_DAY_OF_WEEK,
+      show_weekends: DEFAULT_SHOW_WEEKENDS,
+      show_week_numbers: DEFAULT_SHOW_WEEK_NUMBERS,
+      layout_field_id,
+    }
+  }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq, Default, Serialize_repr, Deserialize_repr)]
+#[repr(u8)]
+pub enum CalendarLayout {
+  #[default]
+  MonthLayout = 0,
+  WeekLayout = 1,
+  DayLayout = 2,
+}
+
+pub const DEFAULT_FIRST_DAY_OF_WEEK: i32 = 0;
+pub const DEFAULT_SHOW_WEEKENDS: bool = true;
+pub const DEFAULT_SHOW_WEEK_NUMBERS: bool = true;

+ 11 - 6
shared-lib/database-model/src/view_rev.rs

@@ -47,8 +47,8 @@ pub struct DatabaseViewRevision {
   pub layout: LayoutRevision,
 
   #[serde(default)]
-  #[serde(skip_serializing_if = "LayoutSettings::is_empty")]
-  pub layout_settings: LayoutSettings,
+  #[serde(skip_serializing_if = "LayoutSetting::is_empty")]
+  pub layout_settings: LayoutSetting,
 
   #[serde(default)]
   pub filters: FilterConfiguration,
@@ -90,18 +90,23 @@ impl DatabaseViewRevision {
 
 #[derive(Debug, Clone, Default, Serialize, Deserialize)]
 #[serde(transparent)]
-pub struct LayoutSettings {
+pub struct LayoutSetting {
   #[serde(with = "indexmap::serde_seq")]
   inner: IndexMap<LayoutRevision, String>,
 }
 
-impl LayoutSettings {
+impl LayoutSetting {
+  pub fn new() -> Self {
+    Self {
+      inner: Default::default(),
+    }
+  }
   pub fn is_empty(&self) -> bool {
     self.inner.is_empty()
   }
 }
 
-impl std::ops::Deref for LayoutSettings {
+impl std::ops::Deref for LayoutSetting {
   type Target = IndexMap<LayoutRevision, String>;
 
   fn deref(&self) -> &Self::Target {
@@ -109,7 +114,7 @@ impl std::ops::Deref for LayoutSettings {
   }
 }
 
-impl std::ops::DerefMut for LayoutSettings {
+impl std::ops::DerefMut for LayoutSetting {
   fn deref_mut(&mut self) -> &mut Self::Target {
     &mut self.inner
   }

+ 9 - 9
shared-lib/flowy-sync/src/errors.rs

@@ -62,15 +62,15 @@ impl fmt::Display for SyncError {
 pub enum ErrorCode {
   DocumentIdInvalid = 0,
   DocumentNotfound = 1,
-  UndoFail = 200,
-  RedoFail = 201,
-  OutOfBound = 202,
-  RevisionConflict = 203,
-  RecordNotFound = 300,
-  CannotDeleteThePrimaryField = 301,
-  UnexpectedEmptyRevision = 302,
-  SerdeError = 999,
-  InternalError = 1000,
+  UndoFail = 2,
+  RedoFail = 3,
+  OutOfBound = 4,
+  RevisionConflict = 5,
+  RecordNotFound = 6,
+  CannotDeleteThePrimaryField = 7,
+  UnexpectedEmptyRevision = 8,
+  SerdeError = 100,
+  InternalError = 101,
 }
 
 impl std::convert::From<lib_ot::errors::OTError> for SyncError {