Bläddra i källkod

feat: calendar UI improvements (#1941)

* chore: enable calendar

* feat: set font of the day event widget

* feat: support add/remove event

* chore: initial settings popover

* chore: calendar bloc can update layout settings

* fix: events overflow in day cell

* feat: calendar layout settings UI

* feat: layout calendar by another date field

* chore: i18n

* chore: hide the show weekend option

* chore: add popover mutex

* fix: clear existing events before adding new ones

---------

Co-authored-by: nathan <[email protected]>
Richard Shiue 2 år sedan
förälder
incheckning
77d787a929
23 ändrade filer med 1336 tillägg och 327 borttagningar
  1. 8 1
      frontend/appflowy_flutter/assets/translations/en.json
  2. 42 10
      frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart
  3. 7 2
      frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart
  4. 49 0
      frontend/appflowy_flutter/lib/plugins/database_view/application/layout/calendar_setting_listener.dart
  5. 9 1
      frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart
  6. 11 11
      frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_list.dart
  7. 62 17
      frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_cache.dart
  8. 135 36
      frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart
  9. 55 0
      frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_setting_bloc.dart
  10. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/calendar/calendar.dart
  11. 267 0
      frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_day.dart
  12. 58 228
      frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart
  13. 410 0
      frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart
  14. 112 0
      frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_setting.dart
  15. 67 7
      frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_toolbar.dart
  16. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart
  17. 3 0
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart
  18. 13 2
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/card_cell.dart
  19. 4 1
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/select_option_card_cell.dart
  20. 20 5
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/text_card_cell.dart
  21. 1 1
      frontend/appflowy_flutter/test/bloc_test/board_test/util.dart
  22. 1 1
      frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart
  23. 0 2
      frontend/rust-lib/flowy-database/src/notification.rs

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

@@ -216,7 +216,8 @@
       "addFilter": "Add Filter",
       "deleteFilter": "Delete filter",
       "filterBy": "Filter by...",
-      "typeAValue": "Type a value..."
+      "typeAValue": "Type a value...",
+      "layout": "Layout"
     },
     "textFilter": {
       "contains": "Contains",
@@ -393,6 +394,12 @@
       "jumpToday": "Jump to Today",
       "previousMonth": "Previous Month",
       "nextMonth": "Next Month"
+    },
+    "settings": {
+      "showWeekNumbers": "Show week numbers",
+      "showWeekends": "Show weekends",
+      "firstDayOfWeek": "First day of week",
+      "layoutDateField": "Layout calendar by"
     }
   }
 }

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

@@ -1,4 +1,5 @@
 import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
+import 'package:appflowy/plugins/database_view/application/layout/calendar_setting_listener.dart';
 import 'package:appflowy/plugins/database_view/application/view/view_cache.dart';
 import 'package:appflowy_backend/log.dart';
 import 'package:appflowy_backend/protobuf/flowy-database/calendar_entities.pb.dart';
@@ -18,11 +19,6 @@ 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>);
@@ -52,16 +48,29 @@ class LayoutCallbacks {
   });
 }
 
+class CalendarLayoutCallbacks {
+  final void Function(LayoutSettingPB) onCalendarLayoutChanged;
+
+  CalendarLayoutCallbacks({required this.onCalendarLayoutChanged});
+}
+
 class DatabaseCallbacks {
   OnDatabaseChanged? onDatabaseChanged;
-  OnRowsChanged? onRowsChanged;
   OnFieldsChanged? onFieldsChanged;
   OnFiltersChanged? onFiltersChanged;
+  OnRowsChanged? onRowsChanged;
+  OnRowsDeleted? onRowsDeleted;
+  OnRowsUpdated? onRowsUpdated;
+  OnRowsCreated? onRowsCreated;
+
   DatabaseCallbacks({
     this.onDatabaseChanged,
     this.onRowsChanged,
     this.onFieldsChanged,
     this.onFiltersChanged,
+    this.onRowsUpdated,
+    this.onRowsDeleted,
+    this.onRowsCreated,
   });
 }
 
