فهرست منبع

feat: show hidden fields in row detail page (#3545)

Richard Shiue 1 سال پیش
والد
کامیت
0738b5f87d
23فایلهای تغییر یافته به همراه457 افزوده شده و 208 حذف شده
  1. 49 1
      frontend/appflowy_flutter/integration_test/database_row_page_test.dart
  2. 28 2
      frontend/appflowy_flutter/integration_test/util/database_test_op.dart
  3. 10 0
      frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_service.dart
  4. 3 3
      frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_action_sheet_bloc.dart
  5. 9 8
      frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_cell_bloc.dart
  6. 38 33
      frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_controller.dart
  7. 2 1
      frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_service.dart
  8. 20 2
      frontend/appflowy_flutter/lib/plugins/database_view/application/field_settings/field_settings_service.dart
  9. 5 8
      frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart
  10. 10 5
      frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_event_editor_bloc.dart
  11. 14 10
      frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_bloc.dart
  12. 124 47
      frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_detail_bloc.dart
  13. 2 2
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell.dart
  14. 11 11
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell_action_sheet.dart
  15. 35 23
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_editor.dart
  16. 3 3
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/grid_header.dart
  17. 2 0
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_bloc.dart
  18. 1 0
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/field/grid_property.dart
  19. 69 41
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_property.dart
  20. 2 2
      frontend/appflowy_flutter/test/bloc_test/board_test/util.dart
  21. 4 4
      frontend/appflowy_flutter/test/bloc_test/grid_test/field/field_cell_bloc_test.dart
  22. 2 2
      frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart
  23. 14 0
      frontend/resources/translations/en.json

+ 49 - 1
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();

+ 28 - 2
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<void> toggleShowHiddenFields() async {
+    final button = find.byType(ToggleHiddenFieldsVisibilityButton);
+    await tapButton(button);
+  }
+
+  Future<void> 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<void> scrollGridByOffset(Offset offset) async {
     await drag(find.byType(GridPage), offset);
     await pumpAndSettle();
@@ -746,7 +772,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
   }
 
   Future<void> tapHidePropertyButtonInFieldEditor() async {
-    final button = find.byType(HideFieldButton);
+    final button = find.byType(FieldVisibilityToggleButton);
     await tapButton(button);
   }
 
@@ -899,7 +925,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
   }
 
   Future<void> assertRowCountInGridPage(int num) async {
-    final text = find.text('${rowCountString()} $num',findRichText: true);
+    final text = find.text('${rowCountString()} $num', findRichText: true);
     expect(text, findsOneWidget);
   }
 

+ 10 - 0
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);
+  }
 }

+ 3 - 3
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<FieldActionSheetEvent>(

+ 9 - 8
frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_cell_bloc.dart

@@ -15,13 +15,14 @@ class FieldCellBloc extends Bloc<FieldCellEvent, FieldCellState> {
   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<FieldCellEvent>(
       (event, emit) async {
         event.when(
@@ -29,7 +30,7 @@ class FieldCellBloc extends Bloc<FieldCellEvent, FieldCellState> {
             _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,
       );
 }

+ 38 - 33
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<FieldIdPB> deletedFields) {
-      if (deletedFields.isEmpty) {
-        return;
-      }
-      final List<FieldInfo> newFields = fieldInfos;
-      final Map<String, FieldIdPB> deletedFieldMap = {
-        for (var fieldOrder in deletedFields) fieldOrder.fieldId: fieldOrder
-      };
-
-      newFields.retainWhere((field) => (deletedFieldMap[field.id] == null));
-      _fieldNotifier.fieldInfos = newFields;
-    }
-
     Future<FieldInfo> 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<void> insertFields(List<IndexFieldPB> insertedFields) async {
+    List<FieldInfo> deleteFields(List<FieldIdPB> deletedFields) {
+      if (deletedFields.isEmpty) {
+        return fieldInfos;
+      }
+      final List<FieldInfo> newFields = fieldInfos;
+      final Map<String, FieldIdPB> deletedFieldMap = {
+        for (final fieldOrder in deletedFields) fieldOrder.fieldId: fieldOrder
+      };
+
+      newFields.retainWhere((field) => (deletedFieldMap[field.id] == null));
+      return newFields;
+    }
+
+    Future<List<FieldInfo>> insertFields(
+      List<IndexFieldPB> insertedFields,
+      List<FieldInfo> fieldInfos,
+    ) async {
       if (insertedFields.isEmpty) {
-        return;
+        return fieldInfos;
       }
       final List<FieldInfo> newFieldInfos = fieldInfos;
       for (final indexField in insertedFields) {
@@ -414,32 +415,32 @@ class FieldController {
           newFieldInfos.add(fieldInfo);
         }
       }
-      _fieldNotifier.fieldInfos = newFieldInfos;
+      return newFieldInfos;
     }
 
-    Future<List<FieldInfo>> updateFields(List<FieldPB> updatedFieldPBs) async {
+    Future<(List<FieldInfo>, List<FieldInfo>)> updateFields(
+      List<FieldPB> updatedFieldPBs,
+      List<FieldInfo> fieldInfos,
+    ) async {
       if (updatedFieldPBs.isEmpty) {
-        return [];
+        return (<FieldInfo>[], fieldInfos);
       }
 
-      final List<FieldInfo> newFields = fieldInfos;
+      final List<FieldInfo> newFieldInfo = fieldInfos;
       final List<FieldInfo> 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);
-
-            final updatedFields = await updateFields(changeset.updatedFields);
+            List<FieldInfo> updatedFields;
+            List<FieldInfo> fieldInfos = deleteFields(changeset.deletedFields);
+            fieldInfos =
+                await insertFields(changeset.insertedFields, fieldInfos);
+            (updatedFields, fieldInfos) =
+                await updateFields(changeset.updatedFields, fieldInfos);
+
+            _fieldNotifier.fieldInfos = fieldInfos;
             for (final listener in _updatedFieldCallbacks.values) {
               listener(updatedFields);
             }

+ 2 - 1
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;
 }

+ 20 - 2
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 = <FieldSettingsPB>[];
+
+          for (final fieldSetting in repeatedFieldSettings.items) {
+            if (!fieldSetting.hasVisibility()) {
+              fieldSetting.visibility = FieldVisibility.AlwaysShown;
+            }
+            fieldSettings.add(fieldSetting);
+          }
+
+          return left(fieldSettings);
+        },
         (r) => right(r),
       );
     });

