mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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 ?? "" : "";
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
@ -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),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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(),
|
||||
};
|
||||
}
|
||||
|
@ -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());
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -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),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -20,6 +20,7 @@ const List<FieldType> _supportedFieldTypes = [
|
||||
FieldType.LastEditedTime,
|
||||
FieldType.CreatedTime,
|
||||
FieldType.Relation,
|
||||
FieldType.Summary,
|
||||
];
|
||||
|
||||
class FieldTypeList extends StatelessWidget with FlowyOverlayDelegate {
|
||||
|
@ -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(),
|
||||
};
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
@ -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();
|
||||
|
||||
|
Reference in New Issue
Block a user