feat: allow fields to not wrap cell content (#5128)

This commit is contained in:
Richard Shiue
2024-04-13 16:48:28 +08:00
committed by GitHub
parent 891fd16a0c
commit 8947a89a24
54 changed files with 660 additions and 475 deletions

View File

@ -113,7 +113,7 @@ List<CellContext> _makeCells(
cellContexts.removeWhere((cellContext) {
final fieldInfo = fieldController.getField(cellContext.fieldId);
return fieldInfo == null ||
!(fieldInfo.fieldSettings?.visibility.isVisibleState() ?? false) ||
!(fieldInfo.visibility?.isVisibleState() ?? false) ||
(groupFieldId != null && cellContext.fieldId == groupFieldId);
});
return cellContexts.toList();

View File

@ -41,7 +41,7 @@ class _DateCellState extends State<DateCardCell> {
widget.databaseController,
widget.cellContext,
).as(),
)..add(const DateCellEvent.initial());
);
},
child: BlocBuilder<DateCellBloc, DateCellState>(
buildWhen: (previous, current) => previous.dateStr != current.dateStr,

View File

@ -41,7 +41,7 @@ class _NumberCellState extends State<NumberCardCell> {
widget.databaseController,
widget.cellContext,
).as(),
)..add(const NumberCellEvent.initial());
);
},
child: BlocBuilder<NumberCellBloc, NumberCellState>(
buildWhen: (previous, current) => previous.content != current.content,

View File

@ -46,7 +46,7 @@ class _SelectOptionCellState extends State<SelectOptionCardCell> {
widget.databaseController,
widget.cellContext,
).as(),
)..add(const SelectOptionCellEvent.initial());
);
},
child: BlocBuilder<SelectOptionCellBloc, SelectOptionCellState>(
buildWhen: (previous, current) {

View File

@ -52,7 +52,7 @@ class _TextCellState extends State<TextCardCell> {
widget.databaseController,
widget.cellContext,
).as(),
)..add(const TextCellEvent.initial());
);
late final TextEditingController _textEditingController =
TextEditingController(text: cellBloc.state.content);
final focusNode = SingleListenerFocusNode();

View File

@ -41,7 +41,7 @@ class _TimestampCellState extends State<TimestampCardCell> {
widget.databaseController,
widget.cellContext,
).as(),
)..add(const TimestampCellEvent.initial());
);
},
child: BlocBuilder<TimestampCellBloc, TimestampCellState>(
buildWhen: (previous, current) => previous.dateStr != current.dateStr,

View File

@ -41,7 +41,7 @@ class _URLCellState extends State<URLCardCell> {
widget.databaseController,
widget.cellContext,
).as(),
)..add(const URLCellEvent.initial());
);
},
child: BlocBuilder<URLCellBloc, URLCellState>(
buildWhen: (previous, current) => previous.content != current.content,

View File

@ -27,27 +27,15 @@ class DesktopGridDateCellSkin extends IEditableDateCellSkin {
direction: PopoverDirection.bottomWithLeftAligned,
constraints: BoxConstraints.loose(const Size(260, 620)),
margin: EdgeInsets.zero,
child: Container(
child: Align(
alignment: AlignmentDirectional.centerStart,
padding: GridSize.cellContentInsets,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: FlowyText.medium(
state.dateStr,
overflow: TextOverflow.ellipsis,
child: state.fieldInfo.wrapCellContent ?? false
? _buildCellContent(state)
: SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
scrollDirection: Axis.horizontal,
child: _buildCellContent(state),
),
),
if (state.data?.reminderId.isNotEmpty ?? false) ...[
const HSpace(4),
FlowyTooltip(
message: LocaleKeys.grid_field_reminderOnDateTooltip.tr(),
child: const FlowySvg(FlowySvgs.clock_alarm_s),
),
],
],
),
),
popupBuilder: (BuildContext popoverContent) {
return DateCellEditor(
@ -60,4 +48,30 @@ class DesktopGridDateCellSkin extends IEditableDateCellSkin {
},
);
}
Widget _buildCellContent(DateCellState state) {
final wrap = state.fieldInfo.wrapCellContent ?? false;
return Padding(
padding: GridSize.cellContentInsets,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: FlowyText.medium(
state.dateStr,
overflow: wrap ? null : TextOverflow.ellipsis,
maxLines: wrap ? null : 1,
),
),
if (state.data?.reminderId.isNotEmpty ?? false) ...[
const HSpace(4),
FlowyTooltip(
message: LocaleKeys.grid_field_reminderOnDateTooltip.tr(),
child: const FlowySvg(FlowySvgs.clock_alarm_s),
),
],
],
),
);
}
}