+ 5 - 8
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;
   }

+ 10 - 5
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),
           );
         }
       },

+ 14 - 10
frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_bloc.dart

@@ -39,6 +39,8 @@ class RowBloc extends Bloc<RowEvent, RowState> {
             _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 {

+ 124 - 47
frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_detail_bloc.dart

@@ -14,49 +14,73 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
 
   RowDetailBloc({
     required this.rowController,
-  }) : super(RowDetailState.initial()) {
+  }) : super(RowDetailState.initial(rowController.loadData())) {
     on<RowDetailEvent>(
       (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();
-          },
-          showField: (fieldId) async {
-            final result =
-                await FieldSettingsBackendService(viewId: rowController.viewId)
-                    .updateFieldSettings(
+            final fieldService = FieldBackendService(
+              viewId: rowController.viewId,
               fieldId: fieldId,
-              fieldVisibility: FieldVisibility.AlwaysShown,
-            );
-            result.fold(
-              (l) {},
-              (err) => Log.error(err),
             );
+            fieldService.deleteField();
           },
-          hideField: (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.AlwaysHidden,
+              fieldVisibility: fieldVisibility,
             );
             result.fold(
               (l) {},
               (err) => Log.error(err),
             );
           },
-          reorderField: (fieldId, fromIndex, toIndex) async {
-            await _reorderField(fieldId, fromIndex, toIndex, emit);
+          reorderField:
+              (reorderedFieldId, targetFieldId, fromIndex, toIndex) async {
+            await _reorderField(
+              reorderedFieldId,
+              targetFieldId,
+              fromIndex,
+              toIndex,
+              emit,
+            );
+          },
+          toggleHiddenFieldVisibility: () {
+            final showHiddenFields = !state.showHiddenFields;
+            final visibleCells = List<DatabaseCellContext>.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<RowDetailEvent, RowDetailState> {
 
   Future<void> _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 = <DatabaseCellContext>[];
+        for (final cell in allCells) {
+          final isPrimary = cell.fieldInfo.isPrimary;
 
-  FieldBackendService _fieldBackendService(String fieldId) {
-    return FieldBackendService(
-      viewId: rowController.viewId,
-      fieldId: fieldId,
+          if (cell.isVisible(showHiddenFields: state.showHiddenFields) &&
+              !isPrimary) {
+            visibleCells.add(cell);
+          }
+
+          if (!cell.isVisible() && !isPrimary) {
+            numHiddenFields++;
+          }
+        }
+
+        add(
+          RowDetailEvent.didReceiveCellDatas(
+            visibleCells,
+            allCells,
+            numHiddenFields,
+          ),
+        );
+      },
     );
   }
 
   Future<void> _reorderField(
-    String fieldId,
+    String reorderedFieldId,
+    String targetFieldId,
     int fromIndex,
     int toIndex,
     Emitter<RowDetailState> emit,
   ) async {
-    final cells = List<DatabaseCellContext>.from(state.cells);
+    final cells = List<DatabaseCellContext>.from(state.visibleCells);
     cells.insert(toIndex, cells.removeAt(fromIndex));
-    emit(state.copyWith(cells: cells));
+    emit(state.copyWith(visibleCells: cells));
+
+    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: fieldId);
+    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<RowDetailEvent, RowDetailState> {
 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<DatabaseCellContext> gridCells,
+    List<DatabaseCellContext> visibleCells,
+    List<DatabaseCellContext> allCells,
+    int numHiddenFields,
   ) = _DidReceiveCellDatas;
 }
 
 @freezed
 class RowDetailState with _$RowDetailState {
   const factory RowDetailState({
-    required List<DatabaseCellContext> cells,
+    required List<DatabaseCellContext> visibleCells,
+    required List<DatabaseCellContext> 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 = <DatabaseCellContext>[];
+    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,
+    );
+  }
 }

+ 2 - 2
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell.dart

@@ -38,7 +38,7 @@ class _GridFieldCellState extends State<GridFieldCell> {
   Widget build(BuildContext context) {
     return BlocProvider(
       create: (context) {
-        return FieldCellBloc(cellContext: widget.cellContext);
+        return FieldCellBloc(fieldContext: widget.cellContext);
       },
       child: BlocBuilder<FieldCellBloc, FieldCellState>(
         builder: (context, state) {
@@ -54,7 +54,7 @@ class _GridFieldCellState extends State<GridFieldCell> {
               );
             },
             child: FieldCellButton(
-              field: widget.cellContext.field,
+              field: widget.cellContext.fieldInfo.field,
               onTap: () => popoverController.show(),
             ),
           );

+ 11 - 11
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell_action_sheet.dart

@@ -34,14 +34,14 @@ class _GridFieldCellActionSheetState extends State<GridFieldCellActionSheet> {
   @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);

+ 35 - 23
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<StatefulWidget> createState() => _FieldEditorState();
@@ -53,14 +57,14 @@ class _FieldEditorState extends State<FieldEditor> {
   @override
   Widget build(BuildContext context) {
     final bool requireSpace = widget.onDeleted != null ||
-        widget.onHidden != null ||
+        widget.onToggleVisibility != null ||
         !widget.typeOptionLoader.field.isPrimary;
 
     final List<Widget> 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<FieldEditor> {
       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<FieldEditor> {
       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<FieldNameTextField> {
   }
 }
 
-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);

+ 3 - 3
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<GridHeaderBloc>()
           .add(GridHeaderEvent.moveField(field, oldIndex, newIndex));

+ 2 - 0
frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_bloc.dart

@@ -93,6 +93,8 @@ List<DatabaseCellContext> _makeCells(
   CellContextByFieldId originalCellMap,
 ) {
   final List<DatabaseCellContext> 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) {

+ 1 - 0
frontend/appflowy_flutter/lib/plugins/database_view/widgets/field/grid_property.dart

@@ -151,6 +151,7 @@ class _GridPropertyCellState extends State<GridPropertyCell> {
       popupBuilder: (BuildContext context) {
         return FieldEditor(
           viewId: widget.viewId,
+          fieldInfo: widget.fieldInfo,
           typeOptionLoader: FieldTypeOptionLoader(
             viewId: widget.viewId,
             field: widget.fieldInfo.field,

+ 69 - 41
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<RowDetailBloc, RowDetailState>(
-      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<RowDetailBloc>().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<RowDetailBloc>().state.numHiddenFields != 0)
+                  const Padding(
+                    padding: EdgeInsets.only(bottom: 4.0),
+                    child: ToggleHiddenFieldsVisibilityButton(),
+                  ),
+                CreateRowFieldButton(viewId: viewId),
+              ],
+            ),
           ),
           children: children,
         );
       },
     );
   }
-
-  void _reorderField(
-    BuildContext context,
-    List<DatabaseCellContext> 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<RowDetailBloc>().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<RowDetailBloc>().add(RowDetailEvent.hideField(fieldId));
+        context
+            .read<RowDetailBloc>()
+            .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<RowDetailBloc, RowDetailState>(
+      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<RowDetailBloc>().add(
+                  const RowDetailEvent.toggleHiddenFieldVisibility(),
+                ),
+          ),
+        );
+      },
+    );
+  }
+}
+
 class CreateRowFieldButton extends StatefulWidget {
   final String viewId;
 

+ 2 - 2
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() {

+ 4 - 4
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()),

+ 2 - 2
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() {

+ 14 - 0
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",