chore: organize code (#5004)

* chore: rename files and classes

* chore: move some files around
This commit is contained in:
Richard Shiue
2024-03-29 11:54:42 +08:00
committed by GitHub
parent 97575d4f6a
commit 490cb48354
28 changed files with 28 additions and 29 deletions

View File

@ -2,7 +2,7 @@ import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_bloc.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_editor.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';

View File

@ -2,7 +2,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_bloc.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_editor.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';

View File

@ -15,7 +15,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import '../../application/cell/bloc/select_option_editor_bloc.dart';
import '../../grid/presentation/layout/sizes.dart';
import '../../grid/presentation/widgets/common/type_option_separator.dart';
import '../../grid/presentation/widgets/header/type_option/select/select_option_editor.dart';
import '../field/type_option_editor/select/select_option_editor.dart';
import 'extension.dart';
import 'select_option_text_field.dart';
@ -332,7 +332,7 @@ class _SelectOptionCellState extends State<_SelectOptionCell> {
),
),
popupBuilder: (BuildContext popoverContext) {
return SelectOptionTypeOptionEditor(
return SelectOptionEditor(
option: widget.option,
onDeleted: () {
context

View File

@ -0,0 +1,601 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
import 'package:appflowy/plugins/database/application/field/field_editor_bloc.dart';
import 'package:appflowy/plugins/database/application/field/field_info.dart';
import 'package:appflowy/plugins/database/domain/field_service.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_option_separator.dart';
import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:styled_widget/styled_widget.dart';
import 'field_type_list.dart';
import 'type_option_editor/builder.dart';
enum FieldEditorPage {
general,
details,
}
class FieldEditor extends StatefulWidget {
const FieldEditor({
super.key,
required this.viewId,
required this.field,
required this.fieldController,
this.initialPage = FieldEditorPage.details,
this.onFieldInserted,
});
final String viewId;
final FieldPB field;
final FieldController fieldController;
final FieldEditorPage initialPage;
final void Function(String fieldId)? onFieldInserted;
@override
State<StatefulWidget> createState() => _FieldEditorState();
}
class _FieldEditorState extends State<FieldEditor> {
late FieldEditorPage _currentPage;
late final TextEditingController textController;
@override
void initState() {
super.initState();
_currentPage = widget.initialPage;
textController = TextEditingController(text: widget.field.name);
}
@override
void dispose() {
textController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => FieldEditorBloc(
viewId: widget.viewId,
field: widget.field,
fieldController: widget.fieldController,
onFieldInserted: widget.onFieldInserted,
)..add(const FieldEditorEvent.initial()),
child: _currentPage == FieldEditorPage.details
? _fieldDetails()
: _fieldGeneral(),
);
}
Widget _fieldGeneral() {
return SizedBox(
width: 240,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
FieldNameTextField(
padding: const EdgeInsets.fromLTRB(4, 4, 4, 8),
textEditingController: textController,
),
_EditFieldButton(
onTap: () {
setState(() => _currentPage = FieldEditorPage.details);
},
),
VSpace(GridSize.typeOptionSeparatorHeight),
_actionCell(FieldAction.insertLeft),
VSpace(GridSize.typeOptionSeparatorHeight),
_actionCell(FieldAction.insertRight),
VSpace(GridSize.typeOptionSeparatorHeight),
_actionCell(FieldAction.toggleVisibility),
VSpace(GridSize.typeOptionSeparatorHeight),
_actionCell(FieldAction.duplicate),
VSpace(GridSize.typeOptionSeparatorHeight),
_actionCell(FieldAction.clearData),
VSpace(GridSize.typeOptionSeparatorHeight),
_actionCell(FieldAction.delete),
],
).padding(all: 8.0),
);
}
Widget _fieldDetails() {
return SizedBox(
width: 260,
child: FieldDetailsEditor(
viewId: widget.viewId,
textEditingController: textController,
),
);
}
Widget _actionCell(FieldAction action) {
return BlocBuilder<FieldEditorBloc, FieldEditorState>(
builder: (context, state) => FieldActionCell(
viewId: widget.viewId,
fieldInfo: state.field,
action: action,
),
);
}
}
class _EditFieldButton extends StatelessWidget {
const _EditFieldButton({this.onTap});
final void Function()? onTap;
@override
Widget build(BuildContext context) {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
leftIcon: const FlowySvg(FlowySvgs.edit_s),
text: FlowyText.medium(
LocaleKeys.grid_field_editProperty.tr(),
),
onTap: onTap,
),
);
}
}
class FieldActionCell extends StatelessWidget {
const FieldActionCell({
super.key,
required this.viewId,
required this.fieldInfo,
required this.action,
this.popoverMutex,
});
final String viewId;
final FieldInfo fieldInfo;
final FieldAction action;
final PopoverMutex? popoverMutex;
@override
Widget build(BuildContext context) {
bool enable = true;
// If the field is primary, delete and duplicate are disabled.
if (fieldInfo.isPrimary &&
(action == FieldAction.duplicate || action == FieldAction.delete)) {
enable = false;
}
return FlowyButton(
disable: !enable,
text: FlowyText.medium(
action.title(fieldInfo),
color: enable ? null : Theme.of(context).disabledColor,
),
onHover: (_) => popoverMutex?.close(),
onTap: () => action.run(context, viewId, fieldInfo),
leftIcon: action.icon(
fieldInfo,
enable ? null : Theme.of(context).disabledColor,
),
);
}
}
enum FieldAction {
insertLeft,
insertRight,
toggleVisibility,
duplicate,
clearData,
delete;
Widget icon(FieldInfo fieldInfo, Color? color) {
late final FlowySvgData svgData;
switch (this) {
case FieldAction.insertLeft:
svgData = FlowySvgs.arrow_s;
case FieldAction.insertRight:
svgData = FlowySvgs.arrow_s;
case FieldAction.toggleVisibility:
if (fieldInfo.visibility != null &&
fieldInfo.visibility == FieldVisibility.AlwaysHidden) {
svgData = FlowySvgs.show_m;
} else {
svgData = FlowySvgs.hide_s;
}
case FieldAction.duplicate:
svgData = FlowySvgs.copy_s;
case FieldAction.clearData:
svgData = FlowySvgs.reload_s;
case FieldAction.delete:
svgData = FlowySvgs.delete_s;
}
final icon = FlowySvg(
svgData,
size: const Size.square(16),
color: color,
);
return this == FieldAction.insertRight
? Transform.flip(flipX: true, child: icon)
: icon;
}
String title(FieldInfo fieldInfo) {
switch (this) {
case FieldAction.insertLeft:
return LocaleKeys.grid_field_insertLeft.tr();
case FieldAction.insertRight:
return LocaleKeys.grid_field_insertRight.tr();
case FieldAction.toggleVisibility:
if (fieldInfo.visibility != null &&
fieldInfo.visibility == FieldVisibility.AlwaysHidden) {
return LocaleKeys.grid_field_show.tr();
} else {
return LocaleKeys.grid_field_hide.tr();
}
case FieldAction.duplicate:
return LocaleKeys.grid_field_duplicate.tr();
case FieldAction.clearData:
return LocaleKeys.grid_field_clear.tr();
case FieldAction.delete:
return LocaleKeys.grid_field_delete.tr();
}
}
void run(BuildContext context, String viewId, FieldInfo fieldInfo) {
switch (this) {
case FieldAction.insertLeft:
PopoverContainer.of(context).close();
context
.read<FieldEditorBloc>()
.add(const FieldEditorEvent.insertLeft());
break;
case FieldAction.insertRight:
PopoverContainer.of(context).close();
context
.read<FieldEditorBloc>()
.add(const FieldEditorEvent.insertRight());
break;
case FieldAction.toggleVisibility:
PopoverContainer.of(context).close();
context
.read<FieldEditorBloc>()
.add(const FieldEditorEvent.toggleFieldVisibility());
break;
case FieldAction.duplicate:
PopoverContainer.of(context).close();
FieldBackendService.duplicateField(
viewId: viewId,
fieldId: fieldInfo.id,
);
break;
case FieldAction.clearData:
NavigatorAlertDialog(
constraints: const BoxConstraints(
maxWidth: 250,
maxHeight: 260,
),
title: LocaleKeys.grid_field_clearFieldPromptMessage.tr(),
confirm: () {
FieldBackendService.clearField(
viewId: viewId,
fieldId: fieldInfo.id,
);
},
).show(context);
PopoverContainer.of(context).close();
break;
case FieldAction.delete:
NavigatorAlertDialog(
title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
confirm: () {
FieldBackendService.deleteField(
viewId: viewId,
fieldId: fieldInfo.id,
);
},
).show(context);
PopoverContainer.of(context).close();
break;
}
}
}
class FieldDetailsEditor extends StatefulWidget {
const FieldDetailsEditor({
super.key,
required this.viewId,
required this.textEditingController,
this.onAction,
});
final String viewId;
final TextEditingController textEditingController;
final Function()? onAction;
@override
State<StatefulWidget> createState() => _FieldDetailsEditorState();
}
class _FieldDetailsEditorState extends State<FieldDetailsEditor> {
late PopoverMutex popoverMutex;
@override
void initState() {
popoverMutex = PopoverMutex();
super.initState();
}
@override
void dispose() {
popoverMutex.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final List<Widget> children = [
FieldNameTextField(
popoverMutex: popoverMutex,
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0),
textEditingController: widget.textEditingController,
),
const VSpace(8.0),
SwitchFieldButton(popoverMutex: popoverMutex),
const TypeOptionSeparator(spacing: 8.0),
Flexible(
child: FieldTypeOptionEditor(
viewId: widget.viewId,
popoverMutex: popoverMutex,
),
),
_addFieldVisibilityToggleButton(),
_addDuplicateFieldButton(),
_addDeleteFieldButton(),
];
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: children,
),
);
}
Widget _addFieldVisibilityToggleButton() {
return BlocBuilder<FieldEditorBloc, FieldEditorState>(
builder: (context, state) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: FieldActionCell(
viewId: widget.viewId,
fieldInfo: state.field,
action: FieldAction.toggleVisibility,
popoverMutex: popoverMutex,
),
);
},
);
}
Widget _addDeleteFieldButton() {
return BlocBuilder<FieldEditorBloc, FieldEditorState>(
builder: (context, state) {
return Padding(
padding: const EdgeInsets.fromLTRB(8.0, 4.0, 8.0, 0),
child: FieldActionCell(
viewId: widget.viewId,
fieldInfo: state.field,
action: FieldAction.delete,
popoverMutex: popoverMutex,
),
);
},
);
}
Widget _addDuplicateFieldButton() {
return BlocBuilder<FieldEditorBloc, FieldEditorState>(
builder: (context, state) {
return Padding(
padding: const EdgeInsets.fromLTRB(8.0, 4.0, 8.0, 0),
child: FieldActionCell(
viewId: widget.viewId,
fieldInfo: state.field,
action: FieldAction.duplicate,
popoverMutex: popoverMutex,
),
);
},
);
}
}
class FieldTypeOptionEditor extends StatelessWidget {
const FieldTypeOptionEditor({
super.key,
required this.viewId,
required this.popoverMutex,
});
final String viewId;
final PopoverMutex popoverMutex;
@override
Widget build(BuildContext context) {
return BlocBuilder<FieldEditorBloc, FieldEditorState>(
builder: (context, state) {
if (state.field.isPrimary) {
return const SizedBox.shrink();
}
final typeOptionEditor = makeTypeOptionEditor(
context: context,
viewId: viewId,
field: state.field.field,
popoverMutex: popoverMutex,
onTypeOptionUpdated: (Uint8List typeOptionData) {
context
.read<FieldEditorBloc>()
.add(FieldEditorEvent.updateTypeOption(typeOptionData));
},
);
if (typeOptionEditor == null) {
return const SizedBox.shrink();
}
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(child: typeOptionEditor),
const TypeOptionSeparator(spacing: 8.0),
],
);
},
);
}
}
class FieldNameTextField extends StatefulWidget {
const FieldNameTextField({
super.key,
required this.textEditingController,
this.popoverMutex,
this.padding = EdgeInsets.zero,
});
final TextEditingController textEditingController;
final PopoverMutex? popoverMutex;
final EdgeInsets padding;
@override
State<FieldNameTextField> createState() => _FieldNameTextFieldState();
}
class _FieldNameTextFieldState extends State<FieldNameTextField> {
FocusNode focusNode = FocusNode();
@override
void initState() {
super.initState();
focusNode.addListener(() {
if (focusNode.hasFocus) {
widget.popoverMutex?.close();
}
});
widget.popoverMutex?.listenOnPopoverChanged(() {
if (focusNode.hasFocus) {
focusNode.unfocus();
}
});
}
@override
Widget build(BuildContext context) {
return Padding(
padding: widget.padding,
child: FlowyTextField(
focusNode: focusNode,
controller: widget.textEditingController,
onSubmitted: (_) => PopoverContainer.of(context).close(),
onChanged: (newName) {
context
.read<FieldEditorBloc>()
.add(FieldEditorEvent.renameField(newName));
},
),
);
}
@override
void dispose() {
focusNode.removeListener(() {
if (focusNode.hasFocus) {
widget.popoverMutex?.close();
}
});
focusNode.dispose();
super.dispose();
}
}
class SwitchFieldButton extends StatefulWidget {
const SwitchFieldButton({
super.key,
required this.popoverMutex,
});
final PopoverMutex popoverMutex;
@override
State<SwitchFieldButton> createState() => _SwitchFieldButtonState();
}
class _SwitchFieldButtonState extends State<SwitchFieldButton> {
final PopoverController _popoverController = PopoverController();
@override
Widget build(BuildContext context) {
return BlocBuilder<FieldEditorBloc, FieldEditorState>(
builder: (context, state) {
final bool isPrimary = state.field.isPrimary;
return SizedBox(
height: GridSize.popoverItemHeight,
child: AppFlowyPopover(
constraints: BoxConstraints.loose(const Size(460, 540)),
triggerActions: isPrimary ? 0 : PopoverTriggerFlags.hover,
mutex: widget.popoverMutex,
controller: _popoverController,
offset: const Offset(8, 0),
margin: const EdgeInsets.all(8),
popupBuilder: (BuildContext popoverContext) {
return FieldTypeList(
onSelectField: (newFieldType) {
context
.read<FieldEditorBloc>()
.add(FieldEditorEvent.switchFieldType(newFieldType));
},
);
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: FlowyButton(
onTap: () {
if (!isPrimary) {
_popoverController.show();
}
},
text: FlowyText.medium(
state.field.fieldType.i18n,
color: isPrimary ? Theme.of(context).disabledColor : null,
),
leftIcon: FlowySvg(
state.field.fieldType.svgData,
color: isPrimary ? Theme.of(context).disabledColor : null,
),
rightIcon: FlowySvg(
FlowySvgs.more_s,
color: isPrimary ? Theme.of(context).disabledColor : null,
),
),
),
),
);
},
);
}
}

