feat: Implement summary field for database row (#5246)

* chore: impl summary field

* chore: draft ui

* chore: add summary event

* chore: impl desktop ui

* chore: impl mobile ui

* chore: update test

* chore: disable ai test
This commit is contained in:
Nathan.fooo
2024-05-05 22:04:34 +08:00
committed by GitHub
parent 999ffeba21
commit a69e83c2cb
83 changed files with 1802 additions and 628 deletions

View File

@ -11,6 +11,7 @@ import 'card_cell_skeleton/checklist_card_cell.dart';
import 'card_cell_skeleton/date_card_cell.dart';
import 'card_cell_skeleton/number_card_cell.dart';
import 'card_cell_skeleton/select_option_card_cell.dart';
import 'card_cell_skeleton/summary_card_cell.dart';
import 'card_cell_skeleton/text_card_cell.dart';
import 'card_cell_skeleton/url_card_cell.dart';
@ -91,6 +92,12 @@ class CardCellBuilder {
databaseController: databaseController,
cellContext: cellContext,
),
FieldType.Summary => SummaryCardCell(
key: key,
style: isStyleOrNull(style),
databaseController: databaseController,
cellContext: cellContext,
),
_ => throw UnimplementedError,
};
}

View File

@ -0,0 +1,62 @@
import 'package:appflowy/plugins/database/application/cell/bloc/summary_cell_bloc.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'card_cell.dart';
class SummaryCardCellStyle extends CardCellStyle {
const SummaryCardCellStyle({
required super.padding,
required this.textStyle,
});
final TextStyle textStyle;
}
class SummaryCardCell extends CardCell<SummaryCardCellStyle> {
const SummaryCardCell({
super.key,
required super.style,
required this.databaseController,
required this.cellContext,
});
final DatabaseController databaseController;
final CellContext cellContext;
@override
State<SummaryCardCell> createState() => _SummaryCellState();
}
class _SummaryCellState extends State<SummaryCardCell> {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) {
return SummaryCellBloc(
cellController: makeCellController(
widget.databaseController,
widget.cellContext,
).as(),
);
},
child: BlocBuilder<SummaryCellBloc, SummaryCellState>(
buildWhen: (previous, current) => previous.content != current.content,
builder: (context, state) {
if (state.content.isEmpty) {
return const SizedBox.shrink();
}
return Container(
alignment: AlignmentDirectional.centerStart,
padding: widget.style.padding,
child: Text(state.content, style: widget.style.textStyle),
);
},
),
);
}
}

View File

@ -1,3 +1,4 @@
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/summary_card_cell.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter/material.dart';
@ -79,5 +80,9 @@ CardCellStyleMap desktopCalendarCardCellStyleMap(BuildContext context) {
wrap: true,
textStyle: textStyle,
),
FieldType.Summary: SummaryCardCellStyle(
padding: padding,
textStyle: textStyle,
),
};
}

View File

@ -1,3 +1,4 @@
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/summary_card_cell.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter/material.dart';
@ -79,5 +80,9 @@ CardCellStyleMap desktopBoardCardCellStyleMap(BuildContext context) {
wrap: true,
textStyle: textStyle,
),
FieldType.Summary: SummaryCardCellStyle(
padding: padding,
textStyle: textStyle,
),
};
}

View File

@ -1,3 +1,4 @@
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/summary_card_cell.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter/material.dart';
@ -78,5 +79,9 @@ CardCellStyleMap mobileBoardCardCellStyleMap(BuildContext context) {
textStyle: textStyle,
wrap: true,
),
FieldType.Summary: SummaryCardCellStyle(
padding: padding,
textStyle: textStyle,
),
};
}

View File

