fix: some mobile database bugs (#4738)

* chore: no autofocus on field name

* refactor: one, two, many

* chore: reduce spacing between tabbar header and grid header

* fix: update field type

* fix: cannot edit view name

* fix: title for select field type bottom sheet

* fix: forget checks, just do it
This commit is contained in:
Richard Shiue 2024-02-26 15:13:38 +08:00 committed by GitHub
parent 95bfda9dc6
commit daedf95c95
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 238 additions and 223 deletions

View File

@ -40,7 +40,7 @@ class MobileRowDetailCreateFieldButton extends StatelessWidget {
LocaleKeys.grid_field_newProperty.tr(),
fontSize: 15,
),
onPressed: () => showCreateFieldBottomSheet(context, viewId),
onPressed: () => mobileCreateFieldWorkflow(context, viewId),
icon: const FlowySvg(FlowySvgs.add_m),
),
);

View File

@ -1,7 +1,10 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/database/field/mobile_field_bottom_sheets.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
class OptionTextField extends StatelessWidget {
@ -10,31 +13,43 @@ class OptionTextField extends StatelessWidget {
required this.controller,
required this.type,
required this.onTextChanged,
required this.onFieldTypeChanged,
});
final TextEditingController controller;
final FieldType type;
final void Function(String value) onTextChanged;
final void Function(FieldType value) onFieldTypeChanged;
@override
Widget build(BuildContext context) {
return FlowyOptionTile.textField(
controller: controller,
autofocus: true,
textFieldPadding: const EdgeInsets.symmetric(horizontal: 12.0),
onTextChanged: onTextChanged,
leftIcon: Container(
height: 38,
width: 38,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: type.mobileIconBackgroundColor,
),
child: Center(
child: FlowySvg(
type.svgData,
blendMode: null,
size: const Size.square(22),
leftIcon: GestureDetector(
onTap: () async {
final fieldType = await showFieldTypeGridBottomSheet(
context,
title: LocaleKeys.grid_field_editProperty.tr(),
);
if (fieldType != null) {
onFieldTypeChanged(fieldType);
}
},
child: Container(
height: 38,
width: 38,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: type.mobileIconBackgroundColor,
),
child: Center(
child: FlowySvg(
type.svgData,
blendMode: null,
size: const Size.square(22),
),
),
),
),

View File

@ -61,7 +61,7 @@ class _MobileEditPropertyScreenState extends State<MobileEditPropertyScreen> {
body: MobileFieldEditor(
mode: FieldOptionMode.edit,
isPrimary: widget.field.isPrimary,
defaultValues: _fieldOptionValues,
defaultValues: FieldOptionValues.fromField(field: widget.field.field),
actions: [
widget.field.fieldSettings?.visibility.isVisibleState() ?? true
? FieldOptionAction.hide
@ -69,9 +69,25 @@ class _MobileEditPropertyScreenState extends State<MobileEditPropertyScreen> {
FieldOptionAction.duplicate,
FieldOptionAction.delete,
],
onOptionValuesChanged: (newFieldOptionValues) {
onOptionValuesChanged: (fieldOptionValues) async {
await fieldService.updateField(name: fieldOptionValues.name);
await FieldBackendService.updateFieldType(
viewId: widget.viewId,
fieldId: widget.field.id,
fieldType: fieldOptionValues.type,
);
final data = fieldOptionValues.getTypeOptionData();
if (data != null) {
await FieldBackendService.updateFieldTypeOption(
viewId: widget.viewId,
fieldId: widget.field.id,
typeOptionData: data,
);
}
setState(() {
_fieldOptionValues = newFieldOptionValues;
_fieldOptionValues = fieldOptionValues;
});
},
onAction: (action) {

View File

@ -29,20 +29,17 @@ const mobileSupportedFieldTypes = [
FieldType.Checklist,
];
/// Shows the field type grid and upon selection, allow users to edit the
/// field's properties and saving it when the user clicks save.
void showCreateFieldBottomSheet(
BuildContext context,
String viewId, {
OrderObjectPositionPB? position,
Future<FieldType?> showFieldTypeGridBottomSheet(
BuildContext context, {
required String title,
}) {
showMobileBottomSheet(
return showMobileBottomSheet<FieldType>(
context,
showHeader: true,
showDragHandle: true,
showCloseButton: true,
elevation: 20,
title: LocaleKeys.grid_field_newProperty.tr(),
title: title,
backgroundColor: Theme.of(context).colorScheme.surface,
enableDraggableScrollable: true,
builder: (context) {
@ -53,24 +50,8 @@ void showCreateFieldBottomSheet(
backgroundColor: fieldType.mobileIconBackgroundColor,
text: fieldType.i18n,
icon: fieldType.svgData,
onTap: (_, fieldType) async {
final optionValues = await context.push<FieldOptionValues>(
Uri(
path: MobileNewPropertyScreen.routeName,
queryParameters: {
MobileNewPropertyScreen.argViewId: viewId,
MobileNewPropertyScreen.argFieldTypeId:
fieldType.value.toString(),
},
).toString(),
);
if (optionValues != null) {
await optionValues.create(viewId: viewId, position: position);
if (context.mounted) {
context.pop();
}
}
},
onTap: (context, fieldType) =>
Navigator.of(context).pop(fieldType),
),
)
.toList();
@ -85,6 +66,37 @@ void showCreateFieldBottomSheet(
);
}
/// Shows the field type grid and upon selection, allow users to edit the
/// field's properties and saving it when the user clicks save.
void mobileCreateFieldWorkflow(
BuildContext context,
String viewId, {
OrderObjectPositionPB? position,
}) async {
final fieldType = await showFieldTypeGridBottomSheet(
context,
title: LocaleKeys.grid_field_newProperty.tr(),
);
if (fieldType == null || !context.mounted) {
return;
}
final optionValues = await context.push<FieldOptionValues>(
Uri(
path: MobileNewPropertyScreen.routeName,
queryParameters: {
MobileNewPropertyScreen.argViewId: viewId,
MobileNewPropertyScreen.argFieldTypeId: fieldType.value.toString(),
},
).toString(),
);
if (optionValues != null) {
await optionValues.create(viewId: viewId, position: position);
}
if (context.mounted) {
context.pop();
}
}
/// Used to edit a field.
Future<FieldOptionValues?> showEditFieldScreen(
BuildContext context,
@ -104,6 +116,7 @@ Future<FieldOptionValues?> showEditFieldScreen(
void showQuickEditField(
BuildContext context,
String viewId,
FieldController fieldController,
FieldInfo fieldInfo,
) {
showMobileBottomSheet(
@ -114,6 +127,7 @@ void showQuickEditField(
return SingleChildScrollView(
child: QuickEditField(
viewId: viewId,
fieldController: fieldController,
fieldInfo: fieldInfo,
),
);

View File

@ -5,7 +5,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/flowy_search_text_field.dart';
import 'package:appflowy/mobile/presentation/base/option_color_list.dart';
import 'package:appflowy/mobile/presentation/base/type_option_menu_item.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/database/card/card_detail/widgets/widgets.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
@ -14,7 +13,6 @@ import 'package:appflowy/plugins/database/domain/field_service.dart';
import 'package:appflowy/plugins/database/application/field/type_option/number_format_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/type_option/date/date_time_format.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:collection/collection.dart';
@ -223,6 +221,18 @@ class _MobileFieldEditorState extends State<MobileFieldEditor> {
isFieldNameChanged = true;
_updateOptionValues(name: value);
},
onFieldTypeChanged: (type) {
setState(
() {
if (widget.mode == FieldOptionMode.add &&
!isFieldNameChanged) {
controller.text = type.i18n;
_updateOptionValues(name: type.i18n);
}
_updateOptionValues(type: type);
},
);
},
),
const _Divider(),
if (!widget.isPrimary) ...[
@ -444,40 +454,14 @@ class _PropertyType extends StatelessWidget {
),
],
),
onTap: () {
showMobileBottomSheet(
onTap: () async {
final fieldType = await showFieldTypeGridBottomSheet(
context,
showHeader: true,
showDragHandle: true,
showCloseButton: true,
elevation: 20,
title: LocaleKeys.grid_field_editProperty.tr(),
backgroundColor: Theme.of(context).colorScheme.surface,
enableDraggableScrollable: true,
builder: (context) {
final typeOptionMenuItemValue = mobileSupportedFieldTypes
.map(
(fieldType) => TypeOptionMenuItemValue(
value: fieldType,
backgroundColor: fieldType.mobileIconBackgroundColor,
text: fieldType.i18n,
icon: fieldType.svgData,
onTap: (_, fieldType) {
onSelected(fieldType);
context.pop();
},
),
)
.toList();
return Padding(
padding: EdgeInsets.all(16 * context.scale),
child: TypeOptionMenu<FieldType>(
values: typeOptionMenuItemValue,
scaleFactor: context.scale,
),
);
},
);
if (fieldType != null) {
onSelected(fieldType);
}
},
);
}

View File

@ -2,27 +2,29 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/database/card/card_detail/widgets/widgets.dart';
import 'package:appflowy/mobile/presentation/database/field/mobile_field_bottom_sheets.dart';
import 'package:appflowy/mobile/presentation/database/field/mobile_full_field_editor.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
import 'package:appflowy/plugins/database/application/field/field_editor_bloc.dart';
import 'package:appflowy/plugins/database/domain/field_backend_service.dart';
import 'package:appflowy/plugins/database/application/field/field_info.dart';
import 'package:appflowy/plugins/database/domain/field_service.dart';
import 'package:appflowy/plugins/database/widgets/setting/field_visibility_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:protobuf/protobuf.dart' hide FieldInfo;
class QuickEditField extends StatefulWidget {
const QuickEditField({
super.key,
required this.viewId,
required this.fieldController,
required this.fieldInfo,
});
final String viewId;
final FieldController fieldController;
final FieldInfo fieldInfo;
@override
@ -38,14 +40,10 @@ class _QuickEditFieldState extends State<QuickEditField> {
);
late FieldVisibility fieldVisibility;
late FieldOptionValues _fieldOptionValues;
@override
void initState() {
super.initState();
_fieldOptionValues =
FieldOptionValues.fromField(field: widget.fieldInfo.field);
fieldVisibility = widget.fieldInfo.fieldSettings?.visibility ??
FieldVisibility.AlwaysShown;
controller.text = widget.fieldInfo.field.name;
@ -59,140 +57,125 @@ class _QuickEditFieldState extends State<QuickEditField> {
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const VSpace(16),
OptionTextField(
controller: controller,
type: _fieldOptionValues.type,
onTextChanged: (text) async {
await service.updateName(text);
},
),
const _Divider(),
FlowyOptionTile.text(
text: LocaleKeys.grid_field_editProperty.tr(),
leftIcon: const FlowySvg(FlowySvgs.m_field_edit_s),
onTap: () async {
widget.fieldInfo.field.freeze();
final field = widget.fieldInfo.field.rebuild((field) {
field.name = controller.text;
field.fieldType = _fieldOptionValues.type;
field.typeOptionData =
_fieldOptionValues.getTypeOptionData() ?? [];
});
final fieldOptionValues = await showEditFieldScreen(
context,
widget.viewId,
widget.fieldInfo.copyWith(field: field),
);
if (fieldOptionValues != null) {
if (fieldOptionValues.name != _fieldOptionValues.name) {
await service.updateName(fieldOptionValues.name);
}
if (fieldOptionValues.type != _fieldOptionValues.type) {
await FieldBackendService.updateFieldType(
viewId: widget.viewId,
fieldId: widget.fieldInfo.id,
fieldType: fieldOptionValues.type,
);
}
final data = fieldOptionValues.getTypeOptionData();
if (data != null) {
await FieldBackendService.updateFieldTypeOption(
viewId: widget.viewId,
fieldId: widget.fieldInfo.id,
typeOptionData: data,
);
}
setState(() {
_fieldOptionValues = fieldOptionValues;
controller.text = fieldOptionValues.name;
});
} else {
if (context.mounted) {
context.pop();
}
}
},
),
if (!widget.fieldInfo.isPrimary)
FlowyOptionTile.text(
showTopBorder: false,
text: fieldVisibility.isVisibleState()
? LocaleKeys.grid_field_hide.tr()
: LocaleKeys.grid_field_show.tr(),
leftIcon: const FlowySvg(FlowySvgs.m_field_hide_s),
onTap: () async {
context.pop();
if (fieldVisibility.isVisibleState()) {
await service.hide();
} else {
await service.hide();
}
},
),
if (!widget.fieldInfo.isPrimary)
FlowyOptionTile.text(
showTopBorder: false,
text: LocaleKeys.grid_field_insertLeft.tr(),
leftIcon: const FlowySvg(FlowySvgs.m_filed_insert_left_s),
onTap: () async {
context.pop();
showCreateFieldBottomSheet(
context,
widget.viewId,
position: OrderObjectPositionPB(
position: OrderObjectPositionTypePB.Before,
objectId: widget.fieldInfo.id,
),
);
},
),
FlowyOptionTile.text(
showTopBorder: false,
text: LocaleKeys.grid_field_insertRight.tr(),
leftIcon: const FlowySvg(FlowySvgs.m_filed_insert_right_s),
onTap: () async {
context.pop();
showCreateFieldBottomSheet(
context,
widget.viewId,
position: OrderObjectPositionPB(
position: OrderObjectPositionTypePB.After,
objectId: widget.fieldInfo.id,
return BlocProvider(
create: (_) => FieldEditorBloc(
viewId: widget.viewId,
fieldController: widget.fieldController,
field: widget.fieldInfo.field,
)..add(const FieldEditorEvent.initial()),
child: BlocConsumer<FieldEditorBloc, FieldEditorState>(
listenWhen: (previous, current) =>
previous.field.name != current.field.name,
listener: (context, state) => controller.text = state.field.name,
builder: (context, state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const VSpace(16),
OptionTextField(
controller: controller,
type: state.field.fieldType,
onTextChanged: (text) {
context
.read<FieldEditorBloc>()
.add(FieldEditorEvent.renameField(text));
},
onFieldTypeChanged: (fieldType) {
context
.read<FieldEditorBloc>()
.add(FieldEditorEvent.switchFieldType(fieldType));
},
),
);
},
),
if (!widget.fieldInfo.isPrimary) ...[
FlowyOptionTile.text(
showTopBorder: false,
text: LocaleKeys.button_duplicate.tr(),
leftIcon: const FlowySvg(FlowySvgs.m_field_copy_s),
onTap: () async {
context.pop();
await service.duplicate();
},
),
FlowyOptionTile.text(
showTopBorder: false,
text: LocaleKeys.button_delete.tr(),
textColor: Theme.of(context).colorScheme.error,
leftIcon: FlowySvg(
FlowySvgs.m_field_delete_s,
color: Theme.of(context).colorScheme.error,
),
onTap: () async {
context.pop();
await service.delete();
},
),
],
],
const _Divider(),
FlowyOptionTile.text(
text: LocaleKeys.grid_field_editProperty.tr(),
leftIcon: const FlowySvg(FlowySvgs.m_field_edit_s),
onTap: () {
showEditFieldScreen(
context,
widget.viewId,
state.field,
);
context.pop();
},
),
if (!widget.fieldInfo.isPrimary)
FlowyOptionTile.text(
showTopBorder: false,
text: fieldVisibility.isVisibleState()
? LocaleKeys.grid_field_hide.tr()
: LocaleKeys.grid_field_show.tr(),
leftIcon: const FlowySvg(FlowySvgs.m_field_hide_s),
onTap: () async {
context.pop();
if (fieldVisibility.isVisibleState()) {
await service.hide();
} else {
await service.hide();
}
},
),
if (!widget.fieldInfo.isPrimary)
FlowyOptionTile.text(
showTopBorder: false,
text: LocaleKeys.grid_field_insertLeft.tr(),
leftIcon: const FlowySvg(FlowySvgs.m_filed_insert_left_s),
onTap: () {
context.pop();
mobileCreateFieldWorkflow(
context,
widget.viewId,
position: OrderObjectPositionPB(
position: OrderObjectPositionTypePB.Before,
objectId: widget.fieldInfo.id,
),
);
},
),
FlowyOptionTile.text(
showTopBorder: false,
text: LocaleKeys.grid_field_insertRight.tr(),
leftIcon: const FlowySvg(FlowySvgs.m_filed_insert_right_s),
onTap: () {
context.pop();
mobileCreateFieldWorkflow(
context,
widget.viewId,
position: OrderObjectPositionPB(
position: OrderObjectPositionTypePB.After,
objectId: widget.fieldInfo.id,
),
);
},
),
if (!widget.fieldInfo.isPrimary) ...[
FlowyOptionTile.text(
showTopBorder: false,
text: LocaleKeys.button_duplicate.tr(),
leftIcon: const FlowySvg(FlowySvgs.m_field_copy_s),
onTap: () {
context.pop();
service.duplicate();
},
),
FlowyOptionTile.text(
showTopBorder: false,
text: LocaleKeys.button_delete.tr(),
textColor: Theme.of(context).colorScheme.error,
leftIcon: FlowySvg(
FlowySvgs.m_field_delete_s,
color: Theme.of(context).colorScheme.error,
),
onTap: () {
context.pop();
service.delete();
},
),
],
],
);
},
),
);
}
}

View File

@ -213,7 +213,7 @@ class _NewDatabaseFieldTile extends StatelessWidget {
color: Theme.of(context).hintColor,
),
textColor: Theme.of(context).hintColor,
onTap: () => showCreateFieldBottomSheet(context, viewId),
onTap: () => mobileCreateFieldWorkflow(context, viewId),
);
}
}

View File

@ -30,10 +30,9 @@ class MobileDatabaseViewQuickActions extends StatelessWidget {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
_actionButton(context, _Action.edit, () {
_actionButton(context, _Action.edit, () async {
final bloc = context.read<ViewBloc>();
context.pop();
showTransitionMobileBottomSheet(
await showTransitionMobileBottomSheet(
context,
showHeader: true,
showDoneButton: true,
@ -45,6 +44,9 @@ class MobileDatabaseViewQuickActions extends StatelessWidget {
),
),
);
if (context.mounted) {
context.pop();
}
}),
_divider(),
_actionButton(

View File

@ -38,7 +38,8 @@ class MobileFieldButton extends StatelessWidget {
width: 200,
decoration: _getDecoration(context),
child: FlowyButton(
onTap: () => showQuickEditField(context, viewId, fieldInfo),
onTap: () =>
showQuickEditField(context, viewId, fieldController, fieldInfo),
radius: radius,
margin: margin,
leftIconSize: const Size.square(18),

View File

@ -182,7 +182,7 @@ class _CreateFieldButtonState extends State<CreateFieldButton> {
color: Theme.of(context).hintColor,
),
hoverColor: AFThemeExtension.of(context).greyHover,
onTap: () => showCreateFieldBottomSheet(context, widget.viewId),
onTap: () => mobileCreateFieldWorkflow(context, widget.viewId),
leftIconSize: const Size.square(18),
leftIcon: FlowySvg(
FlowySvgs.add_s,

View File

@ -26,7 +26,7 @@ class _MobileTabBarHeaderState extends State<MobileTabBarHeader> {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 14),
padding: const EdgeInsets.only(top: 14.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [