diff --git a/.github/workflows/rust_ci.yaml b/.github/workflows/rust_ci.yaml index add107c825..c59d45c58c 100644 --- a/.github/workflows/rust_ci.yaml +++ b/.github/workflows/rust_ci.yaml @@ -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 diff --git a/frontend/appflowy_flutter/ios/Podfile.lock b/frontend/appflowy_flutter/ios/Podfile.lock index 671c9382d0..ef1354faff 100644 --- a/frontend/appflowy_flutter/ios/Podfile.lock +++ b/frontend/appflowy_flutter/ios/Podfile.lock @@ -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 diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/summary_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/summary_cell_bloc.dart new file mode 100644 index 0000000000..34b3981f48 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/summary_cell_bloc.dart @@ -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 { + SummaryCellBloc({ + required this.cellController, + }) : super(SummaryCellState.initial(cellController)) { + _dispatch(); + _startListening(); + } + + final SummaryCellController cellController; + void Function()? _onCellChangedFn; + + @override + Future close() async { + if (_onCellChangedFn != null) { + cellController.removeListener( + onCellChanged: _onCellChangedFn!, + onFieldChanged: _onFieldChangedListener, + ); + } + await cellController.dispose(); + return super.close(); + } + + void _dispatch() { + on( + (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, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/summary_row_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/summary_row_bloc.dart new file mode 100644 index 0000000000..fe69cdb364 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/summary_row_bloc.dart @@ -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 { + 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( + (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 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; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/text_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/text_cell_bloc.dart index f9f4d47d41..22baf26599 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/text_cell_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/text_cell_bloc.dart @@ -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 ?? "" : ""; diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_controller_builder.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_controller_builder.dart index 881e6e164b..e4866a1517 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_controller_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_controller_builder.dart @@ -15,6 +15,7 @@ typedef DateCellController = CellController; typedef TimestampCellController = CellController; typedef URLCellController = CellController; typedef RelationCellController = CellController; +typedef SummaryCellController = CellController; 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; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_data_loader.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_data_loader.dart index c7d1169b4b..4edae575ce 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_data_loader.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_data_loader.dart @@ -51,8 +51,13 @@ class CellDataLoader { class StringCellDataParser implements CellDataParser { @override String? parserData(List 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 { 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 { @override String? parserData(List 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 { 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 { 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 { 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 { 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 { @override RelationCellDataPB? parserData(List 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; + } } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/row.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/row.dart index 50e48377d4..351062933a 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/row.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/row.dart @@ -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; diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_builder.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_builder.dart index ae52567208..a002f73f88 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_builder.dart @@ -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, }; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/summary_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/summary_card_cell.dart new file mode 100644 index 0000000000..8d7577b6ea --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/summary_card_cell.dart @@ -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 { + const SummaryCardCell({ + super.key, + required super.style, + required this.databaseController, + required this.cellContext, + }); + + final DatabaseController databaseController; + final CellContext cellContext; + + @override + State createState() => _SummaryCellState(); +} + +class _SummaryCellState extends State { + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) { + return SummaryCellBloc( + cellController: makeCellController( + widget.databaseController, + widget.cellContext, + ).as(), + ); + }, + child: BlocBuilder( + 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), + ); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/calendar_card_cell_style.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/calendar_card_cell_style.dart index 2ea0f5ac08..431ac44029 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/calendar_card_cell_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/calendar_card_cell_style.dart @@ -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, + ), }; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/desktop_board_card_cell_style.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/desktop_board_card_cell_style.dart index 8f86c69a05..ebe1537cbb 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/desktop_board_card_cell_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/desktop_board_card_cell_style.dart @@ -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, + ), }; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/mobile_board_card_cell_style.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/mobile_board_card_cell_style.dart index 7c5d3be1be..df162abcba 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/mobile_board_card_cell_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/mobile_board_card_cell_style.dart @@ -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, + ), }; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_summary_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_summary_cell.dart new file mode 100644 index 0000000000..4ef64ced2f --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_summary_cell.dart @@ -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(context, listen: false) + .onEnter = true, + onExit: (p) => + Provider.of(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( + 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; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_summary_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_summary_cell.dart new file mode 100644 index 0000000000..424dda870c --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_summary_cell.dart @@ -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, + ), + ), + ], + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_builder.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_builder.dart index c70dedd68e..66beb7c437 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_builder.dart @@ -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(), }; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/summary.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/summary.dart new file mode 100644 index 0000000000..ccd42bc96c --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/summary.dart @@ -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 createState() => + _SummaryCellState(); +} + +class _SummaryCellState extends GridEditableTextCell { + 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( + 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 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( + 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( + 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() + .add(const SummaryRowEvent.startSummary()); + }, + ), + ), + ); + }, + ); + }, + ); + } +} + +class CopyButton extends StatelessWidget { + const CopyButton({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + 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()); + }, + ), + ), + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_summary_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_summary_cell.dart new file mode 100644 index 0000000000..3fdb28a8fa --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_summary_cell.dart @@ -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(context, listen: false) + .onEnter = true, + onExit: (p) => + Provider.of(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( + 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), + ], + ), + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_summary_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_summary_cell.dart new file mode 100644 index 0000000000..cca601ce12 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_summary_cell.dart @@ -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, + ), + ), + ], + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_type_list.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_type_list.dart index f8c5aea5ba..02bc0700a4 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_type_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_type_list.dart @@ -20,6 +20,7 @@ const List _supportedFieldTypes = [ FieldType.LastEditedTime, FieldType.CreatedTime, FieldType.Relation, + FieldType.Summary, ]; class FieldTypeList extends StatelessWidget with FlowyOverlayDelegate { diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/builder.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/builder.dart index 88d81ab5db..2db2c09544 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/builder.dart @@ -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(), }; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/summary.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/summary.dart new file mode 100644 index 0000000000..76a78aa22a --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/summary.dart @@ -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; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/accessory/cell_accessory.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/accessory/cell_accessory.dart index 5e42870208..febd6a6749 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/accessory/cell_accessory.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/accessory/cell_accessory.dart @@ -69,24 +69,32 @@ class _PrimaryCellAccessoryState extends State 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(); diff --git a/frontend/appflowy_flutter/lib/util/field_type_extension.dart b/frontend/appflowy_flutter/lib/util/field_type_extension.dart index 517520408a..9d26149a21 100644 --- a/frontend/appflowy_flutter/lib/util/field_type_extension.dart +++ b/frontend/appflowy_flutter/lib/util/field_type_extension.dart @@ -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(), }; } diff --git a/frontend/resources/flowy_icons/16x/ai_copy.svg b/frontend/resources/flowy_icons/16x/ai_copy.svg new file mode 100644 index 0000000000..e13d31835e --- /dev/null +++ b/frontend/resources/flowy_icons/16x/ai_copy.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/16x/ai_summary.svg b/frontend/resources/flowy_icons/16x/ai_summary.svg new file mode 100644 index 0000000000..2455874bc5 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/ai_summary.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/16x/ai_summary_generate.svg b/frontend/resources/flowy_icons/16x/ai_summary_generate.svg new file mode 100644 index 0000000000..a7ac5efdaa --- /dev/null +++ b/frontend/resources/flowy_icons/16x/ai_summary_generate.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 5cd0c7ec07..dac7b45308 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -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" } -} +} \ No newline at end of file diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index e6fa2283d2..498014d8f3 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -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" diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index f4542347eb..e58e59b0fd 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -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 diff --git a/frontend/rust-lib/event-integration-test/Cargo.toml b/frontend/rust-lib/event-integration-test/Cargo.toml index d3c8a15635..93e7ff2600 100644 --- a/frontend/rust-lib/event-integration-test/Cargo.toml +++ b/frontend/rust-lib/event-integration-test/Cargo.toml @@ -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" diff --git a/frontend/rust-lib/event-integration-test/src/database_event.rs b/frontend/rust-lib/event-integration-test/src/database_event.rs index 424936b6a8..30104ab711 100644 --- a/frontend/rust-lib/event-integration-test/src/database_event.rs +++ b/frontend/rust-lib/event-integration-test/src/database_event.rs @@ -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) -> ViewPB { let payload = CreateViewPayloadPB { parent_view_id: parent_id.to_string(), @@ -199,6 +207,13 @@ impl EventIntegrationTest { .await .parse::() } + 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::() } + 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, + include_time: Option, + 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(&mut self, f: F) -> String + where + F: Fn(Vec) -> SelectOption, + { + let single_select_field = self.field_with_type(&FieldType::SingleSelect); + let type_option = single_select_field + .get_type_option::(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(&mut self, f: F) -> String + where + F: Fn(Vec) -> Vec, + { + let multi_select_field = self.field_with_type(&FieldType::MultiSelect); + let type_option = multi_select_field + .get_type_option::(FieldType::MultiSelect) + .unwrap(); + let options = f(type_option.options); + let ops_ids = options + .iter() + .map(|option| option.id.clone()) + .collect::>(); + 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, + } + } +} diff --git a/frontend/rust-lib/event-integration-test/tests/database/af_cloud/mod.rs b/frontend/rust-lib/event-integration-test/tests/database/af_cloud/mod.rs new file mode 100644 index 0000000000..36f850dd92 --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/database/af_cloud/mod.rs @@ -0,0 +1,2 @@ +// mod summarize_row; +mod util; diff --git a/frontend/rust-lib/event-integration-test/tests/database/af_cloud/summarize_row.rs b/frontend/rust-lib/event-integration-test/tests/database/af_cloud/summarize_row.rs new file mode 100644 index 0000000000..9d99d23117 --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/database/af_cloud/summarize_row.rs @@ -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( + ¤t_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); +} diff --git a/frontend/rust-lib/event-integration-test/tests/database/af_cloud/util.rs b/frontend/rust-lib/event-integration-test/tests/database/af_cloud/util.rs new file mode 100644 index 0000000000..3bfd07cab4 --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/database/af_cloud/util.rs @@ -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 { + 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) -> Vec { + 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() +} diff --git a/frontend/rust-lib/event-integration-test/tests/database/mod.rs b/frontend/rust-lib/event-integration-test/tests/database/mod.rs index 01d3a22023..ee1335d7ff 100644 --- a/frontend/rust-lib/event-integration-test/tests/database/mod.rs +++ b/frontend/rust-lib/event-integration-test/tests/database/mod.rs @@ -1,3 +1,4 @@ +mod af_cloud; mod local_test; // #[cfg(feature = "supabase_cloud_test")] diff --git a/frontend/rust-lib/flowy-ai/Cargo.toml b/frontend/rust-lib/flowy-ai/Cargo.toml deleted file mode 100644 index 67ab23cac1..0000000000 --- a/frontend/rust-lib/flowy-ai/Cargo.toml +++ /dev/null @@ -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" diff --git a/frontend/rust-lib/flowy-ai/src/config.rs b/frontend/rust-lib/flowy-ai/src/config.rs deleted file mode 100644 index a9564f2aa6..0000000000 --- a/frontend/rust-lib/flowy-ai/src/config.rs +++ /dev/null @@ -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 { - let openai_api_key = - std::env::var(OPENAI_API_KEY).map_err(|_| anyhow!("Missing OPENAI_API_KEY"))?; - - Ok(Self { openai_api_key }) - } -} diff --git a/frontend/rust-lib/flowy-ai/src/lib.rs b/frontend/rust-lib/flowy-ai/src/lib.rs deleted file mode 100644 index fd772a64bb..0000000000 --- a/frontend/rust-lib/flowy-ai/src/lib.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod config; -pub mod text; diff --git a/frontend/rust-lib/flowy-ai/src/text/entities.rs b/frontend/rust-lib/flowy-ai/src/text/entities.rs deleted file mode 100644 index 8b13789179..0000000000 --- a/frontend/rust-lib/flowy-ai/src/text/entities.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/frontend/rust-lib/flowy-ai/src/text/mod.rs b/frontend/rust-lib/flowy-ai/src/text/mod.rs deleted file mode 100644 index 26bf90966b..0000000000 --- a/frontend/rust-lib/flowy-ai/src/text/mod.rs +++ /dev/null @@ -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; -} diff --git a/frontend/rust-lib/flowy-ai/src/text/open_ai.rs b/frontend/rust-lib/flowy-ai/src/text/open_ai.rs deleted file mode 100644 index 5138be23ac..0000000000 --- a/frontend/rust-lib/flowy-ai/src/text/open_ai.rs +++ /dev/null @@ -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, -} - -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 { - let response = self.client.completions().create(params).await?; - Ok(response) - } -} diff --git a/frontend/rust-lib/flowy-ai/src/text/stability_ai.rs b/frontend/rust-lib/flowy-ai/src/text/stability_ai.rs deleted file mode 100644 index aa342d2961..0000000000 --- a/frontend/rust-lib/flowy-ai/src/text/stability_ai.rs +++ /dev/null @@ -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 { - todo!() - } -} diff --git a/frontend/rust-lib/flowy-ai/tests/main.rs b/frontend/rust-lib/flowy-ai/tests/main.rs deleted file mode 100644 index 78e95f65e4..0000000000 --- a/frontend/rust-lib/flowy-ai/tests/main.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod text; -mod util; diff --git a/frontend/rust-lib/flowy-ai/tests/text/completion_test.rs b/frontend/rust-lib/flowy-ai/tests/text/completion_test.rs deleted file mode 100644 index c466f160d6..0000000000 --- a/frontend/rust-lib/flowy-ai/tests/text/completion_test.rs +++ /dev/null @@ -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); - } -} diff --git a/frontend/rust-lib/flowy-ai/tests/text/mod.rs b/frontend/rust-lib/flowy-ai/tests/text/mod.rs deleted file mode 100644 index 4b785ba69e..0000000000 --- a/frontend/rust-lib/flowy-ai/tests/text/mod.rs +++ /dev/null @@ -1 +0,0 @@ -// mod completion_test; diff --git a/frontend/rust-lib/flowy-ai/tests/util/mod.rs b/frontend/rust-lib/flowy-ai/tests/util/mod.rs deleted file mode 100644 index 42bdd7c921..0000000000 --- a/frontend/rust-lib/flowy-ai/tests/util/mod.rs +++ /dev/null @@ -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 { - dotenv::from_filename(".env").ok()?; - OpenAISetting::from_env().ok() -} diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/user_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/user_deps.rs index ff5931d473..b8d18af390 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/user_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/user_deps.rs @@ -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(()) } diff --git a/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs b/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs index f0ed50267e..a4c3638d41 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs @@ -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 { + 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 { diff --git a/frontend/rust-lib/flowy-database-pub/src/cloud.rs b/frontend/rust-lib/flowy-database-pub/src/cloud.rs index 4b392fd9dc..5df6325362 100644 --- a/frontend/rust-lib/flowy-database-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-database-pub/src/cloud.rs @@ -5,7 +5,7 @@ use lib_infra::future::FutureResult; use std::collections::HashMap; pub type CollabDocStateByOid = HashMap; - +pub type SummaryRowContent = HashMap; /// 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, Error>; + + fn summary_database_row( + &self, + workspace_id: &str, + object_id: &str, + summary_row: SummaryRowContent, + ) -> FutureResult; } pub struct DatabaseSnapshot { diff --git a/frontend/rust-lib/flowy-database2/src/entities/field_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/field_entities.rs index 6387ff00ed..ad8635d80d 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/field_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/field_entities.rs @@ -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() } diff --git a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/util.rs b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/util.rs index 6d48abf0e8..7bcd2292bf 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/util.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/util.rs @@ -101,11 +101,14 @@ impl From<&Filter> for FilterPB { .cloned::() .unwrap() .try_into(), - FieldType::Relation => condition_and_content .cloned::() .unwrap() .try_into(), + FieldType::Summary => condition_and_content + .cloned::() + .unwrap() + .try_into(), }; Self { @@ -150,6 +153,9 @@ impl TryFrom 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 { diff --git a/frontend/rust-lib/flowy-database2/src/entities/macros.rs b/frontend/rust-lib/flowy-database2/src/entities/macros.rs index 032785dc0f..14c0613442 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/macros.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/macros.rs @@ -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 diff --git a/frontend/rust-lib/flowy-database2/src/entities/row_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/row_entities.rs index 1e0e63a0f2..04d1d70729 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/row_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/row_entities.rs @@ -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, +} diff --git a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/mod.rs b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/mod.rs index f97afeb75b..deeb260f0e 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/mod.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/mod.rs @@ -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::*; diff --git a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/summary_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/summary_entities.rs new file mode 100644 index 0000000000..c8f4a9b5c4 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/summary_entities.rs @@ -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 for SummarizationTypeOptionPB { + fn from(value: SummarizationTypeOption) -> Self { + SummarizationTypeOptionPB { + auto_fill: value.auto_fill, + } + } +} + +impl From for SummarizationTypeOption { + fn from(value: SummarizationTypeOptionPB) -> Self { + SummarizationTypeOption { + auto_fill: value.auto_fill, + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/event_handler.rs b/frontend/rust-lib/flowy-database2/src/event_handler.rs index 92b8223593..ebd621eb87 100644 --- a/frontend/rust-lib/flowy-database2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-database2/src/event_handler.rs @@ -465,7 +465,7 @@ pub(crate) async fn update_cell_handler( database_editor .update_cell_with_changeset( ¶ms.view_id, - RowId::from(params.row_id), + &RowId::from(params.row_id), ¶ms.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( ¶ms.cell_identifier.view_id, - params.cell_identifier.row_id, + ¶ms.cell_identifier.row_id, ¶ms.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( ¶ms.view_id, - params.row_id, + ¶ms.row_id, ¶ms.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, + manager: AFPluginState>, +) -> 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(()) +} diff --git a/frontend/rust-lib/flowy-database2/src/event_map.rs b/frontend/rust-lib/flowy-database2/src/event_map.rs index 12c859fd13..97f390771c 100644 --- a/frontend/rust-lib/flowy-database2/src/event_map.rs +++ b/frontend/rust-lib/flowy-database2/src/event_map.rs @@ -84,11 +84,13 @@ pub fn init(database_manager: Weak) -> 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, } diff --git a/frontend/rust-lib/flowy-database2/src/manager.rs b/frontend/rust-lib/flowy-database2/src/manager.rs index f5ac2a7821..21858ad201 100644 --- a/frontend/rust-lib/flowy-database2/src/manager.rs +++ b/frontend/rust-lib/flowy-database2/src/manager.rs @@ -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 { - 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 { 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>, ) -> 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 { - 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> { 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>(&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>(&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> { - 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> { + /// 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> { 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 { diff --git a/frontend/rust-lib/flowy-database2/src/services/cell/cell_operation.rs b/frontend/rust-lib/flowy-database2/src/services/cell/cell_operation.rs index 13e76d1a17..7212c2fa54 100644 --- a/frontend/rust-lib/flowy-database2/src/services/cell/cell_operation.rs +++ b/frontend/rust-lib/flowy-database2/src/services/cell/cell_operation.rs @@ -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)); + }, } } } diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs index 9067ebfe95..1c62e70b93 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs @@ -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, ) { - 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(), diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/mod.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/mod.rs index f4cd13d020..44d7329567 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/mod.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/mod.rs @@ -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; diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/summary_type_option/mod.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/summary_type_option/mod.rs new file mode 100644 index 0000000000..e927cc4feb --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/summary_type_option/mod.rs @@ -0,0 +1,2 @@ +pub mod summary; +pub mod summary_entities; diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/summary_type_option/summary.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/summary_type_option/summary.rs new file mode 100644 index 0000000000..920f76de8e --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/summary_type_option/summary.rs @@ -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 for SummarizationTypeOption { + fn from(value: TypeOptionData) -> Self { + let auto_fill = value.get_bool_value("auto_fill").unwrap_or_default(); + Self { auto_fill } + } +} + +impl From 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, + ) -> FlowyResult<(Cell, SummaryCellData)> { + let cell_data = SummaryCellData(changeset); + Ok((cell_data.clone().into(), cell_data)) + } +} + +impl TypeOptionCellDataFilter for SummarizationTypeOption { + fn apply_filter( + &self, + filter: &::CellFilter, + cell_data: &::CellData, + ) -> bool { + filter.is_visible(cell_data) + } +} + +impl TypeOptionCellDataCompare for SummarizationTypeOption { + fn apply_cmp( + &self, + cell_data: &::CellData, + other_cell_data: &::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 { + Ok(SummaryCellData::from(cell)) + } + + fn stringify_cell_data(&self, cell_data: SummaryCellData) -> String { + cell_data.to_string() + } + + fn numeric_cell(&self, _cell: &Cell) -> Option { + None + } +} +impl TypeOptionTransform for SummarizationTypeOption {} + +impl TypeOptionCellDataSerde for SummarizationTypeOption { + fn protobuf_encode( + &self, + cell_data: ::CellData, + ) -> ::CellProtobufType { + ProtobufStr::from(cell_data.0) + } + + fn parse_cell(&self, cell: &Cell) -> FlowyResult<::CellData> { + Ok(SummaryCellData::from(cell)) + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/summary_type_option/summary_entities.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/summary_type_option/summary_entities.rs new file mode 100644 index 0000000000..8d45578e38 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/summary_type_option/summary_entities.rs @@ -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 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 for SummaryCellData { + fn as_ref(&self) -> &str { + &self.0 + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_type_option.rs index 02b5b07806..e8c3e8b9d8 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_type_option.rs @@ -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<::CellData> { - Ok(StrCellData::from(cell)) + Ok(StringCellData::from(cell)) } } impl CellDataDecoder for RichTextTypeOption { fn decode_cell(&self, cell: &Cell) -> FlowyResult<::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 { - StrCellData::from(cell).0.parse::().ok() + StringCellData::from(cell).0.parse::().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 for Cell { - fn from(data: StrCellData) -> Self { +impl From 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 for StrCellData { +impl std::convert::From 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 for String { - fn from(value: StrCellData) -> Self { +impl std::convert::From 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 for StrCellData { +impl AsRef for StringCellData { fn as_ref(&self) -> &str { self.0.as_str() } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option.rs index de026994e1..e86c4fd7e8 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option.rs @@ -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>( 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(), } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs index 27ac6ac5e2..7e145bffb7 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs @@ -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 { 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::(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 }, + FieldType::Summary => Box::new(SummarizationTypeOption::from(type_option_data)) + as Box, } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field_settings/entities.rs b/frontend/rust-lib/flowy-database2/src/services/field_settings/entities.rs index 586539df45..9f9e82311f 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field_settings/entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field_settings/entities.rs @@ -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(), diff --git a/frontend/rust-lib/flowy-database2/src/services/field_settings/field_settings_builder.rs b/frontend/rust-lib/flowy-database2/src/services/field_settings/field_settings_builder.rs index 3ddbf16a8f..7602224acd 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field_settings/field_settings_builder.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field_settings/field_settings_builder.rs @@ -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 { diff --git a/frontend/rust-lib/flowy-database2/src/services/filter/entities.rs b/frontend/rust-lib/flowy-database2/src/services/filter/entities.rs index f12bc415d4..6a974cc3d5 100644 --- a/frontend/rust-lib/flowy-database2/src/services/filter/entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/filter/entities.rs @@ -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::()?; (filter.condition as u8, "".to_string()) }, + FieldType::Summary => { + let filter = condition_and_content.cloned::()?; + (filter.condition as u8, filter.content) + }, }; Some((condition, content)) }; diff --git a/frontend/rust-lib/flowy-database2/tests/database/cell_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/cell_test/script.rs index 85f826cd4d..833ce832b5 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/cell_test/script.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/cell_test/script.rs @@ -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(); }, diff --git a/frontend/rust-lib/flowy-database2/tests/database/cell_test/test.rs b/frontend/rust-lib/flowy-database2/tests/database/cell_test/test.rs index 9f7f531771..2ed9db16ff 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/cell_test/test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/cell_test/test.rs @@ -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(), ""), diff --git a/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs b/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs index 2ffb058a56..2d087cce00 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs @@ -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, - include_time: Option, - 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(&mut self, f: F) -> String - where - F: Fn(Vec) -> SelectOption, - { - let single_select_field = self.field_with_type(&FieldType::SingleSelect); - let type_option = single_select_field - .get_type_option::(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(&mut self, f: F) -> String - where - F: Fn(Vec) -> Vec, - { - let multi_select_field = self.field_with_type(&FieldType::MultiSelect); - let type_option = multi_select_field - .get_type_option::(FieldType::MultiSelect) - .unwrap(); - let options = f(type_option.options); - let ops_ids = options - .iter() - .map(|option| option.id.clone()) - .collect::>(); - 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, - } - } -} diff --git a/frontend/rust-lib/flowy-database2/tests/database/group_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/group_test/script.rs index 48f47b01e0..035795f88c 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/group_test/script.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/group_test/script.rs @@ -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(); }, diff --git a/frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs b/frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs index 3d51616e5d..44657d8c23 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs @@ -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); + }, } } diff --git a/frontend/rust-lib/flowy-database2/tests/database/mock_data/calendar_mock_data.rs b/frontend/rust-lib/flowy-database2/tests/database/mock_data/calendar_mock_data.rs index 81f59d4f14..4c7553f754 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/mock_data/calendar_mock_data.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/mock_data/calendar_mock_data.rs @@ -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(); diff --git a/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs b/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs index 4a4236d295..6ef8d08c3a 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs @@ -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); + }, } } diff --git a/frontend/rust-lib/flowy-database2/tests/database/share_test/export_test.rs b/frontend/rust-lib/flowy-database2/tests/database/share_test/export_test.rs index 6351ba0ceb..a54fd17996 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/share_test/export_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/share_test/export_test.rs @@ -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::::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!( diff --git a/frontend/rust-lib/flowy-folder/src/view_operation.rs b/frontend/rust-lib/flowy-folder/src/view_operation.rs index 5a296b659d..c5dfcf6007 100644 --- a/frontend/rust-lib/flowy-folder/src/view_operation.rs +++ b/frontend/rust-lib/flowy-folder/src/view_operation.rs @@ -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. diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs index 4eca4b8d58..6b2a67c67b 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs @@ -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, Error> { FutureResult::new(async move { Ok(vec![]) }) } + + fn summary_database_row( + &self, + workspace_id: &str, + _object_id: &str, + summary_row: SummaryRowContent, + ) -> FutureResult { + let workspace_id = workspace_id.to_string(); + let try_get_client = self.inner.try_get_client(); + FutureResult::new(async move { + let map: Map = 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) + }) + } } diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs index 9a4cad3445..71fc99b465 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs @@ -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, Error> { FutureResult::new(async move { Ok(vec![]) }) } + + fn summary_database_row( + &self, + _workspace_id: &str, + _object_id: &str, + _summary_row: SummaryRowContent, + ) -> FutureResult { + // TODO(lucas): local ai + FutureResult::new(async move { Ok("".to_string()) }) + } } diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/database.rs b/frontend/rust-lib/flowy-server/src/supabase/api/database.rs index 4fe1c395c4..9e7dd7765d 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/database.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/database.rs @@ -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 { + FutureResult::new(async move { Ok("".to_string()) }) + } }