@ -0,0 +1,94 @@
import 'package:appflowy/plugins/database/application/cell/bloc/summary_cell_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/summary.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
class DesktopGridSummaryCellSkin extends IEditableSummaryCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
SummaryCellBloc bloc,
FocusNode focusNode,
TextEditingController textEditingController,
) {
return ChangeNotifierProvider(
create: (_) => SummaryMouseNotifier(),
builder: (context, child) {
return MouseRegion(
cursor: SystemMouseCursors.click,
opaque: false,
onEnter: (p) =>
Provider.of<SummaryMouseNotifier>(context, listen: false)
.onEnter = true,
onExit: (p) =>
Provider.of<SummaryMouseNotifier>(context, listen: false)
.onEnter = false,
child: Stack(
children: [
TextField(
controller: textEditingController,
enabled: false,
focusNode: focusNode,
onEditingComplete: () => focusNode.unfocus(),
onSubmitted: (_) => focusNode.unfocus(),
maxLines: null,
style: Theme.of(context).textTheme.bodyMedium,
textInputAction: TextInputAction.done,
decoration: InputDecoration(
contentPadding: GridSize.cellContentInsets,
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
isDense: true,
),
),
Padding(
padding: EdgeInsets.symmetric(
horizontal: GridSize.cellVPadding,
),
child: Consumer<SummaryMouseNotifier>(
builder: (
BuildContext context,
SummaryMouseNotifier notifier,
Widget? child,
) {
if (notifier.onEnter) {
return SummaryCellAccessory(
viewId: bloc.cellController.viewId,
fieldId: bloc.cellController.fieldId,
rowId: bloc.cellController.rowId,
);
} else {
return const SizedBox.shrink();
}
},
),
).positioned(right: 0, bottom: 0),
],
),
);
},
);
}
}
class SummaryMouseNotifier extends ChangeNotifier {
SummaryMouseNotifier();
bool _onEnter = false;
set onEnter(bool value) {
if (_onEnter != value) {
_onEnter = value;
notifyListeners();
}
}
bool get onEnter => _onEnter;
}

View File

@ -0,0 +1,53 @@
import 'package:appflowy/plugins/database/application/cell/bloc/summary_cell_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/summary.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:flutter/material.dart';
class DesktopRowDetailSummaryCellSkin extends IEditableSummaryCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
SummaryCellBloc bloc,
FocusNode focusNode,
TextEditingController textEditingController,
) {
return Column(
children: [
TextField(
controller: textEditingController,
focusNode: focusNode,
onEditingComplete: () => focusNode.unfocus(),
onSubmitted: (_) => focusNode.unfocus(),
style: Theme.of(context).textTheme.bodyMedium,
textInputAction: TextInputAction.done,
maxLines: null,
minLines: 1,
decoration: InputDecoration(
contentPadding: GridSize.cellContentInsets,
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
isDense: true,
),
),
Row(
children: [
const Spacer(),
Padding(
padding: const EdgeInsets.all(8.0),
child: SummaryCellAccessory(
viewId: bloc.cellController.viewId,
fieldId: bloc.cellController.fieldId,
rowId: bloc.cellController.rowId,
),
),
],
),
],
);
}
}

View File

@ -15,6 +15,7 @@ import 'editable_cell_skeleton/date.dart';
import 'editable_cell_skeleton/number.dart';
import 'editable_cell_skeleton/relation.dart';
import 'editable_cell_skeleton/select_option.dart';
import 'editable_cell_skeleton/summary.dart';
import 'editable_cell_skeleton/text.dart';
import 'editable_cell_skeleton/timestamp.dart';
import 'editable_cell_skeleton/url.dart';
@ -113,6 +114,12 @@ class EditableCellBuilder {
skin: IEditableRelationCellSkin.fromStyle(style),
key: key,
),
FieldType.Summary => EditableSummaryCell(
databaseController: databaseController,
cellContext: cellContext,
skin: IEditableSummaryCellSkin.fromStyle(style),
key: key,
),
_ => throw UnimplementedError(),
};
}

View File

