|
@@ -1,21 +1,27 @@
|
|
|
+import 'package:appflowy/generated/flowy_svgs.g.dart';
|
|
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
|
|
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
|
|
|
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
|
|
|
+import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_service.dart';
|
|
|
import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart';
|
|
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_cell.dart';
|
|
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.dart';
|
|
|
-import 'package:appflowy/plugins/database_view/widgets/row/row_action.dart';
|
|
|
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
|
|
-import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
|
|
+import 'package:appflowy_backend/log.dart';
|
|
|
+import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
|
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
|
|
+import 'package:collection/collection.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:flutter/material.dart';
|
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
|
|
|
|
import 'accessory/cell_accessory.dart';
|
|
|
import 'cell_builder.dart';
|
|
|
+import 'cells/checkbox_cell/checkbox_cell.dart';
|
|
|
import 'cells/date_cell/date_cell.dart';
|
|
|
+import 'cells/number_cell/number_cell.dart';
|
|
|
import 'cells/select_option_cell/select_option_cell.dart';
|
|
|
import 'cells/text_cell/text_cell.dart';
|
|
|
import 'cells/timestamp_cell/timestamp_cell.dart';
|
|
@@ -23,7 +29,6 @@ import 'cells/url_cell/url_cell.dart';
|
|
|
|
|
|
/// Display the row properties in a list. Only use this widget in the
|
|
|
/// [RowDetailPage].
|
|
|
-///
|
|
|
class RowPropertyList extends StatelessWidget {
|
|
|
final String viewId;
|
|
|
final GridCellBuilder cellBuilder;
|
|
@@ -38,39 +43,88 @@ class RowPropertyList extends StatelessWidget {
|
|
|
return BlocBuilder<RowDetailBloc, RowDetailState>(
|
|
|
buildWhen: (previous, current) => previous.cells != current.cells,
|
|
|
builder: (context, state) {
|
|
|
- return Column(
|
|
|
- mainAxisSize: MainAxisSize.min,
|
|
|
- crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
- children: [
|
|
|
- // The rest of the fields are displayed in the order of the field
|
|
|
- // list
|
|
|
- ...state.cells
|
|
|
- .where((element) => !element.fieldInfo.field.isPrimary)
|
|
|
- .map(
|
|
|
- (cell) => _PropertyCell(
|
|
|
- cellContext: cell,
|
|
|
- cellBuilder: cellBuilder,
|
|
|
- ),
|
|
|
- )
|
|
|
- .toList(),
|
|
|
- const VSpace(20),
|
|
|
-
|
|
|
- // Create a new property(field) button
|
|
|
- CreateRowFieldButton(viewId: viewId),
|
|
|
- ],
|
|
|
+ final children = state.cells
|
|
|
+ .where((element) => !element.fieldInfo.field.isPrimary)
|
|
|
+ .mapIndexed(
|
|
|
+ (index, cell) => _PropertyCell(
|
|
|
+ key: ValueKey('row_detail_${cell.fieldId}'),
|
|
|
+ cellContext: cell,
|
|
|
+ cellBuilder: cellBuilder,
|
|
|
+ index: index,
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ .toList();
|
|
|
+ return ReorderableListView(
|
|
|
+ shrinkWrap: true,
|
|
|
+ physics: const NeverScrollableScrollPhysics(),
|
|
|
+ onReorder: (oldIndex, newIndex) {
|
|
|
+ final reorderedField = children[oldIndex].cellContext.fieldId;
|
|
|
+ _reorderField(
|
|
|
+ context,
|
|
|
+ state.cells,
|
|
|
+ reorderedField,
|
|
|
+ oldIndex,
|
|
|
+ newIndex,
|
|
|
+ );
|
|
|
+ },
|
|
|
+ buildDefaultDragHandles: false,
|
|
|
+ proxyDecorator: (child, index, animation) => Material(
|
|
|
+ color: Colors.transparent,
|
|
|
+ child: Stack(
|
|
|
+ children: [
|
|
|
+ child,
|
|
|
+ const MouseRegion(cursor: SystemMouseCursors.grabbing),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ footer: Padding(
|
|
|
+ padding: const EdgeInsets.only(left: 20),
|
|
|
+ child: CreateRowFieldButton(viewId: viewId),
|
|
|
+ ),
|
|
|
+ children: children,
|
|
|
);
|
|
|
},
|
|
|
);
|
|
|
}
|
|
|
+
|
|
|
+ void _reorderField(
|
|
|
+ BuildContext context,
|
|
|
+ List<DatabaseCellContext> cells,
|
|
|
+ String reorderedFieldId,
|
|
|
+ int oldIndex,
|
|
|
+ int newIndex,
|
|
|
+ ) {
|
|
|
+ // when reorderiing downwards, need to update index
|
|
|
+ if (oldIndex < newIndex) {
|
|
|
+ newIndex--;
|
|
|
+ }
|
|
|
+
|
|
|
+ // also update index when the index is after the index of the primary field
|
|
|
+ // in the original list of DatabaseCellContext's
|
|
|
+ final primaryFieldIndex =
|
|
|
+ cells.indexWhere((element) => element.fieldInfo.isPrimary);
|
|
|
+ if (oldIndex >= primaryFieldIndex) {
|
|
|
+ oldIndex++;
|
|
|
+ }
|
|
|
+ if (newIndex >= primaryFieldIndex) {
|
|
|
+ newIndex++;
|
|
|
+ }
|
|
|
+
|
|
|
+ context.read<RowDetailBloc>().add(
|
|
|
+ RowDetailEvent.reorderField(reorderedFieldId, oldIndex, newIndex),
|
|
|
+ );
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
class _PropertyCell extends StatefulWidget {
|
|
|
final DatabaseCellContext cellContext;
|
|
|
final GridCellBuilder cellBuilder;
|
|
|
+ final int index;
|
|
|
const _PropertyCell({
|
|
|
required this.cellContext,
|
|
|
required this.cellBuilder,
|
|
|
Key? key,
|
|
|
+ required this.index,
|
|
|
}) : super(key: key);
|
|
|
|
|
|
@override
|
|
@@ -78,45 +132,65 @@ class _PropertyCell extends StatefulWidget {
|
|
|
}
|
|
|
|
|
|
class _PropertyCellState extends State<_PropertyCell> {
|
|
|
- final PopoverController popover = PopoverController();
|
|
|
+ final PopoverController _popoverController = PopoverController();
|
|
|
+ bool _isFieldHover = false;
|
|
|
|
|
|
@override
|
|
|
Widget build(BuildContext context) {
|
|
|
final style = _customCellStyle(widget.cellContext.fieldType);
|
|
|
final cell = widget.cellBuilder.build(widget.cellContext, style: style);
|
|
|
|
|
|
+ final dragThumb = MouseRegion(
|
|
|
+ cursor: SystemMouseCursors.grab,
|
|
|
+ child: SizedBox(
|
|
|
+ width: 16,
|
|
|
+ height: 30,
|
|
|
+ child: _isFieldHover ? const FlowySvg(FlowySvgs.drag_element_s) : null,
|
|
|
+ ),
|
|
|
+ );
|
|
|
+
|
|
|
final gesture = GestureDetector(
|
|
|
behavior: HitTestBehavior.opaque,
|
|
|
onTap: () => cell.requestFocus.notify(),
|
|
|
- child: AccessoryHover(
|
|
|
- contentPadding: const EdgeInsets.symmetric(horizontal: 3, vertical: 3),
|
|
|
- child: cell,
|
|
|
- ),
|
|
|
+ child: AccessoryHover(child: cell),
|
|
|
);
|
|
|
|
|
|
- return IntrinsicHeight(
|
|
|
- child: ConstrainedBox(
|
|
|
- constraints: const BoxConstraints(minHeight: 30),
|
|
|
+ return Container(
|
|
|
+ margin: const EdgeInsets.only(bottom: 8),
|
|
|
+ constraints: const BoxConstraints(minHeight: 30),
|
|
|
+ child: MouseRegion(
|
|
|
+ onEnter: (event) => setState(() => _isFieldHover = true),
|
|
|
+ onExit: (event) => setState(() => _isFieldHover = false),
|
|
|
child: Row(
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
|
children: [
|
|
|
+ ReorderableDragStartListener(
|
|
|
+ index: widget.index,
|
|
|
+ enabled: _isFieldHover,
|
|
|
+ child: dragThumb,
|
|
|
+ ),
|
|
|
+ const HSpace(4),
|
|
|
AppFlowyPopover(
|
|
|
- controller: popover,
|
|
|
+ controller: _popoverController,
|
|
|
constraints: BoxConstraints.loose(const Size(240, 600)),
|
|
|
margin: EdgeInsets.zero,
|
|
|
triggerActions: PopoverTriggerFlags.none,
|
|
|
+ direction: PopoverDirection.bottomWithLeftAligned,
|
|
|
popupBuilder: (popoverContext) => buildFieldEditor(),
|
|
|
child: SizedBox(
|
|
|
- width: 150,
|
|
|
- height: 40,
|
|
|
+ width: 160,
|
|
|
+ height: 30,
|
|
|
child: FieldCellButton(
|
|
|
field: widget.cellContext.fieldInfo.field,
|
|
|
- onTap: () => popover.show(),
|
|
|
+ onTap: () => _popoverController.show(),
|
|
|
radius: BorderRadius.circular(6),
|
|
|
+ margin:
|
|
|
+ const EdgeInsets.symmetric(horizontal: 4, vertical: 6),
|
|
|
),
|
|
|
),
|
|
|
),
|
|
|
+ const HSpace(8),
|
|
|
Expanded(child: gesture),
|
|
|
],
|
|
|
),
|
|
@@ -133,11 +207,11 @@ class _PropertyCellState extends State<_PropertyCell> {
|
|
|
field: widget.cellContext.fieldInfo.field,
|
|
|
),
|
|
|
onHidden: (fieldId) {
|
|
|
- popover.close();
|
|
|
+ _popoverController.close();
|
|
|
context.read<RowDetailBloc>().add(RowDetailEvent.hideField(fieldId));
|
|
|
},
|
|
|
onDeleted: (fieldId) {
|
|
|
- popover.close();
|
|
|
+ _popoverController.close();
|
|
|
|
|
|
NavigatorAlertDialog(
|
|
|
title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
|
|
@@ -155,26 +229,35 @@ class _PropertyCellState extends State<_PropertyCell> {
|
|
|
GridCellStyle? _customCellStyle(FieldType fieldType) {
|
|
|
switch (fieldType) {
|
|
|
case FieldType.Checkbox:
|
|
|
- return null;
|
|
|
+ return GridCheckboxCellStyle(
|
|
|
+ cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
|
|
|
+ );
|
|
|
case FieldType.DateTime:
|
|
|
return DateCellStyle(
|
|
|
+ placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
|
|
alignment: Alignment.centerLeft,
|
|
|
+ cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
|
|
);
|
|
|
case FieldType.LastEditedTime:
|
|
|
case FieldType.CreatedTime:
|
|
|
return TimestampCellStyle(
|
|
|
+ placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
|
|
alignment: Alignment.centerLeft,
|
|
|
+ cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
|
|
);
|
|
|
case FieldType.MultiSelect:
|
|
|
return SelectOptionCellStyle(
|
|
|
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
|
|
+ cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
|
|
|
);
|
|
|
case FieldType.Checklist:
|
|
|
return SelectOptionCellStyle(
|
|
|
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
|
|
);
|
|
|
case FieldType.Number:
|
|
|
- return null;
|
|
|
+ return GridNumberCellStyle(
|
|
|
+ placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
|
|
+ );
|
|
|
case FieldType.RichText:
|
|
|
return GridTextCellStyle(
|
|
|
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
|
@@ -182,6 +265,7 @@ GridCellStyle? _customCellStyle(FieldType fieldType) {
|
|
|
case FieldType.SingleSelect:
|
|
|
return SelectOptionCellStyle(
|
|
|
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
|
|
+ cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
|
|
|
);
|
|
|
|
|
|
case FieldType.URL:
|
|
@@ -195,3 +279,81 @@ GridCellStyle? _customCellStyle(FieldType fieldType) {
|
|
|
}
|
|
|
throw UnimplementedError;
|
|
|
}
|
|
|
+
|
|
|
+class CreateRowFieldButton extends StatefulWidget {
|
|
|
+ final String viewId;
|
|
|
+
|
|
|
+ const CreateRowFieldButton({required this.viewId, super.key});
|
|
|
+
|
|
|
+ @override
|
|
|
+ State<CreateRowFieldButton> createState() => _CreateRowFieldButtonState();
|
|
|
+}
|
|
|
+
|
|
|
+class _CreateRowFieldButtonState extends State<CreateRowFieldButton> {
|
|
|
+ late PopoverController popoverController;
|
|
|
+ late TypeOptionPB typeOption;
|
|
|
+
|
|
|
+ @override
|
|
|
+ void initState() {
|
|
|
+ popoverController = PopoverController();
|
|
|
+ super.initState();
|
|
|
+ }
|
|
|
+
|
|
|
+ @override
|
|
|
+ Widget build(BuildContext context) {
|
|
|
+ return AppFlowyPopover(
|
|
|
+ constraints: BoxConstraints.loose(const Size(240, 200)),
|
|
|
+ controller: popoverController,
|
|
|
+ direction: PopoverDirection.topWithLeftAligned,
|
|
|
+ triggerActions: PopoverTriggerFlags.none,
|
|
|
+ margin: EdgeInsets.zero,
|
|
|
+ child: SizedBox(
|
|
|
+ height: 30,
|
|
|
+ child: FlowyButton(
|
|
|
+ margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 6),
|
|
|
+ text: FlowyText.medium(
|
|
|
+ LocaleKeys.grid_field_newProperty.tr(),
|
|
|
+ color: Theme.of(context).hintColor,
|
|
|
+ ),
|
|
|
+ hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
|
|
+ onTap: () async {
|
|
|
+ final result = await TypeOptionBackendService.createFieldTypeOption(
|
|
|
+ viewId: widget.viewId,
|
|
|
+ );
|
|
|
+ result.fold(
|
|
|
+ (l) {
|
|
|
+ typeOption = l;
|
|
|
+ popoverController.show();
|
|
|
+ },
|
|
|
+ (r) => Log.error("Failed to create field type option: $r"),
|
|
|
+ );
|
|
|
+ },
|
|
|
+ leftIcon: FlowySvg(
|
|
|
+ FlowySvgs.add_m,
|
|
|
+ color: Theme.of(context).hintColor,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ popupBuilder: (BuildContext popOverContext) {
|
|
|
+ return FieldEditor(
|
|
|
+ viewId: widget.viewId,
|
|
|
+ typeOptionLoader: FieldTypeOptionLoader(
|
|
|
+ viewId: widget.viewId,
|
|
|
+ field: typeOption.field_2,
|
|
|
+ ),
|
|
|
+ onDeleted: (fieldId) {
|
|
|
+ popoverController.close();
|
|
|
+ NavigatorAlertDialog(
|
|
|
+ title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
|
|
|
+ confirm: () {
|
|
|
+ context
|
|
|
+ .read<RowDetailBloc>()
|
|
|
+ .add(RowDetailEvent.deleteField(fieldId));
|
|
|
+ },
|
|
|
+ ).show(context);
|
|
|
+ },
|
|
|
+ );
|
|
|
+ },
|
|
|
+ );
|
|
|
+ }
|
|
|
+}
|