From 55b888e36443e06ca52835a607428085c7f95288 Mon Sep 17 00:00:00 2001 From: appflowy Date: Tue, 29 Mar 2022 11:29:21 +0800 Subject: [PATCH] chore: single selection field --- .../app_flowy/assets/translations/en.json | 5 +- .../field/type_option/option_pannel_bloc.dart | 56 ++++++ .../grid/src/widgets/content/grid_row.dart | 59 ++---- .../widgets/header/create_field_pannel.dart | 2 +- .../src/widgets/header/field_type_list.dart | 25 +-- .../widgets/header/type_option/number.dart | 2 +- .../widgets/header/type_option/selection.dart | 172 +++++++++++++++++- .../widgets/header/type_option/widget.dart | 65 +++++++ .../lib/widget/rounded_input_field.dart | 6 + 9 files changed, 333 insertions(+), 59 deletions(-) create mode 100644 frontend/app_flowy/lib/workspace/application/grid/field/type_option/option_pannel_bloc.dart create mode 100644 frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/widget.dart diff --git a/frontend/app_flowy/assets/translations/en.json b/frontend/app_flowy/assets/translations/en.json index 0fe9008ee6..fbd672c04f 100644 --- a/frontend/app_flowy/assets/translations/en.json +++ b/frontend/app_flowy/assets/translations/en.json @@ -163,7 +163,10 @@ "dateFormatUS": "Month/Month/Day", "timeFormat": " Time format", "timeFormatTwelveHour": "12 hour", - "timeFormatTwentyFourHour": "24 hour" + "timeFormatTwentyFourHour": "24 hour", + "addSelectOption": "Add an option", + "optionTitle": "Options", + "addOption": "Add option" } } } diff --git a/frontend/app_flowy/lib/workspace/application/grid/field/type_option/option_pannel_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/field/type_option/option_pannel_bloc.dart new file mode 100644 index 0000000000..aacf06c98d --- /dev/null +++ b/frontend/app_flowy/lib/workspace/application/grid/field/type_option/option_pannel_bloc.dart @@ -0,0 +1,56 @@ +import 'dart:typed_data'; + +import 'package:flowy_sdk/log.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; +import 'package:dartz/dartz.dart'; + +part 'option_pannel_bloc.freezed.dart'; + +class OptionPannelBloc extends Bloc { + OptionPannelBloc({required List options}) : super(OptionPannelState.initial(options)) { + on( + (event, emit) async { + await event.map( + createOption: (_CreateOption value) async { + emit(state.copyWith(isAddingOption: false)); + }, + beginAddingOption: (_BeginAddingOption value) { + emit(state.copyWith(isAddingOption: true)); + }, + endAddingOption: (_EndAddingOption value) { + emit(state.copyWith(isAddingOption: false)); + }, + ); + }, + ); + } + + @override + Future close() async { + return super.close(); + } +} + +@freezed +class OptionPannelEvent with _$OptionPannelEvent { + const factory OptionPannelEvent.createOption(String optionName) = _CreateOption; + const factory OptionPannelEvent.beginAddingOption() = _BeginAddingOption; + const factory OptionPannelEvent.endAddingOption() = _EndAddingOption; +} + +@freezed +class OptionPannelState with _$OptionPannelState { + const factory OptionPannelState({ + required List options, + required bool isAddingOption, + }) = _OptionPannelState; + + factory OptionPannelState.initial(List options) => OptionPannelState( + options: options, + isAddingOption: false, + ); +} diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/content/grid_row.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/content/grid_row.dart index a813a51dc0..b8834cd213 100755 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/content/grid_row.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/content/grid_row.dart @@ -30,47 +30,28 @@ class _GridRowWidgetState extends State { Widget build(BuildContext context) { return BlocProvider.value( value: _rowBloc, - child: BlocBuilder( - buildWhen: (p, c) => p.rowHeight != c.rowHeight, - builder: (context, state) { - return SizedBox( - height: _rowBloc.state.rowHeight, - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: const [ - _RowLeading(), - _RowCells(), - _RowTrailing(), - ], - ), - ); - }, + child: MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (p) => _rowBloc.add(const RowEvent.activeRow()), + onExit: (p) => _rowBloc.add(const RowEvent.disactiveRow()), + child: BlocBuilder( + buildWhen: (p, c) => p.rowHeight != c.rowHeight, + builder: (context, state) { + return SizedBox( + height: _rowBloc.state.rowHeight, + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: const [ + _RowLeading(), + _RowCells(), + _RowTrailing(), + ], + ), + ); + }, + ), ), ); - // return BlocProvider.value( - // value: _rowBloc, - // child: MouseRegion( - // cursor: SystemMouseCursors.click, - // onEnter: (p) => _rowBloc.add(const RowEvent.activeRow()), - // onExit: (p) => _rowBloc.add(const RowEvent.disactiveRow()), - // child: BlocBuilder( - // buildWhen: (p, c) => p.rowHeight != c.rowHeight, - // builder: (context, state) { - // return SizedBox( - // height: _rowBloc.state.rowHeight, - // child: Row( - // crossAxisAlignment: CrossAxisAlignment.stretch, - // children: const [ - // _RowLeading(), - // _RowCells(), - // _RowTrailing(), - // ], - // ), - // ); - // }, - // ), - // ), - // ); } @override diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/create_field_pannel.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/create_field_pannel.dart index 8b2b74d9ae..c282f10b40 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/create_field_pannel.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/create_field_pannel.dart @@ -20,7 +20,7 @@ class CreateFieldPannel extends FlowyOverlayDelegate { FlowyOverlay.of(context).insertWithAnchor( widget: OverlayContainer( child: _CreateFieldPannelWidget(_createFieldBloc), - constraints: BoxConstraints.loose(const Size(220, 400)), + constraints: BoxConstraints.loose(const Size(220, 500)), ), identifier: identifier(), anchorContext: context, diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_type_list.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_type_list.dart index dfc5c007c6..0019ffa979 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_type_list.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_type_list.dart @@ -49,17 +49,20 @@ class FieldTypeList extends StatelessWidget { ); }).toList(); - return ListView.separated( - shrinkWrap: true, - controller: ScrollController(), - itemCount: cells.length, - separatorBuilder: (context, index) { - return const VSpace(10); - }, - physics: StyledScrollPhysics(), - itemBuilder: (BuildContext context, int index) { - return cells[index]; - }, + return SizedBox( + width: 140, + child: ListView.separated( + shrinkWrap: true, + controller: ScrollController(), + itemCount: cells.length, + separatorBuilder: (context, index) { + return const VSpace(10); + }, + physics: StyledScrollPhysics(), + itemBuilder: (BuildContext context, int index) { + return cells[index]; + }, + ), ); } diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/number.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/number.dart index 6aeb73ac48..57a8254f2e 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/number.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/number.dart @@ -111,7 +111,7 @@ class NumberFormatItem extends StatelessWidget { Widget build(BuildContext context) { final theme = context.watch(); return SizedBox( - height: 26, + height: GridSize.typeOptionItemHeight, child: FlowyButton( text: FlowyText.medium(format.title(), fontSize: 12), hoverColor: theme.hover, diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/selection.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/selection.dart index b5737fd219..2d5c963fc5 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/selection.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/selection.dart @@ -1,9 +1,20 @@ import 'package:app_flowy/startup/startup.dart'; +import 'package:app_flowy/workspace/application/grid/field/type_option/option_pannel_bloc.dart'; import 'package:app_flowy/workspace/application/grid/field/type_option/selection_bloc.dart'; +import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart'; import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/field_tyep_switcher.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra/theme.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:app_flowy/generated/locale_keys.g.dart'; + +import 'widget.dart'; class SingleSelectTypeOptionBuilder extends TypeOptionBuilder { SingleSelectTypeOption typeOption; @@ -12,17 +23,18 @@ class SingleSelectTypeOptionBuilder extends TypeOptionBuilder { : typeOption = SingleSelectTypeOption.fromBuffer(typeOptionData); @override - Widget? get customWidget => const SingleSelectTypeOptionWidget(); + Widget? get customWidget => SingleSelectTypeOptionWidget(typeOption); } class SingleSelectTypeOptionWidget extends TypeOptionWidget { - const SingleSelectTypeOptionWidget({Key? key}) : super(key: key); + final SingleSelectTypeOption typeOption; + const SingleSelectTypeOptionWidget(this.typeOption, {Key? key}) : super(key: key); @override Widget build(BuildContext context) { return BlocProvider( create: (context) => getIt(), - child: Container(height: 100, color: Colors.yellow), + child: OptionPannel(options: typeOption.options), ); } } @@ -34,17 +46,165 @@ class MultiSelectTypeOptionBuilder extends TypeOptionBuilder { : typeOption = MultiSelectTypeOption.fromBuffer(typeOptionData); @override - Widget? get customWidget => const MultiSelectTypeOptionWidget(); + Widget? get customWidget => MultiSelectTypeOptionWidget(typeOption); } class MultiSelectTypeOptionWidget extends TypeOptionWidget { - const MultiSelectTypeOptionWidget({Key? key}) : super(key: key); + final MultiSelectTypeOption typeOption; + const MultiSelectTypeOptionWidget(this.typeOption, {Key? key}) : super(key: key); @override Widget build(BuildContext context) { return BlocProvider( create: (context) => getIt(), - child: Container(height: 100, color: Colors.blue), + child: OptionPannel(options: typeOption.options), ); } } + +class OptionPannel extends StatelessWidget { + final List options; + const OptionPannel({required this.options, Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => OptionPannelBloc(options: options), + child: BlocBuilder( + builder: (context, state) { + List children = [const OptionTitle()]; + if (state.isAddingOption) { + children.add(const _AddOptionTextField()); + } + + if (state.options.isEmpty && !state.isAddingOption) { + children.add(const _AddOptionButton()); + } + + if (state.options.isNotEmpty) { + children.add(const _OptionList()); + } + + return Column(children: children); + }, + ), + ); + } +} + +class OptionTitle extends StatelessWidget { + const OptionTitle({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final theme = context.watch(); + + return BlocBuilder( + buildWhen: (previous, current) => previous.options.length != current.options.length, + builder: (context, state) { + List children = [FlowyText.medium(LocaleKeys.grid_field_optionTitle.tr(), fontSize: 12)]; + + if (state.options.isNotEmpty && state.isAddingOption == false) { + children.add(FlowyButton( + text: FlowyText.medium(LocaleKeys.grid_field_addOption.tr(), fontSize: 12), + hoverColor: theme.hover, + onTap: () { + context.read().add(const OptionPannelEvent.beginAddingOption()); + }, + rightIcon: svg("grid/more", color: theme.iconColor), + )); + } + + return SizedBox( + height: GridSize.typeOptionItemHeight, + child: Row(children: children), + ); + }, + ); + } +} + +class _OptionList extends StatelessWidget { + const _OptionList({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final optionItems = state.options.map((option) { + return _OptionItem(option: option); + }).toList(); + + return SizedBox( + width: 120, + child: ListView.separated( + shrinkWrap: true, + controller: ScrollController(), + separatorBuilder: (context, index) { + return VSpace(GridSize.typeOptionSeparatorHeight); + }, + itemCount: optionItems.length, + itemBuilder: (BuildContext context, int index) { + return optionItems[index]; + }, + ), + ); + }, + ); + } +} + +class _OptionItem extends StatelessWidget { + final SelectOption option; + const _OptionItem({required this.option, Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final theme = context.watch(); + return SizedBox( + height: GridSize.typeOptionItemHeight, + child: FlowyButton( + text: FlowyText.medium(option.name, fontSize: 12), + hoverColor: theme.hover, + onTap: () {}, + rightIcon: svg("grid/more", color: theme.iconColor), + ), + ); + } +} + +class _AddOptionButton extends StatelessWidget { + const _AddOptionButton({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final theme = context.watch(); + return SizedBox( + height: GridSize.typeOptionItemHeight, + child: FlowyButton( + text: FlowyText.medium(LocaleKeys.grid_field_addSelectOption.tr(), fontSize: 12), + hoverColor: theme.hover, + onTap: () { + context.read().add(const OptionPannelEvent.beginAddingOption()); + }, + leftIcon: svg("home/add", color: theme.iconColor), + ), + ); + } +} + +class _AddOptionTextField extends StatelessWidget { + const _AddOptionTextField({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return NameTextField( + name: "", + onCanceled: () { + context.read().add(const OptionPannelEvent.endAddingOption()); + }, + onDone: (optionName) { + context.read().add(OptionPannelEvent.createOption(optionName)); + }); + } +} diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/widget.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/widget.dart new file mode 100644 index 0000000000..f357c9afea --- /dev/null +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/widget.dart @@ -0,0 +1,65 @@ +import 'package:flowy_infra/theme.dart'; +import 'package:flowy_infra_ui/widget/rounded_input_field.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class NameTextField extends StatefulWidget { + final void Function(String) onDone; + final void Function() onCanceled; + final String name; + + const NameTextField({ + required this.name, + required this.onDone, + required this.onCanceled, + Key? key, + }) : super(key: key); + + @override + State createState() => _NameTextFieldState(); +} + +class _NameTextFieldState extends State { + late FocusNode _focusNode; + late TextEditingController _controller; + + @override + void initState() { + _focusNode = FocusNode(); + _controller = TextEditingController(text: widget.name); + + _focusNode.addListener(notifyDidEndEditing); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final theme = context.watch(); + return RoundedInputField( + controller: _controller, + focusNode: _focusNode, + height: 36, + style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500), + normalBorderColor: theme.shader4, + errorBorderColor: theme.red, + focusBorderColor: theme.main1, + cursorColor: theme.main1, + onChanged: (text) { + print(text); + }); + } + + @override + void dispose() { + _focusNode.removeListener(notifyDidEndEditing); + super.dispose(); + } + + void notifyDidEndEditing() { + if (_controller.text.isEmpty) { + // widget.onCanceled(); + } else { + widget.onDone(_controller.text); + } + } +} diff --git a/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/rounded_input_field.dart b/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/rounded_input_field.dart index 110cd29c73..b6bf1daf4b 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/rounded_input_field.dart +++ b/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/rounded_input_field.dart @@ -20,6 +20,8 @@ class RoundedInputField extends StatefulWidget { final EdgeInsets padding; final EdgeInsets contentPadding; final double height; + final FocusNode? focusNode; + final TextEditingController? controller; const RoundedInputField({ Key? key, @@ -39,6 +41,8 @@ class RoundedInputField extends StatefulWidget { this.padding = EdgeInsets.zero, this.contentPadding = const EdgeInsets.symmetric(horizontal: 10), this.height = 48, + this.focusNode, + this.controller, }) : super(key: key); @override @@ -71,7 +75,9 @@ class _RoundedInputFieldState extends State { padding: widget.padding, height: widget.height, child: TextFormField( + controller: widget.controller, initialValue: widget.initialValue, + focusNode: widget.focusNode, onChanged: (value) { inputText = value; if (widget.onChanged != null) {