View File

@ -0,0 +1,84 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
typedef SelectFieldCallback = void Function(FieldType);
const List<FieldType> _supportedFieldTypes = [
FieldType.RichText,
FieldType.Number,
FieldType.SingleSelect,
FieldType.MultiSelect,
FieldType.DateTime,
FieldType.Checkbox,
FieldType.Checklist,
FieldType.URL,
FieldType.LastEditedTime,
FieldType.CreatedTime,
FieldType.Relation,
];
class FieldTypeList extends StatelessWidget with FlowyOverlayDelegate {
const FieldTypeList({required this.onSelectField, super.key});
final SelectFieldCallback onSelectField;
@override
Widget build(BuildContext context) {
final cells = _supportedFieldTypes.map((fieldType) {
return FieldTypeCell(
fieldType: fieldType,
onSelectField: (fieldType) {
onSelectField(fieldType);
PopoverContainer.of(context).closeAll();
},
);
}).toList();
return SizedBox(
width: 140,
child: ListView.separated(
shrinkWrap: true,
itemCount: cells.length,
separatorBuilder: (context, index) {
return VSpace(GridSize.typeOptionSeparatorHeight);
},
physics: StyledScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
return cells[index];
},
),
);
}
}
class FieldTypeCell extends StatelessWidget {
const FieldTypeCell({
super.key,
required this.fieldType,
required this.onSelectField,
});
final FieldType fieldType;
final SelectFieldCallback onSelectField;
@override
Widget build(BuildContext context) {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(
fieldType.i18n,
),
onTap: () => onSelectField(fieldType),
leftIcon: FlowySvg(
fieldType.svgData,
),
),
);
}
}