@ -0,0 +1,249 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/summary_cell_bloc.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/summary_row_bloc.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/widgets/cell/desktop_grid/desktop_grid_summary_cell.dart';
import 'package:appflowy/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_summary_cell.dart';
import 'package:appflowy/plugins/database/widgets/cell/mobile_grid/mobile_grid_summary_cell.dart';
import 'package:appflowy/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_summary_cell.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
abstract class IEditableSummaryCellSkin {
const IEditableSummaryCellSkin();
factory IEditableSummaryCellSkin.fromStyle(EditableCellStyle style) {
return switch (style) {
EditableCellStyle.desktopGrid => DesktopGridSummaryCellSkin(),
EditableCellStyle.desktopRowDetail => DesktopRowDetailSummaryCellSkin(),
EditableCellStyle.mobileGrid => MobileGridSummaryCellSkin(),
EditableCellStyle.mobileRowDetail => MobileRowDetailSummaryCellSkin(),
};
}
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
SummaryCellBloc bloc,
FocusNode focusNode,
TextEditingController textEditingController,
);
}
class EditableSummaryCell extends EditableCellWidget {
EditableSummaryCell({
super.key,
required this.databaseController,
required this.cellContext,
required this.skin,
});
final DatabaseController databaseController;
final CellContext cellContext;
final IEditableSummaryCellSkin skin;
@override
GridEditableTextCell<EditableSummaryCell> createState() =>
_SummaryCellState();
}
class _SummaryCellState extends GridEditableTextCell<EditableSummaryCell> {
late final TextEditingController _textEditingController;
late final cellBloc = SummaryCellBloc(
cellController: makeCellController(
widget.databaseController,
widget.cellContext,
).as(),
);
@override
void initState() {
super.initState();
_textEditingController =
TextEditingController(text: cellBloc.state.content);
}
@override
void dispose() {
_textEditingController.dispose();
cellBloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: cellBloc,
child: BlocListener<SummaryCellBloc, SummaryCellState>(
listener: (context, state) {
_textEditingController.text = state.content;
},
child: Builder(
builder: (context) {
return widget.skin.build(
context,
widget.cellContainerNotifier,
cellBloc,
focusNode,
_textEditingController,
);
},
),
),
);
}
@override
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
@override
void onRequestFocus() {
focusNode.requestFocus();
}
@override
String? onCopy() => cellBloc.state.content;
@override
Future<void> focusChanged() {
if (mounted &&
!cellBloc.isClosed &&
cellBloc.state.content != _textEditingController.text.trim()) {
cellBloc
.add(SummaryCellEvent.updateCell(_textEditingController.text.trim()));
}
return super.focusChanged();
}
}
class SummaryCellAccessory extends StatelessWidget {
const SummaryCellAccessory({
required this.viewId,
required this.rowId,
required this.fieldId,
super.key,
});
final String viewId;
final String rowId;
final String fieldId;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => SummaryRowBloc(
viewId: viewId,
rowId: rowId,
fieldId: fieldId,
),
child: BlocBuilder<SummaryRowBloc, SummaryRowState>(
builder: (context, state) {
return const Row(
children: [SummaryButton(), HSpace(6), CopyButton()],
);
},
),
);
}
}
class SummaryButton extends StatelessWidget {
const SummaryButton({
super.key,
});
@override
Widget build(BuildContext context) {
return BlocBuilder<SummaryRowBloc, SummaryRowState>(
builder: (context, state) {
return state.loadingState.map(
loading: (_) {
return const Center(
child: CircularProgressIndicator.adaptive(),
);
},
finish: (_) {
return FlowyTooltip(
message: LocaleKeys.tooltip_genSummary.tr(),
child: Container(
width: 26,
height: 26,
decoration: BoxDecoration(
border: Border.fromBorderSide(
BorderSide(color: Theme.of(context).dividerColor),
),
borderRadius: Corners.s6Border,
),
child: FlowyIconButton(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
fillColor: Theme.of(context).cardColor,
icon: FlowySvg(
FlowySvgs.ai_summary_generate_s,
color: Theme.of(context).colorScheme.primary,
),
onPressed: () {
context
.read<SummaryRowBloc>()
.add(const SummaryRowEvent.startSummary());
},
),
),
);
},
);
},
);
}
}
class CopyButton extends StatelessWidget {
const CopyButton({
super.key,
});
@override
Widget build(BuildContext context) {
return BlocBuilder<SummaryCellBloc, SummaryCellState>(
builder: (blocContext, state) {
return FlowyTooltip(
message: LocaleKeys.settings_menu_clickToCopy.tr(),
child: Container(
width: 26,
height: 26,
decoration: BoxDecoration(
border: Border.fromBorderSide(
BorderSide(color: Theme.of(context).dividerColor),
),
borderRadius: Corners.s6Border,
),
child: FlowyIconButton(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
fillColor: Theme.of(context).cardColor,
icon: FlowySvg(
FlowySvgs.ai_copy_s,
color: Theme.of(context).colorScheme.primary,
),
onPressed: () {
Clipboard.setData(ClipboardData(text: state.content));
showMessageToast(LocaleKeys.grid_row_copyProperty.tr());
},
),
),
);
},
);
}
}

View File

@ -0,0 +1,79 @@
import 'package:appflowy/plugins/database/application/cell/bloc/summary_cell_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/widgets/cell/desktop_grid/desktop_grid_summary_cell.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/summary.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
class MobileGridSummaryCellSkin extends IEditableSummaryCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
SummaryCellBloc bloc,
FocusNode focusNode,
TextEditingController textEditingController,
) {
return ChangeNotifierProvider(
create: (_) => SummaryMouseNotifier(),
builder: (context, child) {
return MouseRegion(
cursor: SystemMouseCursors.click,
opaque: false,
onEnter: (p) =>
Provider.of<SummaryMouseNotifier>(context, listen: false)
.onEnter = true,
onExit: (p) =>
Provider.of<SummaryMouseNotifier>(context, listen: false)
.onEnter = false,
child: Stack(
children: [
TextField(
controller: textEditingController,
enabled: false,
focusNode: focusNode,
onEditingComplete: () => focusNode.unfocus(),
onSubmitted: (_) => focusNode.unfocus(),
style: Theme.of(context).textTheme.bodyMedium,
textInputAction: TextInputAction.done,
decoration: InputDecoration(
contentPadding: GridSize.cellContentInsets,
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
isDense: true,
),
),
Padding(
padding: EdgeInsets.symmetric(
horizontal: GridSize.cellVPadding,
),
child: Consumer<SummaryMouseNotifier>(
builder: (
BuildContext context,
SummaryMouseNotifier notifier,
Widget? child,
) {
if (notifier.onEnter) {
return SummaryCellAccessory(
viewId: bloc.cellController.viewId,
fieldId: bloc.cellController.fieldId,
rowId: bloc.cellController.rowId,
);
} else {
return const SizedBox.shrink();
}
},
),
).positioned(right: 0, bottom: 0),
],
),
);
},
);
}
}

