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

@ -0,0 +1,111 @@
import 'dart:async';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/field/field_info.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'summary_cell_bloc.freezed.dart';
class SummaryCellBloc extends Bloc<SummaryCellEvent, SummaryCellState> {
SummaryCellBloc({
required this.cellController,
}) : super(SummaryCellState.initial(cellController)) {
_dispatch();
_startListening();
}
final SummaryCellController cellController;
void Function()? _onCellChangedFn;
@override
Future<void> close() async {
if (_onCellChangedFn != null) {
cellController.removeListener(
onCellChanged: _onCellChangedFn!,
onFieldChanged: _onFieldChangedListener,
);
}
await cellController.dispose();
return super.close();
}
void _dispatch() {
on<SummaryCellEvent>(
(event, emit) async {
await event.when(
didReceiveCellUpdate: (cellData) {
emit(
state.copyWith(content: cellData ?? ""),
);
},
didUpdateField: (fieldInfo) {
final wrap = fieldInfo.wrapCellContent;
if (wrap != null) {
emit(state.copyWith(wrap: wrap));
}
},
updateCell: (text) async {
if (state.content != text) {
emit(state.copyWith(content: text));
await cellController.saveCellData(text);
// If the input content is "abc" that can't parsered as number then the data stored in the backend will be an empty string.
// So for every cell data that will be formatted in the backend.
// It needs to get the formatted data after saving.
add(
SummaryCellEvent.didReceiveCellUpdate(
cellController.getCellData() ?? "",
),
);
}
},
);
},
);
}
void _startListening() {
_onCellChangedFn = cellController.addListener(
onCellChanged: (cellContent) {
if (!isClosed) {
add(
SummaryCellEvent.didReceiveCellUpdate(cellContent ?? ""),
);
}
},
onFieldChanged: _onFieldChangedListener,
);
}
void _onFieldChangedListener(FieldInfo fieldInfo) {
if (!isClosed) {
add(SummaryCellEvent.didUpdateField(fieldInfo));
}
}
}
@freezed
class SummaryCellEvent with _$SummaryCellEvent {
const factory SummaryCellEvent.didReceiveCellUpdate(String? cellContent) =
_DidReceiveCellUpdate;
const factory SummaryCellEvent.didUpdateField(FieldInfo fieldInfo) =
_DidUpdateField;
const factory SummaryCellEvent.updateCell(String text) = _UpdateCell;
}
@freezed
class SummaryCellState with _$SummaryCellState {
const factory SummaryCellState({
required String content,
required bool wrap,
}) = _SummaryCellState;
factory SummaryCellState.initial(SummaryCellController cellController) {
final wrap = cellController.fieldInfo.wrapCellContent;
return SummaryCellState(
content: cellController.getCellData() ?? "",
wrap: wrap ?? true,
);
}
}

View File

@ -0,0 +1,99 @@
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'summary_row_bloc.freezed.dart';
class SummaryRowBloc extends Bloc<SummaryRowEvent, SummaryRowState> {
SummaryRowBloc({
required this.viewId,
required this.rowId,
required this.fieldId,
}) : super(SummaryRowState.initial()) {
_dispatch();
}
final String viewId;
final String rowId;
final String fieldId;
void _dispatch() {
on<SummaryRowEvent>(
(event, emit) async {
event.when(
startSummary: () {
final params = SummaryRowPB(
viewId: viewId,
rowId: rowId,
fieldId: fieldId,
);
emit(
state.copyWith(
loadingState: const LoadingState.loading(),
error: null,
),
);
DatabaseEventSummarizeRow(params).send().then(
(result) => {
if (!isClosed) add(SummaryRowEvent.finishSummary(result)),
},
);
},
finishSummary: (result) {
result.fold(
(s) => {
emit(
state.copyWith(
loadingState: const LoadingState.finish(),
error: null,
),
),
},
(err) => {
emit(
state.copyWith(
loadingState: const LoadingState.finish(),
error: err,
),
),
},
);
},
);
},
);
}
}
@freezed
class SummaryRowEvent with _$SummaryRowEvent {
const factory SummaryRowEvent.startSummary() = _DidStartSummary;
const factory SummaryRowEvent.finishSummary(
FlowyResult<void, FlowyError> result,
) = _DidFinishSummary;
}
@freezed
class SummaryRowState with _$SummaryRowState {
const factory SummaryRowState({
required LoadingState loadingState,
required FlowyError? error,
}) = _SummaryRowState;
factory SummaryRowState.initial() {
return const SummaryRowState(
loadingState: LoadingState.finish(),
error: null,
);
}
}
@freezed
class LoadingState with _$LoadingState {
const factory LoadingState.loading() = _Loading;
const factory LoadingState.finish() = _Finish;
}

View File