@@ -76,21 +85,23 @@ class DatabaseController {
   DatabaseCallbacks? _databaseCallbacks;
   GroupCallbacks? _groupCallbacks;
   LayoutCallbacks? _layoutCallbacks;
+  CalendarLayoutCallbacks? _calendarLayoutCallbacks;
 
   // Getters
-  List<RowInfo> get rowInfos => _viewCache.rowInfos;
   RowCache get rowCache => _viewCache.rowCache;
 
   // Listener
   final DatabaseGroupListener groupListener;
   final DatabaseLayoutListener layoutListener;
+  final DatabaseCalendarLayoutListener calendarLayoutListener;
 
   DatabaseController({required ViewPB view, required this.layoutType})
       : viewId = view.id,
         _databaseViewBackendSvc = DatabaseViewBackendService(viewId: view.id),
         fieldController = FieldController(viewId: view.id),
         groupListener = DatabaseGroupListener(view.id),
-        layoutListener = DatabaseLayoutListener(view.id) {
+        layoutListener = DatabaseLayoutListener(view.id),
+        calendarLayoutListener = DatabaseCalendarLayoutListener(view.id) {
     _viewCache = DatabaseViewCache(
       viewId: viewId,
       fieldController: fieldController,
@@ -99,16 +110,21 @@ class DatabaseController {
     _listenOnFieldsChanged();
     _listenOnGroupChanged();
     _listenOnLayoutChanged();
+    if (layoutType == LayoutTypePB.Calendar) {
+      _listenOnCalendarLayoutChanged();
+    }
   }
 
   void addListener({
     DatabaseCallbacks? onDatabaseChanged,
     LayoutCallbacks? onLayoutChanged,
     GroupCallbacks? onGroupChanged,
+    CalendarLayoutCallbacks? onCalendarLayoutChanged,
   }) {
     _layoutCallbacks = onLayoutChanged;
     _databaseCallbacks = onDatabaseChanged;
     _groupCallbacks = onGroupChanged;
+    _calendarLayoutCallbacks = onCalendarLayoutChanged;
   }
 
   Future<Either<Unit, FlowyError>> open() async {
@@ -218,9 +234,17 @@ class DatabaseController {
   }
 
   void _listenOnRowsChanged() {
-    _viewCache.addListener(onRowsChanged: (reason) {
-      _databaseCallbacks?.onRowsChanged?.call(rowInfos, reason);
+    final callbacks =
+        DatabaseViewCallbacks(onRowsChanged: (rows, rowByRowId, reason) {
+      _databaseCallbacks?.onRowsChanged?.call(rows, rowByRowId, reason);
+    }, onRowsDeleted: (ids) {
+      _databaseCallbacks?.onRowsDeleted?.call(ids);
+    }, onRowsUpdated: (ids) {
+      _databaseCallbacks?.onRowsUpdated?.call(ids);
+    }, onRowsCreated: (ids) {
+      _databaseCallbacks?.onRowsCreated?.call(ids);
     });
+    _viewCache.addListener(callbacks);
   }
 
   void _listenOnFieldsChanged() {
@@ -266,6 +290,14 @@ class DatabaseController {
       }, (r) => Log.error(r));
     });
   }
+
+  void _listenOnCalendarLayoutChanged() {
+    calendarLayoutListener.start(onCalendarLayoutChanged: (result) {
+      result.fold((l) {
+        _calendarLayoutCallbacks?.onCalendarLayoutChanged(l);
+      }, (r) => Log.error(r));
+    });
+  }
 }
 
 class RowDataBuilder {

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

@@ -10,9 +10,14 @@ import 'row/row_cache.dart';
 typedef OnFieldsChanged = void Function(UnmodifiableListView<FieldInfo>);
 typedef OnFiltersChanged = void Function(List<FilterInfo>);
 typedef OnDatabaseChanged = void Function(DatabasePB);
+
+typedef OnRowsCreated = void Function(List<String> ids);
+typedef OnRowsUpdated = void Function(List<String> ids);
+typedef OnRowsDeleted = void Function(List<String> ids);
 typedef OnRowsChanged = void Function(
-  List<RowInfo>,
-  RowsChangedReason,
+  UnmodifiableListView<RowInfo> rows,
+  UnmodifiableMapView<String, RowInfo> rowByRowId,
+  RowsChangedReason reason,
 );
 
 typedef OnError = void Function(FlowyError);

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

@@ -0,0 +1,49 @@
+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 NewLayoutFieldValue = Either<LayoutSettingPB, FlowyError>;
+
+class DatabaseCalendarLayoutListener {
+  final String viewId;
+  PublishNotifier<NewLayoutFieldValue>? _newLayoutFieldNotifier =
+      PublishNotifier();
+  DatabaseNotificationListener? _listener;
+  DatabaseCalendarLayoutListener(this.viewId);
+
+  void start(
+      {required void Function(NewLayoutFieldValue) onCalendarLayoutChanged}) {
+    _newLayoutFieldNotifier?.addPublishListener(onCalendarLayoutChanged);
+    _listener = DatabaseNotificationListener(
+      objectId: viewId,
+      handler: _handler,
+    );
+  }
+
+  void _handler(
+    DatabaseNotification ty,
+    Either<Uint8List, FlowyError> result,
+  ) {
+    switch (ty) {
+      case DatabaseNotification.DidSetNewLayoutField:
+        result.fold(
+          (payload) => _newLayoutFieldNotifier?.value =
+              left(LayoutSettingPB.fromBuffer(payload)),
+          (error) => _newLayoutFieldNotifier?.value = right(error),
+        );
+        break;
+      default:
+        break;
+    }
+  }
+
+  Future<void> stop() async {
+    await _listener?.stop();
+    _newLayoutFieldNotifier?.dispose();
+    _newLayoutFieldNotifier = null;
+  }
+}

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

@@ -37,11 +37,15 @@ class RowCache {
   final RowCacheDelegate _delegate;
   final RowChangesetNotifier _rowChangeReasonNotifier;
 
-  UnmodifiableListView<RowInfo> get visibleRows {
+  UnmodifiableListView<RowInfo> get rowInfos {
     var visibleRows = [..._rowList.rows];
     return UnmodifiableListView(visibleRows);
   }
 
+  UnmodifiableMapView<String, RowInfo> get rowByRowId {
+    return UnmodifiableMapView(_rowList.rowInfoByRowId);
+  }
+
   CellCache get cellCache => _cellCache;
 
   RowCache({
@@ -61,6 +65,10 @@ class RowCache {
     });
   }
 
+  RowInfo? getRow(String rowId) {
+    return _rowList.get(rowId);
+  }
+
   void setInitialRows(List<RowPB> rows) {
     for (final row in rows) {
       final rowInfo = buildGridRow(row);

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

@@ -9,14 +9,14 @@ class RowList {
   List<RowInfo> get rows => List.from(_rowInfos);
 
   /// Use Map for faster access the raw row data.
-  final HashMap<String, RowInfo> _rowInfoByRowId = HashMap();
+  final HashMap<String, RowInfo> rowInfoByRowId = HashMap();
 
   RowInfo? get(String rowId) {
-    return _rowInfoByRowId[rowId];
+    return rowInfoByRowId[rowId];
   }
 
   int? indexOfRow(String rowId) {
-    final rowInfo = _rowInfoByRowId[rowId];
+    final rowInfo = rowInfoByRowId[rowId];
     if (rowInfo != null) {
       return _rowInfos.indexOf(rowInfo);
     }
@@ -33,7 +33,7 @@ class RowList {
     } else {
       _rowInfos.add(rowInfo);
     }
-    _rowInfoByRowId[rowId] = rowInfo;
+    rowInfoByRowId[rowId] = rowInfo;
   }
 
   InsertedIndex? insert(int index, RowInfo rowInfo) {
@@ -47,21 +47,21 @@ class RowList {
     if (oldRowInfo != null) {
       _rowInfos.insert(insertedIndex, rowInfo);
       _rowInfos.remove(oldRowInfo);
-      _rowInfoByRowId[rowId] = rowInfo;
+      rowInfoByRowId[rowId] = rowInfo;
       return null;
     } else {
       _rowInfos.insert(insertedIndex, rowInfo);
-      _rowInfoByRowId[rowId] = rowInfo;
+      rowInfoByRowId[rowId] = rowInfo;
       return InsertedIndex(index: insertedIndex, rowId: rowId);
     }
   }
 
   DeletedIndex? remove(String rowId) {
-    final rowInfo = _rowInfoByRowId[rowId];
+    final rowInfo = rowInfoByRowId[rowId];
     if (rowInfo != null) {
       final index = _rowInfos.indexOf(rowInfo);
       if (index != -1) {
-        _rowInfoByRowId.remove(rowInfo.rowPB.id);
+        rowInfoByRowId.remove(rowInfo.rowPB.id);
         _rowInfos.remove(rowInfo);
       }
       return DeletedIndex(index: index, rowInfo: rowInfo);
@@ -105,7 +105,7 @@ class RowList {
       if (deletedRowByRowId[rowInfo.rowPB.id] == null) {
         newRows.add(rowInfo);
       } else {
-        _rowInfoByRowId.remove(rowInfo.rowPB.id);
+        rowInfoByRowId.remove(rowInfo.rowPB.id);
         deletedIndex.add(DeletedIndex(index: index, rowInfo: rowInfo));
       }
     });
@@ -136,7 +136,7 @@ class RowList {
     _rowInfos.clear();
 
     for (final rowId in rowIds) {
-      final rowInfo = _rowInfoByRowId[rowId];
+      final rowInfo = rowInfoByRowId[rowId];
       if (rowInfo != null) {
         _rowInfos.add(rowInfo);
       }
@@ -155,6 +155,6 @@ class RowList {
   }
 
   bool contains(String rowId) {
-    return _rowInfoByRowId[rowId] != null;
+    return rowInfoByRowId[rowId] != null;
   }
 }

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

@@ -1,22 +1,50 @@
 import 'dart:async';
+import 'dart:collection';
 import 'package:appflowy_backend/log.dart';
+import '../defines.dart';
 import '../field/field_controller.dart';
 import '../row/row_cache.dart';
 import 'view_listener.dart';
 
+class DatabaseViewCallbacks {
+  /// Will get called when number of rows were changed that includes
+  /// update/delete/insert rows. The [onRowsChanged] will return all
+  /// the rows of the current database
+  final OnRowsChanged? onRowsChanged;
+
+  // Will get called when creating new rows
+  final OnRowsCreated? onRowsCreated;
+
+  /// Will get called when number of rows were updated
+  final OnRowsUpdated? onRowsUpdated;
+
+  /// Will get called when number of rows were deleted
+  final OnRowsDeleted? onRowsDeleted;
+
+  const DatabaseViewCallbacks({
+    this.onRowsChanged,
+    this.onRowsCreated,
+    this.onRowsUpdated,
+    this.onRowsDeleted,
+  });
+}
+
 /// Read https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid for more information
 class DatabaseViewCache {
   final String viewId;
   late RowCache _rowCache;
-  final DatabaseViewListener _gridViewListener;
+  final DatabaseViewListener _databaseViewListener;
+  DatabaseViewCallbacks? _callbacks;
 
-  List<RowInfo> get rowInfos => _rowCache.visibleRows;
+  UnmodifiableListView<RowInfo> get rowInfos => _rowCache.rowInfos;
   RowCache get rowCache => _rowCache;
 
+  RowInfo? getRow(String rowId) => _rowCache.getRow(rowId);
+
   DatabaseViewCache({
     required this.viewId,
     required FieldController fieldController,
-  }) : _gridViewListener = DatabaseViewListener(viewId: viewId) {
+  }) : _databaseViewListener = DatabaseViewListener(viewId: viewId) {
     final delegate = RowDelegatesImpl(fieldController);
     _rowCache = RowCache(
       viewId: viewId,
@@ -24,10 +52,28 @@ class DatabaseViewCache {
       cacheDelegate: delegate,
     );
 
-    _gridViewListener.start(
+    _databaseViewListener.start(
       onRowsChanged: (result) {
         result.fold(
-          (changeset) => _rowCache.applyRowsChanged(changeset),
+          (changeset) {
+            // Update the cache
+            _rowCache.applyRowsChanged(changeset);
+
+            if (changeset.deletedRows.isNotEmpty) {
+              _callbacks?.onRowsDeleted?.call(changeset.deletedRows);
+            }
+
+            if (changeset.updatedRows.isNotEmpty) {
+              _callbacks?.onRowsUpdated
+                  ?.call(changeset.updatedRows.map((e) => e.row.id).toList());
+            }
+
+            if (changeset.insertedRows.isNotEmpty) {
+              _callbacks?.onRowsCreated?.call(changeset.insertedRows
+                  .map((insertedRow) => insertedRow.row.id)
+                  .toList());
+            }
+          },
           (err) => Log.error(err),
         );
       },
@@ -50,23 +96,22 @@ class DatabaseViewCache {
         );
       },
     );
+
+    _rowCache.onRowsChanged(
+      (reason) => _callbacks?.onRowsChanged?.call(
+        rowInfos,
+        _rowCache.rowByRowId,
+        reason,
+      ),
+    );
   }
 
   Future<void> dispose() async {
-    await _gridViewListener.stop();
+    await _databaseViewListener.stop();
     await _rowCache.dispose();
   }
 
-  void addListener({
-    required void Function(RowsChangedReason) onRowsChanged,
-    bool Function()? listenWhen,
-  }) {
-    _rowCache.onRowsChanged((reason) {
-      if (listenWhen != null && listenWhen() == false) {
-        return;
-      }
-
-      onRowsChanged(reason);
-    });
+  void addListener(DatabaseViewCallbacks callbacks) {
+    _callbacks = callbacks;
   }
 }

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

@@ -17,9 +17,11 @@ part 'calendar_bloc.freezed.dart';
 
 class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
   final DatabaseController _databaseController;
+  Map<String, FieldInfo> fieldInfoByFieldId = {};
 
   // Getters
   String get viewId => _databaseController.viewId;
+  FieldController get fieldController => _databaseController.fieldController;
   CellCache get cellCache => _databaseController.rowCache.cellCache;
   RowCache get rowCache => _databaseController.rowCache;
 
@@ -28,7 +30,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
           view: view,
           layoutType: LayoutTypePB.Calendar,
         ),
-        super(CalendarState.initial(view.id)) {
+        super(CalendarState.initial()) {
     on<CalendarEvent>(
       (event, emit) async {
         await event.when(
@@ -44,16 +46,49 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
             emit(state.copyWith(database: Some(database)));
           },
           didLoadAllEvents: (events) {
-            emit(state.copyWith(events: events));
+            emit(state.copyWith(initialEvents: events, allEvents: events));
+          },
+          didReceiveNewLayoutField: (CalendarLayoutSettingsPB layoutSettings) {
+            _loadAllEvents();
+            emit(state.copyWith(settings: Some(layoutSettings)));
           },
           createEvent: (DateTime date, String title) async {
             await _createEvent(date, title);
           },
-          didReceiveEvent: (CalendarEventData<CalendarCardData> newEvent) {
-            emit(state.copyWith(events: [...state.events, newEvent]));
+          updateCalendarLayoutSetting:
+              (CalendarLayoutSettingsPB layoutSetting) async {
+            await _updateCalendarLayoutSetting(layoutSetting);
+          },
+          didUpdateEvent: (CalendarEventData<CalendarDayEvent> eventData) {
+            var allEvents = [...state.allEvents];
+            final index = allEvents.indexWhere(
+              (element) => element.event!.cellId == eventData.event!.cellId,
+            );
+            if (index != -1) {
+              allEvents[index] = eventData;
+            }
+            emit(state.copyWith(
+              allEvents: allEvents,
+              updateEvent: eventData,
+            ));
+          },
+          didReceiveNewEvent: (CalendarEventData<CalendarDayEvent> event) {
+            emit(state.copyWith(
+              allEvents: [...state.allEvents, event],
+              newEvent: event,
+            ));
           },
-          didUpdateFieldInfos: (Map<String, FieldInfo> fieldInfoByFieldId) {
-            emit(state.copyWith(fieldInfoByFieldId: fieldInfoByFieldId));
+          didDeleteEvents: (List<String> deletedRowIds) {
+            var events = [...state.allEvents];
+            events.retainWhere(
+              (element) => !deletedRowIds.contains(element.event!.cellId.rowId),
+            );
+            emit(
+              state.copyWith(
+                allEvents: events,
+                deleteEventIds: deletedRowIds,
+              ),
+            );
           },
         );
       },
@@ -97,7 +132,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
   }
 
   Future<void> _createEvent(DateTime date, String title) async {
-    state.settings.fold(
+    return state.settings.fold(
       () => null,
       (settings) async {
         final dateField = _getCalendarFieldInfo(settings.layoutFieldId);
@@ -110,8 +145,8 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
             },
           );
 
-          result.fold(
-            (newRow) => _loadEvent(newRow.id),
+          return result.fold(
+            (newRow) {},
             (err) => Log.error(err),
           );
         }
@@ -119,17 +154,23 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
     );
   }
 
-  Future<void> _loadEvent(String rowId) async {
+  Future<void> _updateCalendarLayoutSetting(
+      CalendarLayoutSettingsPB layoutSetting) async {
+    return _databaseController.updateCalenderLayoutSetting(layoutSetting);
+  }
+
+  Future<CalendarEventData<CalendarDayEvent>?> _loadEvent(String rowId) async {
     final payload = RowIdPB(viewId: viewId, rowId: rowId);
-    DatabaseEventGetCalendarEvent(payload).send().then((result) {
-      result.fold(
+    return DatabaseEventGetCalendarEvent(payload).send().then((result) {
+      return result.fold(
         (eventPB) {
           final calendarEvent = _calendarEventDataFromEventPB(eventPB);
-          if (calendarEvent != null) {
-            add(CalendarEvent.didReceiveEvent(calendarEvent));
-          }
+          return calendarEvent;
+        },
+        (r) {
+          Log.error(r);
+          return null;
         },
-        (r) => Log.error(r),
       );
     });
   }
@@ -140,7 +181,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
       result.fold(
         (events) {
           if (!isClosed) {
-            final calendarEvents = <CalendarEventData<CalendarCardData>>[];
+            final calendarEvents = <CalendarEventData<CalendarDayEvent>>[];
             for (final eventPB in events.items) {
               final calendarEvent = _calendarEventDataFromEventPB(eventPB);
               if (calendarEvent != null) {
@@ -156,9 +197,9 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
     });
   }
 
-  CalendarEventData<CalendarCardData>? _calendarEventDataFromEventPB(
+  CalendarEventData<CalendarDayEvent>? _calendarEventDataFromEventPB(
       CalendarEventPB eventPB) {
-    final fieldInfo = state.fieldInfoByFieldId[eventPB.titleFieldId];
+    final fieldInfo = fieldInfoByFieldId[eventPB.titleFieldId];
     if (fieldInfo != null) {
       final cellId = CellIdentifier(
         viewId: viewId,
@@ -166,7 +207,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
         fieldInfo: fieldInfo,
       );
 
-      final eventData = CalendarCardData(
+      final eventData = CalendarDayEvent(
         event: eventPB,
         cellId: cellId,
       );
@@ -192,10 +233,31 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
       },
       onFieldsChanged: (fieldInfos) {
         if (isClosed) return;
-        final fieldInfoByFieldId = {
+        fieldInfoByFieldId = {
           for (var fieldInfo in fieldInfos) fieldInfo.field.id: fieldInfo
         };
-        add(CalendarEvent.didUpdateFieldInfos(fieldInfoByFieldId));
+      },
+      onRowsChanged: ((onRowsChanged, rowByRowId, reason) {}),
+      onRowsCreated: ((ids) async {
+        for (final id in ids) {
+          final event = await _loadEvent(id);
+          if (event != null && !isClosed) {
+            add(CalendarEvent.didReceiveNewEvent(event));
+          }
+        }
+      }),
+      onRowsDeleted: (ids) {
+        if (isClosed) return;
+        add(CalendarEvent.didDeleteEvents(ids));
+      },
+      onRowsUpdated: (ids) async {
+        if (isClosed) return;
+        for (final id in ids) {
+          final event = await _loadEvent(id);
+          if (event != null) {
+            add(CalendarEvent.didUpdateEvent(event));
+          }
+        }
       },
     );
 
@@ -204,9 +266,13 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
       onLoadLayout: _didReceiveLayoutSetting,
     );
 
+    final onCalendarLayoutFieldChanged = CalendarLayoutCallbacks(
+        onCalendarLayoutChanged: _didReceiveNewLayoutField);
+
     _databaseController.addListener(
       onDatabaseChanged: onDatabaseChanged,
       onLayoutChanged: onLayoutChanged,
+      onCalendarLayoutChanged: onCalendarLayoutFieldChanged,
     );
   }
 
@@ -216,44 +282,75 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
       add(CalendarEvent.didReceiveCalendarSettings(layoutSetting.calendar));
     }
   }
+
+  void _didReceiveNewLayoutField(LayoutSettingPB layoutSetting) {
+    if (layoutSetting.hasCalendar()) {
+      if (isClosed) return;
+      add(CalendarEvent.didReceiveNewLayoutField(layoutSetting.calendar));
+    }
+  }
 }
 
-typedef Events = List<CalendarEventData<CalendarCardData>>;
+typedef Events = List<CalendarEventData<CalendarDayEvent>>;
 
 @freezed
 class CalendarEvent with _$CalendarEvent {
   const factory CalendarEvent.initial() = _InitialCalendar;
+
+  // Called after loading the calendar layout setting from the backend
   const factory CalendarEvent.didReceiveCalendarSettings(
       CalendarLayoutSettingsPB settings) = _ReceiveCalendarSettings;
+
+  // Called after loading all the current evnets
   const factory CalendarEvent.didLoadAllEvents(Events events) =
       _ReceiveCalendarEvents;
-  const factory CalendarEvent.didReceiveEvent(
-      CalendarEventData<CalendarCardData> event) = _ReceiveEvent;
-  const factory CalendarEvent.didUpdateFieldInfos(
-      Map<String, FieldInfo> fieldInfoByFieldId) = _DidUpdateFieldInfos;
+
+  // Called when specific event was updated
+  const factory CalendarEvent.didUpdateEvent(
+      CalendarEventData<CalendarDayEvent> event) = _DidUpdateEvent;
+
+  // Called after creating a new event
+  const factory CalendarEvent.didReceiveNewEvent(
+      CalendarEventData<CalendarDayEvent> event) = _DidReceiveNewEvent;
+
+  // Called when deleting events
+  const factory CalendarEvent.didDeleteEvents(List<String> rowIds) =
+      _DidDeleteEvents;
+
+  // Called when creating a new event
   const factory CalendarEvent.createEvent(DateTime date, String title) =
       _CreateEvent;
+
+  // Called when updating the calendar's layout settings
+  const factory CalendarEvent.updateCalendarLayoutSetting(
+      CalendarLayoutSettingsPB layoutSetting) = _UpdateCalendarLayoutSetting;
+
   const factory CalendarEvent.didReceiveDatabaseUpdate(DatabasePB database) =
       _ReceiveDatabaseUpdate;
+
+  const factory CalendarEvent.didReceiveNewLayoutField(
+      CalendarLayoutSettingsPB layoutSettings) = _DidReceiveNewLayoutField;
 }
 
 @freezed
 class CalendarState with _$CalendarState {
   const factory CalendarState({
-    required String databaseId,
     required Option<DatabasePB> database,
-    required Events events,
-    required Map<String, FieldInfo> fieldInfoByFieldId,
+    required Events allEvents,
+    required Events initialEvents,
+    CalendarEventData<CalendarDayEvent>? newEvent,
+    required List<String> deleteEventIds,
+    CalendarEventData<CalendarDayEvent>? updateEvent,
     required Option<CalendarLayoutSettingsPB> settings,
     required DatabaseLoadingState loadingState,
     required Option<FlowyError> noneOrError,
   }) = _CalendarState;
 
-  factory CalendarState.initial(String databaseId) => CalendarState(
+  factory CalendarState.initial() => CalendarState(
         database: none(),
-        databaseId: databaseId,
-        fieldInfoByFieldId: {},
-        events: [],
+        allEvents: [],
+        initialEvents: [],
+        deleteEventIds: [],
         settings: none(),
         noneOrError: none(),
         loadingState: const _Loading(),
@@ -277,8 +374,10 @@ class CalendarEditingRow {
   });
 }
 
-class CalendarCardData {
+class CalendarDayEvent {
   final CalendarEventPB event;
   final CellIdentifier cellId;
-  CalendarCardData({required this.cellId, required this.event});
+
+  String get eventId => cellId.rowId;
+  CalendarDayEvent({required this.cellId, required this.event});
 }

+ 55 - 0
frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_setting_bloc.dart

@@ -0,0 +1,55 @@
+import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart';
+import 'package:bloc/bloc.dart';
+import 'package:dartz/dartz.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+part 'calendar_setting_bloc.freezed.dart';
+
+typedef DayOfWeek = int;
+
+class CalendarSettingBloc
+    extends Bloc<CalendarSettingEvent, CalendarSettingState> {
+  CalendarSettingBloc({required CalendarLayoutSettingsPB? layoutSettings})
+      : super(CalendarSettingState.initial(layoutSettings)) {
+    on<CalendarSettingEvent>((event, emit) {
+      event.when(
+        performAction: (action) {
+          emit(state.copyWith(selectedAction: Some(action)));
+        },
+        updateLayoutSetting: (setting) {
+          emit(state.copyWith(layoutSetting: Some(setting)));
+        },
+      );
+    });
+  }
+
+  @override
+  Future<void> close() async => super.close();
+}
+
+@freezed
+class CalendarSettingState with _$CalendarSettingState {
+  const factory CalendarSettingState({
+    required Option<CalendarSettingAction> selectedAction,
+    required Option<CalendarLayoutSettingsPB> layoutSetting,
+  }) = _CalendarSettingState;
+
+  factory CalendarSettingState.initial(
+          CalendarLayoutSettingsPB? layoutSettings) =>
+      CalendarSettingState(
+        selectedAction: none(),
+        layoutSetting: layoutSettings == null ? none() : Some(layoutSettings),
+      );
+}
+
+@freezed
+class CalendarSettingEvent with _$CalendarSettingEvent {
+  const factory CalendarSettingEvent.performAction(
+      CalendarSettingAction action) = _PerformAction;
+  const factory CalendarSettingEvent.updateLayoutSetting(
+      CalendarLayoutSettingsPB setting) = _UpdateLayoutSetting;
+}
+
+enum CalendarSettingAction {
+  layout,
+}

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

@@ -34,7 +34,7 @@ class CalendarPluginBuilder extends PluginBuilder {
 
 class CalendarPluginConfig implements PluginConfig {
   @override
-  bool get creatable => false;
+  bool get creatable => true;
 }
 
 class CalendarPlugin extends Plugin {

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

@@ -0,0 +1,267 @@
+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/widgets/card/card_cell_builder.dart';
+import 'package:appflowy/plugins/database_view/widgets/card/cells/text_card_cell.dart';
+import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
+import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
+import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pbenum.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/hover.dart';
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+
+import '../../grid/presentation/layout/sizes.dart';
+import '../application/calendar_bloc.dart';
+
+class CalendarDayCard extends StatelessWidget {
+  final String viewId;
+  final bool isToday;
+  final bool isInMonth;
+  final DateTime date;
+  final RowCache _rowCache;
+  final CardCellBuilder _cellBuilder;
+  final List<CalendarDayEvent> events;
+  final void Function(DateTime) onCreateEvent;
+
+  CalendarDayCard({
+    required this.viewId,
+    required this.isToday,
+    required this.isInMonth,
+    required this.date,
+    required this.onCreateEvent,
+    required RowCache rowCache,
+    required this.events,
+    Key? key,
+  })  : _rowCache = rowCache,
+        _cellBuilder = CardCellBuilder(rowCache.cellCache),
+        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) {
+        final children = events.map((event) {
+          return _DayEventCell(
+            event: event,
+            viewId: viewId,
+            onClick: () => _showRowDetailPage(event, context),
+            child: _cellBuilder.buildCell(
+              cellId: event.cellId,
+              styles: {FieldType.RichText: TextCardCellStyle(10)},
+            ),
+          );
+        }).toList();
+
+        final child = Padding(
+            padding: const EdgeInsets.all(8.0),
+            child: Column(
+              mainAxisSize: MainAxisSize.min,
+              children: [
+                _Header(
+                  date: date,
+                  isInMonth: isInMonth,
+                  isToday: isToday,
+                  onCreate: () => onCreateEvent(date),
+                ),
+                VSpace(GridSize.typeOptionSeparatorHeight),
+                Flexible(
+                  child: ListView.separated(
+                    itemBuilder: (BuildContext context, int index) {
+                      return children[index];
+                    },
+                    itemCount: children.length,
+                    separatorBuilder: (BuildContext context, int index) =>
+                        VSpace(GridSize.typeOptionSeparatorHeight),
+                  ),
+                ),
+              ],
+            ));
+
+        return Container(
+          color: backgroundColor,
+          child: MouseRegion(
+            cursor: SystemMouseCursors.click,
+            onEnter: (p) => notifyEnter(context, true),
+            onExit: (p) => notifyEnter(context, false),
+            child: child,
+          ),
+        );
+      }),
+    );
+  }
+
+  void _showRowDetailPage(CalendarDayEvent event, BuildContext context) {
+    final dataController = RowController(
+      rowId: event.cellId.rowId,
+      viewId: viewId,
+      rowCache: _rowCache,
+    );
+
+    FlowyOverlay.show(
+      context: context,
+      builder: (BuildContext context) {
+        return RowDetailPage(
+          cellBuilder: GridCellBuilder(
+            cellCache: _rowCache.cellCache,
+          ),
+          dataController: dataController,
+        );
+      },
+    );
+  }
+
+  notifyEnter(BuildContext context, bool isEnter) {
+    Provider.of<_CardEnterNotifier>(
+      context,
+      listen: false,
+    ).onEnter = isEnter;
+  }
+}
+
+class _DayEventCell extends StatelessWidget {
+  final String viewId;
+  final CalendarDayEvent event;
+  final VoidCallback onClick;
+  final Widget child;
+  const _DayEventCell({
+    required this.viewId,
+    required this.event,
+    required this.onClick,
+    required this.child,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return FlowyHover(
+      child: GestureDetector(
+        onTap: onClick,
+        child: Container(
+          padding: const EdgeInsets.symmetric(horizontal: 8),
+          child: child,
+        ),
+      ),
+    );
+  }
+}
+
+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;
+    String dayString = date.day == 1
+        ? DateFormat('MMM d', context.locale.toLanguageTag()).format(date)
+        : date.day.toString();
+
+    if (isToday) {
+      dayTextColor = Theme.of(context).colorScheme.onPrimary;
+    }
+    if (!isInMonth) {
+      dayTextColor = Theme.of(context).disabledColor;
+    }
+
+    Widget day = Container(
+      decoration: BoxDecoration(
+        color: isToday ? Theme.of(context).colorScheme.primary : null,
+        borderRadius: Corners.s6Border,
+      ),
+      padding: GridSize.typeOptionContentInsets,
+      child: FlowyText.medium(
+        dayString,
+        color: dayTextColor,
+      ),
+    );
+
+    return day;
+  }
+}
+
+class _CardEnterNotifier extends ChangeNotifier {
+  bool _onEnter = false;
+
+  _CardEnterNotifier();
+
+  set onEnter(bool value) {
+    if (_onEnter != value) {
+      _onEnter = value;
+      notifyListeners();
+    }
+  }
+
+  bool get onEnter => _onEnter;
+}

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

@@ -1,22 +1,15 @@
 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/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/hover.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
-import 'package:provider/provider.dart';
 
-import '../../grid/presentation/layout/sizes.dart';
-import '../../widgets/row/cell_builder.dart';
-import '../../widgets/row/row_detail.dart';
+import 'calendar_day.dart';
 import 'layout/sizes.dart';
 import 'toolbar/calendar_toolbar.dart';
 
@@ -29,7 +22,7 @@ class CalendarPage extends StatefulWidget {
 }
 
 class _CalendarPageState extends State<CalendarPage> {
-  final _eventController = EventController<CalendarCardData>();
+  final _eventController = EventController<CalendarDayEvent>();
   GlobalKey<MonthViewState>? _calendarState;
   late CalendarBloc _calendarBloc;
 
@@ -58,21 +51,55 @@ class _CalendarPageState extends State<CalendarPage> {
             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: MultiBlocListener(
+          listeners: [
+            BlocListener<CalendarBloc, CalendarState>(
+              listenWhen: (p, c) => p.initialEvents != c.initialEvents,
+              listener: (context, state) {
+                _eventController.removeWhere((_) => true);
+                _eventController.addAll(state.initialEvents);
+              },
+            ),
+            BlocListener<CalendarBloc, CalendarState>(
+              listenWhen: (p, c) => p.deleteEventIds != c.deleteEventIds,
+              listener: (context, state) {
+                _eventController.removeWhere(
+                  (element) =>
+                      state.deleteEventIds.contains(element.event!.eventId),
+                );
+              },
+            ),
+            BlocListener<CalendarBloc, CalendarState>(
+              listenWhen: (p, c) => p.updateEvent != c.updateEvent,
+              listener: (context, state) {
+                if (state.updateEvent != null) {
+                  _eventController.removeWhere((element) =>
+                      state.updateEvent!.event!.eventId ==
+                      element.event!.eventId);
+                  _eventController.add(state.updateEvent!);
+                }
+              },
+            ),
+            BlocListener<CalendarBloc, CalendarState>(
+              listenWhen: (p, c) => p.newEvent != c.newEvent,
+              listener: (context, state) {
+                if (state.newEvent != null) {
+                  _eventController.add(state.newEvent!);
+                }
+              },
+            ),
+          ],
           child: BlocBuilder<CalendarBloc, CalendarState>(
             builder: (context, state) {
               return Column(
                 children: [
                   // const _ToolbarBlocAdaptor(),
-                  _toolbar(),
-                  _buildCalendar(_eventController),
+                  const CalendarToolbar(),
+                  _buildCalendar(
+                    _eventController,
+                    state.settings
+                        .foldLeft(0, (previous, a) => a.firstDayOfWeek),
+                  ),
                 ],
               );
             },
@@ -82,16 +109,13 @@ class _CalendarPageState extends State<CalendarPage> {
     );
   }
 
-  Widget _toolbar() {
-    return const CalendarToolbar();
-  }
-
-  Widget _buildCalendar(EventController eventController) {
+  Widget _buildCalendar(EventController eventController, int firstDayOfWeek) {
     return Expanded(
       child: MonthView(
         key: _calendarState,
         controller: _eventController,
-        cellAspectRatio: 1.75,
+        cellAspectRatio: .9,
+        startDay: _weekdayFromInt(firstDayOfWeek),
         borderColor: Theme.of(context).dividerColor,
         headerBuilder: _headerNavigatorBuilder,
         weekDayBuilder: _headerWeekDayBuilder,
@@ -154,47 +178,19 @@ class _CalendarPageState extends State<CalendarPage> {
 
   Widget _calendarDayBuilder(
     DateTime date,
-    List<CalendarEventData<CalendarCardData>> calenderEvents,
+    List<CalendarEventData<CalendarDayEvent>> 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 = RowController(
-              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();
+    final events = calenderEvents.map((value) => value.event!).toList();
 
-    return _CalendarCard(
+    return CalendarDayCard(
+      viewId: widget.view.id,
       isToday: isToday,
       isInMonth: isInMonth,
+      events: events,
       date: date,
-      children: cells,
+      rowCache: _calendarBloc.rowCache,
       onCreateEvent: (date) {
         _calendarBloc.add(
           CalendarEvent.createEvent(
@@ -205,175 +201,9 @@ class _CalendarPageState extends State<CalendarPage> {
       },
     );
   }
-}
-
-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;
+  WeekDays _weekdayFromInt(int dayOfWeek) {
+    // MonthView places the first day of week on the second column for some reason.
+    return WeekDays.values[(dayOfWeek + 1) % 7];
   }
 }
-
-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;
-    String dayString = date.day == 1
-        ? DateFormat('MMM d', context.locale.toLanguageTag()).format(date)
-        : date.day.toString();
-
-    if (isToday) {
-      dayTextColor = Theme.of(context).colorScheme.onPrimary;
-    }
-    if (!isInMonth) {
-      dayTextColor = Theme.of(context).disabledColor;
-    }
-
-    Widget day = Container(
-      decoration: BoxDecoration(
-        color: isToday ? Theme.of(context).colorScheme.primary : null,
-        borderRadius: Corners.s6Border,
-      ),
-      padding: GridSize.typeOptionContentInsets,
-      child: FlowyText.medium(
-        dayString,
-        color: dayTextColor,
-      ),
-    );
-
-    return day;
-  }
-}
-
-class _CardEnterNotifier extends ChangeNotifier {
-  bool _onEnter = false;
-
-  _CardEnterNotifier();
-
-  set onEnter(bool value) {
-    if (_onEnter != value) {
-      _onEnter = value;
-      notifyListeners();
-    }
-  }
-
-  bool get onEnter => _onEnter;
-}

+ 410 - 0
frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart

@@ -0,0 +1,410 @@
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
+import 'package:appflowy/plugins/database_view/application/setting/property_bloc.dart';
+import 'package:appflowy/plugins/database_view/calendar/application/calendar_setting_bloc.dart';
+import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
+import 'package:appflowy/startup/startup.dart';
+import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
+import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart';
+import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart'
+    hide DateFormat;
+import 'package:appflowy_popover/appflowy_popover.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra/image.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:protobuf/protobuf.dart';
+
+import 'calendar_setting.dart';
+
+class CalendarLayoutSetting extends StatefulWidget {
+  final CalendarSettingContext settingContext;
+  final Function(CalendarLayoutSettingsPB? layoutSettings) onUpdated;
+
+  const CalendarLayoutSetting({
+    required this.onUpdated,
+    required this.settingContext,
+    super.key,
+  });
+
+  @override
+  State<CalendarLayoutSetting> createState() => _CalendarLayoutSettingState();
+}
+
+class _CalendarLayoutSettingState extends State<CalendarLayoutSetting> {
+  late final PopoverMutex popoverMutex;
+
+  @override
+  void initState() {
+    popoverMutex = PopoverMutex();
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<CalendarSettingBloc, CalendarSettingState>(
+      builder: (context, state) {
+        final CalendarLayoutSettingsPB? settings = state.layoutSetting
+            .foldLeft(null, (previous, settings) => settings);
+
+        if (settings == null) {
+          return const CircularProgressIndicator();
+        }
+        final availableSettings = _availableCalendarSettings(settings);
+
+        final items = availableSettings.map((setting) {
+          switch (setting) {
+            case CalendarLayoutSettingAction.showWeekNumber:
+              return ShowWeekNumber(
+                showWeekNumbers: settings.showWeekNumbers,
+                onUpdated: (showWeekNumbers) {
+                  _updateLayoutSettings(
+                    context,
+                    showWeekNumbers: showWeekNumbers,
+                    onUpdated: widget.onUpdated,
+                  );
+                },
+              );
+            case CalendarLayoutSettingAction.showWeekends:
+              return ShowWeekends(
+                showWeekends: settings.showWeekends,
+                onUpdated: (showWeekends) {
+                  _updateLayoutSettings(
+                    context,
+                    showWeekends: showWeekends,
+                    onUpdated: widget.onUpdated,
+                  );
+                },
+              );
+            case CalendarLayoutSettingAction.firstDayOfWeek:
+              return FirstDayOfWeek(
+                firstDayOfWeek: settings.firstDayOfWeek,
+                popoverMutex: popoverMutex,
+                onUpdated: (firstDayOfWeek) {
+                  _updateLayoutSettings(
+                    context,
+                    onUpdated: widget.onUpdated,
+                    firstDayOfWeek: firstDayOfWeek,
+                  );
+                },
+              );
+            case CalendarLayoutSettingAction.layoutField:
+              return LayoutDateField(
+                fieldController: widget.settingContext.fieldController,
+                viewId: widget.settingContext.viewId,
+                fieldId: settings.layoutFieldId,
+                popoverMutex: popoverMutex,
+                onUpdated: (fieldId) {
+                  _updateLayoutSettings(context,
+                      onUpdated: widget.onUpdated, layoutFieldId: fieldId);
+                },
+              );
+            default:
+              return ShowWeekends(
+                showWeekends: settings.showWeekends,
+                onUpdated: (showWeekends) {
+                  _updateLayoutSettings(context,
+                      onUpdated: widget.onUpdated, showWeekends: showWeekends);
+                },
+              );
+          }
+        }).toList();
+
+        return SizedBox(
+          width: 200,
+          child: ListView.separated(
+            shrinkWrap: true,
+            controller: ScrollController(),
+            itemCount: items.length,
+            separatorBuilder: (context, index) =>
+                VSpace(GridSize.typeOptionSeparatorHeight),
+            physics: StyledScrollPhysics(),
+            itemBuilder: (BuildContext context, int index) => items[index],
+            padding: const EdgeInsets.all(6.0),
+          ),
+        );
+      },
+    );
+  }
+
+  List<CalendarLayoutSettingAction> _availableCalendarSettings(
+      CalendarLayoutSettingsPB layoutSettings) {
+    List<CalendarLayoutSettingAction> settings = [
+      CalendarLayoutSettingAction.layoutField,
+      // CalendarLayoutSettingAction.layoutType,
+      // CalendarLayoutSettingAction.showWeekNumber,
+    ];
+
+    switch (layoutSettings.layoutTy) {
+      case CalendarLayoutPB.DayLayout:
+        // settings.add(CalendarLayoutSettingAction.showTimeLine);
+        break;
+      case CalendarLayoutPB.MonthLayout:
+        settings.addAll([
+          // CalendarLayoutSettingAction.showWeekends,
+          // if (layoutSettings.showWeekends)
+          CalendarLayoutSettingAction.firstDayOfWeek,
+        ]);
+        break;
+      case CalendarLayoutPB.WeekLayout:
+        settings.addAll([
+          // CalendarLayoutSettingAction.showWeekends,
+          // if (layoutSettings.showWeekends)
+          CalendarLayoutSettingAction.firstDayOfWeek,
+          // CalendarLayoutSettingAction.showTimeLine,
+        ]);
+        break;
+    }
+
+    return settings;
+  }
+
+  void _updateLayoutSettings(
+    BuildContext context, {
+    required Function(CalendarLayoutSettingsPB? layoutSettings) onUpdated,
+    bool? showWeekends,
+    bool? showWeekNumbers,
+    int? firstDayOfWeek,
+    String? layoutFieldId,
+  }) {
+    CalendarLayoutSettingsPB setting = context
+        .read<CalendarSettingBloc>()
+        .state
+        .layoutSetting
+        .foldLeft(null, (previous, settings) => settings)!;
+    setting.freeze();
+    setting = setting.rebuild((setting) {
+      if (showWeekends != null) {
+        setting.showWeekends = !showWeekends;
+      }
+      if (showWeekNumbers != null) {
+        setting.showWeekNumbers = !showWeekNumbers;
+      }
+      if (firstDayOfWeek != null) {
+        setting.firstDayOfWeek = firstDayOfWeek;
+      }
+      if (layoutFieldId != null) {
+        setting.layoutFieldId = layoutFieldId;
+      }
+    });
+    context
+        .read<CalendarSettingBloc>()
+        .add(CalendarSettingEvent.updateLayoutSetting(setting));
+    onUpdated(setting);
+  }
+}
+
+class LayoutDateField extends StatelessWidget {
+  final String fieldId;
+  final String viewId;
+  final FieldController fieldController;
+  final PopoverMutex popoverMutex;
+  final Function(String fieldId) onUpdated;
+
+  const LayoutDateField({
+    required this.fieldId,
+    required this.fieldController,
+    required this.viewId,
+    required this.popoverMutex,
+    required this.onUpdated,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return AppFlowyPopover(
+      direction: PopoverDirection.leftWithTopAligned,
+      constraints: BoxConstraints.loose(const Size(300, 400)),
+      mutex: popoverMutex,
+      popupBuilder: (context) {
+        return BlocProvider(
+          create: (context) => getIt<DatabasePropertyBloc>(
+              param1: viewId, param2: fieldController)
+            ..add(const DatabasePropertyEvent.initial()),
+          child: BlocBuilder<DatabasePropertyBloc, DatabasePropertyState>(
+            builder: (context, state) {
+              final items = state.fieldContexts
+                  .where((field) => field.fieldType == FieldType.DateTime)
+                  .map(
+                (fieldInfo) {
+                  return SizedBox(
+                    height: GridSize.popoverItemHeight,
+                    child: FlowyButton(
+                      text: FlowyText.medium(fieldInfo.name),
+                      onTap: () {
+                        onUpdated(fieldInfo.id);
+                        popoverMutex.close();
+                      },
+                      leftIcon: svgWidget('grid/field/date'),
+                      rightIcon: fieldInfo.id == fieldId
+                          ? svgWidget('grid/checkmark')
+                          : null,
+                    ),
+                  );
+                },
+              ).toList();
+
+              return SizedBox(
+                width: 200,
+                child: ListView.separated(
+                  shrinkWrap: true,
+                  itemBuilder: (context, index) => items[index],
+                  separatorBuilder: (context, index) =>
+                      VSpace(GridSize.typeOptionSeparatorHeight),
+                  itemCount: items.length,
+                ),
+              );
+            },
+          ),
+        );
+      },
+      child: SizedBox(
+        height: GridSize.popoverItemHeight,
+        child: FlowyButton(
+          margin: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 10.0),
+          text: FlowyText.medium(
+              LocaleKeys.calendar_settings_layoutDateField.tr()),
+        ),
+      ),
+    );
+  }
+}
+
+class ShowWeekNumber extends StatelessWidget {
+  final bool showWeekNumbers;
+  final Function(bool showWeekNumbers) onUpdated;
+
+  const ShowWeekNumber({
+    required this.showWeekNumbers,
+    required this.onUpdated,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return _toggleItem(
+      onToggle: (showWeekNumbers) {
+        onUpdated(!showWeekNumbers);
+      },
+      value: showWeekNumbers,
+      text: LocaleKeys.calendar_settings_showWeekNumbers.tr(),
+    );
+  }
+}
+
+class ShowWeekends extends StatelessWidget {
+  final bool showWeekends;
+  final Function(bool showWeekends) onUpdated;
+  const ShowWeekends({
+    super.key,
+    required this.showWeekends,
+    required this.onUpdated,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return _toggleItem(
+      onToggle: (showWeekends) {
+        onUpdated(!showWeekends);
+      },
+      value: showWeekends,
+      text: LocaleKeys.calendar_settings_showWeekends.tr(),
+    );
+  }
+}
+
+class FirstDayOfWeek extends StatelessWidget {
+  final int firstDayOfWeek;
+  final PopoverMutex popoverMutex;
+  final Function(int firstDayOfWeek) onUpdated;
+  const FirstDayOfWeek({
+    super.key,
+    required this.firstDayOfWeek,
+    required this.onUpdated,
+    required this.popoverMutex,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return AppFlowyPopover(
+      direction: PopoverDirection.leftWithTopAligned,
+      constraints: BoxConstraints.loose(const Size(300, 400)),
+      mutex: popoverMutex,
+      popupBuilder: (context) {
+        final symbols =
+            DateFormat.EEEE(context.locale.toLanguageTag()).dateSymbols;
+        // starts from sunday
+        final items = symbols.WEEKDAYS.asMap().entries.map((entry) {
+          final index = (entry.key - 1) % 7;
+          final string = entry.value;
+          return SizedBox(
+            height: GridSize.popoverItemHeight,
+            child: FlowyButton(
+              text: FlowyText.medium(string),
+              onTap: () {
+                onUpdated(index);
+                popoverMutex.close();
+              },
+              rightIcon:
+                  firstDayOfWeek == index ? svgWidget('grid/checkmark') : null,
+            ),
+          );
+        }).toList();
+
+        return SizedBox(
+          width: 100,
+          child: ListView.separated(
+            shrinkWrap: true,
+            itemBuilder: (context, index) => items[index],
+            separatorBuilder: (context, index) =>
+                VSpace(GridSize.typeOptionSeparatorHeight),
+            itemCount: 2,
+          ),
+        );
+      },
+      child: SizedBox(
+        height: GridSize.popoverItemHeight,
+        child: FlowyButton(
+          margin: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 10.0),
+          text: FlowyText.medium(
+              LocaleKeys.calendar_settings_firstDayOfWeek.tr()),
+        ),
+      ),
+    );
+  }
+}
+
+Widget _toggleItem({
+  required String text,
+  required bool value,
+  required void Function(bool) onToggle,
+}) {
+  return SizedBox(
+    height: GridSize.popoverItemHeight,
+    child: Padding(
+      padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 10.0),
+      child: Row(
+        children: [
+          FlowyText.medium(text),
+          const Spacer(),
+          Toggle(
+            value: value,
+            onChanged: (value) => onToggle(!value),
+            style: ToggleStyle.big,
+            padding: EdgeInsets.zero,
+          ),
+        ],
+      ),
+    ),
+  );
+}
+
+enum CalendarLayoutSettingAction {
+  layoutField,
+  layoutType,
+  showWeekends,
+  firstDayOfWeek,
+  showWeekNumber,
+  showTimeLine,
+}

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

@@ -0,0 +1,112 @@
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
+import 'package:appflowy/plugins/database_view/calendar/application/calendar_setting_bloc.dart';
+import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
+import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra_ui/style_widget/button.dart';
+import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
+import 'package:flowy_infra_ui/style_widget/text.dart';
+import 'package:flowy_infra_ui/widget/spacing.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:styled_widget/styled_widget.dart';
+
+import 'calendar_layout_setting.dart';
+
+/// The highest-level widget shown in the popover triggered by clicking the
+/// "Settings" button. By default, shows [AllCalendarSettings] but upon
+/// selecting a category, replaces contents with contents of the submenu.
+class CalendarSetting extends StatelessWidget {
+  final CalendarSettingContext settingContext;
+  final CalendarLayoutSettingsPB? layoutSettings;
+  final Function(CalendarLayoutSettingsPB? layoutSettings) onUpdated;
+
+  const CalendarSetting({
+    required this.onUpdated,
+    required this.layoutSettings,
+    required this.settingContext,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocProvider<CalendarSettingBloc>(
+      create: (context) => CalendarSettingBloc(layoutSettings: layoutSettings),
+      child: BlocBuilder<CalendarSettingBloc, CalendarSettingState>(
+        builder: (context, state) {
+          final CalendarSettingAction? action =
+              state.selectedAction.foldLeft(null, (previous, action) => action);
+          switch (action) {
+            case CalendarSettingAction.layout:
+              return CalendarLayoutSetting(
+                onUpdated: onUpdated,
+                settingContext: settingContext,
+              );
+            default:
+              return const AllCalendarSettings().padding(all: 6.0);
+          }
+        },
+      ),
+    );
+  }
+}
+
+/// Shows all of the available categories of settings that can be set here.
+/// For now, this only includes the Layout category.
+class AllCalendarSettings extends StatelessWidget {
+  const AllCalendarSettings({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    final items = CalendarSettingAction.values
+        .map((e) => _settingItem(context, e))
+        .toList();
+
+    return SizedBox(
+      width: 140,
+      child: ListView.separated(
+        shrinkWrap: true,
+        controller: ScrollController(),
+        itemCount: items.length,
+        separatorBuilder: (context, index) =>
+            VSpace(GridSize.typeOptionSeparatorHeight),
+        physics: StyledScrollPhysics(),
+        itemBuilder: (BuildContext context, int index) => items[index],
+      ),
+    );
+  }
+
+  Widget _settingItem(BuildContext context, CalendarSettingAction action) {
+    return SizedBox(
+      height: GridSize.popoverItemHeight,
+      child: FlowyButton(
+        text: FlowyText.medium(action.title()),
+        onTap: () {
+          context
+              .read<CalendarSettingBloc>()
+              .add(CalendarSettingEvent.performAction(action));
+        },
+      ),
+    );
+  }
+}
+
+extension _SettingExtension on CalendarSettingAction {
+  String title() {
+    switch (this) {
+      case CalendarSettingAction.layout:
+        return LocaleKeys.grid_settings_layout.tr();
+    }
+  }
+}
+
+class CalendarSettingContext {
+  final String viewId;
+  final FieldController fieldController;
+
+  CalendarSettingContext({
+    required this.viewId,
+    required this.fieldController,
+  });
+}

+ 67 - 7
frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_toolbar.dart

@@ -1,5 +1,14 @@
-import 'package:flowy_infra_ui/style_widget/button.dart';
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
+import 'package:appflowy_popover/appflowy_popover.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra/theme_extension.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import '../../application/calendar_bloc.dart';
+import 'calendar_setting.dart';
 
 class CalendarToolbar extends StatelessWidget {
   const CalendarToolbar({super.key});
@@ -10,14 +19,65 @@ class CalendarToolbar extends StatelessWidget {
       height: 40,
       child: Row(
         mainAxisAlignment: MainAxisAlignment.end,
-        children: const [
-          FlowyTextButton(
-            "Settings",
-            fillColor: Colors.transparent,
-            padding: EdgeInsets.symmetric(horizontal: 6, vertical: 2),
-          ),
+        children: [
+          _SettingButton(),
         ],
       ),
     );
   }
 }
+
+class _SettingButton extends StatefulWidget {
+  @override
+  State<StatefulWidget> createState() => _SettingButtonState();
+}
+
+class _SettingButtonState extends State<_SettingButton> {
+  late PopoverController popoverController;
+
+  @override
+  void initState() {
+    popoverController = PopoverController();
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return AppFlowyPopover(
+      controller: popoverController,
+      direction: PopoverDirection.bottomWithRightAligned,
+      triggerActions: PopoverTriggerFlags.none,
+      constraints: BoxConstraints.loose(const Size(300, 400)),
+      margin: EdgeInsets.zero,
+      child: FlowyTextButton(
+        LocaleKeys.settings_title.tr(),
+        fillColor: Colors.transparent,
+        hoverColor: AFThemeExtension.of(context).lightGreyHover,
+        padding: GridSize.typeOptionContentInsets,
+        onPressed: () => popoverController.show(),
+      ),
+      popupBuilder: (BuildContext popoverContext) {
+        final bloc = context.watch<CalendarBloc>();
+        final settingContext = CalendarSettingContext(
+          viewId: bloc.viewId,
+          fieldController: bloc.fieldController,
+        );
+        return CalendarSetting(
+          settingContext: settingContext,
+          layoutSettings: bloc.state.settings.fold(
+            () => null,
+            (settings) => settings,
+          ),
+          onUpdated: (layoutSettings) {
+            if (layoutSettings == null) {
+              return;
+            }
+            context
+                .read<CalendarBloc>()
+                .add(CalendarEvent.updateCalendarLayoutSetting(layoutSettings));
+          },
+        );
+      }, // use blocbuilder
+    );
+  }
+}

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

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

+ 3 - 0
frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart

@@ -23,6 +23,7 @@ class CardCellBuilder<CustomCardData> {
     required CellIdentifier cellId,
     EditableCardNotifier? cellNotifier,
     CardConfiguration<CustomCardData>? cardConfiguration,
+    Map<FieldType, CardCellStyle>? styles,
   }) {
     final cellControllerBuilder = CellControllerBuilder(
       cellId: cellId,
@@ -30,6 +31,7 @@ class CardCellBuilder<CustomCardData> {
     );
 
     final key = cellId.key();
+    final style = styles?[cellId.fieldType];
     switch (cellId.fieldType) {
       case FieldType.Checkbox:
         return CheckboxCardCell(
@@ -70,6 +72,7 @@ class CardCellBuilder<CustomCardData> {
         return TextCardCell(
           cellControllerBuilder: cellControllerBuilder,
           editableNotifier: cellNotifier,
+          style: isStyleOrNull<TextCardCellStyle>(style),
           key: key,
         );
       case FieldType.URL:

+ 13 - 2
frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/card_cell.dart

@@ -24,10 +24,21 @@ class CardConfiguration<CustomCardData> {
   }
 }
 
-abstract class CardCell<T> extends StatefulWidget {
+abstract class CardCellStyle {}
+
+S? isStyleOrNull<S>(CardCellStyle? style) {
+  if (style is S) {
+    return style as S;
+  } else {
+    return null;
+  }
+}
+
+abstract class CardCell<T, S extends CardCellStyle> extends StatefulWidget {
   final T? cardData;
+  final S? style;
 
-  const CardCell({super.key, this.cardData});
+  const CardCell({super.key, this.cardData, this.style});
 }
 
 class EditableCardNotifier {

+ 4 - 1
frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/select_option_card_cell.dart

@@ -9,7 +9,10 @@ import 'package:flutter_bloc/flutter_bloc.dart';
 import '../bloc/select_option_card_cell_bloc.dart';
 import 'card_cell.dart';
 
-class SelectOptionCardCell<T> extends CardCell<T> with EditableCell {
+class SelectOptionCardCellStyle extends CardCellStyle {}
+
+class SelectOptionCardCell<T> extends CardCell<T, SelectOptionCardCellStyle>
+    with EditableCell {
   final CellControllerBuilder cellControllerBuilder;
   final CellRenderHook<List<SelectOptionPB>, T>? renderHook;
 

+ 20 - 5
frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/text_card_cell.dart

@@ -1,5 +1,4 @@
 import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
-import 'package:flowy_infra/size.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
@@ -9,7 +8,14 @@ import '../bloc/text_card_cell_bloc.dart';
 import '../define.dart';
 import 'card_cell.dart';
 
-class TextCardCell extends CardCell with EditableCell {
+class TextCardCellStyle extends CardCellStyle {
+  final double fontSize;
+
+  TextCardCellStyle(this.fontSize);
+}
+
+class TextCardCell extends CardCell<String, TextCardCellStyle>
+    with EditableCell {
   @override
   final EditableCardNotifier? editableNotifier;
   final CellControllerBuilder cellControllerBuilder;
@@ -17,8 +23,9 @@ class TextCardCell extends CardCell with EditableCell {
   const TextCardCell({
     required this.cellControllerBuilder,
     this.editableNotifier,
+    TextCardCellStyle? style,
     Key? key,
-  }) : super(key: key);
+  }) : super(key: key, style: style);
 
   @override
   State<TextCardCell> createState() => _TextCardCellState();
@@ -129,6 +136,14 @@ class _TextCardCellState extends State<TextCardCell> {
     super.dispose();
   }
 
+  double _fontSize() {
+    if (widget.style != null) {
+      return widget.style!.fontSize;
+    } else {
+      return 14;
+    }
+  }
+
   Widget _buildText(TextCardCellState state) {
     return Padding(
       padding: EdgeInsets.symmetric(
@@ -136,7 +151,7 @@ class _TextCardCellState extends State<TextCardCell> {
       ),
       child: FlowyText.medium(
         state.content,
-        fontSize: 14,
+        fontSize: _fontSize(),
         maxLines: null, // Enable multiple lines
       ),
     );
@@ -150,7 +165,7 @@ class _TextCardCellState extends State<TextCardCell> {
         onChanged: (value) => focusChanged(),
         onEditingComplete: () => focusNode.unfocus(),
         maxLines: null,
-        style: Theme.of(context).textTheme.bodyMedium!.size(FontSizes.s14),
+        style: Theme.of(context).textTheme.bodyMedium!.size(_fontSize()),
         decoration: InputDecoration(
           // Magic number 4 makes the textField take up the same space as FlowyText
           contentPadding: EdgeInsets.symmetric(

+ 1 - 1
frontend/appflowy_flutter/test/bloc_test/board_test/util.dart

@@ -73,7 +73,7 @@ class BoardTestContext {
   BoardTestContext(this.gridView, this._boardDataController);
 
   List<RowInfo> get rowInfos {
-    return _boardDataController.rowInfos;
+    return _boardDataController.rowCache.rowInfos;
   }
 
   List<FieldInfo> get fieldContexts => fieldController.fieldInfos;

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

@@ -26,7 +26,7 @@ class GridTestContext {
   GridTestContext(this.gridView, this.gridController);
 
   List<RowInfo> get rowInfos {
-    return gridController.rowInfos;
+    return gridController.rowCache.rowInfos;
   }
 
   List<FieldInfo> get fieldContexts => fieldController.fieldInfos;

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

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