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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
83 changed files with 1802 additions and 628 deletions

View File

@ -96,7 +96,7 @@ jobs:
af_cloud_test_ws_url: ws://localhost/ws/v1
af_cloud_test_gotrue_url: http://localhost/gotrue
run: |
DISABLE_CI_TEST_LOG="true" cargo test --no-default-features --features="dart" -- --nocapture
DISABLE_CI_TEST_LOG="true" cargo test --no-default-features --features="dart"
- name: rustfmt rust-lib
run: cargo fmt --all -- --check

View File

@ -165,7 +165,7 @@ SPEC CHECKSUMS:
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c
image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb
image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425
integration_test: 13825b8a9334a850581300559b8839134b124670
@ -185,4 +185,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: d94f9be27d1db182e9bc77d10f065555d518f127
COCOAPODS: 1.15.2
COCOAPODS: 1.11.3

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

View File

@ -21,6 +21,7 @@ extension FieldTypeExtension on FieldType {
LocaleKeys.grid_field_updatedAtFieldName.tr(),
FieldType.CreatedTime => LocaleKeys.grid_field_createdAtFieldName.tr(),
FieldType.Relation => LocaleKeys.grid_field_relationFieldName.tr(),
FieldType.Summary => LocaleKeys.grid_field_summaryFieldName.tr(),
_ => throw UnimplementedError(),
};
@ -36,6 +37,7 @@ extension FieldTypeExtension on FieldType {
FieldType.LastEditedTime => FlowySvgs.last_modified_s,
FieldType.CreatedTime => FlowySvgs.created_at_s,
FieldType.Relation => FlowySvgs.relation_s,
FieldType.Summary => FlowySvgs.ai_summary_s,
_ => throw UnimplementedError(),
};
@ -51,6 +53,7 @@ extension FieldTypeExtension on FieldType {
FieldType.Checkbox => const Color(0xFF98F4CD),
FieldType.Checklist => const Color(0xFF98F4CD),
FieldType.Relation => const Color(0xFFFDEDA7),
FieldType.Summary => const Color(0xFFBECCFF),
_ => throw UnimplementedError(),
};
@ -67,6 +70,7 @@ extension FieldTypeExtension on FieldType {
FieldType.Checkbox => const Color(0xFF42AD93),
FieldType.Checklist => const Color(0xFF42AD93),
FieldType.Relation => const Color(0xFFFDEDA7),
FieldType.Summary => const Color(0xFF6859A7),
_ => throw UnimplementedError(),
};
}

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.6663 0.666504H2.66634C1.93301 0.666504 1.33301 1.2665 1.33301 1.99984V11.3332H2.66634V1.99984H10.6663V0.666504ZM9.99967 3.33317H5.33301C4.59967 3.33317 4.00634 3.93317 4.00634 4.6665L3.99967 13.9998C3.99967 14.7332 4.59301 15.3332 5.32634 15.3332H12.6663C13.3997 15.3332 13.9997 14.7332 13.9997 13.9998V7.33317L9.99967 3.33317ZM5.33301 13.9998V4.6665H9.33301V7.99984H12.6663V13.9998H5.33301Z" fill="#1F2329"/>
</svg>

After

Width:  |  Height:  |  Size: 526 B

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.44667 3.33333L12.6667 6.55333V12.6667H3.33333V3.33333H9.44667ZM9.44667 2H3.33333C2.6 2 2 2.6 2 3.33333V12.6667C2 13.4 2.6 14 3.33333 14H12.6667C13.4 14 14 13.4 14 12.6667V6.55333C14 6.2 13.86 5.86 13.6067 5.61333L10.3867 2.39333C10.14 2.14 9.8 2 9.44667 2ZM4.66667 10H11.3333V11.3333H4.66667V10ZM4.66667 7.33333H11.3333V8.66667H4.66667V7.33333ZM4.66667 4.66667H9.33333V6H4.66667V4.66667Z" fill="#8F959E"/>
</svg>

After

Width:  |  Height:  |  Size: 521 B

View File

@ -0,0 +1,3 @@
<svg width="19" height="18" viewBox="0 0 19 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.87687 3.99L11.0019 4.5L10.4919 3.375L11.0019 2.25L9.87687 2.76L8.75187 2.25L9.26188 3.375L8.75187 4.5L9.87687 3.99ZM14.6619 4.47L16.2519 3.75L15.5319 5.34L16.2519 6.93L14.6619 6.21L13.0719 6.93L13.7919 5.34L13.0719 3.75L14.6619 4.47ZM8.87937 6.6225C9.70437 5.79 11.0469 5.79 11.8794 6.6225C12.7119 7.4475 12.7119 8.79 11.8794 9.6225L6.37437 15.1275C5.54938 15.96 4.20687 15.96 3.37437 15.1275C2.54187 14.3025 2.54187 12.96 3.37437 12.1275L8.87937 6.6225ZM9.32938 10.5825L11.0919 8.82C11.4819 8.43 11.4819 7.8 11.0919 7.41C10.7019 7.02 10.0719 7.02 9.68188 7.41L7.91937 9.1725C7.52938 9.5625 7.52938 10.1925 7.91937 10.5825C8.30937 10.9725 8.93937 10.9725 9.32938 10.5825ZM11.7519 11.25L14.0019 12.27L16.2519 11.25L15.2319 13.5L16.2519 15.75L14.0019 14.73L11.7519 15.75L12.7719 13.5L11.7519 11.25Z" fill="#750D7E"/>
</svg>

After

Width:  |  Height:  |  Size: 970 B

View File

@ -222,7 +222,8 @@
"dragRow": "Long press to reorder the row",
"viewDataBase": "View database",
"referencePage": "This {name} is referenced",
"addBlockBelow": "Add a block below"
"addBlockBelow": "Add a block below",
"genSummary": "Generate summary"
},
"sideBar": {
"closeSidebar": "Close side bar",
@ -710,6 +711,7 @@
"urlFieldName": "URL",
"checklistFieldName": "Checklist",
"relationFieldName": "Relation",
"summaryFieldName": "AI Summary",
"numberFormat": "Number format",
"dateFormat": "Date format",
"includeTime": "Include time",
@ -783,7 +785,8 @@
"drag": "Drag to move",
"dragAndClick": "Drag to move, click to open menu",
"insertRecordAbove": "Insert record above",
"insertRecordBelow": "Insert record below"
"insertRecordBelow": "Insert record below",
"noContent": "No content"
},
"selectOption": {
"create": "Create",
@ -1541,4 +1544,4 @@
"betaTooltip": "We currently only support searching for pages",
"fromTrashHint": "From trash"
}
}
}

View File

