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
This commit is contained in:
Nathan.fooo 2023-03-08 21:19:44 +08:00 committed by GitHub
parent 90da54d12f
commit 7106195d8a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
92 changed files with 2407 additions and 1305 deletions

View File

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

View File

@ -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] = {};

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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}) {
var payload = CreateRowPayloadPB.create()..viewId = viewId;
startRowId?.fold(() => null, (id) => payload.startRowId = id);
return DatabaseEventCreateRow(payload).send();
}
Future<Either<RowPB, FlowyError>> createBoardCard(
String groupId,
Future<Either<RowPB, FlowyError>> createRow({
String? startRowId,
) {
CreateBoardCardPayloadPB payload = CreateBoardCardPayloadPB.create()
..viewId = viewId
..groupId = groupId;
String? groupId,
Map<String, String>? cellDataByFieldId,
}) {
var payload = CreateRowPayloadPB.create()..viewId = viewId;
if (startRowId != null) {
payload.startRowId = startRowId;
}
return DatabaseEventCreateBoardCard(payload).send();
if (groupId != null) {
payload.groupId = groupId;
}
if (cellDataByFieldId != null && cellDataByFieldId.isNotEmpty) {
payload.data = RowDataPB(cellDataByFieldId: cellDataByFieldId);
}
return DatabaseEventCreateRow(payload).send();
}
Future<Either<Unit, FlowyError>> moveRow({
required String fromRowId,
required String? toGroupId,
required String? toRowId,
}) {
var payload = MoveGroupRowPayloadPB.create()
..viewId = viewId
..fromRowId = fromRowId;
if (toGroupId != null) {
payload.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();
}
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();

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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,
));
}

View File

@ -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();
}
}

View File

@ -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))),

View File

@ -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),
),
);
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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,
);

View File

@ -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);
},
);
void _startListening() {
_databaseDataController.addListener(
onDatabaseChanged: (database) {
if (!isClosed) return;
add(CalendarEvent.didReceiveDatabaseUpdate(database));
},
onSettingsChanged: (CalendarSettingsPB settings) {
if (isClosed) return;
add(CalendarEvent.didReceiveCalendarSettings(settings));
},
onArrangeWithNewField: (field) {
if (isClosed) return;
_initializeEvents(field);
// add(CalendarEvent.)
},
onError: (err) {
Log.error(err);
result.fold(
(newRow) => _loadEvent(newRow.id),
(err) => Log.error(err),
);
}
},
);
}
void _initializeEvents(FieldPB dateField) {
calendarEventsController.removeWhere((element) => true);
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),
);
});
}
const events = <CalendarEventData<CalendarData>>[];
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);
}
}
// final List<CalendarEventData<CalendarData>> events = rows.map((row) {
// final event = CalendarEventData(
// title: "",
// date: row -> dateField -> value,
// event: row,
// );
add(CalendarEvent.didLoadAllEvents(calendarEvents));
}
},
(r) => Log.error(r),
);
});
}
// return event;
// }).toList();
CalendarEventData<CalendarCardData>? _calendarEventDataFromEventPB(
CalendarEventPB eventPB) {
final fieldInfo = state.fieldInfoByFieldId[eventPB.titleFieldId];
if (fieldInfo != null) {
final cellId = CellIdentifier(
viewId: viewId,
rowId: eventPB.rowId,
fieldInfo: fieldInfo,
);
calendarEventsController.addAll(events);
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() {
final onDatabaseChanged = DatabaseCallbacks(
onDatabaseChanged: (database) {
if (isClosed) return;
},
onFieldsChanged: (fieldInfos) {
if (isClosed) return;
final fieldInfoByFieldId = {
for (var fieldInfo in fieldInfos) fieldInfo.field.id: fieldInfo
};
add(CalendarEvent.didUpdateFieldInfos(fieldInfoByFieldId));
},
);
final onLayoutChanged = LayoutCallbacks(
onLayoutChanged: _didReceiveLayoutSetting,
onLoadLayout: _didReceiveLayoutSetting,
);
_databaseController.addListener(
onDatabaseChanged: onDatabaseChanged,
onLayoutChanged: onLayoutChanged,
);
}
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});
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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

View File

@ -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();
}
State<CalendarPage> createState() => _CalendarPageState();
}
class CalendarContent extends StatefulWidget {
const CalendarContent({super.key});
@override
State<CalendarContent> createState() => _CalendarContentState();
}
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;
}

View File

@ -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),

View File

@ -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(

View File

@ -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();
}
}

View File

@ -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();

View File

@ -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,
);

View File

@ -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),
),

View File

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

View File

@ -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()));
}
}

View File

@ -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),

View File

@ -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() ?? "",
);
}

View File

@ -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 ?? [],
);
}

View File

@ -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,
);

View File

@ -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 ?? "",
);

View File

@ -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,

View File

@ -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,
),
);

View File

@ -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));

View File

@ -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;
}
// T represents as the Generic card data
class CardCellBuilder<CustomCardData> {
final CellCache cellCache;
class BoardCellBuilder {
final BoardCellBuilderDelegate delegate;
CardCellBuilder(this.cellCache);
BoardCellBuilder(this.delegate);
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,
);

View File

@ -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;
}
}
class EditableCellNotifier {
abstract class CardCell<T> extends StatefulWidget {
final T? cardData;
const CardCell({super.key, this.cardData});
}
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 {

View File

@ -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()),
),
);
},

View File

@ -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),
),

View File

@ -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,

View File

@ -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();
}
}

View File

@ -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;
@override
final EditableCellNotifier? editableNotifier;
final CellRenderHook<List<SelectOptionPB>, T>? renderHook;
const BoardSelectOptionCell({
required this.groupId,
@override
final EditableCardNotifier? editableNotifier;
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,

View File

@ -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,

View File

@ -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,

View File

@ -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();
}
}

View File

@ -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;

View File

@ -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,
);

View File

@ -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,

View File

@ -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);

View File

@ -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();

View File

@ -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;
},

View File

@ -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) => {

View File

@ -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) => {

View File

@ -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
}

View File

@ -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)

View File

@ -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()
}

View File

@ -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,
}

View File

@ -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,
}

View File

@ -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)]

View File

@ -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),
})
}
}

View File

@ -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>,
}

View File

@ -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>>,
) -> 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<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>>,
) -> 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),
}
}

View File

@ -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()]
GetCalendarSetting = 116,
#[event(input = "DatabaseLayoutIdPB", output = "LayoutSettingPB")]
GetLayoutSetting = 116,
#[event(input = "CalendarEventRequestPB", output = "RepeatedCalendarEventPB")]
GetAllCalendarEvents = 117,
#[event(input = "RowIdPB", output = "CalendarEventPB")]
GetCalendarEvent = 118,
#[event(input = "MoveCalendarEventPB")]
MoveCalendarEvent = 119,
}

View File

@ -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);

View File

@ -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 {

View File

@ -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)
}

View File

@ -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();

View File

@ -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![];

View File

@ -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

View File

@ -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());
},
}

View File

@ -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
}

View File

@ -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) {

View File

@ -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()
}

View File

@ -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

View File

@ -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,

View File

@ -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 {

View File

@ -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();
},

View File

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

View File

@ -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);
}
}
},
}
}
}

View File

@ -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;
}

View File

@ -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()
}

View File

@ -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;

View File

@ -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 {

View File

@ -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 {

View File

@ -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;

View File

@ -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
}

View File

@ -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 {