View File

@ -2,6 +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/application/cell/bloc/number_cell_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../editable_cell_skeleton/number.dart';
@ -19,7 +20,7 @@ class DesktopGridNumberCellSkin extends IEditableNumberCellSkin {
focusNode: focusNode,
onEditingComplete: () => focusNode.unfocus(),
onSubmitted: (_) => focusNode.unfocus(),
maxLines: null,
maxLines: context.watch<NumberCellBloc>().state.wrap ? null : 1,
style: Theme.of(context).textTheme.bodyMedium,
textInputAction: TextInputAction.done,
decoration: InputDecoration(

View File

@ -3,6 +3,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/relation_cell_editor.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/relation_cell_bloc.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';
@ -32,13 +33,52 @@ class DesktopGridRelationCellSkin extends IEditableRelationCellSkin {
child: const RelationCellEditor(),
);
},
child: Container(
child: Align(
alignment: AlignmentDirectional.centerStart,
child: state.wrap
? _buildWrapRows(context, state.rows)
: _buildNoWrapRows(context, state.rows),
),
);
}
Widget _buildWrapRows(
BuildContext context,
List<RelatedRowDataPB> rows,
) {
return Padding(
padding: GridSize.cellContentInsets,
child: Wrap(
runSpacing: 4,
spacing: 4.0,
children: rows.map(
(row) {
final isEmpty = row.name.isEmpty;
return FlowyText.medium(
isEmpty ? LocaleKeys.grid_row_titlePlaceholder.tr() : row.name,
color: isEmpty ? Theme.of(context).hintColor : null,
decoration: TextDecoration.underline,
overflow: TextOverflow.ellipsis,
);
},
).toList(),
),
);
}
Widget _buildNoWrapRows(
BuildContext context,
List<RelatedRowDataPB> rows,
) {
return SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
scrollDirection: Axis.horizontal,
child: Padding(
padding: GridSize.cellContentInsets,
child: Wrap(
runSpacing: 4.0,
spacing: 4.0,
children: state.rows.map(
child: SeparatedRow(
separatorBuilder: () => const HSpace(4.0),
mainAxisSize: MainAxisSize.min,
children: rows.map(
(row) {
final isEmpty = row.name.isEmpty;
return FlowyText.medium(

View File

@ -3,7 +3,7 @@ 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_cell_editor.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.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/widgets.dart';
@ -23,6 +23,7 @@ class DesktopGridSelectOptionCellSkin extends IEditableSelectOptionCellSkin {
controller: popoverController,
constraints: const BoxConstraints.tightFor(width: 300),
margin: EdgeInsets.zero,
triggerActions: PopoverTriggerFlags.none,
direction: PopoverDirection.bottomWithLeftAligned,
popupBuilder: (BuildContext popoverContext) {
return SelectOptionCellEditor(
@ -32,35 +33,67 @@ class DesktopGridSelectOptionCellSkin extends IEditableSelectOptionCellSkin {
onClose: () => cellContainerNotifier.isFocus = false,
child: BlocBuilder<SelectOptionCellBloc, SelectOptionCellState>(
builder: (context, state) {
return Container(
return Align(
alignment: AlignmentDirectional.centerStart,
padding: GridSize.cellContentInsets,
child: state.selectedOptions.isEmpty
? const SizedBox.shrink()
: _buildOptions(context, state.selectedOptions),
child: state.wrap
? _buildWrapOptions(context, state.selectedOptions)
: _buildNoWrapOptions(context, state.selectedOptions),
);
},
),
);
}
Widget _buildOptions(context, List<SelectOptionPB> options) {
return Wrap(
runSpacing: 4,
children: options.map(
(option) {
return Padding(
padding: const EdgeInsets.only(right: 4),
child: SelectOptionTag(
option: option,
padding: const EdgeInsets.symmetric(
vertical: 1,
horizontal: 8,
Widget _buildWrapOptions(BuildContext context, List<SelectOptionPB> options) {
return Padding(
padding: GridSize.cellContentInsets,
child: Wrap(
runSpacing: 4,
children: options.map(
(option) {
return Padding(
padding: const EdgeInsets.only(right: 4),
child: SelectOptionTag(
option: option,
padding: const EdgeInsets.symmetric(
vertical: 1,
horizontal: 8,
),
),
),
);
},
).toList(),
);
},
).toList(),
),
);
}
Widget _buildNoWrapOptions(
BuildContext context,
List<SelectOptionPB> options,
) {
return SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
scrollDirection: Axis.horizontal,
child: Padding(
padding: GridSize.cellContentInsets,
child: Row(
mainAxisSize: MainAxisSize.min,
children: options.map(
(option) {
return Padding(
padding: const EdgeInsets.only(right: 4),
child: SelectOptionTag(
option: option,
padding: const EdgeInsets.symmetric(
vertical: 1,
horizontal: 8,
),
),
);
},
).toList(),
),
),
);
}
}

View File

@ -44,7 +44,7 @@ class DesktopGridTextCellSkin extends IEditableTextCellSkin {
child: TextField(
controller: textEditingController,
focusNode: focusNode,
maxLines: null,
maxLines: context.watch<TextCellBloc>().state.wrap ? null : 1,
style: Theme.of(context).textTheme.bodyMedium,
decoration: const InputDecoration(
border: InputBorder.none,

View File

@ -16,10 +16,23 @@ class DesktopGridTimestampCellSkin extends IEditableTimestampCellSkin {
) {
return Container(
alignment: AlignmentDirectional.centerStart,
child: state.wrap
? _buildCellContent(state)
: SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
scrollDirection: Axis.horizontal,
child: _buildCellContent(state),
),
);
}
Widget _buildCellContent(TimestampCellState state) {
return Padding(
padding: GridSize.cellContentInsets,
child: FlowyText.medium(
state.dateStr,
maxLines: null,
overflow: state.wrap ? null : TextOverflow.ellipsis,
maxLines: state.wrap ? null : 1,
),
);
}

View File

@ -11,6 +11,7 @@ import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../editable_cell_skeleton/url.dart';
@ -24,28 +25,31 @@ class DesktopGridURLSkin extends IEditableURLCellSkin {
TextEditingController textEditingController,
URLCellDataNotifier cellDataNotifier,
) {
return TextField(
controller: textEditingController,
focusNode: focusNode,
maxLines: null,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.primary,
decoration: TextDecoration.underline,
),
decoration: InputDecoration(
contentPadding: GridSize.cellContentInsets,
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
hintStyle: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: Theme.of(context).hintColor),
isDense: true,
return BlocSelector<URLCellBloc, URLCellState, bool>(
selector: (state) => state.wrap,
builder: (context, wrap) => TextField(
controller: textEditingController,
focusNode: focusNode,
maxLines: wrap ? null : 1,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.primary,
decoration: TextDecoration.underline,
),
decoration: InputDecoration(
contentPadding: GridSize.cellContentInsets,
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
hintStyle: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: Theme.of(context).hintColor),
isDense: true,
),
onTapOutside: (_) => focusNode.unfocus(),
),
onTapOutside: (_) => focusNode.unfocus(),
);
}
@ -179,8 +183,7 @@ class _VisitURLAccessoryState extends State<_VisitURLAccessory>
bool enable() => widget.cellDataNotifier.value.isNotEmpty;
@override
void onTap() =>
openUrlCellLink(widget.cellDataNotifier.value);
void onTap() => openUrlCellLink(widget.cellDataNotifier.value);
}
class _URLAccessoryIconContainer extends StatelessWidget {
@ -190,9 +193,8 @@ class _URLAccessoryIconContainer extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SizedBox(
width: 26,
height: 26,
return SizedBox.square(
dimension: 26,
child: Padding(
padding: const EdgeInsets.all(3.0),
child: child,

View File

@ -57,7 +57,7 @@ class _DateCellState extends GridCellState<EditableDateCell> {
widget.databaseController,
widget.cellContext,
).as(),
)..add(const DateCellEvent.initial());
);
@override
void dispose() {

View File

@ -58,7 +58,7 @@ class _NumberCellState extends GridEditableTextCell<EditableNumberCell> {
widget.databaseController,
widget.cellContext,
).as(),
)..add(const NumberCellEvent.initial());
);
@override
void initState() {
@ -81,12 +81,16 @@ class _NumberCellState extends GridEditableTextCell<EditableNumberCell> {
child: BlocListener<NumberCellBloc, NumberCellState>(
listener: (context, state) =>
_textEditingController.text = state.content,
child: widget.skin.build(
context,
widget.cellContainerNotifier,
cellBloc,
focusNode,
_textEditingController,
child: Builder(
builder: (context) {
return widget.skin.build(
context,
widget.cellContainerNotifier,
cellBloc,
focusNode,
_textEditingController,
);
},
),
),
);

View File

@ -64,7 +64,7 @@ class _SelectOptionCellState extends GridCellState<EditableSelectOptionCell> {
widget.databaseController,
widget.cellContext,
).as(),
)..add(const SelectOptionCellEvent.initial());
);
@override
void dispose() {

View File

@ -58,7 +58,7 @@ class _TextCellState extends GridEditableTextCell<EditableTextCell> {
widget.databaseController,
widget.cellContext,
).as(),
)..add(const TextCellEvent.initial());
);
@override
void initState() {
@ -82,12 +82,16 @@ class _TextCellState extends GridEditableTextCell<EditableTextCell> {
listener: (context, state) {
_textEditingController.text = state.content;
},
child: widget.skin.build(
context,
widget.cellContainerNotifier,
cellBloc,
focusNode,
_textEditingController,
child: Builder(
builder: (context) {
return widget.skin.build(
context,
widget.cellContainerNotifier,
cellBloc,
focusNode,
_textEditingController,
);
},
),
),
);

View File

@ -57,7 +57,7 @@ class _TimestampCellState extends GridCellState<EditableTimestampCell> {
widget.databaseController,
widget.cellContext,
).as(),
)..add(const TimestampCellEvent.initial());
);
@override
void dispose() {

View File

@ -85,7 +85,7 @@ class _GridURLCellState extends GridEditableTextCell<EditableURLCell> {
widget.databaseController,
widget.cellContext,
).as(),
)..add(const URLCellEvent.initial());
);
@override
SingleListenerFocusNode focusNode = SingleListenerFocusNode();

View File

@ -42,7 +42,7 @@ class _DateCellEditor extends State<DateCellEditor> {
create: (context) => DateCellEditorBloc(
reminderBloc: getIt<ReminderBloc>(),
cellController: widget.cellController,
)..add(const DateCellEditorEvent.initial()),
),
),
],
child: BlocBuilder<DateCellEditorBloc, DateCellEditorState>(

View File

@ -1,7 +1,5 @@
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';
@ -12,12 +10,14 @@ 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/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: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:styled_widget/styled_widget.dart';
import 'field_type_list.dart';
import 'type_option_editor/builder.dart';
@ -87,10 +87,12 @@ class _FieldEditorState extends State<FieldEditor> {
mainAxisSize: MainAxisSize.min,
children: [
FieldNameTextField(
padding: const EdgeInsets.fromLTRB(4, 4, 4, 8),
padding: const EdgeInsets.fromLTRB(12, 12, 12, 8),
textEditingController: textController,
),
VSpace(GridSize.typeOptionSeparatorHeight),
_EditFieldButton(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
onTap: () {
setState(() => _currentPage = FieldEditorPage.details);
},
@ -107,8 +109,11 @@ class _FieldEditorState extends State<FieldEditor> {
_actionCell(FieldAction.clearData),
VSpace(GridSize.typeOptionSeparatorHeight),
_actionCell(FieldAction.delete),
const TypeOptionSeparator(spacing: 8.0),
_actionCell(FieldAction.wrap),
const VSpace(8.0),
],
).padding(all: 8.0),
),
);
}
@ -124,24 +129,32 @@ class _FieldEditorState extends State<FieldEditor> {
Widget _actionCell(FieldAction action) {
return BlocBuilder<FieldEditorBloc, FieldEditorState>(
builder: (context, state) => FieldActionCell(
viewId: widget.viewId,
fieldInfo: state.field,
action: action,
builder: (context, state) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: FieldActionCell(
viewId: widget.viewId,
fieldInfo: state.field,
action: action,
),
),
);
}
}
class _EditFieldButton extends StatelessWidget {
const _EditFieldButton({this.onTap});
const _EditFieldButton({
required this.padding,
this.onTap,
});
final EdgeInsetsGeometry padding;
final void Function()? onTap;
@override
Widget build(BuildContext context) {
return SizedBox(
return Container(
height: GridSize.popoverItemHeight,
padding: padding,
child: FlowyButton(
leftIcon: const FlowySvg(FlowySvgs.edit_s),
text: FlowyText.medium(
@ -184,10 +197,11 @@ class FieldActionCell extends StatelessWidget {
),
onHover: (_) => popoverMutex?.close(),
onTap: () => action.run(context, viewId, fieldInfo),
leftIcon: action.icon(
leftIcon: action.leading(
fieldInfo,
enable ? null : Theme.of(context).disabledColor,
),
rightIcon: action.trailing(context, fieldInfo),
);
}
}
@ -198,10 +212,11 @@ enum FieldAction {
toggleVisibility,
duplicate,
clearData,
delete;
delete,
wrap;
Widget icon(FieldInfo fieldInfo, Color? color) {
late final FlowySvgData svgData;
Widget? leading(FieldInfo fieldInfo, Color? color) {
FlowySvgData? svgData;
switch (this) {
case FieldAction.insertLeft:
svgData = FlowySvgs.arrow_s;
@ -220,6 +235,11 @@ enum FieldAction {
svgData = FlowySvgs.reload_s;
case FieldAction.delete:
svgData = FlowySvgs.delete_s;
default:
}
if (svgData == null) {
return null;
}
final icon = FlowySvg(
svgData,
@ -231,6 +251,21 @@ enum FieldAction {
: icon;
}
Widget? trailing(BuildContext context, FieldInfo fieldInfo) {
if (this == FieldAction.wrap) {
return Toggle(
value: fieldInfo.wrapCellContent ?? false,
onChanged: (_) => context
.read<FieldEditorBloc>()
.add(const FieldEditorEvent.toggleWrapCellContent()),
style: ToggleStyle.big,
padding: EdgeInsets.zero,
);
}
return null;
}
String title(FieldInfo fieldInfo) {
switch (this) {
case FieldAction.insertLeft:
@ -250,6 +285,8 @@ enum FieldAction {
return LocaleKeys.grid_field_clear.tr();
case FieldAction.delete:
return LocaleKeys.grid_field_delete.tr();
case FieldAction.wrap:
return LocaleKeys.grid_field_wrap.tr();
}
}
@ -308,6 +345,11 @@ enum FieldAction {
).show(context);
PopoverContainer.of(context).close();
break;
case FieldAction.wrap:
context
.read<FieldEditorBloc>()
.add(const FieldEditorEvent.toggleWrapCellContent());
break;
}
}
}

View File

@ -117,7 +117,7 @@ class _GridCellEnterRegion extends StatelessWidget {
onExit: (p) =>
CellContainerNotifier.of(context, listen: false).isHover = false,
child: Stack(
alignment: AlignmentDirectional.center,
alignment: Alignment.center,
fit: StackFit.expand,
children: children,
),