View File

@ -0,0 +1,53 @@
import 'package:appflowy/plugins/database/application/cell/bloc/summary_cell_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/summary.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:flutter/material.dart';
class MobileRowDetailSummaryCellSkin extends IEditableSummaryCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
SummaryCellBloc bloc,
FocusNode focusNode,
TextEditingController textEditingController,
) {
return Column(
children: [
TextField(
controller: textEditingController,
focusNode: focusNode,
onEditingComplete: () => focusNode.unfocus(),
onSubmitted: (_) => focusNode.unfocus(),
style: Theme.of(context).textTheme.bodyMedium,
textInputAction: TextInputAction.done,
maxLines: null,
minLines: 1,
decoration: InputDecoration(
contentPadding: GridSize.cellContentInsets,
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
isDense: true,
),
),
Row(
children: [
const Spacer(),
Padding(
padding: const EdgeInsets.all(8.0),
child: SummaryCellAccessory(
viewId: bloc.cellController.viewId,
fieldId: bloc.cellController.fieldId,
rowId: bloc.cellController.rowId,
),
),
],
),
],
);
}
}

View File

@ -20,6 +20,7 @@ const List<FieldType> _supportedFieldTypes = [
FieldType.LastEditedTime,
FieldType.CreatedTime,
FieldType.Relation,
FieldType.Summary,
];
class FieldTypeList extends StatelessWidget with FlowyOverlayDelegate {

View File

@ -12,6 +12,7 @@ import 'number.dart';
import 'relation.dart';
import 'rich_text.dart';
import 'single_select.dart';
import 'summary.dart';
import 'timestamp.dart';
import 'url.dart';
@ -31,6 +32,7 @@ abstract class TypeOptionEditorFactory {
FieldType.Checkbox => const CheckboxTypeOptionEditorFactory(),
FieldType.Checklist => const ChecklistTypeOptionEditorFactory(),
FieldType.Relation => const RelationTypeOptionEditorFactory(),
FieldType.Summary => const SummaryTypeOptionEditorFactory(),
_ => throw UnimplementedError(),
};
}

View File

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

View File

@ -69,24 +69,32 @@ class _PrimaryCellAccessoryState extends State<PrimaryCellAccessory>
with GridCellAccessoryState {
@override
Widget build(BuildContext context) {
return FlowyTooltip(
message: LocaleKeys.tooltip_openAsPage.tr(),
child: Container(
width: 26,
height: 26,
decoration: BoxDecoration(
border: Border.fromBorderSide(
BorderSide(color: Theme.of(context).dividerColor),
),
borderRadius: Corners.s6Border,
),
child: Center(
child: FlowySvg(
FlowySvgs.full_view_s,
color: Theme.of(context).colorScheme.primary,
),
),
return FlowyHover(
style: HoverStyle(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
backgroundColor: Theme.of(context).cardColor,
),
builder: (_, onHover) {
return FlowyTooltip(
message: LocaleKeys.tooltip_openAsPage.tr(),
child: Container(
width: 26,
height: 26,
decoration: BoxDecoration(
border: Border.fromBorderSide(
BorderSide(color: Theme.of(context).dividerColor),
),
borderRadius: Corners.s6Border,
),
child: Center(
child: FlowySvg(
FlowySvgs.full_view_s,
color: Theme.of(context).colorScheme.primary,
),
),
),
);
},
);
}
@ -166,17 +174,10 @@ class CellAccessoryContainer extends StatelessWidget {
Widget build(BuildContext context) {
final children =
accessories.where((accessory) => accessory.enable()).map((accessory) {
final hover = FlowyHover(
style: HoverStyle(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
backgroundColor: Theme.of(context).cardColor,
),
builder: (_, onHover) => accessory.build(),
);
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => accessory.onTap(),
child: hover,
child: accessory.build(),
);
}).toList();