View File

@ -0,0 +1,62 @@
import 'dart:typed_data';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flutter/material.dart';
import 'checkbox.dart';
import 'checklist.dart';
import 'date.dart';
import 'multi_select.dart';
import 'number.dart';
import 'relation.dart';
import 'rich_text.dart';
import 'single_select.dart';
import 'timestamp.dart';
import 'url.dart';
typedef TypeOptionDataCallback = void Function(Uint8List typeOptionData);
abstract class TypeOptionEditorFactory {
factory TypeOptionEditorFactory.makeBuilder(FieldType fieldType) {
return switch (fieldType) {
FieldType.RichText => const RichTextTypeOptionEditorFactory(),
FieldType.Number => const NumberTypeOptionEditorFactory(),
FieldType.URL => const URLTypeOptionEditorFactory(),
FieldType.DateTime => const DateTypeOptionEditorFactory(),
FieldType.LastEditedTime => const TimestampTypeOptionEditorFactory(),
FieldType.CreatedTime => const TimestampTypeOptionEditorFactory(),
FieldType.SingleSelect => const SingleSelectTypeOptionEditorFactory(),
FieldType.MultiSelect => const MultiSelectTypeOptionEditorFactory(),
FieldType.Checkbox => const CheckboxTypeOptionEditorFactory(),
FieldType.Checklist => const ChecklistTypeOptionEditorFactory(),
FieldType.Relation => const RelationTypeOptionEditorFactory(),
_ => throw UnimplementedError(),
};
}
Widget? build({
required BuildContext context,
required String viewId,
required FieldPB field,
required PopoverMutex popoverMutex,
required TypeOptionDataCallback onTypeOptionUpdated,
});
}
Widget? makeTypeOptionEditor({
required BuildContext context,
required String viewId,
required FieldPB field,
required PopoverMutex popoverMutex,
required TypeOptionDataCallback onTypeOptionUpdated,
}) {
final editorBuilder = TypeOptionEditorFactory.makeBuilder(field.fieldType);
return editorBuilder.build(
context: context,
viewId: viewId,
field: field,
onTypeOptionUpdated: onTypeOptionUpdated,
popoverMutex: popoverMutex,
);
}

View File

@ -0,0 +1,19 @@
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flutter/material.dart';
import 'builder.dart';
class CheckboxTypeOptionEditorFactory implements TypeOptionEditorFactory {
const CheckboxTypeOptionEditorFactory();
@override
Widget? build({
required BuildContext context,
required String viewId,
required FieldPB field,
required PopoverMutex popoverMutex,
required TypeOptionDataCallback onTypeOptionUpdated,
}) =>
null;
}

View File

@ -0,0 +1,19 @@
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flutter/material.dart';
import 'builder.dart';
class ChecklistTypeOptionEditorFactory implements TypeOptionEditorFactory {
const ChecklistTypeOptionEditorFactory();
@override
Widget? build({
required BuildContext context,
required String viewId,
required FieldPB field,
required PopoverMutex popoverMutex,
required TypeOptionDataCallback onTypeOptionUpdated,
}) =>
null;
}

View File

