diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/field_editor_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/field/field_editor_bloc.dart index 1a4fe17405..0e8965a5a8 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/field_editor_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/field_editor_bloc.dart @@ -57,6 +57,9 @@ class FieldEditorBloc extends Bloc { }, ); }, + switchToField: (FieldType fieldType) async { + await dataController.switchToField(fieldType); + }, ); }, ); @@ -73,6 +76,8 @@ class FieldEditorEvent with _$FieldEditorEvent { const factory FieldEditorEvent.initial() = _InitialField; const factory FieldEditorEvent.updateName(String name) = _UpdateName; const factory FieldEditorEvent.deleteField() = _DeleteField; + const factory FieldEditorEvent.switchToField(FieldType fieldType) = + _SwitchToField; const factory FieldEditorEvent.didReceiveFieldChanged(FieldPB field) = _DidReceiveFieldChanged; } diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/field_type_option_edit_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/field/field_type_option_edit_bloc.dart index 254a371654..1835ba6262 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/field_type_option_edit_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/field_type_option_edit_bloc.dart @@ -25,6 +25,9 @@ class FieldTypeOptionEditBloc didReceiveFieldUpdated: (field) { emit(state.copyWith(field: field)); }, + switchToField: (FieldType fieldType) async { + await _dataController.switchToField(fieldType); + }, ); }, ); @@ -42,6 +45,8 @@ class FieldTypeOptionEditBloc @freezed class FieldTypeOptionEditEvent with _$FieldTypeOptionEditEvent { const factory FieldTypeOptionEditEvent.initial() = _Initial; + const factory FieldTypeOptionEditEvent.switchToField(FieldType fieldType) = + _SwitchToField; const factory FieldTypeOptionEditEvent.didReceiveFieldUpdated(FieldPB field) = _DidReceiveFieldUpdated; } @@ -53,8 +58,9 @@ class FieldTypeOptionEditState with _$FieldTypeOptionEditState { }) = _FieldTypeOptionEditState; factory FieldTypeOptionEditState.initial( - TypeOptionDataController fieldContext) => + TypeOptionDataController typeOptionDataController, + ) => FieldTypeOptionEditState( - field: fieldContext.field, + field: typeOptionDataController.field, ); } diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_option_editor.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_option_editor.dart index 97c144c491..a3a41ef35b 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_option_editor.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_option_editor.dart @@ -25,31 +25,35 @@ typedef SwitchToFieldCallback ); class FieldTypeOptionEditor extends StatelessWidget { - final TypeOptionDataController dataController; + final TypeOptionDataController _dataController; final PopoverMutex popoverMutex; const FieldTypeOptionEditor({ - required this.dataController, + required TypeOptionDataController dataController, required this.popoverMutex, Key? key, - }) : super(key: key); + }) : _dataController = dataController, + super(key: key); @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => FieldTypeOptionEditBloc(dataController) - ..add(const FieldTypeOptionEditEvent.initial()), + create: (context) { + final bloc = FieldTypeOptionEditBloc(_dataController); + bloc.add(const FieldTypeOptionEditEvent.initial()); + return bloc; + }, child: BlocBuilder( builder: (context, state) { - List children = [ - _switchFieldTypeButton(context, dataController.field) - ]; - final typeOptionWidget = - _typeOptionWidget(context: context, state: state); + final typeOptionWidget = _typeOptionWidget( + context: context, + state: state, + ); - if (typeOptionWidget != null) { - children.add(typeOptionWidget); - } + List children = [ + _SwitchFieldButton(popoverMutex: popoverMutex), + if (typeOptionWidget != null) typeOptionWidget + ]; return ListView( shrinkWrap: true, @@ -60,45 +64,68 @@ class FieldTypeOptionEditor extends StatelessWidget { ); } - Widget _switchFieldTypeButton(BuildContext context, FieldPB field) { - final theme = context.watch(); - return SizedBox( - height: GridSize.typeOptionItemHeight, - child: AppFlowyPopover( - constraints: BoxConstraints.loose(const Size(460, 540)), - asBarrier: true, - triggerActions: PopoverTriggerFlags.click | PopoverTriggerFlags.hover, - mutex: popoverMutex, - offset: const Offset(20, 0), - popupBuilder: (context) { - return FieldTypeList(onSelectField: (newFieldType) { - dataController.switchToField(newFieldType); - }); - }, - child: FlowyButton( - text: FlowyText.medium(field.fieldType.title(), fontSize: 12), - margin: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - hoverColor: theme.hover, - leftIcon: - svgWidget(field.fieldType.iconName(), color: theme.iconColor), - rightIcon: svgWidget("grid/more", color: theme.iconColor), - ), - ), - ); - } - Widget? _typeOptionWidget({ required BuildContext context, required FieldTypeOptionEditState state, }) { return makeTypeOptionWidget( context: context, - dataController: dataController, + dataController: _dataController, popoverMutex: popoverMutex, ); } } +class _SwitchFieldButton extends StatelessWidget { + final PopoverMutex popoverMutex; + const _SwitchFieldButton({ + required this.popoverMutex, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final widget = AppFlowyPopover( + constraints: BoxConstraints.loose(const Size(460, 540)), + asBarrier: true, + triggerActions: PopoverTriggerFlags.click | PopoverTriggerFlags.hover, + mutex: popoverMutex, + offset: const Offset(20, 0), + popupBuilder: (popOverContext) { + return FieldTypeList(onSelectField: (newFieldType) { + context + .read() + .add(FieldTypeOptionEditEvent.switchToField(newFieldType)); + }); + }, + child: _buildMoreButton(context), + ); + + return SizedBox( + height: GridSize.typeOptionItemHeight, + child: widget, + ); + } + + Widget _buildMoreButton(BuildContext context) { + final theme = context.read(); + final bloc = context.read(); + return FlowyButton( + text: FlowyText.medium( + bloc.state.field.fieldType.title(), + fontSize: 12, + ), + margin: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + hoverColor: theme.hover, + leftIcon: svgWidget( + bloc.state.field.fieldType.iconName(), + color: theme.iconColor, + ), + rightIcon: svgWidget("grid/more", color: theme.iconColor), + ); + } +} + abstract class TypeOptionWidget extends StatelessWidget { const TypeOptionWidget({Key? key}) : super(key: key); } diff --git a/frontend/app_flowy/test/bloc_test/board_test/group_by_field_test.dart b/frontend/app_flowy/test/bloc_test/board_test/group_by_field_test.dart index 25895f2e8e..833ff7cab7 100644 --- a/frontend/app_flowy/test/bloc_test/board_test/group_by_field_test.dart +++ b/frontend/app_flowy/test/bloc_test/board_test/group_by_field_test.dart @@ -1,8 +1,10 @@ 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.pbserver.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flutter_test/flutter_test.dart'; import 'util.dart'; @@ -14,41 +16,117 @@ void main() { boardTest = await AppFlowyBoardTest.ensureInitialized(); }); - // Group with not support grouping field - group('Group with not support grouping field:', () { - late FieldEditorBloc editorBloc; + // 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(); - final fieldContext = boardTest.context.singleSelectFieldContext(); - editorBloc = boardTest.context.createFieldEditor( - fieldContext: fieldContext, - )..add(const FieldEditorEvent.initial()); - - await boardResponseFuture(); }); - blocTest( - "switch to text field", - build: () => editorBloc, - wait: boardResponseDuration(), + 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( + "set grouped by multi-select field", + build: () => GridGroupBloc( + viewId: boardTest.context.gridView.id, + fieldController: boardTest.context.fieldController, + ), act: (bloc) async { - await bloc.dataController.switchToField(FieldType.RichText); - }, - verify: (bloc) { - bloc.state.field.fold( - () => throw Exception(), - (field) => field.fieldType == FieldType.RichText, - ); + bloc.add(GridGroupEvent.setGroupByField( + multiSelectField.id, + multiSelectField.fieldType, + )); }, + wait: boardResponseDuration(), ); + blocTest( - 'assert the number of groups is 1', + "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( + "set grouped by 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( + "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"); }, ); }); @@ -78,7 +156,7 @@ void main() { ); test('create checkbox field', () async { - await boardTest.context.createFieldFromType(FieldType.Checkbox); + await boardTest.context.createField(FieldType.Checkbox); await boardResponseFuture(); assert(boardTest.context.fieldContexts.length == 3); @@ -110,4 +188,43 @@ void main() { }, ); }); + + // 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( + "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( + '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}"); + }, + ); + }); } diff --git a/frontend/app_flowy/test/bloc_test/grid_test/field_edit_bloc_test.dart b/frontend/app_flowy/test/bloc_test/grid_test/field_edit_bloc_test.dart index 20a9c036e4..8bbb13be18 100644 --- a/frontend/app_flowy/test/bloc_test/grid_test/field_edit_bloc_test.dart +++ b/frontend/app_flowy/test/bloc_test/grid_test/field_edit_bloc_test.dart @@ -55,7 +55,8 @@ void main() { "switch to text field", build: () => editorBloc, act: (bloc) async { - editorBloc.dataController.switchToField(FieldType.RichText); + editorBloc + .add(const FieldEditorEvent.switchToField(FieldType.RichText)); }, wait: gridResponseDuration(), verify: (bloc) { diff --git a/frontend/app_flowy/test/bloc_test/grid_test/util.dart b/frontend/app_flowy/test/bloc_test/grid_test/util.dart index 3e28883a1a..d09c1584cd 100644 --- a/frontend/app_flowy/test/bloc_test/grid_test/util.dart +++ b/frontend/app_flowy/test/bloc_test/grid_test/util.dart @@ -98,12 +98,52 @@ class AppFlowyGridTest { return editorBloc; } - Future createFieldFromType(FieldType fieldType) async { - final editor = createFieldEditor()..add(const FieldEditorEvent.initial()); + Future makeCellController(String fieldId) async { + final builder = await makeCellControllerBuilder(fieldId); + return builder.build(); + } + + Future makeCellControllerBuilder( + String fieldId, + ) async { + 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 rowDataController = GridRowDataController( + rowInfo: rowInfo, + fieldController: fieldController, + rowCache: rowCache!, + ); + + final rowBloc = RowBloc( + rowInfo: rowInfo, + dataController: rowDataController, + )..add(const RowEvent.initial()); await gridResponseFuture(); - editor.dataController.switchToField(fieldType); + + return GridCellControllerBuilder( + cellId: rowBloc.state.gridCellMap[fieldId]!, + cellCache: rowCache.cellCache, + delegate: rowDataController, + ); + } + + Future createField(FieldType fieldType) async { + final editorBloc = createFieldEditor() + ..add(const FieldEditorEvent.initial()); await gridResponseFuture(); - return Future(() => editor); + editorBloc.add(FieldEditorEvent.switchToField(fieldType)); + await gridResponseFuture(); + return Future(() => editorBloc); } GridFieldContext singleSelectFieldContext() { @@ -162,46 +202,20 @@ class AppFlowyGridTest { /// Create a new Grid for cell test class AppFlowyGridCellTest { - final AppFlowyGridTest _gridTest; - AppFlowyGridCellTest(AppFlowyGridTest gridTest) : _gridTest = gridTest; + final AppFlowyGridTest gridTest; + AppFlowyGridCellTest({required this.gridTest}); static Future ensureInitialized() async { final gridTest = await AppFlowyGridTest.ensureInitialized(); - return AppFlowyGridCellTest(gridTest); + return AppFlowyGridCellTest(gridTest: gridTest); } Future createTestRow() async { - await _gridTest.createRow(); + await gridTest.createRow(); } Future createTestGrid() async { - await _gridTest.createTestGrid(); - } - - Future cellControllerBuilder( - String fieldId, - ) async { - final RowInfo rowInfo = _gridTest.rowInfos.last; - final blockCache = _gridTest.blocks[rowInfo.rowPB.blockId]; - final rowCache = blockCache?.rowCache; - - final rowDataController = GridRowDataController( - rowInfo: rowInfo, - fieldController: _gridTest._gridDataController!.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, - ); + await gridTest.createTestGrid(); } } @@ -229,11 +243,11 @@ class AppFlowyGridSelectOptionCellTest { assert(fieldType == FieldType.SingleSelect || fieldType == FieldType.MultiSelect); - final fieldContexts = _gridCellTest._gridTest.fieldContexts; + final fieldContexts = _gridCellTest.gridTest.fieldContexts; final field = fieldContexts.firstWhere((element) => element.fieldType == fieldType); - final builder = await _gridCellTest.cellControllerBuilder(field.id); - final cellController = builder.build() as GridSelectOptionCellController; + final cellController = await _gridCellTest.gridTest + .makeCellController(field.id) as GridSelectOptionCellController; return cellController; } }