mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-09-11 19:17:43 +03:00
feat: Implement summary field for database row (#5246)
* chore: impl summary field * chore: draft ui * chore: add summary event * chore: impl desktop ui * chore: impl mobile ui * chore: update test * chore: disable ai test
This commit is contained in:
parent
999ffeba21
commit
a69e83c2cb
2
.github/workflows/rust_ci.yaml
vendored
2
.github/workflows/rust_ci.yaml
vendored
@ -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
|
||||
|
@ -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
|
||||
|
@ -0,0 +1,111 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/field_info.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'summary_cell_bloc.freezed.dart';
|
||||
|
||||
class SummaryCellBloc extends Bloc<SummaryCellEvent, SummaryCellState> {
|
||||
SummaryCellBloc({
|
||||
required this.cellController,
|
||||
}) : super(SummaryCellState.initial(cellController)) {
|
||||
_dispatch();
|
||||
_startListening();
|
||||
}
|
||||
|
||||
final SummaryCellController cellController;
|
||||
void Function()? _onCellChangedFn;
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_onCellChangedFn != null) {
|
||||
cellController.removeListener(
|
||||
onCellChanged: _onCellChangedFn!,
|
||||
onFieldChanged: _onFieldChangedListener,
|
||||
);
|
||||
}
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _dispatch() {
|
||||
on<SummaryCellEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
didReceiveCellUpdate: (cellData) {
|
||||
emit(
|
||||
state.copyWith(content: cellData ?? ""),
|
||||
);
|
||||
},
|
||||
didUpdateField: (fieldInfo) {
|
||||
final wrap = fieldInfo.wrapCellContent;
|
||||
if (wrap != null) {
|
||||
emit(state.copyWith(wrap: wrap));
|
||||
}
|
||||
},
|
||||
updateCell: (text) async {
|
||||
if (state.content != text) {
|
||||
emit(state.copyWith(content: text));
|
||||
await cellController.saveCellData(text);
|
||||
|
||||
// If the input content is "abc" that can't parsered as number then the data stored in the backend will be an empty string.
|
||||
// So for every cell data that will be formatted in the backend.
|
||||
// It needs to get the formatted data after saving.
|
||||
add(
|
||||
SummaryCellEvent.didReceiveCellUpdate(
|
||||
cellController.getCellData() ?? "",
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
_onCellChangedFn = cellController.addListener(
|
||||
onCellChanged: (cellContent) {
|
||||
if (!isClosed) {
|
||||
add(
|
||||
SummaryCellEvent.didReceiveCellUpdate(cellContent ?? ""),
|
||||
);
|
||||
}
|
||||
},
|
||||
onFieldChanged: _onFieldChangedListener,
|
||||
);
|
||||
}
|
||||
|
||||
void _onFieldChangedListener(FieldInfo fieldInfo) {
|
||||
if (!isClosed) {
|
||||
add(SummaryCellEvent.didUpdateField(fieldInfo));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SummaryCellEvent with _$SummaryCellEvent {
|
||||
const factory SummaryCellEvent.didReceiveCellUpdate(String? cellContent) =
|
||||
_DidReceiveCellUpdate;
|
||||
const factory SummaryCellEvent.didUpdateField(FieldInfo fieldInfo) =
|
||||
_DidUpdateField;
|
||||
const factory SummaryCellEvent.updateCell(String text) = _UpdateCell;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SummaryCellState with _$SummaryCellState {
|
||||
const factory SummaryCellState({
|
||||
required String content,
|
||||
required bool wrap,
|
||||
}) = _SummaryCellState;
|
||||
|
||||
factory SummaryCellState.initial(SummaryCellController cellController) {
|
||||
final wrap = cellController.fieldInfo.wrapCellContent;
|
||||
return SummaryCellState(
|
||||
content: cellController.getCellData() ?? "",
|
||||
wrap: wrap ?? true,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'summary_row_bloc.freezed.dart';
|
||||
|
||||
class SummaryRowBloc extends Bloc<SummaryRowEvent, SummaryRowState> {
|
||||
SummaryRowBloc({
|
||||
required this.viewId,
|
||||
required this.rowId,
|
||||
required this.fieldId,
|
||||
}) : super(SummaryRowState.initial()) {
|
||||
_dispatch();
|
||||
}
|
||||
|
||||
final String viewId;
|
||||
final String rowId;
|
||||
final String fieldId;
|
||||
|
||||
void _dispatch() {
|
||||
on<SummaryRowEvent>(
|
||||
(event, emit) async {
|
||||
event.when(
|
||||
startSummary: () {
|
||||
final params = SummaryRowPB(
|
||||
viewId: viewId,
|
||||
rowId: rowId,
|
||||
fieldId: fieldId,
|
||||
);
|
||||
emit(
|
||||
state.copyWith(
|
||||
loadingState: const LoadingState.loading(),
|
||||
error: null,
|
||||
),
|
||||
);
|
||||
|
||||
DatabaseEventSummarizeRow(params).send().then(
|
||||
(result) => {
|
||||
if (!isClosed) add(SummaryRowEvent.finishSummary(result)),
|
||||
},
|
||||
);
|
||||
},
|
||||
finishSummary: (result) {
|
||||
result.fold(
|
||||
(s) => {
|
||||
emit(
|
||||
state.copyWith(
|
||||
loadingState: const LoadingState.finish(),
|
||||
error: null,
|
||||
),
|
||||
),
|
||||
},
|
||||
(err) => {
|
||||
emit(
|
||||
state.copyWith(
|
||||
loadingState: const LoadingState.finish(),
|
||||
error: err,
|
||||
),
|
||||
),
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SummaryRowEvent with _$SummaryRowEvent {
|
||||
const factory SummaryRowEvent.startSummary() = _DidStartSummary;
|
||||
const factory SummaryRowEvent.finishSummary(
|
||||
FlowyResult<void, FlowyError> result,
|
||||
) = _DidFinishSummary;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SummaryRowState with _$SummaryRowState {
|
||||
const factory SummaryRowState({
|
||||
required LoadingState loadingState,
|
||||
required FlowyError? error,
|
||||
}) = _SummaryRowState;
|
||||
|
||||
factory SummaryRowState.initial() {
|
||||
return const SummaryRowState(
|
||||
loadingState: LoadingState.finish(),
|
||||
error: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class LoadingState with _$LoadingState {
|
||||
const factory LoadingState.loading() = _Loading;
|
||||
const factory LoadingState.finish() = _Finish;
|
||||
}
|
@ -105,7 +105,7 @@ class TextCellState with _$TextCellState {
|
||||
|
||||
factory TextCellState.initial(TextCellController cellController) {
|
||||
final cellData = cellController.getCellData() ?? "";
|
||||
final wrap = cellController.fieldInfo.wrapCellContent ?? false;
|
||||
final wrap = cellController.fieldInfo.wrapCellContent ?? true;
|
||||
final emoji =
|
||||
cellController.fieldInfo.isPrimary ? cellController.icon ?? "" : "";
|
||||
|
||||
|
@ -15,6 +15,7 @@ typedef DateCellController = CellController<DateCellDataPB, String>;
|
||||
typedef TimestampCellController = CellController<TimestampCellDataPB, String>;
|
||||
typedef URLCellController = CellController<URLCellDataPB, String>;
|
||||
typedef RelationCellController = CellController<RelationCellDataPB, String>;
|
||||
typedef SummaryCellController = CellController<String, String>;
|
||||
|
||||
CellController makeCellController(
|
||||
DatabaseController databaseController,
|
||||
@ -132,6 +133,18 @@ CellController makeCellController(
|
||||
),
|
||||
cellDataPersistence: TextCellDataPersistence(),
|
||||
);
|
||||
case FieldType.Summary:
|
||||
return SummaryCellController(
|
||||
viewId: viewId,
|
||||
fieldController: fieldController,
|
||||
cellContext: cellContext,
|
||||
rowCache: rowCache,
|
||||
cellDataLoader: CellDataLoader(
|
||||
parser: StringCellDataParser(),
|
||||
reloadOnFieldChange: true,
|
||||
),
|
||||
cellDataPersistence: TextCellDataPersistence(),
|
||||
);
|
||||
}
|
||||
throw UnimplementedError;
|
||||
}
|
||||
|
@ -51,8 +51,13 @@ class CellDataLoader<T> {
|
||||
class StringCellDataParser implements CellDataParser<String> {
|
||||
@override
|
||||
String? parserData(List<int> data) {
|
||||
final s = utf8.decode(data);
|
||||
return s;
|
||||
try {
|
||||
final s = utf8.decode(data);
|
||||
return s;
|
||||
} catch (e) {
|
||||
Log.error("Failed to parse string data: $e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -62,14 +67,25 @@ class CheckboxCellDataParser implements CellDataParser<CheckboxCellDataPB> {
|
||||
if (data.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return CheckboxCellDataPB.fromBuffer(data);
|
||||
|
||||
try {
|
||||
return CheckboxCellDataPB.fromBuffer(data);
|
||||
} catch (e) {
|
||||
Log.error("Failed to parse checkbox data: $e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NumberCellDataParser implements CellDataParser<String> {
|
||||
@override
|
||||
String? parserData(List<int> data) {
|
||||
return utf8.decode(data);
|
||||
try {
|
||||
return utf8.decode(data);
|
||||
} catch (e) {
|
||||
Log.error("Failed to parse number data: $e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,7 +95,12 @@ class DateCellDataParser implements CellDataParser<DateCellDataPB> {
|
||||
if (data.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return DateCellDataPB.fromBuffer(data);
|
||||
try {
|
||||
return DateCellDataPB.fromBuffer(data);
|
||||
} catch (e) {
|
||||
Log.error("Failed to parse date data: $e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,7 +110,12 @@ class TimestampCellDataParser implements CellDataParser<TimestampCellDataPB> {
|
||||
if (data.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return TimestampCellDataPB.fromBuffer(data);
|
||||
try {
|
||||
return TimestampCellDataPB.fromBuffer(data);
|
||||
} catch (e) {
|
||||
Log.error("Failed to parse timestamp data: $e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,7 +126,12 @@ class SelectOptionCellDataParser
|
||||
if (data.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return SelectOptionCellDataPB.fromBuffer(data);
|
||||
try {
|
||||
return SelectOptionCellDataPB.fromBuffer(data);
|
||||
} catch (e) {
|
||||
Log.error("Failed to parse select option data: $e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -110,7 +141,13 @@ class ChecklistCellDataParser implements CellDataParser<ChecklistCellDataPB> {
|
||||
if (data.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return ChecklistCellDataPB.fromBuffer(data);
|
||||
|
||||
try {
|
||||
return ChecklistCellDataPB.fromBuffer(data);
|
||||
} catch (e) {
|
||||
Log.error("Failed to parse checklist data: $e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -120,13 +157,27 @@ class URLCellDataParser implements CellDataParser<URLCellDataPB> {
|
||||
if (data.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return URLCellDataPB.fromBuffer(data);
|
||||
try {
|
||||
return URLCellDataPB.fromBuffer(data);
|
||||
} catch (e) {
|
||||
Log.error("Failed to parse url data: $e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RelationCellDataParser implements CellDataParser<RelationCellDataPB> {
|
||||
@override
|
||||
RelationCellDataPB? parserData(List<int> data) {
|
||||
return data.isEmpty ? null : RelationCellDataPB.fromBuffer(data);
|
||||
if (data.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return RelationCellDataPB.fromBuffer(data);
|
||||
} catch (e) {
|
||||
Log.error("Failed to parse relation data: $e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ class GridRow extends StatefulWidget {
|
||||
});
|
||||
|
||||
final FieldController fieldController;
|
||||
final RowId viewId;
|
||||
final String viewId;
|
||||
final RowId rowId;
|
||||
final RowController rowController;
|
||||
final EditableCellBuilder cellBuilder;
|
||||
|
@ -11,6 +11,7 @@ import 'card_cell_skeleton/checklist_card_cell.dart';
|
||||
import 'card_cell_skeleton/date_card_cell.dart';
|
||||
import 'card_cell_skeleton/number_card_cell.dart';
|
||||
import 'card_cell_skeleton/select_option_card_cell.dart';
|
||||
import 'card_cell_skeleton/summary_card_cell.dart';
|
||||
import 'card_cell_skeleton/text_card_cell.dart';
|
||||
import 'card_cell_skeleton/url_card_cell.dart';
|
||||
|
||||
@ -91,6 +92,12 @@ class CardCellBuilder {
|
||||
databaseController: databaseController,
|
||||
cellContext: cellContext,
|
||||
),
|
||||
FieldType.Summary => SummaryCardCell(
|
||||
key: key,
|
||||
style: isStyleOrNull(style),
|
||||
databaseController: databaseController,
|
||||
cellContext: cellContext,
|
||||
),
|
||||
_ => throw UnimplementedError,
|
||||
};
|
||||
}
|
||||
|
@ -0,0 +1,62 @@
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/summary_cell_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'card_cell.dart';
|
||||
|
||||
class SummaryCardCellStyle extends CardCellStyle {
|
||||
const SummaryCardCellStyle({
|
||||
required super.padding,
|
||||
required this.textStyle,
|
||||
});
|
||||
|
||||
final TextStyle textStyle;
|
||||
}
|
||||
|
||||
class SummaryCardCell extends CardCell<SummaryCardCellStyle> {
|
||||
const SummaryCardCell({
|
||||
super.key,
|
||||
required super.style,
|
||||
required this.databaseController,
|
||||
required this.cellContext,
|
||||
});
|
||||
|
||||
final DatabaseController databaseController;
|
||||
final CellContext cellContext;
|
||||
|
||||
@override
|
||||
State<SummaryCardCell> createState() => _SummaryCellState();
|
||||
}
|
||||
|
||||
class _SummaryCellState extends State<SummaryCardCell> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) {
|
||||
return SummaryCellBloc(
|
||||
cellController: makeCellController(
|
||||
widget.databaseController,
|
||||
widget.cellContext,
|
||||
).as(),
|
||||
);
|
||||
},
|
||||
child: BlocBuilder<SummaryCellBloc, SummaryCellState>(
|
||||
buildWhen: (previous, current) => previous.content != current.content,
|
||||
builder: (context, state) {
|
||||
if (state.content.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Container(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
padding: widget.style.padding,
|
||||
child: Text(state.content, style: widget.style.textStyle),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/summary_card_cell.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@ -79,5 +80,9 @@ CardCellStyleMap desktopCalendarCardCellStyleMap(BuildContext context) {
|
||||
wrap: true,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
FieldType.Summary: SummaryCardCellStyle(
|
||||
padding: padding,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/summary_card_cell.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@ -79,5 +80,9 @@ CardCellStyleMap desktopBoardCardCellStyleMap(BuildContext context) {
|
||||
wrap: true,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
FieldType.Summary: SummaryCardCellStyle(
|
||||
padding: padding,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/summary_card_cell.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@ -78,5 +79,9 @@ CardCellStyleMap mobileBoardCardCellStyleMap(BuildContext context) {
|
||||
textStyle: textStyle,
|
||||
wrap: true,
|
||||
),
|
||||
FieldType.Summary: SummaryCardCellStyle(
|
||||
padding: padding,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
@ -0,0 +1,94 @@
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/summary_cell_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/summary.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
class DesktopGridSummaryCellSkin extends IEditableSummaryCellSkin {
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
CellContainerNotifier cellContainerNotifier,
|
||||
SummaryCellBloc bloc,
|
||||
FocusNode focusNode,
|
||||
TextEditingController textEditingController,
|
||||
) {
|
||||
return ChangeNotifierProvider(
|
||||
create: (_) => SummaryMouseNotifier(),
|
||||
builder: (context, child) {
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
opaque: false,
|
||||
onEnter: (p) =>
|
||||
Provider.of<SummaryMouseNotifier>(context, listen: false)
|
||||
.onEnter = true,
|
||||
onExit: (p) =>
|
||||
Provider.of<SummaryMouseNotifier>(context, listen: false)
|
||||
.onEnter = false,
|
||||
child: Stack(
|
||||
children: [
|
||||
TextField(
|
||||
controller: textEditingController,
|
||||
enabled: false,
|
||||
focusNode: focusNode,
|
||||
onEditingComplete: () => focusNode.unfocus(),
|
||||
onSubmitted: (_) => focusNode.unfocus(),
|
||||
maxLines: null,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
textInputAction: TextInputAction.done,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: GridSize.cellContentInsets,
|
||||
border: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
errorBorder: InputBorder.none,
|
||||
disabledBorder: InputBorder.none,
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: GridSize.cellVPadding,
|
||||
),
|
||||
child: Consumer<SummaryMouseNotifier>(
|
||||
builder: (
|
||||
BuildContext context,
|
||||
SummaryMouseNotifier notifier,
|
||||
Widget? child,
|
||||
) {
|
||||
if (notifier.onEnter) {
|
||||
return SummaryCellAccessory(
|
||||
viewId: bloc.cellController.viewId,
|
||||
fieldId: bloc.cellController.fieldId,
|
||||
rowId: bloc.cellController.rowId,
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
),
|
||||
).positioned(right: 0, bottom: 0),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SummaryMouseNotifier extends ChangeNotifier {
|
||||
SummaryMouseNotifier();
|
||||
|
||||
bool _onEnter = false;
|
||||
|
||||
set onEnter(bool value) {
|
||||
if (_onEnter != value) {
|
||||
_onEnter = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
bool get onEnter => _onEnter;
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/summary_cell_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/summary.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DesktopRowDetailSummaryCellSkin extends IEditableSummaryCellSkin {
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
CellContainerNotifier cellContainerNotifier,
|
||||
SummaryCellBloc bloc,
|
||||
FocusNode focusNode,
|
||||
TextEditingController textEditingController,
|
||||
) {
|
||||
return Column(
|
||||
children: [
|
||||
TextField(
|
||||
controller: textEditingController,
|
||||
focusNode: focusNode,
|
||||
onEditingComplete: () => focusNode.unfocus(),
|
||||
onSubmitted: (_) => focusNode.unfocus(),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
textInputAction: TextInputAction.done,
|
||||
maxLines: null,
|
||||
minLines: 1,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: GridSize.cellContentInsets,
|
||||
border: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
errorBorder: InputBorder.none,
|
||||
disabledBorder: InputBorder.none,
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
const Spacer(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: SummaryCellAccessory(
|
||||
viewId: bloc.cellController.viewId,
|
||||
fieldId: bloc.cellController.fieldId,
|
||||
rowId: bloc.cellController.rowId,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -15,6 +15,7 @@ import 'editable_cell_skeleton/date.dart';
|
||||
import 'editable_cell_skeleton/number.dart';
|
||||
import 'editable_cell_skeleton/relation.dart';
|
||||
import 'editable_cell_skeleton/select_option.dart';
|
||||
import 'editable_cell_skeleton/summary.dart';
|
||||
import 'editable_cell_skeleton/text.dart';
|
||||
import 'editable_cell_skeleton/timestamp.dart';
|
||||
import 'editable_cell_skeleton/url.dart';
|
||||
@ -113,6 +114,12 @@ class EditableCellBuilder {
|
||||
skin: IEditableRelationCellSkin.fromStyle(style),
|
||||
key: key,
|
||||
),
|
||||
FieldType.Summary => EditableSummaryCell(
|
||||
databaseController: databaseController,
|
||||
cellContext: cellContext,
|
||||
skin: IEditableSummaryCellSkin.fromStyle(style),
|
||||
key: key,
|
||||
),
|
||||
_ => throw UnimplementedError(),
|
||||
};
|
||||
}
|
||||
|
@ -0,0 +1,249 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/summary_cell_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/summary_row_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/desktop_grid/desktop_grid_summary_cell.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_summary_cell.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/mobile_grid/mobile_grid_summary_cell.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_summary_cell.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
abstract class IEditableSummaryCellSkin {
|
||||
const IEditableSummaryCellSkin();
|
||||
|
||||
factory IEditableSummaryCellSkin.fromStyle(EditableCellStyle style) {
|
||||
return switch (style) {
|
||||
EditableCellStyle.desktopGrid => DesktopGridSummaryCellSkin(),
|
||||
EditableCellStyle.desktopRowDetail => DesktopRowDetailSummaryCellSkin(),
|
||||
EditableCellStyle.mobileGrid => MobileGridSummaryCellSkin(),
|
||||
EditableCellStyle.mobileRowDetail => MobileRowDetailSummaryCellSkin(),
|
||||
};
|
||||
}
|
||||
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
CellContainerNotifier cellContainerNotifier,
|
||||
SummaryCellBloc bloc,
|
||||
FocusNode focusNode,
|
||||
TextEditingController textEditingController,
|
||||
);
|
||||
}
|
||||
|
||||
class EditableSummaryCell extends EditableCellWidget {
|
||||
EditableSummaryCell({
|
||||
super.key,
|
||||
required this.databaseController,
|
||||
required this.cellContext,
|
||||
required this.skin,
|
||||
});
|
||||
|
||||
final DatabaseController databaseController;
|
||||
final CellContext cellContext;
|
||||
final IEditableSummaryCellSkin skin;
|
||||
|
||||
@override
|
||||
GridEditableTextCell<EditableSummaryCell> createState() =>
|
||||
_SummaryCellState();
|
||||
}
|
||||
|
||||
class _SummaryCellState extends GridEditableTextCell<EditableSummaryCell> {
|
||||
late final TextEditingController _textEditingController;
|
||||
late final cellBloc = SummaryCellBloc(
|
||||
cellController: makeCellController(
|
||||
widget.databaseController,
|
||||
widget.cellContext,
|
||||
).as(),
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_textEditingController =
|
||||
TextEditingController(text: cellBloc.state.content);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_textEditingController.dispose();
|
||||
cellBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: cellBloc,
|
||||
child: BlocListener<SummaryCellBloc, SummaryCellState>(
|
||||
listener: (context, state) {
|
||||
_textEditingController.text = state.content;
|
||||
},
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
return widget.skin.build(
|
||||
context,
|
||||
widget.cellContainerNotifier,
|
||||
cellBloc,
|
||||
focusNode,
|
||||
_textEditingController,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
|
||||
|
||||
@override
|
||||
void onRequestFocus() {
|
||||
focusNode.requestFocus();
|
||||
}
|
||||
|
||||
@override
|
||||
String? onCopy() => cellBloc.state.content;
|
||||
|
||||
@override
|
||||
Future<void> focusChanged() {
|
||||
if (mounted &&
|
||||
!cellBloc.isClosed &&
|
||||
cellBloc.state.content != _textEditingController.text.trim()) {
|
||||
cellBloc
|
||||
.add(SummaryCellEvent.updateCell(_textEditingController.text.trim()));
|
||||
}
|
||||
return super.focusChanged();
|
||||
}
|
||||
}
|
||||
|
||||
class SummaryCellAccessory extends StatelessWidget {
|
||||
const SummaryCellAccessory({
|
||||
required this.viewId,
|
||||
required this.rowId,
|
||||
required this.fieldId,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String viewId;
|
||||
final String rowId;
|
||||
final String fieldId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => SummaryRowBloc(
|
||||
viewId: viewId,
|
||||
rowId: rowId,
|
||||
fieldId: fieldId,
|
||||
),
|
||||
child: BlocBuilder<SummaryRowBloc, SummaryRowState>(
|
||||
builder: (context, state) {
|
||||
return const Row(
|
||||
children: [SummaryButton(), HSpace(6), CopyButton()],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SummaryButton extends StatelessWidget {
|
||||
const SummaryButton({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<SummaryRowBloc, SummaryRowState>(
|
||||
builder: (context, state) {
|
||||
return state.loadingState.map(
|
||||
loading: (_) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
);
|
||||
},
|
||||
finish: (_) {
|
||||
return FlowyTooltip(
|
||||
message: LocaleKeys.tooltip_genSummary.tr(),
|
||||
child: Container(
|
||||
width: 26,
|
||||
height: 26,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.fromBorderSide(
|
||||
BorderSide(color: Theme.of(context).dividerColor),
|
||||
),
|
||||
borderRadius: Corners.s6Border,
|
||||
),
|
||||
child: FlowyIconButton(
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
fillColor: Theme.of(context).cardColor,
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.ai_summary_generate_s,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
onPressed: () {
|
||||
context
|
||||
.read<SummaryRowBloc>()
|
||||
.add(const SummaryRowEvent.startSummary());
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CopyButton extends StatelessWidget {
|
||||
const CopyButton({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<SummaryCellBloc, SummaryCellState>(
|
||||
builder: (blocContext, state) {
|
||||
return FlowyTooltip(
|
||||
message: LocaleKeys.settings_menu_clickToCopy.tr(),
|
||||
child: Container(
|
||||
width: 26,
|
||||
height: 26,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.fromBorderSide(
|
||||
BorderSide(color: Theme.of(context).dividerColor),
|
||||
),
|
||||
borderRadius: Corners.s6Border,
|
||||
),
|
||||
child: FlowyIconButton(
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
fillColor: Theme.of(context).cardColor,
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.ai_copy_s,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: state.content));
|
||||
showMessageToast(LocaleKeys.grid_row_copyProperty.tr());
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/summary_cell_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/desktop_grid/desktop_grid_summary_cell.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/summary.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
class MobileGridSummaryCellSkin extends IEditableSummaryCellSkin {
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
CellContainerNotifier cellContainerNotifier,
|
||||
SummaryCellBloc bloc,
|
||||
FocusNode focusNode,
|
||||
TextEditingController textEditingController,
|
||||
) {
|
||||
return ChangeNotifierProvider(
|
||||
create: (_) => SummaryMouseNotifier(),
|
||||
builder: (context, child) {
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
opaque: false,
|
||||
onEnter: (p) =>
|
||||
Provider.of<SummaryMouseNotifier>(context, listen: false)
|
||||
.onEnter = true,
|
||||
onExit: (p) =>
|
||||
Provider.of<SummaryMouseNotifier>(context, listen: false)
|
||||
.onEnter = false,
|
||||
child: Stack(
|
||||
children: [
|
||||
TextField(
|
||||
controller: textEditingController,
|
||||
enabled: false,
|
||||
focusNode: focusNode,
|
||||
onEditingComplete: () => focusNode.unfocus(),
|
||||
onSubmitted: (_) => focusNode.unfocus(),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
textInputAction: TextInputAction.done,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: GridSize.cellContentInsets,
|
||||
border: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
errorBorder: InputBorder.none,
|
||||
disabledBorder: InputBorder.none,
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: GridSize.cellVPadding,
|
||||
),
|
||||
child: Consumer<SummaryMouseNotifier>(
|
||||
builder: (
|
||||
BuildContext context,
|
||||
SummaryMouseNotifier notifier,
|
||||
Widget? child,
|
||||
) {
|
||||
if (notifier.onEnter) {
|
||||
return SummaryCellAccessory(
|
||||
viewId: bloc.cellController.viewId,
|
||||
fieldId: bloc.cellController.fieldId,
|
||||
rowId: bloc.cellController.rowId,
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
),
|
||||
).positioned(right: 0, bottom: 0),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/summary_cell_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/summary.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MobileRowDetailSummaryCellSkin extends IEditableSummaryCellSkin {
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
CellContainerNotifier cellContainerNotifier,
|
||||
SummaryCellBloc bloc,
|
||||
FocusNode focusNode,
|
||||
TextEditingController textEditingController,
|
||||
) {
|
||||
return Column(
|
||||
children: [
|
||||
TextField(
|
||||
controller: textEditingController,
|
||||
focusNode: focusNode,
|
||||
onEditingComplete: () => focusNode.unfocus(),
|
||||
onSubmitted: (_) => focusNode.unfocus(),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
textInputAction: TextInputAction.done,
|
||||
maxLines: null,
|
||||
minLines: 1,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: GridSize.cellContentInsets,
|
||||
border: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
errorBorder: InputBorder.none,
|
||||
disabledBorder: InputBorder.none,
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
const Spacer(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: SummaryCellAccessory(
|
||||
viewId: bloc.cellController.viewId,
|
||||
fieldId: bloc.cellController.fieldId,
|
||||
rowId: bloc.cellController.rowId,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -20,6 +20,7 @@ const List<FieldType> _supportedFieldTypes = [
|
||||
FieldType.LastEditedTime,
|
||||
FieldType.CreatedTime,
|
||||
FieldType.Relation,
|
||||
FieldType.Summary,
|
||||
];
|
||||
|
||||
class FieldTypeList extends StatelessWidget with FlowyOverlayDelegate {
|
||||
|
@ -12,6 +12,7 @@ import 'number.dart';
|
||||
import 'relation.dart';
|
||||
import 'rich_text.dart';
|
||||
import 'single_select.dart';
|
||||
import 'summary.dart';
|
||||
import 'timestamp.dart';
|
||||
import 'url.dart';
|
||||
|
||||
@ -31,6 +32,7 @@ abstract class TypeOptionEditorFactory {
|
||||
FieldType.Checkbox => const CheckboxTypeOptionEditorFactory(),
|
||||
FieldType.Checklist => const ChecklistTypeOptionEditorFactory(),
|
||||
FieldType.Relation => const RelationTypeOptionEditorFactory(),
|
||||
FieldType.Summary => const SummaryTypeOptionEditorFactory(),
|
||||
_ => throw UnimplementedError(),
|
||||
};
|
||||
}
|
||||
|
@ -0,0 +1,19 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'builder.dart';
|
||||
|
||||
class SummaryTypeOptionEditorFactory implements TypeOptionEditorFactory {
|
||||
const SummaryTypeOptionEditorFactory();
|
||||
|
||||
@override
|
||||
Widget? build({
|
||||
required BuildContext context,
|
||||
required String viewId,
|
||||
required FieldPB field,
|
||||
required PopoverMutex popoverMutex,
|
||||
required TypeOptionDataCallback onTypeOptionUpdated,
|
||||
}) =>
|
||||
null;
|
||||
}
|
@ -69,24 +69,32 @@ class _PrimaryCellAccessoryState extends State<PrimaryCellAccessory>
|
||||
with GridCellAccessoryState {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyTooltip(
|
||||
message: LocaleKeys.tooltip_openAsPage.tr(),
|
||||
child: Container(
|
||||
width: 26,
|
||||
height: 26,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.fromBorderSide(
|
||||
BorderSide(color: Theme.of(context).dividerColor),
|
||||
),
|
||||
borderRadius: Corners.s6Border,
|
||||
),
|
||||
child: Center(
|
||||
child: FlowySvg(
|
||||
FlowySvgs.full_view_s,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
return FlowyHover(
|
||||
style: HoverStyle(
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
backgroundColor: Theme.of(context).cardColor,
|
||||
),
|
||||
builder: (_, onHover) {
|
||||
return FlowyTooltip(
|
||||
message: LocaleKeys.tooltip_openAsPage.tr(),
|
||||
child: Container(
|
||||
width: 26,
|
||||
height: 26,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.fromBorderSide(
|
||||
BorderSide(color: Theme.of(context).dividerColor),
|
||||
),
|
||||
borderRadius: Corners.s6Border,
|
||||
),
|
||||
child: Center(
|
||||
child: FlowySvg(
|
||||
FlowySvgs.full_view_s,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -166,17 +174,10 @@ class CellAccessoryContainer extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final children =
|
||||
accessories.where((accessory) => accessory.enable()).map((accessory) {
|
||||
final hover = FlowyHover(
|
||||
style: HoverStyle(
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
backgroundColor: Theme.of(context).cardColor,
|
||||
),
|
||||
builder: (_, onHover) => accessory.build(),
|
||||
);
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => accessory.onTap(),
|
||||
child: hover,
|
||||
child: accessory.build(),
|
||||
);
|
||||
}).toList();
|
||||
|
||||
|
@ -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(),
|
||||
};
|
||||
}
|
||||
|
3
frontend/resources/flowy_icons/16x/ai_copy.svg
Normal file
3
frontend/resources/flowy_icons/16x/ai_copy.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.6663 0.666504H2.66634C1.93301 0.666504 1.33301 1.2665 1.33301 1.99984V11.3332H2.66634V1.99984H10.6663V0.666504ZM9.99967 3.33317H5.33301C4.59967 3.33317 4.00634 3.93317 4.00634 4.6665L3.99967 13.9998C3.99967 14.7332 4.59301 15.3332 5.32634 15.3332H12.6663C13.3997 15.3332 13.9997 14.7332 13.9997 13.9998V7.33317L9.99967 3.33317ZM5.33301 13.9998V4.6665H9.33301V7.99984H12.6663V13.9998H5.33301Z" fill="#1F2329"/>
|
||||
</svg>
|
After Width: | Height: | Size: 526 B |
3
frontend/resources/flowy_icons/16x/ai_summary.svg
Normal file
3
frontend/resources/flowy_icons/16x/ai_summary.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.44667 3.33333L12.6667 6.55333V12.6667H3.33333V3.33333H9.44667ZM9.44667 2H3.33333C2.6 2 2 2.6 2 3.33333V12.6667C2 13.4 2.6 14 3.33333 14H12.6667C13.4 14 14 13.4 14 12.6667V6.55333C14 6.2 13.86 5.86 13.6067 5.61333L10.3867 2.39333C10.14 2.14 9.8 2 9.44667 2ZM4.66667 10H11.3333V11.3333H4.66667V10ZM4.66667 7.33333H11.3333V8.66667H4.66667V7.33333ZM4.66667 4.66667H9.33333V6H4.66667V4.66667Z" fill="#8F959E"/>
|
||||
</svg>
|
After Width: | Height: | Size: 521 B |
@ -0,0 +1,3 @@
|
||||
<svg width="19" height="18" viewBox="0 0 19 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.87687 3.99L11.0019 4.5L10.4919 3.375L11.0019 2.25L9.87687 2.76L8.75187 2.25L9.26188 3.375L8.75187 4.5L9.87687 3.99ZM14.6619 4.47L16.2519 3.75L15.5319 5.34L16.2519 6.93L14.6619 6.21L13.0719 6.93L13.7919 5.34L13.0719 3.75L14.6619 4.47ZM8.87937 6.6225C9.70437 5.79 11.0469 5.79 11.8794 6.6225C12.7119 7.4475 12.7119 8.79 11.8794 9.6225L6.37437 15.1275C5.54938 15.96 4.20687 15.96 3.37437 15.1275C2.54187 14.3025 2.54187 12.96 3.37437 12.1275L8.87937 6.6225ZM9.32938 10.5825L11.0919 8.82C11.4819 8.43 11.4819 7.8 11.0919 7.41C10.7019 7.02 10.0719 7.02 9.68188 7.41L7.91937 9.1725C7.52938 9.5625 7.52938 10.1925 7.91937 10.5825C8.30937 10.9725 8.93937 10.9725 9.32938 10.5825ZM11.7519 11.25L14.0019 12.27L16.2519 11.25L15.2319 13.5L16.2519 15.75L14.0019 14.73L11.7519 15.75L12.7719 13.5L11.7519 11.25Z" fill="#750D7E"/>
|
||||
</svg>
|
After Width: | Height: | Size: 970 B |
@ -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",
|
||||
|
187
frontend/rust-lib/Cargo.lock
generated
187
frontend/rust-lib/Cargo.lock
generated
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -2,8 +2,15 @@ use std::collections::HashMap;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use bytes::Bytes;
|
||||
use collab_database::database::timestamp;
|
||||
use collab_database::fields::Field;
|
||||
use collab_database::rows::{Row, RowId};
|
||||
use flowy_database2::entities::*;
|
||||
use flowy_database2::event_map::DatabaseEvent;
|
||||
use flowy_database2::services::cell::CellBuilder;
|
||||
use flowy_database2::services::field::{
|
||||
MultiSelectTypeOption, SelectOption, SingleSelectTypeOption,
|
||||
};
|
||||
use flowy_database2::services::share::csv::CSVFormat;
|
||||
use flowy_folder::entities::*;
|
||||
use flowy_folder::event_map::FolderEvent;
|
||||
@ -25,6 +32,7 @@ impl EventIntegrationTest {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// The initial data can refer to the [FolderOperationHandler::create_view_with_view_data] method.
|
||||
pub async fn create_grid(&self, parent_id: &str, name: String, initial_data: Vec<u8>) -> ViewPB {
|
||||
let payload = CreateViewPayloadPB {
|
||||
parent_view_id: parent_id.to_string(),
|
||||
@ -199,6 +207,13 @@ impl EventIntegrationTest {
|
||||
.await
|
||||
.parse::<FieldPB>()
|
||||
}
|
||||
pub async fn summary_row(&self, data: SummaryRowPB) {
|
||||
EventBuilder::new(self.clone())
|
||||
.event(DatabaseEvent::SummarizeRow)
|
||||
.payload(data)
|
||||
.async_send()
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn create_row(
|
||||
&self,
|
||||
@ -324,6 +339,11 @@ impl EventIntegrationTest {
|
||||
.parse::<CellPB>()
|
||||
}
|
||||
|
||||
pub async fn get_text_cell(&self, view_id: &str, row_id: &str, field_id: &str) -> String {
|
||||
let cell = self.get_cell(view_id, row_id, field_id).await;
|
||||
String::from_utf8(cell.data).unwrap()
|
||||
}
|
||||
|
||||
pub async fn get_date_cell(&self, view_id: &str, row_id: &str, field_id: &str) -> DateCellDataPB {
|
||||
let cell = self.get_cell(view_id, row_id, field_id).await;
|
||||
DateCellDataPB::try_from(Bytes::from(cell.data)).unwrap()
|
||||
@ -513,3 +533,139 @@ impl EventIntegrationTest {
|
||||
.rows
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TestRowBuilder<'a> {
|
||||
database_id: &'a str,
|
||||
row_id: RowId,
|
||||
fields: &'a [Field],
|
||||
cell_build: CellBuilder<'a>,
|
||||
}
|
||||
|
||||
impl<'a> TestRowBuilder<'a> {
|
||||
pub fn new(database_id: &'a str, row_id: RowId, fields: &'a [Field]) -> Self {
|
||||
let cell_build = CellBuilder::with_cells(Default::default(), fields);
|
||||
Self {
|
||||
database_id,
|
||||
row_id,
|
||||
fields,
|
||||
cell_build,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_text_cell(&mut self, data: &str) -> String {
|
||||
let text_field = self.field_with_type(&FieldType::RichText);
|
||||
self
|
||||
.cell_build
|
||||
.insert_text_cell(&text_field.id, data.to_string());
|
||||
|
||||
text_field.id.clone()
|
||||
}
|
||||
|
||||
pub fn insert_number_cell(&mut self, data: &str) -> String {
|
||||
let number_field = self.field_with_type(&FieldType::Number);
|
||||
self
|
||||
.cell_build
|
||||
.insert_text_cell(&number_field.id, data.to_string());
|
||||
number_field.id.clone()
|
||||
}
|
||||
|
||||
pub fn insert_date_cell(
|
||||
&mut self,
|
||||
date: i64,
|
||||
time: Option<String>,
|
||||
include_time: Option<bool>,
|
||||
field_type: &FieldType,
|
||||
) -> String {
|
||||
let date_field = self.field_with_type(field_type);
|
||||
self
|
||||
.cell_build
|
||||
.insert_date_cell(&date_field.id, date, time, include_time);
|
||||
date_field.id.clone()
|
||||
}
|
||||
|
||||
pub fn insert_checkbox_cell(&mut self, data: &str) -> String {
|
||||
let checkbox_field = self.field_with_type(&FieldType::Checkbox);
|
||||
self
|
||||
.cell_build
|
||||
.insert_text_cell(&checkbox_field.id, data.to_string());
|
||||
|
||||
checkbox_field.id.clone()
|
||||
}
|
||||
|
||||
pub fn insert_url_cell(&mut self, content: &str) -> String {
|
||||
let url_field = self.field_with_type(&FieldType::URL);
|
||||
self
|
||||
.cell_build
|
||||
.insert_url_cell(&url_field.id, content.to_string());
|
||||
url_field.id.clone()
|
||||
}
|
||||
|
||||
pub fn insert_single_select_cell<F>(&mut self, f: F) -> String
|
||||
where
|
||||
F: Fn(Vec<SelectOption>) -> SelectOption,
|
||||
{
|
||||
let single_select_field = self.field_with_type(&FieldType::SingleSelect);
|
||||
let type_option = single_select_field
|
||||
.get_type_option::<SingleSelectTypeOption>(FieldType::SingleSelect)
|
||||
.unwrap();
|
||||
let option = f(type_option.options);
|
||||
self
|
||||
.cell_build
|
||||
.insert_select_option_cell(&single_select_field.id, vec![option.id]);
|
||||
|
||||
single_select_field.id.clone()
|
||||
}
|
||||
|
||||
pub fn insert_multi_select_cell<F>(&mut self, f: F) -> String
|
||||
where
|
||||
F: Fn(Vec<SelectOption>) -> Vec<SelectOption>,
|
||||
{
|
||||
let multi_select_field = self.field_with_type(&FieldType::MultiSelect);
|
||||
let type_option = multi_select_field
|
||||
.get_type_option::<MultiSelectTypeOption>(FieldType::MultiSelect)
|
||||
.unwrap();
|
||||
let options = f(type_option.options);
|
||||
let ops_ids = options
|
||||
.iter()
|
||||
.map(|option| option.id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
self
|
||||
.cell_build
|
||||
.insert_select_option_cell(&multi_select_field.id, ops_ids);
|
||||
|
||||
multi_select_field.id.clone()
|
||||
}
|
||||
|
||||
pub fn insert_checklist_cell(&mut self, options: Vec<(String, bool)>) -> String {
|
||||
let checklist_field = self.field_with_type(&FieldType::Checklist);
|
||||
self
|
||||
.cell_build
|
||||
.insert_checklist_cell(&checklist_field.id, options);
|
||||
checklist_field.id.clone()
|
||||
}
|
||||
|
||||
pub fn field_with_type(&self, field_type: &FieldType) -> Field {
|
||||
self
|
||||
.fields
|
||||
.iter()
|
||||
.find(|field| {
|
||||
let t_field_type = FieldType::from(field.field_type);
|
||||
&t_field_type == field_type
|
||||
})
|
||||
.unwrap()
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub fn build(self) -> Row {
|
||||
let timestamp = timestamp();
|
||||
Row {
|
||||
id: self.row_id,
|
||||
database_id: self.database_id.to_string(),
|
||||
cells: self.cell_build.build(),
|
||||
height: 60,
|
||||
visibility: true,
|
||||
modified_at: timestamp,
|
||||
created_at: timestamp,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,2 @@
|
||||
// mod summarize_row;
|
||||
mod util;
|
@ -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);
|
||||
}
|
@ -0,0 +1,126 @@
|
||||
use collab_database::database::{gen_database_id, gen_database_view_id, gen_row_id, DatabaseData};
|
||||
use collab_database::views::{DatabaseLayout, DatabaseView};
|
||||
use event_integration_test::database_event::TestRowBuilder;
|
||||
|
||||
use collab_database::fields::Field;
|
||||
use collab_database::rows::Row;
|
||||
use flowy_database2::entities::FieldType;
|
||||
use flowy_database2::services::field::summary_type_option::summary::SummarizationTypeOption;
|
||||
use flowy_database2::services::field::{
|
||||
FieldBuilder, NumberFormat, NumberTypeOption, SelectOption, SelectOptionColor,
|
||||
SingleSelectTypeOption,
|
||||
};
|
||||
use flowy_database2::services::field_settings::default_field_settings_for_fields;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn make_test_summary_grid() -> DatabaseData {
|
||||
let database_id = gen_database_id();
|
||||
let fields = create_fields();
|
||||
let field_settings = default_field_settings_for_fields(&fields, DatabaseLayout::Grid);
|
||||
|
||||
let single_select_field = fields
|
||||
.iter()
|
||||
.find(|field| field.field_type == FieldType::SingleSelect.value())
|
||||
.unwrap();
|
||||
|
||||
let options = single_select_field
|
||||
.type_options
|
||||
.get(&FieldType::SingleSelect.to_string())
|
||||
.cloned()
|
||||
.map(|t| SingleSelectTypeOption::from(t).options)
|
||||
.unwrap();
|
||||
|
||||
let rows = create_rows(&database_id, &fields, options);
|
||||
|
||||
let inline_view_id = gen_database_view_id();
|
||||
let view = DatabaseView {
|
||||
database_id: database_id.clone(),
|
||||
id: inline_view_id.clone(),
|
||||
name: "".to_string(),
|
||||
layout: DatabaseLayout::Grid,
|
||||
field_settings,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
DatabaseData {
|
||||
database_id,
|
||||
inline_view_id,
|
||||
views: vec![view],
|
||||
fields,
|
||||
rows,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn create_fields() -> Vec<Field> {
|
||||
let mut fields = Vec::new();
|
||||
for field_type in FieldType::iter() {
|
||||
match field_type {
|
||||
FieldType::RichText => fields.push(create_text_field("Product Name", true)),
|
||||
FieldType::Number => fields.push(create_number_field("Price", NumberFormat::USD)),
|
||||
FieldType::SingleSelect => fields.push(create_single_select_field("Status")),
|
||||
FieldType::Summary => fields.push(create_summary_field("AI summary")),
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
fields
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn create_rows(database_id: &str, fields: &[Field], _options: Vec<SelectOption>) -> Vec<Row> {
|
||||
let mut rows = Vec::new();
|
||||
let fruits = ["Apple", "Pear", "Banana", "Orange"];
|
||||
for (i, fruit) in fruits.iter().enumerate() {
|
||||
let mut row_builder = TestRowBuilder::new(database_id, gen_row_id(), fields);
|
||||
row_builder.insert_text_cell(fruit);
|
||||
row_builder.insert_number_cell(match i {
|
||||
0 => "2.6",
|
||||
1 => "1.6",
|
||||
2 => "3.6",
|
||||
_ => "1.2",
|
||||
});
|
||||
row_builder.insert_single_select_cell(|mut options| options.remove(i % options.len()));
|
||||
rows.push(row_builder.build());
|
||||
}
|
||||
rows
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn create_text_field(name: &str, primary: bool) -> Field {
|
||||
FieldBuilder::from_field_type(FieldType::RichText)
|
||||
.name(name)
|
||||
.primary(primary)
|
||||
.build()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn create_number_field(name: &str, format: NumberFormat) -> Field {
|
||||
let mut type_option = NumberTypeOption::default();
|
||||
type_option.set_format(format);
|
||||
FieldBuilder::new(FieldType::Number, type_option)
|
||||
.name(name)
|
||||
.build()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn create_single_select_field(name: &str) -> Field {
|
||||
let options = vec![
|
||||
SelectOption::with_color("COMPLETED", SelectOptionColor::Purple),
|
||||
SelectOption::with_color("PLANNED", SelectOptionColor::Orange),
|
||||
SelectOption::with_color("PAUSED", SelectOptionColor::Yellow),
|
||||
];
|
||||
let mut type_option = SingleSelectTypeOption::default();
|
||||
type_option.options.extend(options);
|
||||
FieldBuilder::new(FieldType::SingleSelect, type_option)
|
||||
.name(name)
|
||||
.build()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn create_summary_field(name: &str) -> Field {
|
||||
let type_option = SummarizationTypeOption { auto_fill: false };
|
||||
FieldBuilder::new(FieldType::Summary, type_option)
|
||||
.name(name)
|
||||
.build()
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
mod af_cloud;
|
||||
mod local_test;
|
||||
|
||||
// #[cfg(feature = "supabase_cloud_test")]
|
||||
|
@ -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"
|
@ -1,16 +0,0 @@
|
||||
use anyhow::{anyhow, Error};
|
||||
|
||||
pub struct OpenAISetting {
|
||||
pub openai_api_key: String,
|
||||
}
|
||||
|
||||
const OPENAI_API_KEY: &str = "OPENAI_API_KEY";
|
||||
|
||||
impl OpenAISetting {
|
||||
pub fn from_env() -> Result<Self, Error> {
|
||||
let openai_api_key =
|
||||
std::env::var(OPENAI_API_KEY).map_err(|_| anyhow!("Missing OPENAI_API_KEY"))?;
|
||||
|
||||
Ok(Self { openai_api_key })
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
pub mod config;
|
||||
pub mod text;
|
@ -1 +0,0 @@
|
||||
|
@ -1,14 +0,0 @@
|
||||
use anyhow::Error;
|
||||
use lib_infra::async_trait::async_trait;
|
||||
|
||||
mod entities;
|
||||
pub mod open_ai;
|
||||
pub mod stability_ai;
|
||||
|
||||
#[async_trait]
|
||||
pub trait TextCompletion: Send + Sync {
|
||||
type Input: Send + 'static;
|
||||
type Output;
|
||||
|
||||
async fn text_completion(&self, params: Self::Input) -> Result<Self::Output, Error>;
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
use crate::text::TextCompletion;
|
||||
use anyhow::Error;
|
||||
use async_openai::config::OpenAIConfig;
|
||||
use async_openai::types::{CreateCompletionRequest, CreateCompletionResponse};
|
||||
use async_openai::Client;
|
||||
use lib_infra::async_trait::async_trait;
|
||||
|
||||
pub struct OpenAITextCompletion {
|
||||
client: Client<OpenAIConfig>,
|
||||
}
|
||||
|
||||
impl OpenAITextCompletion {
|
||||
pub fn new(api_key: &str) -> Self {
|
||||
// https://docs.rs/async-openai/latest/async_openai/struct.Completions.html
|
||||
let config = OpenAIConfig::new().with_api_key(api_key);
|
||||
let client = Client::with_config(config);
|
||||
Self { client }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl TextCompletion for OpenAITextCompletion {
|
||||
type Input = CreateCompletionRequest;
|
||||
type Output = CreateCompletionResponse;
|
||||
|
||||
async fn text_completion(&self, params: Self::Input) -> Result<Self::Output, Error> {
|
||||
let response = self.client.completions().create(params).await?;
|
||||
Ok(response)
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
use crate::text::TextCompletion;
|
||||
use anyhow::Error;
|
||||
use lib_infra::async_trait::async_trait;
|
||||
|
||||
pub struct StabilityAITextCompletion {}
|
||||
|
||||
#[async_trait]
|
||||
impl TextCompletion for StabilityAITextCompletion {
|
||||
type Input = ();
|
||||
type Output = ();
|
||||
|
||||
async fn text_completion(&self, _params: Self::Input) -> Result<Self::Output, Error> {
|
||||
todo!()
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
mod text;
|
||||
mod util;
|
@ -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);
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
// mod completion_test;
|
@ -1,9 +0,0 @@
|
||||
use flowy_ai::config::OpenAISetting;
|
||||
|
||||
// To run the OpenAI test, you need to create a .env file in the flowy-ai folder.
|
||||
// Use the format: OPENAI_API_KEY=your_api_key
|
||||
#[allow(dead_code)]
|
||||
pub fn get_openai_config() -> Option<OpenAISetting> {
|
||||
dotenv::from_filename(".env").ok()?;
|
||||
OpenAISetting::from_env().ok()
|
||||
}
|
@ -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(())
|
||||
}
|
||||
|
@ -14,7 +14,9 @@ use tracing::debug;
|
||||
use collab_integrate::collab_builder::{
|
||||
CollabCloudPluginProvider, CollabPluginProviderContext, CollabPluginProviderType,
|
||||
};
|
||||
use flowy_database_pub::cloud::{CollabDocStateByOid, DatabaseCloudService, DatabaseSnapshot};
|
||||
use flowy_database_pub::cloud::{
|
||||
CollabDocStateByOid, DatabaseCloudService, DatabaseSnapshot, SummaryRowContent,
|
||||
};
|
||||
use flowy_document::deps::DocumentData;
|
||||
use flowy_document_pub::cloud::{DocumentCloudService, DocumentSnapshot};
|
||||
use flowy_error::FlowyError;
|
||||
@ -267,6 +269,23 @@ impl DatabaseCloudService for ServerProvider {
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
fn summary_database_row(
|
||||
&self,
|
||||
workspace_id: &str,
|
||||
object_id: &str,
|
||||
summary_row: SummaryRowContent,
|
||||
) -> FutureResult<String, Error> {
|
||||
let workspace_id = workspace_id.to_string();
|
||||
let server = self.get_server();
|
||||
let object_id = object_id.to_string();
|
||||
FutureResult::new(async move {
|
||||
server?
|
||||
.database_service()
|
||||
.summary_database_row(&workspace_id, &object_id, summary_row)
|
||||
.await
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl DocumentCloudService for ServerProvider {
|
||||
|
@ -5,7 +5,7 @@ use lib_infra::future::FutureResult;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub type CollabDocStateByOid = HashMap<String, DataSource>;
|
||||
|
||||
pub type SummaryRowContent = HashMap<String, String>;
|
||||
/// A trait for database cloud service.
|
||||
/// Each kind of server should implement this trait. Check out the [AppFlowyServerProvider] of
|
||||
/// [flowy-server] crate for more information.
|
||||
@ -32,6 +32,13 @@ pub trait DatabaseCloudService: Send + Sync {
|
||||
object_id: &str,
|
||||
limit: usize,
|
||||
) -> FutureResult<Vec<DatabaseSnapshot>, Error>;
|
||||
|
||||
fn summary_database_row(
|
||||
&self,
|
||||
workspace_id: &str,
|
||||
object_id: &str,
|
||||
summary_row: SummaryRowContent,
|
||||
) -> FutureResult<String, Error>;
|
||||
}
|
||||
|
||||
pub struct DatabaseSnapshot {
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -101,11 +101,14 @@ impl From<&Filter> for FilterPB {
|
||||
.cloned::<CheckboxFilterPB>()
|
||||
.unwrap()
|
||||
.try_into(),
|
||||
|
||||
FieldType::Relation => condition_and_content
|
||||
.cloned::<RelationFilterPB>()
|
||||
.unwrap()
|
||||
.try_into(),
|
||||
FieldType::Summary => condition_and_content
|
||||
.cloned::<TextFilterPB>()
|
||||
.unwrap()
|
||||
.try_into(),
|
||||
};
|
||||
|
||||
Self {
|
||||
@ -150,6 +153,9 @@ impl TryFrom<FilterDataPB> for FilterInner {
|
||||
FieldType::Relation => {
|
||||
BoxAny::new(RelationFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?)
|
||||
},
|
||||
FieldType::Summary => {
|
||||
BoxAny::new(TextFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?)
|
||||
},
|
||||
};
|
||||
|
||||
Ok(Self::Data {
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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::*;
|
||||
|
@ -0,0 +1,24 @@
|
||||
use crate::services::field::summary_type_option::summary::SummarizationTypeOption;
|
||||
use flowy_derive::ProtoBuf;
|
||||
|
||||
#[derive(Debug, Clone, Default, ProtoBuf)]
|
||||
pub struct SummarizationTypeOptionPB {
|
||||
#[pb(index = 1)]
|
||||
pub auto_fill: bool,
|
||||
}
|
||||
|
||||
impl From<SummarizationTypeOption> for SummarizationTypeOptionPB {
|
||||
fn from(value: SummarizationTypeOption) -> Self {
|
||||
SummarizationTypeOptionPB {
|
||||
auto_fill: value.auto_fill,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SummarizationTypeOptionPB> for SummarizationTypeOption {
|
||||
fn from(value: SummarizationTypeOptionPB) -> Self {
|
||||
SummarizationTypeOption {
|
||||
auto_fill: value.auto_fill,
|
||||
}
|
||||
}
|
||||
}
|
@ -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<SummaryRowPB>,
|
||||
manager: AFPluginState<Weak<DatabaseManager>>,
|
||||
) -> Result<(), FlowyError> {
|
||||
let manager = upgrade_manager(manager)?;
|
||||
let data = data.into_inner();
|
||||
let row_id = RowId::from(data.row_id);
|
||||
manager
|
||||
.summarize_row(data.view_id, row_id, data.field_id)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
@ -84,11 +84,13 @@ pub fn init(database_manager: Weak<DatabaseManager>) -> AFPlugin {
|
||||
.event(DatabaseEvent::GetAllCalculations, get_all_calculations_handler)
|
||||
.event(DatabaseEvent::UpdateCalculation, update_calculation_handler)
|
||||
.event(DatabaseEvent::RemoveCalculation, remove_calculation_handler)
|
||||
// Relation
|
||||
.event(DatabaseEvent::GetRelatedDatabaseIds, get_related_database_ids_handler)
|
||||
.event(DatabaseEvent::UpdateRelationCell, update_relation_cell_handler)
|
||||
.event(DatabaseEvent::GetRelatedRowDatas, get_related_row_datas_handler)
|
||||
.event(DatabaseEvent::GetRelatedDatabaseRows, get_related_database_rows_handler)
|
||||
// Relation
|
||||
.event(DatabaseEvent::GetRelatedDatabaseIds, get_related_database_ids_handler)
|
||||
.event(DatabaseEvent::UpdateRelationCell, update_relation_cell_handler)
|
||||
.event(DatabaseEvent::GetRelatedRowDatas, get_related_row_datas_handler)
|
||||
.event(DatabaseEvent::GetRelatedDatabaseRows, get_related_database_rows_handler)
|
||||
// AI
|
||||
.event(DatabaseEvent::SummarizeRow, summarize_row_handler)
|
||||
}
|
||||
|
||||
/// [DatabaseEvent] defines events that are used to interact with the Grid. You could check [this](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/backend/protobuf)
|
||||
@ -368,4 +370,7 @@ pub enum DatabaseEvent {
|
||||
/// Get the names of all the rows in a related database.
|
||||
#[event(input = "DatabaseIdPB", output = "RepeatedRelatedRowDataPB")]
|
||||
GetRelatedDatabaseRows = 173,
|
||||
|
||||
#[event(input = "SummaryRowPB")]
|
||||
SummarizeRow = 174,
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ use std::sync::{Arc, Weak};
|
||||
use collab::core::collab::{DataSource, MutexCollab};
|
||||
use collab_database::database::DatabaseData;
|
||||
use collab_database::error::DatabaseError;
|
||||
use collab_database::rows::RowId;
|
||||
use collab_database::views::{CreateDatabaseParams, CreateViewParams, DatabaseLayout};
|
||||
use collab_database::workspace_database::{
|
||||
CollabDocStateByOid, CollabFuture, DatabaseCollabService, DatabaseMeta, WorkspaceDatabase,
|
||||
@ -16,11 +17,13 @@ use tracing::{event, instrument, trace};
|
||||
|
||||
use collab_integrate::collab_builder::{AppFlowyCollabBuilder, CollabBuilderConfig};
|
||||
use collab_integrate::{CollabKVAction, CollabKVDB, CollabPersistenceConfig};
|
||||
use flowy_database_pub::cloud::DatabaseCloudService;
|
||||
use flowy_database_pub::cloud::{DatabaseCloudService, SummaryRowContent};
|
||||
use flowy_error::{internal_error, FlowyError, FlowyResult};
|
||||
use lib_infra::box_any::BoxAny;
|
||||
use lib_infra::priority_task::TaskDispatcher;
|
||||
|
||||
use crate::entities::{DatabaseLayoutPB, DatabaseSnapshotPB};
|
||||
use crate::services::cell::stringify_cell;
|
||||
use crate::services::database::DatabaseEditor;
|
||||
use crate::services::database_view::DatabaseLayoutDepsResolver;
|
||||
use crate::services::field_settings::default_field_settings_by_layout_map;
|
||||
@ -156,7 +159,7 @@ impl DatabaseManager {
|
||||
}
|
||||
|
||||
pub async fn get_database_inline_view_id(&self, database_id: &str) -> FlowyResult<String> {
|
||||
let wdb = self.get_workspace_database().await?;
|
||||
let wdb = self.get_database_indexer().await?;
|
||||
let database_collab = wdb.get_database(database_id).await.ok_or_else(|| {
|
||||
FlowyError::record_not_found().with_context(format!("The database:{} not found", database_id))
|
||||
})?;
|
||||
@ -167,17 +170,17 @@ impl DatabaseManager {
|
||||
|
||||
pub async fn get_all_databases_meta(&self) -> Vec<DatabaseMeta> {
|
||||
let mut items = vec![];
|
||||
if let Ok(wdb) = self.get_workspace_database().await {
|
||||
if let Ok(wdb) = self.get_database_indexer().await {
|
||||
items = wdb.get_all_database_meta()
|
||||
}
|
||||
items
|
||||
}
|
||||
|
||||
pub async fn track_database(
|
||||
pub async fn update_database_indexing(
|
||||
&self,
|
||||
view_ids_by_database_id: HashMap<String, Vec<String>>,
|
||||
) -> FlowyResult<()> {
|
||||
let wdb = self.get_workspace_database().await?;
|
||||
let wdb = self.get_database_indexer().await?;
|
||||
view_ids_by_database_id
|
||||
.into_iter()
|
||||
.for_each(|(database_id, view_ids)| {
|
||||
@ -192,7 +195,7 @@ impl DatabaseManager {
|
||||
}
|
||||
|
||||
pub async fn get_database_id_with_view_id(&self, view_id: &str) -> FlowyResult<String> {
|
||||
let wdb = self.get_workspace_database().await?;
|
||||
let wdb = self.get_database_indexer().await?;
|
||||
wdb.get_database_id_with_view_id(view_id).ok_or_else(|| {
|
||||
FlowyError::record_not_found()
|
||||
.with_context(format!("The database for view id: {} not found", view_id))
|
||||
@ -210,7 +213,7 @@ impl DatabaseManager {
|
||||
pub async fn open_database(&self, database_id: &str) -> FlowyResult<Arc<DatabaseEditor>> {
|
||||
trace!("open database editor:{}", database_id);
|
||||
let database = self
|
||||
.get_workspace_database()
|
||||
.get_database_indexer()
|
||||
.await?
|
||||
.get_database(database_id)
|
||||
.await
|
||||
@ -227,7 +230,7 @@ impl DatabaseManager {
|
||||
|
||||
pub async fn open_database_view<T: AsRef<str>>(&self, view_id: T) -> FlowyResult<()> {
|
||||
let view_id = view_id.as_ref();
|
||||
let wdb = self.get_workspace_database().await?;
|
||||
let wdb = self.get_database_indexer().await?;
|
||||
if let Some(database_id) = wdb.get_database_id_with_view_id(view_id) {
|
||||
if let Some(database) = wdb.open_database(&database_id) {
|
||||
if let Some(lock_database) = database.try_lock() {
|
||||
@ -243,7 +246,7 @@ impl DatabaseManager {
|
||||
|
||||
pub async fn close_database_view<T: AsRef<str>>(&self, view_id: T) -> FlowyResult<()> {
|
||||
let view_id = view_id.as_ref();
|
||||
let wdb = self.get_workspace_database().await?;
|
||||
let wdb = self.get_database_indexer().await?;
|
||||
let database_id = wdb.get_database_id_with_view_id(view_id);
|
||||
if let Some(database_id) = database_id {
|
||||
let mut editors = self.editors.lock().await;
|
||||
@ -270,7 +273,7 @@ impl DatabaseManager {
|
||||
}
|
||||
|
||||
pub async fn duplicate_database(&self, view_id: &str) -> FlowyResult<Vec<u8>> {
|
||||
let wdb = self.get_workspace_database().await?;
|
||||
let wdb = self.get_database_indexer().await?;
|
||||
let data = wdb.get_database_data(view_id).await?;
|
||||
let json_bytes = data.to_json_bytes()?;
|
||||
Ok(json_bytes)
|
||||
@ -297,13 +300,13 @@ impl DatabaseManager {
|
||||
create_view_params.view_id = view_id.to_string();
|
||||
}
|
||||
|
||||
let wdb = self.get_workspace_database().await?;
|
||||
let wdb = self.get_database_indexer().await?;
|
||||
let _ = wdb.create_database(create_database_params)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn create_database_with_params(&self, params: CreateDatabaseParams) -> FlowyResult<()> {
|
||||
let wdb = self.get_workspace_database().await?;
|
||||
let wdb = self.get_database_indexer().await?;
|
||||
let _ = wdb.create_database(params)?;
|
||||
Ok(())
|
||||
}
|
||||
@ -317,7 +320,7 @@ impl DatabaseManager {
|
||||
database_id: String,
|
||||
database_view_id: String,
|
||||
) -> FlowyResult<()> {
|
||||
let wdb = self.get_workspace_database().await?;
|
||||
let wdb = self.get_database_indexer().await?;
|
||||
let mut params = CreateViewParams::new(database_id.clone(), database_view_id, name, layout);
|
||||
if let Some(database) = wdb.get_database(&database_id).await {
|
||||
let (field, layout_setting) = DatabaseLayoutDepsResolver::new(database, layout)
|
||||
@ -397,7 +400,9 @@ impl DatabaseManager {
|
||||
Ok(snapshots)
|
||||
}
|
||||
|
||||
async fn get_workspace_database(&self) -> FlowyResult<Arc<WorkspaceDatabase>> {
|
||||
/// Return the database indexer.
|
||||
/// Each workspace has itw own Database indexer that manages all the databases and database views
|
||||
async fn get_database_indexer(&self) -> FlowyResult<Arc<WorkspaceDatabase>> {
|
||||
let database = self.workspace_database.read().await;
|
||||
match &*database {
|
||||
None => Err(FlowyError::internal().with_context("Workspace database not initialized")),
|
||||
@ -405,6 +410,45 @@ impl DatabaseManager {
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip_all)]
|
||||
pub async fn summarize_row(
|
||||
&self,
|
||||
view_id: String,
|
||||
row_id: RowId,
|
||||
field_id: String,
|
||||
) -> FlowyResult<()> {
|
||||
let database = self.get_database_with_view_id(&view_id).await?;
|
||||
|
||||
//
|
||||
let mut summary_row_content = SummaryRowContent::new();
|
||||
if let Some(row) = database.get_row(&view_id, &row_id) {
|
||||
let fields = database.get_fields(&view_id, None);
|
||||
for field in fields {
|
||||
if let Some(cell) = row.cells.get(&field.id) {
|
||||
summary_row_content.insert(field.name.clone(), stringify_cell(cell, &field));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Call the cloud service to summarize the row.
|
||||
trace!(
|
||||
"[AI]: summarize row:{}, content:{:?}",
|
||||
row_id,
|
||||
summary_row_content
|
||||
);
|
||||
let response = self
|
||||
.cloud_service
|
||||
.summary_database_row(&self.user.workspace_id()?, &row_id, summary_row_content)
|
||||
.await?;
|
||||
trace!("[AI]:summarize row response: {}", response);
|
||||
|
||||
// Update the cell with the response from the cloud service.
|
||||
database
|
||||
.update_cell_with_changeset(&view_id, &row_id, &field_id, BoxAny::new(response))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Only expose this method for testing
|
||||
#[cfg(debug_assertions)]
|
||||
pub fn get_cloud_service(&self) -> &Arc<dyn DatabaseCloudService> {
|
||||
|
@ -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));
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ use crate::services::database_view::{
|
||||
use crate::services::field::{
|
||||
default_type_option_data_from_type, select_type_option_from_field, transform_type_option,
|
||||
type_option_data_from_pb, ChecklistCellChangeset, RelationTypeOption, SelectOptionCellChangeset,
|
||||
StrCellData, TimestampCellData, TimestampCellDataWrapper, TypeOptionCellDataHandler,
|
||||
StringCellData, TimestampCellData, TimestampCellDataWrapper, TypeOptionCellDataHandler,
|
||||
TypeOptionCellExt,
|
||||
};
|
||||
use crate::services::field_settings::{default_field_settings_by_layout_map, FieldSettings};
|
||||
@ -34,7 +34,7 @@ use lib_infra::util::timestamp;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{broadcast, RwLock};
|
||||
use tracing::{event, warn};
|
||||
use tracing::{event, instrument, warn};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DatabaseEditor {
|
||||
@ -440,7 +440,7 @@ impl DatabaseEditor {
|
||||
for cell in cells {
|
||||
if let Some(new_cell) = cell.cell.clone() {
|
||||
self
|
||||
.update_cell(view_id, cell.row_id, &new_field_id, new_cell)
|
||||
.update_cell(view_id, &cell.row_id, &new_field_id, new_cell)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
@ -755,10 +755,11 @@ impl DatabaseEditor {
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "trace", skip_all)]
|
||||
pub async fn update_cell_with_changeset(
|
||||
&self,
|
||||
view_id: &str,
|
||||
row_id: RowId,
|
||||
row_id: &RowId,
|
||||
field_id: &str,
|
||||
cell_changeset: BoxAny,
|
||||
) -> FlowyResult<()> {
|
||||
@ -771,7 +772,7 @@ impl DatabaseEditor {
|
||||
Err(FlowyError::internal().with_context(msg))
|
||||
},
|
||||
}?;
|
||||
(field, database.get_cell(field_id, &row_id).cell)
|
||||
(field, database.get_cell(field_id, row_id).cell)
|
||||
};
|
||||
|
||||
let new_cell =
|
||||
@ -800,14 +801,13 @@ impl DatabaseEditor {
|
||||
pub async fn update_cell(
|
||||
&self,
|
||||
view_id: &str,
|
||||
row_id: RowId,
|
||||
row_id: &RowId,
|
||||
field_id: &str,
|
||||
new_cell: Cell,
|
||||
) -> FlowyResult<()> {
|
||||
// Get the old row before updating the cell. It would be better to get the old cell
|
||||
let old_row = { self.get_row_detail(view_id, &row_id) };
|
||||
|
||||
self.database.lock().update_row(&row_id, |row_update| {
|
||||
let old_row = { self.get_row_detail(view_id, row_id) };
|
||||
self.database.lock().update_row(row_id, |row_update| {
|
||||
row_update.update_cells(|cell_update| {
|
||||
cell_update.insert(field_id, new_cell);
|
||||
});
|
||||
@ -831,7 +831,7 @@ impl DatabaseEditor {
|
||||
});
|
||||
|
||||
self
|
||||
.did_update_row(view_id, row_id, field_id, old_row)
|
||||
.did_update_row(view_id, &row_id, field_id, old_row)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
@ -840,11 +840,11 @@ impl DatabaseEditor {
|
||||
async fn did_update_row(
|
||||
&self,
|
||||
view_id: &str,
|
||||
row_id: RowId,
|
||||
row_id: &RowId,
|
||||
field_id: &str,
|
||||
old_row: Option<RowDetail>,
|
||||
) {
|
||||
let option_row = self.get_row_detail(view_id, &row_id);
|
||||
let option_row = self.get_row_detail(view_id, row_id);
|
||||
if let Some(new_row_detail) = option_row {
|
||||
for view in self.database_views.editors().await {
|
||||
view
|
||||
@ -931,7 +931,7 @@ impl DatabaseEditor {
|
||||
|
||||
// Insert the options into the cell
|
||||
self
|
||||
.update_cell_with_changeset(view_id, row_id, field_id, BoxAny::new(cell_changeset))
|
||||
.update_cell_with_changeset(view_id, &row_id, field_id, BoxAny::new(cell_changeset))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@ -970,7 +970,7 @@ impl DatabaseEditor {
|
||||
.await?;
|
||||
|
||||
self
|
||||
.update_cell_with_changeset(view_id, row_id, field_id, BoxAny::new(cell_changeset))
|
||||
.update_cell_with_changeset(view_id, &row_id, field_id, BoxAny::new(cell_changeset))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@ -994,7 +994,7 @@ impl DatabaseEditor {
|
||||
debug_assert!(FieldType::from(field.field_type).is_checklist());
|
||||
|
||||
self
|
||||
.update_cell_with_changeset(view_id, row_id, field_id, BoxAny::new(changeset))
|
||||
.update_cell_with_changeset(view_id, &row_id, field_id, BoxAny::new(changeset))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@ -1294,7 +1294,7 @@ impl DatabaseEditor {
|
||||
.cell
|
||||
.and_then(|cell| handler.handle_get_boxed_cell_data(&cell, &primary_field))
|
||||
.and_then(|cell_data| cell_data.unbox_or_none())
|
||||
.unwrap_or_else(|| StrCellData("".to_string()));
|
||||
.unwrap_or_else(|| StringCellData("".to_string()));
|
||||
|
||||
RelatedRowDataPB {
|
||||
row_id: row.id.to_string(),
|
||||
|
@ -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;
|
||||
|
@ -0,0 +1,2 @@
|
||||
pub mod summary;
|
||||
pub mod summary_entities;
|
@ -0,0 +1,109 @@
|
||||
use crate::entities::TextFilterPB;
|
||||
use crate::services::cell::{CellDataChangeset, CellDataDecoder};
|
||||
use crate::services::field::summary_type_option::summary_entities::SummaryCellData;
|
||||
use crate::services::field::type_options::util::ProtobufStr;
|
||||
use crate::services::field::{
|
||||
TypeOption, TypeOptionCellData, TypeOptionCellDataCompare, TypeOptionCellDataFilter,
|
||||
TypeOptionCellDataSerde, TypeOptionTransform,
|
||||
};
|
||||
use crate::services::sort::SortCondition;
|
||||
use collab::core::any_map::AnyMapExtension;
|
||||
use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder};
|
||||
use collab_database::rows::Cell;
|
||||
use flowy_error::FlowyResult;
|
||||
use std::cmp::Ordering;
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct SummarizationTypeOption {
|
||||
pub auto_fill: bool,
|
||||
}
|
||||
|
||||
impl From<TypeOptionData> for SummarizationTypeOption {
|
||||
fn from(value: TypeOptionData) -> Self {
|
||||
let auto_fill = value.get_bool_value("auto_fill").unwrap_or_default();
|
||||
Self { auto_fill }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SummarizationTypeOption> for TypeOptionData {
|
||||
fn from(value: SummarizationTypeOption) -> Self {
|
||||
TypeOptionDataBuilder::new()
|
||||
.insert_bool_value("auto_fill", value.auto_fill)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeOption for SummarizationTypeOption {
|
||||
type CellData = SummaryCellData;
|
||||
type CellChangeset = String;
|
||||
type CellProtobufType = ProtobufStr;
|
||||
type CellFilter = TextFilterPB;
|
||||
}
|
||||
|
||||
impl CellDataChangeset for SummarizationTypeOption {
|
||||
fn apply_changeset(
|
||||
&self,
|
||||
changeset: String,
|
||||
_cell: Option<Cell>,
|
||||
) -> FlowyResult<(Cell, SummaryCellData)> {
|
||||
let cell_data = SummaryCellData(changeset);
|
||||
Ok((cell_data.clone().into(), cell_data))
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeOptionCellDataFilter for SummarizationTypeOption {
|
||||
fn apply_filter(
|
||||
&self,
|
||||
filter: &<Self as TypeOption>::CellFilter,
|
||||
cell_data: &<Self as TypeOption>::CellData,
|
||||
) -> bool {
|
||||
filter.is_visible(cell_data)
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeOptionCellDataCompare for SummarizationTypeOption {
|
||||
fn apply_cmp(
|
||||
&self,
|
||||
cell_data: &<Self as TypeOption>::CellData,
|
||||
other_cell_data: &<Self as TypeOption>::CellData,
|
||||
sort_condition: SortCondition,
|
||||
) -> Ordering {
|
||||
match (cell_data.is_cell_empty(), other_cell_data.is_cell_empty()) {
|
||||
(true, true) => Ordering::Equal,
|
||||
(true, false) => Ordering::Greater,
|
||||
(false, true) => Ordering::Less,
|
||||
(false, false) => {
|
||||
let order = cell_data.0.cmp(&other_cell_data.0);
|
||||
sort_condition.evaluate_order(order)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CellDataDecoder for SummarizationTypeOption {
|
||||
fn decode_cell(&self, cell: &Cell) -> FlowyResult<SummaryCellData> {
|
||||
Ok(SummaryCellData::from(cell))
|
||||
}
|
||||
|
||||
fn stringify_cell_data(&self, cell_data: SummaryCellData) -> String {
|
||||
cell_data.to_string()
|
||||
}
|
||||
|
||||
fn numeric_cell(&self, _cell: &Cell) -> Option<f64> {
|
||||
None
|
||||
}
|
||||
}
|
||||
impl TypeOptionTransform for SummarizationTypeOption {}
|
||||
|
||||
impl TypeOptionCellDataSerde for SummarizationTypeOption {
|
||||
fn protobuf_encode(
|
||||
&self,
|
||||
cell_data: <Self as TypeOption>::CellData,
|
||||
) -> <Self as TypeOption>::CellProtobufType {
|
||||
ProtobufStr::from(cell_data.0)
|
||||
}
|
||||
|
||||
fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
|
||||
Ok(SummaryCellData::from(cell))
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
use crate::entities::FieldType;
|
||||
use crate::services::field::{TypeOptionCellData, CELL_DATA};
|
||||
use collab::core::any_map::AnyMapExtension;
|
||||
use collab_database::rows::{new_cell_builder, Cell};
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct SummaryCellData(pub String);
|
||||
impl std::ops::Deref for SummaryCellData {
|
||||
type Target = String;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeOptionCellData for SummaryCellData {
|
||||
fn is_cell_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Cell> for SummaryCellData {
|
||||
fn from(cell: &Cell) -> Self {
|
||||
Self(cell.get_str_value(CELL_DATA).unwrap_or_default())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SummaryCellData> for Cell {
|
||||
fn from(data: SummaryCellData) -> Self {
|
||||
new_cell_builder(FieldType::Summary)
|
||||
.insert_str_value(CELL_DATA, data.0)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for SummaryCellData {
|
||||
fn to_string(&self) -> String {
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for SummaryCellData {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
@ -25,7 +25,7 @@ pub struct RichTextTypeOption {
|
||||
}
|
||||
|
||||
impl TypeOption for RichTextTypeOption {
|
||||
type CellData = StrCellData;
|
||||
type CellData = StringCellData;
|
||||
type CellChangeset = String;
|
||||
type CellProtobufType = ProtobufStr;
|
||||
type CellFilter = TextFilterPB;
|
||||
@ -57,13 +57,13 @@ impl TypeOptionCellDataSerde for RichTextTypeOption {
|
||||
}
|
||||
|
||||
fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
|
||||
Ok(StrCellData::from(cell))
|
||||
Ok(StringCellData::from(cell))
|
||||
}
|
||||
}
|
||||
|
||||
impl CellDataDecoder for RichTextTypeOption {
|
||||
fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
|
||||
Ok(StrCellData::from(cell))
|
||||
Ok(StringCellData::from(cell))
|
||||
}
|
||||
|
||||
fn decode_cell_with_transform(
|
||||
@ -79,11 +79,12 @@ impl CellDataDecoder for RichTextTypeOption {
|
||||
| FieldType::SingleSelect
|
||||
| FieldType::MultiSelect
|
||||
| FieldType::Checkbox
|
||||
| FieldType::URL => Some(StrCellData::from(stringify_cell(cell, field))),
|
||||
| FieldType::URL => Some(StringCellData::from(stringify_cell(cell, field))),
|
||||
FieldType::Checklist
|
||||
| FieldType::LastEditedTime
|
||||
| FieldType::CreatedTime
|
||||
| FieldType::Relation => None,
|
||||
FieldType::Summary => Some(StringCellData::from(stringify_cell(cell, field))),
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,7 +93,7 @@ impl CellDataDecoder for RichTextTypeOption {
|
||||
}
|
||||
|
||||
fn numeric_cell(&self, cell: &Cell) -> Option<f64> {
|
||||
StrCellData::from(cell).0.parse::<f64>().ok()
|
||||
StringCellData::from(cell).0.parse::<f64>().ok()
|
||||
}
|
||||
}
|
||||
|
||||
@ -108,7 +109,7 @@ impl CellDataChangeset for RichTextTypeOption {
|
||||
.with_context("The len of the text should not be more than 10000"),
|
||||
)
|
||||
} else {
|
||||
let text_cell_data = StrCellData(changeset);
|
||||
let text_cell_data = StringCellData(changeset);
|
||||
Ok((text_cell_data.clone().into(), text_cell_data))
|
||||
}
|
||||
}
|
||||
@ -144,8 +145,8 @@ impl TypeOptionCellDataCompare for RichTextTypeOption {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct StrCellData(pub String);
|
||||
impl std::ops::Deref for StrCellData {
|
||||
pub struct StringCellData(pub String);
|
||||
impl std::ops::Deref for StringCellData {
|
||||
type Target = String;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
@ -153,57 +154,57 @@ impl std::ops::Deref for StrCellData {
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeOptionCellData for StrCellData {
|
||||
impl TypeOptionCellData for StringCellData {
|
||||
fn is_cell_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Cell> for StrCellData {
|
||||
impl From<&Cell> for StringCellData {
|
||||
fn from(cell: &Cell) -> Self {
|
||||
Self(cell.get_str_value(CELL_DATA).unwrap_or_default())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StrCellData> for Cell {
|
||||
fn from(data: StrCellData) -> Self {
|
||||
impl From<StringCellData> for Cell {
|
||||
fn from(data: StringCellData) -> Self {
|
||||
new_cell_builder(FieldType::RichText)
|
||||
.insert_str_value(CELL_DATA, data.0)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for StrCellData {
|
||||
impl std::ops::DerefMut for StringCellData {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<String> for StrCellData {
|
||||
impl std::convert::From<String> for StringCellData {
|
||||
fn from(s: String) -> Self {
|
||||
Self(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for StrCellData {
|
||||
impl ToString for StringCellData {
|
||||
fn to_string(&self) -> String {
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<StrCellData> for String {
|
||||
fn from(value: StrCellData) -> Self {
|
||||
impl std::convert::From<StringCellData> for String {
|
||||
fn from(value: StringCellData) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<&str> for StrCellData {
|
||||
impl std::convert::From<&str> for StringCellData {
|
||||
fn from(s: &str) -> Self {
|
||||
Self(s.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for StrCellData {
|
||||
impl AsRef<str> for StringCellData {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0.as_str()
|
||||
}
|
||||
|
@ -11,10 +11,11 @@ use flowy_error::FlowyResult;
|
||||
use crate::entities::{
|
||||
CheckboxTypeOptionPB, ChecklistTypeOptionPB, DateTypeOptionPB, FieldType,
|
||||
MultiSelectTypeOptionPB, NumberTypeOptionPB, RelationTypeOptionPB, RichTextTypeOptionPB,
|
||||
SingleSelectTypeOptionPB, TimestampTypeOptionPB, URLTypeOptionPB,
|
||||
SingleSelectTypeOptionPB, SummarizationTypeOptionPB, TimestampTypeOptionPB, URLTypeOptionPB,
|
||||
};
|
||||
use crate::services::cell::CellDataDecoder;
|
||||
use crate::services::field::checklist_type_option::ChecklistTypeOption;
|
||||
use crate::services::field::summary_type_option::summary::SummarizationTypeOption;
|
||||
use crate::services::field::{
|
||||
CheckboxTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption, RelationTypeOption,
|
||||
RichTextTypeOption, SingleSelectTypeOption, TimestampTypeOption, URLTypeOption,
|
||||
@ -181,6 +182,9 @@ pub fn type_option_data_from_pb<T: Into<Bytes>>(
|
||||
FieldType::Relation => {
|
||||
RelationTypeOptionPB::try_from(bytes).map(|pb| RelationTypeOption::from(pb).into())
|
||||
},
|
||||
FieldType::Summary => {
|
||||
SummarizationTypeOptionPB::try_from(bytes).map(|pb| SummarizationTypeOption::from(pb).into())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -242,6 +246,12 @@ pub fn type_option_to_pb(type_option: TypeOptionData, field_type: &FieldType) ->
|
||||
.try_into()
|
||||
.unwrap()
|
||||
},
|
||||
FieldType::Summary => {
|
||||
let summarization_type_option: SummarizationTypeOption = type_option.into();
|
||||
SummarizationTypeOptionPB::from(summarization_type_option)
|
||||
.try_into()
|
||||
.unwrap()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -261,5 +271,6 @@ pub fn default_type_option_data_from_type(field_type: FieldType) -> TypeOptionDa
|
||||
FieldType::URL => URLTypeOption::default().into(),
|
||||
FieldType::Checklist => ChecklistTypeOption.into(),
|
||||
FieldType::Relation => RelationTypeOption::default().into(),
|
||||
FieldType::Summary => SummarizationTypeOption::default().into(),
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ use lib_infra::box_any::BoxAny;
|
||||
|
||||
use crate::entities::FieldType;
|
||||
use crate::services::cell::{CellCache, CellDataChangeset, CellDataDecoder, CellProtobufBlob};
|
||||
use crate::services::field::summary_type_option::summary::SummarizationTypeOption;
|
||||
use crate::services::field::{
|
||||
CheckboxTypeOption, ChecklistTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption,
|
||||
RelationTypeOption, RichTextTypeOption, SingleSelectTypeOption, TimestampTypeOption, TypeOption,
|
||||
@ -166,23 +167,24 @@ where
|
||||
if let Some(cell_data_cache) = self.cell_data_cache.as_ref() {
|
||||
let field_type = FieldType::from(field.field_type);
|
||||
let key = CellDataCacheKey::new(field, field_type, cell);
|
||||
// tracing::trace!(
|
||||
// "Cell cache update: field_type:{}, cell: {:?}, cell_data: {:?}",
|
||||
// field_type,
|
||||
// cell,
|
||||
// cell_data
|
||||
// );
|
||||
tracing::trace!(
|
||||
"Cell cache update: field_type:{}, cell: {:?}, cell_data: {:?}",
|
||||
field_type,
|
||||
cell,
|
||||
cell_data
|
||||
);
|
||||
cell_data_cache.write().insert(key.as_ref(), cell_data);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_cell_data(&self, cell: &Cell, field: &Field) -> Option<T::CellData> {
|
||||
let field_type_of_cell = get_field_type_from_cell(cell)?;
|
||||
|
||||
if let Some(cell_data) = self.get_cell_data_from_cache(cell, field) {
|
||||
return Some(cell_data);
|
||||
}
|
||||
|
||||
// If the field type of the cell is the same as the field type of the handler, we can directly decode the cell.
|
||||
// Otherwise, we need to transform the cell to the field type of the handler.
|
||||
let cell_data = if field_type_of_cell == self.field_type {
|
||||
Some(self.decode_cell(cell).unwrap_or_default())
|
||||
} else if is_type_option_cell_transformable(field_type_of_cell, self.field_type) {
|
||||
@ -437,6 +439,16 @@ impl<'a> TypeOptionCellExt<'a> {
|
||||
self.cell_data_cache.clone(),
|
||||
)
|
||||
}),
|
||||
FieldType::Summary => self
|
||||
.field
|
||||
.get_type_option::<SummarizationTypeOption>(field_type)
|
||||
.map(|type_option| {
|
||||
TypeOptionCellDataHandlerImpl::new_with_boxed(
|
||||
type_option,
|
||||
field_type,
|
||||
self.cell_data_cache.clone(),
|
||||
)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@ -538,6 +550,8 @@ fn get_type_option_transform_handler(
|
||||
FieldType::Relation => {
|
||||
Box::new(RelationTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler>
|
||||
},
|
||||
FieldType::Summary => Box::new(SummarizationTypeOption::from(type_option_data))
|
||||
as Box<dyn TypeOptionTransformHandler>,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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(),
|
||||
|
@ -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 {
|
||||
|
@ -280,6 +280,7 @@ impl FilterInner {
|
||||
FieldType::Checklist => BoxAny::new(ChecklistFilterPB::parse(condition as u8, content)),
|
||||
FieldType::Checkbox => BoxAny::new(CheckboxFilterPB::parse(condition as u8, content)),
|
||||
FieldType::Relation => BoxAny::new(RelationFilterPB::parse(condition as u8, content)),
|
||||
FieldType::Summary => BoxAny::new(TextFilterPB::parse(condition as u8, content)),
|
||||
};
|
||||
|
||||
FilterInner::Data {
|
||||
@ -362,6 +363,10 @@ impl<'a> From<&'a Filter> for FilterMap {
|
||||
let filter = condition_and_content.cloned::<RelationFilterPB>()?;
|
||||
(filter.condition as u8, "".to_string())
|
||||
},
|
||||
FieldType::Summary => {
|
||||
let filter = condition_and_content.cloned::<TextFilterPB>()?;
|
||||
(filter.condition as u8, filter.content)
|
||||
},
|
||||
};
|
||||
Some((condition, content))
|
||||
};
|
||||
|
@ -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();
|
||||
},
|
||||
|
@ -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(), ""),
|
||||
|
@ -1,16 +1,16 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use collab_database::database::{gen_database_view_id, timestamp};
|
||||
use collab_database::database::gen_database_view_id;
|
||||
use collab_database::fields::Field;
|
||||
use collab_database::rows::{Row, RowDetail, RowId};
|
||||
use collab_database::rows::{RowDetail, RowId};
|
||||
use lib_infra::box_any::BoxAny;
|
||||
use strum::EnumCount;
|
||||
|
||||
use event_integration_test::folder_event::ViewTest;
|
||||
use event_integration_test::EventIntegrationTest;
|
||||
use flowy_database2::entities::{FieldType, FilterPB, RowMetaPB};
|
||||
use flowy_database2::services::cell::CellBuilder;
|
||||
|
||||
use flowy_database2::services::database::DatabaseEditor;
|
||||
use flowy_database2::services::field::checklist_type_option::{
|
||||
ChecklistCellChangeset, ChecklistTypeOption,
|
||||
@ -196,7 +196,7 @@ impl DatabaseEditorTest {
|
||||
|
||||
self
|
||||
.editor
|
||||
.update_cell_with_changeset(&self.view_id, row_id, &field.id, cell_changeset)
|
||||
.update_cell_with_changeset(&self.view_id, &row_id, &field.id, cell_changeset)
|
||||
.await
|
||||
}
|
||||
|
||||
@ -282,139 +282,3 @@ impl DatabaseEditorTest {
|
||||
.ok()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TestRowBuilder<'a> {
|
||||
database_id: &'a str,
|
||||
row_id: RowId,
|
||||
fields: &'a [Field],
|
||||
cell_build: CellBuilder<'a>,
|
||||
}
|
||||
|
||||
impl<'a> TestRowBuilder<'a> {
|
||||
pub fn new(database_id: &'a str, row_id: RowId, fields: &'a [Field]) -> Self {
|
||||
let cell_build = CellBuilder::with_cells(Default::default(), fields);
|
||||
Self {
|
||||
database_id,
|
||||
row_id,
|
||||
fields,
|
||||
cell_build,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_text_cell(&mut self, data: &str) -> String {
|
||||
let text_field = self.field_with_type(&FieldType::RichText);
|
||||
self
|
||||
.cell_build
|
||||
.insert_text_cell(&text_field.id, data.to_string());
|
||||
|
||||
text_field.id.clone()
|
||||
}
|
||||
|
||||
pub fn insert_number_cell(&mut self, data: &str) -> String {
|
||||
let number_field = self.field_with_type(&FieldType::Number);
|
||||
self
|
||||
.cell_build
|
||||
.insert_text_cell(&number_field.id, data.to_string());
|
||||
number_field.id.clone()
|
||||
}
|
||||
|
||||
pub fn insert_date_cell(
|
||||
&mut self,
|
||||
date: i64,
|
||||
time: Option<String>,
|
||||
include_time: Option<bool>,
|
||||
field_type: &FieldType,
|
||||
) -> String {
|
||||
let date_field = self.field_with_type(field_type);
|
||||
self
|
||||
.cell_build
|
||||
.insert_date_cell(&date_field.id, date, time, include_time);
|
||||
date_field.id.clone()
|
||||
}
|
||||
|
||||
pub fn insert_checkbox_cell(&mut self, data: &str) -> String {
|
||||
let checkbox_field = self.field_with_type(&FieldType::Checkbox);
|
||||
self
|
||||
.cell_build
|
||||
.insert_text_cell(&checkbox_field.id, data.to_string());
|
||||
|
||||
checkbox_field.id.clone()
|
||||
}
|
||||
|
||||
pub fn insert_url_cell(&mut self, content: &str) -> String {
|
||||
let url_field = self.field_with_type(&FieldType::URL);
|
||||
self
|
||||
.cell_build
|
||||
.insert_url_cell(&url_field.id, content.to_string());
|
||||
url_field.id.clone()
|
||||
}
|
||||
|
||||
pub fn insert_single_select_cell<F>(&mut self, f: F) -> String
|
||||
where
|
||||
F: Fn(Vec<SelectOption>) -> SelectOption,
|
||||
{
|
||||
let single_select_field = self.field_with_type(&FieldType::SingleSelect);
|
||||
let type_option = single_select_field
|
||||
.get_type_option::<SingleSelectTypeOption>(FieldType::SingleSelect)
|
||||
.unwrap();
|
||||
let option = f(type_option.options);
|
||||
self
|
||||
.cell_build
|
||||
.insert_select_option_cell(&single_select_field.id, vec![option.id]);
|
||||
|
||||
single_select_field.id.clone()
|
||||
}
|
||||
|
||||
pub fn insert_multi_select_cell<F>(&mut self, f: F) -> String
|
||||
where
|
||||
F: Fn(Vec<SelectOption>) -> Vec<SelectOption>,
|
||||
{
|
||||
let multi_select_field = self.field_with_type(&FieldType::MultiSelect);
|
||||
let type_option = multi_select_field
|
||||
.get_type_option::<MultiSelectTypeOption>(FieldType::MultiSelect)
|
||||
.unwrap();
|
||||
let options = f(type_option.options);
|
||||
let ops_ids = options
|
||||
.iter()
|
||||
.map(|option| option.id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
self
|
||||
.cell_build
|
||||
.insert_select_option_cell(&multi_select_field.id, ops_ids);
|
||||
|
||||
multi_select_field.id.clone()
|
||||
}
|
||||
|
||||
pub fn insert_checklist_cell(&mut self, options: Vec<(String, bool)>) -> String {
|
||||
let checklist_field = self.field_with_type(&FieldType::Checklist);
|
||||
self
|
||||
.cell_build
|
||||
.insert_checklist_cell(&checklist_field.id, options);
|
||||
checklist_field.id.clone()
|
||||
}
|
||||
|
||||
pub fn field_with_type(&self, field_type: &FieldType) -> Field {
|
||||
self
|
||||
.fields
|
||||
.iter()
|
||||
.find(|field| {
|
||||
let t_field_type = FieldType::from(field.field_type);
|
||||
&t_field_type == field_type
|
||||
})
|
||||
.unwrap()
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub fn build(self) -> Row {
|
||||
let timestamp = timestamp();
|
||||
Row {
|
||||
id: self.row_id,
|
||||
database_id: self.database_id.to_string(),
|
||||
cells: self.cell_build.build(),
|
||||
height: 60,
|
||||
visibility: true,
|
||||
modified_at: timestamp,
|
||||
created_at: timestamp,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
},
|
||||
|
@ -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);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
use chrono::{DateTime, Local, Offset};
|
||||
use collab_database::database::timestamp;
|
||||
use flowy_database2::entities::FieldType;
|
||||
use flowy_database2::services::cell::stringify_cell;
|
||||
use flowy_database2::services::field::CHECK;
|
||||
@ -24,45 +22,6 @@ async fn export_meta_csv_test() {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn export_csv_test() {
|
||||
let test = DatabaseEditorTest::new_grid().await;
|
||||
let database = test.editor.clone();
|
||||
let s = database.export_csv(CSVFormat::Original).await.unwrap();
|
||||
let format = "%Y/%m/%d %R";
|
||||
let naive = chrono::NaiveDateTime::from_timestamp_opt(timestamp(), 0).unwrap();
|
||||
let offset = Local::now().offset().fix();
|
||||
let date_time = DateTime::<Local>::from_naive_utc_and_offset(naive, offset);
|
||||
let date_string = format!("{}", date_time.format(format));
|
||||
let expected = format!(
|
||||
r#"Name,Price,Time,Status,Platform,is urgent,link,TODO,Last Modified,Created At,Related
|
||||
A,$1,2022/03/14,,"Google,Facebook",Yes,AppFlowy website - https://www.appflowy.io,First thing,{},{},
|
||||
,$2,2022/03/14,,"Google,Twitter",Yes,,"Have breakfast,Have lunch,Take a nap,Have dinner,Shower and head to bed",{},{},
|
||||
C,$3,2022/03/14,Completed,"Facebook,Google,Twitter",No,,,{},{},
|
||||
DA,$14,2022/11/17,Completed,,No,,Task 1,{},{},
|
||||
AE,,2022/11/13,Planned,"Facebook,Twitter",No,,,{},{},
|
||||
AE,$5,2022/12/25,Planned,Facebook,Yes,,"Sprint,Sprint some more,Rest",{},{},
|
||||
CB,,,,,,,,{},{},
|
||||
"#,
|
||||
date_string,
|
||||
date_string,
|
||||
date_string,
|
||||
date_string,
|
||||
date_string,
|
||||
date_string,
|
||||
date_string,
|
||||
date_string,
|
||||
date_string,
|
||||
date_string,
|
||||
date_string,
|
||||
date_string,
|
||||
date_string,
|
||||
date_string,
|
||||
);
|
||||
println!("{}", s);
|
||||
assert_eq!(s, expected);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn export_and_then_import_meta_csv_test() {
|
||||
let test = DatabaseEditorTest::new_grid().await;
|
||||
@ -123,6 +82,7 @@ async fn export_and_then_import_meta_csv_test() {
|
||||
FieldType::LastEditedTime => {},
|
||||
FieldType::CreatedTime => {},
|
||||
FieldType::Relation => {},
|
||||
FieldType::Summary => {},
|
||||
}
|
||||
} else {
|
||||
panic!(
|
||||
@ -205,6 +165,7 @@ async fn history_database_import_test() {
|
||||
FieldType::LastEditedTime => {},
|
||||
FieldType::CreatedTime => {},
|
||||
FieldType::Relation => {},
|
||||
FieldType::Summary => {},
|
||||
}
|
||||
} else {
|
||||
panic!(
|
||||
|
@ -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.
|
||||
|
@ -1,14 +1,18 @@
|
||||
use anyhow::Error;
|
||||
use client_api::entity::ai_dto::{SummarizeRowData, SummarizeRowParams};
|
||||
use client_api::entity::QueryCollabResult::{Failed, Success};
|
||||
use client_api::entity::{QueryCollab, QueryCollabParams};
|
||||
use client_api::error::ErrorCode::RecordNotFound;
|
||||
use collab::core::collab::DataSource;
|
||||
use collab::entity::EncodedCollab;
|
||||
use collab_entity::CollabType;
|
||||
use serde_json::{Map, Value};
|
||||
use std::sync::Arc;
|
||||
use tracing::{error, instrument};
|
||||
|
||||
use flowy_database_pub::cloud::{CollabDocStateByOid, DatabaseCloudService, DatabaseSnapshot};
|
||||
use flowy_database_pub::cloud::{
|
||||
CollabDocStateByOid, DatabaseCloudService, DatabaseSnapshot, SummaryRowContent,
|
||||
};
|
||||
use lib_infra::future::FutureResult;
|
||||
|
||||
use crate::af_cloud::define::ServerUser;
|
||||
@ -119,4 +123,26 @@ where
|
||||
) -> FutureResult<Vec<DatabaseSnapshot>, Error> {
|
||||
FutureResult::new(async move { Ok(vec![]) })
|
||||
}
|
||||
|
||||
fn summary_database_row(
|
||||
&self,
|
||||
workspace_id: &str,
|
||||
_object_id: &str,
|
||||
summary_row: SummaryRowContent,
|
||||
) -> FutureResult<String, Error> {
|
||||
let workspace_id = workspace_id.to_string();
|
||||
let try_get_client = self.inner.try_get_client();
|
||||
FutureResult::new(async move {
|
||||
let map: Map<String, Value> = summary_row
|
||||
.into_iter()
|
||||
.map(|(key, value)| (key, Value::String(value)))
|
||||
.collect();
|
||||
let params = SummarizeRowParams {
|
||||
workspace_id,
|
||||
data: SummarizeRowData::Content(map),
|
||||
};
|
||||
let data = try_get_client?.summarize_row(params).await?;
|
||||
Ok(data.text)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,9 @@ use collab_entity::define::{DATABASE, DATABASE_ROW_DATA, WORKSPACE_DATABASES};
|
||||
use collab_entity::CollabType;
|
||||
use yrs::{Any, MapPrelim};
|
||||
|
||||
use flowy_database_pub::cloud::{CollabDocStateByOid, DatabaseCloudService, DatabaseSnapshot};
|
||||
use flowy_database_pub::cloud::{
|
||||
CollabDocStateByOid, DatabaseCloudService, DatabaseSnapshot, SummaryRowContent,
|
||||
};
|
||||
use lib_infra::future::FutureResult;
|
||||
|
||||
pub(crate) struct LocalServerDatabaseCloudServiceImpl();
|
||||
@ -73,4 +75,14 @@ impl DatabaseCloudService for LocalServerDatabaseCloudServiceImpl {
|
||||
) -> FutureResult<Vec<DatabaseSnapshot>, Error> {
|
||||
FutureResult::new(async move { Ok(vec![]) })
|
||||
}
|
||||
|
||||
fn summary_database_row(
|
||||
&self,
|
||||
_workspace_id: &str,
|
||||
_object_id: &str,
|
||||
_summary_row: SummaryRowContent,
|
||||
) -> FutureResult<String, Error> {
|
||||
// TODO(lucas): local ai
|
||||
FutureResult::new(async move { Ok("".to_string()) })
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,9 @@ use anyhow::Error;
|
||||
use collab_entity::CollabType;
|
||||
use tokio::sync::oneshot::channel;
|
||||
|
||||
use flowy_database_pub::cloud::{CollabDocStateByOid, DatabaseCloudService, DatabaseSnapshot};
|
||||
use flowy_database_pub::cloud::{
|
||||
CollabDocStateByOid, DatabaseCloudService, DatabaseSnapshot, SummaryRowContent,
|
||||
};
|
||||
use lib_dispatch::prelude::af_spawn;
|
||||
use lib_infra::future::FutureResult;
|
||||
|
||||
@ -94,4 +96,13 @@ where
|
||||
Ok(snapshots)
|
||||
})
|
||||
}
|
||||
|
||||
fn summary_database_row(
|
||||
&self,
|
||||
_workspace_id: &str,
|
||||
_object_id: &str,
|
||||
_summary_row: SummaryRowContent,
|
||||
) -> FutureResult<String, Error> {
|
||||
FutureResult::new(async move { Ok("".to_string()) })
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user