@ -0,0 +1,121 @@
import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:protobuf/protobuf.dart';
import '../../../grid/presentation/layout/sizes.dart';
import 'builder.dart';
import 'date/date_time_format.dart';
class DateTypeOptionEditorFactory implements TypeOptionEditorFactory {
const DateTypeOptionEditorFactory();
@override
Widget? build({
required BuildContext context,
required String viewId,
required FieldPB field,
required PopoverMutex popoverMutex,
required TypeOptionDataCallback onTypeOptionUpdated,
}) {
final typeOption = _parseTypeOptionData(field.typeOptionData);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
_renderDateFormatButton(
typeOption,
popoverMutex,
onTypeOptionUpdated,
),
VSpace(GridSize.typeOptionSeparatorHeight),
_renderTimeFormatButton(
typeOption,
popoverMutex,
onTypeOptionUpdated,
),
],
);
}
Widget _renderDateFormatButton(
DateTypeOptionPB typeOption,
PopoverMutex popoverMutex,
TypeOptionDataCallback onTypeOptionUpdated,
) {
return AppFlowyPopover(
mutex: popoverMutex,
asBarrier: true,
triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
offset: const Offset(8, 0),
constraints: BoxConstraints.loose(const Size(460, 440)),
popupBuilder: (popoverContext) {
return DateFormatList(
selectedFormat: typeOption.dateFormat,
onSelected: (format) {
final newTypeOption =
_updateTypeOption(typeOption: typeOption, dateFormat: format);
onTypeOptionUpdated(newTypeOption.writeToBuffer());
PopoverContainer.of(popoverContext).close();
},
);
},
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 12.0),
child: DateFormatButton(),
),
);
}
Widget _renderTimeFormatButton(
DateTypeOptionPB typeOption,
PopoverMutex popoverMutex,
TypeOptionDataCallback onTypeOptionUpdated,
) {
return AppFlowyPopover(
mutex: popoverMutex,
asBarrier: true,
triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
offset: const Offset(8, 0),
constraints: BoxConstraints.loose(const Size(460, 440)),
popupBuilder: (BuildContext popoverContext) {
return TimeFormatList(
selectedFormat: typeOption.timeFormat,
onSelected: (format) {
final newTypeOption =
_updateTypeOption(typeOption: typeOption, timeFormat: format);
onTypeOptionUpdated(newTypeOption.writeToBuffer());
PopoverContainer.of(popoverContext).close();
},
);
},
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 12.0),
child: TimeFormatButton(),
),
);
}
DateTypeOptionPB _parseTypeOptionData(List<int> data) {
return DateTypeOptionDataParser().fromBuffer(data);
}
DateTypeOptionPB _updateTypeOption({
required DateTypeOptionPB typeOption,
DateFormatPB? dateFormat,
TimeFormatPB? timeFormat,
}) {
typeOption.freeze();
return typeOption.rebuild((typeOption) {
if (dateFormat != null) {
typeOption.dateFormat = dateFormat;
}
if (timeFormat != null) {
typeOption.timeFormat = timeFormat;
}
});
}
}

View File

@ -0,0 +1,259 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.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';
class DateFormatButton extends StatelessWidget {
const DateFormatButton({
super.key,
this.onTap,
this.onHover,
});
final VoidCallback? onTap;
final void Function(bool)? onHover;
@override
Widget build(BuildContext context) {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(LocaleKeys.grid_field_dateFormat.tr()),
onTap: onTap,
onHover: onHover,
rightIcon: const FlowySvg(FlowySvgs.more_s),
),
);
}
}
class TimeFormatButton extends StatelessWidget {
const TimeFormatButton({
super.key,
this.onTap,
this.onHover,
});
final VoidCallback? onTap;
final void Function(bool)? onHover;
@override
Widget build(BuildContext context) {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(LocaleKeys.grid_field_timeFormat.tr()),
onTap: onTap,
onHover: onHover,
rightIcon: const FlowySvg(FlowySvgs.more_s),
),
);
}
}
class DateFormatList extends StatelessWidget {
const DateFormatList({
super.key,
required this.selectedFormat,
required this.onSelected,
});
final DateFormatPB selectedFormat;
final Function(DateFormatPB format) onSelected;
@override
Widget build(BuildContext context) {
final cells = DateFormatPB.values.map((format) {
return DateFormatCell(
dateFormat: format,
onSelected: onSelected,
isSelected: selectedFormat == format,
);
}).toList();
return SizedBox(
width: 180,
child: ListView.separated(
shrinkWrap: true,
separatorBuilder: (context, index) {
return VSpace(GridSize.typeOptionSeparatorHeight);
},
itemCount: cells.length,
itemBuilder: (BuildContext context, int index) {
return cells[index];
},
),
);
}
}
class DateFormatCell extends StatelessWidget {
const DateFormatCell({
super.key,
required this.dateFormat,
required this.onSelected,
required this.isSelected,
});
final DateFormatPB dateFormat;
final Function(DateFormatPB format) onSelected;
final bool isSelected;
@override
Widget build(BuildContext context) {
Widget? checkmark;
if (isSelected) {
checkmark = const FlowySvg(FlowySvgs.check_s);
}
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(dateFormat.title()),
rightIcon: checkmark,
onTap: () => onSelected(dateFormat),
),
);
}
}
extension DateFormatExtension on DateFormatPB {
String title() {
switch (this) {
case DateFormatPB.Friendly:
return LocaleKeys.grid_field_dateFormatFriendly.tr();
case DateFormatPB.ISO:
return LocaleKeys.grid_field_dateFormatISO.tr();
case DateFormatPB.Local:
return LocaleKeys.grid_field_dateFormatLocal.tr();
case DateFormatPB.US:
return LocaleKeys.grid_field_dateFormatUS.tr();
case DateFormatPB.DayMonthYear:
return LocaleKeys.grid_field_dateFormatDayMonthYear.tr();
default:
throw UnimplementedError;
}
}
}
class TimeFormatList extends StatelessWidget {
const TimeFormatList({
super.key,
required this.selectedFormat,
required this.onSelected,
});
final TimeFormatPB selectedFormat;
final Function(TimeFormatPB format) onSelected;
@override
Widget build(BuildContext context) {
final cells = TimeFormatPB.values.map((format) {
return TimeFormatCell(
isSelected: format == selectedFormat,
timeFormat: format,
onSelected: onSelected,
);
}).toList();
return SizedBox(
width: 120,
child: ListView.separated(
shrinkWrap: true,
separatorBuilder: (context, index) {
return VSpace(GridSize.typeOptionSeparatorHeight);
},
itemCount: cells.length,
itemBuilder: (BuildContext context, int index) {
return cells[index];
},
),
);
}
}
class TimeFormatCell extends StatelessWidget {
const TimeFormatCell({
super.key,
required this.timeFormat,
required this.onSelected,
required this.isSelected,
});
final TimeFormatPB timeFormat;
final bool isSelected;
final Function(TimeFormatPB format) onSelected;
@override
Widget build(BuildContext context) {
Widget? checkmark;
if (isSelected) {
checkmark = const FlowySvg(FlowySvgs.check_s);
}
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(timeFormat.title()),
rightIcon: checkmark,
onTap: () => onSelected(timeFormat),
),
);
}
}
extension TimeFormatExtension on TimeFormatPB {
String title() {
switch (this) {
case TimeFormatPB.TwelveHour:
return LocaleKeys.grid_field_timeFormatTwelveHour.tr();
case TimeFormatPB.TwentyFourHour:
return LocaleKeys.grid_field_timeFormatTwentyFourHour.tr();
default:
throw UnimplementedError;
}
}
}
class IncludeTimeButton extends StatelessWidget {
const IncludeTimeButton({
super.key,
required this.onChanged,
required this.value,
});
final Function(bool value) onChanged;
final bool value;
@override
Widget build(BuildContext context) {
return SizedBox(
height: GridSize.popoverItemHeight,
child: Padding(
padding: GridSize.typeOptionContentInsets,
child: Row(
children: [
FlowySvg(
FlowySvgs.clock_alarm_s,
color: Theme.of(context).iconTheme.color,
),
const HSpace(6),
FlowyText.medium(LocaleKeys.grid_field_includeTime.tr()),
const Spacer(),
Toggle(
value: value,
onChanged: onChanged,
style: ToggleStyle.big,
padding: EdgeInsets.zero,
),
],
),
),
);
}
}

