mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: support select / multi select option (#4039)
* feat: implement select option cell * feat: support adding new select option * feat: support selecting / deselecting option * feat: clear search field after adding new property * feat: support updating option name
This commit is contained in:
@ -11,12 +11,14 @@ Future<void> showMobileBottomSheet({
|
|||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
required WidgetBuilder builder,
|
required WidgetBuilder builder,
|
||||||
bool isDragEnabled = true,
|
bool isDragEnabled = true,
|
||||||
|
ShapeBorder? shape,
|
||||||
}) async {
|
}) async {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
enableDrag: isDragEnabled,
|
enableDrag: isDragEnabled,
|
||||||
useSafeArea: true,
|
useSafeArea: true,
|
||||||
|
shape: shape,
|
||||||
builder: builder,
|
builder: builder,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,49 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
// include single select and multiple select
|
||||||
|
class EditSelectField extends StatefulWidget {
|
||||||
|
const EditSelectField({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<EditSelectField> createState() => _EditSelectFieldState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EditSelectFieldState extends State<EditSelectField> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
_SearchField(
|
||||||
|
hintText: LocaleKeys.grid_selectOption_searchOrCreateOption.tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SearchField extends StatelessWidget {
|
||||||
|
const _SearchField({
|
||||||
|
required this.hintText,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String hintText;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 44, // the height is fixed.
|
||||||
|
child: FlowyTextField(
|
||||||
|
hintText: hintText,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
20
frontend/appflowy_flutter/lib/plugins/base/drag_handler.dart
Normal file
20
frontend/appflowy_flutter/lib/plugins/base/drag_handler.dart
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class DragHandler extends StatelessWidget {
|
||||||
|
const DragHandler({
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
height: 4,
|
||||||
|
width: 40,
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade400,
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,638 @@
|
|||||||
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/plugins/base/drag_handler.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor_bloc.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra/size.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:protobuf/protobuf.dart';
|
||||||
|
|
||||||
|
// include single select and multiple select
|
||||||
|
class MobileSelectOptionEditor extends StatefulWidget {
|
||||||
|
const MobileSelectOptionEditor({
|
||||||
|
super.key,
|
||||||
|
required this.cellController,
|
||||||
|
});
|
||||||
|
|
||||||
|
final SelectOptionCellController cellController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MobileSelectOptionEditor> createState() =>
|
||||||
|
_MobileSelectOptionEditorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MobileSelectOptionEditorState extends State<MobileSelectOptionEditor> {
|
||||||
|
final searchController = TextEditingController();
|
||||||
|
final renameController = TextEditingController();
|
||||||
|
|
||||||
|
String typingOption = '';
|
||||||
|
FieldType get fieldType => widget.cellController.fieldType;
|
||||||
|
|
||||||
|
bool showMoreOptions = false;
|
||||||
|
SelectOptionPB? option;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
searchController.dispose();
|
||||||
|
renameController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
constraints: const BoxConstraints.tightFor(height: 420),
|
||||||
|
child: BlocProvider(
|
||||||
|
create: (context) => SelectOptionCellEditorBloc(
|
||||||
|
cellController: widget.cellController,
|
||||||
|
)..add(const SelectOptionEditorEvent.initial()),
|
||||||
|
child: BlocBuilder<SelectOptionCellEditorBloc, SelectOptionEditorState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const DragHandler(),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
|
child: _buildHeader(
|
||||||
|
context,
|
||||||
|
showSaveButton: state.createOption
|
||||||
|
.fold(() => false, (a) => a.isNotEmpty) ||
|
||||||
|
showMoreOptions,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: showMoreOptions ? 0.0 : 16.0,
|
||||||
|
),
|
||||||
|
child: _buildBody(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHeader(BuildContext context, {required bool showSaveButton}) {
|
||||||
|
const iconWidth = 36.0;
|
||||||
|
const height = 44.0;
|
||||||
|
return Stack(
|
||||||
|
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: FlowyIconButton(
|
||||||
|
icon: const FlowySvg(
|
||||||
|
FlowySvgs.close_s,
|
||||||
|
size: Size.square(iconWidth),
|
||||||
|
),
|
||||||
|
width: iconWidth,
|
||||||
|
iconPadding: EdgeInsets.zero,
|
||||||
|
onPressed: () => _popOrBack(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 44.0,
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: FlowyText.medium(
|
||||||
|
_headerTitle(),
|
||||||
|
fontSize: 18,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: !showSaveButton
|
||||||
|
? const HSpace(iconWidth)
|
||||||
|
: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 2.0,
|
||||||
|
horizontal: 8.0,
|
||||||
|
),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Color(0xFF00bcf0),
|
||||||
|
borderRadius: Corners.s10Border,
|
||||||
|
),
|
||||||
|
child: FlowyButton(
|
||||||
|
text: FlowyText(
|
||||||
|
LocaleKeys.button_save.tr(),
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
useIntrinsicWidth: true,
|
||||||
|
onTap: () {
|
||||||
|
if (showMoreOptions) {
|
||||||
|
final option = this.option;
|
||||||
|
if (option == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
option.freeze();
|
||||||
|
context.read<SelectOptionCellEditorBloc>().add(
|
||||||
|
SelectOptionEditorEvent.updateOption(
|
||||||
|
option.rebuild((p0) {
|
||||||
|
if (p0.name != renameController.text) {
|
||||||
|
p0.name = renameController.text;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
_popOrBack();
|
||||||
|
} else if (typingOption.isNotEmpty) {
|
||||||
|
context.read<SelectOptionCellEditorBloc>().add(
|
||||||
|
SelectOptionEditorEvent.trySelectOption(
|
||||||
|
typingOption,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
searchController.clear();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
].map((e) => SizedBox(height: height, child: e)).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBody(BuildContext context) {
|
||||||
|
if (showMoreOptions && option != null) {
|
||||||
|
return _MoreOptions(
|
||||||
|
option: option!,
|
||||||
|
controller: renameController..text = option!.name,
|
||||||
|
onDelete: () {
|
||||||
|
context
|
||||||
|
.read<SelectOptionCellEditorBloc>()
|
||||||
|
.add(SelectOptionEditorEvent.deleteOption(option!));
|
||||||
|
context.pop();
|
||||||
|
},
|
||||||
|
onUpdate: (name, color) {
|
||||||
|
final option = this.option;
|
||||||
|
if (option == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
option.freeze();
|
||||||
|
context.read<SelectOptionCellEditorBloc>().add(
|
||||||
|
SelectOptionEditorEvent.updateOption(
|
||||||
|
option.rebuild((p0) {
|
||||||
|
if (name != null) {
|
||||||
|
p0.name = name;
|
||||||
|
}
|
||||||
|
if (color != null) {
|
||||||
|
p0.color = color;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
_popOrBack();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_SearchField(
|
||||||
|
controller: searchController,
|
||||||
|
hintText: LocaleKeys.grid_selectOption_searchOrCreateOption.tr(),
|
||||||
|
onSubmitted: (option) {
|
||||||
|
context
|
||||||
|
.read<SelectOptionCellEditorBloc>()
|
||||||
|
.add(SelectOptionEditorEvent.trySelectOption(option));
|
||||||
|
searchController.clear();
|
||||||
|
},
|
||||||
|
onChanged: (value) {
|
||||||
|
typingOption = value;
|
||||||
|
context.read<SelectOptionCellEditorBloc>().add(
|
||||||
|
SelectOptionEditorEvent.selectMultipleOptions(
|
||||||
|
[],
|
||||||
|
value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_OptionList(
|
||||||
|
onCreateOption: (optionName) {
|
||||||
|
context
|
||||||
|
.read<SelectOptionCellEditorBloc>()
|
||||||
|
.add(SelectOptionEditorEvent.newOption(optionName));
|
||||||
|
searchController.clear();
|
||||||
|
},
|
||||||
|
onCheck: (option, value) {
|
||||||
|
if (value) {
|
||||||
|
context
|
||||||
|
.read<SelectOptionCellEditorBloc>()
|
||||||
|
.add(SelectOptionEditorEvent.selectOption(option.id));
|
||||||
|
} else {
|
||||||
|
context
|
||||||
|
.read<SelectOptionCellEditorBloc>()
|
||||||
|
.add(SelectOptionEditorEvent.unSelectOption(option.id));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onMoreOptions: (option) {
|
||||||
|
setState(() {
|
||||||
|
this.option = option;
|
||||||
|
showMoreOptions = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _headerTitle() {
|
||||||
|
switch (fieldType) {
|
||||||
|
case FieldType.SingleSelect:
|
||||||
|
return LocaleKeys.grid_field_singleSelectFieldName.tr();
|
||||||
|
case FieldType.MultiSelect:
|
||||||
|
return LocaleKeys.grid_field_multiSelectFieldName.tr();
|
||||||
|
default:
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _popOrBack() {
|
||||||
|
if (showMoreOptions) {
|
||||||
|
setState(() {
|
||||||
|
showMoreOptions = false;
|
||||||
|
option = null;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SearchField extends StatelessWidget {
|
||||||
|
const _SearchField({
|
||||||
|
this.hintText,
|
||||||
|
required this.onChanged,
|
||||||
|
required this.onSubmitted,
|
||||||
|
required this.controller,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String? hintText;
|
||||||
|
final void Function(String value) onChanged;
|
||||||
|
final void Function(String value) onSubmitted;
|
||||||
|
final TextEditingController controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final textStyle = Theme.of(context).textTheme.bodyMedium;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 44, // the height is fixed.
|
||||||
|
child: FlowyTextField(
|
||||||
|
autoFocus: false,
|
||||||
|
hintText: hintText,
|
||||||
|
textStyle: textStyle,
|
||||||
|
hintStyle: textStyle?.copyWith(color: Theme.of(context).hintColor),
|
||||||
|
onChanged: onChanged,
|
||||||
|
onSubmitted: onSubmitted,
|
||||||
|
controller: controller,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OptionList extends StatelessWidget {
|
||||||
|
const _OptionList({
|
||||||
|
required this.onCreateOption,
|
||||||
|
required this.onCheck,
|
||||||
|
required this.onMoreOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
final void Function(String optionName) onCreateOption;
|
||||||
|
final void Function(SelectOptionPB option, bool value) onCheck;
|
||||||
|
final void Function(SelectOptionPB option) onMoreOptions;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocBuilder<SelectOptionCellEditorBloc, SelectOptionEditorState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
// existing options
|
||||||
|
final List<Widget> cells = [];
|
||||||
|
|
||||||
|
// create an option cell
|
||||||
|
state.createOption.fold(
|
||||||
|
() => null,
|
||||||
|
(createOption) {
|
||||||
|
cells.add(
|
||||||
|
_CreateOptionCell(
|
||||||
|
optionName: createOption,
|
||||||
|
onTap: () => onCreateOption(createOption),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
cells.addAll(
|
||||||
|
state.options.map(
|
||||||
|
(option) => _SelectOption(
|
||||||
|
option: option,
|
||||||
|
checked: state.selectedOptions.contains(option),
|
||||||
|
onCheck: (value) => onCheck(option, value),
|
||||||
|
onMoreOptions: () => onMoreOptions(option),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return ListView.separated(
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: cells.length,
|
||||||
|
separatorBuilder: (_, __) =>
|
||||||
|
VSpace(GridSize.typeOptionSeparatorHeight),
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
itemBuilder: (_, int index) => cells[index],
|
||||||
|
padding: const EdgeInsets.only(bottom: 12.0),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SelectOption extends StatelessWidget {
|
||||||
|
const _SelectOption({
|
||||||
|
required this.option,
|
||||||
|
required this.checked,
|
||||||
|
required this.onCheck,
|
||||||
|
required this.onMoreOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
final SelectOptionPB option;
|
||||||
|
final bool checked;
|
||||||
|
final void Function(bool value) onCheck;
|
||||||
|
final VoidCallback onMoreOptions;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 44,
|
||||||
|
child: GestureDetector(
|
||||||
|
// no need to add click effect, so using gesture detector
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
|
onTap: () => onCheck(!checked),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
// check icon
|
||||||
|
FlowySvg(
|
||||||
|
checked
|
||||||
|
? FlowySvgs.m_checkbox_checked_s
|
||||||
|
: FlowySvgs.m_checkbox_uncheck_s,
|
||||||
|
size: const Size.square(24.0),
|
||||||
|
blendMode: null,
|
||||||
|
),
|
||||||
|
// padding
|
||||||
|
const HSpace(12),
|
||||||
|
// option tag
|
||||||
|
_SelectOptionTag(
|
||||||
|
optionName: option.name,
|
||||||
|
color: option.color.toColor(context),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
// more options
|
||||||
|
FlowyIconButton(
|
||||||
|
icon: const FlowySvg(FlowySvgs.three_dots_s),
|
||||||
|
onPressed: onMoreOptions,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CreateOptionCell extends StatelessWidget {
|
||||||
|
const _CreateOptionCell({
|
||||||
|
required this.optionName,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String optionName;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 44,
|
||||||
|
child: GestureDetector(
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
|
onTap: onTap,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
FlowyText.medium(
|
||||||
|
LocaleKeys.grid_selectOption_create.tr(),
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
|
),
|
||||||
|
const HSpace(8),
|
||||||
|
Expanded(
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: _SelectOptionTag(
|
||||||
|
optionName: optionName,
|
||||||
|
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SelectOptionTag extends StatelessWidget {
|
||||||
|
const _SelectOptionTag({
|
||||||
|
required this.optionName,
|
||||||
|
required this.color,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String optionName;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 6.0,
|
||||||
|
horizontal: 12.0,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color,
|
||||||
|
borderRadius: Corners.s12Border,
|
||||||
|
),
|
||||||
|
child: FlowyText.regular(
|
||||||
|
optionName,
|
||||||
|
fontSize: 16,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
color: AFThemeExtension.of(context).textColor,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MoreOptions extends StatelessWidget {
|
||||||
|
const _MoreOptions({
|
||||||
|
required this.option,
|
||||||
|
required this.onDelete,
|
||||||
|
required this.onUpdate,
|
||||||
|
required this.controller,
|
||||||
|
});
|
||||||
|
|
||||||
|
final SelectOptionPB option;
|
||||||
|
final VoidCallback onDelete;
|
||||||
|
final void Function(String? name, SelectOptionColorPB? color) onUpdate;
|
||||||
|
final TextEditingController controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const VSpace(8.0),
|
||||||
|
_buildRenameTextField(context),
|
||||||
|
const VSpace(16.0),
|
||||||
|
_buildDeleteButton(context),
|
||||||
|
const VSpace(16.0),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 12.0),
|
||||||
|
child: FlowyText(
|
||||||
|
LocaleKeys.grid_field_optionTitle.tr(),
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const VSpace(4.0),
|
||||||
|
_buildColorOptions(context),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRenameTextField(BuildContext context) {
|
||||||
|
return ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints.tightFor(height: 52.0),
|
||||||
|
child: _DefaultDecorateBox(
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDeleteButton(BuildContext context) {
|
||||||
|
return _DefaultDecorateBox(
|
||||||
|
showTopBorder: true,
|
||||||
|
showBottomBorder: true,
|
||||||
|
child: FlowyButton(
|
||||||
|
text: FlowyText(
|
||||||
|
LocaleKeys.button_delete.tr(),
|
||||||
|
fontSize: 16.0,
|
||||||
|
),
|
||||||
|
margin: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12.0,
|
||||||
|
vertical: 16.0,
|
||||||
|
),
|
||||||
|
leftIcon: const FlowySvg(FlowySvgs.delete_s),
|
||||||
|
leftIconSize: const Size.square(24.0),
|
||||||
|
iconPadding: 8.0,
|
||||||
|
onTap: onDelete,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildColorOptions(BuildContext context) {
|
||||||
|
return _DefaultDecorateBox(
|
||||||
|
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(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DefaultDecorateBox extends StatelessWidget {
|
||||||
|
const _DefaultDecorateBox({
|
||||||
|
this.showTopBorder = true,
|
||||||
|
this.showBottomBorder = true,
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool showTopBorder;
|
||||||
|
final bool showBottomBorder;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
|
border: Border(
|
||||||
|
top: BorderSide(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
),
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,10 @@
|
|||||||
|
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/mobile_select_option_editor.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
|
import 'package:flowy_infra/size.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
@ -171,12 +175,19 @@ class SelectOptionWrap extends StatefulWidget {
|
|||||||
class _SelectOptionWrapState extends State<SelectOptionWrap> {
|
class _SelectOptionWrapState extends State<SelectOptionWrap> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final Widget child = _buildOptions(context);
|
|
||||||
|
|
||||||
final constraints = BoxConstraints.loose(
|
final constraints = BoxConstraints.loose(
|
||||||
Size(SelectOptionCellEditor.editorPanelWidth, 300),
|
Size(SelectOptionCellEditor.editorPanelWidth, 300),
|
||||||
);
|
);
|
||||||
return AppFlowyPopover(
|
final cellController =
|
||||||
|
widget.cellControllerBuilder.build() as SelectOptionCellController;
|
||||||
|
|
||||||
|
Widget child = Padding(
|
||||||
|
padding: widget.cellStyle?.cellPadding ?? GridSize.cellContentInsets,
|
||||||
|
child: _buildOptions(context),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (PlatformExtension.isDesktopOrWeb) {
|
||||||
|
child = AppFlowyPopover(
|
||||||
controller: widget.popoverController,
|
controller: widget.popoverController,
|
||||||
constraints: constraints,
|
constraints: constraints,
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
@ -186,16 +197,34 @@ class _SelectOptionWrapState extends State<SelectOptionWrap> {
|
|||||||
widget.onCellEditing(true);
|
widget.onCellEditing(true);
|
||||||
});
|
});
|
||||||
return SelectOptionCellEditor(
|
return SelectOptionCellEditor(
|
||||||
cellController: widget.cellControllerBuilder.build()
|
cellController: cellController,
|
||||||
as SelectOptionCellController,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onClose: () => widget.onCellEditing(false),
|
onClose: () => widget.onCellEditing(false),
|
||||||
child: Padding(
|
|
||||||
padding: widget.cellStyle?.cellPadding ?? GridSize.cellContentInsets,
|
|
||||||
child: child,
|
child: child,
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
child = FlowyButton(
|
||||||
|
text: child,
|
||||||
|
onTap: () {
|
||||||
|
showMobileBottomSheet(
|
||||||
|
context: context,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(
|
||||||
|
top: Corners.s12Radius,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
builder: (context) {
|
||||||
|
return MobileSelectOptionEditor(
|
||||||
|
cellController: cellController,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return child;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildOptions(BuildContext context) {
|
Widget _buildOptions(BuildContext context) {
|
||||||
|
@ -70,12 +70,16 @@ class MobileAppearance extends BaseAppearance {
|
|||||||
onSurface: const Color(0xffC5C6C7), // text/body color
|
onSurface: const Color(0xffC5C6C7), // text/body color
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final hintColor = brightness == Brightness.light
|
||||||
|
? const Color(0xff89909B)
|
||||||
|
: const Color(0xff96989C);
|
||||||
|
|
||||||
return ThemeData(
|
return ThemeData(
|
||||||
// color
|
// color
|
||||||
primaryColor: colorTheme.primary, //primary 100
|
primaryColor: colorTheme.primary, //primary 100
|
||||||
primaryColorLight: const Color(0xFF57B5F8), //primary 80
|
primaryColorLight: const Color(0xFF57B5F8), //primary 80
|
||||||
dividerColor: colorTheme.outline, //caption
|
dividerColor: colorTheme.outline, //caption
|
||||||
hintColor: colorTheme.outline,
|
hintColor: hintColor,
|
||||||
disabledColor: colorTheme.outline,
|
disabledColor: colorTheme.outline,
|
||||||
scaffoldBackgroundColor: colorTheme.background,
|
scaffoldBackgroundColor: colorTheme.background,
|
||||||
appBarTheme: AppBarTheme(
|
appBarTheme: AppBarTheme(
|
||||||
|
@ -27,6 +27,7 @@ class FlowyTextField extends StatefulWidget {
|
|||||||
final BoxConstraints? prefixIconConstraints;
|
final BoxConstraints? prefixIconConstraints;
|
||||||
final BoxConstraints? suffixIconConstraints;
|
final BoxConstraints? suffixIconConstraints;
|
||||||
final BoxConstraints? hintTextConstraints;
|
final BoxConstraints? hintTextConstraints;
|
||||||
|
final TextStyle? hintStyle;
|
||||||
|
|
||||||
const FlowyTextField({
|
const FlowyTextField({
|
||||||
super.key,
|
super.key,
|
||||||
@ -52,6 +53,7 @@ class FlowyTextField extends StatefulWidget {
|
|||||||
this.prefixIconConstraints,
|
this.prefixIconConstraints,
|
||||||
this.suffixIconConstraints,
|
this.suffixIconConstraints,
|
||||||
this.hintTextConstraints,
|
this.hintTextConstraints,
|
||||||
|
this.hintStyle,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -153,7 +155,8 @@ class FlowyTextFieldState extends State<FlowyTextField> {
|
|||||||
.textTheme
|
.textTheme
|
||||||
.bodySmall!
|
.bodySmall!
|
||||||
.copyWith(color: Theme.of(context).colorScheme.error),
|
.copyWith(color: Theme.of(context).colorScheme.error),
|
||||||
hintStyle: Theme.of(context)
|
hintStyle: widget.hintStyle ??
|
||||||
|
Theme.of(context)
|
||||||
.textTheme
|
.textTheme
|
||||||
.bodySmall!
|
.bodySmall!
|
||||||
.copyWith(color: Theme.of(context).hintColor),
|
.copyWith(color: Theme.of(context).hintColor),
|
||||||
|
@ -0,0 +1,4 @@
|
|||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="20" height="20" rx="10" fill="#42ABF6"/>
|
||||||
|
<path d="M6.25 10.3125L9.27885 13.125L14.6875 7.5" stroke="white" stroke-width="1.6875" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 294 B |
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="0.5" y="0.5" width="19" height="19" rx="9.5" stroke="#1F2329" stroke-opacity="0.15"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 198 B |
Reference in New Issue
Block a user