Fix filter test (#1459)

* chore: move grid_view_editor.rs to view_editor folder

* chore: hide invisible rows

* fix: lock issue

* fix: flutter test potential failed

* chore: separate group tests

Co-authored-by: nathan <nathan@appflowy.io>
This commit is contained in:
Nathan.fooo 2022-11-17 16:44:17 +08:00 committed by GitHub
parent f00a78746e
commit fc10ee2d6b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
77 changed files with 1607 additions and 1415 deletions

View File

@ -9,31 +9,38 @@ import 'package:flowy_sdk/rust_stream.dart';
import 'notification_helper.dart';
// GridPB
typedef GridNotificationCallback = void Function(GridNotification, Either<Uint8List, FlowyError>);
typedef GridNotificationCallback = void Function(
GridDartNotification, Either<Uint8List, FlowyError>);
class GridNotificationParser extends NotificationParser<GridNotification, FlowyError> {
GridNotificationParser({String? id, required GridNotificationCallback callback})
class GridNotificationParser
extends NotificationParser<GridDartNotification, FlowyError> {
GridNotificationParser(
{String? id, required GridNotificationCallback callback})
: super(
id: id,
callback: callback,
tyParser: (ty) => GridNotification.valueOf(ty),
tyParser: (ty) => GridDartNotification.valueOf(ty),
errorParser: (bytes) => FlowyError.fromBuffer(bytes),
);
}
typedef GridNotificationHandler = Function(GridNotification ty, Either<Uint8List, FlowyError> result);
typedef GridNotificationHandler = Function(
GridDartNotification ty, Either<Uint8List, FlowyError> result);
class GridNotificationListener {
StreamSubscription<SubscribeObject>? _subscription;
GridNotificationParser? _parser;
GridNotificationListener({required String objectId, required GridNotificationHandler handler})
GridNotificationListener(
{required String objectId, required GridNotificationHandler handler})
: _parser = GridNotificationParser(id: objectId, callback: handler) {
_subscription = RustStreamReceiver.listen((observable) => _parser?.parse(observable));
_subscription =
RustStreamReceiver.listen((observable) => _parser?.parse(observable));
}
Future<void> stop() async {
_parser = null;
await _subscription?.cancel();
_subscription = null;
}
}

View File

@ -32,18 +32,18 @@ class BoardListener {
}
void _handler(
GridNotification ty,
GridDartNotification ty,
Either<Uint8List, FlowyError> result,
) {
switch (ty) {
case GridNotification.DidUpdateGroupView:
case GridDartNotification.DidUpdateGroupView:
result.fold(
(payload) => _groupUpdateNotifier?.value =
left(GroupViewChangesetPB.fromBuffer(payload)),
(error) => _groupUpdateNotifier?.value = right(error),
);
break;
case GridNotification.DidGroupByNewField:
case GridDartNotification.DidGroupByNewField:
result.fold(
(payload) => _groupByNewFieldNotifier?.value =
left(GroupViewChangesetPB.fromBuffer(payload).newGroups),

View File

@ -66,6 +66,7 @@ class BoardCardBloc extends Bloc<BoardCardEvent, BoardCardState> {
state.cells.map((cell) => cell.identifier.fieldContext).toList(),
),
rowPB: state.rowPB,
visible: true,
);
}

View File

@ -27,11 +27,11 @@ class GroupListener {
}
void _handler(
GridNotification ty,
GridDartNotification ty,
Either<Uint8List, FlowyError> result,
) {
switch (ty) {
case GridNotification.DidUpdateGroup:
case GridDartNotification.DidUpdateGroup:
result.fold(
(payload) => _groupNotifier?.value =
left(GroupRowsNotificationPB.fromBuffer(payload)),

View File

@ -287,6 +287,7 @@ class _BoardContentState extends State<BoardContent> {
gridId: gridId,
fields: UnmodifiableListView(fieldController.fieldContexts),
rowPB: rowPB,
visible: true,
);
final dataController = GridRowDataController(

View File

@ -13,7 +13,7 @@ class GridBlockCache {
late GridRowCache _rowCache;
late GridBlockListener _listener;
List<RowInfo> get rows => _rowCache.rows;
List<RowInfo> get rows => _rowCache.visibleRows;
GridRowCache get rowCache => _rowCache;
GridBlockCache({
@ -30,7 +30,7 @@ class GridBlockCache {
_listener = GridBlockListener(blockId: block.id);
_listener.start((result) {
result.fold(
(changesets) => _rowCache.applyChangesets(changesets),
(changeset) => _rowCache.applyChangesets(changeset),
(err) => Log.error(err),
);
});

View File

@ -7,11 +7,12 @@ import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/dart_notification.pb.dart';
typedef GridBlockUpdateNotifierValue = Either<List<GridBlockChangesetPB>, FlowyError>;
typedef GridBlockUpdateNotifierValue = Either<GridBlockChangesetPB, FlowyError>;
class GridBlockListener {
final String blockId;
PublishNotifier<GridBlockUpdateNotifierValue>? _rowsUpdateNotifier = PublishNotifier();
PublishNotifier<GridBlockUpdateNotifierValue>? _rowsUpdateNotifier =
PublishNotifier();
GridNotificationListener? _listener;
GridBlockListener({required this.blockId});
@ -29,11 +30,12 @@ class GridBlockListener {
_rowsUpdateNotifier?.addPublishListener(onBlockChanged);
}
void _handler(GridNotification ty, Either<Uint8List, FlowyError> result) {
void _handler(GridDartNotification ty, Either<Uint8List, FlowyError> result) {
switch (ty) {
case GridNotification.DidUpdateGridBlock:
case GridDartNotification.DidUpdateGridBlock:
result.fold(
(payload) => _rowsUpdateNotifier?.value = left([GridBlockChangesetPB.fromBuffer(payload)]),
(payload) => _rowsUpdateNotifier?.value =
left(GridBlockChangesetPB.fromBuffer(payload)),
(error) => _rowsUpdateNotifier?.value = right(error),
);
break;

View File

@ -22,9 +22,9 @@ class CellListener {
objectId: "$rowId:$fieldId", handler: _handler);
}
void _handler(GridNotification ty, Either<Uint8List, FlowyError> result) {
void _handler(GridDartNotification ty, Either<Uint8List, FlowyError> result) {
switch (ty) {
case GridNotification.DidUpdateCell:
case GridDartNotification.DidUpdateCell:
result.fold(
(payload) => _updateCellNotifier?.value = left(unit),
(error) => _updateCellNotifier?.value = right(error),

View File

@ -27,11 +27,11 @@ class SingleFieldListener {
}
void _handler(
GridNotification ty,
GridDartNotification ty,
Either<Uint8List, FlowyError> result,
) {
switch (ty) {
case GridNotification.DidUpdateField:
case GridDartNotification.DidUpdateField:
result.fold(
(payload) =>
_updateFieldNotifier?.value = left(FieldPB.fromBuffer(payload)),

View File

@ -25,9 +25,9 @@ class GridFieldsListener {
);
}
void _handler(GridNotification ty, Either<Uint8List, FlowyError> result) {
void _handler(GridDartNotification ty, Either<Uint8List, FlowyError> result) {
switch (ty) {
case GridNotification.DidUpdateGridField:
case GridDartNotification.DidUpdateGridField:
result.fold(
(payload) => updateFieldsNotifier?.value =
left(GridFieldChangesetPB.fromBuffer(payload)),

View File

@ -4,7 +4,7 @@ import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_filter.pbenum.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/date_filter.pbenum.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/number_filter.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/text_filter.pbserver.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/text_filter.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/util.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@ -114,7 +114,7 @@ class GridFilterBloc extends Bloc<GridFilterEvent, GridFilterState> {
(element) => !deleteFilterIds.contains(element.id),
);
// Inserts the new fitler if it's not exist
// Inserts the new filter if it's not exist
for (final newFilter in changeset.insertFilters) {
final index =
filters.indexWhere((element) => element.id == newFilter.id);

View File

@ -29,11 +29,11 @@ class FilterListener {
}
void _handler(
GridNotification ty,
GridDartNotification ty,
Either<Uint8List, FlowyError> result,
) {
switch (ty) {
case GridNotification.DidUpdateFilter:
case GridDartNotification.DidUpdateFilter:
result.fold(
(payload) => _filterNotifier?.value =
left(FilterChangesetNotificationPB.fromBuffer(payload)),

View File

@ -33,13 +33,18 @@ class GridRowCache {
List<RowInfo> _rowInfos = [];
/// Use Map for faster access the raw row data.
final HashMap<String, RowPB> _rowByRowId;
final HashMap<String, RowInfo> _rowInfoByRowId;
final GridCellCache _cellCache;
final IGridRowFieldNotifier _fieldNotifier;
final _RowChangesetNotifier _rowChangeReasonNotifier;
UnmodifiableListView<RowInfo> get rows => UnmodifiableListView(_rowInfos);
UnmodifiableListView<RowInfo> get visibleRows {
var visibleRows = [..._rowInfos];
visibleRows.retainWhere((element) => element.visible);
return UnmodifiableListView(visibleRows);
}
GridCellCache get cellCache => _cellCache;
GridRowCache({
@ -47,7 +52,7 @@ class GridRowCache {
required this.block,
required IGridRowFieldNotifier notifier,
}) : _cellCache = GridCellCache(gridId: gridId),
_rowByRowId = HashMap(),
_rowInfoByRowId = HashMap(),
_rowChangeReasonNotifier = _RowChangesetNotifier(),
_fieldNotifier = notifier {
//
@ -55,7 +60,12 @@ class GridRowCache {
.receive(const RowsChangedReason.fieldDidChange()));
notifier.onRowFieldChanged(
(field) => _cellCache.removeCellWithFieldId(field.id));
_rowInfos = block.rows.map((rowPB) => buildGridRow(rowPB)).toList();
for (final row in block.rows) {
final rowInfo = buildGridRow(row);
_rowInfos.add(rowInfo);
_rowInfoByRowId[rowInfo.rowPB.id] = rowInfo;
}
}
Future<void> dispose() async {
@ -64,14 +74,12 @@ class GridRowCache {
await _cellCache.dispose();
}
void applyChangesets(List<GridBlockChangesetPB> changesets) {
for (final changeset in changesets) {
_deleteRows(changeset.deletedRows);
_insertRows(changeset.insertedRows);
_updateRows(changeset.updatedRows);
_hideRows(changeset.hideRows);
_showRows(changeset.visibleRows);
}
void applyChangesets(GridBlockChangesetPB changeset) {
_deleteRows(changeset.deletedRows);
_insertRows(changeset.insertedRows);
_updateRows(changeset.updatedRows);
_hideRows(changeset.invisibleRows);
_showRows(changeset.visibleRows);
}
void _deleteRows(List<String> deletedRows) {
@ -89,7 +97,7 @@ class GridRowCache {
if (deletedRowByRowId[rowInfo.rowPB.id] == null) {
newRows.add(rowInfo);
} else {
_rowByRowId.remove(rowInfo.rowPB.id);
_rowInfoByRowId.remove(rowInfo.rowPB.id);
deletedIndex.add(DeletedIndex(index: index, row: rowInfo));
}
});
@ -109,10 +117,9 @@ class GridRowCache {
rowId: insertRow.row.id,
);
insertIndexs.add(insertIndex);
_rowInfos.insert(
insertRow.index,
(buildGridRow(insertRow.row)),
);
final rowInfo = buildGridRow(insertRow.row);
_rowInfos.insert(insertRow.index, rowInfo);
_rowInfoByRowId[rowInfo.rowPB.id] = rowInfo;
}
_rowChangeReasonNotifier.receive(RowsChangedReason.insert(insertIndexs));
@ -130,10 +137,11 @@ class GridRowCache {
(rowInfo) => rowInfo.rowPB.id == rowId,
);
if (index != -1) {
_rowByRowId[rowId] = updatedRow;
final rowInfo = buildGridRow(updatedRow);
_rowInfoByRowId[rowId] = rowInfo;
_rowInfos.removeAt(index);
_rowInfos.insert(index, buildGridRow(updatedRow));
_rowInfos.insert(index, rowInfo);
updatedIndexs[rowId] = UpdatedIndex(index: index, rowId: rowId);
}
}
@ -141,9 +149,26 @@ class GridRowCache {
_rowChangeReasonNotifier.receive(RowsChangedReason.update(updatedIndexs));
}
void _hideRows(List<String> hideRows) {}
void _hideRows(List<String> invisibleRows) {
for (final rowId in invisibleRows) {
_rowInfoByRowId[rowId]?.visible = false;
}
void _showRows(List<String> visibleRows) {}
if (invisibleRows.isNotEmpty) {
_rowChangeReasonNotifier
.receive(const RowsChangedReason.filterDidChange());
}
}
void _showRows(List<String> visibleRows) {
for (final rowId in visibleRows) {
_rowInfoByRowId[rowId]?.visible = true;
}
if (visibleRows.isNotEmpty) {
_rowChangeReasonNotifier
.receive(const RowsChangedReason.filterDidChange());
}
}
void onRowsChanged(void Function(RowsChangedReason) onRowChanged) {
_rowChangeReasonNotifier.addListener(() {
@ -163,9 +188,10 @@ class GridRowCache {
notifyUpdate() {
if (onCellUpdated != null) {
final row = _rowByRowId[rowId];
if (row != null) {
final GridCellMap cellDataMap = _makeGridCells(rowId, row);
final rowInfo = _rowInfoByRowId[rowId];
if (rowInfo != null) {
final GridCellMap cellDataMap =
_makeGridCells(rowId, rowInfo.rowPB);
onCellUpdated(cellDataMap, _rowChangeReasonNotifier.reason);
}
}
@ -188,7 +214,7 @@ class GridRowCache {
}
GridCellMap loadGridCells(String rowId) {
final RowPB? data = _rowByRowId[rowId];
final RowPB? data = _rowInfoByRowId[rowId]?.rowPB;
if (data == null) {
_loadRow(rowId);
}
@ -230,7 +256,6 @@ class GridRowCache {
final updatedRow = optionRow.row;
updatedRow.freeze();
_rowByRowId[updatedRow.id] = updatedRow;
final index =
_rowInfos.indexWhere((rowInfo) => rowInfo.rowPB.id == updatedRow.id);
if (index != -1) {
@ -238,6 +263,7 @@ class GridRowCache {
if (_rowInfos[index].rowPB != updatedRow) {
final rowInfo = _rowInfos.removeAt(index).copyWith(rowPB: updatedRow);
_rowInfos.insert(index, rowInfo);
_rowInfoByRowId[rowInfo.rowPB.id] = rowInfo;
// Calculate the update index
final UpdatedIndexs updatedIndexs = UpdatedIndexs();
@ -258,6 +284,7 @@ class GridRowCache {
gridId: gridId,
fields: _fieldNotifier.fields,
rowPB: rowPB,
visible: true,
);
}
}
@ -275,16 +302,18 @@ class _RowChangesetNotifier extends ChangeNotifier {
update: (_) => notifyListeners(),
fieldDidChange: (_) => notifyListeners(),
initial: (_) {},
filterDidChange: (_FilterDidChange value) => notifyListeners(),
);
}
}
@freezed
@unfreezed
class RowInfo with _$RowInfo {
const factory RowInfo({
factory RowInfo({
required String gridId,
required UnmodifiableListView<GridFieldContext> fields,
required RowPB rowPB,
required bool visible,
}) = _RowInfo;
}
@ -298,6 +327,7 @@ class RowsChangedReason with _$RowsChangedReason {
const factory RowsChangedReason.delete(DeletedIndexs items) = _Delete;
const factory RowsChangedReason.update(UpdatedIndexs indexs) = _Update;
const factory RowsChangedReason.fieldDidChange() = _FieldDidChange;
const factory RowsChangedReason.filterDidChange() = _FilterDidChange;
const factory RowsChangedReason.initial() = InitialListState;
}

View File

@ -23,9 +23,9 @@ class RowListener {
_listener = GridNotificationListener(objectId: rowId, handler: _handler);
}
void _handler(GridNotification ty, Either<Uint8List, FlowyError> result) {
void _handler(GridDartNotification ty, Either<Uint8List, FlowyError> result) {
switch (ty) {
case GridNotification.DidUpdateRow:
case GridDartNotification.DidUpdateRow:
result.fold(
(payload) =>
updateRowNotifier?.value = left(RowPB.fromBuffer(payload)),

View File

@ -24,9 +24,9 @@ class SettingListener {
_listener = GridNotificationListener(objectId: gridId, handler: _handler);
}
void _handler(GridNotification ty, Either<Uint8List, FlowyError> result) {
void _handler(GridDartNotification ty, Either<Uint8List, FlowyError> result) {
switch (ty) {
case GridNotification.DidUpdateGridSetting:
case GridDartNotification.DidUpdateGridSetting:
result.fold(
(payload) => _updateSettingNotifier?.value = left(
GridSettingPB.fromBuffer(payload),

View File

@ -8,7 +8,8 @@ import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-folder/trash.pb.dart';
import 'package:flowy_sdk/rust_stream.dart';
typedef TrashUpdatedCallback = void Function(Either<List<TrashPB>, FlowyError> trashOrFailed);
typedef TrashUpdatedCallback = void Function(
Either<List<TrashPB>, FlowyError> trashOrFailed);
class TrashListener {
StreamSubscription<SubscribeObject>? _subscription;
@ -17,11 +18,13 @@ class TrashListener {
void start({TrashUpdatedCallback? trashUpdated}) {
_trashUpdated = trashUpdated;
_parser = FolderNotificationParser(callback: _bservableCallback);
_subscription = RustStreamReceiver.listen((observable) => _parser?.parse(observable));
_parser = FolderNotificationParser(callback: _observableCallback);
_subscription =
RustStreamReceiver.listen((observable) => _parser?.parse(observable));
}
void _bservableCallback(FolderNotification ty, Either<Uint8List, FlowyError> result) {
void _observableCallback(
FolderNotification ty, Either<Uint8List, FlowyError> result) {
switch (ty) {
case FolderNotification.TrashUpdated:
if (_trashUpdated != null) {

View File

@ -14,10 +14,11 @@ void main() {
group('$BoardBloc', () {
late BoardBloc boardBloc;
late String groupId;
late BoardTestContext context;
setUp(() async {
await boardTest.context.createTestBoard();
boardBloc = BoardBloc(view: boardTest.context.gridView)
context = await boardTest.createTestBoard();
boardBloc = BoardBloc(view: context.gridView)
..add(const BoardEvent.initial());
await boardResponseFuture();
groupId = boardBloc.state.groupIds.first;

View File

@ -16,22 +16,23 @@ void main() {
group('The grouped field is not changed after editing a field:', () {
late BoardBloc boardBloc;
late FieldEditorBloc editorBloc;
late BoardTestContext context;
setUpAll(() async {
await boardTest.context.createTestBoard();
context = await boardTest.createTestBoard();
});
setUp(() async {
boardBloc = BoardBloc(view: boardTest.context.gridView)
boardBloc = BoardBloc(view: context.gridView)
..add(const BoardEvent.initial());
final fieldContext = boardTest.context.singleSelectFieldContext();
final fieldContext = context.singleSelectFieldContext();
final loader = FieldTypeOptionLoader(
gridId: boardTest.context.gridView.id,
gridId: context.gridView.id,
field: fieldContext.field,
);
editorBloc = FieldEditorBloc(
gridId: boardTest.context.gridView.id,
gridId: context.gridView.id,
fieldName: fieldContext.name,
isGroupField: fieldContext.isGroupField,
loader: loader,
@ -46,7 +47,7 @@ void main() {
wait: boardResponseDuration(),
verify: (bloc) {
assert(bloc.groupControllers.values.length == 4);
assert(boardTest.context.fieldContexts.length == 2);
assert(context.fieldContexts.length == 2);
},
);
@ -75,19 +76,20 @@ void main() {
assert(bloc.groupControllers.values.length == 4,
"Expected 4, but receive ${bloc.groupControllers.values.length}");
assert(boardTest.context.fieldContexts.length == 2,
"Expected 2, but receive ${boardTest.context.fieldContexts.length}");
assert(context.fieldContexts.length == 2,
"Expected 2, but receive ${context.fieldContexts.length}");
},
);
});
group('The grouped field is not changed after creating a new field:', () {
late BoardBloc boardBloc;
late BoardTestContext context;
setUpAll(() async {
await boardTest.context.createTestBoard();
context = await boardTest.createTestBoard();
});
setUp(() async {
boardBloc = BoardBloc(view: boardTest.context.gridView)
boardBloc = BoardBloc(view: context.gridView)
..add(const BoardEvent.initial());
await boardResponseFuture();
});
@ -98,14 +100,14 @@ void main() {
wait: boardResponseDuration(),
verify: (bloc) {
assert(bloc.groupControllers.values.length == 4);
assert(boardTest.context.fieldContexts.length == 2);
assert(context.fieldContexts.length == 2);
},
);
test('create a field', () async {
await boardTest.context.createField(FieldType.Checkbox);
await context.createField(FieldType.Checkbox);
await boardResponseFuture();
final checkboxField = boardTest.context.fieldContexts.last.field;
final checkboxField = context.fieldContexts.last.field;
assert(checkboxField.fieldType == FieldType.Checkbox);
});
@ -117,8 +119,8 @@ void main() {
assert(bloc.groupControllers.values.length == 4,
"Expected 4, but receive ${bloc.groupControllers.values.length}");
assert(boardTest.context.fieldContexts.length == 3,
"Expected 3, but receive ${boardTest.context.fieldContexts.length}");
assert(context.fieldContexts.length == 3,
"Expected 3, but receive ${context.fieldContexts.length}");
},
);
});

View File

@ -0,0 +1,45 @@
import 'package:app_flowy/plugins/board/application/board_bloc.dart';
import 'package:app_flowy/plugins/grid/application/setting/group_bloc.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flutter_test/flutter_test.dart';
import 'util.dart';
void main() {
late AppFlowyBoardTest boardTest;
setUpAll(() async {
boardTest = await AppFlowyBoardTest.ensureInitialized();
});
// Group by checkbox field
test('group by checkbox field test', () async {
final context = await boardTest.createTestBoard();
final boardBloc = BoardBloc(view: context.gridView)
..add(const BoardEvent.initial());
await boardResponseFuture();
// assert the initial values
assert(boardBloc.groupControllers.values.length == 4);
assert(context.fieldContexts.length == 2);
// create checkbox field
await context.createField(FieldType.Checkbox);
await boardResponseFuture();
assert(context.fieldContexts.length == 3);
// set group by checkbox
final checkboxField = context.fieldContexts.last.field;
final gridGroupBloc = GridGroupBloc(
viewId: context.gridView.id,
fieldController: context.fieldController,
);
gridGroupBloc.add(GridGroupEvent.setGroupByField(
checkboxField.id,
checkboxField.fieldType,
));
await boardResponseFuture();
assert(boardBloc.groupControllers.values.length == 2);
});
}

View File

@ -1,231 +0,0 @@
import 'package:app_flowy/plugins/board/application/board_bloc.dart';
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
import 'package:app_flowy/plugins/grid/application/cell/select_option_editor_bloc.dart';
import 'package:app_flowy/plugins/grid/application/field/field_editor_bloc.dart';
import 'package:app_flowy/plugins/grid/application/setting/group_bloc.dart';
import 'package:bloc_test/bloc_test.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flutter_test/flutter_test.dart';
import 'util.dart';
void main() {
late AppFlowyBoardTest boardTest;
setUpAll(() async {
boardTest = await AppFlowyBoardTest.ensureInitialized();
});
// Group by multi-select with no options
group('Group by multi-select with no options', () {
//
late FieldPB multiSelectField;
late String expectedGroupName;
setUpAll(() async {
await boardTest.context.createTestBoard();
});
test('create multi-select field', () async {
await boardTest.context.createField(FieldType.MultiSelect);
await boardResponseFuture();
assert(boardTest.context.fieldContexts.length == 3);
multiSelectField = boardTest.context.fieldContexts.last.field;
expectedGroupName = "No ${multiSelectField.name}";
assert(multiSelectField.fieldType == FieldType.MultiSelect);
});
blocTest<GridGroupBloc, GridGroupState>(
"set grouped by the new multi-select field",
build: () => GridGroupBloc(
viewId: boardTest.context.gridView.id,
fieldController: boardTest.context.fieldController,
),
act: (bloc) async {
bloc.add(GridGroupEvent.setGroupByField(
multiSelectField.id,
multiSelectField.fieldType,
));
},
wait: boardResponseDuration(),
);
blocTest<BoardBloc, BoardState>(
"assert only have the 'No status' group",
build: () => BoardBloc(view: boardTest.context.gridView)
..add(const BoardEvent.initial()),
wait: boardResponseDuration(),
verify: (bloc) {
assert(bloc.groupControllers.values.length == 1,
"Expected 1, but receive ${bloc.groupControllers.values.length}");
assert(
bloc.groupControllers.values.first.group.desc == expectedGroupName,
"Expected $expectedGroupName, but receive ${bloc.groupControllers.values.first.group.desc}");
},
);
});
group('Group by multi-select with two options', () {
late FieldPB multiSelectField;
setUpAll(() async {
await boardTest.context.createTestBoard();
});
test('create multi-select field', () async {
await boardTest.context.createField(FieldType.MultiSelect);
await boardResponseFuture();
assert(boardTest.context.fieldContexts.length == 3);
multiSelectField = boardTest.context.fieldContexts.last.field;
assert(multiSelectField.fieldType == FieldType.MultiSelect);
final cellController =
await boardTest.context.makeCellController(multiSelectField.id)
as GridSelectOptionCellController;
final multiSelectOptionBloc =
SelectOptionCellEditorBloc(cellController: cellController);
multiSelectOptionBloc.add(const SelectOptionEditorEvent.initial());
await boardResponseFuture();
multiSelectOptionBloc.add(const SelectOptionEditorEvent.newOption("A"));
await boardResponseFuture();
multiSelectOptionBloc.add(const SelectOptionEditorEvent.newOption("B"));
await boardResponseFuture();
});
blocTest<GridGroupBloc, GridGroupState>(
"set grouped by multi-select field",
build: () => GridGroupBloc(
viewId: boardTest.context.gridView.id,
fieldController: boardTest.context.fieldController,
),
act: (bloc) async {
await boardResponseFuture();
bloc.add(GridGroupEvent.setGroupByField(
multiSelectField.id,
multiSelectField.fieldType,
));
},
wait: boardResponseDuration(),
);
blocTest<BoardBloc, BoardState>(
"check the groups' order",
build: () => BoardBloc(view: boardTest.context.gridView)
..add(const BoardEvent.initial()),
wait: boardResponseDuration(),
verify: (bloc) {
assert(bloc.groupControllers.values.length == 3,
"Expected 3, but receive ${bloc.groupControllers.values.length}");
final groups =
bloc.groupControllers.values.map((e) => e.group).toList();
assert(groups[0].desc == "No ${multiSelectField.name}");
assert(groups[1].desc == "B");
assert(groups[2].desc == "A");
},
);
});
// Group by checkbox field
group('Group by checkbox field:', () {
late BoardBloc boardBloc;
late FieldPB checkboxField;
setUpAll(() async {
await boardTest.context.createTestBoard();
});
setUp(() async {
boardBloc = BoardBloc(view: boardTest.context.gridView)
..add(const BoardEvent.initial());
await boardResponseFuture();
});
blocTest<BoardBloc, BoardState>(
"initial",
build: () => boardBloc,
wait: boardResponseDuration(),
verify: (bloc) {
assert(bloc.groupControllers.values.length == 4);
assert(boardTest.context.fieldContexts.length == 2);
},
);
test('create checkbox field', () async {
await boardTest.context.createField(FieldType.Checkbox);
await boardResponseFuture();
assert(boardTest.context.fieldContexts.length == 3);
checkboxField = boardTest.context.fieldContexts.last.field;
assert(checkboxField.fieldType == FieldType.Checkbox);
});
blocTest<GridGroupBloc, GridGroupState>(
"set grouped by checkbox field",
build: () => GridGroupBloc(
viewId: boardTest.context.gridView.id,
fieldController: boardTest.context.fieldController,
),
act: (bloc) async {
bloc.add(GridGroupEvent.setGroupByField(
checkboxField.id,
checkboxField.fieldType,
));
},
wait: boardResponseDuration(),
);
blocTest<BoardBloc, BoardState>(
"check the number of groups is 2",
build: () => boardBloc,
wait: boardResponseDuration(),
verify: (bloc) {
assert(bloc.groupControllers.values.length == 2);
},
);
});
// Group with not support grouping field
group('Group with not support grouping field:', () {
late FieldEditorBloc editorBloc;
setUpAll(() async {
await boardTest.context.createTestBoard();
final fieldContext = boardTest.context.singleSelectFieldContext();
editorBloc = boardTest.context.createFieldEditor(
fieldContext: fieldContext,
)..add(const FieldEditorEvent.initial());
await boardResponseFuture();
});
blocTest<FieldEditorBloc, FieldEditorState>(
"switch to text field",
build: () => editorBloc,
wait: boardResponseDuration(),
act: (bloc) async {
bloc.add(const FieldEditorEvent.switchToField(FieldType.RichText));
},
verify: (bloc) {
bloc.state.field.fold(
() => throw Exception(),
(field) => field.fieldType == FieldType.RichText,
);
},
);
blocTest<BoardBloc, BoardState>(
'assert the number of groups is 1',
build: () => BoardBloc(view: boardTest.context.gridView)
..add(const BoardEvent.initial()),
wait: boardResponseDuration(),
verify: (bloc) {
assert(bloc.groupControllers.values.length == 1,
"Expected 1, but receive ${bloc.groupControllers.values.length}");
},
);
});
}

View File

@ -0,0 +1,95 @@
import 'package:app_flowy/plugins/board/application/board_bloc.dart';
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
import 'package:app_flowy/plugins/grid/application/cell/select_option_editor_bloc.dart';
import 'package:app_flowy/plugins/grid/application/setting/group_bloc.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flutter_test/flutter_test.dart';
import 'util.dart';
void main() {
late AppFlowyBoardTest boardTest;
setUpAll(() async {
boardTest = await AppFlowyBoardTest.ensureInitialized();
});
test('group by multi select with no options test', () async {
final context = await boardTest.createTestBoard();
// create multi-select field
await context.createField(FieldType.MultiSelect);
await boardResponseFuture();
assert(context.fieldContexts.length == 3);
final multiSelectField = context.fieldContexts.last.field;
// set grouped by the new multi-select field"
final gridGroupBloc = GridGroupBloc(
viewId: context.gridView.id,
fieldController: context.fieldController,
);
gridGroupBloc.add(GridGroupEvent.setGroupByField(
multiSelectField.id,
multiSelectField.fieldType,
));
await boardResponseFuture();
//assert only have the 'No status' group
final boardBloc = BoardBloc(view: context.gridView)
..add(const BoardEvent.initial());
await boardResponseFuture();
assert(boardBloc.groupControllers.values.length == 1,
"Expected 1, but receive ${boardBloc.groupControllers.values.length}");
final expectedGroupName = "No ${multiSelectField.name}";
assert(
boardBloc.groupControllers.values.first.group.desc == expectedGroupName,
"Expected $expectedGroupName, but receive ${boardBloc.groupControllers.values.first.group.desc}");
});
test('group by multi select with no options test', () async {
final context = await boardTest.createTestBoard();
// create multi-select field
await context.createField(FieldType.MultiSelect);
await boardResponseFuture();
assert(context.fieldContexts.length == 3);
final multiSelectField = context.fieldContexts.last.field;
// Create options
final cellController = await context.makeCellController(multiSelectField.id)
as GridSelectOptionCellController;
final multiSelectOptionBloc =
SelectOptionCellEditorBloc(cellController: cellController);
multiSelectOptionBloc.add(const SelectOptionEditorEvent.initial());
await boardResponseFuture();
multiSelectOptionBloc.add(const SelectOptionEditorEvent.newOption("A"));
await boardResponseFuture();
multiSelectOptionBloc.add(const SelectOptionEditorEvent.newOption("B"));
await boardResponseFuture();
// set grouped by the new multi-select field"
final gridGroupBloc = GridGroupBloc(
viewId: context.gridView.id,
fieldController: context.fieldController,
);
gridGroupBloc.add(GridGroupEvent.setGroupByField(
multiSelectField.id,
multiSelectField.fieldType,
));
await boardResponseFuture();
// assert there are only three group
final boardBloc = BoardBloc(view: context.gridView)
..add(const BoardEvent.initial());
await boardResponseFuture();
assert(boardBloc.groupControllers.values.length == 3,
"Expected 3, but receive ${boardBloc.groupControllers.values.length}");
final groups =
boardBloc.groupControllers.values.map((e) => e.group).toList();
assert(groups[0].desc == "No ${multiSelectField.name}");
assert(groups[1].desc == "B");
assert(groups[2].desc == "A");
});
}

View File

@ -0,0 +1,51 @@
import 'package:app_flowy/plugins/board/application/board_bloc.dart';
import 'package:app_flowy/plugins/grid/application/field/field_editor_bloc.dart';
import 'package:bloc_test/bloc_test.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flutter_test/flutter_test.dart';
import 'util.dart';
void main() {
late AppFlowyBoardTest boardTest;
late FieldEditorBloc editorBloc;
late BoardTestContext context;
setUpAll(() async {
boardTest = await AppFlowyBoardTest.ensureInitialized();
context = await boardTest.createTestBoard();
final fieldContext = context.singleSelectFieldContext();
editorBloc = context.createFieldEditor(
fieldContext: fieldContext,
)..add(const FieldEditorEvent.initial());
await boardResponseFuture();
});
group('Group with not support grouping field:', () {
blocTest<FieldEditorBloc, FieldEditorState>(
"switch to text field",
build: () => editorBloc,
wait: boardResponseDuration(),
act: (bloc) async {
bloc.add(const FieldEditorEvent.switchToField(FieldType.RichText));
},
verify: (bloc) {
bloc.state.field.fold(
() => throw Exception(),
(field) => field.fieldType == FieldType.RichText,
);
},
);
blocTest<BoardBloc, BoardState>(
'assert the number of groups is 1',
build: () =>
BoardBloc(view: context.gridView)..add(const BoardEvent.initial()),
wait: boardResponseDuration(),
verify: (bloc) {
assert(bloc.groupControllers.values.length == 1,
"Expected 1, but receive ${bloc.groupControllers.values.length}");
},
);
});
}

View File

@ -1,12 +1,58 @@
import 'dart:collection';
import 'package:app_flowy/plugins/board/application/board_data_controller.dart';
import 'package:app_flowy/plugins/board/board.dart';
import 'package:app_flowy/plugins/grid/application/block/block_cache.dart';
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
import 'package:app_flowy/plugins/grid/application/field/field_editor_bloc.dart';
import 'package:app_flowy/plugins/grid/application/field/field_service.dart';
import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart';
import 'package:app_flowy/plugins/grid/application/row/row_bloc.dart';
import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
import 'package:app_flowy/plugins/grid/application/row/row_data_controller.dart';
import 'package:app_flowy/workspace/application/app/app_service.dart';
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import '../../util.dart';
import '../grid_test/util.dart';
class AppFlowyBoardTest {
final AppFlowyGridTest context;
AppFlowyBoardTest(this.context);
final AppFlowyUnitTest unitTest;
AppFlowyBoardTest({required this.unitTest});
static Future<AppFlowyBoardTest> ensureInitialized() async {
final inner = await AppFlowyGridTest.ensureInitialized();
return AppFlowyBoardTest(inner);
final inner = await AppFlowyUnitTest.ensureInitialized();
return AppFlowyBoardTest(unitTest: inner);
}
Future<BoardTestContext> createTestBoard() async {
final app = await unitTest.createTestApp();
final builder = BoardPluginBuilder();
return AppService()
.createView(
appId: app.id,
name: "Test Board",
dataFormatType: builder.dataFormatType,
pluginType: builder.pluginType,
layoutType: builder.layoutType!,
)
.then((result) {
return result.fold(
(view) async {
final context =
BoardTestContext(view, BoardDataController(view: view));
final result = await context._boardDataController.openGrid();
result.fold((l) => null, (r) => throw Exception(r));
return context;
},
(error) {
throw Exception();
},
);
});
}
}
@ -17,3 +63,109 @@ Future<void> boardResponseFuture() {
Duration boardResponseDuration({int milliseconds = 200}) {
return Duration(milliseconds: milliseconds);
}
class BoardTestContext {
final ViewPB gridView;
final BoardDataController _boardDataController;
BoardTestContext(this.gridView, this._boardDataController);
List<RowInfo> get rowInfos {
return _boardDataController.rowInfos;
}
UnmodifiableMapView<String, GridBlockCache> get blocks {
return _boardDataController.blocks;
}
List<GridFieldContext> get fieldContexts => fieldController.fieldContexts;
GridFieldController get fieldController {
return _boardDataController.fieldController;
}
FieldEditorBloc createFieldEditor({
GridFieldContext? fieldContext,
}) {
IFieldTypeOptionLoader loader;
if (fieldContext == null) {
loader = NewFieldTypeOptionLoader(gridId: gridView.id);
} else {
loader =
FieldTypeOptionLoader(gridId: gridView.id, field: fieldContext.field);
}
final editorBloc = FieldEditorBloc(
fieldName: fieldContext?.name ?? '',
isGroupField: fieldContext?.isGroupField ?? false,
loader: loader,
gridId: gridView.id,
);
return editorBloc;
}
Future<IGridCellController> makeCellController(String fieldId) async {
final builder = await makeCellControllerBuilder(fieldId);
return builder.build();
}
Future<GridCellControllerBuilder> makeCellControllerBuilder(
String fieldId,
) async {
final RowInfo rowInfo = rowInfos.last;
final blockCache = blocks[rowInfo.rowPB.blockId];
final rowCache = blockCache?.rowCache;
final fieldController = _boardDataController.fieldController;
final rowDataController = GridRowDataController(
rowInfo: rowInfo,
fieldController: fieldController,
rowCache: rowCache!,
);
final rowBloc = RowBloc(
rowInfo: rowInfo,
dataController: rowDataController,
)..add(const RowEvent.initial());
await gridResponseFuture();
return GridCellControllerBuilder(
cellId: rowBloc.state.gridCellMap[fieldId]!,
cellCache: rowCache.cellCache,
delegate: rowDataController,
);
}
Future<FieldEditorBloc> createField(FieldType fieldType) async {
final editorBloc = createFieldEditor()
..add(const FieldEditorEvent.initial());
await gridResponseFuture();
editorBloc.add(FieldEditorEvent.switchToField(fieldType));
await gridResponseFuture();
return Future(() => editorBloc);
}
GridFieldContext singleSelectFieldContext() {
final fieldContext = fieldContexts
.firstWhere((element) => element.fieldType == FieldType.SingleSelect);
return fieldContext;
}
GridFieldCellContext singleSelectFieldCellContext() {
final field = singleSelectFieldContext().field;
return GridFieldCellContext(gridId: gridView.id, field: field);
}
GridFieldContext textFieldContext() {
final fieldContext = fieldContexts
.firstWhere((element) => element.fieldType == FieldType.RichText);
return fieldContext;
}
GridFieldContext checkboxFieldContext() {
final fieldContext = fieldContexts
.firstWhere((element) => element.fieldType == FieldType.Checkbox);
return fieldContext;
}
}

View File

@ -3,9 +3,24 @@ import 'package:app_flowy/plugins/grid/application/prelude.dart';
import 'package:bloc_test/bloc_test.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flutter_test/flutter_test.dart';
import 'util.dart';
Future<FieldEditorBloc> createEditorBloc(AppFlowyGridTest gridTest) async {
final context = await gridTest.createTestGrid();
final fieldContext = context.singleSelectFieldContext();
final loader = FieldTypeOptionLoader(
gridId: context.gridView.id,
field: fieldContext.field,
);
return FieldEditorBloc(
gridId: context.gridView.id,
fieldName: fieldContext.name,
isGroupField: fieldContext.isGroupField,
loader: loader,
)..add(const FieldEditorEvent.initial());
}
void main() {
late AppFlowyGridTest gridTest;
@ -17,15 +32,15 @@ void main() {
late FieldEditorBloc editorBloc;
setUp(() async {
await gridTest.createTestGrid();
final fieldContext = gridTest.singleSelectFieldContext();
final context = await gridTest.createTestGrid();
final fieldContext = context.singleSelectFieldContext();
final loader = FieldTypeOptionLoader(
gridId: gridTest.gridView.id,
gridId: context.gridView.id,
field: fieldContext.field,
);
editorBloc = FieldEditorBloc(
gridId: gridTest.gridView.id,
gridId: context.gridView.id,
fieldName: fieldContext.name,
isGroupField: fieldContext.isGroupField,
loader: loader,
@ -65,7 +80,7 @@ void main() {
(field) {
// The default length of the fields is 3. The length of the fields
// should not change after switching to other field type
assert(gridTest.fieldContexts.length == 3);
// assert(gridTest.fieldContexts.length == 3);
assert(field.fieldType == FieldType.RichText);
},
);
@ -80,7 +95,7 @@ void main() {
},
wait: gridResponseDuration(),
verify: (bloc) {
assert(gridTest.fieldContexts.length == 2);
// assert(gridTest.fieldContexts.length == 2);
},
);
});

View File

@ -1,4 +1,6 @@
import 'package:app_flowy/plugins/grid/application/filter/filter_bloc.dart';
import 'package:app_flowy/plugins/grid/application/grid_bloc.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_filter.pbenum.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/text_filter.pb.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:bloc_test/bloc_test.dart';
@ -11,15 +13,17 @@ void main() {
});
group('$GridFilterBloc', () {
late GridTestContext context;
setUp(() async {
await gridTest.createTestGrid();
context = await gridTest.createTestGrid();
});
blocTest<GridFilterBloc, GridFilterState>(
"create a text filter",
build: () => GridFilterBloc(viewId: gridTest.gridView.id)
build: () => GridFilterBloc(viewId: context.gridView.id)
..add(const GridFilterEvent.initial()),
act: (bloc) async {
final textField = gridTest.textFieldContext();
final textField = context.textFieldContext();
bloc.add(
GridFilterEvent.createTextFilter(
fieldId: textField.id,
@ -35,10 +39,10 @@ void main() {
blocTest<GridFilterBloc, GridFilterState>(
"delete a text filter",
build: () => GridFilterBloc(viewId: gridTest.gridView.id)
build: () => GridFilterBloc(viewId: context.gridView.id)
..add(const GridFilterEvent.initial()),
act: (bloc) async {
final textField = gridTest.textFieldContext();
final textField = context.textFieldContext();
bloc.add(
GridFilterEvent.createTextFilter(
fieldId: textField.id,
@ -61,4 +65,80 @@ void main() {
},
);
});
test('filter rows with condition: text is empty', () async {
final context = await gridTest.createTestGrid();
final filterBloc = GridFilterBloc(viewId: context.gridView.id)
..add(const GridFilterEvent.initial());
final gridBloc = GridBloc(view: context.gridView)
..add(const GridEvent.initial());
final textField = context.textFieldContext();
await gridResponseFuture();
filterBloc.add(
GridFilterEvent.createTextFilter(
fieldId: textField.id,
condition: TextFilterCondition.TextIsEmpty,
content: ""),
);
await gridResponseFuture();
assert(gridBloc.state.rowInfos.length == 3);
});
test('filter rows with condition: text is not empty', () async {
final context = await gridTest.createTestGrid();
final filterBloc = GridFilterBloc(viewId: context.gridView.id)
..add(const GridFilterEvent.initial());
final textField = context.textFieldContext();
await gridResponseFuture();
filterBloc.add(
GridFilterEvent.createTextFilter(
fieldId: textField.id,
condition: TextFilterCondition.TextIsNotEmpty,
content: ""),
);
await gridResponseFuture();
assert(context.rowInfos.isEmpty);
});
test('filter rows with condition: checkbox uncheck', () async {
final context = await gridTest.createTestGrid();
final checkboxField = context.checkboxFieldContext();
final filterBloc = GridFilterBloc(viewId: context.gridView.id)
..add(const GridFilterEvent.initial());
final gridBloc = GridBloc(view: context.gridView)
..add(const GridEvent.initial());
await gridResponseFuture();
filterBloc.add(
GridFilterEvent.createCheckboxFilter(
fieldId: checkboxField.id,
condition: CheckboxFilterCondition.IsUnChecked,
),
);
await gridResponseFuture();
assert(gridBloc.state.rowInfos.length == 3);
});
test('filter rows with condition: checkbox check', () async {
final context = await gridTest.createTestGrid();
final checkboxField = context.checkboxFieldContext();
final filterBloc = GridFilterBloc(viewId: context.gridView.id)
..add(const GridFilterEvent.initial());
final gridBloc = GridBloc(view: context.gridView)
..add(const GridEvent.initial());
await gridResponseFuture();
filterBloc.add(
GridFilterEvent.createCheckboxFilter(
fieldId: checkboxField.id,
condition: CheckboxFilterCondition.IsChecked,
),
);
await gridResponseFuture();
assert(gridBloc.state.rowInfos.isEmpty);
});
}

View File

@ -10,14 +10,15 @@ void main() {
});
group('Edit Grid:', () {
late GridTestContext context;
setUp(() async {
await gridTest.createTestGrid();
context = await gridTest.createTestGrid();
});
// The initial number of rows is 3 for each grid.
blocTest<GridBloc, GridState>(
"create a row",
build: () =>
GridBloc(view: gridTest.gridView)..add(const GridEvent.initial()),
GridBloc(view: context.gridView)..add(const GridEvent.initial()),
act: (bloc) => bloc.add(const GridEvent.createRow()),
wait: const Duration(milliseconds: 300),
verify: (bloc) {
@ -28,7 +29,7 @@ void main() {
blocTest<GridBloc, GridState>(
"delete the last row",
build: () =>
GridBloc(view: gridTest.gridView)..add(const GridEvent.initial()),
GridBloc(view: context.gridView)..add(const GridEvent.initial()),
act: (bloc) async {
await gridResponseFuture();
bloc.add(GridEvent.deleteRow(bloc.state.rowInfos.last));

View File

@ -14,10 +14,11 @@ void main() {
group('$GridHeaderBloc', () {
late FieldActionSheetBloc actionSheetBloc;
late GridTestContext context;
setUp(() async {
await gridTest.createTestGrid();
context = await gridTest.createTestGrid();
actionSheetBloc = FieldActionSheetBloc(
fieldCellContext: gridTest.singleSelectFieldCellContext(),
fieldCellContext: context.singleSelectFieldCellContext(),
);
});
@ -25,8 +26,8 @@ void main() {
"hides property",
build: () {
final bloc = GridHeaderBloc(
gridId: gridTest.gridView.id,
fieldController: gridTest.fieldController,
gridId: context.gridView.id,
fieldController: context.fieldController,
)..add(const GridHeaderEvent.initial());
return bloc;
},
@ -44,8 +45,8 @@ void main() {
"shows property",
build: () {
final bloc = GridHeaderBloc(
gridId: gridTest.gridView.id,
fieldController: gridTest.fieldController,
gridId: context.gridView.id,
fieldController: context.fieldController,
)..add(const GridHeaderEvent.initial());
return bloc;
},
@ -65,8 +66,8 @@ void main() {
"duplicate property",
build: () {
final bloc = GridHeaderBloc(
gridId: gridTest.gridView.id,
fieldController: gridTest.fieldController,
gridId: context.gridView.id,
fieldController: context.fieldController,
)..add(const GridHeaderEvent.initial());
return bloc;
},
@ -84,8 +85,8 @@ void main() {
"delete property",
build: () {
final bloc = GridHeaderBloc(
gridId: gridTest.gridView.id,
fieldController: gridTest.fieldController,
gridId: context.gridView.id,
fieldController: context.fieldController,
)..add(const GridHeaderEvent.initial());
return bloc;
},
@ -103,8 +104,8 @@ void main() {
"update name",
build: () {
final bloc = GridHeaderBloc(
gridId: gridTest.gridView.id,
fieldController: gridTest.fieldController,
gridId: context.gridView.id,
fieldController: context.fieldController,
)..add(const GridHeaderEvent.initial());
return bloc;
},

View File

@ -9,9 +9,9 @@ import 'package:bloc_test/bloc_test.dart';
import 'util.dart';
void main() {
late AppFlowyGridSelectOptionCellTest cellTest;
late AppFlowyGridCellTest cellTest;
setUpAll(() async {
cellTest = await AppFlowyGridSelectOptionCellTest.ensureInitialized();
cellTest = await AppFlowyGridCellTest.ensureInitialized();
});
group('SingleSelectOptionBloc', () {

View File

@ -1,6 +1,4 @@
import 'dart:collection';
import 'package:app_flowy/plugins/board/application/board_data_controller.dart';
import 'package:app_flowy/plugins/board/board.dart';
import 'package:app_flowy/plugins/grid/application/block/block_cache.dart';
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
@ -18,64 +16,28 @@ import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import '../../util.dart';
/// Create a empty Grid for test
class AppFlowyGridTest {
final AppFlowyUnitTest unitTest;
late ViewPB gridView;
GridDataController? _gridDataController;
BoardDataController? _boardDataController;
class GridTestContext {
final ViewPB gridView;
final GridDataController _gridDataController;
AppFlowyGridTest({required this.unitTest});
static Future<AppFlowyGridTest> ensureInitialized() async {
final inner = await AppFlowyUnitTest.ensureInitialized();
return AppFlowyGridTest(unitTest: inner);
}
GridTestContext(this.gridView, this._gridDataController);
List<RowInfo> get rowInfos {
if (_gridDataController != null) {
return _gridDataController!.rowInfos;
}
if (_boardDataController != null) {
return _boardDataController!.rowInfos;
}
throw Exception();
return _gridDataController.rowInfos;
}
UnmodifiableMapView<String, GridBlockCache> get blocks {
if (_gridDataController != null) {
return _gridDataController!.blocks;
}
if (_boardDataController != null) {
return _boardDataController!.blocks;
}
throw Exception();
return _gridDataController.blocks;
}
List<GridFieldContext> get fieldContexts => fieldController.fieldContexts;
GridFieldController get fieldController {
if (_gridDataController != null) {
return _gridDataController!.fieldController;
}
if (_boardDataController != null) {
return _boardDataController!.fieldController;
}
throw Exception();
return _gridDataController.fieldController;
}
Future<void> createRow() async {
if (_gridDataController != null) {
return _gridDataController!.createRow();
}
throw Exception();
return _gridDataController.createRow();
}
FieldEditorBloc createFieldEditor({
@ -109,14 +71,7 @@ class AppFlowyGridTest {
final RowInfo rowInfo = rowInfos.last;
final blockCache = blocks[rowInfo.rowPB.blockId];
final rowCache = blockCache?.rowCache;
late GridFieldController fieldController;
if (_gridDataController != null) {
fieldController = _gridDataController!.fieldController;
}
if (_boardDataController != null) {
fieldController = _boardDataController!.fieldController;
}
final fieldController = _gridDataController.fieldController;
final rowDataController = GridRowDataController(
rowInfo: rowInfo,
@ -163,55 +118,56 @@ class AppFlowyGridTest {
return fieldContext;
}
Future<void> createTestGrid() async {
GridFieldContext checkboxFieldContext() {
final fieldContext = fieldContexts
.firstWhere((element) => element.fieldType == FieldType.Checkbox);
return fieldContext;
}
}
/// Create a empty Grid for test
class AppFlowyGridTest {
final AppFlowyUnitTest unitTest;
AppFlowyGridTest({required this.unitTest});
static Future<AppFlowyGridTest> ensureInitialized() async {
final inner = await AppFlowyUnitTest.ensureInitialized();
return AppFlowyGridTest(unitTest: inner);
}
Future<GridTestContext> createTestGrid() async {
final app = await unitTest.createTestApp();
final builder = GridPluginBuilder();
final result = await AppService().createView(
final context = await AppService()
.createView(
appId: app.id,
name: "Test Grid",
dataFormatType: builder.dataFormatType,
pluginType: builder.pluginType,
layoutType: builder.layoutType!,
);
await result.fold(
(view) async {
gridView = view;
_gridDataController = GridDataController(view: view);
await openGrid();
},
(error) {},
);
}
)
.then((result) {
return result.fold(
(view) async {
final context = GridTestContext(view, GridDataController(view: view));
final result = await context._gridDataController.openGrid();
result.fold((l) => null, (r) => throw Exception(r));
return context;
},
(error) {
throw Exception();
},
);
});
Future<void> openGrid() async {
final result = await _gridDataController!.openGrid();
result.fold((l) => null, (r) => throw Exception(r));
}
Future<void> createTestBoard() async {
final app = await unitTest.createTestApp();
final builder = BoardPluginBuilder();
final result = await AppService().createView(
appId: app.id,
name: "Test Board",
dataFormatType: builder.dataFormatType,
pluginType: builder.pluginType,
layoutType: builder.layoutType!,
);
await result.fold(
(view) async {
_boardDataController = BoardDataController(view: view);
final result = await _boardDataController!.openGrid();
result.fold((l) => null, (r) => throw Exception(r));
gridView = view;
},
(error) {},
);
return context;
}
}
/// Create a new Grid for cell test
class AppFlowyGridCellTest {
late GridTestContext context;
final AppFlowyGridTest gridTest;
AppFlowyGridCellTest({required this.gridTest});
@ -220,32 +176,12 @@ class AppFlowyGridCellTest {
return AppFlowyGridCellTest(gridTest: gridTest);
}
Future<void> createTestRow() async {
await gridTest.createRow();
}
Future<void> createTestGrid() async {
await gridTest.createTestGrid();
}
}
class AppFlowyGridSelectOptionCellTest {
final AppFlowyGridCellTest _gridCellTest;
AppFlowyGridSelectOptionCellTest(AppFlowyGridCellTest cellTest)
: _gridCellTest = cellTest;
static Future<AppFlowyGridSelectOptionCellTest> ensureInitialized() async {
final gridTest = await AppFlowyGridCellTest.ensureInitialized();
return AppFlowyGridSelectOptionCellTest(gridTest);
}
Future<void> createTestGrid() async {
await _gridCellTest.createTestGrid();
context = await gridTest.createTestGrid();
}
Future<void> createTestRow() async {
await _gridCellTest.createTestRow();
await context.createRow();
}
Future<GridSelectOptionCellController> makeCellController(
@ -253,17 +189,17 @@ class AppFlowyGridSelectOptionCellTest {
assert(fieldType == FieldType.SingleSelect ||
fieldType == FieldType.MultiSelect);
final fieldContexts = _gridCellTest.gridTest.fieldContexts;
final fieldContexts = context.fieldContexts;
final field =
fieldContexts.firstWhere((element) => element.fieldType == fieldType);
final cellController = await _gridCellTest.gridTest
.makeCellController(field.id) as GridSelectOptionCellController;
final cellController = await context.makeCellController(field.id)
as GridSelectOptionCellController;
return cellController;
}
}
Future<void> gridResponseFuture() {
return Future.delayed(gridResponseDuration(milliseconds: 200));
Future<void> gridResponseFuture({int milliseconds = 500}) {
return Future.delayed(gridResponseDuration(milliseconds: milliseconds));
}
Duration gridResponseDuration({int milliseconds = 200}) {

View File

@ -1,16 +1,10 @@
import 'package:app_flowy/plugins/board/application/board_bloc.dart';
import 'package:app_flowy/plugins/board/board.dart';
import 'package:app_flowy/plugins/document/application/doc_bloc.dart';
import 'package:app_flowy/plugins/document/document.dart';
import 'package:app_flowy/plugins/grid/application/grid_bloc.dart';
import 'package:app_flowy/plugins/grid/grid.dart';
import 'package:app_flowy/workspace/application/app/app_bloc.dart';
import 'package:app_flowy/workspace/application/menu/menu_view_section_bloc.dart';
import 'package:flowy_sdk/dispatch/dispatch.dart';
import 'package:flowy_sdk/protobuf/flowy-folder/app.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:bloc_test/bloc_test.dart';
import '../../util.dart';
void main() {
@ -19,310 +13,153 @@ void main() {
testContext = await AppFlowyUnitTest.ensureInitialized();
});
group(
'$AppBloc',
() {
late AppPB app;
setUp(() async {
app = await testContext.createTestApp();
});
test('rename app test', () async {
final app = await testContext.createTestApp();
final bloc = AppBloc(app: app)..add(const AppEvent.initial());
await blocResponseFuture();
blocTest<AppBloc, AppState>(
"Create a document",
build: () => AppBloc(app: app)..add(const AppEvent.initial()),
act: (bloc) {
bloc.add(
AppEvent.createView("Test document", DocumentPluginBuilder()));
},
wait: blocResponseDuration(),
verify: (bloc) {
assert(bloc.state.views.length == 1);
assert(bloc.state.views.last.name == "Test document");
assert(bloc.state.views.last.layout == ViewLayoutTypePB.Document);
},
);
bloc.add(const AppEvent.rename('Hello world'));
await blocResponseFuture();
blocTest<AppBloc, AppState>(
"Create a grid",
build: () => AppBloc(app: app)..add(const AppEvent.initial()),
act: (bloc) {
bloc.add(AppEvent.createView("Test grid", GridPluginBuilder()));
},
wait: blocResponseDuration(),
verify: (bloc) {
assert(bloc.state.views.length == 1);
assert(bloc.state.views.last.name == "Test grid");
assert(bloc.state.views.last.layout == ViewLayoutTypePB.Grid);
},
);
blocTest<AppBloc, AppState>(
"Create a Kanban board",
build: () => AppBloc(app: app)..add(const AppEvent.initial()),
act: (bloc) {
bloc.add(AppEvent.createView("Test board", BoardPluginBuilder()));
},
wait: const Duration(milliseconds: 100),
verify: (bloc) {
assert(bloc.state.views.length == 1);
assert(bloc.state.views.last.name == "Test board");
assert(bloc.state.views.last.layout == ViewLayoutTypePB.Board);
},
);
},
);
group('$AppBloc', () {
late AppPB app;
setUpAll(() async {
app = await testContext.createTestApp();
});
blocTest<AppBloc, AppState>(
"rename the app",
build: () => AppBloc(app: app)..add(const AppEvent.initial()),
wait: blocResponseDuration(),
act: (bloc) => bloc.add(const AppEvent.rename('Hello world')),
verify: (bloc) {
assert(bloc.state.app.name == 'Hello world');
},
);
blocTest<AppBloc, AppState>(
"delete the app",
build: () => AppBloc(app: app)..add(const AppEvent.initial()),
wait: blocResponseDuration(),
act: (bloc) => bloc.add(const AppEvent.delete()),
verify: (bloc) async {
final apps = await testContext.loadApps();
assert(apps.where((element) => element.id == app.id).isEmpty);
},
);
assert(bloc.state.app.name == 'Hello world');
});
group('$AppBloc', () {
late ViewPB view;
late AppPB app;
setUpAll(() async {
app = await testContext.createTestApp();
});
test('delete ap test', () async {
final app = await testContext.createTestApp();
final bloc = AppBloc(app: app)..add(const AppEvent.initial());
await blocResponseFuture();
blocTest<AppBloc, AppState>(
"create a document",
build: () => AppBloc(app: app)..add(const AppEvent.initial()),
act: (bloc) {
bloc.add(AppEvent.createView("Test document", DocumentPluginBuilder()));
},
wait: blocResponseDuration(),
verify: (bloc) {
assert(bloc.state.views.length == 1);
view = bloc.state.views.last;
},
);
bloc.add(const AppEvent.delete());
await blocResponseFuture();
blocTest<AppBloc, AppState>(
"delete the document",
build: () => AppBloc(app: app)..add(const AppEvent.initial()),
act: (bloc) => bloc.add(AppEvent.deleteView(view.id)),
wait: blocResponseDuration(),
verify: (bloc) {
assert(bloc.state.views.isEmpty);
},
);
final apps = await testContext.loadApps();
assert(apps.where((element) => element.id == app.id).isEmpty);
});
group('$AppBloc', () {
late AppPB app;
setUpAll(() async {
app = await testContext.createTestApp();
});
blocTest<AppBloc, AppState>(
"create documents' order test",
build: () => AppBloc(app: app)..add(const AppEvent.initial()),
act: (bloc) async {
bloc.add(AppEvent.createView("1", DocumentPluginBuilder()));
await blocResponseFuture();
bloc.add(AppEvent.createView("2", DocumentPluginBuilder()));
await blocResponseFuture();
bloc.add(AppEvent.createView("3", DocumentPluginBuilder()));
await blocResponseFuture();
},
wait: blocResponseDuration(),
verify: (bloc) {
assert(bloc.state.views[0].name == '1');
assert(bloc.state.views[1].name == '2');
assert(bloc.state.views[2].name == '3');
},
);
test('create documents in order', () async {
final app = await testContext.createTestApp();
final bloc = AppBloc(app: app)..add(const AppEvent.initial());
await blocResponseFuture();
bloc.add(AppEvent.createView("1", DocumentPluginBuilder()));
await blocResponseFuture();
bloc.add(AppEvent.createView("2", DocumentPluginBuilder()));
await blocResponseFuture();
bloc.add(AppEvent.createView("3", DocumentPluginBuilder()));
await blocResponseFuture();
assert(bloc.state.views[0].name == '1');
assert(bloc.state.views[1].name == '2');
assert(bloc.state.views[2].name == '3');
});
group('$AppBloc', () {
late AppPB app;
setUpAll(() async {
app = await testContext.createTestApp();
});
blocTest<AppBloc, AppState>(
"reorder documents",
build: () => AppBloc(app: app)..add(const AppEvent.initial()),
act: (bloc) async {
bloc.add(AppEvent.createView("1", DocumentPluginBuilder()));
await blocResponseFuture();
bloc.add(AppEvent.createView("2", DocumentPluginBuilder()));
await blocResponseFuture();
bloc.add(AppEvent.createView("3", DocumentPluginBuilder()));
await blocResponseFuture();
test('reorder documents test', () async {
final app = await testContext.createTestApp();
final bloc = AppBloc(app: app)..add(const AppEvent.initial());
await blocResponseFuture();
final appViewData = AppViewDataContext(appId: app.id);
appViewData.views = bloc.state.views;
final viewSectionBloc = ViewSectionBloc(
appViewData: appViewData,
)..add(const ViewSectionEvent.initial());
await blocResponseFuture();
bloc.add(AppEvent.createView("1", DocumentPluginBuilder()));
await blocResponseFuture();
bloc.add(AppEvent.createView("2", DocumentPluginBuilder()));
await blocResponseFuture();
bloc.add(AppEvent.createView("3", DocumentPluginBuilder()));
await blocResponseFuture();
viewSectionBloc.add(const ViewSectionEvent.moveView(0, 2));
await blocResponseFuture();
},
wait: blocResponseDuration(),
verify: (bloc) {
assert(bloc.state.views[0].name == '2');
assert(bloc.state.views[1].name == '3');
assert(bloc.state.views[2].name == '1');
},
);
final appViewData = AppViewDataContext(appId: app.id);
appViewData.views = bloc.state.views;
final viewSectionBloc = ViewSectionBloc(
appViewData: appViewData,
)..add(const ViewSectionEvent.initial());
await blocResponseFuture();
viewSectionBloc.add(const ViewSectionEvent.moveView(0, 2));
await blocResponseFuture();
assert(bloc.state.views[0].name == '2');
assert(bloc.state.views[1].name == '3');
assert(bloc.state.views[2].name == '1');
});
group('$AppBloc', () {
late AppPB app;
setUpAll(() async {
app = await testContext.createTestApp();
});
blocTest<AppBloc, AppState>(
test('open latest view test', () async {
final app = await testContext.createTestApp();
final bloc = AppBloc(app: app)..add(const AppEvent.initial());
await blocResponseFuture();
assert(
bloc.state.latestCreatedView == null,
"assert initial latest create view is null after initialize",
build: () => AppBloc(app: app)..add(const AppEvent.initial()),
wait: blocResponseDuration(),
verify: (bloc) {
assert(bloc.state.latestCreatedView == null);
},
);
blocTest<AppBloc, AppState>(
"create a view and assert the latest create view is this view",
build: () => AppBloc(app: app)..add(const AppEvent.initial()),
act: (bloc) async {
bloc.add(AppEvent.createView("1", DocumentPluginBuilder()));
},
wait: blocResponseDuration(),
verify: (bloc) {
assert(bloc.state.latestCreatedView!.id == bloc.state.views.last.id);
},
);
blocTest<AppBloc, AppState>(
bloc.add(AppEvent.createView("1", DocumentPluginBuilder()));
await blocResponseFuture();
assert(
bloc.state.latestCreatedView!.id == bloc.state.views.last.id,
"create a view and assert the latest create view is this view",
build: () => AppBloc(app: app)..add(const AppEvent.initial()),
act: (bloc) async {
bloc.add(AppEvent.createView("2", DocumentPluginBuilder()));
},
wait: blocResponseDuration(),
verify: (bloc) {
assert(bloc.state.views[0].name == "1");
assert(bloc.state.latestCreatedView!.id == bloc.state.views.last.id);
},
);
blocTest<AppBloc, AppState>(
"check latest create view is null after reinitialize",
build: () => AppBloc(app: app)..add(const AppEvent.initial()),
wait: blocResponseDuration(),
verify: (bloc) {
assert(bloc.state.latestCreatedView == null);
},
bloc.add(AppEvent.createView("2", DocumentPluginBuilder()));
await blocResponseFuture();
assert(
bloc.state.latestCreatedView!.id == bloc.state.views.last.id,
"create a view and assert the latest create view is this view",
);
});
group('$AppBloc', () {
late AppPB app;
late ViewPB latestCreatedView;
setUpAll(() async {
app = await testContext.createTestApp();
});
test('open latest documents test', () async {
final app = await testContext.createTestApp();
final bloc = AppBloc(app: app)..add(const AppEvent.initial());
await blocResponseFuture();
// Document
blocTest<AppBloc, AppState>(
"create a document view",
build: () => AppBloc(app: app)..add(const AppEvent.initial()),
act: (bloc) async {
bloc.add(AppEvent.createView("New document", DocumentPluginBuilder()));
},
wait: blocResponseDuration(),
verify: (bloc) {
latestCreatedView = bloc.state.views.last;
},
);
bloc.add(AppEvent.createView("document 1", DocumentPluginBuilder()));
await blocResponseFuture();
final document1 = bloc.state.latestCreatedView;
assert(document1!.name == "document 1");
blocTest<DocumentBloc, DocumentState>(
"open the document",
build: () => DocumentBloc(view: latestCreatedView)
..add(const DocumentEvent.initial()),
wait: blocResponseDuration(),
);
bloc.add(AppEvent.createView("document 2", DocumentPluginBuilder()));
await blocResponseFuture();
final document2 = bloc.state.latestCreatedView;
assert(document2!.name == "document 2");
test('check latest opened view is this document', () async {
final workspaceSetting = await FolderEventReadCurrentWorkspace()
.send()
.then((result) => result.fold((l) => l, (r) => throw Exception()));
workspaceSetting.latestView.id == latestCreatedView.id;
});
// Open document 1
// ignore: unused_local_variable
final documentBloc = DocumentBloc(view: document1!)
..add(const DocumentEvent.initial());
await blocResponseFuture();
// Grid
blocTest<AppBloc, AppState>(
"create a grid view",
build: () => AppBloc(app: app)..add(const AppEvent.initial()),
act: (bloc) async {
bloc.add(AppEvent.createView("New grid", GridPluginBuilder()));
},
wait: blocResponseDuration(),
verify: (bloc) {
latestCreatedView = bloc.state.views.last;
},
);
blocTest<GridBloc, GridState>(
"open the grid",
build: () =>
GridBloc(view: latestCreatedView)..add(const GridEvent.initial()),
wait: blocResponseDuration(),
);
final workspaceSetting = await FolderEventReadCurrentWorkspace()
.send()
.then((result) => result.fold((l) => l, (r) => throw Exception()));
workspaceSetting.latestView.id == document1.id;
});
test('check latest opened view is this grid', () async {
final workspaceSetting = await FolderEventReadCurrentWorkspace()
.send()
.then((result) => result.fold((l) => l, (r) => throw Exception()));
workspaceSetting.latestView.id == latestCreatedView.id;
});
test('open latest grid test', () async {
final app = await testContext.createTestApp();
final bloc = AppBloc(app: app)..add(const AppEvent.initial());
await blocResponseFuture();
// Board
blocTest<AppBloc, AppState>(
"create a board view",
build: () => AppBloc(app: app)..add(const AppEvent.initial()),
act: (bloc) async {
bloc.add(AppEvent.createView("New board", BoardPluginBuilder()));
},
wait: blocResponseDuration(),
verify: (bloc) {
latestCreatedView = bloc.state.views.last;
},
);
bloc.add(AppEvent.createView("grid 1", GridPluginBuilder()));
await blocResponseFuture();
final grid1 = bloc.state.latestCreatedView;
assert(grid1!.name == "grid 1");
blocTest<BoardBloc, BoardState>(
"open the board",
build: () =>
BoardBloc(view: latestCreatedView)..add(const BoardEvent.initial()),
wait: blocResponseDuration(),
);
bloc.add(AppEvent.createView("grid 2", GridPluginBuilder()));
await blocResponseFuture();
final grid2 = bloc.state.latestCreatedView;
assert(grid2!.name == "grid 2");
test('check latest opened view is this board', () async {
final workspaceSetting = await FolderEventReadCurrentWorkspace()
.send()
.then((result) => result.fold((l) => l, (r) => throw Exception()));
workspaceSetting.latestView.id == latestCreatedView.id;
});
var workspaceSetting = await FolderEventReadCurrentWorkspace()
.send()
.then((result) => result.fold((l) => l, (r) => throw Exception()));
workspaceSetting.latestView.id == grid1!.id;
// Open grid 1
// ignore: unused_local_variable
final documentBloc = DocumentBloc(view: grid1)
..add(const DocumentEvent.initial());
await blocResponseFuture();
workspaceSetting = await FolderEventReadCurrentWorkspace()
.send()
.then((result) => result.fold((l) => l, (r) => throw Exception()));
workspaceSetting.latestView.id == grid1.id;
});
}

View File

@ -0,0 +1,69 @@
import 'package:app_flowy/plugins/board/board.dart';
import 'package:app_flowy/plugins/document/document.dart';
import 'package:app_flowy/plugins/grid/grid.dart';
import 'package:app_flowy/workspace/application/app/app_bloc.dart';
import 'package:flowy_sdk/protobuf/flowy-folder/app.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:bloc_test/bloc_test.dart';
import '../../util.dart';
void main() {
late AppFlowyUnitTest testContext;
setUpAll(() async {
testContext = await AppFlowyUnitTest.ensureInitialized();
});
group(
'$AppBloc',
() {
late AppPB app;
setUp(() async {
app = await testContext.createTestApp();
});
blocTest<AppBloc, AppState>(
"Create a document",
build: () => AppBloc(app: app)..add(const AppEvent.initial()),
act: (bloc) {
bloc.add(
AppEvent.createView("Test document", DocumentPluginBuilder()));
},
wait: blocResponseDuration(),
verify: (bloc) {
assert(bloc.state.views.length == 1);
assert(bloc.state.views.last.name == "Test document");
assert(bloc.state.views.last.layout == ViewLayoutTypePB.Document);
},
);
blocTest<AppBloc, AppState>(
"Create a grid",
build: () => AppBloc(app: app)..add(const AppEvent.initial()),
act: (bloc) {
bloc.add(AppEvent.createView("Test grid", GridPluginBuilder()));
},
wait: blocResponseDuration(),
verify: (bloc) {
assert(bloc.state.views.length == 1);
assert(bloc.state.views.last.name == "Test grid");
assert(bloc.state.views.last.layout == ViewLayoutTypePB.Grid);
},
);
blocTest<AppBloc, AppState>(
"Create a Kanban board",
build: () => AppBloc(app: app)..add(const AppEvent.initial()),
act: (bloc) {
bloc.add(AppEvent.createView("Test board", BoardPluginBuilder()));
},
wait: const Duration(milliseconds: 100),
verify: (bloc) {
assert(bloc.state.views.length == 1);
assert(bloc.state.views.last.name == "Test board");
assert(bloc.state.views.last.layout == ViewLayoutTypePB.Board);
},
);
},
);
}

View File

@ -1,20 +1,53 @@
import 'package:app_flowy/plugins/document/document.dart';
import 'package:app_flowy/plugins/trash/application/trash_bloc.dart';
import 'package:app_flowy/workspace/application/app/app_bloc.dart';
import 'package:bloc_test/bloc_test.dart';
import 'package:flowy_sdk/protobuf/flowy-folder/app.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../util.dart';
void main() {
late AppFlowyUnitTest test;
class TrashTestContext {
late AppPB app;
late AppBloc appBloc;
late TrashBloc trashBloc;
late List<ViewPB> allViews;
final AppFlowyUnitTest unitTest;
TrashTestContext(this.unitTest);
Future<void> initialize() async {
app = await unitTest.createTestApp();
appBloc = AppBloc(app: app)..add(const AppEvent.initial());
appBloc.add(AppEvent.createView(
"Document 1",
DocumentPluginBuilder(),
));
await blocResponseFuture();
appBloc.add(AppEvent.createView(
"Document 2",
DocumentPluginBuilder(),
));
await blocResponseFuture();
appBloc.add(
AppEvent.createView(
"Document 3",
DocumentPluginBuilder(),
),
);
await blocResponseFuture();
allViews = [...appBloc.state.app.belongings.items];
assert(allViews.length == 3);
}
}
void main() {
late AppFlowyUnitTest unitTest;
setUpAll(() async {
test = await AppFlowyUnitTest.ensureInitialized();
unitTest = await AppFlowyUnitTest.ensureInitialized();
});
// 1. Create three views
@ -22,158 +55,46 @@ void main() {
// 3. Delete all views and check the state
// 4. Put back a view
// 5. Put back all views
group('$TrashBloc', () {
late ViewPB deletedView;
late List<ViewPB> allViews;
setUpAll(() async {
/// Create a new app with three documents
app = await test.createTestApp();
appBloc = AppBloc(app: app)
..add(const AppEvent.initial())
..add(AppEvent.createView(
"Document 1",
DocumentPluginBuilder(),
))
..add(AppEvent.createView(
"Document 2",
DocumentPluginBuilder(),
))
..add(
AppEvent.createView(
"Document 3",
DocumentPluginBuilder(),
),
);
group('trash test: ', () {
test('delete a view', () async {
final context = TrashTestContext(unitTest);
await context.initialize();
final trashBloc = TrashBloc()..add(const TrashEvent.initial());
await blocResponseFuture(millisecond: 200);
allViews = [...appBloc.state.app.belongings.items];
assert(allViews.length == 3);
});
setUp(() async {
trashBloc = TrashBloc()..add(const TrashEvent.initial());
// delete a view
final deletedView = context.appBloc.state.app.belongings.items[0];
context.appBloc.add(AppEvent.deleteView(deletedView.id));
await blocResponseFuture();
});
assert(context.appBloc.state.app.belongings.items.length == 2);
assert(trashBloc.state.objects.length == 1);
assert(trashBloc.state.objects.first.id == deletedView.id);
blocTest<TrashBloc, TrashState>(
"delete a view",
build: () => trashBloc,
act: (bloc) async {
deletedView = appBloc.state.app.belongings.items[0];
appBloc.add(AppEvent.deleteView(deletedView.id));
},
wait: blocResponseDuration(),
verify: (bloc) {
assert(appBloc.state.app.belongings.items.length == 2);
assert(bloc.state.objects.length == 1);
assert(bloc.state.objects.first.id == deletedView.id);
},
);
blocTest<TrashBloc, TrashState>(
"delete all views",
build: () => trashBloc,
act: (bloc) async {
for (final view in appBloc.state.app.belongings.items) {
appBloc.add(AppEvent.deleteView(view.id));
await blocResponseFuture();
}
},
wait: blocResponseDuration(),
verify: (bloc) {
assert(bloc.state.objects[0].id == allViews[0].id);
assert(bloc.state.objects[1].id == allViews[1].id);
assert(bloc.state.objects[2].id == allViews[2].id);
},
);
blocTest<TrashBloc, TrashState>(
"put back a trash",
build: () => trashBloc,
act: (bloc) async {
bloc.add(TrashEvent.putback(allViews[0].id));
},
wait: blocResponseDuration(),
verify: (bloc) {
assert(appBloc.state.app.belongings.items.length == 1);
assert(bloc.state.objects.length == 2);
},
);
blocTest<TrashBloc, TrashState>(
"put back all trash",
build: () => trashBloc,
act: (bloc) async {
bloc.add(const TrashEvent.restoreAll());
},
wait: blocResponseDuration(),
verify: (bloc) {
assert(appBloc.state.app.belongings.items.length == 3);
assert(bloc.state.objects.isEmpty);
},
);
//
});
// 1. Create three views
// 2. Delete a trash permanently and check the state
// 3. Delete all views permanently
group('$TrashBloc', () {
setUpAll(() async {
/// Create a new app with three documents
app = await test.createTestApp();
appBloc = AppBloc(app: app)
..add(const AppEvent.initial())
..add(AppEvent.createView(
"Document 1",
DocumentPluginBuilder(),
))
..add(AppEvent.createView(
"Document 2",
DocumentPluginBuilder(),
))
..add(
AppEvent.createView(
"Document 3",
DocumentPluginBuilder(),
),
);
await blocResponseFuture(millisecond: 200);
});
setUp(() async {
trashBloc = TrashBloc()..add(const TrashEvent.initial());
// put back
trashBloc.add(TrashEvent.putback(deletedView.id));
await blocResponseFuture();
});
assert(context.appBloc.state.app.belongings.items.length == 3);
assert(trashBloc.state.objects.isEmpty);
blocTest<TrashBloc, TrashState>(
"delete a view permanently",
build: () => trashBloc,
act: (bloc) async {
final view = appBloc.state.app.belongings.items[0];
appBloc.add(AppEvent.deleteView(view.id));
// delete all views
for (final view in context.allViews) {
context.appBloc.add(AppEvent.deleteView(view.id));
await blocResponseFuture();
}
assert(trashBloc.state.objects[0].id == context.allViews[0].id);
assert(trashBloc.state.objects[1].id == context.allViews[1].id);
assert(trashBloc.state.objects[2].id == context.allViews[2].id);
trashBloc.add(TrashEvent.delete(trashBloc.state.objects[0]));
},
wait: blocResponseDuration(),
verify: (bloc) {
assert(appBloc.state.app.belongings.items.length == 2);
assert(bloc.state.objects.isEmpty);
},
);
blocTest<TrashBloc, TrashState>(
"delete all view permanently",
build: () => trashBloc,
act: (bloc) async {
for (final view in appBloc.state.app.belongings.items) {
appBloc.add(AppEvent.deleteView(view.id));
await blocResponseFuture();
}
trashBloc.add(const TrashEvent.deleteAll());
},
wait: blocResponseDuration(),
verify: (bloc) {
assert(appBloc.state.app.belongings.items.isEmpty);
assert(bloc.state.objects.isEmpty);
},
);
// delete a view permanently
trashBloc.add(TrashEvent.delete(trashBloc.state.objects[0]));
await blocResponseFuture();
assert(trashBloc.state.objects.length == 2);
// delete all view permanently
trashBloc.add(const TrashEvent.deleteAll());
await blocResponseFuture();
assert(trashBloc.state.objects.isEmpty);
});
});
}

View File

@ -956,6 +956,7 @@ name = "flowy-grid"
version = "0.1.0"
dependencies = [
"anyhow",
"async-stream",
"atomic_refcell",
"bytes",
"chrono",

View File

@ -44,6 +44,7 @@ url = { version = "2"}
futures = "0.3.15"
atomic_refcell = "0.1.8"
crossbeam-utils = "0.8.7"
async-stream = "0.3.2"
[dev-dependencies]
flowy-test = { path = "../flowy-test" }

View File

@ -3,7 +3,7 @@ use flowy_derive::ProtoBuf_Enum;
const OBSERVABLE_CATEGORY: &str = "Grid";
#[derive(ProtoBuf_Enum, Debug)]
pub enum GridNotification {
pub enum GridDartNotification {
Unknown = 0,
DidCreateBlock = 11,
DidUpdateGridBlock = 20,
@ -18,19 +18,19 @@ pub enum GridNotification {
DidUpdateGridSetting = 70,
}
impl std::default::Default for GridNotification {
impl std::default::Default for GridDartNotification {
fn default() -> Self {
GridNotification::Unknown
GridDartNotification::Unknown
}
}
impl std::convert::From<GridNotification> for i32 {
fn from(notification: GridNotification) -> Self {
impl std::convert::From<GridDartNotification> for i32 {
fn from(notification: GridDartNotification) -> Self {
notification as i32
}
}
#[tracing::instrument(level = "trace")]
pub fn send_dart_notification(id: &str, ty: GridNotification) -> DartNotifyBuilder {
pub fn send_dart_notification(id: &str, ty: GridDartNotification) -> DartNotifyBuilder {
DartNotifyBuilder::new(id, ty, OBSERVABLE_CATEGORY)
}

View File

@ -152,7 +152,7 @@ impl std::convert::From<&RowRevision> for InsertedRowPB {
}
}
#[derive(Debug, Default, ProtoBuf)]
#[derive(Debug, Default, Clone, ProtoBuf)]
pub struct GridBlockChangesetPB {
#[pb(index = 1)]
pub block_id: String,
@ -170,7 +170,7 @@ pub struct GridBlockChangesetPB {
pub visible_rows: Vec<String>,
#[pb(index = 6)]
pub hide_rows: Vec<String>,
pub invisible_rows: Vec<String>,
}
impl GridBlockChangesetPB {
pub fn insert(block_id: String, inserted_rows: Vec<InsertedRowPB>) -> Self {

View File

@ -1,7 +1,6 @@
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::ErrorCode;
use grid_rev_model::FilterRevision;
use std::sync::Arc;
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct CheckboxFilterPB {

View File

@ -3,7 +3,6 @@ use flowy_error::ErrorCode;
use grid_rev_model::FilterRevision;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use std::sync::Arc;
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct DateFilterPB {

View File

@ -1,4 +1,4 @@
use crate::entities::{FilterPB, InsertedRowPB, RepeatedFilterPB, RowPB};
use crate::entities::FilterPB;
use flowy_derive::ProtoBuf;
#[derive(Debug, Default, ProtoBuf)]

View File

@ -2,8 +2,6 @@ use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::ErrorCode;
use grid_rev_model::FilterRevision;
use std::sync::Arc;
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct NumberFilterPB {
#[pb(index = 1)]

View File

@ -2,7 +2,6 @@ use crate::services::field::SelectOptionIds;
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::ErrorCode;
use grid_rev_model::FilterRevision;
use std::sync::Arc;
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct SelectOptionFilterPB {

View File

@ -1,7 +1,6 @@
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::ErrorCode;
use grid_rev_model::FilterRevision;
use std::sync::Arc;
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct TextFilterPB {

View File

@ -96,6 +96,7 @@ impl TryInto<DeleteFilterParams> for DeleteFilterPayloadPB {
}
}
#[derive(Debug)]
pub struct DeleteFilterParams {
pub filter_type: FilterType,
pub filter_id: String,
@ -177,6 +178,7 @@ impl TryInto<CreateFilterParams> for CreateFilterPayloadPB {
}
}
#[derive(Debug)]
pub struct CreateFilterParams {
pub field_id: String,
pub field_type_rev: FieldTypeRevision,

View File

@ -1,6 +1,6 @@
use crate::entities::*;
use crate::manager::GridManager;
use crate::services::cell::AnyCellData;
use crate::services::cell::TypeCellData;
use crate::services::field::{
default_type_option_builder_from_type, select_type_option_from_field_rev, type_option_builder_from_json_str,
DateCellChangeset, DateChangesetPB, SelectOptionCellChangeset, SelectOptionCellChangesetPB,
@ -414,8 +414,8 @@ pub(crate) async fn get_select_option_handler(
//
let cell_rev = editor.get_cell_rev(&params.row_id, &params.field_id).await?;
let type_option = select_type_option_from_field_rev(&field_rev)?;
let any_cell_data: AnyCellData = match cell_rev {
None => AnyCellData {
let any_cell_data: TypeCellData = match cell_rev {
None => TypeCellData {
data: "".to_string(),
field_type: field_rev.ty.into(),
},

View File

@ -161,8 +161,8 @@ pub enum GridEvent {
/// [UpdateSelectOption] event is used to update a FieldTypeOptionData whose field_type is
/// FieldType::SingleSelect or FieldType::MultiSelect.
///
/// This event may trigger the GridNotification::DidUpdateCell event.
/// For example, GridNotification::DidUpdateCell will be triggered if the [SelectOptionChangesetPB]
/// This event may trigger the GridDartNotification::DidUpdateCell event.
/// For example, GridDartNotification::DidUpdateCell will be triggered if the [SelectOptionChangesetPB]
/// carries a change that updates the name of the option.
#[event(input = "SelectOptionChangesetPB")]
UpdateSelectOption = 32,

View File

@ -1,12 +1,12 @@
use crate::entities::GridLayout;
use crate::services::grid_editor::{GridRevisionCompress, GridRevisionEditor};
use crate::services::grid_view_manager::make_grid_view_rev_manager;
use crate::services::persistence::block_index::BlockIndexCache;
use crate::services::persistence::kv::GridKVPersistence;
use crate::services::persistence::migration::GridMigration;
use crate::services::persistence::rev_sqlite::SQLiteGridRevisionPersistence;
use crate::services::persistence::GridDatabase;
use crate::services::view_editor::make_grid_view_rev_manager;
use bytes::Bytes;
use flowy_database::ConnectionPool;
@ -126,13 +126,10 @@ impl GridManager {
return Ok(editor);
}
let mut grid_editors = self.grid_editors.write().await;
let db_pool = self.grid_user.db_pool()?;
let editor = self.make_grid_rev_editor(grid_id, db_pool).await?;
self.grid_editors
.write()
.await
.insert(grid_id.to_string(), editor.clone());
// self.task_scheduler.write().await.register_handler(editor.clone());
grid_editors.insert(grid_id.to_string(), editor.clone());
Ok(editor)
}

View File

@ -1,4 +1,4 @@
use crate::dart_notification::{send_dart_notification, GridNotification};
use crate::dart_notification::{send_dart_notification, GridDartNotification};
use crate::entities::{CellChangesetPB, GridBlockChangesetPB, InsertedRowPB, RowPB};
use crate::manager::GridUser;
use crate::services::block_editor::{GridBlockRevisionCompress, GridBlockRevisionEditor};
@ -237,7 +237,7 @@ impl GridBlockManager {
}
async fn notify_did_update_block(&self, block_id: &str, changeset: GridBlockChangesetPB) -> FlowyResult<()> {
send_dart_notification(block_id, GridNotification::DidUpdateGridBlock)
send_dart_notification(block_id, GridDartNotification::DidUpdateGridBlock)
.payload(changeset)
.send();
Ok(())
@ -245,7 +245,7 @@ impl GridBlockManager {
async fn notify_did_update_cell(&self, changeset: CellChangesetPB) -> FlowyResult<()> {
let id = format!("{}:{}", changeset.row_id, changeset.field_id);
send_dart_notification(&id, GridNotification::DidUpdateCell).send();
send_dart_notification(&id, GridDartNotification::DidUpdateCell).send();
Ok(())
}
}

View File

@ -6,17 +6,17 @@ use grid_rev_model::CellRevision;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
/// AnyCellData is a generic CellData, you can parse the cell_data according to the field_type.
/// TypeCellData is a generic CellData, you can parse the cell_data according to the field_type.
/// When the type of field is changed, it's different from the field_type of AnyCellData.
/// So it will return an empty data. You could check the CellDataOperation trait for more information.
#[derive(Debug, Serialize, Deserialize)]
pub struct AnyCellData {
pub struct TypeCellData {
pub data: String,
pub field_type: FieldType,
}
impl AnyCellData {
pub fn from_field_type(field_type: &FieldType) -> AnyCellData {
impl TypeCellData {
pub fn from_field_type(field_type: &FieldType) -> TypeCellData {
Self {
data: "".to_string(),
field_type: field_type.clone(),
@ -24,11 +24,11 @@ impl AnyCellData {
}
}
impl std::str::FromStr for AnyCellData {
impl std::str::FromStr for TypeCellData {
type Err = FlowyError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let type_option_cell_data: AnyCellData = serde_json::from_str(s).map_err(|err| {
let type_option_cell_data: TypeCellData = serde_json::from_str(s).map_err(|err| {
let msg = format!("Deserialize {} to any cell data failed. Serde error: {}", s, err);
FlowyError::internal().context(msg)
})?;
@ -36,15 +36,15 @@ impl std::str::FromStr for AnyCellData {
}
}
impl std::convert::TryInto<AnyCellData> for String {
impl std::convert::TryInto<TypeCellData> for String {
type Error = FlowyError;
fn try_into(self) -> Result<AnyCellData, Self::Error> {
AnyCellData::from_str(&self)
fn try_into(self) -> Result<TypeCellData, Self::Error> {
TypeCellData::from_str(&self)
}
}
impl std::convert::TryFrom<&CellRevision> for AnyCellData {
impl std::convert::TryFrom<&CellRevision> for TypeCellData {
type Error = FlowyError;
fn try_from(value: &CellRevision) -> Result<Self, Self::Error> {
@ -52,7 +52,7 @@ impl std::convert::TryFrom<&CellRevision> for AnyCellData {
}
}
impl std::convert::TryFrom<CellRevision> for AnyCellData {
impl std::convert::TryFrom<CellRevision> for TypeCellData {
type Error = FlowyError;
fn try_from(value: CellRevision) -> Result<Self, Self::Error> {
@ -60,18 +60,18 @@ impl std::convert::TryFrom<CellRevision> for AnyCellData {
}
}
impl<T> std::convert::From<AnyCellData> for CellData<T>
impl<T> std::convert::From<TypeCellData> for CellData<T>
where
T: FromCellString,
{
fn from(any_call_data: AnyCellData) -> Self {
fn from(any_call_data: TypeCellData) -> Self {
CellData::from(any_call_data.data)
}
}
impl AnyCellData {
impl TypeCellData {
pub fn new(content: String, field_type: FieldType) -> Self {
AnyCellData {
TypeCellData {
data: content,
field_type,
}

View File

@ -1,5 +1,5 @@
use crate::entities::FieldType;
use crate::services::cell::{AnyCellData, CellBytes};
use crate::services::cell::{CellBytes, TypeCellData};
use crate::services::field::*;
use std::fmt::Debug;
@ -9,11 +9,11 @@ use grid_rev_model::{CellRevision, FieldRevision, FieldTypeRevision};
/// This trait is used when doing filter/search on the grid.
pub trait CellFilterOperation<T> {
/// Return true if any_cell_data match the filter condition.
fn apply_filter(&self, any_cell_data: AnyCellData, filter: &T) -> FlowyResult<bool>;
fn apply_filter(&self, any_cell_data: TypeCellData, filter: &T) -> FlowyResult<bool>;
}
pub trait CellGroupOperation {
fn apply_group(&self, any_cell_data: AnyCellData, group_content: &str) -> FlowyResult<bool>;
fn apply_group(&self, any_cell_data: TypeCellData, group_content: &str) -> FlowyResult<bool>;
}
/// Return object that describes the cell.
@ -126,17 +126,17 @@ pub fn apply_cell_data_changeset<C: ToString, T: AsRef<FieldRevision>>(
FieldType::URL => URLTypeOptionPB::from(field_rev).apply_changeset(changeset.into(), cell_rev),
}?;
Ok(AnyCellData::new(s, field_type).to_json())
Ok(TypeCellData::new(s, field_type).to_json())
}
pub fn decode_any_cell_data<T: TryInto<AnyCellData, Error = FlowyError> + Debug>(
pub fn decode_any_cell_data<T: TryInto<TypeCellData, Error = FlowyError> + Debug>(
data: T,
field_rev: &FieldRevision,
) -> (FieldType, CellBytes) {
let to_field_type = field_rev.ty.into();
match data.try_into() {
Ok(any_cell_data) => {
let AnyCellData { data, field_type } = any_cell_data;
let TypeCellData { data, field_type } = any_cell_data;
match try_decode_cell_data(data.into(), &field_type, &to_field_type, field_rev) {
Ok(cell_bytes) => (field_type, cell_bytes),
Err(e) => {

View File

@ -1,5 +1,5 @@
use crate::entities::{CheckboxFilterCondition, CheckboxFilterPB};
use crate::services::cell::{AnyCellData, CellData, CellFilterOperation};
use crate::services::cell::{CellData, CellFilterOperation, TypeCellData};
use crate::services::field::{CheckboxCellData, CheckboxTypeOptionPB};
use flowy_error::FlowyResult;
@ -14,7 +14,7 @@ impl CheckboxFilterPB {
}
impl CellFilterOperation<CheckboxFilterPB> for CheckboxTypeOptionPB {
fn apply_filter(&self, any_cell_data: AnyCellData, filter: &CheckboxFilterPB) -> FlowyResult<bool> {
fn apply_filter(&self, any_cell_data: TypeCellData, filter: &CheckboxFilterPB) -> FlowyResult<bool> {
if !any_cell_data.is_checkbox() {
return Ok(true);
}

View File

@ -1,5 +1,5 @@
use crate::entities::{DateFilterCondition, DateFilterPB};
use crate::services::cell::{AnyCellData, CellData, CellFilterOperation};
use crate::services::cell::{CellData, CellFilterOperation, TypeCellData};
use crate::services::field::{DateTimestamp, DateTypeOptionPB};
use chrono::NaiveDateTime;
use flowy_error::FlowyResult;
@ -60,7 +60,7 @@ impl DateFilterPB {
}
impl CellFilterOperation<DateFilterPB> for DateTypeOptionPB {
fn apply_filter(&self, any_cell_data: AnyCellData, filter: &DateFilterPB) -> FlowyResult<bool> {
fn apply_filter(&self, any_cell_data: TypeCellData, filter: &DateFilterPB) -> FlowyResult<bool> {
if !any_cell_data.is_date() {
return Ok(true);
}

View File

@ -1,5 +1,5 @@
use crate::entities::{NumberFilterCondition, NumberFilterPB};
use crate::services::cell::{AnyCellData, CellFilterOperation};
use crate::services::cell::{CellFilterOperation, TypeCellData};
use crate::services::field::{NumberCellData, NumberTypeOptionPB};
use flowy_error::FlowyResult;
use rust_decimal::prelude::Zero;
@ -38,7 +38,7 @@ impl NumberFilterPB {
}
impl CellFilterOperation<NumberFilterPB> for NumberTypeOptionPB {
fn apply_filter(&self, any_cell_data: AnyCellData, filter: &NumberFilterPB) -> FlowyResult<bool> {
fn apply_filter(&self, any_cell_data: TypeCellData, filter: &NumberFilterPB) -> FlowyResult<bool> {
if !any_cell_data.is_number() {
return Ok(true);
}

View File

@ -1,7 +1,7 @@
#![allow(clippy::needless_collect)]
use crate::entities::{SelectOptionCondition, SelectOptionFilterPB};
use crate::services::cell::{AnyCellData, CellFilterOperation};
use crate::services::cell::{CellFilterOperation, TypeCellData};
use crate::services::field::{MultiSelectTypeOptionPB, SingleSelectTypeOptionPB};
use crate::services::field::{SelectTypeOptionSharedAction, SelectedSelectOptions};
use flowy_error::FlowyResult;
@ -41,7 +41,7 @@ impl SelectOptionFilterPB {
}
impl CellFilterOperation<SelectOptionFilterPB> for MultiSelectTypeOptionPB {
fn apply_filter(&self, any_cell_data: AnyCellData, filter: &SelectOptionFilterPB) -> FlowyResult<bool> {
fn apply_filter(&self, any_cell_data: TypeCellData, filter: &SelectOptionFilterPB) -> FlowyResult<bool> {
if !any_cell_data.is_multi_select() {
return Ok(true);
}
@ -52,7 +52,7 @@ impl CellFilterOperation<SelectOptionFilterPB> for MultiSelectTypeOptionPB {
}
impl CellFilterOperation<SelectOptionFilterPB> for SingleSelectTypeOptionPB {
fn apply_filter(&self, any_cell_data: AnyCellData, filter: &SelectOptionFilterPB) -> FlowyResult<bool> {
fn apply_filter(&self, any_cell_data: TypeCellData, filter: &SelectOptionFilterPB) -> FlowyResult<bool> {
if !any_cell_data.is_single_select() {
return Ok(true);
}

View File

@ -1,5 +1,5 @@
use crate::entities::{TextFilterCondition, TextFilterPB};
use crate::services::cell::{AnyCellData, CellData, CellFilterOperation};
use crate::services::cell::{CellData, CellFilterOperation, TypeCellData};
use crate::services::field::{RichTextTypeOptionPB, TextCellData};
use flowy_error::FlowyResult;
@ -21,9 +21,9 @@ impl TextFilterPB {
}
impl CellFilterOperation<TextFilterPB> for RichTextTypeOptionPB {
fn apply_filter(&self, any_cell_data: AnyCellData, filter: &TextFilterPB) -> FlowyResult<bool> {
fn apply_filter(&self, any_cell_data: TypeCellData, filter: &TextFilterPB) -> FlowyResult<bool> {
if !any_cell_data.is_text() {
return Ok(true);
return Ok(false);
}
let cell_data: CellData<TextCellData> = any_cell_data.into();

View File

@ -1,10 +1,10 @@
use crate::entities::TextFilterPB;
use crate::services::cell::{AnyCellData, CellData, CellFilterOperation};
use crate::services::cell::{CellData, CellFilterOperation, TypeCellData};
use crate::services::field::{TextCellData, URLTypeOptionPB};
use flowy_error::FlowyResult;
impl CellFilterOperation<TextFilterPB> for URLTypeOptionPB {
fn apply_filter(&self, any_cell_data: AnyCellData, filter: &TextFilterPB) -> FlowyResult<bool> {
fn apply_filter(&self, any_cell_data: TypeCellData, filter: &TextFilterPB) -> FlowyResult<bool> {
if !any_cell_data.is_url() {
return Ok(true);
}

View File

@ -1,9 +1,9 @@
use crate::services::cell::AnyCellData;
use crate::services::cell::TypeCellData;
use grid_rev_model::CellRevision;
use std::str::FromStr;
pub fn get_cell_data(cell_rev: &CellRevision) -> String {
match AnyCellData::from_str(&cell_rev.data) {
match TypeCellData::from_str(&cell_rev.data) {
Ok(type_option) => type_option.data,
Err(_) => String::new(),
}

View File

@ -1,9 +1,8 @@
use crate::entities::{CheckboxFilterPB, DateFilterPB, FieldType, NumberFilterPB, SelectOptionFilterPB, TextFilterPB};
use crate::services::filter::FilterType;
use std::collections::HashMap;
#[derive(Default)]
#[derive(Default, Debug)]
pub(crate) struct FilterMap {
pub(crate) text_filter: HashMap<FilterType, TextFilterPB>,
pub(crate) url_filter: HashMap<FilterType, TextFilterPB>,
@ -18,6 +17,18 @@ impl FilterMap {
Self::default()
}
pub(crate) fn has_filter(&self, filter_type: &FilterType) -> bool {
match filter_type.field_type {
FieldType::RichText => self.text_filter.get(filter_type).is_some(),
FieldType::Number => self.number_filter.get(filter_type).is_some(),
FieldType::DateTime => self.date_filter.get(filter_type).is_some(),
FieldType::SingleSelect => self.select_option_filter.get(filter_type).is_some(),
FieldType::MultiSelect => self.select_option_filter.get(filter_type).is_some(),
FieldType::Checkbox => self.checkbox_filter.get(filter_type).is_some(),
FieldType::URL => self.url_filter.get(filter_type).is_some(),
}
}
pub(crate) fn is_empty(&self) -> bool {
if !self.text_filter.is_empty() {
return false;

View File

@ -1,21 +1,21 @@
use crate::dart_notification::{send_dart_notification, GridNotification};
use crate::entities::filter_entities::*;
use crate::entities::setting_entities::*;
use crate::entities::{FieldType, GridBlockChangesetPB};
use crate::services::cell::{AnyCellData, CellFilterOperation};
use crate::entities::FieldType;
use crate::services::cell::{CellFilterOperation, TypeCellData};
use crate::services::field::*;
use crate::services::filter::{FilterMap, FilterResult, FILTER_HANDLER_ID};
use crate::services::filter::{FilterChangeset, FilterMap, FilterResult, FilterResultNotification, FilterType};
use crate::services::row::GridBlock;
use crate::services::view_editor::{GridViewChanged, GridViewChangedNotifier};
use flowy_error::FlowyResult;
use flowy_task::{QualityOfService, Task, TaskContent, TaskDispatcher};
use grid_rev_model::{CellRevision, FieldId, FieldRevision, FieldTypeRevision, FilterRevision, RowRevision};
use grid_rev_model::{CellRevision, FieldId, FieldRevision, FilterRevision, RowRevision};
use lib_infra::future::Fut;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
type RowId = String;
pub trait GridViewFilterDelegate: Send + Sync + 'static {
pub trait FilterDelegate: Send + Sync + 'static {
fn get_filter_rev(&self, filter_id: FilterType) -> Fut<Vec<Arc<FilterRevision>>>;
fn get_field_rev(&self, field_id: &str) -> Fut<Option<Arc<FieldRevision>>>;
fn get_field_revs(&self, field_ids: Option<Vec<String>>) -> Fut<Vec<Arc<FieldRevision>>>;
@ -24,41 +24,48 @@ pub trait GridViewFilterDelegate: Send + Sync + 'static {
pub struct FilterController {
view_id: String,
delegate: Box<dyn GridViewFilterDelegate>,
handler_id: String,
delegate: Box<dyn FilterDelegate>,
filter_map: FilterMap,
result_by_row_id: HashMap<RowId, FilterResult>,
task_scheduler: Arc<RwLock<TaskDispatcher>>,
notifier: GridViewChangedNotifier,
}
impl FilterController {
pub async fn new<T>(
view_id: &str,
handler_id: &str,
delegate: T,
task_scheduler: Arc<RwLock<TaskDispatcher>>,
filter_revs: Vec<Arc<FilterRevision>>,
notifier: GridViewChangedNotifier,
) -> Self
where
T: GridViewFilterDelegate,
T: FilterDelegate,
{
let mut this = Self {
view_id: view_id.to_string(),
handler_id: handler_id.to_string(),
delegate: Box::new(delegate),
filter_map: FilterMap::new(),
result_by_row_id: HashMap::default(),
task_scheduler,
notifier,
};
this.load_filters(filter_revs).await;
this
}
pub async fn close(&self) {
self.task_scheduler.write().await.unregister_handler(FILTER_HANDLER_ID);
self.task_scheduler.write().await.unregister_handler(&self.handler_id);
}
#[tracing::instrument(name = "schedule_filter_task", level = "trace", skip(self))]
async fn gen_task(&mut self, predicate: &str) {
let task_id = self.task_scheduler.read().await.next_task_id();
let task = Task::new(
FILTER_HANDLER_ID,
&self.handler_id,
task_id,
TaskContent::Text(predicate.to_owned()),
QualityOfService::UserInteractive,
@ -71,17 +78,14 @@ impl FilterController {
return;
}
let field_rev_by_field_id = self.get_filter_revs_map().await;
let _ = row_revs
.iter()
.flat_map(|row_rev| {
filter_row(
row_rev,
&self.filter_map,
&mut self.result_by_row_id,
&field_rev_by_field_id,
)
})
.collect::<Vec<String>>();
row_revs.iter().for_each(|row_rev| {
let _ = filter_row(
row_rev,
&self.filter_map,
&mut self.result_by_row_id,
&field_rev_by_field_id,
);
});
row_revs.retain(|row_rev| {
self.result_by_row_id
@ -100,53 +104,40 @@ impl FilterController {
.collect::<HashMap<String, Arc<FieldRevision>>>()
}
#[tracing::instrument(name = "receive_task_result", level = "trace", skip_all, fields(filter_result), err)]
pub async fn process(&mut self, _predicate: &str) -> FlowyResult<()> {
let field_rev_by_field_id = self.get_filter_revs_map().await;
let mut changesets = vec![];
for block in self.delegate.get_blocks().await.into_iter() {
// The row_ids contains the row that its visibility was changed.
let row_ids = block
.row_revs
.iter()
.flat_map(|row_rev| {
filter_row(
row_rev,
&self.filter_map,
&mut self.result_by_row_id,
&field_rev_by_field_id,
)
})
.collect::<Vec<String>>();
let mut visible_rows = vec![];
let mut hide_rows = vec![];
let mut invisible_rows = vec![];
// Query the filter result from the cache
for row_id in row_ids {
if self
.result_by_row_id
.get(&row_id)
.map(|result| result.is_visible())
.unwrap_or(false)
{
visible_rows.push(row_id);
for row_rev in &block.row_revs {
let (row_id, is_visible) = filter_row(
row_rev,
&self.filter_map,
&mut self.result_by_row_id,
&field_rev_by_field_id,
);
if is_visible {
visible_rows.push(row_id)
} else {
hide_rows.push(row_id);
invisible_rows.push(row_id);
}
}
let changeset = GridBlockChangesetPB {
let notification = FilterResultNotification {
view_id: self.view_id.clone(),
block_id: block.block_id,
hide_rows,
invisible_rows,
visible_rows,
..Default::default()
};
// Save the changeset for each block
changesets.push(changeset);
tracing::Span::current().record("filter_result", &format!("{:?}", &notification).as_str());
let _ = self
.notifier
.send(GridViewChanged::DidReceiveFilterResult(notification));
}
self.notify(changesets).await;
Ok(())
}
@ -163,20 +154,13 @@ impl FilterController {
self.gen_task("").await;
}
async fn notify(&self, changesets: Vec<GridBlockChangesetPB>) {
for changeset in changesets {
send_dart_notification(&self.view_id, GridNotification::DidUpdateGridBlock)
.payload(changeset)
.send();
}
}
#[tracing::instrument(level = "trace", skip_all)]
async fn load_filters(&mut self, filter_revs: Vec<Arc<FilterRevision>>) {
for filter_rev in filter_revs {
if let Some(field_rev) = self.delegate.get_field_rev(&filter_rev.field_id).await {
let filter_type = FilterType::from(&field_rev);
let field_type: FieldType = field_rev.ty.into();
match &field_type {
tracing::trace!("Create filter with type: {:?}", filter_type);
match &filter_type.field_type {
FieldType::RichText => {
let _ = self
.filter_map
@ -220,12 +204,13 @@ impl FilterController {
}
/// Returns None if there is no change in this row after applying the filter
#[tracing::instrument(level = "trace", skip_all)]
fn filter_row(
row_rev: &Arc<RowRevision>,
filter_map: &FilterMap,
result_by_row_id: &mut HashMap<RowId, FilterResult>,
field_rev_by_field_id: &HashMap<FieldId, Arc<FieldRevision>>,
) -> Option<String> {
) -> (String, bool) {
// Create a filter result cache if it's not exist
let filter_result = result_by_row_id
.entry(row_rev.id.clone())
@ -234,31 +219,31 @@ fn filter_row(
// Iterate each cell of the row to check its visibility
for (field_id, field_rev) in field_rev_by_field_id {
let filter_type = FilterType::from(field_rev);
if !filter_map.has_filter(&filter_type) {
// tracing::trace!(
// "Can't find filter for filter type: {:?}. Current filters: {:?}",
// filter_type,
// filter_map
// );
continue;
}
let cell_rev = row_rev.cells.get(field_id);
// if the visibility of the cell_rew is changed, which means the visibility of the
// row is changed too.
if let Some(is_visible) = filter_cell(&filter_type, field_rev, filter_map, cell_rev) {
let prev_is_visible = filter_result.visible_by_filter_id.get(&filter_type).cloned();
filter_result.visible_by_filter_id.insert(filter_type, is_visible);
match prev_is_visible {
None => {
if !is_visible {
return Some(row_rev.id.clone());
}
}
Some(prev_is_visible) => {
if prev_is_visible != is_visible {
return Some(row_rev.id.clone());
}
}
}
return (row_rev.id.clone(), is_visible);
}
}
None
(row_rev.id.clone(), true)
}
// Return None if there is no change in this cell after applying the filter
// Returns None if there is no change in this cell after applying the filter
// Returns Some if the visibility of the cell is changed
#[tracing::instrument(level = "trace", skip_all)]
fn filter_cell(
filter_id: &FilterType,
field_rev: &Arc<FieldRevision>,
@ -266,9 +251,16 @@ fn filter_cell(
cell_rev: Option<&CellRevision>,
) -> Option<bool> {
let any_cell_data = match cell_rev {
None => AnyCellData::from_field_type(&filter_id.field_type),
Some(cell_rev) => AnyCellData::try_from(cell_rev).ok()?,
None => TypeCellData::from_field_type(&filter_id.field_type),
Some(cell_rev) => match TypeCellData::try_from(cell_rev) {
Ok(cell_data) => cell_data,
Err(err) => {
tracing::error!("Deserialize TypeCellData failed: {}", err);
TypeCellData::from_field_type(&filter_id.field_type)
}
},
};
tracing::trace!("filter cell: {:?}", any_cell_data);
let is_visible = match &filter_id.field_type {
FieldType::RichText => filter_map.text_filter.get(filter_id).and_then(|filter| {
@ -331,79 +323,3 @@ fn filter_cell(
is_visible
}
pub struct FilterChangeset {
insert_filter: Option<FilterType>,
delete_filter: Option<FilterType>,
}
impl FilterChangeset {
pub fn from_insert(filter_id: FilterType) -> Self {
Self {
insert_filter: Some(filter_id),
delete_filter: None,
}
}
pub fn from_delete(filter_id: FilterType) -> Self {
Self {
insert_filter: None,
delete_filter: Some(filter_id),
}
}
}
impl std::convert::From<&GridSettingChangesetParams> for FilterChangeset {
fn from(params: &GridSettingChangesetParams) -> Self {
let insert_filter = params.insert_filter.as_ref().map(|insert_filter_params| FilterType {
field_id: insert_filter_params.field_id.clone(),
field_type: insert_filter_params.field_type_rev.into(),
});
let delete_filter = params
.delete_filter
.as_ref()
.map(|delete_filter_params| delete_filter_params.filter_type.clone());
FilterChangeset {
insert_filter,
delete_filter,
}
}
}
#[derive(Hash, Eq, PartialEq, Clone)]
pub struct FilterType {
pub field_id: String,
pub field_type: FieldType,
}
impl FilterType {
pub fn field_type_rev(&self) -> FieldTypeRevision {
self.field_type.clone().into()
}
}
impl std::convert::From<&Arc<FieldRevision>> for FilterType {
fn from(rev: &Arc<FieldRevision>) -> Self {
Self {
field_id: rev.id.clone(),
field_type: rev.ty.into(),
}
}
}
impl std::convert::From<&CreateFilterParams> for FilterType {
fn from(params: &CreateFilterParams) -> Self {
let field_type: FieldType = params.field_type_rev.into();
Self {
field_id: params.field_id.clone(),
field_type,
}
}
}
impl std::convert::From<&DeleteFilterParams> for FilterType {
fn from(params: &DeleteFilterParams) -> Self {
params.filter_type.clone()
}
}

View File

@ -0,0 +1,87 @@
use crate::entities::{CreateFilterParams, DeleteFilterParams, FieldType, GridSettingChangesetParams};
use grid_rev_model::{FieldRevision, FieldTypeRevision};
use std::sync::Arc;
pub struct FilterChangeset {
pub(crate) insert_filter: Option<FilterType>,
pub(crate) delete_filter: Option<FilterType>,
}
impl FilterChangeset {
pub fn from_insert(filter_id: FilterType) -> Self {
Self {
insert_filter: Some(filter_id),
delete_filter: None,
}
}
pub fn from_delete(filter_id: FilterType) -> Self {
Self {
insert_filter: None,
delete_filter: Some(filter_id),
}
}
}
impl std::convert::From<&GridSettingChangesetParams> for FilterChangeset {
fn from(params: &GridSettingChangesetParams) -> Self {
let insert_filter = params.insert_filter.as_ref().map(|insert_filter_params| FilterType {
field_id: insert_filter_params.field_id.clone(),
field_type: insert_filter_params.field_type_rev.into(),
});
let delete_filter = params
.delete_filter
.as_ref()
.map(|delete_filter_params| delete_filter_params.filter_type.clone());
FilterChangeset {
insert_filter,
delete_filter,
}
}
}
#[derive(Hash, Eq, PartialEq, Debug, Clone)]
pub struct FilterType {
pub field_id: String,
pub field_type: FieldType,
}
impl FilterType {
pub fn field_type_rev(&self) -> FieldTypeRevision {
self.field_type.clone().into()
}
}
impl std::convert::From<&Arc<FieldRevision>> for FilterType {
fn from(rev: &Arc<FieldRevision>) -> Self {
Self {
field_id: rev.id.clone(),
field_type: rev.ty.into(),
}
}
}
impl std::convert::From<&CreateFilterParams> for FilterType {
fn from(params: &CreateFilterParams) -> Self {
let field_type: FieldType = params.field_type_rev.into();
Self {
field_id: params.field_id.clone(),
field_type,
}
}
}
impl std::convert::From<&DeleteFilterParams> for FilterType {
fn from(params: &DeleteFilterParams) -> Self {
params.filter_type.clone()
}
}
#[derive(Clone, Debug)]
pub struct FilterResultNotification {
pub view_id: String,
pub block_id: String,
pub visible_rows: Vec<String>,
pub invisible_rows: Vec<String>,
}

View File

@ -1,7 +1,9 @@
mod cache;
mod controller;
mod entities;
mod task;
pub(crate) use cache::*;
pub use controller::*;
pub use entities::*;
pub(crate) use task::*;

View File

@ -4,22 +4,27 @@ use lib_infra::future::BoxResultFuture;
use std::sync::Arc;
use tokio::sync::RwLock;
pub const FILTER_HANDLER_ID: &str = "grid_filter";
pub struct FilterTaskHandler {
handler_id: String,
filter_controller: Arc<RwLock<FilterController>>,
}
pub struct FilterTaskHandler(Arc<RwLock<FilterController>>);
impl FilterTaskHandler {
pub fn new(filter_controller: Arc<RwLock<FilterController>>) -> Self {
Self(filter_controller)
pub fn new(handler_id: String, filter_controller: Arc<RwLock<FilterController>>) -> Self {
Self {
handler_id,
filter_controller,
}
}
}
impl TaskHandler for FilterTaskHandler {
fn handler_id(&self) -> &str {
FILTER_HANDLER_ID
&self.handler_id
}
fn run(&self, content: TaskContent) -> BoxResultFuture<(), anyhow::Error> {
let filter_controller = self.0.clone();
let filter_controller = self.filter_controller.clone();
Box::pin(async move {
if let TaskContent::Text(predicate) = content {
let _ = filter_controller

View File

@ -1,4 +1,4 @@
use crate::dart_notification::{send_dart_notification, GridNotification};
use crate::dart_notification::{send_dart_notification, GridDartNotification};
use crate::entities::CellPathParams;
use crate::entities::*;
use crate::manager::GridUser;
@ -11,9 +11,9 @@ use crate::services::field::{
use crate::services::filter::FilterType;
use crate::services::grid_editor_trait_impl::GridViewEditorDelegateImpl;
use crate::services::grid_view_manager::GridViewManager;
use crate::services::persistence::block_index::BlockIndexCache;
use crate::services::row::{GridBlock, RowRevisionBuilder};
use crate::services::view_editor::{GridViewChanged, GridViewManager};
use bytes::Bytes;
use flowy_database::ConnectionPool;
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
@ -30,7 +30,7 @@ use lib_infra::future::{to_future, FutureResult};
use lib_ot::core::EmptyAttributes;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use tokio::sync::{broadcast, RwLock};
pub struct GridRevisionEditor {
pub grid_id: String,
@ -73,6 +73,7 @@ impl GridRevisionEditor {
// View manager
let view_manager = Arc::new(GridViewManager::new(grid_id.to_owned(), user.clone(), delegate).await?);
let editor = Arc::new(Self {
grid_id: grid_id.to_owned(),
user,
@ -96,7 +97,7 @@ impl GridRevisionEditor {
});
}
/// Save the type-option data to disk and send a `GridNotification::DidUpdateField` notification
/// Save the type-option data to disk and send a `GridDartNotification::DidUpdateField` notification
/// to dart side.
///
/// It will do nothing if the passed-in type_option_data is empty
@ -439,6 +440,10 @@ impl GridRevisionEditor {
Ok(())
}
pub async fn subscribe_view_changed(&self) -> broadcast::Receiver<GridViewChanged> {
self.view_manager.subscribe_view_changed().await
}
pub async fn duplicate_row(&self, _row_id: &str) -> FlowyResult<()> {
Ok(())
}
@ -811,7 +816,7 @@ impl GridRevisionEditor {
let notified_changeset = GridFieldChangesetPB::update(&self.grid_id, vec![updated_field.clone()]);
let _ = self.notify_did_update_grid(notified_changeset).await?;
send_dart_notification(field_id, GridNotification::DidUpdateField)
send_dart_notification(field_id, GridDartNotification::DidUpdateField)
.payload(updated_field)
.send();
}
@ -820,7 +825,7 @@ impl GridRevisionEditor {
}
async fn notify_did_update_grid(&self, changeset: GridFieldChangesetPB) -> FlowyResult<()> {
send_dart_notification(&self.grid_id, GridNotification::DidUpdateGridField)
send_dart_notification(&self.grid_id, GridDartNotification::DidUpdateGridField)
.payload(changeset)
.send();
Ok(())

View File

@ -1,6 +1,6 @@
use crate::services::block_manager::GridBlockManager;
use crate::services::grid_view_editor::GridViewEditorDelegate;
use crate::services::row::GridBlock;
use crate::services::view_editor::GridViewEditorDelegate;
use flowy_sync::client_grid::GridRevisionPad;
use flowy_task::TaskDispatcher;
use grid_rev_model::{FieldRevision, RowRevision};

View File

@ -7,9 +7,8 @@ pub mod field;
pub mod filter;
pub mod grid_editor;
mod grid_editor_trait_impl;
pub mod grid_view_editor;
pub mod grid_view_manager;
pub mod group;
pub mod persistence;
pub mod row;
pub mod setting;
pub mod view_editor;

View File

@ -0,0 +1,46 @@
use crate::dart_notification::{send_dart_notification, GridDartNotification};
use crate::entities::GridBlockChangesetPB;
use crate::services::filter::FilterResultNotification;
use async_stream::stream;
use futures::stream::StreamExt;
use tokio::sync::broadcast;
#[derive(Clone)]
pub enum GridViewChanged {
DidReceiveFilterResult(FilterResultNotification),
}
pub type GridViewChangedNotifier = broadcast::Sender<GridViewChanged>;
pub(crate) struct GridViewChangedReceiverRunner(pub(crate) Option<broadcast::Receiver<GridViewChanged>>);
impl GridViewChangedReceiverRunner {
pub(crate) async fn run(mut self) {
let mut receiver = self.0.take().expect("Only take once");
let stream = stream! {
loop {
match receiver.recv().await {
Ok(changed) => yield changed,
Err(_e) => break,
}
}
};
stream
.for_each(|changed| async {
match changed {
GridViewChanged::DidReceiveFilterResult(notification) => {
let changeset = GridBlockChangesetPB {
block_id: notification.block_id,
visible_rows: notification.visible_rows,
invisible_rows: notification.invisible_rows,
..Default::default()
};
send_dart_notification(&changeset.block_id, GridDartNotification::DidUpdateGridBlock)
.payload(changeset)
.send()
}
}
})
.await;
}
}

View File

@ -1,29 +1,23 @@
use crate::dart_notification::{send_dart_notification, GridNotification};
use crate::dart_notification::{send_dart_notification, GridDartNotification};
use crate::entities::*;
use crate::services::filter::{
FilterChangeset, FilterController, FilterTaskHandler, FilterType, GridViewFilterDelegate,
};
use crate::services::filter::{FilterChangeset, FilterController, FilterTaskHandler, FilterType};
use crate::services::group::{
default_group_configuration, find_group_field, make_group_controller, Group, GroupConfigurationReader,
GroupConfigurationWriter, GroupController, MoveGroupRowContext,
GroupController, MoveGroupRowContext,
};
use crate::services::row::GridBlock;
use bytes::Bytes;
use crate::services::view_editor::changed_notifier::GridViewChangedNotifier;
use crate::services::view_editor::trait_impl::*;
use flowy_database::ConnectionPool;
use flowy_error::{FlowyError, FlowyResult};
use flowy_http_model::revision::Revision;
use flowy_revision::{
RevisionCloudService, RevisionManager, RevisionMergeable, RevisionObjectDeserializer, RevisionObjectSerializer,
};
use flowy_error::FlowyResult;
use flowy_revision::RevisionManager;
use flowy_sync::client_grid::{GridViewRevisionChangeset, GridViewRevisionPad};
use flowy_sync::util::make_operations_from_revisions;
use flowy_task::TaskDispatcher;
use grid_rev_model::{
gen_grid_filter_id, FieldRevision, FieldTypeRevision, FilterRevision, GroupConfigurationRevision, RowChangeset,
RowRevision,
};
use lib_infra::future::{to_future, Fut, FutureResult};
use lib_ot::core::EmptyAttributes;
use grid_rev_model::{gen_grid_filter_id, FieldRevision, FieldTypeRevision, FilterRevision, RowChangeset, RowRevision};
use lib_infra::future::Fut;
use lib_infra::ref_map::RefCountValue;
use nanoid::nanoid;
use std::future::Future;
use std::sync::Arc;
use tokio::sync::RwLock;
@ -41,7 +35,6 @@ pub trait GridViewEditorDelegate: Send + Sync + 'static {
fn get_task_scheduler(&self) -> Arc<RwLock<TaskDispatcher>>;
}
#[allow(dead_code)]
pub struct GridViewRevisionEditor {
user_id: String,
view_id: String,
@ -51,13 +44,15 @@ pub struct GridViewRevisionEditor {
group_controller: Arc<RwLock<Box<dyn GroupController>>>,
filter_controller: Arc<RwLock<FilterController>>,
}
impl GridViewRevisionEditor {
#[tracing::instrument(level = "trace", skip_all, err)]
pub(crate) async fn new(
pub async fn new(
user_id: &str,
token: &str,
view_id: String,
delegate: Arc<dyn GridViewEditorDelegate>,
notifier: GridViewChangedNotifier,
mut rev_manager: RevisionManager<Arc<ConnectionPool>>,
) -> FlowyResult<Self> {
let cloud = Arc::new(GridViewRevisionCloudService {
@ -77,7 +72,7 @@ impl GridViewRevisionEditor {
let user_id = user_id.to_owned();
let group_controller = Arc::new(RwLock::new(group_controller));
let filter_controller = make_filter_controller(&view_id, delegate.clone(), pad.clone()).await;
let filter_controller = make_filter_controller(&view_id, delegate.clone(), notifier.clone(), pad.clone()).await;
Ok(Self {
pad,
user_id,
@ -89,21 +84,25 @@ impl GridViewRevisionEditor {
})
}
pub(crate) async fn close(&self) {
self.filter_controller.read().await.close().await;
#[tracing::instrument(name = "close grid view editor", level = "trace", skip_all)]
pub fn close(&self) {
let filter_controller = self.filter_controller.clone();
tokio::spawn(async move {
filter_controller.read().await.close().await;
});
}
pub(crate) async fn filter_rows(&self, _block_id: &str, mut rows: Vec<Arc<RowRevision>>) -> Vec<Arc<RowRevision>> {
pub async fn filter_rows(&self, _block_id: &str, mut rows: Vec<Arc<RowRevision>>) -> Vec<Arc<RowRevision>> {
self.filter_controller.write().await.filter_row_revs(&mut rows).await;
rows
}
pub(crate) async fn duplicate_view_data(&self) -> FlowyResult<String> {
pub async fn duplicate_view_data(&self) -> FlowyResult<String> {
let json_str = self.pad.read().await.json_str()?;
Ok(json_str)
}
pub(crate) async fn will_create_view_row(&self, row_rev: &mut RowRevision, params: &CreateRowParams) {
pub async fn will_create_view_row(&self, row_rev: &mut RowRevision, params: &CreateRowParams) {
if params.group_id.is_none() {
return;
}
@ -116,7 +115,7 @@ impl GridViewRevisionEditor {
.await;
}
pub(crate) async fn did_create_view_row(&self, row_pb: &RowPB, params: &CreateRowParams) {
pub async fn did_create_view_row(&self, row_pb: &RowPB, params: &CreateRowParams) {
// Send the group notification if the current view has groups
match params.group_id.as_ref() {
None => {}
@ -139,7 +138,7 @@ impl GridViewRevisionEditor {
}
#[tracing::instrument(level = "trace", skip_all)]
pub(crate) async fn did_delete_view_row(&self, row_rev: &RowRevision) {
pub async fn did_delete_view_row(&self, row_rev: &RowRevision) {
// Send the group notification if the current view has groups;
let changesets = self
.mut_group_controller(|group_controller, field_rev| {
@ -155,7 +154,7 @@ impl GridViewRevisionEditor {
}
}
pub(crate) async fn did_update_view_cell(&self, row_rev: &RowRevision) {
pub async fn did_update_view_cell(&self, row_rev: &RowRevision) {
let changesets = self
.mut_group_controller(|group_controller, field_rev| {
group_controller.did_update_group_row(row_rev, &field_rev)
@ -169,7 +168,7 @@ impl GridViewRevisionEditor {
}
}
pub(crate) async fn move_view_group_row(
pub async fn move_view_group_row(
&self,
row_rev: &RowRevision,
row_changeset: &mut RowChangeset,
@ -195,7 +194,7 @@ impl GridViewRevisionEditor {
}
/// Only call once after grid view editor initialized
#[tracing::instrument(level = "trace", skip(self))]
pub(crate) async fn load_view_groups(&self) -> FlowyResult<Vec<GroupPB>> {
pub async fn load_view_groups(&self) -> FlowyResult<Vec<GroupPB>> {
let groups = self
.group_controller
.read()
@ -209,7 +208,7 @@ impl GridViewRevisionEditor {
}
#[tracing::instrument(level = "trace", skip(self), err)]
pub(crate) async fn move_view_group(&self, params: MoveGroupParams) -> FlowyResult<()> {
pub async fn move_view_group(&self, params: MoveGroupParams) -> FlowyResult<()> {
let _ = self
.group_controller
.write()
@ -237,22 +236,22 @@ impl GridViewRevisionEditor {
Ok(())
}
pub(crate) async fn group_id(&self) -> String {
pub async fn group_id(&self) -> String {
self.group_controller.read().await.field_id().to_string()
}
pub(crate) async fn get_view_setting(&self) -> GridSettingPB {
pub async fn get_view_setting(&self) -> GridSettingPB {
let field_revs = self.delegate.get_field_revs(None).await;
let grid_setting = make_grid_setting(&*self.pad.read().await, &field_revs);
grid_setting
}
pub(crate) async fn get_all_view_filters(&self) -> Vec<Arc<FilterRevision>> {
pub async fn get_all_view_filters(&self) -> Vec<Arc<FilterRevision>> {
let field_revs = self.delegate.get_field_revs(None).await;
self.pad.read().await.get_all_filters(&field_revs)
}
pub(crate) async fn get_view_filters(&self, filter_type: &FilterType) -> Vec<Arc<FilterRevision>> {
pub async fn get_view_filters(&self, filter_type: &FilterType) -> Vec<Arc<FilterRevision>> {
let field_type_rev: FieldTypeRevision = filter_type.field_type.clone().into();
self.pad
.read()
@ -262,7 +261,7 @@ impl GridViewRevisionEditor {
/// Initialize new group when grouping by a new field
///
pub(crate) async fn initialize_new_group(&self, params: InsertGroupParams) -> FlowyResult<()> {
pub async fn initialize_new_group(&self, params: InsertGroupParams) -> FlowyResult<()> {
if let Some(field_rev) = self.delegate.get_field_rev(&params.field_id).await {
let _ = self
.modify(|pad| {
@ -283,7 +282,7 @@ impl GridViewRevisionEditor {
Ok(())
}
pub(crate) async fn delete_view_group(&self, params: DeleteGroupParams) -> FlowyResult<()> {
pub async fn delete_view_group(&self, params: DeleteGroupParams) -> FlowyResult<()> {
self.modify(|pad| {
let changeset = pad.delete_group(&params.group_id, &params.field_id, &params.field_type_rev)?;
Ok(changeset)
@ -291,7 +290,8 @@ impl GridViewRevisionEditor {
.await
}
pub(crate) async fn insert_view_filter(&self, params: CreateFilterParams) -> FlowyResult<()> {
#[tracing::instrument(level = "trace", skip(self), err)]
pub async fn insert_view_filter(&self, params: CreateFilterParams) -> FlowyResult<()> {
let filter_type = FilterType::from(&params);
let filter_rev = FilterRevision {
id: gen_grid_filter_id(),
@ -319,7 +319,8 @@ impl GridViewRevisionEditor {
Ok(())
}
pub(crate) async fn delete_view_filter(&self, params: DeleteFilterParams) -> FlowyResult<()> {
#[tracing::instrument(level = "trace", skip(self), err)]
pub async fn delete_view_filter(&self, params: DeleteFilterParams) -> FlowyResult<()> {
let filter_type = params.filter_type;
let field_type_rev = filter_type.field_type_rev();
let filters = self
@ -347,7 +348,7 @@ impl GridViewRevisionEditor {
}
#[tracing::instrument(level = "trace", skip_all, err)]
pub(crate) async fn did_update_view_field_type_option(&self, field_id: &str) -> FlowyResult<()> {
pub async fn did_update_view_field_type_option(&self, field_id: &str) -> FlowyResult<()> {
if let Some(field_rev) = self.delegate.get_field_rev(field_id).await {
let filter_type = FilterType::from(&field_rev);
let filter_changeset = FilterChangeset::from_insert(filter_type);
@ -367,7 +368,7 @@ impl GridViewRevisionEditor {
/// * `field_id`:
///
#[tracing::instrument(level = "debug", skip_all, err)]
pub(crate) async fn group_by_view_field(&self, field_id: &str) -> FlowyResult<()> {
pub async fn group_by_view_field(&self, field_id: &str) -> FlowyResult<()> {
if let Some(field_rev) = self.delegate.get_field_rev(field_id).await {
let row_revs = self.delegate.get_row_revs().await;
let new_group_controller = new_group_controller_with_field_rev(
@ -395,7 +396,7 @@ impl GridViewRevisionEditor {
debug_assert!(!changeset.is_empty());
if !changeset.is_empty() {
send_dart_notification(&changeset.view_id, GridNotification::DidGroupByNewField)
send_dart_notification(&changeset.view_id, GridDartNotification::DidGroupByNewField)
.payload(changeset)
.send();
}
@ -405,25 +406,25 @@ impl GridViewRevisionEditor {
async fn notify_did_update_setting(&self) {
let setting = self.get_view_setting().await;
send_dart_notification(&self.view_id, GridNotification::DidUpdateGridSetting)
send_dart_notification(&self.view_id, GridDartNotification::DidUpdateGridSetting)
.payload(setting)
.send();
}
pub async fn notify_did_update_group_rows(&self, payload: GroupRowsNotificationPB) {
send_dart_notification(&payload.group_id, GridNotification::DidUpdateGroup)
send_dart_notification(&payload.group_id, GridDartNotification::DidUpdateGroup)
.payload(payload)
.send();
}
pub async fn notify_did_update_filter(&self, changeset: FilterChangesetNotificationPB) {
send_dart_notification(&changeset.view_id, GridNotification::DidUpdateFilter)
send_dart_notification(&changeset.view_id, GridDartNotification::DidUpdateFilter)
.payload(changeset)
.send();
}
async fn notify_did_update_view(&self, changeset: GroupViewChangesetPB) {
send_dart_notification(&self.view_id, GridNotification::DidUpdateGroupView)
send_dart_notification(&self.view_id, GridDartNotification::DidUpdateGroupView)
.payload(changeset)
.send();
}
@ -473,6 +474,12 @@ impl GridViewRevisionEditor {
}
}
impl RefCountValue for GridViewRevisionEditor {
fn did_remove(&self) {
self.close();
}
}
async fn new_group_controller(
user_id: String,
view_id: String,
@ -521,6 +528,7 @@ async fn new_group_controller_with_field_rev(
async fn make_filter_controller(
view_id: &str,
delegate: Arc<dyn GridViewEditorDelegate>,
notifier: GridViewChangedNotifier,
pad: Arc<RwLock<GridViewRevisionPad>>,
) -> Arc<RwLock<FilterController>> {
let field_revs = delegate.get_field_revs(None).await;
@ -530,160 +538,26 @@ async fn make_filter_controller(
editor_delegate: delegate.clone(),
view_revision_pad: pad,
};
let filter_controller = FilterController::new(view_id, filter_delegate, task_scheduler.clone(), filter_revs).await;
let handler_id = gen_handler_id();
let filter_controller = FilterController::new(
view_id,
&handler_id,
filter_delegate,
task_scheduler.clone(),
filter_revs,
notifier,
)
.await;
let filter_controller = Arc::new(RwLock::new(filter_controller));
task_scheduler
.write()
.await
.register_handler(FilterTaskHandler::new(filter_controller.clone()));
.register_handler(FilterTaskHandler::new(handler_id, filter_controller.clone()));
filter_controller
}
async fn apply_change(
_user_id: &str,
rev_manager: Arc<RevisionManager<Arc<ConnectionPool>>>,
change: GridViewRevisionChangeset,
) -> FlowyResult<()> {
let GridViewRevisionChangeset { operations: delta, md5 } = change;
let (base_rev_id, rev_id) = rev_manager.next_rev_id_pair();
let delta_data = delta.json_bytes();
let revision = Revision::new(&rev_manager.object_id, base_rev_id, rev_id, delta_data, md5);
let _ = rev_manager.add_local_revision(&revision).await?;
Ok(())
}
struct GridViewRevisionCloudService {
#[allow(dead_code)]
token: String,
}
impl RevisionCloudService for GridViewRevisionCloudService {
fn fetch_object(&self, _user_id: &str, _object_id: &str) -> FutureResult<Vec<Revision>, FlowyError> {
FutureResult::new(async move { Ok(vec![]) })
}
}
pub struct GridViewRevisionSerde();
impl RevisionObjectDeserializer for GridViewRevisionSerde {
type Output = GridViewRevisionPad;
fn deserialize_revisions(object_id: &str, revisions: Vec<Revision>) -> FlowyResult<Self::Output> {
let pad = GridViewRevisionPad::from_revisions(object_id, revisions)?;
Ok(pad)
}
}
impl RevisionObjectSerializer for GridViewRevisionSerde {
fn combine_revisions(revisions: Vec<Revision>) -> FlowyResult<Bytes> {
let operations = make_operations_from_revisions::<EmptyAttributes>(revisions)?;
Ok(operations.json_bytes())
}
}
pub struct GridViewRevisionCompress();
impl RevisionMergeable for GridViewRevisionCompress {
fn combine_revisions(&self, revisions: Vec<Revision>) -> FlowyResult<Bytes> {
GridViewRevisionSerde::combine_revisions(revisions)
}
}
struct GroupConfigurationReaderImpl(Arc<RwLock<GridViewRevisionPad>>);
impl GroupConfigurationReader for GroupConfigurationReaderImpl {
fn get_configuration(&self) -> Fut<Option<Arc<GroupConfigurationRevision>>> {
let view_pad = self.0.clone();
to_future(async move {
let mut groups = view_pad.read().await.get_all_groups();
if groups.is_empty() {
None
} else {
debug_assert_eq!(groups.len(), 1);
Some(groups.pop().unwrap())
}
})
}
}
struct GroupConfigurationWriterImpl {
user_id: String,
rev_manager: Arc<RevisionManager<Arc<ConnectionPool>>>,
view_pad: Arc<RwLock<GridViewRevisionPad>>,
}
impl GroupConfigurationWriter for GroupConfigurationWriterImpl {
fn save_configuration(
&self,
field_id: &str,
field_type: FieldTypeRevision,
group_configuration: GroupConfigurationRevision,
) -> Fut<FlowyResult<()>> {
let user_id = self.user_id.clone();
let rev_manager = self.rev_manager.clone();
let view_pad = self.view_pad.clone();
let field_id = field_id.to_owned();
to_future(async move {
let changeset = view_pad.write().await.insert_or_update_group_configuration(
&field_id,
&field_type,
group_configuration,
)?;
if let Some(changeset) = changeset {
let _ = apply_change(&user_id, rev_manager, changeset).await?;
}
Ok(())
})
}
}
pub fn make_grid_setting(view_pad: &GridViewRevisionPad, field_revs: &[Arc<FieldRevision>]) -> GridSettingPB {
let layout_type: GridLayout = view_pad.layout.clone().into();
let filter_configurations = view_pad
.get_all_filters(field_revs)
.into_iter()
.map(|filter| FilterPB::from(filter.as_ref()))
.collect::<Vec<FilterPB>>();
let group_configurations = view_pad
.get_groups_by_field_revs(field_revs)
.into_iter()
.map(|group| GridGroupConfigurationPB::from(group.as_ref()))
.collect::<Vec<GridGroupConfigurationPB>>();
GridSettingPB {
layouts: GridLayoutPB::all(),
layout_type,
filter_configurations: filter_configurations.into(),
group_configurations: group_configurations.into(),
}
}
struct GridViewFilterDelegateImpl {
editor_delegate: Arc<dyn GridViewEditorDelegate>,
view_revision_pad: Arc<RwLock<GridViewRevisionPad>>,
}
impl GridViewFilterDelegate for GridViewFilterDelegateImpl {
fn get_filter_rev(&self, filter_id: FilterType) -> Fut<Vec<Arc<FilterRevision>>> {
let pad = self.view_revision_pad.clone();
to_future(async move {
let field_type_rev: FieldTypeRevision = filter_id.field_type.into();
pad.read().await.get_filters(&filter_id.field_id, &field_type_rev)
})
}
fn get_field_rev(&self, field_id: &str) -> Fut<Option<Arc<FieldRevision>>> {
self.editor_delegate.get_field_rev(field_id)
}
fn get_field_revs(&self, field_ids: Option<Vec<String>>) -> Fut<Vec<Arc<FieldRevision>>> {
self.editor_delegate.get_field_revs(field_ids)
}
fn get_blocks(&self) -> Fut<Vec<GridBlock>> {
self.editor_delegate.get_blocks()
}
fn gen_handler_id() -> String {
nanoid!(10)
}
#[cfg(test)]

View File

@ -3,49 +3,54 @@ use crate::entities::{
MoveGroupParams, RepeatedGridGroupPB, RowPB,
};
use crate::manager::GridUser;
use crate::services::grid_view_editor::{GridViewEditorDelegate, GridViewRevisionCompress, GridViewRevisionEditor};
use crate::services::filter::FilterType;
use crate::services::persistence::rev_sqlite::SQLiteGridViewRevisionPersistence;
use dashmap::DashMap;
use crate::services::view_editor::changed_notifier::*;
use crate::services::view_editor::trait_impl::GridViewRevisionCompress;
use crate::services::view_editor::{GridViewEditorDelegate, GridViewRevisionEditor};
use flowy_database::ConnectionPool;
use flowy_error::FlowyResult;
use flowy_revision::{
RevisionManager, RevisionPersistence, RevisionPersistenceConfiguration, SQLiteRevisionSnapshotPersistence,
};
use crate::services::filter::FilterType;
use grid_rev_model::{FilterRevision, RowChangeset, RowRevision};
use lib_infra::future::Fut;
use lib_infra::ref_map::RefCountHashMap;
use std::sync::Arc;
use tokio::sync::{broadcast, RwLock};
type ViewId = String;
pub(crate) struct GridViewManager {
pub struct GridViewManager {
grid_id: String,
user: Arc<dyn GridUser>,
delegate: Arc<dyn GridViewEditorDelegate>,
view_editors: DashMap<ViewId, Arc<GridViewRevisionEditor>>,
view_editors: RwLock<RefCountHashMap<Arc<GridViewRevisionEditor>>>,
pub notifier: broadcast::Sender<GridViewChanged>,
}
impl GridViewManager {
pub(crate) async fn new(
pub async fn new(
grid_id: String,
user: Arc<dyn GridUser>,
delegate: Arc<dyn GridViewEditorDelegate>,
) -> FlowyResult<Self> {
let (notifier, _) = broadcast::channel(100);
tokio::spawn(GridViewChangedReceiverRunner(Some(notifier.subscribe())).run());
let view_editors = RwLock::new(RefCountHashMap::default());
Ok(Self {
grid_id,
user,
delegate,
view_editors: DashMap::default(),
view_editors,
notifier,
})
}
pub(crate) async fn close(&self, _view_id: &str) {
if let Ok(editor) = self.get_default_view_editor().await {
let _ = editor.close().await;
}
pub async fn close(&self, view_id: &str) {
self.view_editors.write().await.remove(view_id);
}
pub async fn subscribe_view_changed(&self) -> broadcast::Receiver<GridViewChanged> {
self.notifier.subscribe()
}
pub async fn filter_rows(&self, block_id: &str, rows: Vec<Arc<RowRevision>>) -> FlowyResult<Vec<Arc<RowRevision>>> {
@ -54,94 +59,94 @@ impl GridViewManager {
Ok(rows)
}
pub(crate) async fn duplicate_grid_view(&self) -> FlowyResult<String> {
pub async fn duplicate_grid_view(&self) -> FlowyResult<String> {
let editor = self.get_default_view_editor().await?;
let view_data = editor.duplicate_view_data().await?;
Ok(view_data)
}
/// When the row was created, we may need to modify the [RowRevision] according to the [CreateRowParams].
pub(crate) async fn will_create_row(&self, row_rev: &mut RowRevision, params: &CreateRowParams) {
for view_editor in self.view_editors.iter() {
pub async fn will_create_row(&self, row_rev: &mut RowRevision, params: &CreateRowParams) {
for view_editor in self.view_editors.read().await.values() {
view_editor.will_create_view_row(row_rev, params).await;
}
}
/// Notify the view that the row was created. For the moment, the view is just sending notifications.
pub(crate) async fn did_create_row(&self, row_pb: &RowPB, params: &CreateRowParams) {
for view_editor in self.view_editors.iter() {
pub async fn did_create_row(&self, row_pb: &RowPB, params: &CreateRowParams) {
for view_editor in self.view_editors.read().await.values() {
view_editor.did_create_view_row(row_pb, params).await;
}
}
/// Insert/Delete the group's row if the corresponding cell data was changed.
pub(crate) async fn did_update_cell(&self, row_id: &str) {
pub async fn did_update_cell(&self, row_id: &str) {
match self.delegate.get_row_rev(row_id).await {
None => {
tracing::warn!("Can not find the row in grid view");
}
Some(row_rev) => {
for view_editor in self.view_editors.iter() {
for view_editor in self.view_editors.read().await.values() {
view_editor.did_update_view_cell(&row_rev).await;
}
}
}
}
pub(crate) async fn group_by_field(&self, field_id: &str) -> FlowyResult<()> {
pub async fn group_by_field(&self, field_id: &str) -> FlowyResult<()> {
let view_editor = self.get_default_view_editor().await?;
let _ = view_editor.group_by_view_field(field_id).await?;
Ok(())
}
pub(crate) async fn did_delete_row(&self, row_rev: Arc<RowRevision>) {
for view_editor in self.view_editors.iter() {
pub async fn did_delete_row(&self, row_rev: Arc<RowRevision>) {
for view_editor in self.view_editors.read().await.values() {
view_editor.did_delete_view_row(&row_rev).await;
}
}
pub(crate) async fn get_setting(&self) -> FlowyResult<GridSettingPB> {
pub async fn get_setting(&self) -> FlowyResult<GridSettingPB> {
let view_editor = self.get_default_view_editor().await?;
Ok(view_editor.get_view_setting().await)
}
pub(crate) async fn get_all_filters(&self) -> FlowyResult<Vec<Arc<FilterRevision>>> {
pub async fn get_all_filters(&self) -> FlowyResult<Vec<Arc<FilterRevision>>> {
let view_editor = self.get_default_view_editor().await?;
Ok(view_editor.get_all_view_filters().await)
}
pub(crate) async fn get_filters(&self, filter_id: &FilterType) -> FlowyResult<Vec<Arc<FilterRevision>>> {
pub async fn get_filters(&self, filter_id: &FilterType) -> FlowyResult<Vec<Arc<FilterRevision>>> {
let view_editor = self.get_default_view_editor().await?;
Ok(view_editor.get_view_filters(filter_id).await)
}
pub(crate) async fn insert_or_update_filter(&self, params: CreateFilterParams) -> FlowyResult<()> {
pub async fn insert_or_update_filter(&self, params: CreateFilterParams) -> FlowyResult<()> {
let view_editor = self.get_default_view_editor().await?;
view_editor.insert_view_filter(params).await
}
pub(crate) async fn delete_filter(&self, params: DeleteFilterParams) -> FlowyResult<()> {
pub async fn delete_filter(&self, params: DeleteFilterParams) -> FlowyResult<()> {
let view_editor = self.get_default_view_editor().await?;
view_editor.delete_view_filter(params).await
}
pub(crate) async fn load_groups(&self) -> FlowyResult<RepeatedGridGroupPB> {
pub async fn load_groups(&self) -> FlowyResult<RepeatedGridGroupPB> {
let view_editor = self.get_default_view_editor().await?;
let groups = view_editor.load_view_groups().await?;
Ok(RepeatedGridGroupPB { items: groups })
}
pub(crate) async fn insert_or_update_group(&self, params: InsertGroupParams) -> FlowyResult<()> {
pub async fn insert_or_update_group(&self, params: InsertGroupParams) -> FlowyResult<()> {
let view_editor = self.get_default_view_editor().await?;
view_editor.initialize_new_group(params).await
}
pub(crate) async fn delete_group(&self, params: DeleteGroupParams) -> FlowyResult<()> {
pub async fn delete_group(&self, params: DeleteGroupParams) -> FlowyResult<()> {
let view_editor = self.get_default_view_editor().await?;
view_editor.delete_view_group(params).await
}
pub(crate) async fn move_group(&self, params: MoveGroupParams) -> FlowyResult<()> {
pub async fn move_group(&self, params: MoveGroupParams) -> FlowyResult<()> {
let view_editor = self.get_default_view_editor().await?;
let _ = view_editor.move_view_group(params).await?;
Ok(())
@ -150,7 +155,7 @@ impl GridViewManager {
/// It may generate a RowChangeset when the Row was moved from one group to another.
/// The return value, [RowChangeset], contains the changes made by the groups.
///
pub(crate) async fn move_group_row(
pub async fn move_group_row(
&self,
row_rev: Arc<RowRevision>,
to_group_id: String,
@ -182,7 +187,7 @@ impl GridViewManager {
/// * `field_id`: the id of the field in current view
///
#[tracing::instrument(level = "trace", skip(self), err)]
pub(crate) async fn did_update_view_field_type_option(&self, field_id: &str) -> FlowyResult<()> {
pub async fn did_update_view_field_type_option(&self, field_id: &str) -> FlowyResult<()> {
let view_editor = self.get_default_view_editor().await?;
if view_editor.group_id().await == field_id {
let _ = view_editor.group_by_view_field(field_id).await?;
@ -192,34 +197,38 @@ impl GridViewManager {
Ok(())
}
pub(crate) async fn get_view_editor(&self, view_id: &str) -> FlowyResult<Arc<GridViewRevisionEditor>> {
pub async fn get_view_editor(&self, view_id: &str) -> FlowyResult<Arc<GridViewRevisionEditor>> {
debug_assert!(!view_id.is_empty());
match self.view_editors.get(view_id) {
None => {
let editor = Arc::new(make_view_editor(&self.user, view_id, self.delegate.clone()).await?);
self.view_editors.insert(view_id.to_owned(), editor.clone());
Ok(editor)
}
Some(view_editor) => Ok(view_editor.clone()),
if let Some(editor) = self.view_editors.read().await.get(view_id) {
return Ok(editor);
}
tracing::trace!("{:p} create view_editor", self);
let mut view_editors = self.view_editors.write().await;
let editor = Arc::new(self.make_view_editor(view_id).await?);
view_editors.insert(view_id.to_owned(), editor.clone());
Ok(editor)
}
async fn get_default_view_editor(&self) -> FlowyResult<Arc<GridViewRevisionEditor>> {
self.get_view_editor(&self.grid_id).await
}
}
async fn make_view_editor(
user: &Arc<dyn GridUser>,
view_id: &str,
delegate: Arc<dyn GridViewEditorDelegate>,
) -> FlowyResult<GridViewRevisionEditor> {
let rev_manager = make_grid_view_rev_manager(user, view_id).await?;
let user_id = user.user_id()?;
let token = user.token()?;
let view_id = view_id.to_owned();
async fn make_view_editor(&self, view_id: &str) -> FlowyResult<GridViewRevisionEditor> {
let rev_manager = make_grid_view_rev_manager(&self.user, view_id).await?;
let user_id = self.user.user_id()?;
let token = self.user.token()?;
let view_id = view_id.to_owned();
GridViewRevisionEditor::new(&user_id, &token, view_id, delegate, rev_manager).await
GridViewRevisionEditor::new(
&user_id,
&token,
view_id,
self.delegate.clone(),
self.notifier.clone(),
rev_manager,
)
.await
}
}
pub async fn make_grid_view_rev_manager(

View File

@ -0,0 +1,8 @@
mod changed_notifier;
mod editor;
mod editor_manager;
mod trait_impl;
pub use changed_notifier::*;
pub use editor::*;
pub use editor_manager::*;

View File

@ -0,0 +1,166 @@
use crate::entities::{FilterPB, GridGroupConfigurationPB, GridLayout, GridLayoutPB, GridSettingPB};
use crate::services::filter::{FilterDelegate, FilterType};
use crate::services::group::{GroupConfigurationReader, GroupConfigurationWriter};
use crate::services::row::GridBlock;
use crate::services::view_editor::GridViewEditorDelegate;
use bytes::Bytes;
use flowy_database::ConnectionPool;
use flowy_error::{FlowyError, FlowyResult};
use flowy_http_model::revision::Revision;
use flowy_revision::{
RevisionCloudService, RevisionManager, RevisionMergeable, RevisionObjectDeserializer, RevisionObjectSerializer,
};
use flowy_sync::client_grid::{GridViewRevisionChangeset, GridViewRevisionPad};
use flowy_sync::util::make_operations_from_revisions;
use grid_rev_model::{FieldRevision, FieldTypeRevision, FilterRevision, GroupConfigurationRevision};
use lib_infra::future::{to_future, Fut, FutureResult};
use lib_ot::core::EmptyAttributes;
use std::sync::Arc;
use tokio::sync::RwLock;
pub(crate) struct GridViewRevisionCloudService {
#[allow(dead_code)]
pub(crate) token: String,
}
impl RevisionCloudService for GridViewRevisionCloudService {
fn fetch_object(&self, _user_id: &str, _object_id: &str) -> FutureResult<Vec<Revision>, FlowyError> {
FutureResult::new(async move { Ok(vec![]) })
}
}
pub(crate) struct GridViewRevisionSerde();
impl RevisionObjectDeserializer for GridViewRevisionSerde {
type Output = GridViewRevisionPad;
fn deserialize_revisions(object_id: &str, revisions: Vec<Revision>) -> FlowyResult<Self::Output> {
let pad = GridViewRevisionPad::from_revisions(object_id, revisions)?;
Ok(pad)
}
}
impl RevisionObjectSerializer for GridViewRevisionSerde {
fn combine_revisions(revisions: Vec<Revision>) -> FlowyResult<Bytes> {
let operations = make_operations_from_revisions::<EmptyAttributes>(revisions)?;
Ok(operations.json_bytes())
}
}
pub(crate) struct GridViewRevisionCompress();
impl RevisionMergeable for GridViewRevisionCompress {
fn combine_revisions(&self, revisions: Vec<Revision>) -> FlowyResult<Bytes> {
GridViewRevisionSerde::combine_revisions(revisions)
}
}
pub(crate) struct GroupConfigurationReaderImpl(pub(crate) Arc<RwLock<GridViewRevisionPad>>);
impl GroupConfigurationReader for GroupConfigurationReaderImpl {
fn get_configuration(&self) -> Fut<Option<Arc<GroupConfigurationRevision>>> {
let view_pad = self.0.clone();
to_future(async move {
let mut groups = view_pad.read().await.get_all_groups();
if groups.is_empty() {
None
} else {
debug_assert_eq!(groups.len(), 1);
Some(groups.pop().unwrap())
}
})
}
}
pub(crate) struct GroupConfigurationWriterImpl {
pub(crate) user_id: String,
pub(crate) rev_manager: Arc<RevisionManager<Arc<ConnectionPool>>>,
pub(crate) view_pad: Arc<RwLock<GridViewRevisionPad>>,
}
impl GroupConfigurationWriter for GroupConfigurationWriterImpl {
fn save_configuration(
&self,
field_id: &str,
field_type: FieldTypeRevision,
group_configuration: GroupConfigurationRevision,
) -> Fut<FlowyResult<()>> {
let user_id = self.user_id.clone();
let rev_manager = self.rev_manager.clone();
let view_pad = self.view_pad.clone();
let field_id = field_id.to_owned();
to_future(async move {
let changeset = view_pad.write().await.insert_or_update_group_configuration(
&field_id,
&field_type,
group_configuration,
)?;
if let Some(changeset) = changeset {
let _ = apply_change(&user_id, rev_manager, changeset).await?;
}
Ok(())
})
}
}
pub(crate) async fn apply_change(
_user_id: &str,
rev_manager: Arc<RevisionManager<Arc<ConnectionPool>>>,
change: GridViewRevisionChangeset,
) -> FlowyResult<()> {
let GridViewRevisionChangeset { operations: delta, md5 } = change;
let (base_rev_id, rev_id) = rev_manager.next_rev_id_pair();
let delta_data = delta.json_bytes();
let revision = Revision::new(&rev_manager.object_id, base_rev_id, rev_id, delta_data, md5);
let _ = rev_manager.add_local_revision(&revision).await?;
Ok(())
}
pub fn make_grid_setting(view_pad: &GridViewRevisionPad, field_revs: &[Arc<FieldRevision>]) -> GridSettingPB {
let layout_type: GridLayout = view_pad.layout.clone().into();
let filter_configurations = view_pad
.get_all_filters(field_revs)
.into_iter()
.map(|filter| FilterPB::from(filter.as_ref()))
.collect::<Vec<FilterPB>>();
let group_configurations = view_pad
.get_groups_by_field_revs(field_revs)
.into_iter()
.map(|group| GridGroupConfigurationPB::from(group.as_ref()))
.collect::<Vec<GridGroupConfigurationPB>>();
GridSettingPB {
layouts: GridLayoutPB::all(),
layout_type,
filter_configurations: filter_configurations.into(),
group_configurations: group_configurations.into(),
}
}
pub(crate) struct GridViewFilterDelegateImpl {
pub(crate) editor_delegate: Arc<dyn GridViewEditorDelegate>,
pub(crate) view_revision_pad: Arc<RwLock<GridViewRevisionPad>>,
}
impl FilterDelegate for GridViewFilterDelegateImpl {
fn get_filter_rev(&self, filter_id: FilterType) -> Fut<Vec<Arc<FilterRevision>>> {
let pad = self.view_revision_pad.clone();
to_future(async move {
let field_type_rev: FieldTypeRevision = filter_id.field_type.into();
pad.read().await.get_filters(&filter_id.field_id, &field_type_rev)
})
}
fn get_field_rev(&self, field_id: &str) -> Fut<Option<Arc<FieldRevision>>> {
self.editor_delegate.get_field_rev(field_id)
}
fn get_field_revs(&self, field_ids: Option<Vec<String>>) -> Fut<Vec<Arc<FieldRevision>>> {
self.editor_delegate.get_field_revs(field_ids)
}
fn get_blocks(&self) -> Fut<Vec<GridBlock>> {
self.editor_delegate.get_blocks()
}
}

View File

@ -9,7 +9,10 @@ async fn grid_filter_checkbox_is_check_test() {
CreateCheckboxFilter {
condition: CheckboxFilterCondition::IsChecked,
},
AssertNumberOfRows { expected: 2 },
AssertFilterChanged {
visible_row_len: 2,
hide_row_len: 3,
},
];
test.run_scripts(scripts).await;
}
@ -21,7 +24,7 @@ async fn grid_filter_checkbox_is_uncheck_test() {
CreateCheckboxFilter {
condition: CheckboxFilterCondition::IsUnChecked,
},
AssertNumberOfRows { expected: 3 },
AssertNumberOfVisibleRows { expected: 3 },
];
test.run_scripts(scripts).await;
}

View File

@ -12,7 +12,7 @@ async fn grid_filter_date_is_test() {
end: None,
timestamp: Some(1647251762),
},
AssertNumberOfRows { expected: 3 },
AssertNumberOfVisibleRows { expected: 3 },
];
test.run_scripts(scripts).await;
}
@ -27,7 +27,7 @@ async fn grid_filter_date_after_test() {
end: None,
timestamp: Some(1647251762),
},
AssertNumberOfRows { expected: 2 },
AssertNumberOfVisibleRows { expected: 2 },
];
test.run_scripts(scripts).await;
}
@ -42,7 +42,7 @@ async fn grid_filter_date_on_or_after_test() {
end: None,
timestamp: Some(1668359085),
},
AssertNumberOfRows { expected: 2 },
AssertNumberOfVisibleRows { expected: 2 },
];
test.run_scripts(scripts).await;
}
@ -57,7 +57,7 @@ async fn grid_filter_date_on_or_before_test() {
end: None,
timestamp: Some(1668359085),
},
AssertNumberOfRows { expected: 4 },
AssertNumberOfVisibleRows { expected: 4 },
];
test.run_scripts(scripts).await;
}
@ -72,7 +72,7 @@ async fn grid_filter_date_within_test() {
end: Some(1668704685),
timestamp: None,
},
AssertNumberOfRows { expected: 5 },
AssertNumberOfVisibleRows { expected: 5 },
];
test.run_scripts(scripts).await;
}

View File

@ -10,7 +10,7 @@ async fn grid_filter_number_is_equal_test() {
condition: NumberFilterCondition::Equal,
content: "1".to_string(),
},
AssertNumberOfRows { expected: 1 },
AssertNumberOfVisibleRows { expected: 1 },
];
test.run_scripts(scripts).await;
}
@ -23,7 +23,7 @@ async fn grid_filter_number_is_less_than_test() {
condition: NumberFilterCondition::LessThan,
content: "3".to_string(),
},
AssertNumberOfRows { expected: 2 },
AssertNumberOfVisibleRows { expected: 2 },
];
test.run_scripts(scripts).await;
}
@ -37,7 +37,7 @@ async fn grid_filter_number_is_less_than_test2() {
condition: NumberFilterCondition::LessThan,
content: "$3".to_string(),
},
AssertNumberOfRows { expected: 2 },
AssertNumberOfVisibleRows { expected: 2 },
];
test.run_scripts(scripts).await;
}
@ -50,7 +50,7 @@ async fn grid_filter_number_is_less_than_or_equal_test() {
condition: NumberFilterCondition::LessThanOrEqualTo,
content: "3".to_string(),
},
AssertNumberOfRows { expected: 3 },
AssertNumberOfVisibleRows { expected: 3 },
];
test.run_scripts(scripts).await;
}
@ -63,7 +63,7 @@ async fn grid_filter_number_is_empty_test() {
condition: NumberFilterCondition::NumberIsEmpty,
content: "".to_string(),
},
AssertNumberOfRows { expected: 1 },
AssertNumberOfVisibleRows { expected: 1 },
];
test.run_scripts(scripts).await;
}
@ -76,7 +76,7 @@ async fn grid_filter_number_is_not_empty_test() {
condition: NumberFilterCondition::NumberIsNotEmpty,
content: "".to_string(),
},
AssertNumberOfRows { expected: 4 },
AssertNumberOfVisibleRows { expected: 4 },
];
test.run_scripts(scripts).await;
}

View File

@ -3,6 +3,7 @@
#![allow(dead_code)]
#![allow(unused_imports)]
use std::time::Duration;
use bytes::Bytes;
use futures::TryFutureExt;
use flowy_grid::entities::{CreateFilterParams, CreateFilterPayloadPB, DeleteFilterParams, GridLayout, GridSettingChangesetParams, GridSettingPB, RowPB, TextFilterCondition, FieldType, NumberFilterCondition, CheckboxFilterCondition, DateFilterCondition, DateFilterContent, SelectOptionCondition, TextFilterPB, NumberFilterPB, CheckboxFilterPB, DateFilterPB, SelectOptionFilterPB};
@ -10,6 +11,7 @@ use flowy_grid::services::field::SelectOptionIds;
use flowy_grid::services::setting::GridSettingChangesetBuilder;
use grid_rev_model::{FieldRevision, FieldTypeRevision};
use flowy_grid::services::filter::FilterType;
use flowy_grid::services::view_editor::GridViewChanged;
use crate::grid::grid_editor::GridEditorTest;
pub enum FilterScript {
@ -53,13 +55,18 @@ pub enum FilterScript {
condition: u32,
content: String
},
AssertNumberOfRows{
AssertNumberOfVisibleRows {
expected: usize,
},
AssertFilterChanged{
visible_row_len:usize,
hide_row_len: usize,
},
#[allow(dead_code)]
AssertGridSetting {
expected_setting: GridSettingPB,
},
Wait { millisecond: u64 }
}
pub struct GridFilterTest {
@ -160,12 +167,23 @@ impl GridFilterTest {
let setting = self.editor.get_setting().await.unwrap();
assert_eq!(expected_setting, setting);
}
FilterScript::AssertNumberOfRows { expected } => {
FilterScript::AssertFilterChanged { visible_row_len, hide_row_len} => {
let mut receiver = self.editor.subscribe_view_changed().await;
let changed = receiver.recv().await.unwrap();
match changed { GridViewChanged::DidReceiveFilterResult(changed) => {
assert_eq!(changed.visible_rows.len(), visible_row_len);
assert_eq!(changed.invisible_rows.len(), hide_row_len);
} }
}
FilterScript::AssertNumberOfVisibleRows { expected } => {
//
let grid = self.editor.get_grid().await.unwrap();
let rows = grid.blocks.into_iter().map(|block| block.rows).flatten().collect::<Vec<RowPB>>();
assert_eq!(rows.len(), expected);
}
FilterScript::Wait { millisecond } => {
tokio::time::sleep(Duration::from_millis(millisecond)).await;
}
}
}

View File

@ -10,7 +10,7 @@ async fn grid_filter_multi_select_is_empty_test() {
condition: SelectOptionCondition::OptionIsEmpty,
option_ids: vec![],
},
AssertNumberOfRows { expected: 2 },
AssertNumberOfVisibleRows { expected: 2 },
];
test.run_scripts(scripts).await;
}
@ -23,7 +23,7 @@ async fn grid_filter_multi_select_is_not_empty_test() {
condition: SelectOptionCondition::OptionIsNotEmpty,
option_ids: vec![],
},
AssertNumberOfRows { expected: 3 },
AssertNumberOfVisibleRows { expected: 3 },
];
test.run_scripts(scripts).await;
}
@ -37,7 +37,7 @@ async fn grid_filter_multi_select_is_test() {
condition: SelectOptionCondition::OptionIs,
option_ids: vec![options.remove(0).id, options.remove(0).id],
},
AssertNumberOfRows { expected: 2 },
AssertNumberOfVisibleRows { expected: 2 },
];
test.run_scripts(scripts).await;
}
@ -51,7 +51,7 @@ async fn grid_filter_multi_select_is_test2() {
condition: SelectOptionCondition::OptionIs,
option_ids: vec![options.remove(1).id],
},
AssertNumberOfRows { expected: 1 },
AssertNumberOfVisibleRows { expected: 1 },
];
test.run_scripts(scripts).await;
}
@ -64,7 +64,7 @@ async fn grid_filter_single_select_is_empty_test() {
condition: SelectOptionCondition::OptionIsEmpty,
option_ids: vec![],
},
AssertNumberOfRows { expected: 2 },
AssertNumberOfVisibleRows { expected: 2 },
];
test.run_scripts(scripts).await;
}
@ -78,7 +78,7 @@ async fn grid_filter_single_select_is_test() {
condition: SelectOptionCondition::OptionIs,
option_ids: vec![options.remove(0).id],
},
AssertNumberOfRows { expected: 2 },
AssertNumberOfVisibleRows { expected: 2 },
];
test.run_scripts(scripts).await;
}

View File

@ -12,7 +12,27 @@ async fn grid_filter_text_is_empty_test() {
content: "".to_string(),
},
AssertFilterCount { count: 1 },
AssertNumberOfRows { expected: 0 },
AssertFilterChanged {
visible_row_len: 1,
hide_row_len: 4,
},
];
test.run_scripts(scripts).await;
}
#[tokio::test]
async fn grid_filter_text_is_not_empty_test() {
let mut test = GridFilterTest::new().await;
let scripts = vec![
CreateTextFilter {
condition: TextFilterCondition::TextIsNotEmpty,
content: "".to_string(),
},
AssertFilterCount { count: 1 },
AssertFilterChanged {
visible_row_len: 4,
hide_row_len: 1,
},
];
test.run_scripts(scripts).await;
}
@ -25,7 +45,10 @@ async fn grid_filter_is_text_test() {
condition: TextFilterCondition::Is,
content: "A".to_string(),
},
AssertNumberOfRows { expected: 1 },
AssertFilterChanged {
visible_row_len: 1,
hide_row_len: 4,
},
];
test.run_scripts(scripts).await;
}
@ -38,7 +61,10 @@ async fn grid_filter_contain_text_test() {
condition: TextFilterCondition::Contains,
content: "A".to_string(),
},
AssertNumberOfRows { expected: 3 },
AssertFilterChanged {
visible_row_len: 3,
hide_row_len: 2,
},
];
test.run_scripts(scripts).await;
}
@ -51,7 +77,10 @@ async fn grid_filter_start_with_text_test() {
condition: TextFilterCondition::StartsWith,
content: "A".to_string(),
},
AssertNumberOfRows { expected: 2 },
AssertFilterChanged {
visible_row_len: 2,
hide_row_len: 3,
},
];
test.run_scripts(scripts).await;
}
@ -64,7 +93,7 @@ async fn grid_filter_ends_with_text_test() {
condition: TextFilterCondition::EndsWith,
content: "A".to_string(),
},
AssertNumberOfRows { expected: 2 },
AssertNumberOfVisibleRows { expected: 2 },
];
test.run_scripts(scripts).await;
}
@ -81,7 +110,7 @@ async fn grid_filter_delete_test() {
let scripts = vec![
InsertFilter { payload },
AssertFilterCount { count: 1 },
AssertNumberOfRows { expected: 0 },
AssertNumberOfVisibleRows { expected: 1 },
];
test.run_scripts(scripts).await;
@ -92,7 +121,7 @@ async fn grid_filter_delete_test() {
filter_type: FilterType::from(&field_rev),
},
AssertFilterCount { count: 0 },
AssertNumberOfRows { expected: 5 },
AssertNumberOfVisibleRows { expected: 5 },
])
.await;
}

View File

@ -220,7 +220,7 @@ fn make_test_grid() -> BuildGridContext {
1 => {
for field_type in FieldType::iter() {
match field_type {
FieldType::RichText => row_builder.insert_text_cell("B"),
FieldType::RichText => row_builder.insert_text_cell(""),
FieldType::Number => row_builder.insert_number_cell("2"),
FieldType::DateTime => row_builder.insert_date_cell("1647251762"),
FieldType::MultiSelect => row_builder

View File

@ -4,7 +4,7 @@ dependencies = ["build-test-lib"]
description = "Run flutter unit tests"
script = '''
cd app_flowy
flutter test --dart-define=RUST_LOG=${TEST_RUST_LOG}
flutter test --dart-define=RUST_LOG=${TEST_RUST_LOG} --concurrency=1
'''
[tasks.rust_unit_test]