View File

@ -0,0 +1,38 @@
import 'package:appflowy/plugins/database/application/field/type_option/select_type_option_actions.dart';
import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter/material.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'builder.dart';
import 'select/select_option.dart';
class MultiSelectTypeOptionEditorFactory implements TypeOptionEditorFactory {
const MultiSelectTypeOptionEditorFactory();
@override
Widget? build({
required BuildContext context,
required String viewId,
required FieldPB field,
required PopoverMutex popoverMutex,
required TypeOptionDataCallback onTypeOptionUpdated,
}) {
final typeOption = _parseTypeOptionData(field.typeOptionData);
return SelectOptionTypeOptionWidget(
options: typeOption.options,
beginEdit: () => PopoverContainer.of(context).closeAll(),
popoverMutex: popoverMutex,
typeOptionAction: MultiSelectAction(
viewId: viewId,
fieldId: field.id,
onTypeOptionUpdated: onTypeOptionUpdated,
),
);
}
MultiSelectTypeOptionPB _parseTypeOptionData(List<int> data) {
return MultiSelectTypeOptionDataParser().fromBuffer(data);
}
}

View File

@ -0,0 +1,191 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/field/type_option/number_format_bloc.dart';
import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:protobuf/protobuf.dart';
import '../../../grid/presentation/layout/sizes.dart';
import '../../../grid/presentation/widgets/common/type_option_separator.dart';
import 'builder.dart';
class NumberTypeOptionEditorFactory implements TypeOptionEditorFactory {
const NumberTypeOptionEditorFactory();
@override
Widget? build({
required BuildContext context,
required String viewId,
required FieldPB field,
required PopoverMutex popoverMutex,
required TypeOptionDataCallback onTypeOptionUpdated,
}) {
final typeOption = _parseTypeOptionData(field.typeOptionData);
final selectNumUnitButton = SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
rightIcon: const FlowySvg(FlowySvgs.more_s),
text: FlowyText.medium(
typeOption.format.title(),
),
),
);
final numFormatTitle = Container(
padding: const EdgeInsets.only(left: 6),
height: GridSize.popoverItemHeight,
alignment: Alignment.centerLeft,
child: FlowyText.regular(
LocaleKeys.grid_field_numberFormat.tr(),
color: Theme.of(context).hintColor,
fontSize: 11,
),
);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
numFormatTitle,
AppFlowyPopover(
mutex: popoverMutex,
triggerActions:
PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
offset: const Offset(16, 0),
constraints: BoxConstraints.loose(const Size(460, 440)),
margin: EdgeInsets.zero,
child: selectNumUnitButton,
popupBuilder: (BuildContext popoverContext) {
return NumberFormatList(
selectedFormat: typeOption.format,
onSelected: (format) {
final newTypeOption = _updateNumberFormat(typeOption, format);
onTypeOptionUpdated(newTypeOption.writeToBuffer());
PopoverContainer.of(popoverContext).close();
},
);
},
),
],
),
);
}
NumberTypeOptionPB _parseTypeOptionData(List<int> data) {
return NumberTypeOptionDataParser().fromBuffer(data);
}
NumberTypeOptionPB _updateNumberFormat(
NumberTypeOptionPB typeOption,
NumberFormatPB format,
) {
typeOption.freeze();
return typeOption.rebuild((typeOption) => typeOption.format = format);
}
}
typedef SelectNumberFormatCallback = void Function(NumberFormatPB format);
class NumberFormatList extends StatelessWidget {
const NumberFormatList({
super.key,
required this.selectedFormat,
required this.onSelected,
});
final NumberFormatPB selectedFormat;
final SelectNumberFormatCallback onSelected;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => NumberFormatBloc(),
child: SizedBox(
width: 180,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const _FilterTextField(),
const TypeOptionSeparator(spacing: 0.0),
BlocBuilder<NumberFormatBloc, NumberFormatState>(
builder: (context, state) {
final cells = state.formats.map((format) {
return NumberFormatCell(
isSelected: format == selectedFormat,
format: format,
onSelected: (format) {
onSelected(format);
},
);
}).toList();
final list = ListView.separated(
shrinkWrap: true,
separatorBuilder: (context, index) {
return VSpace(GridSize.typeOptionSeparatorHeight);
},
itemCount: cells.length,
itemBuilder: (BuildContext context, int index) {
return cells[index];
},
padding: const EdgeInsets.all(6.0),
);
return Flexible(child: list);
},
),
],
),
),
);
}
}
class NumberFormatCell extends StatelessWidget {
const NumberFormatCell({
super.key,
required this.format,
required this.isSelected,
required this.onSelected,
});
final NumberFormatPB format;
final bool isSelected;
final SelectNumberFormatCallback onSelected;
@override
Widget build(BuildContext context) {
final checkmark = isSelected ? const FlowySvg(FlowySvgs.check_s) : null;
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(format.title()),
onTap: () => onSelected(format),
rightIcon: checkmark,
),
);
}
}
class _FilterTextField extends StatelessWidget {
const _FilterTextField();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(6.0),
child: FlowyTextField(
onChanged: (text) => context
.read<NumberFormatBloc>()
.add(NumberFormatEvent.setFilter(text)),
),
);
}
}

View File

@ -0,0 +1,160 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/field/type_option/relation_type_option_cubit.dart';
import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:protobuf/protobuf.dart';
import 'builder.dart';
class RelationTypeOptionEditorFactory implements TypeOptionEditorFactory {
const RelationTypeOptionEditorFactory();
@override
Widget? build({
required BuildContext context,
required String viewId,
required FieldPB field,
required PopoverMutex popoverMutex,
required TypeOptionDataCallback onTypeOptionUpdated,
}) {
final typeOption = _parseTypeOptionData(field.typeOptionData);
return BlocProvider(
create: (_) => RelationDatabaseListCubit(),
child: Builder(
builder: (context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.only(left: 14, right: 8),
height: GridSize.popoverItemHeight,
alignment: Alignment.centerLeft,
child: FlowyText.regular(
LocaleKeys.grid_relation_relatedDatabasePlaceLabel.tr(),
color: Theme.of(context).hintColor,
fontSize: 11,
),
),
AppFlowyPopover(
mutex: popoverMutex,
triggerActions:
PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
offset: const Offset(6, 0),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8),
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: BlocBuilder<RelationDatabaseListCubit,
RelationDatabaseListState>(
builder: (context, state) {
final databaseMeta =
state.databaseMetas.firstWhereOrNull(
(meta) => meta.databaseId == typeOption.databaseId,
);
return FlowyText(
databaseMeta == null
? LocaleKeys
.grid_relation_relatedDatabasePlaceholder
.tr()
: databaseMeta.databaseName,
color: databaseMeta == null
? Theme.of(context).hintColor
: null,
overflow: TextOverflow.ellipsis,
);
},
),
rightIcon: const FlowySvg(FlowySvgs.more_s),
),
),
popupBuilder: (popoverContext) {
return BlocProvider.value(
value: context.read<RelationDatabaseListCubit>(),
child: _DatabaseList(
onSelectDatabase: (newDatabaseId) {
final newTypeOption = _updateTypeOption(
typeOption: typeOption,
databaseId: newDatabaseId,
);
onTypeOptionUpdated(newTypeOption.writeToBuffer());
PopoverContainer.of(context).close();
},
currentDatabaseId: typeOption.databaseId,
),
);
},
),
],
);
},
),
);
}
RelationTypeOptionPB _parseTypeOptionData(List<int> data) {
return RelationTypeOptionDataParser().fromBuffer(data);
}
RelationTypeOptionPB _updateTypeOption({
required RelationTypeOptionPB typeOption,
required String databaseId,
}) {
typeOption.freeze();
return typeOption.rebuild((typeOption) {
typeOption.databaseId = databaseId;
});
}
}
class _DatabaseList extends StatelessWidget {
const _DatabaseList({
required this.onSelectDatabase,
required this.currentDatabaseId,
});
final String currentDatabaseId;
final void Function(String databaseId) onSelectDatabase;
@override
Widget build(BuildContext context) {
return BlocBuilder<RelationDatabaseListCubit, RelationDatabaseListState>(
builder: (context, state) {
final children = state.databaseMetas.map((meta) {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
onTap: () => onSelectDatabase(meta.databaseId),
text: FlowyText.medium(
meta.databaseName,
overflow: TextOverflow.ellipsis,
),
rightIcon: meta.databaseId == currentDatabaseId
? const FlowySvg(
FlowySvgs.check_s,
)
: null,
),
);
}).toList();
return ListView.separated(
shrinkWrap: true,
padding: EdgeInsets.zero,
separatorBuilder: (_, __) =>
VSpace(GridSize.typeOptionSeparatorHeight),
itemCount: children.length,
itemBuilder: (context, index) => children[index],
);
},
);
}
}

View File

@ -0,0 +1,18 @@
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flutter/material.dart';
import 'builder.dart';
class RichTextTypeOptionEditorFactory implements TypeOptionEditorFactory {
const RichTextTypeOptionEditorFactory();
@override
Widget? build({
required BuildContext context,
required String viewId,
required FieldPB field,
required PopoverMutex popoverMutex,
required TypeOptionDataCallback onTypeOptionUpdated,
}) =>
null;
}

View File

@ -0,0 +1,255 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/field/type_option/select_option_type_option_bloc.dart';
import 'package:appflowy/plugins/database/application/field/type_option/select_type_option_actions.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
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:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'select_option_editor.dart';
class SelectOptionTypeOptionWidget extends StatelessWidget {
const SelectOptionTypeOptionWidget({
super.key,
required this.options,
required this.beginEdit,
required this.typeOptionAction,
this.popoverMutex,
});
final List<SelectOptionPB> options;
final VoidCallback beginEdit;
final ISelectOptionAction typeOptionAction;
final PopoverMutex? popoverMutex;
@override
Widget build(BuildContext context) {
return BlocProvider<SelectOptionTypeOptionBloc>(
create: (context) => SelectOptionTypeOptionBloc(
options: options,
typeOptionAction: typeOptionAction,
),
child:
BlocBuilder<SelectOptionTypeOptionBloc, SelectOptionTypeOptionState>(
builder: (context, state) {
final List<Widget> children = [
const _OptionTitle(),
const VSpace(4),
if (state.isEditingOption) ...[
CreateOptionTextField(popoverMutex: popoverMutex),
const VSpace(4),
] else
const _AddOptionButton(),
const VSpace(4),
...state.options.map((option) {
return _OptionCell(
option: option,
popoverMutex: popoverMutex,
);
}),
];
return ListView(
shrinkWrap: true,
children: children,
);
},
),
);
}
}
class _OptionTitle extends StatelessWidget {
const _OptionTitle();
@override
Widget build(BuildContext context) {
return BlocBuilder<SelectOptionTypeOptionBloc, SelectOptionTypeOptionState>(
builder: (context, state) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Align(
alignment: AlignmentDirectional.centerStart,
child: FlowyText.regular(
LocaleKeys.grid_field_optionTitle.tr(),
fontSize: 11,
color: Theme.of(context).hintColor,
),
),
);
},
);
}
}
class _OptionCell extends StatefulWidget {
const _OptionCell({required this.option, this.popoverMutex});
final SelectOptionPB option;
final PopoverMutex? popoverMutex;
@override
State<_OptionCell> createState() => _OptionCellState();
}
class _OptionCellState extends State<_OptionCell> {
final PopoverController _popoverController = PopoverController();
@override
Widget build(BuildContext context) {
final child = SizedBox(
height: 28,
child: SelectOptionTagCell(
option: widget.option,
onSelected: () => _popoverController.show(),
children: [
FlowyIconButton(
onPressed: () => _popoverController.show(),
iconPadding: const EdgeInsets.symmetric(horizontal: 6.0),
hoverColor: Colors.transparent,
icon: FlowySvg(
FlowySvgs.details_s,
color: Theme.of(context).iconTheme.color,
),
),
],
),
);
return AppFlowyPopover(
controller: _popoverController,
mutex: widget.popoverMutex,
offset: const Offset(8, 0),
margin: EdgeInsets.zero,
asBarrier: true,
constraints: BoxConstraints.loose(const Size(460, 470)),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: FlowyHover(
resetHoverOnRebuild: false,
style: HoverStyle(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
),
child: child,
),
),
popupBuilder: (BuildContext popoverContext) {
return SelectOptionEditor(
option: widget.option,
onDeleted: () {
context
.read<SelectOptionTypeOptionBloc>()
.add(SelectOptionTypeOptionEvent.deleteOption(widget.option));
PopoverContainer.of(popoverContext).close();
},
onUpdated: (updatedOption) {
context
.read<SelectOptionTypeOptionBloc>()
.add(SelectOptionTypeOptionEvent.updateOption(updatedOption));
PopoverContainer.of(popoverContext).close();
},
key: ValueKey(widget.option.id),
);
},
);
}
}
class _AddOptionButton extends StatelessWidget {
const _AddOptionButton();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(
LocaleKeys.grid_field_addSelectOption.tr(),
),
onTap: () {
context
.read<SelectOptionTypeOptionBloc>()
.add(const SelectOptionTypeOptionEvent.addingOption());
},
leftIcon: const FlowySvg(FlowySvgs.add_s),
),
),
);
}
}
class CreateOptionTextField extends StatefulWidget {
const CreateOptionTextField({super.key, this.popoverMutex});
final PopoverMutex? popoverMutex;
@override
State<CreateOptionTextField> createState() => _CreateOptionTextFieldState();
}
class _CreateOptionTextFieldState extends State<CreateOptionTextField> {
late final FocusNode _focusNode;
@override
void initState() {
super.initState();
_focusNode = FocusNode()
..addListener(() {
if (_focusNode.hasFocus) {
widget.popoverMutex?.close();
}
});
widget.popoverMutex?.listenOnPopoverChanged(() {
if (_focusNode.hasFocus) {
_focusNode.unfocus();
}
});
}
@override
Widget build(BuildContext context) {
return BlocBuilder<SelectOptionTypeOptionBloc, SelectOptionTypeOptionState>(
builder: (context, state) {
final text = state.newOptionName ?? '';
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 14.0),
child: FlowyTextField(
autoClearWhenDone: true,
text: text,
focusNode: _focusNode,
onCanceled: () {
context
.read<SelectOptionTypeOptionBloc>()
.add(const SelectOptionTypeOptionEvent.endAddingOption());
},
onEditingComplete: () {},
onSubmitted: (optionName) {
context
.read<SelectOptionTypeOptionBloc>()
.add(SelectOptionTypeOptionEvent.createOption(optionName));
},
),
);
},
);
}
@override
void dispose() {
_focusNode.removeListener(() {
if (_focusNode.hasFocus) {
widget.popoverMutex?.close();
}
});
_focusNode.dispose();
super.dispose();
}
}

