chore: improve setting tab bar dropdown (#3756)

* chore: improve setting tab bar dropdown

* test: fix integration tests
This commit is contained in:
Richard Shiue 2023-10-23 19:23:27 +08:00 committed by GitHub
parent d51c7f382f
commit b16a102f85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 178 additions and 172 deletions

View File

@ -38,7 +38,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/tab_bar_header.dart';
import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.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/database_layout_ext.dart';
import 'package:appflowy/plugins/database_view/widgets/field/grid_property.dart'; import 'package:appflowy/plugins/database_view/widgets/setting/setting_property_list.dart';
import 'package:appflowy/plugins/database_view/widgets/row/accessory/cell_accessory.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/cells.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor.dart';
@ -52,7 +52,6 @@ import 'package:appflowy/plugins/database_view/widgets/row/row_banner.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart'; import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_document.dart'; import 'package:appflowy/plugins/database_view/widgets/row/row_document.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_property.dart'; 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/database_view/widgets/setting/setting_button.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
@ -1132,7 +1131,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
/// Should call [tapDatabaseSettingButton] first. /// Should call [tapDatabaseSettingButton] first.
Future<void> tapViewPropertiesButton() async { Future<void> tapViewPropertiesButton() async {
final findSettingItem = find.byType(DatabaseSettingItem); final findSettingItem = find.byType(DatabaseSettingListPopover);
final findLayoutButton = find.byWidgetPredicate( final findLayoutButton = find.byWidgetPredicate(
(widget) => (widget) =>
widget is FlowyText && widget is FlowyText &&
@ -1149,7 +1148,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
/// Should call [tapDatabaseSettingButton] first. /// Should call [tapDatabaseSettingButton] first.
Future<void> tapDatabaseLayoutButton() async { Future<void> tapDatabaseLayoutButton() async {
final findSettingItem = find.byType(DatabaseSettingItem); final findSettingItem = find.byType(DatabaseSettingListPopover);
final findLayoutButton = find.byWidgetPredicate( final findLayoutButton = find.byWidgetPredicate(
(widget) => (widget) =>
widget is FlowyText && widget is FlowyText &&
@ -1165,7 +1164,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
} }
Future<void> tapCalendarLayoutSettingButton() async { Future<void> tapCalendarLayoutSettingButton() async {
final findSettingItem = find.byType(DatabaseSettingItem); final findSettingItem = find.byType(DatabaseSettingListPopover);
final findLayoutButton = find.byWidgetPredicate( final findLayoutButton = find.byWidgetPredicate(
(widget) => (widget) =>
widget is FlowyText && widget is FlowyText &&
@ -1505,7 +1504,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
) async { ) async {
final field = find.byWidgetPredicate( final field = find.byWidgetPredicate(
(widget) => (widget) =>
widget is GridPropertyCell && widget.fieldInfo.name == fieldName, widget is DatabasePropertyCell && widget.fieldInfo.name == fieldName,
); );
final toggleVisibilityButton = final toggleVisibilityButton =
find.descendant(of: field, matching: find.byType(FlowyIconButton)); find.descendant(of: field, matching: find.byType(FlowyIconButton));

View File

@ -25,34 +25,44 @@ class DatabasePropertyBloc
) { ) {
on<DatabasePropertyEvent>( on<DatabasePropertyEvent>(
(event, emit) async { (event, emit) async {
await event.map( await event.when(
initial: (_Initial value) { initial: () {
_startListening(); _startListening();
}, },
setFieldVisibility: (_SetFieldVisibility value) async { setFieldVisibility: (fieldId, visibility) async {
final fieldSettingsSvc = FieldSettingsBackendService( final fieldSettingsSvc = FieldSettingsBackendService(
viewId: viewId, viewId: viewId,
); );
final result = await fieldSettingsSvc.updateFieldSettings( final result = await fieldSettingsSvc.updateFieldSettings(
fieldId: value.fieldId, fieldId: fieldId,
fieldVisibility: value.visibility, fieldVisibility: visibility,
); );
result.fold((l) => null, (err) => Log.error(err)); result.fold((l) => null, (err) => Log.error(err));
}, },
didReceiveFieldUpdate: (_DidReceiveFieldUpdate value) { didReceiveFieldUpdate: (fields) {
emit(state.copyWith(fieldContexts: value.fields)); emit(state.copyWith(fieldContexts: fields));
}, },
moveField: (_MoveField value) async { moveField: (fieldId, fromIndex, toIndex) async {
if (fromIndex < toIndex) {
toIndex--;
}
final fieldContexts = List<FieldInfo>.from(state.fieldContexts);
fieldContexts.insert(
toIndex,
fieldContexts.removeAt(fromIndex),
);
emit(state.copyWith(fieldContexts: fieldContexts));
final fieldBackendService = FieldBackendService( final fieldBackendService = FieldBackendService(
viewId: viewId, viewId: viewId,
fieldId: value.fieldId, fieldId: fieldId,
); );
final result = await fieldBackendService.moveField( final result = await fieldBackendService.moveField(
value.fromIndex, fromIndex,
value.toIndex, toIndex,
); );
result.fold((l) => null, (r) => Log.error(r)); result.fold((l) => null, (r) => Log.error(r));

View File

@ -228,6 +228,7 @@ class LayoutDateField extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppFlowyPopover( return AppFlowyPopover(
direction: PopoverDirection.leftWithTopAligned, direction: PopoverDirection.leftWithTopAligned,
triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
constraints: BoxConstraints.loose(const Size(300, 400)), constraints: BoxConstraints.loose(const Size(300, 400)),
mutex: popoverMutex, mutex: popoverMutex,
offset: const Offset(-16, 0), offset: const Offset(-16, 0),
@ -346,6 +347,7 @@ class FirstDayOfWeek extends StatelessWidget {
return AppFlowyPopover( return AppFlowyPopover(
direction: PopoverDirection.leftWithTopAligned, direction: PopoverDirection.leftWithTopAligned,
constraints: BoxConstraints.loose(const Size(300, 400)), constraints: BoxConstraints.loose(const Size(300, 400)),
triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
mutex: popoverMutex, mutex: popoverMutex,
offset: const Offset(-16, 0), offset: const Offset(-16, 0),
popupBuilder: (context) { popupBuilder: (context) {

View File

@ -1,75 +0,0 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import '../../grid/presentation/layout/sizes.dart';
import 'setting_button.dart';
class DatabaseSettingList extends StatelessWidget {
final DatabaseController databaseContoller;
final Function(DatabaseSettingAction, DatabaseController) onAction;
const DatabaseSettingList({
required this.databaseContoller,
required this.onAction,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final cells = actionsForDatabaseLayout(databaseContoller.databaseLayout)
.map((action) {
return DatabaseSettingItem(
action: action,
onAction: (action) => onAction(action, databaseContoller),
);
}).toList();
return ListView.separated(
shrinkWrap: true,
controller: ScrollController(),
itemCount: cells.length,
separatorBuilder: (context, index) {
return VSpace(GridSize.typeOptionSeparatorHeight);
},
physics: StyledScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
return cells[index];
},
);
}
}
class DatabaseSettingItem extends StatelessWidget {
final DatabaseSettingAction action;
final Function(DatabaseSettingAction) onAction;
const DatabaseSettingItem({
required this.action,
required this.onAction,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
text: FlowyText.medium(
action.title(),
color: AFThemeExtension.of(context).textColor,
),
onTap: () => onAction(action),
leftIcon: FlowySvg(
action.iconData(),
color: Theme.of(context).iconTheme.color,
),
),
);
}
}

View File

@ -11,12 +11,10 @@ import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:styled_widget/styled_widget.dart';
import '../../grid/presentation/layout/sizes.dart'; import '../../grid/presentation/layout/sizes.dart';
import '../../grid/presentation/widgets/toolbar/grid_layout.dart'; import '../../grid/presentation/widgets/toolbar/grid_layout.dart';
import '../field/grid_property.dart'; import 'setting_property_list.dart';
import 'database_setting.dart';
class SettingButton extends StatefulWidget { class SettingButton extends StatefulWidget {
final DatabaseController databaseController; final DatabaseController databaseController;
@ -39,7 +37,6 @@ class _SettingButtonState extends State<SettingButton> {
constraints: BoxConstraints.loose(const Size(200, 400)), constraints: BoxConstraints.loose(const Size(200, 400)),
direction: PopoverDirection.bottomWithCenterAligned, direction: PopoverDirection.bottomWithCenterAligned,
offset: const Offset(0, 8), offset: const Offset(0, 8),
margin: EdgeInsets.zero,
triggerActions: PopoverTriggerFlags.none, triggerActions: PopoverTriggerFlags.none,
child: FlowyTextButton( child: FlowyTextButton(
LocaleKeys.settings_title.tr(), LocaleKeys.settings_title.tr(),
@ -53,7 +50,7 @@ class _SettingButtonState extends State<SettingButton> {
onPressed: () => _popoverController.show(), onPressed: () => _popoverController.show(),
), ),
popupBuilder: (BuildContext context) { popupBuilder: (BuildContext context) {
return _DatabaseSettingListPopover( return DatabaseSettingListPopover(
databaseController: widget.databaseController, databaseController: widget.databaseController,
); );
}, },
@ -61,10 +58,10 @@ class _SettingButtonState extends State<SettingButton> {
} }
} }
class _DatabaseSettingListPopover extends StatefulWidget { class DatabaseSettingListPopover extends StatefulWidget {
final DatabaseController databaseController; final DatabaseController databaseController;
const _DatabaseSettingListPopover({ const DatabaseSettingListPopover({
required this.databaseController, required this.databaseController,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@ -74,59 +71,40 @@ class _DatabaseSettingListPopover extends StatefulWidget {
} }
class _DatabaseSettingListPopoverState class _DatabaseSettingListPopoverState
extends State<_DatabaseSettingListPopover> { extends State<DatabaseSettingListPopover> {
DatabaseSettingAction? _action; late final PopoverMutex popoverMutex;
@override
void initState() {
super.initState();
popoverMutex = PopoverMutex();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_action == null) { final cells =
return DatabaseSettingList( actionsForDatabaseLayout(widget.databaseController.databaseLayout)
databaseContoller: widget.databaseController, .map(
onAction: (action, settingContext) { (action) => action.build(
setState(() { context,
_action = action; widget.databaseController,
}); popoverMutex,
},
).padding(all: 6.0);
} else {
switch (_action!) {
case DatabaseSettingAction.showLayout:
return DatabaseLayoutList(
viewId: widget.databaseController.viewId,
currentLayout: widget.databaseController.databaseLayout,
);
case DatabaseSettingAction.showGroup:
return DatabaseGroupList(
viewId: widget.databaseController.viewId,
fieldController: widget.databaseController.fieldController,
onDismissed: () {
// widget.popoverController.close();
},
);
case DatabaseSettingAction.showProperties:
return Column(
mainAxisSize: MainAxisSize.min,
children: [
DatabasePropertyList(
viewId: widget.databaseController.viewId,
fieldController: widget.databaseController.fieldController,
), ),
FlowyText.regular( )
LocaleKeys.grid_settings_reorderPropertiesTooltip.tr(), .toList();
),
const VSpace(8), return ListView.separated(
], shrinkWrap: true,
); controller: ScrollController(),
case DatabaseSettingAction.showCalendarLayout: itemCount: cells.length,
return CalendarLayoutSetting( separatorBuilder: (context, index) {
viewId: widget.databaseController.viewId, return VSpace(GridSize.typeOptionSeparatorHeight);
fieldController: widget.databaseController.fieldController, },
calendarSettingController: ICalendarSettingImpl( physics: StyledScrollPhysics(),
widget.databaseController, itemBuilder: (BuildContext context, int index) {
), return cells[index];
); },
} );
}
} }
} }
@ -179,6 +157,58 @@ extension DatabaseSettingActionExtension on DatabaseSettingAction {
return LocaleKeys.calendar_settings_name.tr(); return LocaleKeys.calendar_settings_name.tr();
} }
} }
Widget build(
BuildContext context,
DatabaseController databaseController,
PopoverMutex popoverMutex,
) {
final popover = switch (this) {
DatabaseSettingAction.showLayout => DatabaseLayoutList(
viewId: databaseController.viewId,
currentLayout: databaseController.databaseLayout,
),
DatabaseSettingAction.showGroup => DatabaseGroupList(
viewId: databaseController.viewId,
fieldController: databaseController.fieldController,
onDismissed: () {},
),
DatabaseSettingAction.showProperties => DatabasePropertyList(
viewId: databaseController.viewId,
fieldController: databaseController.fieldController,
),
DatabaseSettingAction.showCalendarLayout => CalendarLayoutSetting(
viewId: databaseController.viewId,
fieldController: databaseController.fieldController,
calendarSettingController: ICalendarSettingImpl(
databaseController,
),
),
};
return AppFlowyPopover(
triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
direction: PopoverDirection.leftWithTopAligned,
mutex: popoverMutex,
margin: EdgeInsets.zero,
offset: const Offset(-16, 0),
child: SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
text: FlowyText.medium(
title(),
color: AFThemeExtension.of(context).textColor,
),
leftIcon: FlowySvg(
iconData(),
color: Theme.of(context).iconTheme.color,
),
),
),
popupBuilder: (context) => popover,
);
}
} }
/// Returns the list of actions that should be shown for the given database layout. /// Returns the list of actions that should be shown for the given database layout.

View File

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; 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/field_info.dart';
@ -7,12 +9,12 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.dart';
import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:reorderables/reorderables.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import '../../grid/presentation/layout/sizes.dart'; import '../../grid/presentation/layout/sizes.dart';
@ -44,29 +46,43 @@ class _DatabasePropertyListState extends State<DatabasePropertyList> {
)..add(const DatabasePropertyEvent.initial()), )..add(const DatabasePropertyEvent.initial()),
child: BlocBuilder<DatabasePropertyBloc, DatabasePropertyState>( child: BlocBuilder<DatabasePropertyBloc, DatabasePropertyState>(
builder: (context, state) { builder: (context, state) {
final cells = state.fieldContexts.map((field) { final cells = state.fieldContexts.mapIndexed((index, field) {
return GridPropertyCell( return DatabasePropertyCell(
key: ValueKey(field.id), key: ValueKey(field.id),
viewId: widget.viewId, viewId: widget.viewId,
fieldInfo: field, fieldInfo: field,
popoverMutex: _popoverMutex, popoverMutex: _popoverMutex,
index: index,
); );
}).toList(); }).toList();
return ReorderableColumn( return ReorderableListView(
needsLongPressDraggable: false, proxyDecorator: (child, index, _) => Material(
buildDraggableFeedback: (context, constraints, child) => color: Colors.transparent,
ConstrainedBox( child: Stack(
constraints: constraints, children: [
child: Material(color: Colors.transparent, child: child), child,
), MouseRegion(
onReorder: (from, to) => context.read<DatabasePropertyBloc>().add( cursor: Platform.isWindows
DatabasePropertyEvent.moveField( ? SystemMouseCursors.click
fieldId: cells[from].fieldInfo.id, : SystemMouseCursors.grabbing,
fromIndex: from, child: const SizedBox.expand(),
toIndex: to,
), ),
), ],
),
),
buildDefaultDragHandles: false,
shrinkWrap: true,
onReorder: (from, to) {
context.read<DatabasePropertyBloc>().add(
DatabasePropertyEvent.moveField(
fieldId: cells[from].fieldInfo.id,
fromIndex: from,
toIndex: to,
),
);
},
onReorderStart: (_) => _popoverMutex.close(),
padding: const EdgeInsets.symmetric(vertical: 6.0), padding: const EdgeInsets.symmetric(vertical: 6.0),
children: cells, children: cells,
); );
@ -77,23 +93,25 @@ class _DatabasePropertyListState extends State<DatabasePropertyList> {
} }
@visibleForTesting @visibleForTesting
class GridPropertyCell extends StatefulWidget { class DatabasePropertyCell extends StatefulWidget {
final FieldInfo fieldInfo; final FieldInfo fieldInfo;
final String viewId; final String viewId;
final PopoverMutex popoverMutex; final PopoverMutex popoverMutex;
final int index;
const GridPropertyCell({ const DatabasePropertyCell({
super.key, super.key,
required this.fieldInfo, required this.fieldInfo,
required this.viewId, required this.viewId,
required this.popoverMutex, required this.popoverMutex,
required this.index,
}); });
@override @override
State<GridPropertyCell> createState() => _GridPropertyCellState(); State<DatabasePropertyCell> createState() => _DatabasePropertyCellState();
} }
class _GridPropertyCellState extends State<GridPropertyCell> { class _DatabasePropertyCellState extends State<DatabasePropertyCell> {
final PopoverController _popoverController = PopoverController(); final PopoverController _popoverController = PopoverController();
@override @override
@ -109,7 +127,7 @@ class _GridPropertyCellState extends State<GridPropertyCell> {
return AppFlowyPopover( return AppFlowyPopover(
mutex: widget.popoverMutex, mutex: widget.popoverMutex,
controller: _popoverController, controller: _popoverController,
offset: const Offset(8, 0), offset: const Offset(-8, 0),
direction: PopoverDirection.leftWithTopAligned, direction: PopoverDirection.leftWithTopAligned,
constraints: BoxConstraints.loose(const Size(240, 400)), constraints: BoxConstraints.loose(const Size(240, 400)),
triggerActions: PopoverTriggerFlags.none, triggerActions: PopoverTriggerFlags.none,
@ -122,9 +140,31 @@ class _GridPropertyCellState extends State<GridPropertyCell> {
widget.fieldInfo.name, widget.fieldInfo.name,
color: AFThemeExtension.of(context).textColor, color: AFThemeExtension.of(context).textColor,
), ),
leftIcon: FlowySvg( leftIconSize: const Size(36, 18),
widget.fieldInfo.fieldType.icon(), leftIcon: Row(
color: Theme.of(context).iconTheme.color, children: [
ReorderableDragStartListener(
index: widget.index,
child: MouseRegion(
cursor: Platform.isWindows
? SystemMouseCursors.click
: SystemMouseCursors.grab,
child: SizedBox(
width: 14,
height: 14,
child: FlowySvg(
FlowySvgs.drag_element_s,
color: Theme.of(context).iconTheme.color,
),
),
),
),
const HSpace(6.0),
FlowySvg(
widget.fieldInfo.fieldType.icon(),
color: Theme.of(context).iconTheme.color,
),
],
), ),
rightIcon: FlowyIconButton( rightIcon: FlowyIconButton(
hoverColor: Colors.transparent, hoverColor: Colors.transparent,