From 0738b5f87d70e990a2aa172c9845de05f8b0f1ce Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Mon, 2 Oct 2023 10:52:22 +0800 Subject: [PATCH] feat: show hidden fields in row detail page (#3545) --- .../database_row_page_test.dart | 50 ++++- .../util/database_test_op.dart | 30 ++- .../application/cell/cell_service.dart | 10 + .../field/field_action_sheet_bloc.dart | 6 +- .../application/field/field_cell_bloc.dart | 17 +- .../application/field/field_controller.dart | 69 +++---- .../application/field/field_service.dart | 3 +- .../field_settings_service.dart | 22 ++- .../application/row/row_cache.dart | 13 +- .../calendar_event_editor_bloc.dart | 15 +- .../grid/application/row/row_bloc.dart | 24 ++- .../grid/application/row/row_detail_bloc.dart | 171 +++++++++++++----- .../widgets/header/field_cell.dart | 4 +- .../header/field_cell_action_sheet.dart | 22 +-- .../widgets/header/field_editor.dart | 58 +++--- .../widgets/header/grid_header.dart | 6 +- .../database_view/widgets/card/card_bloc.dart | 2 + .../widgets/field/grid_property.dart | 1 + .../widgets/row/row_property.dart | 110 ++++++----- .../test/bloc_test/board_test/util.dart | 4 +- .../grid_test/field/field_cell_bloc_test.dart | 8 +- .../test/bloc_test/grid_test/util.dart | 4 +- frontend/resources/translations/en.json | 14 ++ 23 files changed, 456 insertions(+), 207 deletions(-) diff --git a/frontend/appflowy_flutter/integration_test/database_row_page_test.dart b/frontend/appflowy_flutter/integration_test/database_row_page_test.dart index 6b88fafab8..553748de68 100644 --- a/frontend/appflowy_flutter/integration_test/database_row_page_test.dart +++ b/frontend/appflowy_flutter/integration_test/database_row_page_test.dart @@ -146,7 +146,7 @@ void main() { await tester.openFirstRowDetailPage(); // Assert that the first field in the row details page is the select - // option tyoe + // option type tester.assertFirstFieldInRowDetailByType(FieldType.SingleSelect); // Reorder first field in list @@ -168,6 +168,54 @@ void main() { tester.assertFirstFieldInRowDetailByType(FieldType.SingleSelect); }); + testWidgets('hide and show hidden fields', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + // Create a new grid + await tester.createNewPageWithName(layout: ViewLayoutPB.Grid); + + // Hover first row and then open the row page + await tester.openFirstRowDetailPage(); + + // Assert that the show hidden fields button isn't visible + tester.assertToggleShowHiddenFieldsVisibility(false); + + // Hide the first field in the field list + await tester.tapGridFieldWithNameInRowDetailPage("Type"); + await tester.tapHidePropertyButtonInFieldEditor(); + + // Assert that the field is now hidden + tester.noFieldWithName("Type"); + + // Assert that the show hidden fields button appears + tester.assertToggleShowHiddenFieldsVisibility(true); + + // Click on the show hidden fields button + await tester.toggleShowHiddenFields(); + + // Assert that the hidden field is shown again and that the show + // hidden fields button is still present + tester.findFieldWithName("Type"); + tester.assertToggleShowHiddenFieldsVisibility(true); + + // Click hide hidden fields + await tester.toggleShowHiddenFields(); + + // Assert that the hidden field has vanished + tester.noFieldWithName("Type"); + + // Click show hidden fields + await tester.toggleShowHiddenFields(); + + // delete the hidden field + await tester.tapGridFieldWithNameInRowDetailPage("Type"); + await tester.tapDeletePropertyInFieldEditor(); + + // Assert that the that the show hidden fields button is gone + tester.assertToggleShowHiddenFieldsVisibility(false); + }); + testWidgets('check document exists in row detail page', (tester) async { await tester.initializeAppFlowy(); await tester.tapGoButton(); diff --git a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart index 7505bd88a9..b2f4c4a2f2 100644 --- a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart +++ b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart @@ -55,6 +55,7 @@ import 'package:appflowy/plugins/database_view/widgets/row/row_property.dart'; import 'package:appflowy/plugins/database_view/widgets/setting/database_setting.dart'; import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/emoji_picker/emoji_menu_item.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; @@ -673,6 +674,31 @@ extension AppFlowyDatabaseTest on WidgetTester { await pumpAndSettle(); } + void assertToggleShowHiddenFieldsVisibility(bool shown) { + final button = find.byType(ToggleHiddenFieldsVisibilityButton); + if (shown) { + expect(button, findsOneWidget); + } else { + expect(button, findsNothing); + } + } + + Future toggleShowHiddenFields() async { + final button = find.byType(ToggleHiddenFieldsVisibilityButton); + await tapButton(button); + } + + Future tapDeletePropertyInFieldEditor() async { + final deleteButton = find.byType(DeleteFieldButton); + await tapButton(deleteButton); + + final confirmButton = find.descendant( + of: find.byType(NavigatorAlertDialog), + matching: find.byType(PrimaryTextButton), + ); + await tapButton(confirmButton); + } + Future scrollGridByOffset(Offset offset) async { await drag(find.byType(GridPage), offset); await pumpAndSettle(); @@ -746,7 +772,7 @@ extension AppFlowyDatabaseTest on WidgetTester { } Future tapHidePropertyButtonInFieldEditor() async { - final button = find.byType(HideFieldButton); + final button = find.byType(FieldVisibilityToggleButton); await tapButton(button); } @@ -899,7 +925,7 @@ extension AppFlowyDatabaseTest on WidgetTester { } Future assertRowCountInGridPage(int num) async { - final text = find.text('${rowCountString()} $num',findRichText: true); + final text = find.text('${rowCountString()} $num', findRichText: true); expect(text, findsOneWidget); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_service.dart index 6f4d330c25..f73d4be332 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_service.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:collection'; import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart'; @@ -73,4 +74,13 @@ class DatabaseCellContext with _$DatabaseCellContext { /// Only the primary field can have an emoji. String? get emoji => fieldInfo.field.isPrimary ? rowMeta.icon : null; + + /// Determines whether a database cell context should be visible. + /// It will be visible when the field is not hidden or when hidden fields + /// should be shown. + bool isVisible({bool showHiddenFields = false}) { + return fieldInfo.visibility != null && + (showHiddenFields || + fieldInfo.visibility != FieldVisibility.AlwaysHidden); + } } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_action_sheet_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_action_sheet_bloc.dart index 952006215e..d72ced9fbe 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_action_sheet_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_action_sheet_bloc.dart @@ -15,16 +15,16 @@ class FieldActionSheetBloc final FieldSettingsBackendService fieldSettingsService; FieldActionSheetBloc({required FieldContext fieldCellContext}) - : fieldId = fieldCellContext.field.id, + : fieldId = fieldCellContext.fieldInfo.id, fieldService = FieldBackendService( viewId: fieldCellContext.viewId, - fieldId: fieldCellContext.field.id, + fieldId: fieldCellContext.fieldInfo.id, ), fieldSettingsService = FieldSettingsBackendService(viewId: fieldCellContext.viewId), super( FieldActionSheetState.initial( - TypeOptionPB.create()..field_2 = fieldCellContext.field, + TypeOptionPB.create()..field_2 = fieldCellContext.fieldInfo.field, ), ) { on( diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_cell_bloc.dart index 12d3a229cf..b916177a55 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_cell_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_cell_bloc.dart @@ -15,13 +15,14 @@ class FieldCellBloc extends Bloc { final FieldBackendService _fieldBackendSvc; FieldCellBloc({ - required FieldContext cellContext, - }) : _fieldListener = SingleFieldListener(fieldId: cellContext.field.id), + required FieldContext fieldContext, + }) : _fieldListener = + SingleFieldListener(fieldId: fieldContext.fieldInfo.id), _fieldBackendSvc = FieldBackendService( - viewId: cellContext.viewId, - fieldId: cellContext.field.id, + viewId: fieldContext.viewId, + fieldId: fieldContext.fieldInfo.id, ), - super(FieldCellState.initial(cellContext)) { + super(FieldCellState.initial(fieldContext)) { on( (event, emit) async { event.when( @@ -29,7 +30,7 @@ class FieldCellBloc extends Bloc { _startListening(); }, didReceiveFieldUpdate: (field) { - emit(state.copyWith(field: cellContext.field)); + emit(state.copyWith(field: fieldContext.fieldInfo.field)); }, onResizeStart: () { emit(state.copyWith(resizeStart: state.width)); @@ -88,8 +89,8 @@ class FieldCellState with _$FieldCellState { factory FieldCellState.initial(FieldContext cellContext) => FieldCellState( viewId: cellContext.viewId, - field: cellContext.field, - width: cellContext.field.width.toDouble(), + field: cellContext.fieldInfo.field, + width: cellContext.fieldInfo.field.width.toDouble(), resizeStart: 0, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_controller.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_controller.dart index 6faf52aab5..b7975b9abe 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_controller.dart @@ -367,28 +367,13 @@ class FieldController { /// Listen for field changes in the backend. void _listenOnFieldChanges() { - void deleteFields(List deletedFields) { - if (deletedFields.isEmpty) { - return; - } - final List newFields = fieldInfos; - final Map deletedFieldMap = { - for (var fieldOrder in deletedFields) fieldOrder.fieldId: fieldOrder - }; - - newFields.retainWhere((field) => (deletedFieldMap[field.id] == null)); - _fieldNotifier.fieldInfos = newFields; - } - Future attachFieldSettings(FieldInfo fieldInfo) async { return _fieldSettingsBackendSvc .getFieldSettings(fieldInfo.id) .then((result) { final fieldSettings = result.fold( (fieldSettings) => fieldSettings, - (err) { - return null; - }, + (err) => null, ); if (fieldSettings == null) { return fieldInfo; @@ -400,9 +385,25 @@ class FieldController { }); } - Future insertFields(List insertedFields) async { + List deleteFields(List deletedFields) { + if (deletedFields.isEmpty) { + return fieldInfos; + } + final List newFields = fieldInfos; + final Map deletedFieldMap = { + for (final fieldOrder in deletedFields) fieldOrder.fieldId: fieldOrder + }; + + newFields.retainWhere((field) => (deletedFieldMap[field.id] == null)); + return newFields; + } + + Future> insertFields( + List insertedFields, + List fieldInfos, + ) async { if (insertedFields.isEmpty) { - return; + return fieldInfos; } final List newFieldInfos = fieldInfos; for (final indexField in insertedFields) { @@ -414,32 +415,32 @@ class FieldController { newFieldInfos.add(fieldInfo); } } - _fieldNotifier.fieldInfos = newFieldInfos; + return newFieldInfos; } - Future> updateFields(List updatedFieldPBs) async { + Future<(List, List)> updateFields( + List updatedFieldPBs, + List fieldInfos, + ) async { if (updatedFieldPBs.isEmpty) { - return []; + return ([], fieldInfos); } - final List newFields = fieldInfos; + final List newFieldInfo = fieldInfos; final List updatedFields = []; for (final updatedFieldPB in updatedFieldPBs) { final index = - newFields.indexWhere((field) => field.id == updatedFieldPB.id); + newFieldInfo.indexWhere((field) => field.id == updatedFieldPB.id); if (index != -1) { - newFields.removeAt(index); + newFieldInfo.removeAt(index); final initial = FieldInfo.initial(updatedFieldPB); final fieldInfo = await attachFieldSettings(initial); - newFields.insert(index, fieldInfo); + newFieldInfo.insert(index, fieldInfo); updatedFields.add(fieldInfo); } } - if (updatedFields.isNotEmpty) { - _fieldNotifier.fieldInfos = newFields; - } - return updatedFields; + return (updatedFields, newFieldInfo); } // Listen on field's changes @@ -450,10 +451,14 @@ class FieldController { if (_isDisposed) { return; } - deleteFields(changeset.deletedFields); - insertFields(changeset.insertedFields); + List updatedFields; + List fieldInfos = deleteFields(changeset.deletedFields); + fieldInfos = + await insertFields(changeset.insertedFields, fieldInfos); + (updatedFields, fieldInfos) = + await updateFields(changeset.updatedFields, fieldInfos); - final updatedFields = await updateFields(changeset.updatedFields); + _fieldNotifier.fieldInfos = fieldInfos; for (final listener in _updatedFieldCallbacks.values) { listener(updatedFields); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_service.dart index 953da61162..6cf15e8b44 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_service.dart @@ -1,3 +1,4 @@ +import 'package:appflowy/plugins/database_view/application/field/field_info.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart'; import 'package:dartz/dartz.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; @@ -108,6 +109,6 @@ class FieldBackendService { class FieldContext with _$FieldContext { const factory FieldContext({ required String viewId, - required FieldPB field, + required FieldInfo fieldInfo, }) = _FieldCellContext; } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field_settings/field_settings_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field_settings/field_settings_service.dart index 3352ae2a62..e699b61ad8 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/field_settings/field_settings_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field_settings/field_settings_service.dart @@ -20,7 +20,14 @@ class FieldSettingsBackendService { return DatabaseEventGetFieldSettings(payload).send().then((result) { return result.fold( - (fieldSettings) => left(fieldSettings.items.first), + (repeatedFieldSettings) { + final fieldSetting = repeatedFieldSettings.items.first; + if (!fieldSetting.hasVisibility()) { + fieldSetting.visibility = FieldVisibility.AlwaysShown; + } + + return left(fieldSetting); + }, (r) => right(r), ); }); @@ -31,7 +38,18 @@ class FieldSettingsBackendService { return DatabaseEventGetAllFieldSettings(payload).send().then((result) { return result.fold( - (fieldSettings) => left(fieldSettings.items), + (repeatedFieldSettings) { + final fieldSettings = []; + + for (final fieldSetting in repeatedFieldSettings.items) { + if (!fieldSetting.hasVisibility()) { + fieldSetting.visibility = FieldVisibility.AlwaysShown; + } + fieldSettings.add(fieldSetting); + } + + return left(fieldSettings); + }, (r) => right(r), ); }); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart index b1acb8f734..87e2c69959 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart @@ -246,14 +246,11 @@ class RowCache { // ignore: prefer_collection_literals final cellContextMap = CellContextByFieldId(); for (final fieldInfo in _fieldDelegate.fieldInfos) { - if (fieldInfo.visibility != null && - fieldInfo.visibility != FieldVisibility.AlwaysHidden) { - cellContextMap[fieldInfo.id] = DatabaseCellContext( - rowMeta: rowMeta, - viewId: viewId, - fieldInfo: fieldInfo, - ); - } + cellContextMap[fieldInfo.id] = DatabaseCellContext( + rowMeta: rowMeta, + viewId: viewId, + fieldInfo: fieldInfo, + ); } return cellContextMap; } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_event_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_event_editor_bloc.dart index 06904f3994..a19450390e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_event_editor_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_event_editor_bloc.dart @@ -23,12 +23,14 @@ class CalendarEventEditorBloc await event.when( initial: () { _startListening(); - final cells = rowController.loadData(); + final cells = rowController + .loadData() + .values + .where((cellContext) => cellContext.isVisible()) + .toList(); if (!isClosed) { add( - CalendarEventEditorEvent.didReceiveCellDatas( - cells.values.toList(), - ), + CalendarEventEditorEvent.didReceiveCellDatas(cells), ); } }, @@ -47,8 +49,11 @@ class CalendarEventEditorBloc rowController.addListener( onRowChanged: (cells, reason) { if (!isClosed) { + final cellData = cells.values + .where((cellContext) => cellContext.isVisible()) + .toList(); add( - CalendarEventEditorEvent.didReceiveCellDatas(cells.values.toList()), + CalendarEventEditorEvent.didReceiveCellDatas(cellData), ); } }, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_bloc.dart index 63247328fe..9842bd5f0c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_bloc.dart @@ -39,6 +39,8 @@ class RowBloc extends Bloc { _rowBackendSvc.createRowAfterRow(rowId); }, didReceiveCells: (cellByFieldId, reason) async { + cellByFieldId + .removeWhere((_, cellContext) => !cellContext.isVisible()); final cells = cellByFieldId.values .map((e) => GridCellEquatable(e.fieldInfo)) .toList(); @@ -106,16 +108,18 @@ class RowState with _$RowState { factory RowState.initial( CellContextByFieldId cellByFieldId, - ) => - RowState( - cellByFieldId: cellByFieldId, - cells: UnmodifiableListView( - cellByFieldId.values - .map((e) => GridCellEquatable(e.fieldInfo)) - .toList(), - ), - rowSource: const RowSourece.disk(), - ); + ) { + cellByFieldId.removeWhere((_, cellContext) => !cellContext.isVisible()); + return RowState( + cellByFieldId: cellByFieldId, + cells: UnmodifiableListView( + cellByFieldId.values + .map((e) => GridCellEquatable(e.fieldInfo)) + .toList(), + ), + rowSource: const RowSourece.disk(), + ); + } } class GridCellEquatable extends Equatable { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_detail_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_detail_bloc.dart index 6023e3a891..ddecefd10d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_detail_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_detail_bloc.dart @@ -14,49 +14,73 @@ class RowDetailBloc extends Bloc { RowDetailBloc({ required this.rowController, - }) : super(RowDetailState.initial()) { + }) : super(RowDetailState.initial(rowController.loadData())) { on( (event, emit) async { await event.when( initial: () async { await _startListening(); - final cells = rowController.loadData(); - if (!isClosed) { - add(RowDetailEvent.didReceiveCellDatas(cells.values.toList())); - } }, - didReceiveCellDatas: (cells) { - emit(state.copyWith(cells: cells)); + didReceiveCellDatas: (visibleCells, allCells, numHiddenFields) { + emit( + state.copyWith( + visibleCells: visibleCells, + allCells: allCells, + numHiddenFields: numHiddenFields, + ), + ); }, deleteField: (fieldId) { - _fieldBackendService(fieldId).deleteField(); + final fieldService = FieldBackendService( + viewId: rowController.viewId, + fieldId: fieldId, + ); + fieldService.deleteField(); }, - showField: (fieldId) async { + toggleFieldVisibility: (fieldId) async { + final fieldInfo = state.allCells + .where((cellContext) => cellContext.fieldId == fieldId) + .first + .fieldInfo; + final fieldVisibility = + fieldInfo.visibility == FieldVisibility.AlwaysShown + ? FieldVisibility.AlwaysHidden + : FieldVisibility.AlwaysShown; final result = await FieldSettingsBackendService(viewId: rowController.viewId) .updateFieldSettings( fieldId: fieldId, - fieldVisibility: FieldVisibility.AlwaysShown, + fieldVisibility: fieldVisibility, ); result.fold( (l) {}, (err) => Log.error(err), ); }, - hideField: (fieldId) async { - final result = - await FieldSettingsBackendService(viewId: rowController.viewId) - .updateFieldSettings( - fieldId: fieldId, - fieldVisibility: FieldVisibility.AlwaysHidden, - ); - result.fold( - (l) {}, - (err) => Log.error(err), + reorderField: + (reorderedFieldId, targetFieldId, fromIndex, toIndex) async { + await _reorderField( + reorderedFieldId, + targetFieldId, + fromIndex, + toIndex, + emit, ); }, - reorderField: (fieldId, fromIndex, toIndex) async { - await _reorderField(fieldId, fromIndex, toIndex, emit); + toggleHiddenFieldVisibility: () { + final showHiddenFields = !state.showHiddenFields; + final visibleCells = List.from(state.allCells); + visibleCells.retainWhere( + (cellContext) => + !cellContext.fieldInfo.isPrimary && + cellContext.isVisible(showHiddenFields: showHiddenFields), + ); + emit( + state.copyWith( + showHiddenFields: showHiddenFields, + visibleCells: visibleCells, + ), + ); }, ); }, @@ -71,36 +95,60 @@ class RowDetailBloc extends Bloc { Future _startListening() async { rowController.addListener( - onRowChanged: (cells, reason) { - if (!isClosed) { - add(RowDetailEvent.didReceiveCellDatas(cells.values.toList())); + onRowChanged: (cellMap, reason) { + if (isClosed) { + return; } + final allCells = cellMap.values.toList(); + int numHiddenFields = 0; + final visibleCells = []; + for (final cell in allCells) { + final isPrimary = cell.fieldInfo.isPrimary; + + if (cell.isVisible(showHiddenFields: state.showHiddenFields) && + !isPrimary) { + visibleCells.add(cell); + } + + if (!cell.isVisible() && !isPrimary) { + numHiddenFields++; + } + } + + add( + RowDetailEvent.didReceiveCellDatas( + visibleCells, + allCells, + numHiddenFields, + ), + ); }, ); } - FieldBackendService _fieldBackendService(String fieldId) { - return FieldBackendService( - viewId: rowController.viewId, - fieldId: fieldId, - ); - } - Future _reorderField( - String fieldId, + String reorderedFieldId, + String targetFieldId, int fromIndex, int toIndex, Emitter emit, ) async { - final cells = List.from(state.cells); + final cells = List.from(state.visibleCells); cells.insert(toIndex, cells.removeAt(fromIndex)); - emit(state.copyWith(cells: cells)); + emit(state.copyWith(visibleCells: cells)); - final fieldService = - FieldBackendService(viewId: rowController.viewId, fieldId: fieldId); + final fromIndexInAllFields = + state.allCells.indexWhere((cell) => cell.fieldId == reorderedFieldId); + final toIndexInAllFields = + state.allCells.indexWhere((cell) => cell.fieldId == targetFieldId); + + final fieldService = FieldBackendService( + viewId: rowController.viewId, + fieldId: reorderedFieldId, + ); final result = await fieldService.moveField( - fromIndex, - toIndex, + fromIndexInAllFields, + toIndexInAllFields, ); result.fold((l) {}, (err) => Log.error(err)); } @@ -110,25 +158,54 @@ class RowDetailBloc extends Bloc { class RowDetailEvent with _$RowDetailEvent { const factory RowDetailEvent.initial() = _Initial; const factory RowDetailEvent.deleteField(String fieldId) = _DeleteField; - const factory RowDetailEvent.showField(String fieldId) = _ShowField; - const factory RowDetailEvent.hideField(String fieldId) = _HideField; + const factory RowDetailEvent.toggleFieldVisibility(String fieldId) = + _ToggleFieldVisibility; const factory RowDetailEvent.reorderField( - String fieldId, + String reorderFieldID, + String targetFieldID, int fromIndex, int toIndex, ) = _ReorderField; + const factory RowDetailEvent.toggleHiddenFieldVisibility() = + _ToggleHiddenFieldVisibility; const factory RowDetailEvent.didReceiveCellDatas( - List gridCells, + List visibleCells, + List allCells, + int numHiddenFields, ) = _DidReceiveCellDatas; } @freezed class RowDetailState with _$RowDetailState { const factory RowDetailState({ - required List cells, + required List visibleCells, + required List allCells, + required bool showHiddenFields, + required int numHiddenFields, }) = _RowDetailState; - factory RowDetailState.initial() => RowDetailState( - cells: List.empty(), - ); + factory RowDetailState.initial(CellContextByFieldId cellByFieldId) { + final allCells = cellByFieldId.values.toList(); + int numHiddenFields = 0; + final visibleCells = []; + for (final cell in allCells) { + final isVisible = cell.isVisible(); + final isPrimary = cell.fieldInfo.isPrimary; + + if (isVisible && !isPrimary) { + visibleCells.add(cell); + } + + if (!isVisible && !isPrimary) { + numHiddenFields++; + } + } + + return RowDetailState( + visibleCells: visibleCells, + allCells: allCells, + showHiddenFields: false, + numHiddenFields: numHiddenFields, + ); + } } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell.dart index c643183479..e3cacc6140 100755 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell.dart @@ -38,7 +38,7 @@ class _GridFieldCellState extends State { Widget build(BuildContext context) { return BlocProvider( create: (context) { - return FieldCellBloc(cellContext: widget.cellContext); + return FieldCellBloc(fieldContext: widget.cellContext); }, child: BlocBuilder( builder: (context, state) { @@ -54,7 +54,7 @@ class _GridFieldCellState extends State { ); }, child: FieldCellButton( - field: widget.cellContext.field, + field: widget.cellContext.fieldInfo.field, onTap: () => popoverController.show(), ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell_action_sheet.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell_action_sheet.dart index b707ccf7b7..3f4de78de9 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell_action_sheet.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell_action_sheet.dart @@ -34,14 +34,14 @@ class _GridFieldCellActionSheetState extends State { @override Widget build(BuildContext context) { if (_showFieldEditor) { - final field = widget.cellContext.field; return SizedBox( width: 400, child: FieldEditor( viewId: widget.cellContext.viewId, + fieldInfo: widget.cellContext.fieldInfo, typeOptionLoader: FieldTypeOptionLoader( viewId: widget.cellContext.viewId, - field: field, + field: widget.cellContext.fieldInfo.field, ), ), ); @@ -96,8 +96,8 @@ class _EditFieldButton extends StatelessWidget { } class _FieldOperationList extends StatelessWidget { - final FieldContext fieldInfo; - const _FieldOperationList(this.fieldInfo, {Key? key}) : super(key: key); + final FieldContext fieldContext; + const _FieldOperationList(this.fieldContext, {Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -128,7 +128,7 @@ class _FieldOperationList extends StatelessWidget { bool enable = true; // If the field is primary, delete and duplicate are disabled. - if (fieldInfo.field.isPrimary) { + if (fieldContext.fieldInfo.isPrimary) { switch (action) { case FieldAction.hide: break; @@ -145,7 +145,7 @@ class _FieldOperationList extends StatelessWidget { child: SizedBox( height: GridSize.popoverItemHeight, child: FieldActionCell( - fieldInfo: fieldInfo, + fieldInfo: fieldContext, action: action, enable: enable, ), @@ -217,7 +217,7 @@ extension _FieldActionExtension on FieldAction { } } - void run(BuildContext context, FieldContext fieldInfo) { + void run(BuildContext context, FieldContext fieldContext) { switch (this) { case FieldAction.hide: context @@ -228,8 +228,8 @@ extension _FieldActionExtension on FieldAction { PopoverContainer.of(context).close(); FieldBackendService( - viewId: fieldInfo.viewId, - fieldId: fieldInfo.field.id, + viewId: fieldContext.viewId, + fieldId: fieldContext.fieldInfo.id, ).duplicateField(); break; @@ -240,8 +240,8 @@ extension _FieldActionExtension on FieldAction { title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(), confirm: () { FieldBackendService( - viewId: fieldInfo.viewId, - fieldId: fieldInfo.field.id, + viewId: fieldContext.viewId, + fieldId: fieldContext.fieldInfo.field.id, ).deleteField(); }, ).show(context); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_editor.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_editor.dart index b4f57e2b60..bf571a3b74 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_editor.dart @@ -1,6 +1,8 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/plugins/database_view/application/field/field_editor_bloc.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_info.dart'; import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -19,17 +21,19 @@ class FieldEditor extends StatefulWidget { final String viewId; final bool isGroupingField; final Function(String)? onDeleted; - final Function(String)? onHidden; + final Function(String)? onToggleVisibility; final FieldTypeOptionLoader typeOptionLoader; + final FieldInfo? fieldInfo; const FieldEditor({ required this.viewId, required this.typeOptionLoader, + this.fieldInfo, this.isGroupingField = false, this.onDeleted, - this.onHidden, - Key? key, - }) : super(key: key); + this.onToggleVisibility, + super.key, + }); @override State createState() => _FieldEditorState(); @@ -53,14 +57,14 @@ class _FieldEditorState extends State { @override Widget build(BuildContext context) { final bool requireSpace = widget.onDeleted != null || - widget.onHidden != null || + widget.onToggleVisibility != null || !widget.typeOptionLoader.field.isPrimary; final List children = [ FieldNameTextField(popoverMutex: popoverMutex), if (requireSpace) const VSpace(4), if (widget.onDeleted != null) _addDeleteFieldButton(), - if (widget.onHidden != null) _addHideFieldButton(), + if (widget.onToggleVisibility != null) _addHideFieldButton(), if (!widget.typeOptionLoader.field.isPrimary) FieldTypeOptionCell(popoverMutex: popoverMutex), ]; @@ -88,7 +92,7 @@ class _FieldEditorState extends State { builder: (context, state) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: _DeleteFieldButton( + child: DeleteFieldButton( popoverMutex: popoverMutex, onDeleted: () { state.field.fold( @@ -107,12 +111,14 @@ class _FieldEditorState extends State { builder: (context, state) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: HideFieldButton( + child: FieldVisibilityToggleButton( + isFieldHidden: + widget.fieldInfo!.visibility == FieldVisibility.AlwaysHidden, popoverMutex: popoverMutex, - onHidden: () { + onTap: () { state.field.fold( () => Log.error('Can not hidden the field'), - (field) => widget.onHidden?.call(field.id), + (field) => widget.onToggleVisibility?.call(field.id), ); }, ), @@ -218,15 +224,16 @@ class _FieldNameTextFieldState extends State { } } -class _DeleteFieldButton extends StatelessWidget { +@visibleForTesting +class DeleteFieldButton extends StatelessWidget { final PopoverMutex popoverMutex; final VoidCallback? onDeleted; - const _DeleteFieldButton({ + const DeleteFieldButton({ required this.popoverMutex, required this.onDeleted, - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { @@ -253,15 +260,17 @@ class _DeleteFieldButton extends StatelessWidget { } @visibleForTesting -class HideFieldButton extends StatelessWidget { +class FieldVisibilityToggleButton extends StatelessWidget { + final bool isFieldHidden; final PopoverMutex popoverMutex; - final VoidCallback? onHidden; + final VoidCallback? onTap; - const HideFieldButton({ + const FieldVisibilityToggleButton({ + required this.isFieldHidden, required this.popoverMutex, - required this.onHidden, - Key? key, - }) : super(key: key); + required this.onTap, + super.key, + }); @override Widget build(BuildContext context) { @@ -270,10 +279,13 @@ class HideFieldButton extends StatelessWidget { builder: (context, state) { final Widget button = FlowyButton( text: FlowyText.medium( - LocaleKeys.grid_field_hide.tr(), + isFieldHidden + ? LocaleKeys.grid_field_show.tr() + : LocaleKeys.grid_field_hide.tr(), ), - leftIcon: const FlowySvg(FlowySvgs.hide_s), - onTap: () => onHidden?.call(), + leftIcon: + FlowySvg(isFieldHidden ? FlowySvgs.show_m : FlowySvgs.hide_m), + onTap: onTap, onHover: (_) => popoverMutex.close(), ); return SizedBox(height: GridSize.popoverItemHeight, child: button); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/grid_header.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/grid_header.dart index b406263388..71f9f1375f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/grid_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/grid_header.dart @@ -98,12 +98,12 @@ class _GridHeaderState extends State<_GridHeader> { .map( (field) => FieldContext( viewId: widget.viewId, - field: field.field, + fieldInfo: field, ), ) .map( (ctx) => GridFieldCell( - key: _getKeyById(ctx.field.id), + key: _getKeyById(ctx.fieldInfo.id), cellContext: ctx, ), ) @@ -136,7 +136,7 @@ class _GridHeaderState extends State<_GridHeader> { int newIndex, ) { if (cells.length > oldIndex) { - final field = cells[oldIndex].cellContext.field; + final field = cells[oldIndex].cellContext.fieldInfo.field; context .read() .add(GridHeaderEvent.moveField(field, oldIndex, newIndex)); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_bloc.dart index d3e8c91ba6..296a94a661 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_bloc.dart @@ -93,6 +93,8 @@ List _makeCells( CellContextByFieldId originalCellMap, ) { final List cells = []; + originalCellMap + .removeWhere((fieldId, cellContext) => !cellContext.isVisible()); for (final entry in originalCellMap.entries) { // Filter out the cell if it's fieldId equal to the groupFieldId if (groupFieldId != null) { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/field/grid_property.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/field/grid_property.dart index 3e6dd158c1..c06b9eaf13 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/field/grid_property.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/field/grid_property.dart @@ -151,6 +151,7 @@ class _GridPropertyCellState extends State { popupBuilder: (BuildContext context) { return FieldEditor( viewId: widget.viewId, + fieldInfo: widget.fieldInfo, typeOptionLoader: FieldTypeOptionLoader( viewId: widget.viewId, field: widget.fieldInfo.field, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_property.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_property.dart index 789b34a98c..ca3c2f15ed 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_property.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_property.dart @@ -37,9 +37,8 @@ class RowPropertyList extends StatelessWidget { @override Widget build(BuildContext context) { return BlocBuilder( - buildWhen: (previous, current) => previous.cells != current.cells, builder: (context, state) { - final children = state.cells + final children = state.visibleCells .where((element) => !element.fieldInfo.field.isPrimary) .mapIndexed( (index, cell) => _PropertyCell( @@ -50,18 +49,26 @@ class RowPropertyList extends StatelessWidget { ), ) .toList(); + return ReorderableListView( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), onReorder: (oldIndex, newIndex) { - final reorderedField = children[oldIndex].cellContext.fieldId; - _reorderField( - context, - state.cells, - reorderedField, - oldIndex, - newIndex, - ); + // when reorderiing downwards, need to update index + if (oldIndex < newIndex) { + newIndex--; + } + final reorderedFieldId = children[oldIndex].cellContext.fieldId; + final targetFieldId = children[newIndex].cellContext.fieldId; + + context.read().add( + RowDetailEvent.reorderField( + reorderedFieldId, + targetFieldId, + oldIndex, + newIndex, + ), + ); }, buildDefaultDragHandles: false, proxyDecorator: (child, index, animation) => Material( @@ -84,41 +91,22 @@ class RowPropertyList extends StatelessWidget { ), footer: Padding( padding: const EdgeInsets.only(left: 20), - child: CreateRowFieldButton(viewId: viewId), + child: Column( + children: [ + if (context.read().state.numHiddenFields != 0) + const Padding( + padding: EdgeInsets.only(bottom: 4.0), + child: ToggleHiddenFieldsVisibilityButton(), + ), + CreateRowFieldButton(viewId: viewId), + ], + ), ), children: children, ); }, ); } - - void _reorderField( - BuildContext context, - List cells, - String reorderedFieldId, - int oldIndex, - int newIndex, - ) { - // when reorderiing downwards, need to update index - if (oldIndex < newIndex) { - newIndex--; - } - - // also update index when the index is after the index of the primary field - // in the original list of DatabaseCellContext's - final primaryFieldIndex = - cells.indexWhere((element) => element.fieldInfo.isPrimary); - if (oldIndex >= primaryFieldIndex) { - oldIndex++; - } - if (newIndex >= primaryFieldIndex) { - newIndex++; - } - - context.read().add( - RowDetailEvent.reorderField(reorderedFieldId, oldIndex, newIndex), - ); - } } class _PropertyCell extends StatefulWidget { @@ -208,14 +196,17 @@ class _PropertyCellState extends State<_PropertyCell> { Widget buildFieldEditor() { return FieldEditor( viewId: widget.cellContext.viewId, + fieldInfo: widget.cellContext.fieldInfo, isGroupingField: widget.cellContext.fieldInfo.isGroupField, typeOptionLoader: FieldTypeOptionLoader( viewId: widget.cellContext.viewId, field: widget.cellContext.fieldInfo.field, ), - onHidden: (fieldId) { + onToggleVisibility: (fieldId) { _popoverController.close(); - context.read().add(RowDetailEvent.hideField(fieldId)); + context + .read() + .add(RowDetailEvent.toggleFieldVisibility(fieldId)); }, onDeleted: (fieldId) { _popoverController.close(); @@ -288,6 +279,43 @@ GridCellStyle? _customCellStyle(FieldType fieldType) { throw UnimplementedError; } +class ToggleHiddenFieldsVisibilityButton extends StatelessWidget { + const ToggleHiddenFieldsVisibilityButton({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final text = switch (state.showHiddenFields) { + false => LocaleKeys.grid_rowPage_showHiddenFields + .plural(state.numHiddenFields), + true => LocaleKeys.grid_rowPage_hideHiddenFields + .plural(state.numHiddenFields), + }; + + return SizedBox( + height: 30, + child: FlowyButton( + text: FlowyText.medium(text, color: Theme.of(context).hintColor), + hoverColor: AFThemeExtension.of(context).lightGreyHover, + leftIcon: RotatedBox( + quarterTurns: state.showHiddenFields ? 1 : 3, + child: FlowySvg( + FlowySvgs.arrow_left_s, + color: Theme.of(context).hintColor, + ), + ), + margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 6), + onTap: () => context.read().add( + const RowDetailEvent.toggleHiddenFieldVisibility(), + ), + ), + ); + }, + ); + } +} + class CreateRowFieldButton extends StatefulWidget { final String viewId; diff --git a/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart b/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart index 74ffaf31ee..6a630e3697 100644 --- a/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart +++ b/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart @@ -140,8 +140,8 @@ class BoardTestContext { } FieldContext singleSelectFieldCellContext() { - final field = singleSelectFieldContext().field; - return FieldContext(viewId: gridView.id, field: field); + final fieldInfo = singleSelectFieldContext(); + return FieldContext(viewId: gridView.id, fieldInfo: fieldInfo); } FieldInfo textFieldContext() { diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/field/field_cell_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/field/field_cell_bloc_test.dart index 58c2d715a5..e36b9587c8 100644 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/field/field_cell_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/field/field_cell_bloc_test.dart @@ -23,8 +23,8 @@ void main() { blocTest( 'update field width', build: () => FieldCellBloc( - cellContext: FieldContext( - field: context.fieldContexts[0].field, + fieldContext: FieldContext( + fieldInfo: context.fieldContexts[0], viewId: context.gridView.id, ), )..add(const FieldCellEvent.initial()), @@ -42,8 +42,8 @@ void main() { blocTest( 'field width should not be lesser than 50px', build: () => FieldCellBloc( - cellContext: FieldContext( - field: context.fieldContexts[0].field, + fieldContext: FieldContext( + fieldInfo: context.fieldContexts[0], viewId: context.gridView.id, ), )..add(const FieldCellEvent.initial()), diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart index b3cb4df4ab..52a981fcb6 100644 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart @@ -89,8 +89,8 @@ class GridTestContext { } FieldContext singleSelectFieldCellContext() { - final field = singleSelectFieldContext().field; - return FieldContext(viewId: gridView.id, field: field); + final fieldInfo = singleSelectFieldContext(); + return FieldContext(viewId: gridView.id, fieldInfo: fieldInfo); } FieldInfo textFieldContext() { diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index ffc48f5a39..890638adbd 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -411,6 +411,7 @@ }, "field": { "hide": "Hide", + "show": "Show", "insertLeft": "Insert Left", "insertRight": "Insert Right", "duplicate": "Duplicate", @@ -447,6 +448,19 @@ "deleteFieldPromptMessage": "Are you sure? This property will be deleted", "newColumn": "New Column" }, + "rowPage": { + "newField": "Add a new field", + "showHiddenFields": { + "one": "Show {} hidden field", + "many": "Show {} hidden fields", + "other": "Show {} hidden fields" + }, + "hideHiddenFields": { + "one": "Hide {} hidden field", + "many": "Hide {} hidden fields", + "other": "Hide {} hidden fields" + } + }, "sort": { "ascending": "Ascending", "descending": "Descending",