View File

@ -0,0 +1,242 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/field/type_option/edit_select_option_bloc.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../grid/presentation/layout/sizes.dart';
import '../../../../grid/presentation/widgets/common/type_option_separator.dart';
class SelectOptionEditor extends StatelessWidget {
const SelectOptionEditor({
super.key,
required this.option,
required this.onDeleted,
required this.onUpdated,
this.showOptions = true,
this.autoFocus = true,
});
final SelectOptionPB option;
final VoidCallback onDeleted;
final Function(SelectOptionPB) onUpdated;
final bool showOptions;
final bool autoFocus;
static String get identifier => (SelectOptionEditor).toString();
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => EditSelectOptionBloc(option: option),
child: MultiBlocListener(
listeners: [
BlocListener<EditSelectOptionBloc, EditSelectOptionState>(
listenWhen: (p, c) => p.deleted != c.deleted,
listener: (context, state) {
if (state.deleted) {
onDeleted();
}
},
),
BlocListener<EditSelectOptionBloc, EditSelectOptionState>(
listenWhen: (p, c) => p.option != c.option,
listener: (context, state) {
onUpdated(state.option);
},
),
],
child: BlocBuilder<EditSelectOptionBloc, EditSelectOptionState>(
builder: (context, state) {
final List<Widget> cells = [
_OptionNameTextField(
name: state.option.name,
autoFocus: autoFocus,
),
const VSpace(10),
const _DeleteTag(),
const TypeOptionSeparator(),
SelectOptionColorList(
selectedColor: state.option.color,
onSelectedColor: (color) => context
.read<EditSelectOptionBloc>()
.add(EditSelectOptionEvent.updateColor(color)),
),
];
return SizedBox(
width: 180,
child: ListView.builder(
shrinkWrap: true,
physics: StyledScrollPhysics(),
itemCount: cells.length,
itemBuilder: (context, index) {
if (cells[index] is TypeOptionSeparator) {
return cells[index];
} else {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 6.0),
child: cells[index],
);
}
},
padding: const EdgeInsets.symmetric(vertical: 6.0),
),
);
},
),
),
);
}
}
class _DeleteTag extends StatelessWidget {
const _DeleteTag();
@override
Widget build(BuildContext context) {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(
LocaleKeys.grid_selectOption_deleteTag.tr(),
),
leftIcon: const FlowySvg(FlowySvgs.delete_s),
onTap: () {
context
.read<EditSelectOptionBloc>()
.add(const EditSelectOptionEvent.delete());
},
),
);
}
}
class _OptionNameTextField extends StatelessWidget {
const _OptionNameTextField({
required this.name,
required this.autoFocus,
});
final String name;
final bool autoFocus;
@override
Widget build(BuildContext context) {
return FlowyTextField(
autoFocus: autoFocus,
text: name,
submitOnLeave: true,
onSubmitted: (newName) {
if (name != newName) {
context
.read<EditSelectOptionBloc>()
.add(EditSelectOptionEvent.updateName(newName));
}
},
);
}
}
class SelectOptionColorList extends StatelessWidget {
const SelectOptionColorList({
super.key,
this.selectedColor,
required this.onSelectedColor,
});
final SelectOptionColorPB? selectedColor;
final void Function(SelectOptionColorPB color) onSelectedColor;
@override
Widget build(BuildContext context) {
final cells = SelectOptionColorPB.values.map((color) {
return _SelectOptionColorCell(
color: color,
isSelected: selectedColor != null ? selectedColor == color : false,
onSelectedColor: onSelectedColor,
);
}).toList();
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: GridSize.typeOptionContentInsets,
child: SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyText.medium(
LocaleKeys.grid_selectOption_colorPanelTitle.tr(),
textAlign: TextAlign.left,
color: Theme.of(context).hintColor,
),
),
),
ListView.separated(
shrinkWrap: true,
separatorBuilder: (context, index) {
return VSpace(GridSize.typeOptionSeparatorHeight);
},
itemCount: cells.length,
physics: StyledScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
return cells[index];
},
),
],
);
}
}
class _SelectOptionColorCell extends StatelessWidget {
const _SelectOptionColorCell({
required this.color,
required this.isSelected,
required this.onSelectedColor,
});
final SelectOptionColorPB color;
final bool isSelected;
final void Function(SelectOptionColorPB color) onSelectedColor;
@override
Widget build(BuildContext context) {
Widget? checkmark;
if (isSelected) {
checkmark = const FlowySvg(FlowySvgs.check_s);
}
final colorIcon = SizedBox.square(
dimension: 16,
child: DecoratedBox(
decoration: BoxDecoration(
color: color.toColor(context),
shape: BoxShape.circle,
),
),
);
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
text: FlowyText.medium(
color.optionName(),
color: AFThemeExtension.of(context).textColor,
),
leftIcon: colorIcon,
rightIcon: checkmark,
onTap: () => onSelectedColor(color),
),
);
}
}