@ -202,38 +202,6 @@ dependencies = [
"serde_json",
]
[[package]]
name = "async-convert"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d416feee97712e43152cd42874de162b8f9b77295b1c85e5d92725cc8310bae"
dependencies = [
"async-trait",
]
[[package]]
name = "async-openai"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7150fb5d9cc4eb0184af43ce75a89620dc3747d3c816e8b0ba200682d0155c05"
dependencies = [
"async-convert",
"backoff",
"base64 0.21.5",
"derive_builder",
"futures",
"rand 0.8.5",
"reqwest",
"reqwest-eventsource",
"serde",
"serde_json",
"thiserror",
"tokio",
"tokio-stream",
"tokio-util",
"tracing",
]
[[package]]
name = "async-stream"
version = "0.3.5"
@ -330,20 +298,6 @@ dependencies = [
"tower-service",
]
[[package]]
name = "backoff"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1"
dependencies = [
"futures-core",
"getrandom 0.2.10",
"instant",
"pin-project-lite",
"rand 0.8.5",
"tokio",
]
[[package]]
name = "backtrace"
version = "0.3.69"
@ -1236,41 +1190,6 @@ dependencies = [
"cipher",
]
[[package]]
name = "darling"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim 0.10.0",
"syn 1.0.109",
]
[[package]]
name = "darling_macro"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e"
dependencies = [
"darling_core",
"quote",
"syn 1.0.109",
]
[[package]]
name = "dart-ffi"
version = "0.1.0"
@ -1384,37 +1303,6 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "derive_builder"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8"
dependencies = [
"derive_builder_macro",
]
[[package]]
name = "derive_builder_core"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "derive_builder_macro"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e"
dependencies = [
"derive_builder_core",
"syn 1.0.109",
]
[[package]]
name = "derive_more"
version = "0.99.17"
@ -1620,6 +1508,7 @@ dependencies = [
"rand 0.8.5",
"serde",
"serde_json",
"strum",
"tempdir",
"thread-id",
"tokio",
@ -1630,17 +1519,6 @@ dependencies = [
"zip",
]
[[package]]
name = "eventsource-stream"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74fef4569247a5f429d9156b9d0a2599914385dd189c539334c625d8099d90ab"
dependencies = [
"futures-core",
"nom",
"pin-project-lite",
]
[[package]]
name = "faccess"
version = "0.2.4"
@ -1734,20 +1612,6 @@ dependencies = [
"miniz_oxide",
]
[[package]]
name = "flowy-ai"
version = "0.1.0"
dependencies = [
"anyhow",
"async-openai",
"dotenv",
"lib-infra",
"reqwest",
"serde",
"serde_json",
"tokio",
]
[[package]]
name = "flowy-ast"
version = "0.1.0"
@ -2112,7 +1976,7 @@ dependencies = [
"protobuf",
"serde",
"serde_json",
"strsim 0.11.0",
"strsim",
"strum_macros 0.26.1",
"tantivy",
"tempfile",
@ -2436,12 +2300,6 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
[[package]]
name = "futures-timer"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c"
[[package]]
name = "futures-util"
version = "0.3.30"
@ -2887,12 +2745,6 @@ dependencies = [
"cc",
]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "0.3.0"
@ -4626,7 +4478,6 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"rustls",
"rustls-native-certs",
"rustls-pemfile",
"serde",
"serde_json",
@ -4647,22 +4498,6 @@ dependencies = [
"winreg",
]
[[package]]
name = "reqwest-eventsource"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f03f570355882dd8d15acc3a313841e6e90eddbc76a93c748fd82cc13ba9f51"
dependencies = [
"eventsource-stream",
"futures-core",
"futures-timer",
"mime",
"nom",
"pin-project-lite",
"reqwest",
"thiserror",
]
[[package]]
name = "ring"
version = "0.16.20"
@ -4788,18 +4623,6 @@ dependencies = [
"sct",
]
[[package]]
name = "rustls-native-certs"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
dependencies = [
"openssl-probe",
"rustls-pemfile",
"schannel",
"security-framework",
]
[[package]]
name = "rustls-pemfile"
version = "1.0.3"
@ -5291,12 +5114,6 @@ dependencies = [
"unicode-normalization",
]
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strsim"
version = "0.11.0"

View File

@ -22,7 +22,6 @@ members = [
"flowy-encrypt",
"flowy-storage",
"collab-integrate",
"flowy-ai",
"flowy-date",
"flowy-search",
"lib-infra",
@ -61,7 +60,6 @@ flowy-storage = { workspace = true, path = "flowy-storage" }
flowy-search = { workspace = true, path = "flowy-search" }
flowy-search-pub = { workspace = true, path = "flowy-search-pub" }
collab-integrate = { workspace = true, path = "collab-integrate" }
flowy-ai = { workspace = true, path = "flowy-ai" }
flowy-date = { workspace = true, path = "flowy-date" }
anyhow = "1.0"
tracing = "0.1.40"
@ -84,6 +82,7 @@ yrs = { version = "0.17.2" }
opt-level = 1
lto = false
codegen-units = 16
debug = true
[profile.release]
lto = true

View File

@ -43,6 +43,7 @@ collab-database = { version = "0.1.0" }
collab-plugins = { version = "0.1.0" }
collab-entity = { version = "0.1.0" }
rand = { version = "0.8.5", features = [] }
strum = "0.25.0"
[dev-dependencies]
dotenv = "0.15.0"

View File

@ -2,8 +2,15 @@ use std::collections::HashMap;
use std::convert::TryFrom;
use bytes::Bytes;
use collab_database::database::timestamp;
use collab_database::fields::Field;
use collab_database::rows::{Row, RowId};
use flowy_database2::entities::*;
use flowy_database2::event_map::DatabaseEvent;
use flowy_database2::services::cell::CellBuilder;
use flowy_database2::services::field::{
MultiSelectTypeOption, SelectOption, SingleSelectTypeOption,
};
use flowy_database2::services::share::csv::CSVFormat;
use flowy_folder::entities::*;
use flowy_folder::event_map::FolderEvent;
@ -25,6 +32,7 @@ impl EventIntegrationTest {
.unwrap()
}
/// The initial data can refer to the [FolderOperationHandler::create_view_with_view_data] method.
pub async fn create_grid(&self, parent_id: &str, name: String, initial_data: Vec<u8>) -> ViewPB {
let payload = CreateViewPayloadPB {
parent_view_id: parent_id.to_string(),
@ -199,6 +207,13 @@ impl EventIntegrationTest {
.await
.parse::<FieldPB>()
}
pub async fn summary_row(&self, data: SummaryRowPB) {
EventBuilder::new(self.clone())
.event(DatabaseEvent::SummarizeRow)
.payload(data)
.async_send()
.await;
}
pub async fn create_row(
&self,
@ -324,6 +339,11 @@ impl EventIntegrationTest {
.parse::<CellPB>()
}
pub async fn get_text_cell(&self, view_id: &str, row_id: &str, field_id: &str) -> String {
let cell = self.get_cell(view_id, row_id, field_id).await;
String::from_utf8(cell.data).unwrap()
}
pub async fn get_date_cell(&self, view_id: &str, row_id: &str, field_id: &str) -> DateCellDataPB {
let cell = self.get_cell(view_id, row_id, field_id).await;
DateCellDataPB::try_from(Bytes::from(cell.data)).unwrap()
@ -513,3 +533,139 @@ impl EventIntegrationTest {
.rows
}
}
pub struct TestRowBuilder<'a> {
database_id: &'a str,
row_id: RowId,
fields: &'a [Field],
cell_build: CellBuilder<'a>,
}
impl<'a> TestRowBuilder<'a> {
pub fn new(database_id: &'a str, row_id: RowId, fields: &'a [Field]) -> Self {
let cell_build = CellBuilder::with_cells(Default::default(), fields);
Self {
database_id,
row_id,
fields,
cell_build,
}
}
pub fn insert_text_cell(&mut self, data: &str) -> String {
let text_field = self.field_with_type(&FieldType::RichText);
self
.cell_build
.insert_text_cell(&text_field.id, data.to_string());
text_field.id.clone()
}
pub fn insert_number_cell(&mut self, data: &str) -> String {
let number_field = self.field_with_type(&FieldType::Number);
self
.cell_build
.insert_text_cell(&number_field.id, data.to_string());
number_field.id.clone()
}
pub fn insert_date_cell(
&mut self,
date: i64,
time: Option<String>,
include_time: Option<bool>,
field_type: &FieldType,
) -> String {
let date_field = self.field_with_type(field_type);
self
.cell_build
.insert_date_cell(&date_field.id, date, time, include_time);
date_field.id.clone()
}
pub fn insert_checkbox_cell(&mut self, data: &str) -> String {
let checkbox_field = self.field_with_type(&FieldType::Checkbox);
self
.cell_build
.insert_text_cell(&checkbox_field.id, data.to_string());
checkbox_field.id.clone()
}
pub fn insert_url_cell(&mut self, content: &str) -> String {
let url_field = self.field_with_type(&FieldType::URL);
self
.cell_build
.insert_url_cell(&url_field.id, content.to_string());
url_field.id.clone()
}
pub fn insert_single_select_cell<F>(&mut self, f: F) -> String
where
F: Fn(Vec<SelectOption>) -> SelectOption,
{
let single_select_field = self.field_with_type(&FieldType::SingleSelect);
let type_option = single_select_field
.get_type_option::<SingleSelectTypeOption>(FieldType::SingleSelect)
.unwrap();
let option = f(type_option.options);
self
.cell_build
.insert_select_option_cell(&single_select_field.id, vec![option.id]);
single_select_field.id.clone()
}
pub fn insert_multi_select_cell<F>(&mut self, f: F) -> String
where
F: Fn(Vec<SelectOption>) -> Vec<SelectOption>,
{
let multi_select_field = self.field_with_type(&FieldType::MultiSelect);
let type_option = multi_select_field
.get_type_option::<MultiSelectTypeOption>(FieldType::MultiSelect)
.unwrap();
let options = f(type_option.options);
let ops_ids = options
.iter()
.map(|option| option.id.clone())
.collect::<Vec<_>>();
self
.cell_build
.insert_select_option_cell(&multi_select_field.id, ops_ids);
multi_select_field.id.clone()
}
pub fn insert_checklist_cell(&mut self, options: Vec<(String, bool)>) -> String {
let checklist_field = self.field_with_type(&FieldType::Checklist);
self
.cell_build
.insert_checklist_cell(&checklist_field.id, options);
checklist_field.id.clone()
}
pub fn field_with_type(&self, field_type: &FieldType) -> Field {
self
.fields
.iter()
.find(|field| {
let t_field_type = FieldType::from(field.field_type);
&t_field_type == field_type
})
.unwrap()
.clone()
}
pub fn build(self) -> Row {
let timestamp = timestamp();
Row {
id: self.row_id,
database_id: self.database_id.to_string(),
cells: self.cell_build.build(),
height: 60,
visibility: true,
modified_at: timestamp,
created_at: timestamp,
}
}
}

View File

@ -0,0 +1,2 @@
// mod summarize_row;
mod util;

View File

@ -0,0 +1,48 @@
use crate::database::af_cloud::util::make_test_summary_grid;
use std::time::Duration;
use tokio::time::sleep;
use event_integration_test::user_event::user_localhost_af_cloud;
use event_integration_test::EventIntegrationTest;
use flowy_database2::entities::{FieldType, SummaryRowPB};
#[tokio::test]
async fn af_cloud_summarize_row_test() {
user_localhost_af_cloud().await;
let test = EventIntegrationTest::new().await;
test.af_cloud_sign_up().await;
// create document and then insert content
let current_workspace = test.get_current_workspace().await;
let initial_data = make_test_summary_grid().to_json_bytes().unwrap();
let view = test
.create_grid(
&current_workspace.id,
"summary database".to_string(),
initial_data,
)
.await;
let database_pb = test.get_database(&view.id).await;
let field = test
.get_all_database_fields(&view.id)
.await
.items
.into_iter()
.find(|field| field.field_type == FieldType::Summary)
.unwrap();
assert_eq!(database_pb.rows.len(), 4);
let row_id = database_pb.rows[0].id.clone();
let data = SummaryRowPB {
view_id: view.id.clone(),
row_id: row_id.clone(),
field_id: field.id.clone(),
};
test.summary_row(data).await;
sleep(Duration::from_secs(1)).await;
let cell = test.get_text_cell(&view.id, &row_id, &field.id).await;
// should be something like this: The product "Apple" was completed at a price of $2.60.
assert!(cell.contains("Apple"), "cell: {}", cell);
}

View File

@ -0,0 +1,126 @@
use collab_database::database::{gen_database_id, gen_database_view_id, gen_row_id, DatabaseData};
use collab_database::views::{DatabaseLayout, DatabaseView};
use event_integration_test::database_event::TestRowBuilder;
use collab_database::fields::Field;
use collab_database::rows::Row;
use flowy_database2::entities::FieldType;
use flowy_database2::services::field::summary_type_option::summary::SummarizationTypeOption;
use flowy_database2::services::field::{
FieldBuilder, NumberFormat, NumberTypeOption, SelectOption, SelectOptionColor,
SingleSelectTypeOption,
};
use flowy_database2::services::field_settings::default_field_settings_for_fields;
use strum::IntoEnumIterator;
#[allow(dead_code)]
pub fn make_test_summary_grid() -> DatabaseData {
let database_id = gen_database_id();
let fields = create_fields();
let field_settings = default_field_settings_for_fields(&fields, DatabaseLayout::Grid);
let single_select_field = fields
.iter()
.find(|field| field.field_type == FieldType::SingleSelect.value())
.unwrap();
let options = single_select_field
.type_options
.get(&FieldType::SingleSelect.to_string())
.cloned()
.map(|t| SingleSelectTypeOption::from(t).options)
.unwrap();
let rows = create_rows(&database_id, &fields, options);
let inline_view_id = gen_database_view_id();
let view = DatabaseView {
database_id: database_id.clone(),
id: inline_view_id.clone(),
name: "".to_string(),
layout: DatabaseLayout::Grid,
field_settings,
..Default::default()
};
DatabaseData {
database_id,
inline_view_id,
views: vec![view],
fields,
rows,
}
}
#[allow(dead_code)]
fn create_fields() -> Vec<Field> {
let mut fields = Vec::new();
for field_type in FieldType::iter() {
match field_type {
FieldType::RichText => fields.push(create_text_field("Product Name", true)),
FieldType::Number => fields.push(create_number_field("Price", NumberFormat::USD)),
FieldType::SingleSelect => fields.push(create_single_select_field("Status")),
FieldType::Summary => fields.push(create_summary_field("AI summary")),
_ => {},
}
}
fields
}
#[allow(dead_code)]
fn create_rows(database_id: &str, fields: &[Field], _options: Vec<SelectOption>) -> Vec<Row> {
let mut rows = Vec::new();
let fruits = ["Apple", "Pear", "Banana", "Orange"];
for (i, fruit) in fruits.iter().enumerate() {
let mut row_builder = TestRowBuilder::new(database_id, gen_row_id(), fields);
row_builder.insert_text_cell(fruit);
row_builder.insert_number_cell(match i {
0 => "2.6",
1 => "1.6",
2 => "3.6",
_ => "1.2",
});
row_builder.insert_single_select_cell(|mut options| options.remove(i % options.len()));
rows.push(row_builder.build());
}
rows
}
#[allow(dead_code)]
fn create_text_field(name: &str, primary: bool) -> Field {
FieldBuilder::from_field_type(FieldType::RichText)
.name(name)
.primary(primary)
.build()
}
#[allow(dead_code)]
fn create_number_field(name: &str, format: NumberFormat) -> Field {
let mut type_option = NumberTypeOption::default();
type_option.set_format(format);
FieldBuilder::new(FieldType::Number, type_option)
.name(name)
.build()
}
#[allow(dead_code)]
fn create_single_select_field(name: &str) -> Field {
let options = vec![
SelectOption::with_color("COMPLETED", SelectOptionColor::Purple),
SelectOption::with_color("PLANNED", SelectOptionColor::Orange),
SelectOption::with_color("PAUSED", SelectOptionColor::Yellow),
];
let mut type_option = SingleSelectTypeOption::default();
type_option.options.extend(options);
FieldBuilder::new(FieldType::SingleSelect, type_option)
.name(name)
.build()
}
#[allow(dead_code)]
fn create_summary_field(name: &str) -> Field {
let type_option = SummarizationTypeOption { auto_fill: false };
FieldBuilder::new(FieldType::Summary, type_option)
.name(name)
.build()
}

View File

@ -1,3 +1,4 @@
mod af_cloud;
mod local_test;
// #[cfg(feature = "supabase_cloud_test")]

View File

@ -1,18 +0,0 @@
[package]
name = "flowy-ai"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
reqwest = { version = "0.11", features = ["json"] }
serde.workspace = true
serde_json.workspace = true
anyhow.workspace = true
lib-infra = { workspace = true }
async-openai = "0.14.2"
tokio = { workspace = true, features = ["rt", "sync"] }
[dev-dependencies]
dotenv = "0.15.0"

View File

@ -1,16 +0,0 @@
use anyhow::{anyhow, Error};
pub struct OpenAISetting {
pub openai_api_key: String,
}
const OPENAI_API_KEY: &str = "OPENAI_API_KEY";
impl OpenAISetting {
pub fn from_env() -> Result<Self, Error> {
let openai_api_key =
std::env::var(OPENAI_API_KEY).map_err(|_| anyhow!("Missing OPENAI_API_KEY"))?;
Ok(Self { openai_api_key })
}
}

View File

@ -1,2 +0,0 @@
pub mod config;
pub mod text;

View File

@ -1,14 +0,0 @@
use anyhow::Error;
use lib_infra::async_trait::async_trait;
mod entities;
pub mod open_ai;
pub mod stability_ai;
#[async_trait]
pub trait TextCompletion: Send + Sync {
type Input: Send + 'static;
type Output;
async fn text_completion(&self, params: Self::Input) -> Result<Self::Output, Error>;
}

View File

@ -1,30 +0,0 @@
use crate::text::TextCompletion;
use anyhow::Error;
use async_openai::config::OpenAIConfig;
use async_openai::types::{CreateCompletionRequest, CreateCompletionResponse};
use async_openai::Client;
use lib_infra::async_trait::async_trait;
pub struct OpenAITextCompletion {
client: Client<OpenAIConfig>,
}
impl OpenAITextCompletion {
pub fn new(api_key: &str) -> Self {
// https://docs.rs/async-openai/latest/async_openai/struct.Completions.html
let config = OpenAIConfig::new().with_api_key(api_key);
let client = Client::with_config(config);
Self { client }
}
}
#[async_trait]
impl TextCompletion for OpenAITextCompletion {
type Input = CreateCompletionRequest;
type Output = CreateCompletionResponse;
async fn text_completion(&self, params: Self::Input) -> Result<Self::Output, Error> {
let response = self.client.completions().create(params).await?;
Ok(response)
}
}

View File

@ -1,15 +0,0 @@
use crate::text::TextCompletion;
use anyhow::Error;
use lib_infra::async_trait::async_trait;
pub struct StabilityAITextCompletion {}
#[async_trait]
impl TextCompletion for StabilityAITextCompletion {
type Input = ();
type Output = ();
async fn text_completion(&self, _params: Self::Input) -> Result<Self::Output, Error> {
todo!()
}
}

View File

@ -1,2 +0,0 @@
mod text;
mod util;

View File

@ -1,18 +0,0 @@
use crate::util::get_openai_config;
use async_openai::types::CreateCompletionRequestArgs;
use flowy_ai::text::open_ai::OpenAITextCompletion;
use flowy_ai::text::TextCompletion;
#[tokio::test]
async fn text_completion_test() {
if let Some(config) = get_openai_config() {
let client = OpenAITextCompletion::new(&config.openai_api_key);
let params = CreateCompletionRequestArgs::default()
.model("text-davinci-003")
.prompt("Write a rust function to calculate the sum of two numbers")
.build()
.unwrap();
let resp = client.text_completion(params).await.unwrap();
dbg!("{:?}", resp);
}
}

View File

@ -1 +0,0 @@
// mod completion_test;

View File

@ -1,9 +0,0 @@
use flowy_ai::config::OpenAISetting;
// To run the OpenAI test, you need to create a .env file in the flowy-ai folder.
// Use the format: OPENAI_API_KEY=your_api_key
#[allow(dead_code)]
pub fn get_openai_config() -> Option<OpenAISetting> {
dotenv::from_filename(".env").ok()?;
OpenAISetting::from_env().ok()
}

View File

@ -55,7 +55,7 @@ impl UserWorkspaceService for UserWorkspaceServiceImpl {
) -> FlowyResult<()> {
self
.database_manager
.track_database(ids_by_database_id)
.update_database_indexing(ids_by_database_id)
.await?;
Ok(())
}

View File

@ -14,7 +14,9 @@ use tracing::debug;
use collab_integrate::collab_builder::{
CollabCloudPluginProvider, CollabPluginProviderContext, CollabPluginProviderType,
};
use flowy_database_pub::cloud::{CollabDocStateByOid, DatabaseCloudService, DatabaseSnapshot};
use flowy_database_pub::cloud::{
CollabDocStateByOid, DatabaseCloudService, DatabaseSnapshot, SummaryRowContent,
};
use flowy_document::deps::DocumentData;
use flowy_document_pub::cloud::{DocumentCloudService, DocumentSnapshot};
use flowy_error::FlowyError;
@ -267,6 +269,23 @@ impl DatabaseCloudService for ServerProvider {
.await
})
}
fn summary_database_row(
&self,
workspace_id: &str,
object_id: &str,
summary_row: SummaryRowContent,
) -> FutureResult<String, Error> {
let workspace_id = workspace_id.to_string();
let server = self.get_server();
let object_id = object_id.to_string();
FutureResult::new(async move {
server?
.database_service()
.summary_database_row(&workspace_id, &object_id, summary_row)
.await
})
}
}
impl DocumentCloudService for ServerProvider {

View File

@ -5,7 +5,7 @@ use lib_infra::future::FutureResult;
use std::collections::HashMap;
pub type CollabDocStateByOid = HashMap<String, DataSource>;
pub type SummaryRowContent = HashMap<String, String>;
/// A trait for database cloud service.
/// Each kind of server should implement this trait. Check out the [AppFlowyServerProvider] of
/// [flowy-server] crate for more information.
@ -32,6 +32,13 @@ pub trait DatabaseCloudService: Send + Sync {
object_id: &str,
limit: usize,
) -> FutureResult<Vec<DatabaseSnapshot>, Error>;
fn summary_database_row(
&self,
workspace_id: &str,
object_id: &str,
summary_row: SummaryRowContent,
) -> FutureResult<String, Error>;
}
pub struct DatabaseSnapshot {

View File

@ -448,6 +448,7 @@ pub enum FieldType {
LastEditedTime = 8,
CreatedTime = 9,
Relation = 10,
Summary = 11,
}
impl Display for FieldType {
@ -487,6 +488,7 @@ impl FieldType {
FieldType::LastEditedTime => "Last modified",
FieldType::CreatedTime => "Created time",
FieldType::Relation => "Relation",
FieldType::Summary => "Summarize",
};
s.to_string()
}

View File

@ -101,11 +101,14 @@ impl From<&Filter> for FilterPB {
.cloned::<CheckboxFilterPB>()
.unwrap()
.try_into(),
FieldType::Relation => condition_and_content
.cloned::<RelationFilterPB>()
.unwrap()
.try_into(),
FieldType::Summary => condition_and_content
.cloned::<TextFilterPB>()
.unwrap()
.try_into(),
};
Self {
@ -150,6 +153,9 @@ impl TryFrom<FilterDataPB> for FilterInner {
FieldType::Relation => {
BoxAny::new(RelationFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?)
},
FieldType::Summary => {
BoxAny::new(TextFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?)
},
};
Ok(Self::Data {

View File

@ -15,6 +15,7 @@ macro_rules! impl_into_field_type {
8 => FieldType::LastEditedTime,
9 => FieldType::CreatedTime,
10 => FieldType::Relation,
11 => FieldType::Summary,
_ => {
tracing::error!("🔴Can't parse FieldType from value: {}", ty);
FieldType::RichText

View File

@ -359,3 +359,15 @@ pub struct CreateRowParams {
pub collab_params: collab_database::rows::CreateRowParams,
pub open_after_create: bool,
}
#[derive(Debug, Default, Clone, ProtoBuf)]
pub struct SummaryRowPB {
#[pb(index = 1)]
pub view_id: String,
#[pb(index = 2)]
pub row_id: String,
#[pb(index = 3)]
pub field_id: String,
}

View File

@ -4,6 +4,7 @@ mod date_entities;
mod number_entities;
mod relation_entities;
mod select_option_entities;
mod summary_entities;
mod text_entities;
mod timestamp_entities;
mod url_entities;
@ -14,6 +15,7 @@ pub use date_entities::*;
pub use number_entities::*;
pub use relation_entities::*;
pub use select_option_entities::*;
pub use summary_entities::*;
pub use text_entities::*;
pub use timestamp_entities::*;
pub use url_entities::*;

View File

@ -0,0 +1,24 @@
use crate::services::field::summary_type_option::summary::SummarizationTypeOption;
use flowy_derive::ProtoBuf;
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct SummarizationTypeOptionPB {
#[pb(index = 1)]
pub auto_fill: bool,
}
impl From<SummarizationTypeOption> for SummarizationTypeOptionPB {
fn from(value: SummarizationTypeOption) -> Self {
SummarizationTypeOptionPB {
auto_fill: value.auto_fill,
}
}
}
impl From<SummarizationTypeOptionPB> for SummarizationTypeOption {
fn from(value: SummarizationTypeOptionPB) -> Self {
SummarizationTypeOption {
auto_fill: value.auto_fill,
}
}
}

View File

@ -465,7 +465,7 @@ pub(crate) async fn update_cell_handler(
database_editor
.update_cell_with_changeset(
&params.view_id,
RowId::from(params.row_id),
&RowId::from(params.row_id),
&params.field_id,
BoxAny::new(params.cell_changeset),
)
@ -548,7 +548,7 @@ pub(crate) async fn update_select_option_cell_handler(
database_editor
.update_cell_with_changeset(
&params.cell_identifier.view_id,
params.cell_identifier.row_id,
&params.cell_identifier.row_id,
&params.cell_identifier.field_id,
BoxAny::new(changeset),
)
@ -577,7 +577,7 @@ pub(crate) async fn update_checklist_cell_handler(
database_editor
.update_cell_with_changeset(
&params.view_id,
params.row_id,
&params.row_id,
&params.field_id,
BoxAny::new(changeset),
)
@ -608,7 +608,7 @@ pub(crate) async fn update_date_cell_handler(
database_editor
.update_cell_with_changeset(
&cell_id.view_id,
cell_id.row_id,
&cell_id.row_id,
&cell_id.field_id,
BoxAny::new(cell_changeset),
)
@ -868,7 +868,7 @@ pub(crate) async fn move_calendar_event_handler(
database_editor
.update_cell_with_changeset(
&cell_id.view_id,
cell_id.row_id,
&cell_id.row_id,
&cell_id.field_id,
BoxAny::new(cell_changeset),
)
@ -1053,7 +1053,7 @@ pub(crate) async fn update_relation_cell_handler(
database_editor
.update_cell_with_changeset(
&view_id,
cell_id.row_id,
&cell_id.row_id,
&cell_id.field_id,
BoxAny::new(params),
)
@ -1086,3 +1086,16 @@ pub(crate) async fn get_related_database_rows_handler(
data_result_ok(RepeatedRelatedRowDataPB { rows: row_datas })
}
pub(crate) async fn summarize_row_handler(
data: AFPluginData<SummaryRowPB>,
manager: AFPluginState<Weak<DatabaseManager>>,
) -> Result<(), FlowyError> {
let manager = upgrade_manager(manager)?;
let data = data.into_inner();
let row_id = RowId::from(data.row_id);
manager
.summarize_row(data.view_id, row_id, data.field_id)
.await?;
Ok(())
}

View File

@ -84,11 +84,13 @@ pub fn init(database_manager: Weak<DatabaseManager>) -> AFPlugin {
.event(DatabaseEvent::GetAllCalculations, get_all_calculations_handler)
.event(DatabaseEvent::UpdateCalculation, update_calculation_handler)
.event(DatabaseEvent::RemoveCalculation, remove_calculation_handler)
// Relation
.event(DatabaseEvent::GetRelatedDatabaseIds, get_related_database_ids_handler)
.event(DatabaseEvent::UpdateRelationCell, update_relation_cell_handler)
.event(DatabaseEvent::GetRelatedRowDatas, get_related_row_datas_handler)
.event(DatabaseEvent::GetRelatedDatabaseRows, get_related_database_rows_handler)
// Relation
.event(DatabaseEvent::GetRelatedDatabaseIds, get_related_database_ids_handler)
.event(DatabaseEvent::UpdateRelationCell, update_relation_cell_handler)
.event(DatabaseEvent::GetRelatedRowDatas, get_related_row_datas_handler)
.event(DatabaseEvent::GetRelatedDatabaseRows, get_related_database_rows_handler)
// AI
.event(DatabaseEvent::SummarizeRow, summarize_row_handler)
}
/// [DatabaseEvent] defines events that are used to interact with the Grid. You could check [this](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/backend/protobuf)
@ -368,4 +370,7 @@ pub enum DatabaseEvent {
/// Get the names of all the rows in a related database.
#[event(input = "DatabaseIdPB", output = "RepeatedRelatedRowDataPB")]
GetRelatedDatabaseRows = 173,
#[event(input = "SummaryRowPB")]
SummarizeRow = 174,
}

View File

@ -5,6 +5,7 @@ use std::sync::{Arc, Weak};
use collab::core::collab::{DataSource, MutexCollab};
use collab_database::database::DatabaseData;
use collab_database::error::DatabaseError;
use collab_database::rows::RowId;
use collab_database::views::{CreateDatabaseParams, CreateViewParams, DatabaseLayout};
use collab_database::workspace_database::{
CollabDocStateByOid, CollabFuture, DatabaseCollabService, DatabaseMeta, WorkspaceDatabase,
@ -16,11 +17,13 @@ use tracing::{event, instrument, trace};
use collab_integrate::collab_builder::{AppFlowyCollabBuilder, CollabBuilderConfig};
use collab_integrate::{CollabKVAction, CollabKVDB, CollabPersistenceConfig};
use flowy_database_pub::cloud::DatabaseCloudService;
use flowy_database_pub::cloud::{DatabaseCloudService, SummaryRowContent};
use flowy_error::{internal_error, FlowyError, FlowyResult};
use lib_infra::box_any::BoxAny;
use lib_infra::priority_task::TaskDispatcher;
use crate::entities::{DatabaseLayoutPB, DatabaseSnapshotPB};
use crate::services::cell::stringify_cell;
use crate::services::database::DatabaseEditor;
use crate::services::database_view::DatabaseLayoutDepsResolver;
use crate::services::field_settings::default_field_settings_by_layout_map;
@ -156,7 +159,7 @@ impl DatabaseManager {
}
pub async fn get_database_inline_view_id(&self, database_id: &str) -> FlowyResult<String> {
let wdb = self.get_workspace_database().await?;
let wdb = self.get_database_indexer().await?;
let database_collab = wdb.get_database(database_id).await.ok_or_else(|| {
FlowyError::record_not_found().with_context(format!("The database:{} not found", database_id))
})?;
@ -167,17 +170,17 @@ impl DatabaseManager {
pub async fn get_all_databases_meta(&self) -> Vec<DatabaseMeta> {
let mut items = vec![];
if let Ok(wdb) = self.get_workspace_database().await {
if let Ok(wdb) = self.get_database_indexer().await {
items = wdb.get_all_database_meta()
}
items
}
pub async fn track_database(
pub async fn update_database_indexing(
&self,
view_ids_by_database_id: HashMap<String, Vec<String>>,
) -> FlowyResult<()> {
let wdb = self.get_workspace_database().await?;
let wdb = self.get_database_indexer().await?;
view_ids_by_database_id
.into_iter()
.for_each(|(database_id, view_ids)| {
@ -192,7 +195,7 @@ impl DatabaseManager {
}
pub async fn get_database_id_with_view_id(&self, view_id: &str) -> FlowyResult<String> {
let wdb = self.get_workspace_database().await?;
let wdb = self.get_database_indexer().await?;
wdb.get_database_id_with_view_id(view_id).ok_or_else(|| {
FlowyError::record_not_found()
.with_context(format!("The database for view id: {} not found", view_id))
@ -210,7 +213,7 @@ impl DatabaseManager {
pub async fn open_database(&self, database_id: &str) -> FlowyResult<Arc<DatabaseEditor>> {
trace!("open database editor:{}", database_id);
let database = self
.get_workspace_database()
.get_database_indexer()
.await?
.get_database(database_id)
.await
@ -227,7 +230,7 @@ impl DatabaseManager {
pub async fn open_database_view<T: AsRef<str>>(&self, view_id: T) -> FlowyResult<()> {
let view_id = view_id.as_ref();
let wdb = self.get_workspace_database().await?;
let wdb = self.get_database_indexer().await?;
if let Some(database_id) = wdb.get_database_id_with_view_id(view_id) {
if let Some(database) = wdb.open_database(&database_id) {
if let Some(lock_database) = database.try_lock() {
@ -243,7 +246,7 @@ impl DatabaseManager {
pub async fn close_database_view<T: AsRef<str>>(&self, view_id: T) -> FlowyResult<()> {
let view_id = view_id.as_ref();
let wdb = self.get_workspace_database().await?;
let wdb = self.get_database_indexer().await?;
let database_id = wdb.get_database_id_with_view_id(view_id);
if let Some(database_id) = database_id {
let mut editors = self.editors.lock().await;
@ -270,7 +273,7 @@ impl DatabaseManager {
}
pub async fn duplicate_database(&self, view_id: &str) -> FlowyResult<Vec<u8>> {
let wdb = self.get_workspace_database().await?;
let wdb = self.get_database_indexer().await?;
let data = wdb.get_database_data(view_id).await?;
let json_bytes = data.to_json_bytes()?;
Ok(json_bytes)
@ -297,13 +300,13 @@ impl DatabaseManager {
create_view_params.view_id = view_id.to_string();
}
let wdb = self.get_workspace_database().await?;
let wdb = self.get_database_indexer().await?;
let _ = wdb.create_database(create_database_params)?;
Ok(())
}
pub async fn create_database_with_params(&self, params: CreateDatabaseParams) -> FlowyResult<()> {
let wdb = self.get_workspace_database().await?;
let wdb = self.get_database_indexer().await?;
let _ = wdb.create_database(params)?;
Ok(())
}
@ -317,7 +320,7 @@ impl DatabaseManager {
database_id: String,
database_view_id: String,
) -> FlowyResult<()> {
let wdb = self.get_workspace_database().await?;
let wdb = self.get_database_indexer().await?;
let mut params = CreateViewParams::new(database_id.clone(), database_view_id, name, layout);
if let Some(database) = wdb.get_database(&database_id).await {
let (field, layout_setting) = DatabaseLayoutDepsResolver::new(database, layout)
@ -397,7 +400,9 @@ impl DatabaseManager {
Ok(snapshots)
}
async fn get_workspace_database(&self) -> FlowyResult<Arc<WorkspaceDatabase>> {
/// Return the database indexer.
/// Each workspace has itw own Database indexer that manages all the databases and database views
async fn get_database_indexer(&self) -> FlowyResult<Arc<WorkspaceDatabase>> {
let database = self.workspace_database.read().await;
match &*database {
None => Err(FlowyError::internal().with_context("Workspace database not initialized")),
@ -405,6 +410,45 @@ impl DatabaseManager {
}
}
#[instrument(level = "debug", skip_all)]
pub async fn summarize_row(
&self,
view_id: String,
row_id: RowId,
field_id: String,
) -> FlowyResult<()> {
let database = self.get_database_with_view_id(&view_id).await?;
//
let mut summary_row_content = SummaryRowContent::new();
if let Some(row) = database.get_row(&view_id, &row_id) {
let fields = database.get_fields(&view_id, None);
for field in fields {
if let Some(cell) = row.cells.get(&field.id) {
summary_row_content.insert(field.name.clone(), stringify_cell(cell, &field));
}
}
}
// Call the cloud service to summarize the row.
trace!(
"[AI]: summarize row:{}, content:{:?}",
row_id,
summary_row_content
);
let response = self
.cloud_service
.summary_database_row(&self.user.workspace_id()?, &row_id, summary_row_content)
.await?;
trace!("[AI]:summarize row response: {}", response);
// Update the cell with the response from the cloud service.
database
.update_cell_with_changeset(&view_id, &row_id, &field_id, BoxAny::new(response))
.await?;
Ok(())
}
/// Only expose this method for testing
#[cfg(debug_assertions)]
pub fn get_cloud_service(&self) -> &Arc<dyn DatabaseCloudService> {

View File

@ -259,6 +259,9 @@ impl<'a> CellBuilder<'a> {
FieldType::Relation => {
cells.insert(field_id, (&RelationCellData::from(cell_str)).into());
},
FieldType::Summary => {
cells.insert(field_id, insert_text_cell(cell_str, field));
},
}
}
}

View File

@ -10,7 +10,7 @@ use crate::services::database_view::{
use crate::services::field::{
default_type_option_data_from_type, select_type_option_from_field, transform_type_option,
type_option_data_from_pb, ChecklistCellChangeset, RelationTypeOption, SelectOptionCellChangeset,
StrCellData, TimestampCellData, TimestampCellDataWrapper, TypeOptionCellDataHandler,
StringCellData, TimestampCellData, TimestampCellDataWrapper, TypeOptionCellDataHandler,
TypeOptionCellExt,
};
use crate::services::field_settings::{default_field_settings_by_layout_map, FieldSettings};
@ -34,7 +34,7 @@ use lib_infra::util::timestamp;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::{broadcast, RwLock};
use tracing::{event, warn};
use tracing::{event, instrument, warn};
#[derive(Clone)]
pub struct DatabaseEditor {
@ -440,7 +440,7 @@ impl DatabaseEditor {
for cell in cells {
if let Some(new_cell) = cell.cell.clone() {
self
.update_cell(view_id, cell.row_id, &new_field_id, new_cell)
.update_cell(view_id, &cell.row_id, &new_field_id, new_cell)
.await?;
}
}
@ -755,10 +755,11 @@ impl DatabaseEditor {
}
}
#[instrument(level = "trace", skip_all)]
pub async fn update_cell_with_changeset(
&self,
view_id: &str,
row_id: RowId,
row_id: &RowId,
field_id: &str,
cell_changeset: BoxAny,
) -> FlowyResult<()> {
@ -771,7 +772,7 @@ impl DatabaseEditor {
Err(FlowyError::internal().with_context(msg))
},
}?;
(field, database.get_cell(field_id, &row_id).cell)
(field, database.get_cell(field_id, row_id).cell)
};
let new_cell =
@ -800,14 +801,13 @@ impl DatabaseEditor {
pub async fn update_cell(
&self,
view_id: &str,
row_id: RowId,
row_id: &RowId,
field_id: &str,
new_cell: Cell,
) -> FlowyResult<()> {
// Get the old row before updating the cell. It would be better to get the old cell
let old_row = { self.get_row_detail(view_id, &row_id) };
self.database.lock().update_row(&row_id, |row_update| {
let old_row = { self.get_row_detail(view_id, row_id) };
self.database.lock().update_row(row_id, |row_update| {
row_update.update_cells(|cell_update| {
cell_update.insert(field_id, new_cell);
});
@ -831,7 +831,7 @@ impl DatabaseEditor {
});
self
.did_update_row(view_id, row_id, field_id, old_row)
.did_update_row(view_id, &row_id, field_id, old_row)
.await;
Ok(())
@ -840,11 +840,11 @@ impl DatabaseEditor {
async fn did_update_row(
&self,
view_id: &str,
row_id: RowId,
row_id: &RowId,
field_id: &str,
old_row: Option<RowDetail>,
) {
let option_row = self.get_row_detail(view_id, &row_id);
let option_row = self.get_row_detail(view_id, row_id);
if let Some(new_row_detail) = option_row {
for view in self.database_views.editors().await {
view
@ -931,7 +931,7 @@ impl DatabaseEditor {
// Insert the options into the cell
self
.update_cell_with_changeset(view_id, row_id, field_id, BoxAny::new(cell_changeset))
.update_cell_with_changeset(view_id, &row_id, field_id, BoxAny::new(cell_changeset))
.await?;
Ok(())
}
@ -970,7 +970,7 @@ impl DatabaseEditor {
.await?;
self
.update_cell_with_changeset(view_id, row_id, field_id, BoxAny::new(cell_changeset))
.update_cell_with_changeset(view_id, &row_id, field_id, BoxAny::new(cell_changeset))
.await?;
Ok(())
}
@ -994,7 +994,7 @@ impl DatabaseEditor {
debug_assert!(FieldType::from(field.field_type).is_checklist());
self
.update_cell_with_changeset(view_id, row_id, field_id, BoxAny::new(changeset))
.update_cell_with_changeset(view_id, &row_id, field_id, BoxAny::new(changeset))
.await?;
Ok(())
}
@ -1294,7 +1294,7 @@ impl DatabaseEditor {
.cell
.and_then(|cell| handler.handle_get_boxed_cell_data(&cell, &primary_field))
.and_then(|cell_data| cell_data.unbox_or_none())
.unwrap_or_else(|| StrCellData("".to_string()));
.unwrap_or_else(|| StringCellData("".to_string()));
RelatedRowDataPB {
row_id: row.id.to_string(),

View File

@ -4,6 +4,7 @@ pub mod date_type_option;
pub mod number_type_option;
pub mod relation_type_option;
pub mod selection_type_option;
pub mod summary_type_option;
pub mod text_type_option;
pub mod timestamp_type_option;
mod type_option;

View File

@ -0,0 +1,2 @@
pub mod summary;
pub mod summary_entities;

View File

@ -0,0 +1,109 @@
use crate::entities::TextFilterPB;
use crate::services::cell::{CellDataChangeset, CellDataDecoder};
use crate::services::field::summary_type_option::summary_entities::SummaryCellData;
use crate::services::field::type_options::util::ProtobufStr;
use crate::services::field::{
TypeOption, TypeOptionCellData, TypeOptionCellDataCompare, TypeOptionCellDataFilter,
TypeOptionCellDataSerde, TypeOptionTransform,
};
use crate::services::sort::SortCondition;
use collab::core::any_map::AnyMapExtension;
use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder};
use collab_database::rows::Cell;
use flowy_error::FlowyResult;
use std::cmp::Ordering;
#[derive(Default, Debug, Clone)]
pub struct SummarizationTypeOption {
pub auto_fill: bool,
}
impl From<TypeOptionData> for SummarizationTypeOption {
fn from(value: TypeOptionData) -> Self {
let auto_fill = value.get_bool_value("auto_fill").unwrap_or_default();
Self { auto_fill }
}
}
impl From<SummarizationTypeOption> for TypeOptionData {
fn from(value: SummarizationTypeOption) -> Self {
TypeOptionDataBuilder::new()
.insert_bool_value("auto_fill", value.auto_fill)
.build()
}
}
impl TypeOption for SummarizationTypeOption {
type CellData = SummaryCellData;
type CellChangeset = String;
type CellProtobufType = ProtobufStr;
type CellFilter = TextFilterPB;
}
impl CellDataChangeset for SummarizationTypeOption {
fn apply_changeset(
&self,
changeset: String,
_cell: Option<Cell>,
) -> FlowyResult<(Cell, SummaryCellData)> {
let cell_data = SummaryCellData(changeset);
Ok((cell_data.clone().into(), cell_data))
}
}
impl TypeOptionCellDataFilter for SummarizationTypeOption {
fn apply_filter(
&self,
filter: &<Self as TypeOption>::CellFilter,
cell_data: &<Self as TypeOption>::CellData,
) -> bool {
filter.is_visible(cell_data)
}
}
impl TypeOptionCellDataCompare for SummarizationTypeOption {
fn apply_cmp(
&self,
cell_data: &<Self as TypeOption>::CellData,
other_cell_data: &<Self as TypeOption>::CellData,
sort_condition: SortCondition,
) -> Ordering {
match (cell_data.is_cell_empty(), other_cell_data.is_cell_empty()) {
(true, true) => Ordering::Equal,
(true, false) => Ordering::Greater,
(false, true) => Ordering::Less,
(false, false) => {
let order = cell_data.0.cmp(&other_cell_data.0);
sort_condition.evaluate_order(order)
},
}
}
}
impl CellDataDecoder for SummarizationTypeOption {
fn decode_cell(&self, cell: &Cell) -> FlowyResult<SummaryCellData> {
Ok(SummaryCellData::from(cell))
}
fn stringify_cell_data(&self, cell_data: SummaryCellData) -> String {
cell_data.to_string()
}
fn numeric_cell(&self, _cell: &Cell) -> Option<f64> {
None
}
}
impl TypeOptionTransform for SummarizationTypeOption {}
impl TypeOptionCellDataSerde for SummarizationTypeOption {
fn protobuf_encode(
&self,
cell_data: <Self as TypeOption>::CellData,
) -> <Self as TypeOption>::CellProtobufType {
ProtobufStr::from(cell_data.0)
}
fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
Ok(SummaryCellData::from(cell))
}
}

View File

@ -0,0 +1,46 @@
use crate::entities::FieldType;
use crate::services::field::{TypeOptionCellData, CELL_DATA};
use collab::core::any_map::AnyMapExtension;
use collab_database::rows::{new_cell_builder, Cell};
#[derive(Default, Debug, Clone)]
pub struct SummaryCellData(pub String);
impl std::ops::Deref for SummaryCellData {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl TypeOptionCellData for SummaryCellData {
fn is_cell_empty(&self) -> bool {
self.0.is_empty()
}
}
impl From<&Cell> for SummaryCellData {
fn from(cell: &Cell) -> Self {
Self(cell.get_str_value(CELL_DATA).unwrap_or_default())
}
}
impl From<SummaryCellData> for Cell {
fn from(data: SummaryCellData) -> Self {
new_cell_builder(FieldType::Summary)
.insert_str_value(CELL_DATA, data.0)
.build()
}
}
impl ToString for SummaryCellData {
fn to_string(&self) -> String {
self.0.clone()
}
}
impl AsRef<str> for SummaryCellData {
fn as_ref(&self) -> &str {
&self.0
}
}

View File

@ -25,7 +25,7 @@ pub struct RichTextTypeOption {
}
impl TypeOption for RichTextTypeOption {
type CellData = StrCellData;
type CellData = StringCellData;
type CellChangeset = String;
type CellProtobufType = ProtobufStr;
type CellFilter = TextFilterPB;
@ -57,13 +57,13 @@ impl TypeOptionCellDataSerde for RichTextTypeOption {
}
fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
Ok(StrCellData::from(cell))
Ok(StringCellData::from(cell))
}
}
impl CellDataDecoder for RichTextTypeOption {
fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
Ok(StrCellData::from(cell))
Ok(StringCellData::from(cell))
}
fn decode_cell_with_transform(
@ -79,11 +79,12 @@ impl CellDataDecoder for RichTextTypeOption {
| FieldType::SingleSelect
| FieldType::MultiSelect
| FieldType::Checkbox
| FieldType::URL => Some(StrCellData::from(stringify_cell(cell, field))),
| FieldType::URL => Some(StringCellData::from(stringify_cell(cell, field))),
FieldType::Checklist
| FieldType::LastEditedTime
| FieldType::CreatedTime
| FieldType::Relation => None,
FieldType::Summary => Some(StringCellData::from(stringify_cell(cell, field))),
}
}
@ -92,7 +93,7 @@ impl CellDataDecoder for RichTextTypeOption {
}
fn numeric_cell(&self, cell: &Cell) -> Option<f64> {
StrCellData::from(cell).0.parse::<f64>().ok()
StringCellData::from(cell).0.parse::<f64>().ok()
}
}
@ -108,7 +109,7 @@ impl CellDataChangeset for RichTextTypeOption {
.with_context("The len of the text should not be more than 10000"),
)
} else {
let text_cell_data = StrCellData(changeset);
let text_cell_data = StringCellData(changeset);
Ok((text_cell_data.clone().into(), text_cell_data))
}
}
@ -144,8 +145,8 @@ impl TypeOptionCellDataCompare for RichTextTypeOption {
}
#[derive(Default, Debug, Clone)]
pub struct StrCellData(pub String);
impl std::ops::Deref for StrCellData {
pub struct StringCellData(pub String);
impl std::ops::Deref for StringCellData {
type Target = String;
fn deref(&self) -> &Self::Target {
@ -153,57 +154,57 @@ impl std::ops::Deref for StrCellData {
}
}
impl TypeOptionCellData for StrCellData {
impl TypeOptionCellData for StringCellData {
fn is_cell_empty(&self) -> bool {
self.0.is_empty()
}
}
impl From<&Cell> for StrCellData {
impl From<&Cell> for StringCellData {
fn from(cell: &Cell) -> Self {
Self(cell.get_str_value(CELL_DATA).unwrap_or_default())
}
}
impl From<StrCellData> for Cell {
fn from(data: StrCellData) -> Self {
impl From<StringCellData> for Cell {
fn from(data: StringCellData) -> Self {
new_cell_builder(FieldType::RichText)
.insert_str_value(CELL_DATA, data.0)
.build()
}
}
impl std::ops::DerefMut for StrCellData {
impl std::ops::DerefMut for StringCellData {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl std::convert::From<String> for StrCellData {
impl std::convert::From<String> for StringCellData {
fn from(s: String) -> Self {
Self(s)
}
}
impl ToString for StrCellData {
impl ToString for StringCellData {
fn to_string(&self) -> String {
self.0.clone()
}
}
impl std::convert::From<StrCellData> for String {
fn from(value: StrCellData) -> Self {
impl std::convert::From<StringCellData> for String {
fn from(value: StringCellData) -> Self {
value.0
}
}
impl std::convert::From<&str> for StrCellData {
impl std::convert::From<&str> for StringCellData {
fn from(s: &str) -> Self {
Self(s.to_owned())
}
}
impl AsRef<str> for StrCellData {
impl AsRef<str> for StringCellData {
fn as_ref(&self) -> &str {
self.0.as_str()
}

View File

@ -11,10 +11,11 @@ use flowy_error::FlowyResult;
use crate::entities::{
CheckboxTypeOptionPB, ChecklistTypeOptionPB, DateTypeOptionPB, FieldType,
MultiSelectTypeOptionPB, NumberTypeOptionPB, RelationTypeOptionPB, RichTextTypeOptionPB,
SingleSelectTypeOptionPB, TimestampTypeOptionPB, URLTypeOptionPB,
SingleSelectTypeOptionPB, SummarizationTypeOptionPB, TimestampTypeOptionPB, URLTypeOptionPB,
};
use crate::services::cell::CellDataDecoder;
use crate::services::field::checklist_type_option::ChecklistTypeOption;
use crate::services::field::summary_type_option::summary::SummarizationTypeOption;
use crate::services::field::{
CheckboxTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption, RelationTypeOption,
RichTextTypeOption, SingleSelectTypeOption, TimestampTypeOption, URLTypeOption,
@ -181,6 +182,9 @@ pub fn type_option_data_from_pb<T: Into<Bytes>>(
FieldType::Relation => {
RelationTypeOptionPB::try_from(bytes).map(|pb| RelationTypeOption::from(pb).into())
},
FieldType::Summary => {
SummarizationTypeOptionPB::try_from(bytes).map(|pb| SummarizationTypeOption::from(pb).into())
},
}
}
@ -242,6 +246,12 @@ pub fn type_option_to_pb(type_option: TypeOptionData, field_type: &FieldType) ->
.try_into()
.unwrap()
},
FieldType::Summary => {
let summarization_type_option: SummarizationTypeOption = type_option.into();
SummarizationTypeOptionPB::from(summarization_type_option)
.try_into()
.unwrap()
},
}
}
@ -261,5 +271,6 @@ pub fn default_type_option_data_from_type(field_type: FieldType) -> TypeOptionDa
FieldType::URL => URLTypeOption::default().into(),
FieldType::Checklist => ChecklistTypeOption.into(),
FieldType::Relation => RelationTypeOption::default().into(),
FieldType::Summary => SummarizationTypeOption::default().into(),
}
}

View File

@ -10,6 +10,7 @@ use lib_infra::box_any::BoxAny;
use crate::entities::FieldType;
use crate::services::cell::{CellCache, CellDataChangeset, CellDataDecoder, CellProtobufBlob};
use crate::services::field::summary_type_option::summary::SummarizationTypeOption;
use crate::services::field::{
CheckboxTypeOption, ChecklistTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption,
RelationTypeOption, RichTextTypeOption, SingleSelectTypeOption, TimestampTypeOption, TypeOption,
@ -166,23 +167,24 @@ where
if let Some(cell_data_cache) = self.cell_data_cache.as_ref() {
let field_type = FieldType::from(field.field_type);
let key = CellDataCacheKey::new(field, field_type, cell);
// tracing::trace!(
// "Cell cache update: field_type:{}, cell: {:?}, cell_data: {:?}",
// field_type,
// cell,
// cell_data
// );
tracing::trace!(
"Cell cache update: field_type:{}, cell: {:?}, cell_data: {:?}",
field_type,
cell,
cell_data
);
cell_data_cache.write().insert(key.as_ref(), cell_data);
}
}
fn get_cell_data(&self, cell: &Cell, field: &Field) -> Option<T::CellData> {
let field_type_of_cell = get_field_type_from_cell(cell)?;
if let Some(cell_data) = self.get_cell_data_from_cache(cell, field) {
return Some(cell_data);
}
// If the field type of the cell is the same as the field type of the handler, we can directly decode the cell.
// Otherwise, we need to transform the cell to the field type of the handler.
let cell_data = if field_type_of_cell == self.field_type {
Some(self.decode_cell(cell).unwrap_or_default())
} else if is_type_option_cell_transformable(field_type_of_cell, self.field_type) {
@ -437,6 +439,16 @@ impl<'a> TypeOptionCellExt<'a> {
self.cell_data_cache.clone(),
)
}),
FieldType::Summary => self
.field
.get_type_option::<SummarizationTypeOption>(field_type)
.map(|type_option| {
TypeOptionCellDataHandlerImpl::new_with_boxed(
type_option,
field_type,
self.cell_data_cache.clone(),
)
}),
}
}
@ -538,6 +550,8 @@ fn get_type_option_transform_handler(
FieldType::Relation => {
Box::new(RelationTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler>
},
FieldType::Summary => Box::new(SummarizationTypeOption::from(type_option_data))
as Box<dyn TypeOptionTransformHandler>,
}
}

View File

@ -34,7 +34,7 @@ impl FieldSettings {
.unwrap_or(DEFAULT_WIDTH);
let wrap_cell_content = field_settings
.get_bool_value(WRAP_CELL_CONTENT)
.unwrap_or(false);
.unwrap_or(true);
Self {
field_id: field_id.to_string(),

View File

@ -20,7 +20,7 @@ impl FieldSettingsBuilder {
field_id: field_id.to_string(),
visibility: FieldVisibility::AlwaysShown,
width: DEFAULT_WIDTH,
wrap_cell_content: false,
wrap_cell_content: true,
};
Self {

View File

@ -280,6 +280,7 @@ impl FilterInner {
FieldType::Checklist => BoxAny::new(ChecklistFilterPB::parse(condition as u8, content)),
FieldType::Checkbox => BoxAny::new(CheckboxFilterPB::parse(condition as u8, content)),
FieldType::Relation => BoxAny::new(RelationFilterPB::parse(condition as u8, content)),
FieldType::Summary => BoxAny::new(TextFilterPB::parse(condition as u8, content)),
};
FilterInner::Data {
@ -362,6 +363,10 @@ impl<'a> From<&'a Filter> for FilterMap {
let filter = condition_and_content.cloned::<RelationFilterPB>()?;
(filter.condition as u8, "".to_string())
},
FieldType::Summary => {
let filter = condition_and_content.cloned::<TextFilterPB>()?;
(filter.condition as u8, filter.content)
},
};
Some((condition, content))
};

View File

@ -46,7 +46,7 @@ impl DatabaseCellTest {
} => {
self
.editor
.update_cell_with_changeset(&view_id, row_id, &field_id, changeset)
.update_cell_with_changeset(&view_id, &row_id, &field_id, changeset)
.await
.unwrap();
},

View File

@ -3,7 +3,7 @@ use std::time::Duration;
use flowy_database2::entities::FieldType;
use flowy_database2::services::field::{
ChecklistCellChangeset, DateCellChangeset, DateCellData, MultiSelectTypeOption,
RelationCellChangeset, SelectOptionCellChangeset, SingleSelectTypeOption, StrCellData,
RelationCellChangeset, SelectOptionCellChangeset, SingleSelectTypeOption, StringCellData,
URLCellData,
};
use lib_infra::box_any::BoxAny;
@ -84,7 +84,7 @@ async fn text_cell_data_test() {
.await;
for (i, row_cell) in cells.into_iter().enumerate() {
let text = StrCellData::from(row_cell.cell.as_ref().unwrap());
let text = StringCellData::from(row_cell.cell.as_ref().unwrap());
match i {
0 => assert_eq!(text.as_str(), "A"),
1 => assert_eq!(text.as_str(), ""),

View File

@ -1,16 +1,16 @@
use std::collections::HashMap;
use std::sync::Arc;
use collab_database::database::{gen_database_view_id, timestamp};
use collab_database::database::gen_database_view_id;
use collab_database::fields::Field;
use collab_database::rows::{Row, RowDetail, RowId};
use collab_database::rows::{RowDetail, RowId};
use lib_infra::box_any::BoxAny;
use strum::EnumCount;
use event_integration_test::folder_event::ViewTest;
use event_integration_test::EventIntegrationTest;
use flowy_database2::entities::{FieldType, FilterPB, RowMetaPB};
use flowy_database2::services::cell::CellBuilder;
use flowy_database2::services::database::DatabaseEditor;
use flowy_database2::services::field::checklist_type_option::{
ChecklistCellChangeset, ChecklistTypeOption,
@ -196,7 +196,7 @@ impl DatabaseEditorTest {
self
.editor
.update_cell_with_changeset(&self.view_id, row_id, &field.id, cell_changeset)
.update_cell_with_changeset(&self.view_id, &row_id, &field.id, cell_changeset)
.await
}
@ -282,139 +282,3 @@ impl DatabaseEditorTest {
.ok()
}
}
pub struct TestRowBuilder<'a> {
database_id: &'a str,
row_id: RowId,
fields: &'a [Field],
cell_build: CellBuilder<'a>,
}
impl<'a> TestRowBuilder<'a> {
pub fn new(database_id: &'a str, row_id: RowId, fields: &'a [Field]) -> Self {
let cell_build = CellBuilder::with_cells(Default::default(), fields);
Self {
database_id,
row_id,
fields,
cell_build,
}
}
pub fn insert_text_cell(&mut self, data: &str) -> String {
let text_field = self.field_with_type(&FieldType::RichText);
self
.cell_build
.insert_text_cell(&text_field.id, data.to_string());
text_field.id.clone()
}
pub fn insert_number_cell(&mut self, data: &str) -> String {
let number_field = self.field_with_type(&FieldType::Number);
self
.cell_build
.insert_text_cell(&number_field.id, data.to_string());
number_field.id.clone()
}
pub fn insert_date_cell(
&mut self,
date: i64,
time: Option<String>,
include_time: Option<bool>,
field_type: &FieldType,
) -> String {
let date_field = self.field_with_type(field_type);
self
.cell_build
.insert_date_cell(&date_field.id, date, time, include_time);
date_field.id.clone()
}
pub fn insert_checkbox_cell(&mut self, data: &str) -> String {
let checkbox_field = self.field_with_type(&FieldType::Checkbox);
self
.cell_build
.insert_text_cell(&checkbox_field.id, data.to_string());
checkbox_field.id.clone()
}
pub fn insert_url_cell(&mut self, content: &str) -> String {
let url_field = self.field_with_type(&FieldType::URL);
self
.cell_build
.insert_url_cell(&url_field.id, content.to_string());
url_field.id.clone()
}
pub fn insert_single_select_cell<F>(&mut self, f: F) -> String
where
F: Fn(Vec<SelectOption>) -> SelectOption,
{
let single_select_field = self.field_with_type(&FieldType::SingleSelect);
let type_option = single_select_field
.get_type_option::<SingleSelectTypeOption>(FieldType::SingleSelect)
.unwrap();
let option = f(type_option.options);
self
.cell_build
.insert_select_option_cell(&single_select_field.id, vec![option.id]);
single_select_field.id.clone()
}
pub fn insert_multi_select_cell<F>(&mut self, f: F) -> String
where
F: Fn(Vec<SelectOption>) -> Vec<SelectOption>,
{
let multi_select_field = self.field_with_type(&FieldType::MultiSelect);
let type_option = multi_select_field
.get_type_option::<MultiSelectTypeOption>(FieldType::MultiSelect)
.unwrap();
let options = f(type_option.options);
let ops_ids = options
.iter()
.map(|option| option.id.clone())
.collect::<Vec<_>>();
self
.cell_build
.insert_select_option_cell(&multi_select_field.id, ops_ids);
multi_select_field.id.clone()
}
pub fn insert_checklist_cell(&mut self, options: Vec<(String, bool)>) -> String {
let checklist_field = self.field_with_type(&FieldType::Checklist);
self
.cell_build
.insert_checklist_cell(&checklist_field.id, options);
checklist_field.id.clone()
}
pub fn field_with_type(&self, field_type: &FieldType) -> Field {
self
.fields
.iter()
.find(|field| {
let t_field_type = FieldType::from(field.field_type);
&t_field_type == field_type
})
.unwrap()
.clone()
}
pub fn build(self) -> Row {
let timestamp = timestamp();
Row {
id: self.row_id,
database_id: self.database_id.to_string(),
cells: self.cell_build.build(),
height: 60,
visibility: true,
modified_at: timestamp,
created_at: timestamp,
}
}
}

View File

@ -192,7 +192,7 @@ impl DatabaseGroupTest {
let row_id = RowId::from(self.row_at_index(from_group_index, row_index).await.id);
self
.editor
.update_cell(&self.view_id, row_id, &field_id, cell)
.update_cell(&self.view_id, &row_id, &field_id, cell)
.await
.unwrap();
},
@ -218,7 +218,7 @@ impl DatabaseGroupTest {
let row_id = RowId::from(self.row_at_index(from_group_index, row_index).await.id);
self
.editor
.update_cell(&self.view_id, row_id, &field_id, cell)
.update_cell(&self.view_id, &row_id, &field_id, cell)
.await
.unwrap();
},

View File

@ -2,8 +2,11 @@ use collab_database::database::{gen_database_id, gen_database_view_id, gen_row_i
use collab_database::views::{DatabaseLayout, DatabaseView, LayoutSetting, LayoutSettings};
use strum::IntoEnumIterator;
use crate::database::mock_data::{COMPLETED, FACEBOOK, GOOGLE, PAUSED, PLANNED, TWITTER};
use event_integration_test::database_event::TestRowBuilder;
use flowy_database2::entities::FieldType;
use flowy_database2::services::field::checklist_type_option::ChecklistTypeOption;
use flowy_database2::services::field::summary_type_option::summary::SummarizationTypeOption;
use flowy_database2::services::field::{
DateFormat, DateTypeOption, FieldBuilder, MultiSelectTypeOption, RelationTypeOption,
SelectOption, SelectOptionColor, SingleSelectTypeOption, TimeFormat, TimestampTypeOption,
@ -11,9 +14,6 @@ use flowy_database2::services::field::{
use flowy_database2::services::field_settings::default_field_settings_for_fields;
use flowy_database2::services::setting::BoardLayoutSetting;
use crate::database::database_editor::TestRowBuilder;
use crate::database::mock_data::{COMPLETED, FACEBOOK, GOOGLE, PAUSED, PLANNED, TWITTER};
// Kanban board unit test mock data
pub fn make_test_board() -> DatabaseData {
let database_id = gen_database_id();
@ -127,6 +127,13 @@ pub fn make_test_board() -> DatabaseData {
.build();
fields.push(relation_field);
},
FieldType::Summary => {
let type_option = SummarizationTypeOption { auto_fill: false };
let relation_field = FieldBuilder::new(field_type, type_option)
.name("AI summary")
.build();
fields.push(relation_field);
},
}
}

View File

@ -3,12 +3,11 @@ use collab_database::views::{DatabaseLayout, DatabaseView, LayoutSetting, Layout
use flowy_database2::services::field_settings::default_field_settings_for_fields;
use strum::IntoEnumIterator;
use event_integration_test::database_event::TestRowBuilder;
use flowy_database2::entities::FieldType;
use flowy_database2::services::field::{FieldBuilder, MultiSelectTypeOption};
use flowy_database2::services::setting::CalendarLayoutSetting;
use crate::database::database_editor::TestRowBuilder;
// Calendar unit test mock data
pub fn make_test_calendar() -> DatabaseData {
let database_id = gen_database_id();

View File

@ -2,7 +2,10 @@ use collab_database::database::{gen_database_id, gen_database_view_id, gen_row_i
use collab_database::views::{DatabaseLayout, DatabaseView};
use strum::IntoEnumIterator;
use crate::database::mock_data::{COMPLETED, FACEBOOK, GOOGLE, PAUSED, PLANNED, TWITTER};
use event_integration_test::database_event::TestRowBuilder;
use flowy_database2::entities::FieldType;
use flowy_database2::services::field::summary_type_option::summary::SummarizationTypeOption;
use flowy_database2::services::field::{
ChecklistTypeOption, DateFormat, DateTypeOption, FieldBuilder, MultiSelectTypeOption,
NumberFormat, NumberTypeOption, RelationTypeOption, SelectOption, SelectOptionColor,
@ -10,9 +13,6 @@ use flowy_database2::services::field::{
};
use flowy_database2::services::field_settings::default_field_settings_for_fields;
use crate::database::database_editor::TestRowBuilder;
use crate::database::mock_data::{COMPLETED, FACEBOOK, GOOGLE, PAUSED, PLANNED, TWITTER};
pub fn make_test_grid() -> DatabaseData {
let database_id = gen_database_id();
let mut fields = vec![];
@ -125,6 +125,13 @@ pub fn make_test_grid() -> DatabaseData {
.build();
fields.push(relation_field);
},
FieldType::Summary => {
let type_option = SummarizationTypeOption { auto_fill: false };
let relation_field = FieldBuilder::new(field_type, type_option)
.name("AI summary")
.build();
fields.push(relation_field);
},
}
}

View File

@ -1,5 +1,3 @@
use chrono::{DateTime, Local, Offset};
use collab_database::database::timestamp;
use flowy_database2::entities::FieldType;
use flowy_database2::services::cell::stringify_cell;
use flowy_database2::services::field::CHECK;
@ -24,45 +22,6 @@ async fn export_meta_csv_test() {
}
}
#[tokio::test]
async fn export_csv_test() {
let test = DatabaseEditorTest::new_grid().await;
let database = test.editor.clone();
let s = database.export_csv(CSVFormat::Original).await.unwrap();
let format = "%Y/%m/%d %R";
let naive = chrono::NaiveDateTime::from_timestamp_opt(timestamp(), 0).unwrap();
let offset = Local::now().offset().fix();
let date_time = DateTime::<Local>::from_naive_utc_and_offset(naive, offset);
let date_string = format!("{}", date_time.format(format));
let expected = format!(
r#"Name,Price,Time,Status,Platform,is urgent,link,TODO,Last Modified,Created At,Related
A,$1,2022/03/14,,"Google,Facebook",Yes,AppFlowy website - https://www.appflowy.io,First thing,{},{},
,$2,2022/03/14,,"Google,Twitter",Yes,,"Have breakfast,Have lunch,Take a nap,Have dinner,Shower and head to bed",{},{},
C,$3,2022/03/14,Completed,"Facebook,Google,Twitter",No,,,{},{},
DA,$14,2022/11/17,Completed,,No,,Task 1,{},{},
AE,,2022/11/13,Planned,"Facebook,Twitter",No,,,{},{},
AE,$5,2022/12/25,Planned,Facebook,Yes,,"Sprint,Sprint some more,Rest",{},{},
CB,,,,,,,,{},{},
"#,
date_string,
date_string,
date_string,
date_string,
date_string,
date_string,
date_string,
date_string,
date_string,
date_string,
date_string,
date_string,
date_string,
date_string,
);
println!("{}", s);
assert_eq!(s, expected);
}
#[tokio::test]
async fn export_and_then_import_meta_csv_test() {
let test = DatabaseEditorTest::new_grid().await;
@ -123,6 +82,7 @@ async fn export_and_then_import_meta_csv_test() {
FieldType::LastEditedTime => {},
FieldType::CreatedTime => {},
FieldType::Relation => {},
FieldType::Summary => {},
}
} else {
panic!(
@ -205,6 +165,7 @@ async fn history_database_import_test() {
FieldType::LastEditedTime => {},
FieldType::CreatedTime => {},
FieldType::Relation => {},
FieldType::Summary => {},
}
} else {
panic!(

View File

@ -2,6 +2,7 @@ use std::collections::HashMap;
use std::sync::Arc;
use bytes::Bytes;
pub use collab_folder::View;
use collab_folder::ViewLayout;
use tokio::sync::RwLock;
@ -52,7 +53,10 @@ pub trait FolderOperationHandler {
/// * `view_id`: the view id
/// * `name`: the name of the view
/// * `data`: initial data of the view. The data should be parsed by the [FolderOperationHandler]
/// implementation. For example, the data of the database will be [DatabaseData].
/// implementation.
/// For example,
/// 1. the data of the database will be [DatabaseData] that is serialized to JSON
/// 2. the data of the document will be [DocumentData] that is serialized to JSON
/// * `layout`: the layout of the view
/// * `meta`: use to carry extra information. For example, the database view will use this
/// to carry the reference database id.

View File

@ -1,14 +1,18 @@
use anyhow::Error;
use client_api::entity::ai_dto::{SummarizeRowData, SummarizeRowParams};
use client_api::entity::QueryCollabResult::{Failed, Success};
use client_api::entity::{QueryCollab, QueryCollabParams};
use client_api::error::ErrorCode::RecordNotFound;
use collab::core::collab::DataSource;
use collab::entity::EncodedCollab;
use collab_entity::CollabType;
use serde_json::{Map, Value};
use std::sync::Arc;
use tracing::{error, instrument};
use flowy_database_pub::cloud::{CollabDocStateByOid, DatabaseCloudService, DatabaseSnapshot};
use flowy_database_pub::cloud::{
CollabDocStateByOid, DatabaseCloudService, DatabaseSnapshot, SummaryRowContent,
};
use lib_infra::future::FutureResult;
use crate::af_cloud::define::ServerUser;
@ -119,4 +123,26 @@ where
) -> FutureResult<Vec<DatabaseSnapshot>, Error> {
FutureResult::new(async move { Ok(vec![]) })
}
fn summary_database_row(
&self,
workspace_id: &str,
_object_id: &str,
summary_row: SummaryRowContent,
) -> FutureResult<String, Error> {
let workspace_id = workspace_id.to_string();
let try_get_client = self.inner.try_get_client();
FutureResult::new(async move {
let map: Map<String, Value> = summary_row
.into_iter()
.map(|(key, value)| (key, Value::String(value)))
.collect();
let params = SummarizeRowParams {
workspace_id,
data: SummarizeRowData::Content(map),
};
let data = try_get_client?.summarize_row(params).await?;
Ok(data.text)
})
}
}

View File

@ -4,7 +4,9 @@ use collab_entity::define::{DATABASE, DATABASE_ROW_DATA, WORKSPACE_DATABASES};
use collab_entity::CollabType;
use yrs::{Any, MapPrelim};
use flowy_database_pub::cloud::{CollabDocStateByOid, DatabaseCloudService, DatabaseSnapshot};
use flowy_database_pub::cloud::{
CollabDocStateByOid, DatabaseCloudService, DatabaseSnapshot, SummaryRowContent,
};
use lib_infra::future::FutureResult;
pub(crate) struct LocalServerDatabaseCloudServiceImpl();
@ -73,4 +75,14 @@ impl DatabaseCloudService for LocalServerDatabaseCloudServiceImpl {
) -> FutureResult<Vec<DatabaseSnapshot>, Error> {
FutureResult::new(async move { Ok(vec![]) })
}
fn summary_database_row(
&self,
_workspace_id: &str,
_object_id: &str,
_summary_row: SummaryRowContent,
) -> FutureResult<String, Error> {
// TODO(lucas): local ai
FutureResult::new(async move { Ok("".to_string()) })
}
}

View File

@ -2,7 +2,9 @@ use anyhow::Error;
use collab_entity::CollabType;
use tokio::sync::oneshot::channel;
use flowy_database_pub::cloud::{CollabDocStateByOid, DatabaseCloudService, DatabaseSnapshot};
use flowy_database_pub::cloud::{
CollabDocStateByOid, DatabaseCloudService, DatabaseSnapshot, SummaryRowContent,
};
use lib_dispatch::prelude::af_spawn;
use lib_infra::future::FutureResult;
@ -94,4 +96,13 @@ where
Ok(snapshots)
})
}
fn summary_database_row(
&self,
_workspace_id: &str,
_object_id: &str,
_summary_row: SummaryRowContent,
) -> FutureResult<String, Error> {
FutureResult::new(async move { Ok("".to_string()) })
}
}