From af07b53484ca32169e941754a60228c61ce7f99a Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Fri, 1 Dec 2023 20:16:43 +0800 Subject: [PATCH] feat: new property ui revamp (#4063) * feat: implement new property page * feat: implement date option * feat: add include time * feat: add field header * feat: implement new property page * feat: add icons * feat: add color list * feat: add color list * feat: integrate new property page * feat: support creating property with values * fix: select option doesn't work * feat: set textinputaction to done --- .../presentation/base/app_bar_actions.dart | 26 + .../presentation/base/option_color_list.dart | 56 ++ .../show_mobile_bottom_sheet.dart | 12 +- .../mobile_create_field_screen.dart | 100 ++ .../card_detail/widgets/_field_options.dart | 114 +++ .../widgets/_new_field_option.dart | 867 ++++++++++++++++++ .../mobile_date_picker_screen.dart | 63 +- .../flowy_mobile_option_decorate_box.dart | 1 + .../widgets/flowy_option_tile.dart | 279 +++++- .../type_option/type_option_service.dart | 18 +- .../widgets/header/grid_header.dart | 61 +- .../tab_bar/mobile/mobile_tab_bar_header.dart | 1 + .../mobile_select_option_editor.dart | 88 +- .../lib/startup/tasks/generate_router.dart | 25 + .../lib/util/field_type_extension.dart | 46 + .../lib/style_widget/text_field.dart | 3 + .../resources/flowy_icons/16x/blue_check.svg | 3 + .../flowy_icons/16x/field_option_checkbox.svg | 4 + .../flowy_icons/16x/field_option_date.svg | 3 + .../flowy_icons/16x/field_option_number.svg | 3 + .../flowy_icons/16x/field_option_select.svg | 4 + .../flowy_icons/16x/field_option_text.svg | 4 + .../flowy_icons/16x/field_option_url.svg | 4 + .../flowy_icons/40x/field_option_checkbox.svg | 4 + .../40x/field_option_checklist.svg | 4 + .../flowy_icons/40x/field_option_date.svg | 4 + .../flowy_icons/40x/field_option_document.svg | 5 + .../flowy_icons/40x/field_option_email.svg | 4 + .../flowy_icons/40x/field_option_file.svg | 4 + .../flowy_icons/40x/field_option_formula.svg | 4 + .../40x/field_option_multiselect.svg | 4 + .../flowy_icons/40x/field_option_number.svg | 8 + .../flowy_icons/40x/field_option_person.svg | 8 + .../flowy_icons/40x/field_option_phone.svg | 4 + .../flowy_icons/40x/field_option_relation.svg | 8 + .../flowy_icons/40x/field_option_select.svg | 5 + .../flowy_icons/40x/field_option_text.svg | 4 + .../flowy_icons/40x/field_option_url.svg | 4 + frontend/resources/translations/en.json | 4 +- 39 files changed, 1704 insertions(+), 159 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/base/option_color_list.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_create_field_screen.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/_field_options.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/_new_field_option.dart create mode 100644 frontend/appflowy_flutter/lib/util/field_type_extension.dart create mode 100644 frontend/resources/flowy_icons/16x/blue_check.svg create mode 100644 frontend/resources/flowy_icons/16x/field_option_checkbox.svg create mode 100644 frontend/resources/flowy_icons/16x/field_option_date.svg create mode 100644 frontend/resources/flowy_icons/16x/field_option_number.svg create mode 100644 frontend/resources/flowy_icons/16x/field_option_select.svg create mode 100644 frontend/resources/flowy_icons/16x/field_option_text.svg create mode 100644 frontend/resources/flowy_icons/16x/field_option_url.svg create mode 100644 frontend/resources/flowy_icons/40x/field_option_checkbox.svg create mode 100644 frontend/resources/flowy_icons/40x/field_option_checklist.svg create mode 100644 frontend/resources/flowy_icons/40x/field_option_date.svg create mode 100644 frontend/resources/flowy_icons/40x/field_option_document.svg create mode 100644 frontend/resources/flowy_icons/40x/field_option_email.svg create mode 100644 frontend/resources/flowy_icons/40x/field_option_file.svg create mode 100644 frontend/resources/flowy_icons/40x/field_option_formula.svg create mode 100644 frontend/resources/flowy_icons/40x/field_option_multiselect.svg create mode 100644 frontend/resources/flowy_icons/40x/field_option_number.svg create mode 100644 frontend/resources/flowy_icons/40x/field_option_person.svg create mode 100644 frontend/resources/flowy_icons/40x/field_option_phone.svg create mode 100644 frontend/resources/flowy_icons/40x/field_option_relation.svg create mode 100644 frontend/resources/flowy_icons/40x/field_option_select.svg create mode 100644 frontend/resources/flowy_icons/40x/field_option_text.svg create mode 100644 frontend/resources/flowy_icons/40x/field_option_url.svg diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/app_bar_actions.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/app_bar_actions.dart index e43fbcc857..7552de066b 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/app_bar_actions.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/app_bar_actions.dart @@ -1,3 +1,6 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; class AppBarBackButton extends StatelessWidget { @@ -17,6 +20,26 @@ class AppBarBackButton extends StatelessWidget { } } +class AppBarCancelButton extends StatelessWidget { + const AppBarCancelButton({ + super.key, + required this.onTap, + }); + + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return AppBarButton( + onTap: onTap, + child: FlowyText( + LocaleKeys.button_cancel.tr(), + fontSize: 16.0, + ), + ); + } +} + class AppBarMoreButton extends StatelessWidget { const AppBarMoreButton({ super.key, @@ -56,6 +79,9 @@ class AppBarButton extends StatelessWidget { return InkWell( enableFeedback: true, borderRadius: BorderRadius.circular(28), + splashColor: Colors.transparent, + focusColor: Colors.transparent, + highlightColor: Colors.transparent, onTap: onTap, child: Padding( padding: EdgeInsets.all(extent), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/option_color_list.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/option_color_list.dart new file mode 100644 index 0000000000..13e01a7223 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/option_color_list.dart @@ -0,0 +1,56 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; +import 'package:flowy_infra/size.dart'; +import 'package:flutter/material.dart'; + +class OptionColorList extends StatelessWidget { + const OptionColorList({ + super.key, + this.selectedColor, + required this.onSelectedColor, + }); + + final SelectOptionColorPB? selectedColor; + final void Function(SelectOptionColorPB color) onSelectedColor; + + @override + Widget build(BuildContext context) { + return GridView.count( + crossAxisCount: 6, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + children: SelectOptionColorPB.values.map( + (colorPB) { + final color = colorPB.toColor(context); + final isSelected = selectedColor?.value == colorPB.value; + return GestureDetector( + onTap: () => onSelectedColor(colorPB), + child: Container( + margin: const EdgeInsets.all( + 8.0, + ), + decoration: BoxDecoration( + color: color, + borderRadius: Corners.s12Border, + border: Border.all( + color: isSelected + ? const Color(0xff00C6F1) + : Theme.of(context).dividerColor, + ), + ), + alignment: Alignment.center, + child: isSelected + ? const FlowySvg( + FlowySvgs.blue_check_s, + size: Size.square(28.0), + blendMode: null, + ) + : null, + ), + ); + }, + ).toList(), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart index fec272fee1..8ff7056021 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart @@ -44,7 +44,7 @@ Future showMobileBottomSheet( if (showHeader) { children.addAll([ - const VSpace(4), + VSpace(padding.top), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -81,10 +81,12 @@ Future showMobileBottomSheet( if (resizeToAvoidBottomInset) { children.add( AnimatedPadding( - padding: padding + - EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom, - ), + padding: EdgeInsets.only( + top: showHeader ? 0 : padding.top, + left: padding.left, + right: padding.right, + bottom: padding.bottom + MediaQuery.of(context).viewInsets.bottom, + ), duration: Duration.zero, child: child, ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_create_field_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_create_field_screen.dart new file mode 100644 index 0000000000..de7a12d4e5 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_create_field_screen.dart @@ -0,0 +1,100 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart'; +import 'package:appflowy/mobile/presentation/database/card/card_detail/widgets/_new_field_option.dart'; +import 'package:appflowy/util/field_type_extension.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class MobileNewPropertyScreen extends StatefulWidget { + static const routeName = '/new_property'; + static const argViewId = 'view_id'; + static const argFieldTypeId = 'field_type_id'; + + const MobileNewPropertyScreen({ + super.key, + required this.viewId, + this.fieldType, + }); + + final String viewId; + final FieldType? fieldType; + + @override + State createState() => + _MobileNewPropertyScreenState(); +} + +class _MobileNewPropertyScreenState extends State { + late FieldOptionValues optionValues; + + @override + void initState() { + super.initState(); + + final type = widget.fieldType ?? FieldType.RichText; + optionValues = FieldOptionValues( + type: type, + name: type.i18n, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + centerTitle: true, + title: FlowyText( + LocaleKeys.grid_field_newProperty.tr(), + fontSize: 16.0, + ), + leading: AppBarCancelButton( + onTap: () => context.pop(), + ), + leadingWidth: 120, + actions: [ + _SaveButton( + onSave: () { + context.pop(optionValues); + }, + ), + ], + ), + body: FieldOption( + mode: FieldOptionMode.add, + defaultValues: optionValues, + onOptionValuesChanged: (optionValues) { + this.optionValues = optionValues; + }, + ), + ); + } +} + +class _SaveButton extends StatelessWidget { + const _SaveButton({ + required this.onSave, + }); + + final VoidCallback onSave; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(right: 16.0), + child: Align( + alignment: Alignment.center, + child: GestureDetector( + onTap: onSave, + child: FlowyText( + LocaleKeys.button_save.tr(), + color: const Color(0xFF00ADDC), + fontSize: 16.0, + ), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/_field_options.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/_field_options.dart new file mode 100644 index 0000000000..86945d67ba --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/_field_options.dart @@ -0,0 +1,114 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/base/app_bar_actions.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:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +const _supportedFieldTypes = [ + FieldType.RichText, + FieldType.Number, + FieldType.URL, + FieldType.SingleSelect, + FieldType.MultiSelect, + FieldType.DateTime, + FieldType.Checkbox, + FieldType.Checklist, +]; + +class FieldOptions extends StatelessWidget { + const FieldOptions({ + super.key, + required this.onAddField, + this.scrollController, + }); + + final void Function(FieldType) onAddField; + final ScrollController? scrollController; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const _FieldHeader(), + const VSpace(12.0), + Expanded( + child: GridView.count( + controller: scrollController, + crossAxisCount: 3, + childAspectRatio: 0.9, + mainAxisSpacing: 12.0, + children: _supportedFieldTypes + .map( + (e) => _Field( + type: e, + onTap: () => onAddField(e), + ), + ) + .toList(), + ), + ), + ], + ); + } +} + +class _FieldHeader extends StatelessWidget { + const _FieldHeader(); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 56, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox( + width: 120, + child: AppBarCancelButton( + onTap: () => context.pop(), + ), + ), + FlowyText.medium( + LocaleKeys.titleBar_addField.tr(), + fontSize: 16.0, + ), + const HSpace(120), + ], + ), + ); + } +} + +class _Field extends StatelessWidget { + const _Field({ + required this.type, + required this.onTap, + }); + + final FieldType type; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final width = MediaQuery.of(context).size.width; + return GestureDetector( + onTap: onTap, + child: Column( + children: [ + FlowySvg( + type.svgData, + blendMode: null, + size: Size.square(width / 4.0), + ), + const VSpace(6.0), + FlowyText(type.i18n), + ], + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/_new_field_option.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/_new_field_option.dart new file mode 100644 index 0000000000..ed6804131f --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/_new_field_option.dart @@ -0,0 +1,867 @@ +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/base/option_color_list.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; +import 'package:appflowy/mobile/presentation/database/card/card_detail/widgets/_field_options.dart'; +import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/number_format_bloc.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_service.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/date.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart'; +import 'package:appflowy/util/field_type_extension.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:collection/collection.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra/uuid.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:protobuf/protobuf.dart'; + +enum FieldOptionMode { + add, + edit, +} + +class FieldOptionValues { + FieldOptionValues({ + required this.type, + required this.name, + this.dateFormate, + this.includeTime = false, + this.timeFormat, + this.numberFormat, + this.selectOption = const [], + }); + + FieldType type; + String name; + + // FieldType.Date + DateFormatPB? dateFormate; + bool includeTime; + TimeFormatPB? timeFormat; + + // FieldType.Num + NumberFormatPB? numberFormat; + + // FieldType.Select + // FieldType.MultiSelect + List selectOption; + + Future create({ + required String viewId, + }) async { + Uint8List? typeOptionData; + + switch (type) { + case FieldType.RichText: + break; + case FieldType.URL: + break; + case FieldType.Checkbox: + break; + case FieldType.Number: + typeOptionData = NumberTypeOptionPB( + format: numberFormat, + ).writeToBuffer(); + break; + case FieldType.DateTime: + typeOptionData = DateTypeOptionPB( + dateFormat: dateFormate, + timeFormat: timeFormat, + ).writeToBuffer(); + break; + case FieldType.SingleSelect: + typeOptionData = SingleSelectTypeOptionPB( + options: selectOption, + ).writeToBuffer(); + case FieldType.MultiSelect: + typeOptionData = MultiSelectTypeOptionPB( + options: selectOption, + ).writeToBuffer(); + break; + default: + throw UnimplementedError(); + } + + await TypeOptionBackendService.createFieldTypeOption( + viewId: viewId, + fieldType: type, + fieldName: name, + typeOptionData: typeOptionData, + ); + } +} + +class FieldOption extends StatefulWidget { + const FieldOption({ + super.key, + required this.mode, + required this.defaultValues, + required this.onOptionValuesChanged, + }); + + final FieldOptionMode mode; + final FieldOptionValues defaultValues; + final void Function(FieldOptionValues values) onOptionValuesChanged; + + @override + State createState() => _FieldOptionState(); +} + +class _FieldOptionState extends State { + final controller = TextEditingController(); + + late FieldOptionValues values; + + @override + void initState() { + super.initState(); + + values = widget.defaultValues; + controller.text = values.type.i18n; + } + + @override + void dispose() { + controller.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + color: Theme.of(context).colorScheme.secondaryContainer, + height: MediaQuery.of(context).size.height, + child: SingleChildScrollView( + child: Column( + children: [ + const _Divider(), + _OptionTextField( + controller: controller, + type: values.type, + onTextChanged: (value) { + _updateOptionValues(name: value); + }, + ), + const _Divider(), + _PropertyType( + type: values.type, + onSelected: (type) => setState( + () { + controller.text = type.i18n; + _updateOptionValues(type: type, name: type.i18n); + }, + ), + ), + const _Divider(), + ..._buildOption(), + ..._buildOptionActions(), + ], + ), + ), + ); + } + + List _buildOption() { + switch (values.type) { + case FieldType.RichText: + return [ + const _TextOption(), + ]; + case FieldType.URL: + return [ + const _URLOption(), + ]; + case FieldType.Checkbox: + return [ + const _CheckboxOption(), + ]; + case FieldType.Number: + return [ + _NumberOption( + selectedFormat: values.numberFormat ?? NumberFormatPB.Num, + onSelected: (format) => setState( + () => _updateOptionValues( + numberFormat: format, + ), + ), + ), + ]; + case FieldType.DateTime: + return [ + _DateOption( + selectedFormat: values.dateFormate ?? DateFormatPB.Local, + onSelected: (format) => _updateOptionValues( + dateFormate: format, + ), + ), + const _Divider(), + _TimeOption( + includeTime: values.includeTime, + selectedFormat: values.timeFormat ?? TimeFormatPB.TwelveHour, + onSelected: (includeTime, format) => _updateOptionValues( + includeTime: includeTime, + timeFormat: format, + ), + ), + ]; + case FieldType.SingleSelect: + case FieldType.MultiSelect: + return [ + _SelectOption( + mode: widget.mode, + selectOption: values.selectOption, + onAddOptions: (options) { + if (values.selectOption.lastOrNull?.name.isEmpty == true) { + // ignore the add action if the last one doesn't have a name + return; + } + setState(() { + _updateOptionValues( + selectOption: values.selectOption + options, + ); + }); + }, + onUpdateOptions: (options) { + _updateOptionValues(selectOption: options); + }, + ), + ]; + default: + return []; + } + } + + List _buildOptionActions() { + return switch (widget.mode) { + FieldOptionMode.add => [], + FieldOptionMode.edit => [ + FlowyOptionTile.text( + text: LocaleKeys.button_delete.tr(), + leftIcon: const FlowySvg(FlowySvgs.delete_s), + ), + FlowyOptionTile.text( + showTopBorder: false, + text: LocaleKeys.button_duplicate.tr(), + leftIcon: const FlowySvg(FlowySvgs.copy_s), + ), + FlowyOptionTile.text( + showTopBorder: false, + text: LocaleKeys.grid_field_hide.tr(), + leftIcon: const FlowySvg(FlowySvgs.hide_s), + ), + ] + }; + } + + void _updateOptionValues({ + FieldType? type, + String? name, + DateFormatPB? dateFormate, + bool? includeTime, + TimeFormatPB? timeFormat, + NumberFormatPB? numberFormat, + List? selectOption, + }) { + if (type != null) { + values.type = type; + } + if (name != null) { + values.name = name; + } + if (dateFormate != null) { + values.dateFormate = dateFormate; + } + if (includeTime != null) { + values.includeTime = includeTime; + } + if (timeFormat != null) { + values.timeFormat = timeFormat; + } + if (numberFormat != null) { + values.numberFormat = numberFormat; + } + if (selectOption != null) { + values.selectOption = selectOption; + } + + widget.onOptionValuesChanged(values); + } +} + +class _OptionTextField extends StatelessWidget { + const _OptionTextField({ + required this.controller, + required this.type, + required this.onTextChanged, + }); + + final TextEditingController controller; + final FieldType type; + final void Function(String value) onTextChanged; + + @override + Widget build(BuildContext context) { + return FlowyOptionTile.textField( + controller: controller, + textFieldPadding: const EdgeInsets.symmetric(horizontal: 12.0), + onTextChanged: onTextChanged, + leftIcon: Padding( + padding: const EdgeInsets.only(left: 16.0), + child: FlowySvg( + type.svgData, + size: const Size.square(36.0), + blendMode: null, + ), + ), + ); + } +} + +class _PropertyType extends StatelessWidget { + const _PropertyType({ + required this.type, + required this.onSelected, + }); + + final FieldType type; + final void Function(FieldType type) onSelected; + + @override + Widget build(BuildContext context) { + return FlowyOptionTile.text( + text: LocaleKeys.grid_field_propertyType.tr(), + trailing: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + FlowySvg( + type.smallSvgData, + ), + const HSpace(6.0), + FlowyText( + type.i18n, + color: Theme.of(context).hintColor, + fontSize: 16.0, + ), + const HSpace(4.0), + FlowySvg( + FlowySvgs.arrow_right_s, + color: Theme.of(context).hintColor, + size: const Size.square(18.0), + ), + ], + ), + onTap: () { + showMobileBottomSheet( + context, + padding: EdgeInsets.zero, + builder: (context) { + return DraggableScrollableSheet( + expand: false, + snap: true, + initialChildSize: 0.7, + minChildSize: 0.7, + builder: (context, controller) => FieldOptions( + scrollController: controller, + onAddField: (type) { + onSelected(type); + context.pop(); + }, + ), + ); + }, + ); + }, + ); + } +} + +class _Divider extends StatelessWidget { + const _Divider(); + + @override + Widget build(BuildContext context) { + return const VSpace( + 24.0, + ); + } +} + +class _TextOption extends StatelessWidget { + const _TextOption(); + + @override + Widget build(BuildContext context) { + return const SizedBox.shrink(); + } +} + +class _URLOption extends StatelessWidget { + const _URLOption(); + + @override + Widget build(BuildContext context) { + return const SizedBox.shrink(); + } +} + +class _CheckboxOption extends StatelessWidget { + const _CheckboxOption(); + + @override + Widget build(BuildContext context) { + return const SizedBox.shrink(); + } +} + +class _DateOption extends StatefulWidget { + const _DateOption({ + required this.selectedFormat, + required this.onSelected, + }); + + final DateFormatPB selectedFormat; + final Function(DateFormatPB format) onSelected; + + @override + State<_DateOption> createState() => _DateOptionState(); +} + +class _DateOptionState extends State<_DateOption> { + DateFormatPB selectedFormat = DateFormatPB.Local; + + @override + void initState() { + super.initState(); + + selectedFormat = widget.selectedFormat; + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + vertical: 6.0, + horizontal: 16.0, + ), + child: FlowyText( + LocaleKeys.grid_field_dateFormat.tr(), + fontSize: 16.0, + color: Theme.of(context).hintColor, + ), + ), + ...DateFormatPB.values.mapIndexed((index, format) { + return FlowyOptionTile.checkbox( + text: format.title(), + isSelected: selectedFormat == format, + showTopBorder: index == 0, + onTap: () { + widget.onSelected(format); + setState(() { + selectedFormat = format; + }); + }, + ); + }), + ], + ); + } +} + +class _TimeOption extends StatefulWidget { + const _TimeOption({ + required this.includeTime, + required this.selectedFormat, + required this.onSelected, + }); + + final bool includeTime; + final TimeFormatPB selectedFormat; + final Function(bool includeTime, TimeFormatPB format) onSelected; + + @override + State<_TimeOption> createState() => _TimeOptionState(); +} + +class _TimeOptionState extends State<_TimeOption> { + TimeFormatPB selectedFormat = TimeFormatPB.TwelveHour; + bool includeTime = false; + + @override + void initState() { + super.initState(); + + selectedFormat = widget.selectedFormat; + includeTime = widget.includeTime; + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + vertical: 6.0, + horizontal: 16.0, + ), + child: FlowyText( + LocaleKeys.grid_field_timeFormat.tr(), + fontSize: 16.0, + color: Theme.of(context).hintColor, + ), + ), + FlowyOptionTile.switcher( + text: LocaleKeys.grid_field_includeTime.tr(), + isSelected: includeTime, + onValueChanged: (includeTime) { + widget.onSelected(includeTime, selectedFormat); + setState(() { + this.includeTime = includeTime; + }); + }, + ), + if (includeTime) + ...TimeFormatPB.values.mapIndexed((index, format) { + return FlowyOptionTile.checkbox( + text: format.title(), + isSelected: selectedFormat == format, + showTopBorder: false, + onTap: () { + widget.onSelected(includeTime, format); + setState(() { + selectedFormat = format; + }); + }, + ); + }), + ], + ); + } +} + +class _NumberOption extends StatelessWidget { + const _NumberOption({ + required this.selectedFormat, + required this.onSelected, + }); + + final NumberFormatPB selectedFormat; + final void Function(NumberFormatPB format) onSelected; + + @override + Widget build(BuildContext context) { + return FlowyOptionTile.text( + text: LocaleKeys.grid_field_numberFormat.tr(), + trailing: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + FlowyText( + selectedFormat.title(), + color: Theme.of(context).hintColor, + fontSize: 16.0, + ), + const HSpace(4.0), + FlowySvg( + FlowySvgs.arrow_right_s, + color: Theme.of(context).hintColor, + size: const Size.square(18.0), + ), + ], + ), + onTap: () { + showMobileBottomSheet( + context, + padding: EdgeInsets.zero, + builder: (context) { + return DraggableScrollableSheet( + expand: false, + snap: true, + initialChildSize: 0.6, + minChildSize: 0.6, + builder: (context, scrollController) => _NumberFormatList( + scrollController: scrollController, + selectedFormat: selectedFormat, + onSelected: (type) { + onSelected(type); + context.pop(); + }, + ), + ); + }, + ); + }, + ); + } +} + +class _NumberFormatList extends StatelessWidget { + const _NumberFormatList({ + this.scrollController, + required this.selectedFormat, + required this.onSelected, + }); + + final NumberFormatPB selectedFormat; + final ScrollController? scrollController; + final void Function(NumberFormatPB format) onSelected; + + @override + Widget build(BuildContext context) { + return ListView( + controller: scrollController, + children: NumberFormatPB.values + .mapIndexed( + (index, element) => FlowyOptionTile.checkbox( + text: element.title(), + isSelected: selectedFormat == element, + showTopBorder: index == 0, + onTap: () => onSelected(element), + ), + ) + .toList(), + ); + } +} + +// single select or multi select +class _SelectOption extends StatelessWidget { + _SelectOption({ + required this.mode, + required this.selectOption, + required this.onAddOptions, + required this.onUpdateOptions, + }); + + final List selectOption; + final void Function(List options) onAddOptions; + final void Function(List options) onUpdateOptions; + final FieldOptionMode mode; + + final random = Random(); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + vertical: 6.0, + horizontal: 16.0, + ), + child: FlowyText( + LocaleKeys.grid_field_optionTitle.tr(), + fontSize: 16.0, + color: Theme.of(context).hintColor, + ), + ), + _SelectOptionList( + selectOptions: selectOption, + onUpdateOptions: onUpdateOptions, + ), + FlowyOptionTile.text( + text: LocaleKeys.grid_field_addOption.tr(), + leftIcon: const FlowySvg(FlowySvgs.add_s), + onTap: () { + onAddOptions([ + SelectOptionPB( + id: uuid(), + name: '', + color: SelectOptionColorPB.valueOf( + random.nextInt(SelectOptionColorPB.values.length), + ), + ), + ]); + }, + ), + ], + ); + } +} + +class _SelectOptionList extends StatefulWidget { + const _SelectOptionList({ + required this.selectOptions, + required this.onUpdateOptions, + }); + + final List selectOptions; + final void Function(List options) onUpdateOptions; + + @override + State<_SelectOptionList> createState() => _SelectOptionListState(); +} + +class _SelectOptionListState extends State<_SelectOptionList> { + late List options; + + @override + void initState() { + super.initState(); + + options = widget.selectOptions; + } + + @override + void didUpdateWidget(covariant _SelectOptionList oldWidget) { + super.didUpdateWidget(oldWidget); + + options = widget.selectOptions; + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (widget.selectOptions.isEmpty) { + return const SizedBox.shrink(); + } + return ListView( + shrinkWrap: true, + padding: EdgeInsets.zero, + children: widget.selectOptions + .mapIndexed( + (index, option) => _SelectOptionTile( + option: option, + showTopBorder: index == 0, + showBottomBorder: index != widget.selectOptions.length - 1, + onUpdateOption: (option) { + _updateOption(index, option); + }, + ), + ) + .toList(), + ); + } + + void _updateOption(int index, SelectOptionPB option) { + final options = [...this.options]; + options[index] = option; + this.options = options; + widget.onUpdateOptions(options); + } +} + +class _SelectOptionTile extends StatefulWidget { + const _SelectOptionTile({ + required this.option, + required this.showTopBorder, + required this.showBottomBorder, + required this.onUpdateOption, + }); + + final SelectOptionPB option; + final bool showTopBorder; + final bool showBottomBorder; + final void Function(SelectOptionPB option) onUpdateOption; + + @override + State<_SelectOptionTile> createState() => __SelectOptionTileState(); +} + +class __SelectOptionTileState extends State<_SelectOptionTile> { + final TextEditingController controller = TextEditingController(); + late SelectOptionPB option; + + @override + void initState() { + super.initState(); + + controller.text = widget.option.name; + option = widget.option; + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return FlowyOptionTile.textField( + controller: controller, + textFieldHintText: LocaleKeys.grid_field_typeANewOption.tr(), + showTopBorder: widget.showTopBorder, + showBottomBorder: widget.showBottomBorder, + textFieldPadding: const EdgeInsets.symmetric(horizontal: 16.0), + trailing: _SelectOptionColor( + color: option.color, + onChanged: (color) { + setState(() { + option.freeze(); + option = option.rebuild((p0) => p0.color = color); + widget.onUpdateOption(option); + }); + context.pop(); + }, + ), + onTextChanged: (name) { + setState(() { + option.freeze(); + option = option.rebuild((p0) => p0.name = name); + widget.onUpdateOption(option); + }); + }, + ); + } +} + +class _SelectOptionColor extends StatelessWidget { + const _SelectOptionColor({ + required this.color, + required this.onChanged, + }); + + final SelectOptionColorPB color; + final void Function(SelectOptionColorPB) onChanged; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + showMobileBottomSheet( + context, + showHeader: true, + showCloseButton: true, + title: LocaleKeys.grid_selectOption_colorPanelTitle.tr(), + padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), + builder: (context) { + return OptionColorList( + selectedColor: color, + onSelectedColor: onChanged, + ); + }, + ); + }, + child: Container( + decoration: BoxDecoration( + color: color.toColor(context), + borderRadius: Corners.s10Border, + ), + width: 32, + height: 32, + alignment: Alignment.center, + child: const FlowySvg( + FlowySvgs.arrow_down_s, + size: Size.square(20), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart index c648acbbb7..a29f43cd86 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart @@ -207,17 +207,15 @@ class _EndDateSwitch extends StatelessWidget { return BlocSelector( selector: (state) => state.isRange, builder: (context, isRange) { - return FlowyOptionTile( + return FlowyOptionTile.switcher( text: LocaleKeys.grid_field_isRange.tr(), leftIcon: const FlowySvg(FlowySvgs.date_s), - leading: _Switcher( - value: isRange, - onChanged: (value) { - context - .read() - .add(DateCellCalendarEvent.setIsRange(value)); - }, - ), + isSelected: isRange, + onValueChanged: (value) { + context + .read() + .add(DateCellCalendarEvent.setIsRange(value)); + }, ); }, ); @@ -232,18 +230,16 @@ class _IncludeTimeSwitch extends StatelessWidget { return BlocSelector( selector: (state) => state.includeTime, builder: (context, includeTime) { - return FlowyOptionTile( + return FlowyOptionTile.switcher( showTopBorder: false, text: LocaleKeys.grid_field_includeTime.tr(), leftIcon: const FlowySvg(FlowySvgs.clock_alarm_s), - leading: _Switcher( - value: includeTime, - onChanged: (value) { - context - .read() - .add(DateCellCalendarEvent.setIncludeTime(value)); - }, - ), + isSelected: includeTime, + onValueChanged: (value) { + context + .read() + .add(DateCellCalendarEvent.setIncludeTime(value)); + }, ); }, ); @@ -379,7 +375,7 @@ class _ClearDateButton extends StatelessWidget { @override Widget build(BuildContext context) { - return FlowyOptionTile( + return FlowyOptionTile.text( showTopBorder: false, text: LocaleKeys.grid_field_clearDate.tr(), onTap: () => context @@ -398,7 +394,7 @@ class _TimeFormatOption extends StatelessWidget { TimeFormatPB>( selector: (state) => state.dateTypeOptionPB.timeFormat, builder: (context, state) { - return FlowyOptionTile( + return FlowyOptionTile.text( showTopBorder: false, text: LocaleKeys.settings_appearance_timeFormat_label.tr(), leftIcon: const FlowySvg(FlowySvgs.time_s), @@ -431,7 +427,7 @@ class _DateFormatOption extends StatelessWidget { DateFormatPB>( selector: (state) => state.dateTypeOptionPB.dateFormat, builder: (context, state) { - return FlowyOptionTile( + return FlowyOptionTile.text( text: LocaleKeys.settings_appearance_dateFormat_label.tr(), leftIcon: const FlowySvg(FlowySvgs.clock_alarm_s), ); @@ -453,28 +449,3 @@ class _DateFormatOption extends StatelessWidget { ); } } - -class _Switcher extends StatelessWidget { - const _Switcher({ - required this.value, - required this.onChanged, - }); - - final bool value; - final void Function(bool value) onChanged; - - @override - Widget build(BuildContext context) { - return SizedBox( - width: 48, - child: FittedBox( - fit: BoxFit.fill, - child: Switch.adaptive( - value: value, - activeColor: const Color(0xFF00BCF0), - onChanged: onChanged, - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_option_decorate_box.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_option_decorate_box.dart index 719a51bf80..33a10280f8 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_option_decorate_box.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_option_decorate_box.dart @@ -16,6 +16,7 @@ class FlowyOptionDecorateBox extends StatelessWidget { Widget build(BuildContext context) { return DecoratedBox( decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, border: Border( top: showTopBorder ? BorderSide( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_option_tile.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_option_tile.dart index 0dd0b21110..d0a3a31e52 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_option_tile.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_option_tile.dart @@ -1,52 +1,271 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +enum FlowyOptionTileType { + text, + textField, + checkbox, +} + // used in cell editor + class FlowyOptionTile extends StatelessWidget { - const FlowyOptionTile({ - super.key, + const FlowyOptionTile._({ + required this.type, this.showTopBorder = true, this.showBottomBorder = true, - required this.text, - this.leftIcon, - this.onTap, + this.text, + this.controller, this.leading, + this.onTap, + this.trailing, + this.textFieldPadding = const EdgeInsets.symmetric( + horizontal: 16.0, + ), + this.isSelected = false, + this.textFieldHintText, + this.onTextChanged, + this.onTextSubmitted, }); + factory FlowyOptionTile.text({ + required String text, + bool showTopBorder = true, + bool showBottomBorder = true, + Widget? leftIcon, + Widget? trailing, + VoidCallback? onTap, + }) { + return FlowyOptionTile._( + type: FlowyOptionTileType.text, + text: text, + controller: null, + onTap: onTap, + showTopBorder: showTopBorder, + showBottomBorder: showBottomBorder, + leading: leftIcon, + trailing: trailing, + ); + } + + factory FlowyOptionTile.textField({ + required TextEditingController controller, + void Function(String value)? onTextChanged, + void Function(String value)? onTextSubmitted, + EdgeInsets textFieldPadding = const EdgeInsets.symmetric( + horizontal: 16.0, + ), + bool showTopBorder = true, + bool showBottomBorder = true, + Widget? leftIcon, + Widget? trailing, + String? textFieldHintText, + }) { + return FlowyOptionTile._( + type: FlowyOptionTileType.text, + controller: controller, + textFieldPadding: textFieldPadding, + text: null, + onTap: null, + showTopBorder: showTopBorder, + showBottomBorder: showBottomBorder, + leading: leftIcon, + trailing: trailing, + textFieldHintText: textFieldHintText, + onTextChanged: onTextChanged, + onTextSubmitted: onTextSubmitted, + ); + } + + factory FlowyOptionTile.checkbox({ + required String text, + required bool isSelected, + required VoidCallback? onTap, + bool showTopBorder = true, + bool showBottomBorder = true, + }) { + return FlowyOptionTile._( + type: FlowyOptionTileType.checkbox, + isSelected: isSelected, + text: text, + onTap: onTap, + showTopBorder: showTopBorder, + showBottomBorder: showBottomBorder, + trailing: isSelected + ? const FlowySvg( + FlowySvgs.blue_check_s, + size: Size.square(24.0), + blendMode: null, + ) + : null, + ); + } + + factory FlowyOptionTile.switcher({ + required String text, + required bool isSelected, + required void Function(bool value) onValueChanged, + bool showTopBorder = true, + bool showBottomBorder = true, + Widget? leftIcon, + }) { + return FlowyOptionTile._( + type: FlowyOptionTileType.text, + text: text, + controller: null, + onTap: null, + showTopBorder: showTopBorder, + showBottomBorder: showBottomBorder, + leading: leftIcon, + trailing: _Switcher(value: isSelected, onChanged: onValueChanged), + ); + } + final bool showTopBorder; final bool showBottomBorder; - final String text; + final String? text; + final TextEditingController? controller; + final EdgeInsets textFieldPadding; final void Function()? onTap; - final Widget? leftIcon; final Widget? leading; + final Widget? trailing; + + // only used in checkbox or switcher + final bool isSelected; + + // only used in textfield + final String? textFieldHintText; + final void Function(String value)? onTextChanged; + final void Function(String value)? onTextSubmitted; + + final FlowyOptionTileType type; @override Widget build(BuildContext context) { - return FlowyOptionDecorateBox( - showTopBorder: showTopBorder, - showBottomBorder: showBottomBorder, - child: Row( - children: [ - FlowyButton( - useIntrinsicWidth: true, - text: FlowyText( - text, - fontSize: 16.0, - ), - margin: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 16.0, - ), - leftIcon: leftIcon, - leftIconSize: const Size.square(24.0), - iconPadding: 8.0, - onTap: onTap, + final child = ColoredBox( + color: Theme.of(context).colorScheme.surface, + child: FlowyOptionDecorateBox( + showTopBorder: showTopBorder, + showBottomBorder: showBottomBorder, + child: Row( + children: [ + _buildText(), + ..._buildTextField(), + const Spacer(), + trailing ?? const SizedBox.shrink(), + const HSpace(12.0), + ], + ), + ), + ); + + if (type == FlowyOptionTileType.checkbox || + type == FlowyOptionTileType.text) { + return FlowyButton( + expandText: true, + margin: EdgeInsets.zero, + onTap: onTap, + text: child, + ); + } + + return child; + } + + Widget _buildText() { + if (text == null) { + return const SizedBox.shrink(); + } + + switch (type) { + case FlowyOptionTileType.text: + return FlowyButton( + useIntrinsicWidth: true, + text: FlowyText( + text!, + fontSize: 16.0, ), - const Spacer(), - leading ?? const SizedBox.shrink(), - const HSpace(12.0), - ], + margin: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 16.0, + ), + leftIcon: leading, + leftIconSize: const Size.square(24.0), + iconPadding: 8.0, + onTap: onTap, + ); + case FlowyOptionTileType.textField: + return const SizedBox.shrink(); + case FlowyOptionTileType.checkbox: + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 16.0, + ), + child: FlowyText( + text!, + fontSize: 16.0, + ), + ); + } + } + + List _buildTextField() { + if (controller == null) { + return [ + const SizedBox.shrink(), + ]; + } + + return [ + if (leading != null) leading!, + Expanded( + child: ConstrainedBox( + constraints: const BoxConstraints.tightFor( + height: 52.0, + width: double.infinity, + ), + child: TextField( + controller: controller, + textInputAction: TextInputAction.done, + decoration: InputDecoration( + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + contentPadding: textFieldPadding, + hintText: textFieldHintText, + ), + onChanged: onTextChanged, + onSubmitted: onTextSubmitted, + ), + ), + ), + ]; + } +} + +class _Switcher extends StatelessWidget { + const _Switcher({ + required this.value, + required this.onChanged, + }); + + final bool value; + final void Function(bool value) onChanged; + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 48, + child: FittedBox( + fit: BoxFit.fill, + child: Switch.adaptive( + value: value, + activeColor: const Color(0xFF00BCF0), + onChanged: onChanged, + ), ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/type_option_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/type_option_service.dart index 84251e85ce..4f1e523abc 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/type_option_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/type_option_service.dart @@ -1,7 +1,9 @@ -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:dartz/dartz.dart'; +import 'dart:typed_data'; + import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:dartz/dartz.dart'; class TypeOptionBackendService { final String viewId; @@ -26,12 +28,22 @@ class TypeOptionBackendService { static Future> createFieldTypeOption({ required String viewId, FieldType fieldType = FieldType.RichText, + String? fieldName, + Uint8List? typeOptionData, CreateFieldPosition position = CreateFieldPosition.End, String? targetFieldId, }) { final payload = CreateFieldPayloadPB.create() ..viewId = viewId - ..fieldType = FieldType.RichText; + ..fieldType = fieldType; + + if (fieldName != null) { + payload.fieldName = fieldName; + } + + if (typeOptionData != null) { + payload.typeOptionData = typeOptionData; + } if (position == CreateFieldPosition.Before || position == CreateFieldPosition.After && targetFieldId != null) { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/grid_header.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/grid_header.dart index 6f214f970d..0733f73a4b 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/grid_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/grid_header.dart @@ -1,5 +1,9 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; +import 'package:appflowy/mobile/presentation/database/card/card_detail/mobile_create_field_screen.dart'; +import 'package:appflowy/mobile/presentation/database/card/card_detail/widgets/_field_options.dart'; +import 'package:appflowy/mobile/presentation/database/card/card_detail/widgets/_new_field_option.dart'; import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; import 'package:appflowy/plugins/database_view/grid/application/grid_bloc.dart'; import 'package:appflowy/plugins/database_view/grid/application/grid_header_bloc.dart'; @@ -8,11 +12,12 @@ import 'package:appflowy_backend/log.dart'; import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.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:reorderables/reorderables.dart'; + import '../../../../application/field/type_option/type_option_service.dart'; import '../../layout/sizes.dart'; import 'field_cell.dart'; @@ -230,13 +235,17 @@ class _CreateFieldButtonState extends State { ), hoverColor: AFThemeExtension.of(context).greyHover, onTap: () async { - final result = await TypeOptionBackendService.createFieldTypeOption( - viewId: widget.viewId, - ); - result.fold( - (typeOptionPB) => widget.onFieldCreated(typeOptionPB.field_2.id), - (err) => Log.error("Failed to create field type option: $err"), - ); + if (PlatformExtension.isMobile) { + _showCreateFieldBottomSheet(context); + } else { + final result = await TypeOptionBackendService.createFieldTypeOption( + viewId: widget.viewId, + ); + result.fold( + (typeOptionPB) => widget.onFieldCreated(typeOptionPB.field_2.id), + (err) => Log.error("Failed to create field type option: $err"), + ); + } }, leftIcon: FlowySvg( FlowySvgs.add_s, @@ -244,4 +253,40 @@ class _CreateFieldButtonState extends State { ), ); } + + void _showCreateFieldBottomSheet(BuildContext context) { + showMobileBottomSheet( + context, + padding: EdgeInsets.zero, + builder: (context) { + return DraggableScrollableSheet( + expand: false, + snap: true, + initialChildSize: 0.7, + minChildSize: 0.7, + builder: (context, controller) => FieldOptions( + scrollController: controller, + onAddField: (type) async { + final optionValues = await context.push( + Uri( + path: MobileNewPropertyScreen.routeName, + queryParameters: { + MobileNewPropertyScreen.argViewId: widget.viewId, + MobileNewPropertyScreen.argFieldTypeId: + type.value.toString(), + }, + ).toString(), + ); + if (optionValues != null) { + await optionValues.create(viewId: widget.viewId); + if (context.mounted) { + context.pop(); + } + } + }, + ), + ); + }, + ); + } } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/tab_bar/mobile/mobile_tab_bar_header.dart b/frontend/appflowy_flutter/lib/plugins/database_view/tab_bar/mobile/mobile_tab_bar_header.dart index 96d34953f0..40179a1732 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/tab_bar/mobile/mobile_tab_bar_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/tab_bar/mobile/mobile_tab_bar_header.dart @@ -60,6 +60,7 @@ class _MobileTabBarHeaderState extends State { maxLines: null, controller: controller, textAlignVertical: TextAlignVertical.top, + textInputAction: TextInputAction.done, decoration: const InputDecoration( border: InputBorder.none, enabledBorder: InputBorder.none, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/mobile_select_option_editor.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/mobile_select_option_editor.dart index c318d14ff4..54e279ba49 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/mobile_select_option_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/mobile_select_option_editor.dart @@ -1,5 +1,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/base/option_color_list.dart'; import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; import 'package:appflowy/plugins/base/drag_handler.dart'; import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; @@ -92,7 +93,6 @@ class _MobileSelectOptionEditorState extends State { const iconWidth = 36.0; const height = 44.0; return Stack( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Align( alignment: Alignment.centerLeft, @@ -510,17 +510,11 @@ class _MoreOptions extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - VSpace(8.0, color: color), + const VSpace(8.0), _buildRenameTextField(context), - VSpace( - 16.0, - color: color, - ), + const VSpace(16.0), _buildDeleteButton(context), - VSpace( - 16.0, - color: color, - ), + const VSpace(16.0), Padding( padding: const EdgeInsets.only(left: 12.0), child: ColoredBox( @@ -531,11 +525,22 @@ class _MoreOptions extends StatelessWidget { ), ), ), - VSpace( - 4.0, - color: color, + const VSpace(4.0), + FlowyOptionDecorateBox( + showTopBorder: true, + showBottomBorder: true, + child: Padding( + padding: const EdgeInsets.only( + top: 12.0, + left: 6.0, + right: 6.0, + ), + child: OptionColorList( + selectedColor: option.color, + onSelectedColor: (color) => onUpdate(null, color), + ), + ), ), - _buildColorOptions(context), ], ), ); @@ -544,66 +549,17 @@ class _MoreOptions extends StatelessWidget { Widget _buildRenameTextField(BuildContext context) { return ConstrainedBox( constraints: const BoxConstraints.tightFor(height: 52.0), - child: FlowyOptionDecorateBox( - showTopBorder: true, - showBottomBorder: true, - child: TextField( - controller: controller, - decoration: const InputDecoration( - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - contentPadding: EdgeInsets.only(left: 16.0), - ), - onChanged: (value) {}, - onSubmitted: (value) => onUpdate(value, null), - ), + child: FlowyOptionTile.textField( + controller: controller, ), ); } Widget _buildDeleteButton(BuildContext context) { - return FlowyOptionTile( + return FlowyOptionTile.text( text: LocaleKeys.button_delete.tr(), leftIcon: const FlowySvg(FlowySvgs.delete_s), onTap: onDelete, ); } - - Widget _buildColorOptions(BuildContext context) { - return FlowyOptionDecorateBox( - showTopBorder: true, - showBottomBorder: true, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 12.0), - child: GridView.count( - crossAxisCount: 6, - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - children: SelectOptionColorPB.values.map( - (colorPB) { - final color = colorPB.toColor(context); - return GestureDetector( - onTap: () => onUpdate(null, colorPB), - child: Container( - margin: const EdgeInsets.all( - 10.0, - ), - decoration: BoxDecoration( - color: color, - borderRadius: Corners.s12Border, - border: Border.all( - color: option.color.value == colorPB.value - ? const Color(0xff00C6F1) - : Theme.of(context).dividerColor, - ), - ), - ), - ); - }, - ).toList(), - ), - ), - ); - } } diff --git a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart index 9d84e82281..36a18d0db3 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart @@ -1,5 +1,6 @@ import 'package:appflowy/mobile/presentation/database/board/mobile_board_screen.dart'; import 'package:appflowy/mobile/presentation/database/card/card.dart'; +import 'package:appflowy/mobile/presentation/database/card/card_detail/mobile_create_field_screen.dart'; import 'package:appflowy/mobile/presentation/database/card/card_detail/mobile_create_row_field_screen.dart'; import 'package:appflowy/mobile/presentation/database/card/card_property_edit/card_property_edit_screen.dart'; import 'package:appflowy/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart'; @@ -23,6 +24,7 @@ import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/user/presentation/presentation.dart'; import 'package:appflowy/util/platform_extension.dart'; import 'package:appflowy/workspace/presentation/home/desktop_home_screen.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:flowy_infra/time/duration.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -61,6 +63,7 @@ GoRouter generateRouter(Widget child) { _mobileCardPropertyEditScreenRoute(), _mobileDateCellEditScreenRoute(), _mobileCreateRowFieldScreenRoute(), + _mobileNewPropertyPageRoute(), // home // MobileHomeSettingPage is outside the bottom navigation bar, thus it is not in the StatefulShellRoute. @@ -348,6 +351,28 @@ GoRoute _mobileFontPickerPageRoute() { ); } +GoRoute _mobileNewPropertyPageRoute() { + return GoRoute( + parentNavigatorKey: AppGlobals.rootNavKey, + path: MobileNewPropertyScreen.routeName, + pageBuilder: (context, state) { + final viewId = state + .uri.queryParameters[MobileNewPropertyScreen.argViewId] as String; + final fieldTypeId = + state.uri.queryParameters[MobileNewPropertyScreen.argFieldTypeId] ?? + FieldType.RichText.value.toString(); + final value = int.parse(fieldTypeId); + return MaterialPage( + fullscreenDialog: true, + child: MobileNewPropertyScreen( + viewId: viewId, + fieldType: FieldType.valueOf(value), + ), + ); + }, + ); +} + GoRoute _mobileCalendarEventsPageRoute() { return GoRoute( path: MobileCalendarEventsScreen.routeName, diff --git a/frontend/appflowy_flutter/lib/util/field_type_extension.dart b/frontend/appflowy_flutter/lib/util/field_type_extension.dart new file mode 100644 index 0000000000..e24565d0dd --- /dev/null +++ b/frontend/appflowy_flutter/lib/util/field_type_extension.dart @@ -0,0 +1,46 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:easy_localization/easy_localization.dart'; + +extension FieldTypeExtension on FieldType { + String get i18n => switch (this) { + FieldType.RichText => LocaleKeys.grid_field_textFieldName.tr(), + FieldType.Number => LocaleKeys.grid_field_numberFieldName.tr(), + FieldType.DateTime => LocaleKeys.grid_field_dateFieldName.tr(), + FieldType.SingleSelect => + LocaleKeys.grid_field_singleSelectFieldName.tr(), + FieldType.MultiSelect => + LocaleKeys.grid_field_multiSelectFieldName.tr(), + FieldType.Checkbox => LocaleKeys.grid_field_checkboxFieldName.tr(), + FieldType.Checklist => LocaleKeys.grid_field_checklistFieldName.tr(), + FieldType.URL => LocaleKeys.grid_field_urlFieldName.tr(), + FieldType.LastEditedTime => + LocaleKeys.grid_field_updatedAtFieldName.tr(), + FieldType.CreatedTime => LocaleKeys.grid_field_createdAtFieldName.tr(), + _ => throw UnimplementedError(), + }; + + FlowySvgData get svgData => switch (this) { + FieldType.RichText => FlowySvgs.field_option_text_xl, + FieldType.Number => FlowySvgs.field_option_number_xl, + FieldType.DateTime => FlowySvgs.field_option_date_xl, + FieldType.SingleSelect => FlowySvgs.field_option_select_xl, + FieldType.MultiSelect => FlowySvgs.field_option_multiselect_xl, + FieldType.Checkbox => FlowySvgs.field_option_checkbox_xl, + FieldType.Checklist => FlowySvgs.field_option_checklist_xl, + FieldType.URL => FlowySvgs.field_option_url_xl, + _ => throw UnimplementedError(), + }; + + FlowySvgData get smallSvgData => switch (this) { + FieldType.RichText => FlowySvgs.field_option_text_s, + FieldType.Number => FlowySvgs.field_option_number_s, + FieldType.DateTime => FlowySvgs.field_option_date_s, + FieldType.SingleSelect => FlowySvgs.field_option_select_s, + FieldType.MultiSelect => FlowySvgs.field_option_select_s, + FieldType.Checkbox => FlowySvgs.field_option_checkbox_s, + FieldType.URL => FlowySvgs.field_option_url_s, + _ => throw UnimplementedError(), + }; +} diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart index 5289edb89d..a6e6ce9a0a 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart @@ -30,6 +30,7 @@ class FlowyTextField extends StatefulWidget { final TextStyle? hintStyle; final InputDecoration? decoration; final TextAlignVertical? textAlignVertical; + final TextInputAction? textInputAction; const FlowyTextField({ super.key, @@ -58,6 +59,7 @@ class FlowyTextField extends StatefulWidget { this.hintStyle, this.decoration, this.textAlignVertical, + this.textInputAction, }); @override @@ -127,6 +129,7 @@ class FlowyTextFieldState extends State { _onChanged(text); } }, + textInputAction: widget.textInputAction, onSubmitted: (text) => _onSubmitted(text), onEditingComplete: widget.onEditingComplete, minLines: 1, diff --git a/frontend/resources/flowy_icons/16x/blue_check.svg b/frontend/resources/flowy_icons/16x/blue_check.svg new file mode 100644 index 0000000000..2c5746c2b6 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/blue_check.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/16x/field_option_checkbox.svg b/frontend/resources/flowy_icons/16x/field_option_checkbox.svg new file mode 100644 index 0000000000..235cd7f6a7 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/field_option_checkbox.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/16x/field_option_date.svg b/frontend/resources/flowy_icons/16x/field_option_date.svg new file mode 100644 index 0000000000..896ebf8078 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/field_option_date.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/16x/field_option_number.svg b/frontend/resources/flowy_icons/16x/field_option_number.svg new file mode 100644 index 0000000000..01648a14bf --- /dev/null +++ b/frontend/resources/flowy_icons/16x/field_option_number.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/16x/field_option_select.svg b/frontend/resources/flowy_icons/16x/field_option_select.svg new file mode 100644 index 0000000000..e15f904e67 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/field_option_select.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/16x/field_option_text.svg b/frontend/resources/flowy_icons/16x/field_option_text.svg new file mode 100644 index 0000000000..42dd49cd41 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/field_option_text.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/16x/field_option_url.svg b/frontend/resources/flowy_icons/16x/field_option_url.svg new file mode 100644 index 0000000000..1cd066ed8f --- /dev/null +++ b/frontend/resources/flowy_icons/16x/field_option_url.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/40x/field_option_checkbox.svg b/frontend/resources/flowy_icons/40x/field_option_checkbox.svg new file mode 100644 index 0000000000..1ace2179bd --- /dev/null +++ b/frontend/resources/flowy_icons/40x/field_option_checkbox.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/40x/field_option_checklist.svg b/frontend/resources/flowy_icons/40x/field_option_checklist.svg new file mode 100644 index 0000000000..45c1b45a9a --- /dev/null +++ b/frontend/resources/flowy_icons/40x/field_option_checklist.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/40x/field_option_date.svg b/frontend/resources/flowy_icons/40x/field_option_date.svg new file mode 100644 index 0000000000..cf11f2d06c --- /dev/null +++ b/frontend/resources/flowy_icons/40x/field_option_date.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/40x/field_option_document.svg b/frontend/resources/flowy_icons/40x/field_option_document.svg new file mode 100644 index 0000000000..92ada1cfa4 --- /dev/null +++ b/frontend/resources/flowy_icons/40x/field_option_document.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/flowy_icons/40x/field_option_email.svg b/frontend/resources/flowy_icons/40x/field_option_email.svg new file mode 100644 index 0000000000..2df23d50be --- /dev/null +++ b/frontend/resources/flowy_icons/40x/field_option_email.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/40x/field_option_file.svg b/frontend/resources/flowy_icons/40x/field_option_file.svg new file mode 100644 index 0000000000..4680ec52eb --- /dev/null +++ b/frontend/resources/flowy_icons/40x/field_option_file.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/40x/field_option_formula.svg b/frontend/resources/flowy_icons/40x/field_option_formula.svg new file mode 100644 index 0000000000..6c797443c4 --- /dev/null +++ b/frontend/resources/flowy_icons/40x/field_option_formula.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/40x/field_option_multiselect.svg b/frontend/resources/flowy_icons/40x/field_option_multiselect.svg new file mode 100644 index 0000000000..44f89d0ccf --- /dev/null +++ b/frontend/resources/flowy_icons/40x/field_option_multiselect.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/40x/field_option_number.svg b/frontend/resources/flowy_icons/40x/field_option_number.svg new file mode 100644 index 0000000000..0c140e641c --- /dev/null +++ b/frontend/resources/flowy_icons/40x/field_option_number.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/flowy_icons/40x/field_option_person.svg b/frontend/resources/flowy_icons/40x/field_option_person.svg new file mode 100644 index 0000000000..b08675ae64 --- /dev/null +++ b/frontend/resources/flowy_icons/40x/field_option_person.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/flowy_icons/40x/field_option_phone.svg b/frontend/resources/flowy_icons/40x/field_option_phone.svg new file mode 100644 index 0000000000..f849eaed28 --- /dev/null +++ b/frontend/resources/flowy_icons/40x/field_option_phone.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/40x/field_option_relation.svg b/frontend/resources/flowy_icons/40x/field_option_relation.svg new file mode 100644 index 0000000000..f1b1e342ff --- /dev/null +++ b/frontend/resources/flowy_icons/40x/field_option_relation.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/flowy_icons/40x/field_option_select.svg b/frontend/resources/flowy_icons/40x/field_option_select.svg new file mode 100644 index 0000000000..48976cbb69 --- /dev/null +++ b/frontend/resources/flowy_icons/40x/field_option_select.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/flowy_icons/40x/field_option_text.svg b/frontend/resources/flowy_icons/40x/field_option_text.svg new file mode 100644 index 0000000000..7800d5b5b3 --- /dev/null +++ b/frontend/resources/flowy_icons/40x/field_option_text.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/40x/field_option_url.svg b/frontend/resources/flowy_icons/40x/field_option_url.svg new file mode 100644 index 0000000000..be8849bd10 --- /dev/null +++ b/frontend/resources/flowy_icons/40x/field_option_url.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 96c47d4400..15caebccc2 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -532,6 +532,7 @@ "visibility": "Visibility", "propertyType": "Property type", "addSelectOption": "Add an option", + "typeANewOption": "Type a new option", "optionTitle": "Options", "addOption": "Add option", "editProperty": "Edit property", @@ -1131,6 +1132,7 @@ "language": "Language", "font": "Font", "actions": "Actions", - "date": "Date" + "date": "Date", + "addField": "Add field" } }