View File

@ -0,0 +1,38 @@
import 'package:appflowy/plugins/database/application/field/type_option/select_type_option_actions.dart';
import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter/material.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'builder.dart';
import 'select/select_option.dart';
class SingleSelectTypeOptionEditorFactory implements TypeOptionEditorFactory {
const SingleSelectTypeOptionEditorFactory();
@override
Widget? build({
required BuildContext context,
required String viewId,
required FieldPB field,
required PopoverMutex popoverMutex,
required TypeOptionDataCallback onTypeOptionUpdated,
}) {
final typeOption = _parseTypeOptionData(field.typeOptionData);
return SelectOptionTypeOptionWidget(
options: typeOption.options,
beginEdit: () => PopoverContainer.of(context).closeAll(),
popoverMutex: popoverMutex,
typeOptionAction: SingleSelectAction(
viewId: viewId,
fieldId: field.id,
onTypeOptionUpdated: onTypeOptionUpdated,
),
);
}
SingleSelectTypeOptionPB _parseTypeOptionData(List<int> data) {
return SingleSelectTypeOptionDataParser().fromBuffer(data);
}
}

View File

@ -0,0 +1,131 @@
import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:protobuf/protobuf.dart';
import 'builder.dart';
import 'date/date_time_format.dart';
class TimestampTypeOptionEditorFactory implements TypeOptionEditorFactory {
const TimestampTypeOptionEditorFactory();
@override
Widget? build({
required BuildContext context,
required String viewId,
required FieldPB field,
required PopoverMutex popoverMutex,
required TypeOptionDataCallback onTypeOptionUpdated,
}) {
final typeOption = _parseTypeOptionData(field.typeOptionData);
return SeparatedColumn(
mainAxisSize: MainAxisSize.min,
separatorBuilder: () => VSpace(GridSize.typeOptionSeparatorHeight),
children: [
_renderDateFormatButton(typeOption, popoverMutex, onTypeOptionUpdated),
_renderTimeFormatButton(typeOption, popoverMutex, onTypeOptionUpdated),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: IncludeTimeButton(
onChanged: (value) {
final newTypeOption = _updateTypeOption(
typeOption: typeOption,
includeTime: !value,
);
onTypeOptionUpdated(newTypeOption.writeToBuffer());
},
value: typeOption.includeTime,
),
),
],
);
}
Widget _renderDateFormatButton(
TimestampTypeOptionPB typeOption,
PopoverMutex popoverMutex,
TypeOptionDataCallback onTypeOptionUpdated,
) {
return AppFlowyPopover(
mutex: popoverMutex,
asBarrier: true,
triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
offset: const Offset(8, 0),
constraints: BoxConstraints.loose(const Size(460, 440)),
popupBuilder: (popoverContext) {
return DateFormatList(
selectedFormat: typeOption.dateFormat,
onSelected: (format) {
final newTypeOption =
_updateTypeOption(typeOption: typeOption, dateFormat: format);
onTypeOptionUpdated(newTypeOption.writeToBuffer());
PopoverContainer.of(popoverContext).close();
},
);
},
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 12.0),
child: DateFormatButton(),
),
);
}
Widget _renderTimeFormatButton(
TimestampTypeOptionPB typeOption,
PopoverMutex popoverMutex,
TypeOptionDataCallback onTypeOptionUpdated,
) {
return AppFlowyPopover(
mutex: popoverMutex,
asBarrier: true,
triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
offset: const Offset(8, 0),
constraints: BoxConstraints.loose(const Size(460, 440)),
popupBuilder: (BuildContext popoverContext) {
return TimeFormatList(
selectedFormat: typeOption.timeFormat,
onSelected: (format) {
final newTypeOption =
_updateTypeOption(typeOption: typeOption, timeFormat: format);
onTypeOptionUpdated(newTypeOption.writeToBuffer());
PopoverContainer.of(popoverContext).close();
},
);
},
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 12.0),
child: TimeFormatButton(),
),
);
}
TimestampTypeOptionPB _parseTypeOptionData(List<int> data) {
return TimestampTypeOptionDataParser().fromBuffer(data);
}
TimestampTypeOptionPB _updateTypeOption({
required TimestampTypeOptionPB typeOption,
DateFormatPB? dateFormat,
TimeFormatPB? timeFormat,
bool? includeTime,
}) {
typeOption.freeze();
return typeOption.rebuild((typeOption) {
if (dateFormat != null) {
typeOption.dateFormat = dateFormat;
}
if (timeFormat != null) {
typeOption.timeFormat = timeFormat;
}
if (includeTime != null) {
typeOption.includeTime = includeTime;
}
});
}
}

View File

@ -0,0 +1,18 @@
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flutter/material.dart';
import 'builder.dart';
class URLTypeOptionEditorFactory implements TypeOptionEditorFactory {
const URLTypeOptionEditorFactory();
@override
Widget? build({
required BuildContext context,
required String viewId,
required FieldPB field,
required PopoverMutex popoverMutex,
required TypeOptionDataCallback onTypeOptionUpdated,
}) =>
null;
}

View File

@ -8,7 +8,7 @@ import 'package:appflowy/plugins/database/application/field/field_info.dart';
import 'package:appflowy/plugins/database/domain/field_service.dart';
import 'package:appflowy/plugins/database/grid/application/row/row_detail_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_editor.dart';
import 'package:appflowy/plugins/database/widgets/field/field_editor.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_button.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';

View File

@ -5,7 +5,7 @@ import 'package:appflowy/plugins/database/application/field/field_controller.dar
import 'package:appflowy/plugins/database/application/field/field_info.dart';
import 'package:appflowy/plugins/database/application/setting/property_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_editor.dart';
import 'package:appflowy/plugins/database/widgets/field/field_editor.dart';
import 'package:appflowy/plugins/database/widgets/setting/field_visibility_extension.dart';
import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';