mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
chore: organize code (#5004)
* chore: rename files and classes * chore: move some files around
This commit is contained in:
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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';
|
||||
|
@ -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';
|
||||
|
Reference in New Issue
Block a user