@ -105,7 +105,7 @@ class TextCellState with _$TextCellState {
factory TextCellState.initial(TextCellController cellController) {
final cellData = cellController.getCellData() ?? "";
final wrap = cellController.fieldInfo.wrapCellContent ?? false;
final wrap = cellController.fieldInfo.wrapCellContent ?? true;
final emoji =
cellController.fieldInfo.isPrimary ? cellController.icon ?? "" : "";

View File

@ -15,6 +15,7 @@ typedef DateCellController = CellController<DateCellDataPB, String>;
typedef TimestampCellController = CellController<TimestampCellDataPB, String>;
typedef URLCellController = CellController<URLCellDataPB, String>;
typedef RelationCellController = CellController<RelationCellDataPB, String>;
typedef SummaryCellController = CellController<String, String>;
CellController makeCellController(
DatabaseController databaseController,
@ -132,6 +133,18 @@ CellController makeCellController(
),
cellDataPersistence: TextCellDataPersistence(),
);
case FieldType.Summary:
return SummaryCellController(
viewId: viewId,
fieldController: fieldController,
cellContext: cellContext,
rowCache: rowCache,
cellDataLoader: CellDataLoader(
parser: StringCellDataParser(),
reloadOnFieldChange: true,
),
cellDataPersistence: TextCellDataPersistence(),
);
}
throw UnimplementedError;
}

View File

@ -51,8 +51,13 @@ class CellDataLoader<T> {
class StringCellDataParser implements CellDataParser<String> {
@override
String? parserData(List<int> data) {
final s = utf8.decode(data);
return s;
try {
final s = utf8.decode(data);
return s;
} catch (e) {
Log.error("Failed to parse string data: $e");
return null;
}
}
}
@ -62,14 +67,25 @@ class CheckboxCellDataParser implements CellDataParser<CheckboxCellDataPB> {
if (data.isEmpty) {
return null;
}
return CheckboxCellDataPB.fromBuffer(data);
try {
return CheckboxCellDataPB.fromBuffer(data);
} catch (e) {
Log.error("Failed to parse checkbox data: $e");
return null;
}
}
}
class NumberCellDataParser implements CellDataParser<String> {
@override
String? parserData(List<int> data) {
return utf8.decode(data);
try {
return utf8.decode(data);
} catch (e) {
Log.error("Failed to parse number data: $e");
return null;
}
}
}
@ -79,7 +95,12 @@ class DateCellDataParser implements CellDataParser<DateCellDataPB> {
if (data.isEmpty) {
return null;
}
return DateCellDataPB.fromBuffer(data);
try {
return DateCellDataPB.fromBuffer(data);
} catch (e) {
Log.error("Failed to parse date data: $e");
return null;
}
}
}
@ -89,7 +110,12 @@ class TimestampCellDataParser implements CellDataParser<TimestampCellDataPB> {
if (data.isEmpty) {
return null;
}
return TimestampCellDataPB.fromBuffer(data);
try {
return TimestampCellDataPB.fromBuffer(data);
} catch (e) {
Log.error("Failed to parse timestamp data: $e");
return null;
}
}
}
@ -100,7 +126,12 @@ class SelectOptionCellDataParser
if (data.isEmpty) {
return null;
}
return SelectOptionCellDataPB.fromBuffer(data);
try {
return SelectOptionCellDataPB.fromBuffer(data);
} catch (e) {
Log.error("Failed to parse select option data: $e");
return null;
}
}
}
@ -110,7 +141,13 @@ class ChecklistCellDataParser implements CellDataParser<ChecklistCellDataPB> {
if (data.isEmpty) {
return null;
}
return ChecklistCellDataPB.fromBuffer(data);
try {
return ChecklistCellDataPB.fromBuffer(data);
} catch (e) {
Log.error("Failed to parse checklist data: $e");
return null;
}
}
}
@ -120,13 +157,27 @@ class URLCellDataParser implements CellDataParser<URLCellDataPB> {
if (data.isEmpty) {
return null;
}
return URLCellDataPB.fromBuffer(data);
try {
return URLCellDataPB.fromBuffer(data);
} catch (e) {
Log.error("Failed to parse url data: $e");
return null;
}
}
}
class RelationCellDataParser implements CellDataParser<RelationCellDataPB> {
@override
RelationCellDataPB? parserData(List<int> data) {
return data.isEmpty ? null : RelationCellDataPB.fromBuffer(data);
if (data.isEmpty) {
return null;
}
try {
return RelationCellDataPB.fromBuffer(data);
} catch (e) {
Log.error("Failed to parse relation data: $e");
return null;
}
}
}

View File

@ -35,7 +35,7 @@ class GridRow extends StatefulWidget {
});
final FieldController fieldController;
final RowId viewId;
final String viewId;
final RowId rowId;
final RowController rowController;
final EditableCellBuilder cellBuilder;

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();