浏览代码

chore: per-view field visibility UI (#3296)

* chore: default field settings if not exist

* chore: field settings listeners and services

* chore: don't need to updateFieldInfos

* feat: per-view field visibilty UI

* fix: remove unresolved imports
Richard Shiue 1 年之前
父节点
当前提交
f3aaff77b9
共有 17 个文件被更改,包括 241 次插入110 次删除
  1. 55 0
      frontend/appflowy_flutter/integration_test/database_field_settings_test.dart
  2. 0 20
      frontend/appflowy_flutter/integration_test/database_field_test.dart
  3. 2 0
      frontend/appflowy_flutter/integration_test/runner.dart
  4. 52 0
      frontend/appflowy_flutter/integration_test/util/database_test_op.dart
  5. 16 3
      frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_action_sheet_bloc.dart
  6. 7 6
      frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_controller.dart
  7. 0 5
      frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_service.dart
  8. 2 1
      frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart
  9. 7 5
      frontend/appflowy_flutter/lib/plugins/database_view/application/setting/property_bloc.dart
  10. 11 7
      frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_header_bloc.dart
  11. 27 9
      frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_detail_bloc.dart
  12. 1 6
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell.dart
  13. 4 3
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_editor.dart
  14. 2 5
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/grid_header.dart
  15. 54 38
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/field/grid_property.dart
  16. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart
  17. 0 1
      frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs

+ 55 - 0
frontend/appflowy_flutter/integration_test/database_field_settings_test.dart

@@ -0,0 +1,55 @@
+import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
+import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart';
+import 'package:appflowy_backend/protobuf/flowy-folder2/view.pbenum.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+
+import 'util/database_test_op.dart';
+import 'util/util.dart';
+
+void main() {
+  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+  group('database field settings', () {
+    testWidgets('field visibility', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
+      await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.grid);
+
+      // create a field
+      await tester.scrollToRight(find.byType(GridPage));
+      await tester.tapNewPropertyButton();
+      await tester.renameField('New field 1');
+      await tester.dismissFieldEditor();
+
+      // hide the field
+      await tester.tapGridFieldWithName('New field 1');
+      await tester.tapHidePropertyButton();
+      await tester.noFieldWithName('New field 1');
+
+      // go back to inline database view, expect field to be shown
+      await tester.tapTabBarLinkedViewByViewName('Untitled');
+      await tester.findFieldWithName('New field 1');
+
+      // go back to linked database view, expect field to be hidden
+      await tester.tapTabBarLinkedViewByViewName('grid');
+      await tester.noFieldWithName('New field 1');
+
+      // use the settings button to show the field
+      await tester.tapDatabaseSettingButton();
+      await tester.tapViewPropertiesButton();
+      await tester.tapViewTogglePropertyVisibilityButtonByName('New field 1');
+      await tester.dismissFieldEditor();
+      await tester.findFieldWithName('New field 1');
+
+      // open first row in popup then hide the field
+      await tester.openFirstRowDetailPage();
+      await tester.tapGridFieldWithNameInRowDetailPage('New field 1');
+      await tester.tapHidePropertyButtonInFieldEditor();
+      await tester.dismissRowDetailPage();
+      await tester.noFieldWithName('New field 1');
+    });
+  });
+}

+ 0 - 20
frontend/appflowy_flutter/integration_test/database_field_test.dart

@@ -106,26 +106,6 @@ void main() {
       await tester.pumpAndSettle();
     });
 
-    testWidgets('hide field', (tester) async {
-      await tester.initializeAppFlowy();
-      await tester.tapGoButton();
-
-      await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
-
-      // create a field
-      await tester.scrollToRight(find.byType(GridPage));
-      await tester.tapNewPropertyButton();
-      await tester.renameField('New field 1');
-      await tester.dismissFieldEditor();
-
-      // Delete the field
-      await tester.tapGridFieldWithName('New field 1');
-      await tester.tapHidePropertyButton();
-
-      await tester.noFieldWithName('New field 1');
-      await tester.pumpAndSettle();
-    });
-
     testWidgets('create checklist field ', (tester) async {
       await tester.initializeAppFlowy();
       await tester.tapGoButton();

+ 2 - 0
frontend/appflowy_flutter/integration_test/runner.dart

@@ -4,6 +4,7 @@ import 'package:integration_test/integration_test.dart';
 import 'database_calendar_test.dart' as database_calendar_test;
 import 'database_cell_test.dart' as database_cell_test;
 import 'database_field_test.dart' as database_field_test;
+import 'database_field_settings_test.dart' as database_field_settings_test;
 import 'database_filter_test.dart' as database_filter_test;
 import 'database_row_page_test.dart' as database_row_page_test;
 import 'database_row_test.dart' as database_row_test;
@@ -47,6 +48,7 @@ void main() {
   // Database integration tests
   database_cell_test.main();
   database_field_test.main();
+  database_field_settings_test.main();
   database_share_test.main();
   database_row_page_test.main();
   database_row_test.main();

+ 52 - 0
frontend/appflowy_flutter/integration_test/util/database_test_op.dart

@@ -35,6 +35,7 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar
 import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_header.dart';
 import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart';
 import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart';
+import 'package:appflowy/plugins/database_view/widgets/field/grid_property.dart';
 import 'package:appflowy/plugins/database_view/widgets/row/accessory/cell_accessory.dart';
 import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart';
 import 'package:appflowy/plugins/database_view/widgets/row/cells/checklist_cell/checklist_progress_bar.dart';
@@ -503,6 +504,18 @@ extension AppFlowyDatabaseTest on WidgetTester {
     await tapButton(findDateCell);
   }
 
+  Future<void> tapGridFieldWithNameInRowDetailPage(String name) async {
+    final fields = find.byWidgetPredicate(
+      (widget) => widget is FieldCellButton && widget.field.name == name,
+    );
+    final field = find.descendant(
+      of: find.byType(RowDetailPage),
+      matching: fields,
+    );
+    await tapButton(field);
+    await pumpAndSettle();
+  }
+
   Future<void> duplicateRowInRowDetailPage() async {
     final duplicateButton = find.byType(RowDetailPageDuplicateButton);
     await tapButton(duplicateButton);
@@ -585,6 +598,11 @@ extension AppFlowyDatabaseTest on WidgetTester {
     await tapButton(field);
   }
 
+  Future<void> tapHidePropertyButtonInFieldEditor() async {
+    final button = find.byType(HideFieldButton);
+    await tapButton(button);
+  }
+
   Future<void> tapRowDetailPageCreatePropertyButton() async {
     await tapButton(find.byType(CreateRowFieldButton));
   }
@@ -925,6 +943,23 @@ extension AppFlowyDatabaseTest on WidgetTester {
     await tapButton(button);
   }
 
+  /// Should call [tapDatabaseSettingButton] first.
+  Future<void> tapViewPropertiesButton() async {
+    final findSettingItem = find.byType(DatabaseSettingItem);
+    final findLayoutButton = find.byWidgetPredicate(
+      (widget) =>
+          widget is FlowyText &&
+          widget.text == DatabaseSettingAction.showProperties.title(),
+    );
+
+    final button = find.descendant(
+      of: findSettingItem,
+      matching: findLayoutButton,
+    );
+
+    await tapButton(button);
+  }
+
   /// Should call [tapDatabaseSettingButton] first.
   Future<void> tapDatabaseLayoutButton() async {
     final findSettingItem = find.byType(DatabaseSettingItem);
@@ -1111,6 +1146,11 @@ extension AppFlowyDatabaseTest on WidgetTester {
     await tapButton(findCreateButton);
   }
 
+  Future<void> tapTabBarLinkedViewByViewName(String name) async {
+    final viewButton = findTabBarLinkViewByViewName(name);
+    await tapButton(viewButton);
+  }
+
   Finder findTabBarLinkViewByViewLayout(ViewLayoutPB layout) {
     return find.byWidgetPredicate(
       (widget) => widget is TabBarItemButton && widget.view.layout == layout,
@@ -1212,6 +1252,18 @@ extension AppFlowyDatabaseTest on WidgetTester {
   Future<void> tapAddSelectOptionButton() async {
     await tapButtonWithName(LocaleKeys.grid_field_addSelectOption.tr());
   }
+
+  Future<void> tapViewTogglePropertyVisibilityButtonByName(
+    String fieldName,
+  ) async {
+    final field = find.byWidgetPredicate(
+      (widget) =>
+          widget is GridPropertyCell && widget.fieldInfo.name == fieldName,
+    );
+    final toggleVisibilityButton =
+        find.descendant(of: field, matching: find.byType(FlowyIconButton));
+    await tapButton(toggleVisibilityButton);
+  }
 }
 
 Finder finderForDatabaseLayoutType(DatabaseLayoutPB layout) {

+ 16 - 3
frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_action_sheet_bloc.dart

@@ -1,5 +1,7 @@
+import 'package:appflowy/plugins/database_view/application/field_settings/field_settings_service.dart';
 import 'package:appflowy_backend/log.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'field_service.dart';
@@ -8,13 +10,18 @@ part 'field_action_sheet_bloc.freezed.dart';
 
 class FieldActionSheetBloc
     extends Bloc<FieldActionSheetEvent, FieldActionSheetState> {
+  final String fieldId;
   final FieldBackendService fieldService;
+  final FieldSettingsBackendService fieldSettingsService;
 
   FieldActionSheetBloc({required FieldContext fieldCellContext})
-      : fieldService = FieldBackendService(
+      : fieldId = fieldCellContext.field.id,
+        fieldService = FieldBackendService(
           viewId: fieldCellContext.viewId,
           fieldId: fieldCellContext.field.id,
         ),
+        fieldSettingsService =
+            FieldSettingsBackendService(viewId: fieldCellContext.viewId),
         super(
           FieldActionSheetState.initial(
             TypeOptionPB.create()..field_2 = fieldCellContext.field,
@@ -31,14 +38,20 @@ class FieldActionSheetBloc
             );
           },
           hideField: (_HideField value) async {
-            final result = await fieldService.updateField(visibility: false);
+            final result = await fieldSettingsService.updateFieldSettings(
+              fieldId: fieldId,
+              fieldVisibility: FieldVisibility.AlwaysHidden,
+            );
             result.fold(
               (l) => null,
               (err) => Log.error(err),
             );
           },
           showField: (_ShowField value) async {
-            final result = await fieldService.updateField(visibility: true);
+            final result = await fieldSettingsService.updateFieldSettings(
+              fieldId: fieldId,
+              fieldVisibility: FieldVisibility.AlwaysShown,
+            );
             result.fold(
               (l) => null,
               (err) => Log.error(err),

+ 7 - 6
frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_controller.dart

@@ -417,7 +417,7 @@ class FieldController {
       _fieldNotifier.fieldInfos = newFieldInfos;
     }
 
-    List<FieldInfo> updateFields(List<FieldPB> updatedFieldPBs) {
+    Future<List<FieldInfo>> updateFields(List<FieldPB> updatedFieldPBs) async {
       if (updatedFieldPBs.isEmpty) {
         return [];
       }
@@ -429,7 +429,8 @@ class FieldController {
             newFields.indexWhere((field) => field.id == updatedFieldPB.id);
         if (index != -1) {
           newFields.removeAt(index);
-          final fieldInfo = FieldInfo.initial(updatedFieldPB);
+          final initial = FieldInfo.initial(updatedFieldPB);
+          final fieldInfo = await attachFieldSettings(initial);
           newFields.insert(index, fieldInfo);
           updatedFields.add(fieldInfo);
         }
@@ -443,16 +444,16 @@ class FieldController {
 
     // Listen on field's changes
     _fieldListener.start(
-      onFieldsChanged: (result) {
+      onFieldsChanged: (result) async {
         result.fold(
-          (changeset) {
+          (changeset) async {
             if (_isDisposed) {
               return;
             }
             deleteFields(changeset.deletedFields);
             insertFields(changeset.insertedFields);
 
-            final updatedFields = updateFields(changeset.updatedFields);
+            final updatedFields = await updateFields(changeset.updatedFields);
             for (final listener in _updatedFieldCallbacks.values) {
               listener(updatedFields);
             }
@@ -548,7 +549,7 @@ class FieldController {
 
           _fieldNotifier.fieldInfos =
               newFields.map((field) => FieldInfo.initial(field)).toList();
-          Future.wait([
+          await Future.wait([
             _loadFilters(),
             _loadSorts(),
             _loadAllFieldSettings(),

+ 0 - 5
frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_service.dart

@@ -30,7 +30,6 @@ class FieldBackendService {
   Future<Either<Unit, FlowyError>> updateField({
     String? name,
     bool? frozen,
-    bool? visibility,
     double? width,
   }) {
     final payload = FieldChangesetPB.create()
@@ -45,10 +44,6 @@ class FieldBackendService {
       payload.frozen = frozen;
     }
 
-    if (visibility != null) {
-      payload.visibility = visibility;
-    }
-
     if (width != null) {
       payload.width = width.toInt();
     }

+ 2 - 1
frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart

@@ -246,7 +246,8 @@ class RowCache {
     // ignore: prefer_collection_literals
     final cellContextMap = CellContextByFieldId();
     for (final fieldInfo in _fieldDelegate.fieldInfos) {
-      if (fieldInfo.field.visibility) {
+      if (fieldInfo.visibility != null &&
+          fieldInfo.visibility != FieldVisibility.AlwaysHidden) {
         cellContextMap[fieldInfo.id] = DatabaseCellContext(
           rowMeta: rowMeta,
           viewId: viewId,

+ 7 - 5
frontend/appflowy_flutter/lib/plugins/database_view/application/setting/property_bloc.dart

@@ -1,6 +1,8 @@
 import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
 import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
+import 'package:appflowy/plugins/database_view/application/field_settings/field_settings_service.dart';
 import 'package:appflowy_backend/log.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
@@ -28,13 +30,13 @@ class DatabasePropertyBloc
             _startListening();
           },
           setFieldVisibility: (_SetFieldVisibility value) async {
-            final fieldBackendSvc = FieldBackendService(
+            final fieldSettingsSvc = FieldSettingsBackendService(
               viewId: viewId,
-              fieldId: value.fieldId,
             );
 
-            final result = await fieldBackendSvc.updateField(
-              visibility: value.visibility,
+            final result = await fieldSettingsSvc.updateFieldSettings(
+              fieldId: value.fieldId,
+              fieldVisibility: value.visibility,
             );
 
             result.fold((l) => null, (err) => Log.error(err));
@@ -84,7 +86,7 @@ class DatabasePropertyEvent with _$DatabasePropertyEvent {
   const factory DatabasePropertyEvent.initial() = _Initial;
   const factory DatabasePropertyEvent.setFieldVisibility(
     String fieldId,
-    bool visibility,
+    FieldVisibility visibility,
   ) = _SetFieldVisibility;
   const factory DatabasePropertyEvent.didReceiveFieldUpdate(
     List<FieldInfo> fields,

+ 11 - 7
frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_header_bloc.dart

@@ -2,6 +2,7 @@ import 'package:appflowy/plugins/database_view/application/field/field_controlle
 import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
 import 'package:appflowy_backend/log.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
@@ -16,18 +17,25 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
   GridHeaderBloc({
     required this.viewId,
     required this.fieldController,
-  }) : super(GridHeaderState.initial(fieldController.fieldInfos)) {
+  }) : super(GridHeaderState.initial()) {
     on<GridHeaderEvent>(
       (event, emit) async {
         await event.map(
           initial: (_InitialHeader value) async {
             _startListening();
+            add(
+              GridHeaderEvent.didReceiveFieldUpdate(fieldController.fieldInfos),
+            );
           },
           didReceiveFieldUpdate: (_DidReceiveFieldUpdate value) {
             emit(
               state.copyWith(
                 fields: value.fields
-                    .where((element) => element.field.visibility)
+                    .where(
+                      (element) =>
+                          element.visibility != null &&
+                          element.visibility != FieldVisibility.AlwaysHidden,
+                    )
                     .toList(),
               ),
             );
@@ -83,9 +91,5 @@ class GridHeaderState with _$GridHeaderState {
   const factory GridHeaderState({required List<FieldInfo> fields}) =
       _GridHeaderState;
 
-  factory GridHeaderState.initial(List<FieldInfo> fields) {
-    // final List<FieldPB> newFields = List.from(fields);
-    // newFields.retainWhere((field) => field.visibility);
-    return GridHeaderState(fields: fields);
-  }
+  factory GridHeaderState.initial() => const GridHeaderState(fields: []);
 }

+ 27 - 9
frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_detail_bloc.dart

@@ -1,5 +1,7 @@
+import 'package:appflowy/plugins/database_view/application/field_settings/field_settings_service.dart';
 import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
 import 'package:appflowy_backend/log.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
@@ -10,18 +12,18 @@ part 'row_detail_bloc.freezed.dart';
 
 class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
   final RowBackendService rowService;
-  final RowController dataController;
+  final RowController rowController;
 
   RowDetailBloc({
-    required this.dataController,
-  })  : rowService = RowBackendService(viewId: dataController.viewId),
+    required this.rowController,
+  })  : rowService = RowBackendService(viewId: rowController.viewId),
         super(RowDetailState.initial()) {
     on<RowDetailEvent>(
       (event, emit) async {
         await event.when(
           initial: () async {
             await _startListening();
-            final cells = dataController.loadData();
+            final cells = rowController.loadData();
             if (!isClosed) {
               add(RowDetailEvent.didReceiveCellDatas(cells.values.toList()));
             }
@@ -32,9 +34,24 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
           deleteField: (fieldId) {
             _fieldBackendService(fieldId).deleteField();
           },
+          showField: (fieldId) async {
+            final result =
+                await FieldSettingsBackendService(viewId: rowController.viewId)
+                    .updateFieldSettings(
+              fieldId: fieldId,
+              fieldVisibility: FieldVisibility.AlwaysShown,
+            );
+            result.fold(
+              (l) {},
+              (err) => Log.error(err),
+            );
+          },
           hideField: (fieldId) async {
-            final result = await _fieldBackendService(fieldId).updateField(
-              visibility: false,
+            final result =
+                await FieldSettingsBackendService(viewId: rowController.viewId)
+                    .updateFieldSettings(
+              fieldId: fieldId,
+              fieldVisibility: FieldVisibility.AlwaysHidden,
             );
             result.fold(
               (l) {},
@@ -57,12 +74,12 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
 
   @override
   Future<void> close() async {
-    dataController.dispose();
+    rowController.dispose();
     return super.close();
   }
 
   Future<void> _startListening() async {
-    dataController.addListener(
+    rowController.addListener(
       onRowChanged: (cells, reason) {
         if (!isClosed) {
           add(RowDetailEvent.didReceiveCellDatas(cells.values.toList()));
@@ -73,7 +90,7 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
 
   FieldBackendService _fieldBackendService(String fieldId) {
     return FieldBackendService(
-      viewId: dataController.viewId,
+      viewId: rowController.viewId,
       fieldId: fieldId,
     );
   }
@@ -83,6 +100,7 @@ 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.deleteRow(String rowId) = _DeleteRow;
   const factory RowDetailEvent.duplicateRow(String rowId, String? groupId) =

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

@@ -164,11 +164,6 @@ class FieldCellButton extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    // Using this technique to have proper text ellipsis
-    // https://github.com/flutter/flutter/issues/18761#issuecomment-812390920
-    final text = Characters(field.name)
-        .replaceAll(Characters(''), Characters('\u{200B}'))
-        .toString();
     return FlowyButton(
       hoverColor: AFThemeExtension.of(context).greyHover,
       onTap: onTap,
@@ -177,7 +172,7 @@ class FieldCellButton extends StatelessWidget {
       ),
       radius: radius,
       text: FlowyText.medium(
-        text,
+        field.name,
         maxLines: maxLines,
         overflow: TextOverflow.ellipsis,
       ),

+ 4 - 3
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_editor.dart

@@ -102,7 +102,7 @@ class _FieldEditorState extends State<FieldEditor> {
       builder: (context, state) {
         return Padding(
           padding: const EdgeInsets.symmetric(horizontal: 12.0),
-          child: _HideFieldButton(
+          child: HideFieldButton(
             popoverMutex: popoverMutex,
             onHidden: () {
               state.field.fold(
@@ -236,11 +236,12 @@ class _DeleteFieldButton extends StatelessWidget {
   }
 }
 
-class _HideFieldButton extends StatelessWidget {
+@visibleForTesting
+class HideFieldButton extends StatelessWidget {
   final PopoverMutex popoverMutex;
   final VoidCallback? onHidden;
 
-  const _HideFieldButton({
+  const HideFieldButton({
     required this.popoverMutex,
     required this.onHidden,
     Key? key,

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

@@ -42,12 +42,10 @@ class _GridHeaderSliverAdaptorState extends State<GridHeaderSliverAdaptor> {
   Widget build(BuildContext context) {
     return BlocProvider(
       create: (context) {
-        final bloc = getIt<GridHeaderBloc>(
+        return getIt<GridHeaderBloc>(
           param1: widget.viewId,
           param2: widget.fieldController,
-        );
-        bloc.add(const GridHeaderEvent.initial());
-        return bloc;
+        )..add(const GridHeaderEvent.initial());
       },
       child: BlocBuilder<GridHeaderBloc, GridHeaderState>(
         buildWhen: (previous, current) =>
@@ -97,7 +95,6 @@ class _GridHeaderState extends State<_GridHeader> {
       buildWhen: (previous, current) => previous.fields != current.fields,
       builder: (context, state) {
         final cells = state.fields
-            .where((fieldInfo) => fieldInfo.field.visibility)
             .map(
               (field) => FieldContext(
                 viewId: widget.viewId,

+ 54 - 38
frontend/appflowy_flutter/lib/plugins/database_view/widgets/field/grid_property.dart

@@ -5,6 +5,7 @@ import 'package:appflowy/plugins/database_view/application/field/type_option/typ
 import 'package:appflowy/plugins/database_view/application/setting/property_bloc.dart';
 import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
 import 'package:appflowy/startup/startup.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
 
 import 'package:flowy_infra/theme_extension.dart';
@@ -44,7 +45,7 @@ class _DatabasePropertyListState extends State<DatabasePropertyList> {
       child: BlocBuilder<DatabasePropertyBloc, DatabasePropertyState>(
         builder: (context, state) {
           final cells = state.fieldContexts.map((field) {
-            return _GridPropertyCell(
+            return GridPropertyCell(
               key: ValueKey(field.id),
               viewId: widget.viewId,
               fieldInfo: field,
@@ -75,12 +76,13 @@ class _DatabasePropertyListState extends State<DatabasePropertyList> {
   }
 }
 
-class _GridPropertyCell extends StatefulWidget {
+@visibleForTesting
+class GridPropertyCell extends StatefulWidget {
   final FieldInfo fieldInfo;
   final String viewId;
   final PopoverMutex popoverMutex;
 
-  const _GridPropertyCell({
+  const GridPropertyCell({
     super.key,
     required this.fieldInfo,
     required this.viewId,
@@ -88,26 +90,22 @@ class _GridPropertyCell extends StatefulWidget {
   });
 
   @override
-  State<_GridPropertyCell> createState() => _GridPropertyCellState();
+  State<GridPropertyCell> createState() => _GridPropertyCellState();
 }
 
-class _GridPropertyCellState extends State<_GridPropertyCell> {
+class _GridPropertyCellState extends State<GridPropertyCell> {
   final PopoverController _popoverController = PopoverController();
 
   @override
   Widget build(BuildContext context) {
-    final checkmark = FlowySvg(
-      widget.fieldInfo.field.visibility ? FlowySvgs.show_m : FlowySvgs.hide_m,
+    final visiblity = widget.fieldInfo.visibility;
+    final visibleIcon = FlowySvg(
+      visiblity != null && visiblity != FieldVisibility.AlwaysHidden
+          ? FlowySvgs.show_m
+          : FlowySvgs.hide_m,
       color: Theme.of(context).iconTheme.color,
     );
 
-    return SizedBox(
-      height: GridSize.popoverItemHeight,
-      child: _editFieldButton(context, checkmark),
-    );
-  }
-
-  Widget _editFieldButton(BuildContext context, Widget checkmark) {
     return AppFlowyPopover(
       mutex: widget.popoverMutex,
       controller: _popoverController,
@@ -116,30 +114,40 @@ class _GridPropertyCellState extends State<_GridPropertyCell> {
       constraints: BoxConstraints.loose(const Size(240, 400)),
       triggerActions: PopoverTriggerFlags.none,
       margin: EdgeInsets.zero,
-      child: FlowyButton(
-        hoverColor: AFThemeExtension.of(context).lightGreyHover,
-        text: FlowyText.medium(
-          widget.fieldInfo.name,
-          color: AFThemeExtension.of(context).textColor,
-        ),
-        leftIcon: FlowySvg(
-          widget.fieldInfo.fieldType.icon(),
-          color: Theme.of(context).iconTheme.color,
-        ),
-        rightIcon: FlowyIconButton(
-          hoverColor: Colors.transparent,
-          onPressed: () {
-            context.read<DatabasePropertyBloc>().add(
-                  DatabasePropertyEvent.setFieldVisibility(
-                    widget.fieldInfo.id,
-                    !widget.fieldInfo.field.visibility,
-                  ),
-                );
-          },
-          icon: checkmark.padding(all: 6.0),
-        ),
-        onTap: () => _popoverController.show(),
-      ).padding(horizontal: 6.0),
+      child: SizedBox(
+        height: GridSize.popoverItemHeight,
+        child: FlowyButton(
+          hoverColor: AFThemeExtension.of(context).lightGreyHover,
+          text: FlowyText.medium(
+            widget.fieldInfo.name,
+            color: AFThemeExtension.of(context).textColor,
+          ),
+          leftIcon: FlowySvg(
+            widget.fieldInfo.fieldType.icon(),
+            color: Theme.of(context).iconTheme.color,
+          ),
+          rightIcon: FlowyIconButton(
+            hoverColor: Colors.transparent,
+            onPressed: () {
+              if (widget.fieldInfo.fieldSettings == null) {
+                return;
+              }
+
+              final newVisiblity = _newFieldVisibility(
+                widget.fieldInfo.fieldSettings!.visibility,
+              );
+              context.read<DatabasePropertyBloc>().add(
+                    DatabasePropertyEvent.setFieldVisibility(
+                      widget.fieldInfo.id,
+                      newVisiblity,
+                    ),
+                  );
+            },
+            icon: visibleIcon.padding(all: 6.0),
+          ),
+          onTap: () => _popoverController.show(),
+        ).padding(horizontal: 6.0),
+      ),
       popupBuilder: (BuildContext context) {
         return FieldEditor(
           viewId: widget.viewId,
@@ -151,4 +159,12 @@ class _GridPropertyCellState extends State<_GridPropertyCell> {
       },
     );
   }
+
+  FieldVisibility _newFieldVisibility(FieldVisibility current) {
+    return switch (current) {
+      FieldVisibility.AlwaysShown => FieldVisibility.AlwaysHidden,
+      FieldVisibility.AlwaysHidden => FieldVisibility.AlwaysShown,
+      _ => FieldVisibility.AlwaysHidden,
+    };
+  }
 }

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart

@@ -48,7 +48,7 @@ class _RowDetailPageState extends State<RowDetailPage> {
     return FlowyDialog(
       child: BlocProvider(
         create: (context) {
-          return RowDetailBloc(dataController: widget.rowController)
+          return RowDetailBloc(rowController: widget.rowController)
             ..add(const RowDetailEvent.initial());
         },
         child: ListView(

+ 0 - 1
frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs

@@ -17,7 +17,6 @@ use crate::database::mock_data::{COMPLETED, FACEBOOK, GOOGLE, PAUSED, PLANNED, T
 pub fn make_test_board() -> DatabaseData {
   let mut fields = vec![];
   let mut rows = vec![];
-
   // Iterate through the FieldType to create the corresponding Field.
   for field_type in FieldType::iter() {
     match field_type {