Compare commits

...
Author SHA1 Message Date
Nathan a1dff84018 chore: fmt 2025-04-10 16:02:57 +08:00
Nathan 9de76b17a6 chore: bump collab id 2025-04-10 16:02:49 +08:00
Nathan b791d6af70 chore: clippy 2025-04-10 16:02:40 +08:00
Nathan 0975f6c8c0 chore: replace str with uuid 2025-04-10 16:02:16 +08:00
Richard Shiue e870481ef9 fix: use normal underline for AI suggested text style (#7712) 2025-04-09 22:03:26 +08:00
Lucas c1f6a0efea fix: replace the selected text with ai response in the same line (#7708)
* fix: replace the selected text with ai response in the same line

* fix: replace the selected text with ai response in the multiple lines

* fix: integrate the replace function in the ai writer cubit

* fix: unit test and integration test
2025-04-09 22:02:28 +08:00
Morn 5c19c08cb3 fix: overwriting a selected text removes more than expected (#7714) 2025-04-09 22:02:14 +08:00
Nathan 210ea0c2ac chore: fix local ai setting 2025-04-09 20:56:54 +08:00
Nathan ab70dccc8e chore: remove enable sync log toggle 2025-04-09 20:56:45 +08:00
Nathan eb22d86f3e chore: clippy 2025-04-09 20:56:19 +08:00
Richard Shiue a7d5c389b0 fix(flutter_desktop): grid bug (#7697)
* fix: field width doesn't persist

* test: add test

* test: fix test

* test: grid width integration test
2025-04-09 20:56:09 +08:00
Nathan 71a6ba660f chore: enable sync log by default 2025-04-09 20:54:56 +08:00
125 changed files with 2525 additions and 1488 deletions

No files matched your search

@@ -15,6 +15,7 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// create a database and add a linked database view
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Grid);
@@ -29,6 +30,11 @@ void main() {
await tester.tapHidePropertyButton();
tester.noFieldWithName('New field 1');
// create another field, New field 1 to be hidden still
await tester.tapNewPropertyButton();
await tester.dismissFieldEditor();
tester.noFieldWithName('New field 1');
// go back to inline database view, expect field to be shown
await tester.tapTabBarLinkedViewByViewName('Untitled');
tester.findFieldWithName('New field 1');
@@ -60,5 +66,40 @@ void main() {
await tester.tapDatabaseSortButton();
await tester.tapCreateSortByFieldType(FieldType.RichText, "New field 1");
});
testWidgets('field cell width', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// create a database and add a linked database view
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Grid);
// create a field
await tester.scrollToRight(find.byType(GridPage));
await tester.tapNewPropertyButton();
await tester.renameField('New field 1');
await tester.dismissFieldEditor();
// check the width of the field
expect(tester.getFieldWidth('New field 1'), 150);
// change the width of the field
await tester.changeFieldWidth('New field 1', 200);
expect(tester.getFieldWidth('New field 1'), 205);
// create another field, New field 1 to be same width
await tester.tapNewPropertyButton();
await tester.dismissFieldEditor();
expect(tester.getFieldWidth('New field 1'), 205);
// go back to inline database view, expect New field 1 to be 150px
await tester.tapTabBarLinkedViewByViewName('Untitled');
expect(tester.getFieldWidth('New field 1'), 150);
// go back to linked database view, expect New field 1 to be 205px
await tester.tapTabBarLinkedViewByViewName('Grid');
expect(tester.getFieldWidth('New field 1'), 205);
});
});
}
@@ -1,5 +1,6 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -47,5 +48,41 @@ void main() {
expect(editorState.selection!.start.offset, 0);
});
testWidgets('select and delete text', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
/// create a new document
await tester.createNewPageWithNameUnderParent();
/// input text
final editor = tester.editor;
final editorState = editor.getCurrentEditorState();
const inputText = 'Test for text selection and deletion';
final texts = inputText.split(' ');
await editor.tapLineOfEditorAt(0);
await tester.ime.insertText(inputText);
/// selecte and delete
int index = 0;
while (texts.isNotEmpty) {
final text = texts.removeAt(0);
await tester.editor.updateSelection(
Selection(
start: Position(path: [0], offset: index),
end: Position(path: [0], offset: index + text.length),
),
);
await tester.simulateKeyEvent(LogicalKeyboardKey.delete);
index++;
}
/// excpete the text value is correct
final node = editorState.getNodeAtPath([0])!;
final nodeText = node.delta?.toPlainText() ?? '';
expect(nodeText, ' ' * (index - 1));
});
});
}
@@ -942,6 +942,31 @@ extension AppFlowyDatabaseTest on WidgetTester {
await pumpAndSettle(const Duration(milliseconds: 200));
}
Future<void> changeFieldWidth(String fieldName, double width) async {
final field = find.byWidgetPredicate(
(widget) => widget is GridFieldCell && widget.fieldInfo.name == fieldName,
);
await hoverOnWidget(
field,
onHover: () async {
final dragHandle = find.descendant(
of: field,
matching: find.byType(DragToExpandLine),
);
await drag(dragHandle, Offset(width - getSize(field).width, 0));
await pumpAndSettle(const Duration(milliseconds: 200));
},
);
}
double getFieldWidth(String fieldName) {
final field = find.byWidgetPredicate(
(widget) => widget is GridFieldCell && widget.fieldInfo.name == fieldName,
);
return getSize(field).width;
}
Future<void> findDateEditor(dynamic matcher) async {
final finder = find.byType(DateCellEditor);
expect(finder, matcher);
@@ -411,23 +411,28 @@ class FieldController {
/// Listen for field setting changes in the backend.
void _listenOnFieldSettingsChanged() {
FieldInfo? updateFieldSettings(FieldSettingsPB updatedFieldSettings) {
final List<FieldInfo> newFields = fieldInfos;
var updatedField = newFields.firstOrNull;
final newFields = [...fieldInfos];
if (updatedField == null) {
if (newFields.isEmpty) {
return null;
}
final index = newFields
.indexWhere((field) => field.id == updatedFieldSettings.fieldId);
if (index != -1) {
newFields[index] =
newFields[index].copyWith(fieldSettings: updatedFieldSettings);
updatedField = newFields[index];
_fieldNotifier.fieldInfos = newFields;
_fieldSettings
..removeWhere(
(field) => field.fieldId == updatedFieldSettings.fieldId,
)
..add(updatedFieldSettings);
return newFields[index];
}
_fieldNotifier.fieldInfos = newFields;
return updatedField;
return null;
}
_fieldSettingsListener.start(
@@ -108,7 +108,7 @@ class _GridFieldCellState extends State<GridFieldCell> {
top: 0,
bottom: 0,
right: 0,
child: _DragToExpandLine(),
child: DragToExpandLine(),
);
return _GridHeaderCellContainer(
@@ -158,8 +158,11 @@ class _GridHeaderCellContainer extends StatelessWidget {
}
}
class _DragToExpandLine extends StatelessWidget {
const _DragToExpandLine();
@visibleForTesting
class DragToExpandLine extends StatelessWidget {
const DragToExpandLine({
super.key,
});
@override
Widget build(BuildContext context) {
@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:appflowy/ai/ai.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_result/appflowy_result.dart';
@@ -15,6 +16,11 @@ import 'ai_writer_block_operations.dart';
import 'ai_writer_entities.dart';
import 'ai_writer_node_extension.dart';
/// Enable the debug log for the AiWriterCubit.
///
/// This is useful for debugging the AI writer cubit.
const _aiWriterCubitDebugLog = false;
class AiWriterCubit extends Cubit<AiWriterState> {
AiWriterCubit({
required this.documentId,
@@ -95,6 +101,10 @@ class AiWriterCubit extends Cubit<AiWriterState> {
final command = node.aiWriterCommand;
final (run, prompt) = await _addSelectionTextToRecords(command);
_aiWriterCubitLog(
'command: $command, run: $run, prompt: $prompt',
);
if (!run) {
await exit();
return;
@@ -211,20 +221,26 @@ class AiWriterCubit extends Cubit<AiWriterState> {
return;
}
// Accept
//
// If the user clicks accept, we need to replace the selection with the AI's response
if (action case SuggestionAction.accept) {
await _textRobot.persist();
await formatSelection(
editorState,
selection,
ApplySuggestionFormatType.clear,
// trim the markdown text to avoid extra new lines
final trimmedMarkdownText = _textRobot.markdownText.trim();
_aiWriterCubitLog(
'trigger accept action, markdown text: $trimmedMarkdownText',
);
final nodes = editorState.getNodesInSelection(selection);
final transaction = editorState.transaction..deleteNodes(nodes);
await editorState.apply(
transaction,
withUpdateSelection: false,
await _textRobot.deleteAINodes();
await _textRobot.replace(
selection: selection,
markdownText: trimmedMarkdownText,
);
await exit(withDiscard: false, withUnformat: false);
return;
}
@@ -276,17 +292,24 @@ class AiWriterCubit extends Cubit<AiWriterState> {
AiWriterCommand command,
) async {
final node = aiWriterNode;
// check the node is registered
if (node == null) {
return (false, '');
}
// check the selection is valid
final selection = node.aiWriterSelection?.normalized;
if (selection == null) {
return (false, '');
}
// if the command is continue writing, we don't need to get the selection text
if (command == AiWriterCommand.continueWriting) {
return (true, '');
}
// if the selection is collapsed, we don't need to get the selection text
if (selection.isCollapsed) {
return (true, '');
}
@@ -297,6 +320,7 @@ class AiWriterCubit extends Cubit<AiWriterState> {
records.add(
AiWriterRecord.user(content: selectionText, format: null),
);
return (true, '');
} else {
return (true, selectionText);
@@ -540,6 +564,10 @@ class AiWriterCubit extends Cubit<AiWriterState> {
attributes: ApplySuggestionFormatType.replace.attributes,
);
onAppendToDocument?.call();
_aiWriterCubitLog(
'received message: $text',
);
},
processAssistMessage: (text) async {
if (state case final GeneratingAiWriterState generatingState) {
@@ -551,6 +579,10 @@ class AiWriterCubit extends Cubit<AiWriterState> {
),
);
}
_aiWriterCubitLog(
'received assist message: $text',
);
},
onEnd: () async {
if (state case final GeneratingAiWriterState generatingState) {
@@ -567,6 +599,10 @@ class AiWriterCubit extends Cubit<AiWriterState> {
records.add(
AiWriterRecord.ai(content: _textRobot.markdownText),
);
_aiWriterCubitLog(
'returned response: ${_textRobot.markdownText}',
);
}
},
onError: (error) async {
@@ -658,6 +694,12 @@ class AiWriterCubit extends Cubit<AiWriterState> {
);
}
}
void _aiWriterCubitLog(String message) {
if (_aiWriterCubitDebugLog) {
Log.debug('[AiWriterCubit] $message');
}
}
}
mixin RegisteredAiWriter {
@@ -57,11 +57,30 @@ extension AiWriterNodeExtension on EditorState {
slicedNodes.add(copiedNode);
}
for (final (i, node) in slicedNodes.indexed) {
final childNodesShouldBeDeleted = <Node>[];
for (final child in node.children) {
if (!child.path.inSelection(selection)) {
childNodesShouldBeDeleted.add(child);
}
}
for (final child in childNodesShouldBeDeleted) {
slicedNodes[i] = node.copyWith(
children: node.children.where((e) => e.id != child.id).toList(),
type: selection.startIndex != 0 ? ParagraphBlockKeys.type : node.type,
);
}
}
// use \n\n as line break to improve the ai response
// using \n will cause the ai response treat the text as a single line
final markdown = await customDocumentToMarkdown(
Document.blank()..insert([0], slicedNodes),
lineBreak: '\n\n',
);
return markdown;
// trim the last \n if it exists
return markdown.trimRight();
}
List<String> getPlainTextInSelection(Selection? selection) {
@@ -110,10 +110,13 @@ class MarkdownTextRobot {
}
/// Persist the text into the document
Future<void> persist({String? markdownText}) async {
Future<void> persist({
String? markdownText,
}) async {
if (markdownText != null) {
_markdownText = markdownText;
}
await _lock.synchronized(() async {
await _refresh(inMemoryUpdate: false);
});
@@ -124,6 +127,34 @@ class MarkdownTextRobot {
}
}
/// Replace the selected content with the AI's response
Future<void> replace({
required Selection selection,
required String markdownText,
}) async {
if (selection.isSingle) {
await _replaceInSameLine(
selection: selection,
markdownText: markdownText,
);
} else {
await _replaceInMultiLines(
selection: selection,
markdownText: markdownText,
);
}
}
/// Delete the temporary inserted AI nodes
Future<void> deleteAINodes() async {
final nodes = getInsertedNodes();
final transaction = editorState.transaction..deleteNodes(nodes);
await editorState.apply(
transaction,
options: const ApplyOptions(recordUndo: false),
);
}
/// Discard the inserted content
Future<void> discard() async {
final start = _insertPosition;
@@ -282,6 +313,161 @@ class MarkdownTextRobot {
children: children,
);
}
/// If the selected content is in the same line,
/// keep the selected node and replace the delta.
Future<void> _replaceInSameLine({
required Selection selection,
required String markdownText,
}) async {
selection = selection.normalized;
// If the selection is not a single node, do nothing.
if (!selection.isSingle) {
assert(false, 'Expected single node selection');
Log.error('Expected single node selection');
return;
}
final startIndex = selection.startIndex;
final endIndex = selection.endIndex;
final length = endIndex - startIndex;
// Get the selected node.
final node = editorState.getNodeAtPath(selection.start.path);
final delta = node?.delta;
if (node == null || delta == null) {
assert(false, 'Expected non-null node and delta');
Log.error('Expected non-null node and delta');
return;
}
// Convert the markdown text to delta.
// Question: Why we need to convert the markdown to document first?
// Answer: Because the markdown text may contain the list item,
// if we convert the markdown to delta directly, the list item will be
// treated as a normal text node, and the delta will be incorrect.
// For example, the markdown text is:
// ```
// 1. item1
// ```
// if we convert the markdown to delta directly, the delta will be:
// ```
// [
// {
// "insert": "1. item1"
// }
// ]
// ```
// if we convert the markdown to document first, the document will be:
// ```
// [
// {
// "type": "numbered_list",
// "children": [
// {
// "insert": "item1"
// }
// ]
// }
// ]
final document = customMarkdownToDocument(markdownText);
final decoder = DeltaMarkdownDecoder();
final markdownDelta =
document.nodeAtPath([0])?.delta ?? decoder.convert(markdownText);
// Replace the delta of the selected node.
final transaction = editorState.transaction;
transaction
..deleteText(node, startIndex, length)
..insertTextDelta(node, startIndex, markdownDelta);
await editorState.apply(transaction);
}
/// If the selected content is in multiple lines
Future<void> _replaceInMultiLines({
required Selection selection,
required String markdownText,
}) async {
selection = selection.normalized;
// If the selection is a single node, do nothing.
if (selection.isSingle) {
assert(false, 'Expected multi-line selection');
Log.error('Expected multi-line selection');
return;
}
final markdownNodes = customMarkdownToDocument(
markdownText,
tableWidth: 250.0,
).root.children;
// Get the selected nodes.
final nodes = editorState.getNodesInSelection(selection);
// Note: Don't change its order, otherwise the delta will be incorrect.
// step 1. merge the first selected node and the first node from the ai response
// step 2. merge the last selected node and the last node from the ai response
// step 3. insert the middle nodes from the ai response
// step 4. delete the middle nodes
final transaction = editorState.transaction;
// step 1
final firstNode = nodes.firstOrNull;
final delta = firstNode?.delta;
final firstMarkdownNode = markdownNodes.firstOrNull;
final firstMarkdownDelta = firstMarkdownNode?.delta;
if (firstNode != null &&
delta != null &&
firstMarkdownNode != null &&
firstMarkdownDelta != null) {
final startIndex = selection.startIndex;
final length = delta.length - startIndex;
transaction
..deleteText(firstNode, startIndex, length)
..insertTextDelta(firstNode, startIndex, firstMarkdownDelta);
}
// step 2
final lastNode = nodes.lastOrNull;
final lastDelta = lastNode?.delta;
final lastMarkdownNode = markdownNodes.lastOrNull;
final lastMarkdownDelta = lastMarkdownNode?.delta;
if (lastNode != null &&
lastDelta != null &&
lastMarkdownNode != null &&
lastMarkdownDelta != null) {
final endIndex = selection.endIndex;
transaction.deleteText(lastNode, 0, endIndex);
// if the last node is same as the first node, it means we have replaced the
// selected text in the first node.
if (lastMarkdownNode.id != firstMarkdownNode?.id) {
transaction.insertTextDelta(lastNode, 0, lastMarkdownDelta);
}
}
// step 3
final insertedPath = selection.start.path.nextNPath(1);
if (markdownNodes.length > 2) {
transaction.insertNodes(
insertedPath,
markdownNodes.skip(1).take(markdownNodes.length - 2).toList(),
);
}
// step 4
final length = nodes.length - 2;
if (length > 0) {
final middleNodes = nodes.skip(1).take(length).toList();
transaction.deleteNodes(middleNodes);
}
await editorState.apply(transaction);
}
}
class AINodeExternalValues extends NodeExternalValues {
@@ -568,7 +568,6 @@ class EditorStyleCustomizer {
if (style == null) {
return null;
}
final fontSize = style.fontSize ?? 14.0;
final isLight = Theme.of(context).isLightMode;
final textColor = isLight ? Color(0xFF007296) : Color(0xFF49CFF4);
final underlineColor = isLight ? Color(0x33005A7A) : Color(0x3349CFF4);
@@ -578,17 +577,10 @@ class EditorStyleCustomizer {
decoration: TextDecoration.lineThrough,
),
AiWriterBlockKeys.suggestionReplacement => style.copyWith(
color: Colors.transparent,
color: textColor,
decoration: TextDecoration.underline,
decorationColor: underlineColor,
decorationThickness: 1.0,
// hack: https://jtmuller5.medium.com/the-ultimate-guide-to-underlining-text-in-flutter-57936f5c79bb
shadows: [
Shadow(
color: textColor,
offset: Offset(0, -fontSize * 0.2),
),
],
),
_ => style,
};
@@ -38,10 +38,6 @@ Future<bool> emojiCommandHandler(
return false;
}
if (!selection.isCollapsed) {
await editorState.deleteSelection(selection);
}
final node = editorState.getNodeAtPath(selection.end.path);
final delta = node?.delta;
if (node == null ||
@@ -58,6 +54,8 @@ Future<bool> emojiCommandHandler(
if (previousCharacter != _emojiCharacter) return false;
if (!context.mounted) return false;
if (!selection.isCollapsed) return false;
await editorState.insertTextAtPosition(
character,
position: selection.start,
@@ -27,6 +27,7 @@ Future<String> customDocumentToMarkdown(
Document document, {
String path = '',
AsyncValueSetter<Archive>? onArchive,
String lineBreak = '',
}) async {
final List<Future<ArchiveFile>> fileFutures = [];
@@ -41,6 +42,7 @@ Future<String> customDocumentToMarkdown(
try {
markdown = documentToMarkdown(
document,
lineBreak: lineBreak,
customParsers: [
const MathEquationNodeParser(),
const CalloutNodeParser(),
@@ -140,10 +140,8 @@ class LocalAISettingPanel extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const LocalAIStatusIndicator(),
if (state.showSettings) ...[
const VSpace(10),
OllamaSettingPage(),
],
const VSpace(10),
OllamaSettingPage(),
],
);
},
@@ -288,6 +288,10 @@ class _LackOfResource extends StatelessWidget {
text: LocaleKeys.settings_aiPage_keys_modelsMissing.tr(),
style: textStyle,
),
TextSpan(
text: modelNames.join(', '),
style: textStyle,
),
TextSpan(
text: ' ',
style: textStyle,
@@ -71,7 +71,7 @@ class AppFlowyCloudViewSetting extends StatelessWidget {
const VSpace(8),
const AppFlowyCloudEnableSync(),
const VSpace(6),
const AppFlowyCloudSyncLogEnabled(),
// const AppFlowyCloudSyncLogEnabled(),
const VSpace(12),
RestartButton(
onClick: () {
@@ -130,7 +130,7 @@ class CustomAppFlowyCloudView extends StatelessWidget {
final List<Widget> children = [];
children.addAll([
const AppFlowyCloudEnableSync(),
const AppFlowyCloudSyncLogEnabled(),
// const AppFlowyCloudSyncLogEnabled(),
const VSpace(40),
]);
+2 -2
View File
@@ -98,8 +98,8 @@ packages:
dependency: "direct main"
description:
path: "."
ref: "552f95f"
resolved-ref: "552f95fd15627e10a138c6db2a6d0a8089bc9a25"
ref: "361b99c38370abeeb19656f89e8c31cb3666623b"
resolved-ref: "361b99c38370abeeb19656f89e8c31cb3666623b"
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
source: git
version: "5.1.0"
+1 -1
View File
@@ -184,7 +184,7 @@ dependency_overrides:
appflowy_editor:
git:
url: https://github.com/AppFlowy-IO/appflowy-editor.git
ref: "552f95f"
ref: "361b99c38370abeeb19656f89e8c31cb3666623b"
appflowy_editor_plugins:
git:
@@ -375,7 +375,7 @@ void main() {
await blocResponseFuture();
bloc.runResponseAction(SuggestionAction.accept);
await blocResponseFuture();
expect(editorState.document.root.children.length, 1);
expect(editorState.document.root.children.length, 2);
expect(
editorState.getNodeAtPath([0])!.delta!.toPlainText(),
'Hello World',
@@ -294,6 +294,514 @@ void main() {
);
});
});
group('markdown text robot - replace in same line:', () {
final text1 =
'''The introduction of the World Wide Web in the early 1990s marked a turning point. ''';
final text2 =
'''Tim Berners-Lee's invention made the internet accessible to non-technical users, opening the floodgates for mass adoption. ''';
final text3 =
'''Email became widespread, and instant messaging services like ICQ and AOL Instant Messenger gained popularity, allowing for real-time text communication.''';
Document buildTestDocument() {
return Document(
root: pageNode(
children: [
paragraphNode(delta: Delta()..insert(text1 + text2 + text3)),
],
),
);
}
// 1. create a document with a paragraph node
// 2. use the text robot to replace the selected content in the same line
// 3. check the document
test('the selection is in the middle of the text', () async {
final document = buildTestDocument();
final editorState = EditorState(document: document);
editorState.selection = Selection(
start: Position(
path: [0],
offset: text1.length,
),
end: Position(
path: [0],
offset: text1.length + text2.length,
),
);
final markdownText =
'''Tim Berners-Lee's invention of the **World Wide Web** transformed the internet, making it accessible to _non-technical users_ and opening the floodgates for global mass adoption.''';
final markdownTextRobot = MarkdownTextRobot(
editorState: editorState,
);
await markdownTextRobot.replace(
selection: editorState.selection!,
markdownText: markdownText,
);
final afterDelta = editorState.document.root.children[0].delta!.toList();
expect(afterDelta.length, 5);
final d1 = afterDelta[0] as TextInsert;
expect(d1.text, '${text1}Tim Berners-Lee\'s invention of the ');
expect(d1.attributes, null);
final d2 = afterDelta[1] as TextInsert;
expect(d2.text, 'World Wide Web');
expect(d2.attributes, {AppFlowyRichTextKeys.bold: true});
final d3 = afterDelta[2] as TextInsert;
expect(d3.text, ' transformed the internet, making it accessible to ');
expect(d3.attributes, null);
final d4 = afterDelta[3] as TextInsert;
expect(d4.text, 'non-technical users');
expect(d4.attributes, {AppFlowyRichTextKeys.italic: true});
final d5 = afterDelta[4] as TextInsert;
expect(
d5.text,
' and opening the floodgates for global mass adoption.$text3',
);
expect(d5.attributes, null);
});
test('replace markdown text with selection from start to middle', () async {
final document = buildTestDocument();
final editorState = EditorState(document: document);
editorState.selection = Selection(
start: Position(
path: [0],
),
end: Position(
path: [0],
offset: text1.length,
),
);
final markdownText =
'''The **invention** of the _World Wide Web_ by Tim Berners-Lee transformed how we access information.''';
final markdownTextRobot = MarkdownTextRobot(
editorState: editorState,
);
await markdownTextRobot.replace(
selection: editorState.selection!,
markdownText: markdownText,
);
final afterDelta = editorState.document.root.children[0].delta!.toList();
expect(afterDelta.length, 5);
final d1 = afterDelta[0] as TextInsert;
expect(d1.text, 'The ');
expect(d1.attributes, null);
final d2 = afterDelta[1] as TextInsert;
expect(d2.text, 'invention');
expect(d2.attributes, {AppFlowyRichTextKeys.bold: true});
final d3 = afterDelta[2] as TextInsert;
expect(d3.text, ' of the ');
expect(d3.attributes, null);
final d4 = afterDelta[3] as TextInsert;
expect(d4.text, 'World Wide Web');
expect(d4.attributes, {AppFlowyRichTextKeys.italic: true});
final d5 = afterDelta[4] as TextInsert;
expect(
d5.text,
' by Tim Berners-Lee transformed how we access information.$text2$text3',
);
expect(d5.attributes, null);
});
test('replace markdown text with selection from middle to end', () async {
final document = buildTestDocument();
final editorState = EditorState(document: document);
editorState.selection = Selection(
start: Position(
path: [0],
offset: text1.length + text2.length,
),
end: Position(
path: [0],
offset: text1.length + text2.length + text3.length,
),
);
final markdownText =
'''**Email** became widespread, and instant messaging services like *ICQ* and **AOL Instant Messenger** gained tremendous popularity, allowing for seamless real-time text communication across the globe.''';
final markdownTextRobot = MarkdownTextRobot(
editorState: editorState,
);
await markdownTextRobot.replace(
selection: editorState.selection!,
markdownText: markdownText,
);
final afterDelta = editorState.document.root.children[0].delta!.toList();
expect(afterDelta.length, 7);
final d1 = afterDelta[0] as TextInsert;
expect(
d1.text,
text1 + text2,
);
expect(d1.attributes, null);
final d2 = afterDelta[1] as TextInsert;
expect(d2.text, 'Email');
expect(d2.attributes, {AppFlowyRichTextKeys.bold: true});
final d3 = afterDelta[2] as TextInsert;
expect(
d3.text,
' became widespread, and instant messaging services like ',
);
expect(d3.attributes, null);
final d4 = afterDelta[3] as TextInsert;
expect(d4.text, 'ICQ');
expect(d4.attributes, {AppFlowyRichTextKeys.italic: true});
final d5 = afterDelta[4] as TextInsert;
expect(d5.text, ' and ');
expect(d5.attributes, null);
final d6 = afterDelta[5] as TextInsert;
expect(
d6.text,
'AOL Instant Messenger',
);
expect(d6.attributes, {AppFlowyRichTextKeys.bold: true});
final d7 = afterDelta[6] as TextInsert;
expect(
d7.text,
' gained tremendous popularity, allowing for seamless real-time text communication across the globe.',
);
expect(d7.attributes, null);
});
});
group('markdown text robot - replace in multiple lines:', () {
final text1 =
'''The introduction of the World Wide Web in the early 1990s marked a turning point. ''';
final text2 =
'''Tim Berners-Lee's invention made the internet accessible to non-technical users, opening the floodgates for mass adoption. ''';
final text3 =
'''Email became widespread, and instant messaging services like ICQ and AOL Instant Messenger gained popularity, allowing for real-time text communication.''';
Document buildTestDocument() {
return Document(
root: pageNode(
children: [
paragraphNode(delta: Delta()..insert(text1)),
paragraphNode(delta: Delta()..insert(text2)),
paragraphNode(delta: Delta()..insert(text3)),
],
),
);
}
// 1. create a document with 3 paragraph nodes
// 2. use the text robot to replace the selected content in the multiple lines
// 3. check the document
test(
'the selection starts with the first paragraph and ends with the middle of second paragraph',
() async {
final document = buildTestDocument();
final editorState = EditorState(document: document);
editorState.selection = Selection(
start: Position(
path: [0],
),
end: Position(
path: [1],
offset: text2.length -
', opening the floodgates for mass adoption. '.length,
),
);
final markdownText =
'''The **introduction** of the World Wide Web in the *early 1990s* marked a significant turning point.
Tim Berners-Lee's **revolutionary invention** made the internet accessible to non-technical users''';
final markdownTextRobot = MarkdownTextRobot(
editorState: editorState,
);
await markdownTextRobot.replace(
selection: editorState.selection!,
markdownText: markdownText,
);
final afterNodes = editorState.document.root.children;
expect(afterNodes.length, 3);
{
// first paragraph
final delta1 = afterNodes[0].delta!.toList();
expect(delta1.length, 5);
final d1 = delta1[0] as TextInsert;
expect(d1.text, 'The ');
expect(d1.attributes, null);
final d2 = delta1[1] as TextInsert;
expect(d2.text, 'introduction');
expect(d2.attributes, {AppFlowyRichTextKeys.bold: true});
final d3 = delta1[2] as TextInsert;
expect(d3.text, ' of the World Wide Web in the ');
expect(d3.attributes, null);
final d4 = delta1[3] as TextInsert;
expect(d4.text, 'early 1990s');
expect(d4.attributes, {AppFlowyRichTextKeys.italic: true});
final d5 = delta1[4] as TextInsert;
expect(d5.text, ' marked a significant turning point.');
expect(d5.attributes, null);
}
{
// second paragraph
final delta2 = afterNodes[1].delta!.toList();
expect(delta2.length, 3);
final d1 = delta2[0] as TextInsert;
expect(d1.text, "Tim Berners-Lee's ");
expect(d1.attributes, null);
final d2 = delta2[1] as TextInsert;
expect(d2.text, "revolutionary invention");
expect(d2.attributes, {AppFlowyRichTextKeys.bold: true});
final d3 = delta2[2] as TextInsert;
expect(
d3.text,
" made the internet accessible to non-technical users, opening the floodgates for mass adoption. ",
);
expect(d3.attributes, null);
}
{
// third paragraph
final delta3 = afterNodes[2].delta!.toList();
expect(delta3.length, 1);
final d1 = delta3[0] as TextInsert;
expect(d1.text, text3);
expect(d1.attributes, null);
}
});
test(
'the selection starts with the middle of the first paragraph and ends with the middle of last paragraph',
() async {
final document = buildTestDocument();
final editorState = EditorState(document: document);
editorState.selection = Selection(
start: Position(
path: [0],
offset: 'The introduction of the World Wide Web'.length,
),
end: Position(
path: [2],
offset:
'Email became widespread, and instant messaging services like ICQ and AOL Instant Messenger gained popularity'
.length,
),
);
final markdownText =
''' in the **early 1990s** marked a *significant turning point* in technological history.
Tim Berners-Lee's **revolutionary invention** made the internet accessible to non-technical users, opening the floodgates for *unprecedented mass adoption*.
Email became **widely prevalent**, and instant messaging services like *ICQ* and *AOL Instant Messenger* gained tremendous popularity
''';
final markdownTextRobot = MarkdownTextRobot(
editorState: editorState,
);
await markdownTextRobot.replace(
selection: editorState.selection!,
markdownText: markdownText,
);
final afterNodes = editorState.document.root.children;
expect(afterNodes.length, 3);
{
// first paragraph
final delta1 = afterNodes[0].delta!.toList();
expect(delta1.length, 5);
final d1 = delta1[0] as TextInsert;
expect(d1.text, 'The introduction of the World Wide Web in the ');
expect(d1.attributes, null);
final d2 = delta1[1] as TextInsert;
expect(d2.text, 'early 1990s');
expect(d2.attributes, {AppFlowyRichTextKeys.bold: true});
final d3 = delta1[2] as TextInsert;
expect(d3.text, ' marked a ');
expect(d3.attributes, null);
final d4 = delta1[3] as TextInsert;
expect(d4.text, 'significant turning point');
expect(d4.attributes, {AppFlowyRichTextKeys.italic: true});
final d5 = delta1[4] as TextInsert;
expect(d5.text, ' in technological history.');
expect(d5.attributes, null);
}
{
// second paragraph
final delta2 = afterNodes[1].delta!.toList();
expect(delta2.length, 5);
final d1 = delta2[0] as TextInsert;
expect(d1.text, "Tim Berners-Lee's ");
expect(d1.attributes, null);
final d2 = delta2[1] as TextInsert;
expect(d2.text, "revolutionary invention");
expect(d2.attributes, {AppFlowyRichTextKeys.bold: true});
final d3 = delta2[2] as TextInsert;
expect(
d3.text,
" made the internet accessible to non-technical users, opening the floodgates for ",
);
expect(d3.attributes, null);
final d4 = delta2[3] as TextInsert;
expect(d4.text, "unprecedented mass adoption");
expect(d4.attributes, {AppFlowyRichTextKeys.italic: true});
final d5 = delta2[4] as TextInsert;
expect(d5.text, ".");
expect(d5.attributes, null);
}
{
// third paragraph
// third paragraph
final delta3 = afterNodes[2].delta!.toList();
expect(delta3.length, 7);
final d1 = delta3[0] as TextInsert;
expect(d1.text, "Email became ");
expect(d1.attributes, null);
final d2 = delta3[1] as TextInsert;
expect(d2.text, "widely prevalent");
expect(d2.attributes, {AppFlowyRichTextKeys.bold: true});
final d3 = delta3[2] as TextInsert;
expect(d3.text, ", and instant messaging services like ");
expect(d3.attributes, null);
final d4 = delta3[3] as TextInsert;
expect(d4.text, "ICQ");
expect(d4.attributes, {AppFlowyRichTextKeys.italic: true});
final d5 = delta3[4] as TextInsert;
expect(d5.text, " and ");
expect(d5.attributes, null);
final d6 = delta3[5] as TextInsert;
expect(d6.text, "AOL Instant Messenger");
expect(d6.attributes, {AppFlowyRichTextKeys.italic: true});
final d7 = delta3[6] as TextInsert;
expect(
d7.text,
" gained tremendous popularity, allowing for real-time text communication.",
);
expect(d7.attributes, null);
}
});
test(
'the length of the returned response less than the length of the selected text',
() async {
final document = buildTestDocument();
final editorState = EditorState(document: document);
editorState.selection = Selection(
start: Position(
path: [0],
offset: 'The introduction of the World Wide Web'.length,
),
end: Position(
path: [2],
offset:
'Email became widespread, and instant messaging services like ICQ and AOL Instant Messenger gained popularity'
.length,
),
);
final markdownText =
''' in the **early 1990s** marked a *significant turning point* in technological history.''';
final markdownTextRobot = MarkdownTextRobot(
editorState: editorState,
);
await markdownTextRobot.replace(
selection: editorState.selection!,
markdownText: markdownText,
);
final afterNodes = editorState.document.root.children;
expect(afterNodes.length, 2);
{
// first paragraph
final delta1 = afterNodes[0].delta!.toList();
expect(delta1.length, 5);
final d1 = delta1[0] as TextInsert;
expect(d1.text, "The introduction of the World Wide Web in the ");
expect(d1.attributes, null);
final d2 = delta1[1] as TextInsert;
expect(d2.text, "early 1990s");
expect(d2.attributes, {AppFlowyRichTextKeys.bold: true});
final d3 = delta1[2] as TextInsert;
expect(d3.text, " marked a ");
expect(d3.attributes, null);
final d4 = delta1[3] as TextInsert;
expect(d4.text, "significant turning point");
expect(d4.attributes, {AppFlowyRichTextKeys.italic: true});
final d5 = delta1[4] as TextInsert;
expect(d5.text, " in technological history.");
expect(d5.attributes, null);
}
{
// second paragraph
final delta2 = afterNodes[1].delta!.toList();
expect(delta2.length, 1);
final d1 = delta2[0] as TextInsert;
expect(d1.text, ", allowing for real-time text communication.");
expect(d1.attributes, null);
}
});
});
}
const _sample1 = '''# The Curious Cat
+2 -2
View File
@@ -884,7 +884,7 @@
"pleaseFollowThese": "Please follow these",
"instructions": "instructions",
"installOllamaLai": "to set up Ollama and AppFlowy Local AI.",
"modelsMissing": "Cannot find the required models.",
"modelsMissing": "Cannot find the required models: ",
"downloadModel": "to download them."
}
},
@@ -3209,4 +3209,4 @@
"rewrite": "Rewrite",
"insertBelow": "Insert below"
}
}
}
+33 -55
View File
@@ -496,7 +496,7 @@ checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e"
dependencies = [
"anyhow",
"bincode",
@@ -516,16 +516,16 @@ dependencies = [
[[package]]
name = "appflowy-ai-client"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e"
dependencies = [
"anyhow",
"bytes",
"futures",
"pin-project",
"serde",
"serde_json",
"serde_repr",
"thiserror 1.0.64",
"uuid",
]
[[package]]
@@ -1137,7 +1137,7 @@ dependencies = [
[[package]]
name = "client-api"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e"
dependencies = [
"again",
"anyhow",
@@ -1192,7 +1192,7 @@ dependencies = [
[[package]]
name = "client-api-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e"
dependencies = [
"collab-entity",
"collab-rt-entity",
@@ -1205,7 +1205,7 @@ dependencies = [
[[package]]
name = "client-websocket"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e"
dependencies = [
"futures-channel",
"futures-util",
@@ -1248,7 +1248,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=80d1c6147d1139289c2eaadab40557cc86c0f4b6#80d1c6147d1139289c2eaadab40557cc86c0f4b6"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4e717c2c6a15c42feda9e1ff1b122c7b0baf1821#4e717c2c6a15c42feda9e1ff1b122c7b0baf1821"
dependencies = [
"anyhow",
"arc-swap",
@@ -1273,7 +1273,7 @@ dependencies = [
[[package]]
name = "collab-database"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=80d1c6147d1139289c2eaadab40557cc86c0f4b6#80d1c6147d1139289c2eaadab40557cc86c0f4b6"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4e717c2c6a15c42feda9e1ff1b122c7b0baf1821#4e717c2c6a15c42feda9e1ff1b122c7b0baf1821"
dependencies = [
"anyhow",
"async-trait",
@@ -1313,7 +1313,7 @@ dependencies = [
[[package]]
name = "collab-document"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=80d1c6147d1139289c2eaadab40557cc86c0f4b6#80d1c6147d1139289c2eaadab40557cc86c0f4b6"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4e717c2c6a15c42feda9e1ff1b122c7b0baf1821#4e717c2c6a15c42feda9e1ff1b122c7b0baf1821"
dependencies = [
"anyhow",
"arc-swap",
@@ -1334,7 +1334,7 @@ dependencies = [
[[package]]
name = "collab-entity"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=80d1c6147d1139289c2eaadab40557cc86c0f4b6#80d1c6147d1139289c2eaadab40557cc86c0f4b6"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4e717c2c6a15c42feda9e1ff1b122c7b0baf1821#4e717c2c6a15c42feda9e1ff1b122c7b0baf1821"
dependencies = [
"anyhow",
"bytes",
@@ -1354,7 +1354,7 @@ dependencies = [
[[package]]
name = "collab-folder"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=80d1c6147d1139289c2eaadab40557cc86c0f4b6#80d1c6147d1139289c2eaadab40557cc86c0f4b6"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4e717c2c6a15c42feda9e1ff1b122c7b0baf1821#4e717c2c6a15c42feda9e1ff1b122c7b0baf1821"
dependencies = [
"anyhow",
"arc-swap",
@@ -1376,7 +1376,7 @@ dependencies = [
[[package]]
name = "collab-importer"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=80d1c6147d1139289c2eaadab40557cc86c0f4b6#80d1c6147d1139289c2eaadab40557cc86c0f4b6"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4e717c2c6a15c42feda9e1ff1b122c7b0baf1821#4e717c2c6a15c42feda9e1ff1b122c7b0baf1821"
dependencies = [
"anyhow",
"async-recursion",
@@ -1418,7 +1418,6 @@ version = "0.1.0"
dependencies = [
"anyhow",
"arc-swap",
"async-trait",
"collab",
"collab-database",
"collab-document",
@@ -1429,18 +1428,18 @@ dependencies = [
"diesel",
"flowy-error",
"flowy-sqlite",
"futures",
"lib-infra",
"serde",
"serde_json",
"tokio",
"tracing",
"uuid",
]
[[package]]
name = "collab-plugins"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=80d1c6147d1139289c2eaadab40557cc86c0f4b6#80d1c6147d1139289c2eaadab40557cc86c0f4b6"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4e717c2c6a15c42feda9e1ff1b122c7b0baf1821#4e717c2c6a15c42feda9e1ff1b122c7b0baf1821"
dependencies = [
"anyhow",
"async-stream",
@@ -1478,7 +1477,7 @@ dependencies = [
[[package]]
name = "collab-rt-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e"
dependencies = [
"anyhow",
"bincode",
@@ -1500,7 +1499,7 @@ dependencies = [
[[package]]
name = "collab-rt-protocol"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e"
dependencies = [
"anyhow",
"async-trait",
@@ -1511,13 +1510,14 @@ dependencies = [
"thiserror 1.0.64",
"tokio",
"tracing",
"uuid",
"yrs",
]
[[package]]
name = "collab-user"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=80d1c6147d1139289c2eaadab40557cc86c0f4b6#80d1c6147d1139289c2eaadab40557cc86c0f4b6"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4e717c2c6a15c42feda9e1ff1b122c7b0baf1821#4e717c2c6a15c42feda9e1ff1b122c7b0baf1821"
dependencies = [
"anyhow",
"collab",
@@ -1764,7 +1764,7 @@ dependencies = [
"cssparser-macros",
"dtoa-short",
"itoa",
"phf 0.11.2",
"phf 0.8.0",
"smallvec",
]
@@ -1947,7 +1947,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e"
dependencies = [
"bincode",
"bytes",
@@ -2513,13 +2513,13 @@ dependencies = [
name = "flowy-ai-pub"
version = "0.1.0"
dependencies = [
"bytes",
"client-api",
"flowy-error",
"futures",
"lib-infra",
"serde",
"serde_json",
"uuid",
]
[[package]]
@@ -2592,7 +2592,6 @@ dependencies = [
"flowy-storage-pub",
"flowy-user",
"flowy-user-pub",
"futures",
"futures-core",
"lib-dispatch",
"lib-infra",
@@ -2606,19 +2605,18 @@ dependencies = [
"tokio-stream",
"tracing",
"uuid",
"walkdir",
]
[[package]]
name = "flowy-database-pub"
version = "0.1.0"
dependencies = [
"anyhow",
"client-api",
"collab",
"collab-entity",
"flowy-error",
"lib-infra",
"uuid",
]
[[package]]
@@ -2667,6 +2665,7 @@ dependencies = [
"tokio-util",
"tracing",
"url",
"uuid",
"validator 0.18.1",
]
@@ -2744,11 +2743,11 @@ dependencies = [
name = "flowy-document-pub"
version = "0.1.0"
dependencies = [
"anyhow",
"collab",
"collab-document",
"flowy-error",
"lib-infra",
"uuid",
]
[[package]]
@@ -2778,6 +2777,7 @@ dependencies = [
"thiserror 1.0.64",
"tokio",
"url",
"uuid",
"validator 0.18.1",
]
@@ -2864,16 +2864,12 @@ dependencies = [
"bytes",
"collab",
"collab-folder",
"diesel",
"diesel_derives",
"diesel_migrations",
"flowy-codegen",
"flowy-derive",
"flowy-error",
"flowy-folder",
"flowy-notification",
"flowy-search-pub",
"flowy-sqlite",
"flowy-user",
"futures",
"lib-dispatch",
@@ -2887,7 +2883,7 @@ dependencies = [
"tempfile",
"tokio",
"tracing",
"validator 0.18.1",
"uuid",
]
[[package]]
@@ -2898,8 +2894,8 @@ dependencies = [
"collab",
"collab-folder",
"flowy-error",
"futures",
"lib-infra",
"uuid",
]
[[package]]
@@ -2991,7 +2987,6 @@ name = "flowy-storage"
version = "0.1.0"
dependencies = [
"allo-isolate",
"anyhow",
"async-trait",
"bytes",
"chrono",
@@ -3003,8 +2998,6 @@ dependencies = [
"flowy-notification",
"flowy-sqlite",
"flowy-storage-pub",
"futures-util",
"fxhash",
"lib-dispatch",
"lib-infra",
"mime_guess",
@@ -3032,9 +3025,8 @@ dependencies = [
"mime",
"mime_guess",
"serde",
"serde_json",
"tokio",
"tracing",
"uuid",
]
[[package]]
@@ -3432,7 +3424,7 @@ dependencies = [
[[package]]
name = "gotrue"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e"
dependencies = [
"anyhow",
"getrandom 0.2.10",
@@ -3447,7 +3439,7 @@ dependencies = [
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e"
dependencies = [
"app-error",
"jsonwebtoken",
@@ -4068,7 +4060,7 @@ dependencies = [
[[package]]
name = "infra"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e"
dependencies = [
"anyhow",
"bytes",
@@ -5181,7 +5173,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
dependencies = [
"phf_macros 0.8.0",
"phf_macros",
"phf_shared 0.8.0",
"proc-macro-hack",
]
@@ -5201,7 +5193,6 @@ version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
dependencies = [
"phf_macros 0.11.3",
"phf_shared 0.11.2",
]
@@ -5269,19 +5260,6 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "phf_macros"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
dependencies = [
"phf_generator 0.11.2",
"phf_shared 0.11.2",
"proc-macro2",
"quote",
"syn 2.0.94",
]
[[package]]
name = "phf_shared"
version = "0.8.0"
@@ -6782,7 +6760,7 @@ dependencies = [
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f7288f46c27dc8e3c7829cda1b70b61118e88336#f7288f46c27dc8e3c7829cda1b70b61118e88336"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e#2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e"
dependencies = [
"anyhow",
"app-error",
+10 -10
View File
@@ -103,8 +103,8 @@ dashmap = "6.0.1"
# Run the script.add_workspace_members:
# scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "f7288f46c27dc8e3c7829cda1b70b61118e88336" }
client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "f7288f46c27dc8e3c7829cda1b70b61118e88336" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e" }
client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "2922db6801ca23c5fd6fe8b4958f03bc54dbcb7e" }
[profile.dev]
opt-level = 0
@@ -139,14 +139,14 @@ rocksdb = { git = "https://github.com/rust-rocksdb/rust-rocksdb", rev = "1710120
# To switch to the local path, run:
# scripts/tool/update_collab_source.sh
# ⚠️⚠️⚠️️
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "80d1c6147d1139289c2eaadab40557cc86c0f4b6" }
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "80d1c6147d1139289c2eaadab40557cc86c0f4b6" }
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "80d1c6147d1139289c2eaadab40557cc86c0f4b6" }
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "80d1c6147d1139289c2eaadab40557cc86c0f4b6" }
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "80d1c6147d1139289c2eaadab40557cc86c0f4b6" }
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "80d1c6147d1139289c2eaadab40557cc86c0f4b6" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "80d1c6147d1139289c2eaadab40557cc86c0f4b6" }
collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "80d1c6147d1139289c2eaadab40557cc86c0f4b6" }
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" }
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" }
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" }
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" }
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" }
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" }
collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4e717c2c6a15c42feda9e1ff1b122c7b0baf1821" }
# Working directory: frontend
# To update the commit ID, run:
@@ -19,14 +19,13 @@ serde.workspace = true
serde_json.workspace = true
anyhow.workspace = true
tracing.workspace = true
async-trait.workspace = true
tokio = { workspace = true, features = ["sync"] }
lib-infra = { workspace = true }
futures = "0.3.31"
arc-swap = "1.7"
flowy-sqlite = { workspace = true }
diesel.workspace = true
flowy-error.workspace = true
uuid.workspace = true
[features]
default = []
@@ -33,8 +33,10 @@ use collab_plugins::local_storage::kv::KVTransactionDB;
use collab_plugins::local_storage::CollabPersistenceConfig;
use collab_user::core::{UserAwareness, UserAwarenessNotifier};
use flowy_error::FlowyError;
use lib_infra::{if_native, if_wasm};
use tracing::{error, instrument, trace, warn};
use uuid::Uuid;
#[derive(Clone, Debug)]
pub enum CollabPluginProviderType {
@@ -66,8 +68,8 @@ impl Display for CollabPluginProviderContext {
}
pub trait WorkspaceCollabIntegrate: Send + Sync {
fn workspace_id(&self) -> Result<String, Error>;
fn device_id(&self) -> Result<String, Error>;
fn workspace_id(&self) -> Result<Uuid, FlowyError>;
fn device_id(&self) -> Result<String, FlowyError>;
}
pub struct AppFlowyCollabBuilder {
@@ -119,15 +121,15 @@ impl AppFlowyCollabBuilder {
pub fn collab_object(
&self,
workspace_id: &str,
workspace_id: &Uuid,
uid: i64,
object_id: &str,
object_id: &Uuid,
collab_type: CollabType,
) -> Result<CollabObject, Error> {
// Compare the workspace_id with the currently opened workspace_id. Return an error if they do not match.
// This check is crucial in asynchronous code contexts where the workspace_id might change during operation.
let actual_workspace_id = self.workspace_integrate.workspace_id()?;
if workspace_id != actual_workspace_id {
if workspace_id != &actual_workspace_id {
return Err(anyhow::anyhow!(
"workspace_id not match when build collab. expect workspace_id: {}, actual workspace_id: {}",
workspace_id,
@@ -135,12 +137,11 @@ impl AppFlowyCollabBuilder {
));
}
let device_id = self.workspace_integrate.device_id()?;
let workspace_id = self.workspace_integrate.workspace_id()?;
Ok(CollabObject::new(
uid,
object_id.to_string(),
collab_type,
workspace_id,
workspace_id.to_string(),
device_id,
))
}
@@ -399,11 +400,11 @@ impl CollabBuilderConfig {
pub struct CollabPersistenceImpl {
pub db: Weak<CollabKVDB>,
pub uid: i64,
pub workspace_id: String,
pub workspace_id: Uuid,
}
impl CollabPersistenceImpl {
pub fn new(db: Weak<CollabKVDB>, uid: i64, workspace_id: String) -> Self {
pub fn new(db: Weak<CollabKVDB>, uid: i64, workspace_id: Uuid) -> Self {
Self {
db,
uid,
@@ -425,10 +426,11 @@ impl CollabPersistence for CollabPersistenceImpl {
let object_id = collab.object_id().to_string();
let rocksdb_read = collab_db.read_txn();
let workspace_id = self.workspace_id.to_string();
if rocksdb_read.is_exist(self.uid, &self.workspace_id, &object_id) {
if rocksdb_read.is_exist(self.uid, &workspace_id, &object_id) {
let mut txn = collab.transact_mut();
match rocksdb_read.load_doc_with_txn(self.uid, &self.workspace_id, &object_id, &mut txn) {
match rocksdb_read.load_doc_with_txn(self.uid, &workspace_id, &object_id, &mut txn) {
Ok(update_count) => {
trace!(
"did load collab:{}-{} from disk, update_count:{}",
@@ -453,6 +455,7 @@ impl CollabPersistence for CollabPersistenceImpl {
object_id: &str,
encoded_collab: EncodedCollab,
) -> Result<(), CollabError> {
let workspace_id = self.workspace_id.to_string();
let collab_db = self
.db
.upgrade()
@@ -461,7 +464,7 @@ impl CollabPersistence for CollabPersistenceImpl {
write_txn
.flush_doc(
self.uid,
self.workspace_id.as_str(),
workspace_id.as_str(),
object_id,
encoded_collab.state_vector.to_vec(),
encoded_collab.doc_state.to_vec(),
@@ -7,6 +7,8 @@ use flowy_sqlite::{
DBConnection, ExpressionMethods, Identifiable, Insertable, Queryable,
};
use std::collections::HashMap;
use std::str::FromStr;
use uuid::Uuid;
#[derive(Queryable, Insertable, Identifiable)]
#[diesel(table_name = af_collab_metadata)]
@@ -43,13 +45,18 @@ pub fn batch_insert_collab_metadata(
pub fn batch_select_collab_metadata(
mut conn: DBConnection,
object_ids: &[String],
) -> FlowyResult<HashMap<String, AFCollabMetadata>> {
object_ids: &[Uuid],
) -> FlowyResult<HashMap<Uuid, AFCollabMetadata>> {
let object_ids = object_ids
.iter()
.map(|id| id.to_string())
.collect::<Vec<String>>();
let metadata = dsl::af_collab_metadata
.filter(af_collab_metadata::object_id.eq_any(object_ids))
.filter(af_collab_metadata::object_id.eq_any(&object_ids))
.load::<AFCollabMetadata>(&mut conn)?
.into_iter()
.map(|m| (m.object_id.clone(), m))
.flat_map(|m| Uuid::from_str(&m.object_id).map(|v| (v, m)))
.collect();
Ok(metadata)
}
@@ -1,8 +1,6 @@
use collab::entity::EncodedCollab;
use std::collections::HashMap;
use serde_json::Value;
use flowy_document::entities::*;
use flowy_document::event_map::DocumentEvent;
use flowy_document::parser::parser_entities::{
@@ -11,6 +9,8 @@ use flowy_document::parser::parser_entities::{
};
use flowy_folder::entities::{CreateViewPayloadPB, ViewLayoutPB, ViewPB};
use flowy_folder::event_map::FolderEvent;
use serde_json::Value;
use uuid::Uuid;
use crate::document::utils::{gen_delta_str, gen_id, gen_text_block_data};
use crate::event_builder::EventBuilder;
@@ -37,7 +37,7 @@ impl DocumentEventTest {
Self { event_test: core }
}
pub async fn get_encoded_v1(&self, doc_id: &str) -> EncodedCollab {
pub async fn get_encoded_v1(&self, doc_id: &Uuid) -> EncodedCollab {
let doc = self
.event_test
.appflowy_core
@@ -1,4 +1,5 @@
use flowy_folder::view_operation::{GatherEncodedCollab, ViewData};
use std::str::FromStr;
use std::sync::Arc;
use collab_folder::{FolderData, View};
@@ -16,6 +17,7 @@ use flowy_user::entities::{
use flowy_user::errors::FlowyError;
use flowy_user::event_map::UserEvent;
use flowy_user_pub::entities::Role;
use uuid::Uuid;
use crate::event_builder::EventBuilder;
use crate::EventIntegrationTest;
@@ -123,10 +125,10 @@ impl EventIntegrationTest {
let create_view_params = views
.into_iter()
.map(|view| CreateViewParams {
parent_view_id: view.parent_view_id,
parent_view_id: Uuid::from_str(&view.parent_view_id).unwrap(),
name: view.name,
layout: view.layout.into(),
view_id: view.id,
view_id: Uuid::from_str(&view.id).unwrap(),
initial_data: ViewData::Empty,
meta: Default::default(),
set_as_current: false,
@@ -195,9 +197,10 @@ impl EventIntegrationTest {
view_id: &str,
layout: ViewLayout,
) -> GatherEncodedCollab {
let view_id = Uuid::from_str(view_id).unwrap();
self
.folder_manager
.gather_publish_encode_collab(view_id, &layout)
.gather_publish_encode_collab(&view_id, &layout)
.await
.unwrap()
}
@@ -1,3 +1,4 @@
use crate::user_event::TestNotificationSender;
use collab::core::collab::DataSource;
use collab::core::origin::CollabOrigin;
use collab::preclude::Collab;
@@ -15,14 +16,14 @@ use nanoid::nanoid;
use semver::Version;
use std::env::temp_dir;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::select;
use tokio::task::LocalSet;
use tokio::time::sleep;
use crate::user_event::TestNotificationSender;
use uuid::Uuid;
mod chat_event;
pub mod database_event;
@@ -145,10 +146,16 @@ impl EventIntegrationTest {
) -> Result<Vec<u8>, FlowyError> {
let server = self.server_provider.get_server().unwrap();
let workspace_id = self.get_current_workspace().await.id;
let oid = Uuid::from_str(oid).unwrap();
let uid = self.get_user_profile().await?.id;
let doc_state = server
.folder_service()
.get_folder_doc_state(&workspace_id, uid, collab_type, oid)
.get_folder_doc_state(
&Uuid::from_str(&workspace_id).unwrap(),
uid,
collab_type,
&oid,
)
.await?;
Ok(doc_state)
@@ -3,10 +3,12 @@ use event_integration_test::user_event::use_localhost_af_cloud;
use event_integration_test::EventIntegrationTest;
use flowy_ai::entities::ChatMessageListPB;
use flowy_ai::notification::ChatNotification;
use std::str::FromStr;
use flowy_ai_pub::cloud::ChatMessageType;
use std::time::Duration;
use uuid::Uuid;
#[tokio::test]
async fn af_cloud_create_chat_message_test() {
@@ -21,8 +23,8 @@ async fn af_cloud_create_chat_message_test() {
for i in 0..10 {
let _ = chat_service
.create_question(
&current_workspace.id,
&chat_id,
&Uuid::from_str(&current_workspace.id).unwrap(),
&Uuid::from_str(&chat_id).unwrap(),
&format!("hello world {}", i),
ChatMessageType::System,
&[],
@@ -77,8 +79,8 @@ async fn af_cloud_load_remote_system_message_test() {
for i in 0..10 {
let _ = chat_service
.create_question(
&current_workspace.id,
&chat_id,
&Uuid::from_str(&current_workspace.id).unwrap(),
&Uuid::from_str(&chat_id).unwrap(),
&format!("hello server {}", i),
ChatMessageType::System,
&[],
@@ -8,6 +8,8 @@ use flowy_document::parser::parser_entities::{
};
use serde_json::{json, Value};
use std::collections::HashMap;
use std::str::FromStr;
use uuid::Uuid;
#[tokio::test]
async fn get_document_event_test() {
@@ -101,8 +103,8 @@ async fn document_size_test() {
let s = generate_random_string(string_size);
test.insert_index(&view.id, &s, 1, None).await;
}
let encoded_v1 = test.get_encoded_v1(&view.id).await;
let view_id = Uuid::from_str(&view.id).unwrap();
let encoded_v1 = test.get_encoded_v1(&view_id).await;
if encoded_v1.doc_state.len() > max_size {
panic!(
"The document size is too large. {}",
@@ -1,8 +1,8 @@
use collab_folder::ViewLayout;
use event_integration_test::EventIntegrationTest;
use flowy_folder::entities::icon::{ViewIconPB, ViewIconTypePB};
use flowy_folder::entities::ViewLayoutPB;
use uuid::Uuid;
use crate::folder::local_test::script::FolderScript::*;
use crate::folder::local_test::script::FolderTest;
@@ -338,11 +338,11 @@ async fn move_view_event_test() {
async fn create_orphan_child_view_and_get_its_ancestors_test() {
let test = EventIntegrationTest::new_anon().await;
let name = "Orphan View";
let view_id = "20240521";
let view_id = Uuid::new_v4().to_string();
test
.create_orphan_view(name, view_id, ViewLayoutPB::Grid)
.create_orphan_view(name, &view_id, ViewLayoutPB::Grid)
.await;
let ancestors = test.get_view_ancestors(view_id).await;
let ancestors = test.get_view_ancestors(&view_id).await;
assert_eq!(ancestors.len(), 1);
assert_eq!(ancestors[0].name, "Orphan View");
assert_eq!(ancestors[0].id, view_id);
@@ -6,6 +6,7 @@ use flowy_folder::view_operation::GatherEncodedCollab;
use flowy_folder_pub::entities::{
PublishDocumentPayload, PublishPayload, PublishViewInfo, PublishViewMeta, PublishViewMetaData,
};
use uuid::Uuid;
async fn mock_single_document_view_publish_payload(
test: &EventIntegrationTest,
@@ -140,11 +141,11 @@ async fn create_nested_document(test: &EventIntegrationTest, view_id: &str, name
#[tokio::test]
async fn single_document_get_publish_view_payload_test() {
let test = EventIntegrationTest::new_anon().await;
let view_id = "20240521";
let view_id = Uuid::new_v4().to_string();
let name = "Orphan View";
create_single_document(&test, view_id, name).await;
let view = test.get_view(view_id).await;
let payload = test.get_publish_payload(view_id, true).await;
create_single_document(&test, &view_id, name).await;
let view = test.get_view(&view_id).await;
let payload = test.get_publish_payload(&view_id, true).await;
let expect_payload = mock_single_document_view_publish_payload(
&test,
@@ -160,10 +161,10 @@ async fn single_document_get_publish_view_payload_test() {
async fn nested_document_get_publish_view_payload_test() {
let test = EventIntegrationTest::new_anon().await;
let name = "Orphan View";
let view_id = "20240521";
create_nested_document(&test, view_id, name).await;
let view = test.get_view(view_id).await;
let payload = test.get_publish_payload(view_id, true).await;
let view_id = Uuid::new_v4().to_string();
create_nested_document(&test, &view_id, name).await;
let view = test.get_view(&view_id).await;
let payload = test.get_publish_payload(&view_id, true).await;
let expect_payload = mock_nested_document_view_publish_payload(
&test,
@@ -180,10 +181,10 @@ async fn nested_document_get_publish_view_payload_test() {
async fn no_children_publish_view_payload_test() {
let test = EventIntegrationTest::new_anon().await;
let name = "Orphan View";
let view_id = "20240521";
create_nested_document(&test, view_id, name).await;
let view = test.get_view(view_id).await;
let payload = test.get_publish_payload(view_id, false).await;
let view_id = Uuid::new_v4().to_string();
create_nested_document(&test, &view_id, name).await;
let view = test.get_view(&view_id).await;
let payload = test.get_publish_payload(&view_id, false).await;
let data = mock_single_document_view_publish_payload(
&test,
@@ -1,44 +1,9 @@
use event_integration_test::EventIntegrationTest;
use flowy_core::DEFAULT_NAME;
use flowy_folder::entities::ViewLayoutPB;
use std::time::Duration;
use crate::util::unzip;
#[tokio::test]
async fn migrate_020_historical_empty_document_test() {
let user_db_path = unzip(
"./tests/user/migration_test/history_user_db",
"020_historical_user_data",
)
.unwrap();
let test =
EventIntegrationTest::new_with_user_data_path(user_db_path, DEFAULT_NAME.to_string()).await;
let mut views = test.get_all_workspace_views().await;
assert_eq!(views.len(), 1);
// Check the parent view
let parent_view = views.pop().unwrap();
assert_eq!(parent_view.layout, ViewLayoutPB::Document);
let data = test.open_document(parent_view.id.clone()).await.data;
assert!(!data.page_id.is_empty());
assert_eq!(data.blocks.len(), 2);
assert!(!data.meta.children_map.is_empty());
// Check the child views of the parent view
let child_views = test.get_view(&parent_view.id).await.child_views;
assert_eq!(child_views.len(), 4);
assert_eq!(child_views[0].layout, ViewLayoutPB::Document);
assert_eq!(child_views[1].layout, ViewLayoutPB::Grid);
assert_eq!(child_views[2].layout, ViewLayoutPB::Calendar);
assert_eq!(child_views[3].layout, ViewLayoutPB::Board);
let database = test.get_database(&child_views[1].id).await;
assert_eq!(database.fields.len(), 8);
assert_eq!(database.rows.len(), 3);
}
#[tokio::test]
async fn migrate_036_fav_v1_workspace_array_test() {
// Used to test migration: FavoriteV1AndWorkspaceArrayMigration
+2 -2
View File
@@ -9,7 +9,7 @@ edition = "2021"
lib-infra = { workspace = true }
flowy-error = { workspace = true }
client-api = { workspace = true }
bytes.workspace = true
futures.workspace = true
serde_json.workspace = true
serde.workspace = true
serde.workspace = true
uuid.workspace = true
+29 -28
View File
@@ -19,6 +19,7 @@ use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::path::Path;
use uuid::Uuid;
pub type ChatMessageStream = BoxStream<'static, Result<ChatMessage, AppResponseError>>;
pub type StreamAnswer = BoxStream<'static, Result<QuestionStreamValue, FlowyError>>;
@@ -81,15 +82,15 @@ pub trait ChatCloudService: Send + Sync + 'static {
async fn create_chat(
&self,
uid: &i64,
workspace_id: &str,
chat_id: &str,
rag_ids: Vec<String>,
workspace_id: &Uuid,
chat_id: &Uuid,
rag_ids: Vec<Uuid>,
) -> Result<(), FlowyError>;
async fn create_question(
&self,
workspace_id: &str,
chat_id: &str,
workspace_id: &Uuid,
chat_id: &Uuid,
message: &str,
message_type: ChatMessageType,
metadata: &[ChatMessageMetadata],
@@ -97,8 +98,8 @@ pub trait ChatCloudService: Send + Sync + 'static {
async fn create_answer(
&self,
workspace_id: &str,
chat_id: &str,
workspace_id: &Uuid,
chat_id: &Uuid,
message: &str,
question_id: i64,
metadata: Option<serde_json::Value>,
@@ -106,8 +107,8 @@ pub trait ChatCloudService: Send + Sync + 'static {
async fn stream_answer(
&self,
workspace_id: &str,
chat_id: &str,
workspace_id: &Uuid,
chat_id: &Uuid,
message_id: i64,
format: ResponseFormat,
ai_model: Option<AIModel>,
@@ -115,68 +116,68 @@ pub trait ChatCloudService: Send + Sync + 'static {
async fn get_answer(
&self,
workspace_id: &str,
chat_id: &str,
workspace_id: &Uuid,
chat_id: &Uuid,
question_message_id: i64,
) -> Result<ChatMessage, FlowyError>;
async fn get_chat_messages(
&self,
workspace_id: &str,
chat_id: &str,
workspace_id: &Uuid,
chat_id: &Uuid,
offset: MessageCursor,
limit: u64,
) -> Result<RepeatedChatMessage, FlowyError>;
async fn get_question_from_answer_id(
&self,
workspace_id: &str,
chat_id: &str,
workspace_id: &Uuid,
chat_id: &Uuid,
answer_message_id: i64,
) -> Result<ChatMessage, FlowyError>;
async fn get_related_message(
&self,
workspace_id: &str,
chat_id: &str,
workspace_id: &Uuid,
chat_id: &Uuid,
message_id: i64,
) -> Result<RepeatedRelatedQuestion, FlowyError>;
async fn stream_complete(
&self,
workspace_id: &str,
workspace_id: &Uuid,
params: CompleteTextParams,
ai_model: Option<AIModel>,
) -> Result<StreamComplete, FlowyError>;
async fn embed_file(
&self,
workspace_id: &str,
workspace_id: &Uuid,
file_path: &Path,
chat_id: &str,
chat_id: &Uuid,
metadata: Option<HashMap<String, Value>>,
) -> Result<(), FlowyError>;
async fn get_local_ai_config(&self, workspace_id: &str) -> Result<LocalAIConfig, FlowyError>;
async fn get_local_ai_config(&self, workspace_id: &Uuid) -> Result<LocalAIConfig, FlowyError>;
async fn get_workspace_plan(
&self,
workspace_id: &str,
workspace_id: &Uuid,
) -> Result<Vec<SubscriptionPlan>, FlowyError>;
async fn get_chat_settings(
&self,
workspace_id: &str,
chat_id: &str,
workspace_id: &Uuid,
chat_id: &Uuid,
) -> Result<ChatSettings, FlowyError>;
async fn update_chat_settings(
&self,
workspace_id: &str,
chat_id: &str,
workspace_id: &Uuid,
chat_id: &Uuid,
params: UpdateChatParams,
) -> Result<(), FlowyError>;
async fn get_available_models(&self, workspace_id: &str) -> Result<ModelList, FlowyError>;
async fn get_workspace_default_model(&self, workspace_id: &str) -> Result<String, FlowyError>;
async fn get_available_models(&self, workspace_id: &Uuid) -> Result<ModelList, FlowyError>;
async fn get_workspace_default_model(&self, workspace_id: &Uuid) -> Result<String, FlowyError>;
}
+50 -39
View File
@@ -27,14 +27,16 @@ use flowy_storage_pub::storage::StorageService;
use lib_infra::async_trait::async_trait;
use lib_infra::util::timestamp;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::{Arc, Weak};
use tokio::sync::RwLock;
use tracing::{error, info, instrument, trace};
use uuid::Uuid;
pub trait AIUserService: Send + Sync + 'static {
fn user_id(&self) -> Result<i64, FlowyError>;
fn device_id(&self) -> Result<String, FlowyError>;
fn workspace_id(&self) -> Result<String, FlowyError>;
fn workspace_id(&self) -> Result<Uuid, FlowyError>;
fn sqlite_connection(&self, uid: i64) -> Result<DBConnection, FlowyError>;
fn application_root_dir(&self) -> Result<PathBuf, FlowyError>;
}
@@ -44,18 +46,18 @@ pub trait AIUserService: Send + Sync + 'static {
pub trait AIExternalService: Send + Sync + 'static {
async fn query_chat_rag_ids(
&self,
parent_view_id: &str,
chat_id: &str,
) -> Result<Vec<String>, FlowyError>;
parent_view_id: &Uuid,
chat_id: &Uuid,
) -> Result<Vec<Uuid>, FlowyError>;
async fn sync_rag_documents(
&self,
workspace_id: &str,
rag_ids: Vec<String>,
rag_metadata_map: HashMap<String, AFCollabMetadata>,
workspace_id: &Uuid,
rag_ids: Vec<Uuid>,
rag_metadata_map: HashMap<Uuid, AFCollabMetadata>,
) -> Result<Vec<AFCollabMetadata>, FlowyError>;
async fn notify_did_send_message(&self, chat_id: &str, message: &str) -> Result<(), FlowyError>;
async fn notify_did_send_message(&self, chat_id: &Uuid, message: &str) -> Result<(), FlowyError>;
}
#[derive(Debug, Default)]
@@ -70,7 +72,7 @@ pub struct AIManager {
pub cloud_service_wm: Arc<AICloudServiceMiddleware>,
pub user_service: Arc<dyn AIUserService>,
pub external_service: Arc<dyn AIExternalService>,
chats: Arc<DashMap<String, Arc<Chat>>>,
chats: Arc<DashMap<Uuid, Arc<Chat>>>,
pub local_ai: Arc<LocalAIController>,
pub store_preferences: Arc<KVStorePreferences>,
server_models: Arc<RwLock<ServerModelsCache>>,
@@ -132,11 +134,11 @@ impl AIManager {
Ok(())
}
pub async fn open_chat(&self, chat_id: &str) -> Result<(), FlowyError> {
self.chats.entry(chat_id.to_string()).or_insert_with(|| {
pub async fn open_chat(&self, chat_id: &Uuid) -> Result<(), FlowyError> {
self.chats.entry(*chat_id).or_insert_with(|| {
Arc::new(Chat::new(
self.user_service.user_id().unwrap(),
chat_id.to_string(),
*chat_id,
self.user_service.clone(),
self.cloud_service_wm.clone(),
))
@@ -150,7 +152,7 @@ impl AIManager {
let cloud_service_wm = self.cloud_service_wm.clone();
let store_preferences = self.store_preferences.clone();
let external_service = self.external_service.clone();
let chat_id = chat_id.to_string();
let chat_id = *chat_id;
tokio::spawn(async move {
match refresh_chat_setting(
&user_service,
@@ -161,7 +163,12 @@ impl AIManager {
.await
{
Ok(settings) => {
let _ = sync_chat_documents(user_service, external_service, settings.rag_ids).await;
let rag_ids = settings
.rag_ids
.into_iter()
.flat_map(|r| Uuid::from_str(&r).ok())
.collect();
let _ = sync_chat_documents(user_service, external_service, rag_ids).await;
},
Err(err) => {
error!("failed to refresh chat settings: {}", err);
@@ -172,13 +179,13 @@ impl AIManager {
Ok(())
}
pub async fn close_chat(&self, chat_id: &str) -> Result<(), FlowyError> {
pub async fn close_chat(&self, chat_id: &Uuid) -> Result<(), FlowyError> {
trace!("close chat: {}", chat_id);
self.local_ai.close_chat(chat_id);
Ok(())
}
pub async fn delete_chat(&self, chat_id: &str) -> Result<(), FlowyError> {
pub async fn delete_chat(&self, chat_id: &Uuid) -> Result<(), FlowyError> {
if let Some((_, chat)) = self.chats.remove(chat_id) {
chat.close();
@@ -212,8 +219,8 @@ impl AIManager {
pub async fn create_chat(
&self,
uid: &i64,
parent_view_id: &str,
chat_id: &str,
parent_view_id: &Uuid,
chat_id: &Uuid,
) -> Result<Arc<Chat>, FlowyError> {
let workspace_id = self.user_service.workspace_id()?;
let rag_ids = self
@@ -231,11 +238,11 @@ impl AIManager {
let chat = Arc::new(Chat::new(
self.user_service.user_id()?,
chat_id.to_string(),
*chat_id,
self.user_service.clone(),
self.cloud_service_wm.clone(),
));
self.chats.insert(chat_id.to_string(), chat.clone());
self.chats.insert(*chat_id, chat.clone());
Ok(chat)
}
@@ -244,7 +251,7 @@ impl AIManager {
params: StreamMessageParams,
) -> Result<ChatMessagePB, FlowyError> {
let chat = self.get_or_create_chat_instance(&params.chat_id).await?;
let ai_model = self.get_active_model(&params.chat_id).await;
let ai_model = self.get_active_model(&params.chat_id.to_string()).await;
let question = chat.stream_chat_message(&params, ai_model).await?;
let _ = self
.external_service
@@ -255,7 +262,7 @@ impl AIManager {
pub async fn stream_regenerate_response(
&self,
chat_id: &str,
chat_id: &Uuid,
answer_message_id: i64,
answer_stream_port: i64,
format: Option<PredefinedFormatPB>,
@@ -270,7 +277,7 @@ impl AIManager {
|| {
self
.store_preferences
.get_object::<AIModel>(&ai_available_models_key(chat_id))
.get_object::<AIModel>(&ai_available_models_key(&chat_id.to_string()))
},
|model| Some(model.into()),
);
@@ -520,17 +527,17 @@ impl AIManager {
})
}
pub async fn get_or_create_chat_instance(&self, chat_id: &str) -> Result<Arc<Chat>, FlowyError> {
pub async fn get_or_create_chat_instance(&self, chat_id: &Uuid) -> Result<Arc<Chat>, FlowyError> {
let chat = self.chats.get(chat_id).as_deref().cloned();
match chat {
None => {
let chat = Arc::new(Chat::new(
self.user_service.user_id()?,
chat_id.to_string(),
*chat_id,
self.user_service.clone(),
self.cloud_service_wm.clone(),
));
self.chats.insert(chat_id.to_string(), chat.clone());
self.chats.insert(*chat_id, chat.clone());
Ok(chat)
},
Some(chat) => Ok(chat),
@@ -554,7 +561,7 @@ impl AIManager {
pub async fn load_prev_chat_messages(
&self,
chat_id: &str,
chat_id: &Uuid,
limit: i64,
before_message_id: Option<i64>,
) -> Result<ChatMessageListPB, FlowyError> {
@@ -567,7 +574,7 @@ impl AIManager {
pub async fn load_latest_chat_messages(
&self,
chat_id: &str,
chat_id: &Uuid,
limit: i64,
after_message_id: Option<i64>,
) -> Result<ChatMessageListPB, FlowyError> {
@@ -580,7 +587,7 @@ impl AIManager {
pub async fn get_related_questions(
&self,
chat_id: &str,
chat_id: &Uuid,
message_id: i64,
) -> Result<RepeatedRelatedQuestionPB, FlowyError> {
let chat = self.get_or_create_chat_instance(chat_id).await?;
@@ -590,7 +597,7 @@ impl AIManager {
pub async fn generate_answer(
&self,
chat_id: &str,
chat_id: &Uuid,
question_message_id: i64,
) -> Result<ChatMessagePB, FlowyError> {
let chat = self.get_or_create_chat_instance(chat_id).await?;
@@ -598,19 +605,19 @@ impl AIManager {
Ok(resp)
}
pub async fn stop_stream(&self, chat_id: &str) -> Result<(), FlowyError> {
pub async fn stop_stream(&self, chat_id: &Uuid) -> Result<(), FlowyError> {
let chat = self.get_or_create_chat_instance(chat_id).await?;
chat.stop_stream_message().await;
Ok(())
}
pub async fn chat_with_file(&self, chat_id: &str, file_path: PathBuf) -> FlowyResult<()> {
pub async fn chat_with_file(&self, chat_id: &Uuid, file_path: PathBuf) -> FlowyResult<()> {
let chat = self.get_or_create_chat_instance(chat_id).await?;
chat.index_file(file_path).await?;
Ok(())
}
pub async fn get_rag_ids(&self, chat_id: &str) -> FlowyResult<Vec<String>> {
pub async fn get_rag_ids(&self, chat_id: &Uuid) -> FlowyResult<Vec<String>> {
if let Some(settings) = self
.store_preferences
.get_object::<ChatSettings>(&setting_store_key(chat_id))
@@ -628,7 +635,7 @@ impl AIManager {
Ok(settings.rag_ids)
}
pub async fn update_rag_ids(&self, chat_id: &str, rag_ids: Vec<String>) -> FlowyResult<()> {
pub async fn update_rag_ids(&self, chat_id: &Uuid, rag_ids: Vec<String>) -> FlowyResult<()> {
info!("[Chat] update chat:{} rag ids: {:?}", chat_id, rag_ids);
let workspace_id = self.user_service.workspace_id()?;
let update_setting = UpdateChatParams {
@@ -659,6 +666,10 @@ impl AIManager {
let user_service = self.user_service.clone();
let external_service = self.external_service.clone();
let rag_ids = rag_ids
.into_iter()
.flat_map(|r| Uuid::from_str(&r).ok())
.collect();
sync_chat_documents(user_service, external_service, rag_ids).await?;
Ok(())
}
@@ -667,7 +678,7 @@ impl AIManager {
async fn sync_chat_documents(
user_service: Arc<dyn AIUserService>,
external_service: Arc<dyn AIExternalService>,
rag_ids: Vec<String>,
rag_ids: Vec<Uuid>,
) -> FlowyResult<()> {
if rag_ids.is_empty() {
return Ok(());
@@ -697,7 +708,7 @@ async fn sync_chat_documents(
Ok(())
}
fn save_chat(conn: DBConnection, chat_id: &str) -> FlowyResult<()> {
fn save_chat(conn: DBConnection, chat_id: &Uuid) -> FlowyResult<()> {
let row = ChatTable {
chat_id: chat_id.to_string(),
created_at: timestamp(),
@@ -716,7 +727,7 @@ async fn refresh_chat_setting(
user_service: &Arc<dyn AIUserService>,
cloud_service: &Arc<AICloudServiceMiddleware>,
store_preferences: &Arc<KVStorePreferences>,
chat_id: &str,
chat_id: &Uuid,
) -> FlowyResult<ChatSettings> {
info!("[Chat] refresh chat:{} setting", chat_id);
let workspace_id = user_service.workspace_id()?;
@@ -728,7 +739,7 @@ async fn refresh_chat_setting(
error!("failed to set chat settings: {}", err);
}
chat_notification_builder(chat_id, ChatNotification::DidUpdateChatSettings)
chat_notification_builder(chat_id.to_string(), ChatNotification::DidUpdateChatSettings)
.payload(ChatSettingsPB {
rag_ids: settings.rag_ids.clone(),
})
@@ -737,6 +748,6 @@ async fn refresh_chat_setting(
Ok(settings)
}
fn setting_store_key(chat_id: &str) -> String {
fn setting_store_key(chat_id: &Uuid) -> String {
format!("chat_settings_{}", chat_id)
}
+18 -17
View File
@@ -23,6 +23,7 @@ use std::sync::atomic::{AtomicBool, AtomicI64};
use std::sync::Arc;
use tokio::sync::{Mutex, RwLock};
use tracing::{error, instrument, trace};
use uuid::Uuid;
enum PrevMessageState {
HasMore,
@@ -31,7 +32,7 @@ enum PrevMessageState {
}
pub struct Chat {
chat_id: String,
chat_id: Uuid,
uid: i64,
user_service: Arc<dyn AIUserService>,
chat_service: Arc<AICloudServiceMiddleware>,
@@ -44,7 +45,7 @@ pub struct Chat {
impl Chat {
pub fn new(
uid: i64,
chat_id: String,
chat_id: Uuid,
user_service: Arc<dyn AIUserService>,
chat_service: Arc<AICloudServiceMiddleware>,
) -> Chat {
@@ -197,13 +198,13 @@ impl Chat {
answer_stream_port: i64,
answer_stream_buffer: Arc<Mutex<StringBuffer>>,
uid: i64,
workspace_id: String,
workspace_id: Uuid,
question_id: i64,
format: ResponseFormat,
ai_model: Option<AIModel>,
) {
let stop_stream = self.stop_stream.clone();
let chat_id = self.chat_id.clone();
let chat_id = self.chat_id;
let cloud_service = self.chat_service.clone();
let user_service = self.user_service.clone();
tokio::spawn(async move {
@@ -254,10 +255,10 @@ impl Chat {
.send(StreamMessage::OnError(err.msg.clone()).to_string())
.await;
let pb = ChatMessageErrorPB {
chat_id: chat_id.clone(),
chat_id: chat_id.to_string(),
error_message: err.to_string(),
};
chat_notification_builder(&chat_id, ChatNotification::StreamChatMessageError)
chat_notification_builder(chat_id, ChatNotification::StreamChatMessageError)
.payload(pb)
.send();
return Err(err);
@@ -293,17 +294,17 @@ impl Chat {
}
let pb = ChatMessageErrorPB {
chat_id: chat_id.clone(),
chat_id: chat_id.to_string(),
error_message: err.to_string(),
};
chat_notification_builder(&chat_id, ChatNotification::StreamChatMessageError)
chat_notification_builder(chat_id, ChatNotification::StreamChatMessageError)
.payload(pb)
.send();
return Err(err);
},
}
chat_notification_builder(&chat_id, ChatNotification::FinishStreaming).send();
chat_notification_builder(chat_id, ChatNotification::FinishStreaming).send();
trace!("[Chat] finish streaming");
if answer_stream_buffer.lock().await.is_empty() {
@@ -359,7 +360,7 @@ impl Chat {
has_more: true,
total: 0,
};
chat_notification_builder(&self.chat_id, ChatNotification::DidLoadPrevChatMessage)
chat_notification_builder(self.chat_id, ChatNotification::DidLoadPrevChatMessage)
.payload(pb.clone())
.send();
return Ok(pb);
@@ -432,7 +433,7 @@ impl Chat {
after_message_id
);
let workspace_id = self.user_service.workspace_id()?;
let chat_id = self.chat_id.clone();
let chat_id = self.chat_id;
let cloud_service = self.chat_service.clone();
let user_service = self.user_service.clone();
let uid = self.uid;
@@ -480,11 +481,11 @@ impl Chat {
} else {
*prev_message_state.write().await = PrevMessageState::NoMore;
}
chat_notification_builder(&chat_id, ChatNotification::DidLoadPrevChatMessage)
chat_notification_builder(chat_id, ChatNotification::DidLoadPrevChatMessage)
.payload(pb)
.send();
} else {
chat_notification_builder(&chat_id, ChatNotification::DidLoadLatestChatMessage)
chat_notification_builder(chat_id, ChatNotification::DidLoadLatestChatMessage)
.payload(pb)
.send();
}
@@ -510,7 +511,7 @@ impl Chat {
}
let workspace_id = self.user_service.workspace_id()?;
let chat_id = self.chat_id.clone();
let chat_id = self.chat_id;
let cloud_service = self.chat_service.clone();
let question = cloud_service
@@ -566,7 +567,7 @@ impl Chat {
let conn = self.user_service.sqlite_connection(self.uid)?;
let records = select_chat_messages(
conn,
&self.chat_id,
&self.chat_id.to_string(),
limit,
after_message_id,
before_message_id,
@@ -628,7 +629,7 @@ impl Chat {
fn save_chat_message_disk(
conn: DBConnection,
chat_id: &str,
chat_id: &Uuid,
messages: Vec<ChatMessage>,
) -> FlowyResult<()> {
let records = messages
@@ -683,7 +684,7 @@ impl StringBuffer {
pub(crate) fn save_and_notify_message(
uid: i64,
chat_id: &str,
chat_id: &Uuid,
user_service: &Arc<dyn AIUserService>,
message: ChatMessage,
) -> Result<(), FlowyError> {
+59 -53
View File
@@ -1,6 +1,7 @@
use crate::ai_manager::AIUserService;
use crate::entities::{CompleteTextPB, CompleteTextTaskPB, CompletionTypePB};
use allo_isolate::Isolate;
use std::str::FromStr;
use dashmap::DashMap;
use flowy_ai_pub::cloud::{
@@ -15,7 +16,8 @@ use lib_infra::isolate_stream::IsolateSink;
use crate::stream_message::StreamMessage;
use std::sync::{Arc, Weak};
use tokio::select;
use tracing::info;
use tracing::{error, info};
use uuid::Uuid;
pub struct AICompletion {
tasks: Arc<DashMap<String, tokio::sync::mpsc::Sender<()>>>,
@@ -77,7 +79,7 @@ impl AICompletion {
}
pub struct CompletionTask {
workspace_id: String,
workspace_id: Uuid,
task_id: String,
stop_rx: tokio::sync::mpsc::Receiver<()>,
context: CompleteTextPB,
@@ -87,7 +89,7 @@ pub struct CompletionTask {
impl CompletionTask {
pub fn new(
workspace_id: String,
workspace_id: Uuid,
context: CompleteTextPB,
preferred_model: Option<AIModel>,
cloud_service: Weak<dyn ChatCloudService>,
@@ -122,59 +124,63 @@ impl CompletionTask {
let _ = sink.send("start:".to_string()).await;
let completion_history = Some(self.context.history.iter().map(Into::into).collect());
let format = self.context.format.map(Into::into).unwrap_or_default();
let params = CompleteTextParams {
text: self.context.text,
completion_type: Some(complete_type),
metadata: Some(CompletionMetadata {
object_id: self.context.object_id,
workspace_id: Some(self.workspace_id.clone()),
rag_ids: Some(self.context.rag_ids),
completion_history,
custom_prompt: self
.context
.custom_prompt
.map(|v| CustomPrompt { system: v }),
}),
format,
};
if let Ok(object_id) = Uuid::from_str(&self.context.object_id) {
let params = CompleteTextParams {
text: self.context.text,
completion_type: Some(complete_type),
metadata: Some(CompletionMetadata {
object_id,
workspace_id: Some(self.workspace_id),
rag_ids: Some(self.context.rag_ids),
completion_history,
custom_prompt: self
.context
.custom_prompt
.map(|v| CustomPrompt { system: v }),
}),
format,
};
info!("start completion: {:?}", params);
match cloud_service
.stream_complete(&self.workspace_id, params, self.preferred_model)
.await
{
Ok(mut stream) => loop {
select! {
_ = self.stop_rx.recv() => {
return;
},
result = stream.next() => {
match result {
Some(Ok(data)) => {
match data {
CompletionStreamValue::Answer{ value } => {
let _ = sink.send(format!("data:{}", value)).await;
info!("start completion: {:?}", params);
match cloud_service
.stream_complete(&self.workspace_id, params, self.preferred_model)
.await
{
Ok(mut stream) => loop {
select! {
_ = self.stop_rx.recv() => {
return;
},
result = stream.next() => {
match result {
Some(Ok(data)) => {
match data {
CompletionStreamValue::Answer{ value } => {
let _ = sink.send(format!("data:{}", value)).await;
}
CompletionStreamValue::Comment{ value } => {
let _ = sink.send(format!("comment:{}", value)).await;
}
}
CompletionStreamValue::Comment{ value } => {
let _ = sink.send(format!("comment:{}", value)).await;
}
}
},
Some(Err(error)) => {
handle_error(&mut sink, error).await;
return;
},
None => {
let _ = sink.send(format!("finish:{}", self.task_id)).await;
return;
},
},
Some(Err(error)) => {
handle_error(&mut sink, error).await;
return;
},
None => {
let _ = sink.send(format!("finish:{}", self.task_id)).await;
return;
},
}
}
}
}
},
Err(error) => {
handle_error(&mut sink, error).await;
},
}
},
Err(error) => {
handle_error(&mut sink, error).await;
},
}
} else {
error!("Invalid uuid: {}", self.context.object_id);
}
}
});
+4 -4
View File
@@ -1,8 +1,6 @@
use af_plugin::core::plugin::RunningState;
use std::collections::HashMap;
use crate::local_ai::controller::LocalAISetting;
use crate::local_ai::resource::PendingResource;
use af_plugin::core::plugin::RunningState;
use flowy_ai_pub::cloud::{
AIModel, ChatMessage, ChatMessageMetadata, ChatMessageType, CompletionMessage, LLMModel,
OutputContent, OutputLayout, RelatedQuestion, RepeatedChatMessage, RepeatedRelatedQuestion,
@@ -10,6 +8,8 @@ use flowy_ai_pub::cloud::{
};
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use lib_infra::validator_fn::required_not_empty_str;
use std::collections::HashMap;
use uuid::Uuid;
use validator::Validate;
#[derive(Default, ProtoBuf, Validate, Clone, Debug)]
@@ -78,7 +78,7 @@ pub struct StreamChatPayloadPB {
#[derive(Default, Debug)]
pub struct StreamMessageParams {
pub chat_id: String,
pub chat_id: Uuid,
pub message: String,
pub message_type: ChatMessageType,
pub answer_stream_port: i64,
+22 -13
View File
@@ -1,6 +1,3 @@
use std::fs;
use std::path::PathBuf;
use crate::ai_manager::{AIManager, GLOBAL_ACTIVE_MODEL_KEY};
use crate::completion::AICompletion;
use crate::entities::*;
@@ -10,8 +7,12 @@ use flowy_ai_pub::cloud::{
};
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult};
use std::fs;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::{Arc, Weak};
use tracing::trace;
use uuid::Uuid;
use validator::Validate;
fn upgrade_ai_manager(ai_manager: AFPluginState<Weak<AIManager>>) -> FlowyResult<Arc<AIManager>> {
@@ -70,6 +71,7 @@ pub(crate) async fn stream_chat_message_handler(
trace!("Stream chat message with metadata: {:?}", metadata);
let chat_id = Uuid::from_str(&chat_id)?;
let params = StreamMessageParams {
chat_id,
message,
@@ -91,11 +93,12 @@ pub(crate) async fn regenerate_response_handler(
ai_manager: AFPluginState<Weak<AIManager>>,
) -> FlowyResult<()> {
let data = data.try_into_inner()?;
let chat_id = Uuid::from_str(&data.chat_id)?;
let ai_manager = upgrade_ai_manager(ai_manager)?;
ai_manager
.stream_regenerate_response(
&data.chat_id,
&chat_id,
data.answer_message_id,
data.answer_stream_port,
data.format,
@@ -147,8 +150,9 @@ pub(crate) async fn load_prev_message_handler(
let data = data.into_inner();
data.validate()?;
let chat_id = Uuid::from_str(&data.chat_id)?;
let messages = ai_manager
.load_prev_chat_messages(&data.chat_id, data.limit, data.before_message_id)
.load_prev_chat_messages(&chat_id, data.limit, data.before_message_id)
.await?;
data_result_ok(messages)
}
@@ -162,8 +166,9 @@ pub(crate) async fn load_next_message_handler(
let data = data.into_inner();
data.validate()?;
let chat_id = Uuid::from_str(&data.chat_id)?;
let messages = ai_manager
.load_latest_chat_messages(&data.chat_id, data.limit, data.after_message_id)
.load_latest_chat_messages(&chat_id, data.limit, data.after_message_id)
.await?;
data_result_ok(messages)
}
@@ -175,8 +180,9 @@ pub(crate) async fn get_related_question_handler(
) -> DataResult<RepeatedRelatedQuestionPB, FlowyError> {
let ai_manager = upgrade_ai_manager(ai_manager)?;
let data = data.into_inner();
let chat_id = Uuid::from_str(&data.chat_id)?;
let messages = ai_manager
.get_related_questions(&data.chat_id, data.message_id)
.get_related_questions(&chat_id, data.message_id)
.await?;
data_result_ok(messages)
}
@@ -188,8 +194,9 @@ pub(crate) async fn get_answer_handler(
) -> DataResult<ChatMessagePB, FlowyError> {
let ai_manager = upgrade_ai_manager(ai_manager)?;
let data = data.into_inner();
let chat_id = Uuid::from_str(&data.chat_id)?;
let message = ai_manager
.generate_answer(&data.chat_id, data.message_id)
.generate_answer(&chat_id, data.message_id)
.await?;
data_result_ok(message)
}
@@ -203,7 +210,8 @@ pub(crate) async fn stop_stream_handler(
data.validate()?;
let ai_manager = upgrade_ai_manager(ai_manager)?;
ai_manager.stop_stream(&data.chat_id).await?;
let chat_id = Uuid::from_str(&data.chat_id)?;
ai_manager.stop_stream(&chat_id).await?;
Ok(())
}
@@ -273,7 +281,8 @@ pub(crate) async fn chat_file_handler(
tracing::debug!("File size: {} bytes", file_size);
let ai_manager = upgrade_ai_manager(ai_manager)?;
ai_manager.chat_with_file(&data.chat_id, file_path).await?;
let chat_id = Uuid::from_str(&data.chat_id)?;
ai_manager.chat_with_file(&chat_id, file_path).await?;
Ok(())
}
@@ -332,6 +341,7 @@ pub(crate) async fn get_chat_settings_handler(
ai_manager: AFPluginState<Weak<AIManager>>,
) -> DataResult<ChatSettingsPB, FlowyError> {
let chat_id = data.try_into_inner()?.value;
let chat_id = Uuid::from_str(&chat_id)?;
let ai_manager = upgrade_ai_manager(ai_manager)?;
let rag_ids = ai_manager.get_rag_ids(&chat_id).await?;
let pb = ChatSettingsPB { rag_ids };
@@ -345,9 +355,8 @@ pub(crate) async fn update_chat_settings_handler(
) -> FlowyResult<()> {
let params = data.try_into_inner()?;
let ai_manager = upgrade_ai_manager(ai_manager)?;
ai_manager
.update_rag_ids(&params.chat_id.value, params.rag_ids)
.await?;
let chat_id = Uuid::from_str(&params.chat_id.value)?;
ai_manager.update_rag_ids(&chat_id, params.rag_ids).await?;
Ok(())
}
@@ -28,6 +28,7 @@ use std::sync::Arc;
use tokio::select;
use tokio_stream::StreamExt;
use tracing::{debug, error, info, instrument};
use uuid::Uuid;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LocalAISetting {
@@ -51,7 +52,7 @@ const LOCAL_AI_SETTING_KEY: &str = "appflowy_local_ai_setting:v1";
pub struct LocalAIController {
ai_plugin: Arc<OllamaAIPlugin>,
resource: Arc<LocalAIResourceController>,
current_chat_id: ArcSwapOption<String>,
current_chat_id: ArcSwapOption<Uuid>,
store_preferences: Arc<KVStorePreferences>,
user_service: Arc<dyn AIUserService>,
#[allow(dead_code)]
@@ -240,7 +241,7 @@ impl LocalAIController {
Some(self.resource.get_llm_setting().chat_model_name)
}
pub fn open_chat(&self, chat_id: &str) {
pub fn open_chat(&self, chat_id: &Uuid) {
if !self.is_enabled() {
return;
}
@@ -252,9 +253,7 @@ impl LocalAIController {
self.close_chat(current_chat_id);
}
self
.current_chat_id
.store(Some(Arc::new(chat_id.to_string())));
self.current_chat_id.store(Some(Arc::new(*chat_id)));
let chat_id = chat_id.to_string();
let weak_ctrl = Arc::downgrade(&self.ai_plugin);
tokio::spawn(async move {
@@ -266,7 +265,7 @@ impl LocalAIController {
});
}
pub fn close_chat(&self, chat_id: &str) {
pub fn close_chat(&self, chat_id: &Uuid) {
if !self.is_running() {
return;
}
@@ -383,7 +382,7 @@ impl LocalAIController {
#[instrument(level = "debug", skip_all)]
pub async fn index_message_metadata(
&self,
chat_id: &str,
chat_id: &Uuid,
metadata_list: &[ChatMessageMetadata],
index_process_sink: &mut (impl Sink<String> + Unpin),
) -> FlowyResult<()> {
@@ -434,7 +433,7 @@ impl LocalAIController {
async fn process_index_file(
&self,
chat_id: &str,
chat_id: &Uuid,
file_path: PathBuf,
index_metadata: &HashMap<String, serde_json::Value>,
index_process_sink: &mut (impl Sink<String> + Unpin),
@@ -456,7 +455,11 @@ impl LocalAIController {
let result = self
.ai_plugin
.embed_file(chat_id, file_path, Some(index_metadata.clone()))
.embed_file(
&chat_id.to_string(),
file_path,
Some(index_metadata.clone()),
)
.await;
match result {
Ok(_) => {
@@ -616,6 +619,6 @@ impl LLMResourceService for LLMResourceServiceImpl {
}
const APPFLOWY_LOCAL_AI_ENABLED: &str = "appflowy_local_ai_enabled";
fn local_ai_enabled_key(workspace_id: &str) -> String {
fn local_ai_enabled_key(workspace_id: &Uuid) -> String {
format!("{}:{}", APPFLOWY_LOCAL_AI_ENABLED, workspace_id)
}
@@ -195,9 +195,14 @@ impl LocalAIResourceController {
let tags: TagsResponse = resp.json().await.inspect_err(|e| {
log::error!("[LLM Resource] Failed to parse /api/tags JSON response: {e:?}")
})?;
// Check each required model is present in the response.
// Check if each of our required models exists in the list of available models
trace!("[LLM Resource] ollama available models: {:?}", tags.models);
for required in &required_models {
if !tags.models.iter().any(|m| m.name.contains(required)) {
if !tags
.models
.iter()
.any(|m| m.name == *required || m.name == format!("{}:latest", required))
{
log::trace!(
"[LLM Resource] required model '{}' not found in API response",
required
@@ -26,6 +26,7 @@ use serde_json::{json, Value};
use std::path::Path;
use std::sync::{Arc, Weak};
use tracing::{info, trace};
use uuid::Uuid;
pub struct AICloudServiceMiddleware {
cloud_service: Arc<dyn ChatCloudService>,
@@ -55,7 +56,7 @@ impl AICloudServiceMiddleware {
pub async fn index_message_metadata(
&self,
chat_id: &str,
chat_id: &Uuid,
metadata_list: &[ChatMessageMetadata],
index_process_sink: &mut (impl Sink<String> + Unpin),
) -> Result<(), FlowyError> {
@@ -114,9 +115,9 @@ impl ChatCloudService for AICloudServiceMiddleware {
async fn create_chat(
&self,
uid: &i64,
workspace_id: &str,
chat_id: &str,
rag_ids: Vec<String>,
workspace_id: &Uuid,
chat_id: &Uuid,
rag_ids: Vec<Uuid>,
) -> Result<(), FlowyError> {
self
.cloud_service
@@ -126,8 +127,8 @@ impl ChatCloudService for AICloudServiceMiddleware {
async fn create_question(
&self,
workspace_id: &str,
chat_id: &str,
workspace_id: &Uuid,
chat_id: &Uuid,
message: &str,
message_type: ChatMessageType,
metadata: &[ChatMessageMetadata],
@@ -140,8 +141,8 @@ impl ChatCloudService for AICloudServiceMiddleware {
async fn create_answer(
&self,
workspace_id: &str,
chat_id: &str,
workspace_id: &Uuid,
chat_id: &Uuid,
message: &str,
question_id: i64,
metadata: Option<serde_json::Value>,
@@ -154,8 +155,8 @@ impl ChatCloudService for AICloudServiceMiddleware {
async fn stream_answer(
&self,
workspace_id: &str,
chat_id: &str,
workspace_id: &Uuid,
chat_id: &Uuid,
message_id: i64,
format: ResponseFormat,
ai_model: Option<AIModel>,
@@ -171,7 +172,12 @@ impl ChatCloudService for AICloudServiceMiddleware {
let row = self.get_message_record(message_id)?;
match self
.local_ai
.stream_question(chat_id, &row.content, Some(json!(format)), json!({}))
.stream_question(
&chat_id.to_string(),
&row.content,
Some(json!(format)),
json!({}),
)
.await
{
Ok(stream) => Ok(QuestionStream::new(stream).boxed()),
@@ -195,13 +201,17 @@ impl ChatCloudService for AICloudServiceMiddleware {
async fn get_answer(
&self,
workspace_id: &str,
chat_id: &str,
workspace_id: &Uuid,
chat_id: &Uuid,
question_message_id: i64,
) -> Result<ChatMessage, FlowyError> {
if self.local_ai.is_running() {
let content = self.get_message_record(question_message_id)?.content;
match self.local_ai.ask_question(chat_id, &content).await {
match self
.local_ai
.ask_question(&chat_id.to_string(), &content)
.await
{
Ok(answer) => {
let message = self
.cloud_service
@@ -224,8 +234,8 @@ impl ChatCloudService for AICloudServiceMiddleware {
async fn get_chat_messages(
&self,
workspace_id: &str,
chat_id: &str,
workspace_id: &Uuid,
chat_id: &Uuid,
offset: MessageCursor,
limit: u64,
) -> Result<RepeatedChatMessage, FlowyError> {
@@ -237,26 +247,26 @@ impl ChatCloudService for AICloudServiceMiddleware {
async fn get_question_from_answer_id(
&self,
workspace_id: &str,
chat_id: &str,
answer_id: i64,
workspace_id: &Uuid,
chat_id: &Uuid,
answer_message_id: i64,
) -> Result<ChatMessage, FlowyError> {
self
.cloud_service
.get_question_from_answer_id(workspace_id, chat_id, answer_id)
.get_question_from_answer_id(workspace_id, chat_id, answer_message_id)
.await
}
async fn get_related_message(
&self,
workspace_id: &str,
chat_id: &str,
workspace_id: &Uuid,
chat_id: &Uuid,
message_id: i64,
) -> Result<RepeatedRelatedQuestion, FlowyError> {
if self.local_ai.is_running() {
let questions = self
.local_ai
.get_related_question(chat_id)
.get_related_question(&chat_id.to_string())
.await
.map_err(|err| FlowyError::local_ai().with_context(err))?;
trace!("LocalAI related questions: {:?}", questions);
@@ -280,7 +290,7 @@ impl ChatCloudService for AICloudServiceMiddleware {
async fn stream_complete(
&self,
workspace_id: &str,
workspace_id: &Uuid,
params: CompleteTextParams,
ai_model: Option<AIModel>,
) -> Result<StreamComplete, FlowyError> {
@@ -329,15 +339,15 @@ impl ChatCloudService for AICloudServiceMiddleware {
async fn embed_file(
&self,
workspace_id: &str,
workspace_id: &Uuid,
file_path: &Path,
chat_id: &str,
chat_id: &Uuid,
metadata: Option<HashMap<String, Value>>,
) -> Result<(), FlowyError> {
if self.local_ai.is_running() {
self
.local_ai
.embed_file(chat_id, file_path.to_path_buf(), metadata)
.embed_file(&chat_id.to_string(), file_path.to_path_buf(), metadata)
.await
.map_err(|err| FlowyError::local_ai().with_context(err))?;
Ok(())
@@ -349,21 +359,21 @@ impl ChatCloudService for AICloudServiceMiddleware {
}
}
async fn get_local_ai_config(&self, workspace_id: &str) -> Result<LocalAIConfig, FlowyError> {
async fn get_local_ai_config(&self, workspace_id: &Uuid) -> Result<LocalAIConfig, FlowyError> {
self.cloud_service.get_local_ai_config(workspace_id).await
}
async fn get_workspace_plan(
&self,
workspace_id: &str,
workspace_id: &Uuid,
) -> Result<Vec<SubscriptionPlan>, FlowyError> {
self.cloud_service.get_workspace_plan(workspace_id).await
}
async fn get_chat_settings(
&self,
workspace_id: &str,
chat_id: &str,
workspace_id: &Uuid,
chat_id: &Uuid,
) -> Result<ChatSettings, FlowyError> {
self
.cloud_service
@@ -373,8 +383,8 @@ impl ChatCloudService for AICloudServiceMiddleware {
async fn update_chat_settings(
&self,
workspace_id: &str,
chat_id: &str,
workspace_id: &Uuid,
chat_id: &Uuid,
params: UpdateChatParams,
) -> Result<(), FlowyError> {
self
@@ -383,11 +393,11 @@ impl ChatCloudService for AICloudServiceMiddleware {
.await
}
async fn get_available_models(&self, workspace_id: &str) -> Result<ModelList, FlowyError> {
async fn get_available_models(&self, workspace_id: &Uuid) -> Result<ModelList, FlowyError> {
self.cloud_service.get_available_models(workspace_id).await
}
async fn get_workspace_default_model(&self, workspace_id: &str) -> Result<String, FlowyError> {
async fn get_workspace_default_model(&self, workspace_id: &Uuid) -> Result<String, FlowyError> {
self
.cloud_service
.get_workspace_default_model(workspace_id)
@@ -1,5 +1,6 @@
use flowy_derive::ProtoBuf_Enum;
use flowy_notification::NotificationBuilder;
use tracing::trace;
const CHAT_OBSERVABLE_SOURCE: &str = "Chat";
pub const APPFLOWY_AI_NOTIFICATION_KEY: &str = "appflowy_ai_plugin";
@@ -39,7 +40,12 @@ impl std::convert::From<i32> for ChatNotification {
}
}
#[tracing::instrument(level = "trace")]
pub(crate) fn chat_notification_builder(id: &str, ty: ChatNotification) -> NotificationBuilder {
NotificationBuilder::new(id, ty, CHAT_OBSERVABLE_SOURCE)
#[tracing::instrument(level = "trace", skip_all)]
pub(crate) fn chat_notification_builder<T: ToString>(
id: T,
ty: ChatNotification,
) -> NotificationBuilder {
let id = id.to_string();
trace!("chat_notification_builder: id = {id}, ty = {ty:?}");
NotificationBuilder::new(&id, ty, CHAT_OBSERVABLE_SOURCE)
}
+1 -3
View File
@@ -32,7 +32,6 @@ collab = { workspace = true }
#collab = { workspace = true, features = ["verbose_log"] }
diesel.workspace = true
uuid.workspace = true
flowy-storage = { workspace = true }
flowy-storage-pub = { workspace = true }
client-api.workspace = true
@@ -56,8 +55,7 @@ lib-infra = { workspace = true }
serde.workspace = true
serde_json.workspace = true
serde_repr.workspace = true
futures.workspace = true
walkdir = "2.4.0"
uuid.workspace = true
sysinfo = "0.30.5"
semver = { version = "1.0.22", features = ["serde"] }
+3 -7
View File
@@ -83,15 +83,11 @@ impl AppFlowyCoreConfig {
name: String,
) -> Self {
let cloud_config = AFCloudConfiguration::from_env().ok();
let mut log_crates = vec![];
// By default enable sync trace log
let log_crates = vec!["sync_trace_log".to_string()];
let storage_path = match &cloud_config {
None => custom_application_path,
Some(config) => {
if config.enable_sync_trace {
log_crates.push("sync_trace_log".to_string());
}
make_user_data_folder(&custom_application_path, &config.base_url)
},
Some(config) => make_user_data_folder(&custom_application_path, &config.base_url),
};
let log_filter = create_log_filter(
@@ -21,6 +21,7 @@ use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, Weak};
use tracing::{error, info};
use uuid::Uuid;
pub struct ChatDepsResolver;
@@ -56,9 +57,9 @@ struct ChatQueryServiceImpl {
impl AIExternalService for ChatQueryServiceImpl {
async fn query_chat_rag_ids(
&self,
parent_view_id: &str,
chat_id: &str,
) -> Result<Vec<String>, FlowyError> {
parent_view_id: &Uuid,
chat_id: &Uuid,
) -> Result<Vec<Uuid>, FlowyError> {
let mut ids = self
.folder_service
.get_surrounding_view_ids_with_view_layout(parent_view_id, ViewLayout::Document)
@@ -72,9 +73,9 @@ impl AIExternalService for ChatQueryServiceImpl {
}
async fn sync_rag_documents(
&self,
workspace_id: &str,
rag_ids: Vec<String>,
mut rag_metadata_map: HashMap<String, AFCollabMetadata>,
workspace_id: &Uuid,
rag_ids: Vec<Uuid>,
mut rag_metadata_map: HashMap<Uuid, AFCollabMetadata>,
) -> Result<Vec<AFCollabMetadata>, FlowyError> {
let mut result = Vec::new();
@@ -96,7 +97,7 @@ impl AIExternalService for ChatQueryServiceImpl {
if let Ok(prev_sv) = StateVector::decode_v1(&metadata.prev_sync_state_vector) {
let collab = Collab::new_with_source(
CollabOrigin::Empty,
&rag_id,
&rag_id.to_string(),
DataSource::DocStateV1(query_collab.encoded_collab.doc_state.to_vec()),
vec![],
false,
@@ -111,7 +112,7 @@ impl AIExternalService for ChatQueryServiceImpl {
// Perform full sync if changes are detected or no state vector is found
let params = FullSyncCollabParams {
object_id: rag_id.clone(),
object_id: rag_id,
collab_type: CollabType::Document,
encoded_collab: query_collab.encoded_collab.clone(),
};
@@ -125,7 +126,7 @@ impl AIExternalService for ChatQueryServiceImpl {
} else {
info!("[Chat] full sync rag document: {}", rag_id);
result.push(AFCollabMetadata {
object_id: rag_id,
object_id: rag_id.to_string(),
updated_at: timestamp(),
prev_sync_state_vector: query_collab.encoded_collab.state_vector.to_vec(),
collab_type: CollabType::Document as i32,
@@ -136,7 +137,7 @@ impl AIExternalService for ChatQueryServiceImpl {
Ok(result)
}
async fn notify_did_send_message(&self, chat_id: &str, message: &str) -> Result<(), FlowyError> {
async fn notify_did_send_message(&self, chat_id: &Uuid, message: &str) -> Result<(), FlowyError> {
info!(
"notify_did_send_message: chat_id: {}, message: {}",
chat_id, message
@@ -169,7 +170,7 @@ impl AIUserService for ChatUserServiceImpl {
self.upgrade_user()?.device_id()
}
fn workspace_id(&self) -> Result<String, FlowyError> {
fn workspace_id(&self) -> Result<Uuid, FlowyError> {
self.upgrade_user()?.workspace_id()
}
@@ -7,16 +7,6 @@ use collab::core::origin::{CollabClient, CollabOrigin};
use collab::entity::EncodedCollab;
use collab::preclude::CollabPlugin;
use collab_entity::CollabType;
use flowy_search_pub::cloud::SearchCloudService;
use serde_json::Value;
use std::collections::HashMap;
use std::path::Path;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::Duration;
use tokio_stream::wrappers::WatchStream;
use tracing::{debug, info};
use collab_integrate::collab_builder::{
CollabCloudPluginProvider, CollabPluginProviderContext, CollabPluginProviderType,
};
@@ -37,12 +27,24 @@ use flowy_folder_pub::cloud::{
Workspace, WorkspaceRecord,
};
use flowy_folder_pub::entities::PublishPayload;
use flowy_search_pub::cloud::SearchCloudService;
use flowy_server_pub::af_cloud_config::AFCloudConfiguration;
use flowy_storage_pub::cloud::{ObjectIdentity, ObjectValue, StorageCloudService};
use flowy_storage_pub::storage::{CompletedPartRequest, CreateUploadResponse, UploadPartResponse};
use flowy_user_pub::cloud::{UserCloudService, UserCloudServiceProvider};
use flowy_user_pub::entities::{Authenticator, UserTokenState};
use lib_infra::async_trait::async_trait;
use serde_json::Value;
use std::collections::HashMap;
use std::path::Path;
use std::str::FromStr;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::Duration;
use tokio_stream::wrappers::WatchStream;
use tracing::log::error;
use tracing::{debug, info};
use uuid::Uuid;
use crate::server_layer::{Server, ServerProvider};
@@ -82,7 +84,7 @@ impl StorageCloudService for ServerProvider {
async fn get_object_url_v1(
&self,
workspace_id: &str,
workspace_id: &Uuid,
parent_dir: &str,
file_id: &str,
) -> FlowyResult<String> {
@@ -93,7 +95,7 @@ impl StorageCloudService for ServerProvider {
.await
}
async fn parse_object_url_v1(&self, url: &str) -> Option<(String, String, String)> {
async fn parse_object_url_v1(&self, url: &str) -> Option<(Uuid, String, String)> {
self
.get_server()
.ok()?
@@ -104,7 +106,7 @@ impl StorageCloudService for ServerProvider {
async fn create_upload(
&self,
workspace_id: &str,
workspace_id: &Uuid,
parent_dir: &str,
file_id: &str,
content_type: &str,
@@ -119,7 +121,7 @@ impl StorageCloudService for ServerProvider {
async fn upload_part(
&self,
workspace_id: &str,
workspace_id: &Uuid,
parent_dir: &str,
upload_id: &str,
file_id: &str,
@@ -142,7 +144,7 @@ impl StorageCloudService for ServerProvider {
async fn complete_upload(
&self,
workspace_id: &str,
workspace_id: &Uuid,
parent_dir: &str,
upload_id: &str,
file_id: &str,
@@ -233,10 +235,9 @@ impl FolderCloudService for ServerProvider {
server.folder_service().create_workspace(uid, &name).await
}
async fn open_workspace(&self, workspace_id: &str) -> Result<(), FlowyError> {
let workspace_id = workspace_id.to_string();
async fn open_workspace(&self, workspace_id: &Uuid) -> Result<(), FlowyError> {
let server = self.get_server()?;
server.folder_service().open_workspace(&workspace_id).await
server.folder_service().open_workspace(workspace_id).await
}
async fn get_all_workspace(&self) -> Result<Vec<WorkspaceRecord>, FlowyError> {
@@ -246,7 +247,7 @@ impl FolderCloudService for ServerProvider {
async fn get_folder_data(
&self,
workspace_id: &str,
workspace_id: &Uuid,
uid: &i64,
) -> Result<Option<FolderData>, FlowyError> {
let server = self.get_server()?;
@@ -272,10 +273,10 @@ impl FolderCloudService for ServerProvider {
async fn get_folder_doc_state(
&self,
workspace_id: &str,
workspace_id: &Uuid,
uid: i64,
collab_type: CollabType,
object_id: &str,
object_id: &Uuid,
) -> Result<Vec<u8>, FlowyError> {
let server = self.get_server()?;
@@ -287,7 +288,7 @@ impl FolderCloudService for ServerProvider {
async fn batch_create_folder_collab_objects(
&self,
workspace_id: &str,
workspace_id: &Uuid,
objects: Vec<FolderCollabParams>,
) -> Result<(), FlowyError> {
let server = self.get_server()?;
@@ -307,7 +308,7 @@ impl FolderCloudService for ServerProvider {
async fn publish_view(
&self,
workspace_id: &str,
workspace_id: &Uuid,
payload: Vec<PublishPayload>,
) -> Result<(), FlowyError> {
let server = self.get_server()?;
@@ -320,8 +321,8 @@ impl FolderCloudService for ServerProvider {
async fn unpublish_views(
&self,
workspace_id: &str,
view_ids: Vec<String>,
workspace_id: &Uuid,
view_ids: Vec<Uuid>,
) -> Result<(), FlowyError> {
let server = self.get_server()?;
server
@@ -330,15 +331,15 @@ impl FolderCloudService for ServerProvider {
.await
}
async fn get_publish_info(&self, view_id: &str) -> Result<PublishInfo, FlowyError> {
async fn get_publish_info(&self, view_id: &Uuid) -> Result<PublishInfo, FlowyError> {
let server = self.get_server()?;
server.folder_service().get_publish_info(view_id).await
}
async fn set_publish_name(
&self,
workspace_id: &str,
view_id: String,
workspace_id: &Uuid,
view_id: Uuid,
new_name: String,
) -> Result<(), FlowyError> {
let server = self.get_server()?;
@@ -350,7 +351,7 @@ impl FolderCloudService for ServerProvider {
async fn set_publish_namespace(
&self,
workspace_id: &str,
workspace_id: &Uuid,
new_namespace: String,
) -> Result<(), FlowyError> {
let server = self.get_server()?;
@@ -360,7 +361,7 @@ impl FolderCloudService for ServerProvider {
.await
}
async fn get_publish_namespace(&self, workspace_id: &str) -> Result<String, FlowyError> {
async fn get_publish_namespace(&self, workspace_id: &Uuid) -> Result<String, FlowyError> {
let server = self.get_server()?;
server
.folder_service()
@@ -371,7 +372,7 @@ impl FolderCloudService for ServerProvider {
/// List all published views of the current workspace.
async fn list_published_views(
&self,
workspace_id: &str,
workspace_id: &Uuid,
) -> Result<Vec<PublishInfoView>, FlowyError> {
let server = self.get_server()?;
server
@@ -382,7 +383,7 @@ impl FolderCloudService for ServerProvider {
async fn get_default_published_view_info(
&self,
workspace_id: &str,
workspace_id: &Uuid,
) -> Result<PublishInfo, FlowyError> {
let server = self.get_server()?;
server
@@ -393,7 +394,7 @@ impl FolderCloudService for ServerProvider {
async fn set_default_published_view(
&self,
workspace_id: &str,
workspace_id: &Uuid,
view_id: uuid::Uuid,
) -> Result<(), FlowyError> {
let server = self.get_server()?;
@@ -403,7 +404,7 @@ impl FolderCloudService for ServerProvider {
.await
}
async fn remove_default_published_view(&self, workspace_id: &str) -> Result<(), FlowyError> {
async fn remove_default_published_view(&self, workspace_id: &Uuid) -> Result<(), FlowyError> {
let server = self.get_server()?;
server
.folder_service()
@@ -421,7 +422,7 @@ impl FolderCloudService for ServerProvider {
async fn full_sync_collab_object(
&self,
workspace_id: &str,
workspace_id: &Uuid,
params: FullSyncCollabParams,
) -> Result<(), FlowyError> {
self
@@ -436,24 +437,22 @@ impl FolderCloudService for ServerProvider {
impl DatabaseCloudService for ServerProvider {
async fn get_database_encode_collab(
&self,
object_id: &str,
object_id: &Uuid,
collab_type: CollabType,
workspace_id: &str,
workspace_id: &Uuid,
) -> Result<Option<EncodedCollab>, FlowyError> {
let workspace_id = workspace_id.to_string();
let server = self.get_server()?;
let database_id = object_id.to_string();
server
.database_service()
.get_database_encode_collab(&database_id, collab_type, &workspace_id)
.get_database_encode_collab(object_id, collab_type, workspace_id)
.await
}
async fn create_database_encode_collab(
&self,
object_id: &str,
object_id: &Uuid,
collab_type: CollabType,
workspace_id: &str,
workspace_id: &Uuid,
encoded_collab: EncodedCollab,
) -> Result<(), FlowyError> {
let server = self.get_server()?;
@@ -465,30 +464,28 @@ impl DatabaseCloudService for ServerProvider {
async fn batch_get_database_encode_collab(
&self,
object_ids: Vec<String>,
object_ids: Vec<Uuid>,
object_ty: CollabType,
workspace_id: &str,
workspace_id: &Uuid,
) -> Result<EncodeCollabByOid, FlowyError> {
let workspace_id = workspace_id.to_string();
let server = self.get_server()?;
server
.database_service()
.batch_get_database_encode_collab(object_ids, object_ty, &workspace_id)
.batch_get_database_encode_collab(object_ids, object_ty, workspace_id)
.await
}
async fn get_database_collab_object_snapshots(
&self,
object_id: &str,
object_id: &Uuid,
limit: usize,
) -> Result<Vec<DatabaseSnapshot>, FlowyError> {
let server = self.get_server()?;
let database_id = object_id.to_string();
server
.database_service()
.get_database_collab_object_snapshots(&database_id, limit)
.get_database_collab_object_snapshots(object_id, limit)
.await
}
}
@@ -497,29 +494,29 @@ impl DatabaseCloudService for ServerProvider {
impl DatabaseAIService for ServerProvider {
async fn summary_database_row(
&self,
workspace_id: &str,
object_id: &str,
summary_row: SummaryRowContent,
_workspace_id: &Uuid,
_object_id: &Uuid,
_summary_row: SummaryRowContent,
) -> Result<String, FlowyError> {
self
.get_server()?
.database_ai_service()
.ok_or_else(FlowyError::not_support)?
.summary_database_row(workspace_id, object_id, summary_row)
.summary_database_row(_workspace_id, _object_id, _summary_row)
.await
}
async fn translate_database_row(
&self,
workspace_id: &str,
translate_row: TranslateRowContent,
language: &str,
_workspace_id: &Uuid,
_translate_row: TranslateRowContent,
_language: &str,
) -> Result<TranslateRowResponse, FlowyError> {
self
.get_server()?
.database_ai_service()
.ok_or_else(FlowyError::not_support)?
.translate_database_row(workspace_id, translate_row, language)
.translate_database_row(_workspace_id, _translate_row, _language)
.await
}
}
@@ -528,8 +525,8 @@ impl DatabaseAIService for ServerProvider {
impl DocumentCloudService for ServerProvider {
async fn get_document_doc_state(
&self,
document_id: &str,
workspace_id: &str,
document_id: &Uuid,
workspace_id: &Uuid,
) -> Result<Vec<u8>, FlowyError> {
let server = self.get_server()?;
server
@@ -540,7 +537,7 @@ impl DocumentCloudService for ServerProvider {
async fn get_document_snapshots(
&self,
document_id: &str,
document_id: &Uuid,
limit: usize,
workspace_id: &str,
) -> Result<Vec<DocumentSnapshot>, FlowyError> {
@@ -554,8 +551,8 @@ impl DocumentCloudService for ServerProvider {
async fn get_document_data(
&self,
document_id: &str,
workspace_id: &str,
document_id: &Uuid,
workspace_id: &Uuid,
) -> Result<Option<DocumentData>, FlowyError> {
let server = self.get_server()?;
server
@@ -566,8 +563,8 @@ impl DocumentCloudService for ServerProvider {
async fn create_document_collab(
&self,
workspace_id: &str,
document_id: &str,
workspace_id: &Uuid,
document_id: &Uuid,
encoded_collab: EncodedCollab,
) -> Result<(), FlowyError> {
let server = self.get_server()?;
@@ -611,26 +608,37 @@ impl CollabCloudPluginProvider for ServerProvider {
collab_object.uid,
collab_object.device_id.clone(),
));
let sync_object = SyncObject::new(
&collab_object.object_id,
&collab_object.workspace_id,
collab_object.collab_type,
&collab_object.device_id,
);
let (sink, stream) = (channel.sink(), channel.stream());
let sink_config = SinkConfig::new().send_timeout(8);
let sync_plugin = SyncPlugin::new(
origin,
sync_object,
local_collab,
sink,
sink_config,
stream,
Some(channel),
ws_connect_state,
Some(Duration::from_secs(60)),
);
plugins.push(Box::new(sync_plugin));
if let (Ok(object_id), Ok(workspace_id)) = (
Uuid::from_str(&collab_object.object_id),
Uuid::from_str(&collab_object.workspace_id),
) {
let sync_object = SyncObject::new(
object_id,
workspace_id,
collab_object.collab_type,
&collab_object.device_id,
);
let (sink, stream) = (channel.sink(), channel.stream());
let sink_config = SinkConfig::new().send_timeout(8);
let sync_plugin = SyncPlugin::new(
origin,
sync_object,
local_collab,
sink,
sink_config,
stream,
Some(channel),
ws_connect_state,
Some(Duration::from_secs(60)),
);
plugins.push(Box::new(sync_plugin));
} else {
error!(
"Failed to parse collab object id: {}",
collab_object.object_id
);
}
},
Ok(None) => {
tracing::error!("🔴Failed to get collab ws channel: channel is none");
@@ -655,9 +663,9 @@ impl ChatCloudService for ServerProvider {
async fn create_chat(
&self,
uid: &i64,
workspace_id: &str,
chat_id: &str,
rag_ids: Vec<String>,
workspace_id: &Uuid,
chat_id: &Uuid,
rag_ids: Vec<Uuid>,
) -> Result<(), FlowyError> {
let server = self.get_server();
server?
@@ -668,26 +676,24 @@ impl ChatCloudService for ServerProvider {
async fn create_question(
&self,
workspace_id: &str,
chat_id: &str,
workspace_id: &Uuid,
chat_id: &Uuid,
message: &str,
message_type: ChatMessageType,
metadata: &[ChatMessageMetadata],
) -> Result<ChatMessage, FlowyError> {
let workspace_id = workspace_id.to_string();
let chat_id = chat_id.to_string();
let message = message.to_string();
self
.get_server()?
.chat_service()
.create_question(&workspace_id, &chat_id, &message, message_type, metadata)
.create_question(workspace_id, chat_id, &message, message_type, metadata)
.await
}
async fn create_answer(
&self,
workspace_id: &str,
chat_id: &str,
workspace_id: &Uuid,
chat_id: &Uuid,
message: &str,
question_id: i64,
metadata: Option<serde_json::Value>,
@@ -701,25 +707,23 @@ impl ChatCloudService for ServerProvider {
async fn stream_answer(
&self,
workspace_id: &str,
chat_id: &str,
workspace_id: &Uuid,
chat_id: &Uuid,
message_id: i64,
format: ResponseFormat,
ai_model: Option<AIModel>,
) -> Result<StreamAnswer, FlowyError> {
let workspace_id = workspace_id.to_string();
let chat_id = chat_id.to_string();
let server = self.get_server()?;
server
.chat_service()
.stream_answer(&workspace_id, &chat_id, message_id, format, ai_model)
.stream_answer(workspace_id, chat_id, message_id, format, ai_model)
.await
}
async fn get_chat_messages(
&self,
workspace_id: &str,
chat_id: &str,
workspace_id: &Uuid,
chat_id: &Uuid,
offset: MessageCursor,
limit: u64,
) -> Result<RepeatedChatMessage, FlowyError> {
@@ -732,8 +736,8 @@ impl ChatCloudService for ServerProvider {
async fn get_question_from_answer_id(
&self,
workspace_id: &str,
chat_id: &str,
workspace_id: &Uuid,
chat_id: &Uuid,
answer_message_id: i64,
) -> Result<ChatMessage, FlowyError> {
self
@@ -745,8 +749,8 @@ impl ChatCloudService for ServerProvider {
async fn get_related_message(
&self,
workspace_id: &str,
chat_id: &str,
workspace_id: &Uuid,
chat_id: &Uuid,
message_id: i64,
) -> Result<RepeatedRelatedQuestion, FlowyError> {
self
@@ -758,8 +762,8 @@ impl ChatCloudService for ServerProvider {
async fn get_answer(
&self,
workspace_id: &str,
chat_id: &str,
workspace_id: &Uuid,
chat_id: &Uuid,
question_message_id: i64,
) -> Result<ChatMessage, FlowyError> {
let server = self.get_server();
@@ -771,23 +775,22 @@ impl ChatCloudService for ServerProvider {
async fn stream_complete(
&self,
workspace_id: &str,
workspace_id: &Uuid,
params: CompleteTextParams,
ai_model: Option<AIModel>,
) -> Result<StreamComplete, FlowyError> {
let workspace_id = workspace_id.to_string();
let server = self.get_server()?;
server
.chat_service()
.stream_complete(&workspace_id, params, ai_model)
.stream_complete(workspace_id, params, ai_model)
.await
}
async fn embed_file(
&self,
workspace_id: &str,
workspace_id: &Uuid,
file_path: &Path,
chat_id: &str,
chat_id: &Uuid,
metadata: Option<HashMap<String, Value>>,
) -> Result<(), FlowyError> {
self
@@ -797,7 +800,7 @@ impl ChatCloudService for ServerProvider {
.await
}
async fn get_local_ai_config(&self, workspace_id: &str) -> Result<LocalAIConfig, FlowyError> {
async fn get_local_ai_config(&self, workspace_id: &Uuid) -> Result<LocalAIConfig, FlowyError> {
self
.get_server()?
.chat_service()
@@ -807,7 +810,7 @@ impl ChatCloudService for ServerProvider {
async fn get_workspace_plan(
&self,
workspace_id: &str,
workspace_id: &Uuid,
) -> Result<Vec<SubscriptionPlan>, FlowyError> {
self
.get_server()?
@@ -818,8 +821,8 @@ impl ChatCloudService for ServerProvider {
async fn get_chat_settings(
&self,
workspace_id: &str,
chat_id: &str,
workspace_id: &Uuid,
chat_id: &Uuid,
) -> Result<ChatSettings, FlowyError> {
self
.get_server()?
@@ -830,8 +833,8 @@ impl ChatCloudService for ServerProvider {
async fn update_chat_settings(
&self,
workspace_id: &str,
chat_id: &str,
workspace_id: &Uuid,
chat_id: &Uuid,
params: UpdateChatParams,
) -> Result<(), FlowyError> {
self
@@ -841,7 +844,7 @@ impl ChatCloudService for ServerProvider {
.await
}
async fn get_available_models(&self, workspace_id: &str) -> Result<ModelList, FlowyError> {
async fn get_available_models(&self, workspace_id: &Uuid) -> Result<ModelList, FlowyError> {
self
.get_server()?
.chat_service()
@@ -849,7 +852,7 @@ impl ChatCloudService for ServerProvider {
.await
}
async fn get_workspace_default_model(&self, workspace_id: &str) -> Result<String, FlowyError> {
async fn get_workspace_default_model(&self, workspace_id: &Uuid) -> Result<String, FlowyError> {
self
.get_server()?
.chat_service()
@@ -862,7 +865,7 @@ impl ChatCloudService for ServerProvider {
impl SearchCloudService for ServerProvider {
async fn document_search(
&self,
workspace_id: &str,
workspace_id: &Uuid,
query: String,
) -> Result<Vec<SearchDocumentResponseItem>, FlowyError> {
let server = self.get_server()?;
@@ -13,6 +13,7 @@ use collab_integrate::collab_builder::WorkspaceCollabIntegrate;
use lib_infra::util::timestamp;
use std::sync::{Arc, Weak};
use tracing::debug;
use uuid::Uuid;
pub struct SnapshotDBImpl(pub Weak<AuthenticateUser>);
@@ -222,12 +223,12 @@ impl WorkspaceCollabIntegrateImpl {
}
impl WorkspaceCollabIntegrate for WorkspaceCollabIntegrateImpl {
fn workspace_id(&self) -> Result<String, anyhow::Error> {
fn workspace_id(&self) -> Result<Uuid, FlowyError> {
let workspace_id = self.upgrade_user()?.workspace_id()?;
Ok(workspace_id)
}
fn device_id(&self) -> Result<String, anyhow::Error> {
fn device_id(&self) -> Result<String, FlowyError> {
Ok(self.upgrade_user()?.user_config.device_id.clone())
}
}
@@ -13,6 +13,7 @@ use lib_infra::async_trait::async_trait;
use lib_infra::priority_task::TaskDispatcher;
use std::sync::{Arc, Weak};
use tokio::sync::RwLock;
use uuid::Uuid;
pub struct DatabaseDepsResolver();
@@ -47,41 +48,41 @@ struct DatabaseAIServiceMiddleware {
impl DatabaseAIService for DatabaseAIServiceMiddleware {
async fn summary_database_row(
&self,
workspace_id: &str,
object_id: &str,
summary_row: SummaryRowContent,
workspace_id: &Uuid,
object_id: &Uuid,
_summary_row: SummaryRowContent,
) -> Result<String, FlowyError> {
if self.ai_manager.local_ai.is_running() {
self
.ai_manager
.local_ai
.summary_database_row(summary_row)
.summary_database_row(_summary_row)
.await
.map_err(|err| FlowyError::local_ai().with_context(err))
} else {
self
.ai_service
.summary_database_row(workspace_id, object_id, summary_row)
.summary_database_row(workspace_id, object_id, _summary_row)
.await
}
}
async fn translate_database_row(
&self,
workspace_id: &str,
translate_row: TranslateRowContent,
language: &str,
_workspace_id: &Uuid,
_translate_row: TranslateRowContent,
_language: &str,
) -> Result<TranslateRowResponse, FlowyError> {
if self.ai_manager.local_ai.is_running() {
let data = LocalAITranslateRowData {
cells: translate_row
cells: _translate_row
.into_iter()
.map(|row| LocalAITranslateItem {
title: row.title,
content: row.content,
})
.collect(),
language: language.to_string(),
language: _language.to_string(),
include_header: false,
};
let resp = self
@@ -95,7 +96,7 @@ impl DatabaseAIService for DatabaseAIServiceMiddleware {
} else {
self
.ai_service
.translate_database_row(workspace_id, translate_row, language)
.translate_database_row(_workspace_id, _translate_row, _language)
.await
}
}
@@ -121,11 +122,11 @@ impl DatabaseUser for DatabaseUserImpl {
self.upgrade_user()?.get_collab_db(uid)
}
fn workspace_id(&self) -> Result<String, FlowyError> {
fn workspace_id(&self) -> Result<Uuid, FlowyError> {
self.upgrade_user()?.workspace_id()
}
fn workspace_database_object_id(&self) -> Result<String, FlowyError> {
fn workspace_database_object_id(&self) -> Result<Uuid, FlowyError> {
self.upgrade_user()?.workspace_database_object_id()
}
}
@@ -1,5 +1,3 @@
use std::sync::{Arc, Weak};
use crate::deps_resolve::CollabSnapshotSql;
use collab_integrate::collab_builder::AppFlowyCollabBuilder;
use collab_integrate::CollabKVDB;
@@ -10,6 +8,8 @@ use flowy_document_pub::cloud::DocumentCloudService;
use flowy_error::{FlowyError, FlowyResult};
use flowy_storage_pub::storage::StorageService;
use flowy_user::services::authenticate_user::AuthenticateUser;
use std::sync::{Arc, Weak};
use uuid::Uuid;
pub struct DocumentDepsResolver();
impl DocumentDepsResolver {
@@ -97,7 +97,7 @@ impl DocumentUserService for DocumentUserImpl {
.device_id()
}
fn workspace_id(&self) -> Result<String, FlowyError> {
fn workspace_id(&self) -> Result<Uuid, FlowyError> {
self
.0
.upgrade()
@@ -4,6 +4,7 @@ use flowy_storage::manager::{StorageManager, StorageUserService};
use flowy_storage_pub::cloud::StorageCloudService;
use flowy_user::services::authenticate_user::AuthenticateUser;
use std::sync::{Arc, Weak};
use uuid::Uuid;
pub struct FileStorageResolver;
@@ -40,7 +41,7 @@ impl StorageUserService for FileStorageServiceImpl {
self.upgrade_user()?.user_id()
}
fn workspace_id(&self) -> Result<String, FlowyError> {
fn workspace_id(&self) -> Result<Uuid, FlowyError> {
self.upgrade_user()?.workspace_id()
}
@@ -8,6 +8,7 @@ use flowy_folder::share::ImportType;
use flowy_folder::view_operation::{FolderOperationHandler, ImportedData};
use lib_infra::async_trait::async_trait;
use std::sync::Arc;
use uuid::Uuid;
pub struct ChatFolderOperation(pub Arc<AIManager>);
@@ -17,19 +18,19 @@ impl FolderOperationHandler for ChatFolderOperation {
"ChatFolderOperationHandler"
}
async fn open_view(&self, view_id: &str) -> Result<(), FlowyError> {
async fn open_view(&self, view_id: &Uuid) -> Result<(), FlowyError> {
self.0.open_chat(view_id).await
}
async fn close_view(&self, view_id: &str) -> Result<(), FlowyError> {
async fn close_view(&self, view_id: &Uuid) -> Result<(), FlowyError> {
self.0.close_chat(view_id).await
}
async fn delete_view(&self, view_id: &str) -> Result<(), FlowyError> {
async fn delete_view(&self, view_id: &Uuid) -> Result<(), FlowyError> {
self.0.delete_chat(view_id).await
}
async fn duplicate_view(&self, _view_id: &str) -> Result<Bytes, FlowyError> {
async fn duplicate_view(&self, _view_id: &Uuid) -> Result<Bytes, FlowyError> {
Err(FlowyError::not_support())
}
@@ -44,8 +45,8 @@ impl FolderOperationHandler for ChatFolderOperation {
async fn create_default_view(
&self,
user_id: i64,
parent_view_id: &str,
view_id: &str,
parent_view_id: &Uuid,
view_id: &Uuid,
_name: &str,
_layout: ViewLayout,
) -> Result<(), FlowyError> {
@@ -59,7 +60,7 @@ impl FolderOperationHandler for ChatFolderOperation {
async fn import_from_bytes(
&self,
_uid: i64,
_view_id: &str,
_view_id: &Uuid,
_name: &str,
_import_type: ImportType,
_bytes: Vec<u8>,
@@ -1,3 +1,4 @@
#![allow(unused_variables)]
use bytes::Bytes;
use collab::entity::EncodedCollab;
use collab_entity::CollabType;
@@ -19,23 +20,31 @@ use lib_infra::async_trait::async_trait;
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use uuid::Uuid;
pub struct DatabaseFolderOperation(pub Arc<DatabaseManager>);
#[async_trait]
impl FolderOperationHandler for DatabaseFolderOperation {
async fn open_view(&self, view_id: &str) -> Result<(), FlowyError> {
async fn open_view(&self, view_id: &Uuid) -> Result<(), FlowyError> {
self.0.open_database_view(view_id).await?;
Ok(())
}
async fn close_view(&self, view_id: &str) -> Result<(), FlowyError> {
self.0.close_database_view(view_id).await?;
async fn close_view(&self, view_id: &Uuid) -> Result<(), FlowyError> {
self
.0
.close_database_view(view_id.to_string().as_str())
.await?;
Ok(())
}
async fn delete_view(&self, view_id: &str) -> Result<(), FlowyError> {
match self.0.delete_database_view(view_id).await {
async fn delete_view(&self, view_id: &Uuid) -> Result<(), FlowyError> {
match self
.0
.delete_database_view(view_id.to_string().as_str())
.await
{
Ok(_) => tracing::trace!("Delete database view: {}", view_id),
Err(e) => tracing::error!("🔴delete database failed: {}", e),
}
@@ -44,16 +53,20 @@ impl FolderOperationHandler for DatabaseFolderOperation {
async fn gather_publish_encode_collab(
&self,
user: &Arc<dyn FolderUser>,
view_id: &str,
_user: &Arc<dyn FolderUser>,
view_id: &Uuid,
) -> Result<GatherEncodedCollab, FlowyError> {
let workspace_id = user.workspace_id()?;
let workspace_id = _user.workspace_id()?;
let view_id_str = view_id.to_string();
// get the collab_object_id for the database.
//
// the collab object_id for the database is not the view_id,
// we should use the view_id to get the database_id
let oid = self.0.get_database_id_with_view_id(view_id).await?;
let row_oids = self.0.get_database_row_ids_with_view_id(view_id).await?;
let oid = self.0.get_database_id_with_view_id(&view_id_str).await?;
let row_oids = self
.0
.get_database_row_ids_with_view_id(&view_id_str)
.await?;
let row_metas = self
.0
.get_database_row_metas_with_view_id(view_id, row_oids.clone())
@@ -68,12 +81,12 @@ impl FolderOperationHandler for DatabaseFolderOperation {
.collect::<Vec<_>>();
let database_metas = self.0.get_all_databases_meta().await;
let uid = user
let uid = _user
.user_id()
.map_err(|e| e.with_context("unable to get the uid: {}"))?;
// get the collab db
let collab_db = user
let collab_db = _user
.collab_db(uid)
.map_err(|e| e.with_context("unable to get the collab"))?;
let collab_db = collab_db.upgrade().ok_or_else(|| {
@@ -84,7 +97,7 @@ impl FolderOperationHandler for DatabaseFolderOperation {
tokio::task::spawn_blocking(move || {
let collab_read_txn = collab_db.read_txn();
let database_collab = load_collab_by_object_id(uid, &collab_read_txn, &workspace_id, &oid)
let database_collab = load_collab_by_object_id(uid, &collab_read_txn, &workspace_id.to_string(), &oid)
.map_err(|e| {
FlowyError::internal().with_context(format!("load database collab failed: {}", e))
})?;
@@ -97,7 +110,7 @@ impl FolderOperationHandler for DatabaseFolderOperation {
})?;
let database_row_encoded_collabs =
load_collab_by_object_ids(uid, &workspace_id, &collab_read_txn, &row_oids)
load_collab_by_object_ids(uid, &workspace_id.to_string(), &collab_read_txn, &row_oids)
.0
.into_iter()
.map(|(oid, collab)| {
@@ -123,7 +136,7 @@ impl FolderOperationHandler for DatabaseFolderOperation {
.collect::<HashMap<_, _>>();
let database_row_document_encoded_collabs =
load_collab_by_object_ids(uid, &workspace_id, &collab_read_txn, &row_document_ids)
load_collab_by_object_ids(uid, &workspace_id.to_string(), &collab_read_txn, &row_document_ids)
.0
.into_iter()
.map(|(oid, collab)| {
@@ -147,7 +160,7 @@ impl FolderOperationHandler for DatabaseFolderOperation {
.await?
}
async fn duplicate_view(&self, view_id: &str) -> Result<Bytes, FlowyError> {
async fn duplicate_view(&self, view_id: &Uuid) -> Result<Bytes, FlowyError> {
Ok(Bytes::from(view_id.to_string()))
}
@@ -166,14 +179,14 @@ impl FolderOperationHandler for DatabaseFolderOperation {
String::from_utf8(data.to_vec()).map_err(|_| FlowyError::invalid_data())?;
let encoded_collab = self
.0
.duplicate_database(&duplicated_view_id, &params.view_id)
.duplicate_database(&duplicated_view_id, &params.view_id.to_string())
.await?;
Ok(Some(encoded_collab))
},
ViewData::Data(data) => {
let encoded_collab = self
.0
.create_database_with_data(&params.view_id, data.to_vec())
.create_database_with_data(&params.view_id.to_string(), data.to_vec())
.await?;
Ok(Some(encoded_collab))
},
@@ -212,17 +225,18 @@ impl FolderOperationHandler for DatabaseFolderOperation {
/// these references views.
async fn create_default_view(
&self,
_user_id: i64,
_parent_view_id: &str,
view_id: &str,
user_id: i64,
parent_view_id: &Uuid,
view_id: &Uuid,
name: &str,
layout: ViewLayout,
) -> Result<(), FlowyError> {
let name = name.to_string();
let view_id = view_id.to_string();
let data = match layout {
ViewLayout::Grid => make_default_grid(view_id, &name),
ViewLayout::Board => make_default_board(view_id, &name),
ViewLayout::Calendar => make_default_calendar(view_id, &name),
ViewLayout::Grid => make_default_grid(&view_id, &name),
ViewLayout::Board => make_default_board(&view_id, &name),
ViewLayout::Calendar => make_default_calendar(&view_id, &name),
ViewLayout::Document | ViewLayout::Chat => {
return Err(
FlowyError::internal().with_context(format!("Can't handle {:?} layout type", layout)),
@@ -244,9 +258,9 @@ impl FolderOperationHandler for DatabaseFolderOperation {
async fn import_from_bytes(
&self,
_uid: i64,
view_id: &str,
_name: &str,
uid: i64,
view_id: &Uuid,
name: &str,
import_type: ImportType,
bytes: Vec<u8>,
) -> Result<Vec<ImportedData>, FlowyError> {
@@ -17,8 +17,10 @@ use flowy_folder::view_operation::{
use lib_dispatch::prelude::ToBytes;
use lib_infra::async_trait::async_trait;
use std::convert::TryFrom;
use std::str::FromStr;
use std::sync::Arc;
use tokio::sync::RwLock;
use uuid::Uuid;
pub struct DocumentFolderOperation(pub Arc<DocumentManager>);
#[async_trait]
@@ -33,6 +35,7 @@ impl FolderOperationHandler for DocumentFolderOperation {
workspace_view_builder: Arc<RwLock<NestedViewBuilder>>,
) -> Result<(), FlowyError> {
let manager = self.0.clone();
let mut write_guard = workspace_view_builder.write().await;
// Create a view named "Getting started" with an icon ⭐️ and the built-in README data.
// Don't modify this code unless you know what you are doing.
@@ -45,8 +48,9 @@ impl FolderOperationHandler for DocumentFolderOperation {
// create a empty document
let json_str = include_str!("../../../assets/read_me.json");
let document_pb = JsonToDocumentParser::json_str_to_document(json_str).unwrap();
let view_id = Uuid::from_str(&view.view.id).unwrap();
manager
.create_document(uid, &view.view.id, Some(document_pb.into()))
.create_document(uid, &view_id, Some(document_pb.into()))
.await
.unwrap();
view
@@ -55,18 +59,18 @@ impl FolderOperationHandler for DocumentFolderOperation {
Ok(())
}
async fn open_view(&self, view_id: &str) -> Result<(), FlowyError> {
async fn open_view(&self, view_id: &Uuid) -> Result<(), FlowyError> {
self.0.open_document(view_id).await?;
Ok(())
}
/// Close the document view.
async fn close_view(&self, view_id: &str) -> Result<(), FlowyError> {
async fn close_view(&self, view_id: &Uuid) -> Result<(), FlowyError> {
self.0.close_document(view_id).await?;
Ok(())
}
async fn delete_view(&self, view_id: &str) -> Result<(), FlowyError> {
async fn delete_view(&self, view_id: &Uuid) -> Result<(), FlowyError> {
match self.0.delete_document(view_id).await {
Ok(_) => tracing::trace!("Delete document: {}", view_id),
Err(e) => tracing::error!("🔴delete document failed: {}", e),
@@ -74,7 +78,7 @@ impl FolderOperationHandler for DocumentFolderOperation {
Ok(())
}
async fn duplicate_view(&self, view_id: &str) -> Result<Bytes, FlowyError> {
async fn duplicate_view(&self, view_id: &Uuid) -> Result<Bytes, FlowyError> {
let data: DocumentDataPB = self.0.get_document_data(view_id).await?.into();
let data_bytes = data.into_bytes().map_err(|_| FlowyError::invalid_data())?;
Ok(data_bytes)
@@ -83,10 +87,11 @@ impl FolderOperationHandler for DocumentFolderOperation {
async fn gather_publish_encode_collab(
&self,
user: &Arc<dyn FolderUser>,
view_id: &str,
view_id: &Uuid,
) -> Result<GatherEncodedCollab, FlowyError> {
let encoded_collab =
get_encoded_collab_v1_from_disk(user, view_id, CollabType::Document).await?;
get_encoded_collab_v1_from_disk(user, view_id.to_string().as_str(), CollabType::Document)
.await?;
Ok(GatherEncodedCollab::Document(encoded_collab))
}
@@ -112,8 +117,8 @@ impl FolderOperationHandler for DocumentFolderOperation {
async fn create_default_view(
&self,
user_id: i64,
_parent_view_id: &str,
view_id: &str,
_parent_view_id: &Uuid,
view_id: &Uuid,
_name: &str,
layout: ViewLayout,
) -> Result<(), FlowyError> {
@@ -133,7 +138,7 @@ impl FolderOperationHandler for DocumentFolderOperation {
async fn import_from_bytes(
&self,
uid: i64,
view_id: &str,
view_id: &Uuid,
_name: &str,
_import_type: ImportType,
bytes: Vec<u8>,
@@ -17,6 +17,7 @@ use flowy_search::folder::indexer::FolderIndexManagerImpl;
use flowy_sqlite::kv::KVStorePreferences;
use flowy_user::services::authenticate_user::AuthenticateUser;
use flowy_user::services::data_import::load_collab_by_object_id;
use std::str::FromStr;
use std::sync::{Arc, Weak};
use crate::deps_resolve::folder_deps::folder_deps_chat_impl::ChatFolderOperation;
@@ -25,6 +26,7 @@ use crate::deps_resolve::folder_deps::folder_deps_doc_impl::DocumentFolderOperat
use collab_plugins::local_storage::kv::KVTransactionDB;
use flowy_folder_pub::query::{FolderQueryService, FolderService, FolderViewEdit, QueryCollab};
use lib_infra::async_trait::async_trait;
use uuid::Uuid;
pub struct FolderDepsResolver();
#[allow(clippy::too_many_arguments)]
@@ -89,7 +91,7 @@ impl FolderUser for FolderUserImpl {
self.upgrade_user()?.user_id()
}
fn workspace_id(&self) -> Result<String, FlowyError> {
fn workspace_id(&self) -> Result<Uuid, FlowyError> {
self.upgrade_user()?.workspace_id()
}
@@ -97,8 +99,10 @@ impl FolderUser for FolderUserImpl {
self.upgrade_user()?.get_collab_db(uid)
}
fn is_folder_exist_on_disk(&self, uid: i64, workspace_id: &str) -> FlowyResult<bool> {
self.upgrade_user()?.is_collab_on_disk(uid, workspace_id)
fn is_folder_exist_on_disk(&self, uid: i64, workspace_id: &Uuid) -> FlowyResult<bool> {
self
.upgrade_user()?
.is_collab_on_disk(uid, workspace_id.to_string().as_str())
}
}
@@ -124,13 +128,13 @@ impl FolderServiceImpl {
#[async_trait]
impl FolderViewEdit for FolderServiceImpl {
async fn set_view_title_if_empty(&self, view_id: &str, title: &str) -> FlowyResult<()> {
async fn set_view_title_if_empty(&self, view_id: &Uuid, title: &str) -> FlowyResult<()> {
if title.is_empty() {
return Ok(());
}
if let Some(folder_manager) = self.folder_manager.upgrade() {
if let Ok(view) = folder_manager.get_view(view_id).await {
if let Ok(view) = folder_manager.get_view(view_id.to_string().as_str()).await {
if view.name.is_empty() {
let title = if title.len() > 50 {
title.chars().take(50).collect()
@@ -160,22 +164,25 @@ impl FolderViewEdit for FolderServiceImpl {
impl FolderQueryService for FolderServiceImpl {
async fn get_surrounding_view_ids_with_view_layout(
&self,
parent_view_id: &str,
parent_view_id: &Uuid,
view_layout: ViewLayout,
) -> Vec<String> {
) -> Vec<Uuid> {
let folder_manager = match self.folder_manager.upgrade() {
Some(folder_manager) => folder_manager,
None => return vec![],
};
if let Ok(view) = folder_manager.get_view(parent_view_id).await {
if let Ok(view) = folder_manager
.get_view(parent_view_id.to_string().as_str())
.await
{
if view.space_info().is_some() {
return vec![];
}
}
match folder_manager
.get_untrashed_views_belong_to(parent_view_id)
.get_untrashed_views_belong_to(parent_view_id.to_string().as_str())
.await
{
Ok(views) => {
@@ -183,23 +190,24 @@ impl FolderQueryService for FolderServiceImpl {
.into_iter()
.filter_map(|child| {
if child.layout == view_layout {
Some(child.id.clone())
Uuid::from_str(&child.id).ok()
} else {
None
}
})
.collect::<Vec<_>>();
children.push(parent_view_id.to_string());
children.push(*parent_view_id);
children
},
_ => vec![],
}
}
async fn get_collab(&self, object_id: &str, collab_type: CollabType) -> Option<QueryCollab> {
let encode_collab = get_encoded_collab_v1_from_disk(&self.user, object_id, collab_type)
.await
.ok();
async fn get_collab(&self, object_id: &Uuid, collab_type: CollabType) -> Option<QueryCollab> {
let encode_collab =
get_encoded_collab_v1_from_disk(&self.user, object_id.to_string().as_str(), collab_type)
.await
.ok();
encode_collab.map(|encoded_collab| QueryCollab {
collab_type,
@@ -229,8 +237,8 @@ async fn get_encoded_collab_v1_from_disk(
)
})?;
let collab_read_txn = collab_db.read_txn();
let collab =
load_collab_by_object_id(uid, &collab_read_txn, &workspace_id, view_id).map_err(|e| {
let collab = load_collab_by_object_id(uid, &collab_read_txn, &workspace_id.to_string(), view_id)
.map_err(|e| {
FlowyError::internal().with_context(format!("load document collab failed: {}", e))
})?;
@@ -13,6 +13,7 @@ use lib_infra::async_trait::async_trait;
use std::collections::HashMap;
use std::sync::Arc;
use tracing::info;
use uuid::Uuid;
pub struct UserDepsResolver();
@@ -81,7 +82,7 @@ impl UserWorkspaceService for UserWorkspaceServiceImpl {
Ok(())
}
fn did_delete_workspace(&self, workspace_id: String) -> FlowyResult<()> {
fn did_delete_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()> {
// The remove_indices_for_workspace should not block the deletion of the workspace
// Log the error and continue
if let Err(err) = self
+9 -9
View File
@@ -1,20 +1,20 @@
#![allow(unused_doc_comments)]
use flowy_search::folder::indexer::FolderIndexManagerImpl;
use flowy_search::services::manager::SearchManager;
use std::sync::{Arc, Weak};
use std::time::Duration;
use sysinfo::System;
use tokio::sync::RwLock;
use tracing::{debug, error, event, info, instrument};
use collab_integrate::collab_builder::{AppFlowyCollabBuilder, CollabPluginProviderType};
use flowy_ai::ai_manager::AIManager;
use flowy_database2::DatabaseManager;
use flowy_document::manager::DocumentManager;
use flowy_error::{FlowyError, FlowyResult};
use flowy_folder::manager::FolderManager;
use flowy_search::folder::indexer::FolderIndexManagerImpl;
use flowy_search::services::manager::SearchManager;
use flowy_server::af_cloud::define::ServerUser;
use std::sync::{Arc, Weak};
use std::time::Duration;
use sysinfo::System;
use tokio::sync::RwLock;
use tracing::{debug, error, event, info, instrument};
use uuid::Uuid;
use flowy_sqlite::kv::KVStorePreferences;
use flowy_storage::manager::StorageManager;
@@ -329,7 +329,7 @@ impl ServerUserImpl {
}
}
impl ServerUser for ServerUserImpl {
fn workspace_id(&self) -> FlowyResult<String> {
fn workspace_id(&self) -> FlowyResult<Uuid> {
self.upgrade_user()?.workspace_id()
}
}
@@ -55,6 +55,7 @@ impl UserStatusCallback for UserStatusCallbackImpl {
_device_id: &str,
authenticator: &Authenticator,
) -> FlowyResult<()> {
let workspace_id = user_workspace.workspace_id()?;
self
.server_provider
.set_user_authenticator(user_authenticator);
@@ -74,7 +75,7 @@ impl UserStatusCallback for UserStatusCallbackImpl {
.folder_manager
.initialize(
user_id,
&user_workspace.id,
&workspace_id,
FolderInitDataSource::LocalDisk {
create_if_not_exist: false,
},
@@ -140,6 +141,7 @@ impl UserStatusCallback for UserStatusCallbackImpl {
user_workspace,
device_id
);
let workspace_id = user_workspace.workspace_id()?;
// In the current implementation, when a user signs up for AppFlowy Cloud, a default workspace
// is automatically created for them. However, for users who sign up through Supabase, the creation
@@ -149,10 +151,10 @@ impl UserStatusCallback for UserStatusCallbackImpl {
.folder_manager
.cloud_service
.get_folder_doc_state(
&user_workspace.id,
&workspace_id,
user_profile.uid,
CollabType::Folder,
&user_workspace.id,
&workspace_id,
)
.await
{
@@ -179,7 +181,7 @@ impl UserStatusCallback for UserStatusCallbackImpl {
&user_profile.token,
is_new_user,
data_source,
&user_workspace.id,
&workspace_id,
)
.await
.context("FolderManager error")?;
@@ -9,6 +9,6 @@ edition = "2021"
lib-infra = { workspace = true }
collab-entity = { workspace = true }
collab = { workspace = true }
anyhow.workspace = true
client-api = { workspace = true }
flowy-error = { workspace = true }
flowy-error = { workspace = true }
uuid.workspace = true
@@ -4,8 +4,9 @@ use collab_entity::CollabType;
use flowy_error::FlowyError;
use lib_infra::async_trait::async_trait;
use std::collections::HashMap;
use uuid::Uuid;
pub type EncodeCollabByOid = HashMap<String, EncodedCollab>;
pub type EncodeCollabByOid = HashMap<Uuid, EncodedCollab>;
pub type SummaryRowContent = HashMap<String, String>;
pub type TranslateRowContent = Vec<TranslateItem>;
@@ -13,8 +14,8 @@ pub type TranslateRowContent = Vec<TranslateItem>;
pub trait DatabaseAIService: Send + Sync {
async fn summary_database_row(
&self,
_workspace_id: &str,
_object_id: &str,
_workspace_id: &Uuid,
_object_id: &Uuid,
_summary_row: SummaryRowContent,
) -> Result<String, FlowyError> {
Ok("".to_string())
@@ -22,7 +23,7 @@ pub trait DatabaseAIService: Send + Sync {
async fn translate_database_row(
&self,
_workspace_id: &str,
_workspace_id: &Uuid,
_translate_row: TranslateRowContent,
_language: &str,
) -> Result<TranslateRowResponse, FlowyError> {
@@ -41,29 +42,29 @@ pub trait DatabaseAIService: Send + Sync {
pub trait DatabaseCloudService: Send + Sync {
async fn get_database_encode_collab(
&self,
object_id: &str,
object_id: &Uuid,
collab_type: CollabType,
workspace_id: &str,
workspace_id: &Uuid,
) -> Result<Option<EncodedCollab>, FlowyError>;
async fn create_database_encode_collab(
&self,
object_id: &str,
object_id: &Uuid,
collab_type: CollabType,
workspace_id: &str,
workspace_id: &Uuid,
encoded_collab: EncodedCollab,
) -> Result<(), FlowyError>;
async fn batch_get_database_encode_collab(
&self,
object_ids: Vec<String>,
object_ids: Vec<Uuid>,
object_ty: CollabType,
workspace_id: &str,
workspace_id: &Uuid,
) -> Result<EncodeCollabByOid, FlowyError>;
async fn get_database_collab_object_snapshots(
&self,
object_id: &str,
object_id: &Uuid,
limit: usize,
) -> Result<Vec<DatabaseSnapshot>, FlowyError>;
}
@@ -51,6 +51,7 @@ strum_macros = "0.25"
validator = { workspace = true, features = ["derive"] }
tokio-util.workspace = true
moka = { version = "0.12.8", features = ["future"] }
uuid.workspace = true
[dev-dependencies]
event-integration-test = { path = "../event-integration-test", default-features = false }
@@ -1261,7 +1261,7 @@ pub(crate) async fn summarize_row_handler(
let (tx, rx) = oneshot::channel();
tokio::spawn(async move {
let result = manager
.summarize_row(data.view_id, row_id, data.field_id)
.summarize_row(&data.view_id, row_id, data.field_id)
.await;
let _ = tx.send(result);
});
@@ -1280,7 +1280,7 @@ pub(crate) async fn translate_row_handler(
let (tx, rx) = oneshot::channel();
tokio::spawn(async move {
let result = manager
.translate_row(data.view_id, row_id, data.field_id)
.translate_row(&data.view_id, row_id, data.field_id)
.await;
let _ = tx.send(result);
});
@@ -20,6 +20,7 @@ use collab_entity::{CollabObject, CollabType, EncodedCollab};
use collab_plugins::local_storage::kv::KVTransactionDB;
use rayon::prelude::*;
use std::collections::HashMap;
use std::str::FromStr;
use std::sync::{Arc, Weak};
use std::time::Duration;
use tokio::sync::Mutex;
@@ -42,12 +43,13 @@ use crate::services::database_view::DatabaseLayoutDepsResolver;
use crate::services::field_settings::default_field_settings_by_layout_map;
use crate::services::share::csv::{CSVFormat, CSVImporter, ImportResult};
use tokio::sync::RwLock as TokioRwLock;
use uuid::Uuid;
pub trait DatabaseUser: Send + Sync {
fn user_id(&self) -> Result<i64, FlowyError>;
fn collab_db(&self, uid: i64) -> Result<Weak<CollabKVDB>, FlowyError>;
fn workspace_id(&self) -> Result<String, FlowyError>;
fn workspace_database_object_id(&self) -> Result<String, FlowyError>;
fn workspace_id(&self) -> Result<Uuid, FlowyError>;
fn workspace_database_object_id(&self) -> Result<Uuid, FlowyError>;
}
pub(crate) type DatabaseEditorMap = HashMap<String, Arc<DatabaseEditor>>;
@@ -110,7 +112,7 @@ impl DatabaseManager {
let workspace_database_object_id = self.user.workspace_database_object_id()?;
let workspace_database_collab = collab_service
.build_collab(
workspace_database_object_id.as_str(),
workspace_database_object_id.to_string().as_str(),
CollabType::WorkspaceDatabase,
None,
)
@@ -189,8 +191,10 @@ impl DatabaseManager {
})
}
pub async fn encode_database(&self, view_id: &str) -> FlowyResult<EncodedDatabase> {
let editor = self.get_database_editor_with_view_id(view_id).await?;
pub async fn encode_database(&self, view_id: &Uuid) -> FlowyResult<EncodedDatabase> {
let editor = self
.get_database_editor_with_view_id(view_id.to_string().as_str())
.await?;
let collabs = editor
.database
.read()
@@ -207,10 +211,12 @@ impl DatabaseManager {
pub async fn get_database_row_metas_with_view_id(
&self,
view_id: &str,
view_id: &Uuid,
row_ids: Vec<RowId>,
) -> FlowyResult<Vec<RowMetaPB>> {
let database = self.get_database_editor_with_view_id(view_id).await?;
let database = self
.get_database_editor_with_view_id(view_id.to_string().as_str())
.await?;
let view_id = view_id.to_string();
let mut row_metas: Vec<RowMetaPB> = vec![];
for row_id in row_ids {
@@ -275,11 +281,11 @@ impl DatabaseManager {
/// Open the database view
#[instrument(level = "trace", skip_all, err)]
pub async fn open_database_view<T: AsRef<str>>(&self, view_id: T) -> FlowyResult<()> {
let view_id = view_id.as_ref();
pub async fn open_database_view(&self, view_id: &Uuid) -> FlowyResult<()> {
let view_id = view_id.to_string();
let lock = self.workspace_database()?;
let workspace_database = lock.read().await;
let result = workspace_database.get_database_id_with_view_id(view_id);
let result = workspace_database.get_database_id_with_view_id(&view_id);
drop(workspace_database);
if let Some(database_id) = result {
@@ -292,8 +298,7 @@ impl DatabaseManager {
}
#[instrument(level = "trace", skip_all, err)]
pub async fn close_database_view<T: AsRef<str>>(&self, view_id: T) -> FlowyResult<()> {
let view_id = view_id.as_ref();
pub async fn close_database_view(&self, view_id: &str) -> FlowyResult<()> {
let lock = self.workspace_database()?;
let workspace_database = lock.read().await;
let database_id = workspace_database.get_database_id_with_view_id(view_id);
@@ -518,7 +523,9 @@ impl DatabaseManager {
layout: DatabaseLayoutPB,
) -> FlowyResult<()> {
let database = self.get_database_editor_with_view_id(view_id).await?;
database.update_view_layout(view_id, layout.into()).await
database
.update_view_layout(view_id.to_string().as_str(), layout.into())
.await
}
pub async fn get_database_snapshots(
@@ -526,7 +533,7 @@ impl DatabaseManager {
view_id: &str,
limit: usize,
) -> FlowyResult<Vec<DatabaseSnapshotPB>> {
let database_id = self.get_database_id_with_view_id(view_id).await?;
let database_id = Uuid::from_str(&self.get_database_id_with_view_id(view_id).await?)?;
let snapshots = self
.cloud_service
.get_database_collab_object_snapshots(&database_id, limit)
@@ -553,14 +560,14 @@ impl DatabaseManager {
#[instrument(level = "debug", skip_all)]
pub async fn summarize_row(
&self,
view_id: String,
view_id: &str,
row_id: RowId,
field_id: String,
) -> FlowyResult<()> {
let database = self.get_database_editor_with_view_id(&view_id).await?;
let database = self.get_database_editor_with_view_id(view_id).await?;
let mut summary_row_content = SummaryRowContent::new();
if let Some(row) = database.get_row(&view_id, &row_id).await {
let fields = database.get_fields(&view_id, None).await;
if let Some(row) = database.get_row(view_id, &row_id).await {
let fields = database.get_fields(view_id, None).await;
for field in fields {
// When summarizing a row, skip the content in the "AI summary" cell; it does not need to
// be summarized.
@@ -583,13 +590,17 @@ impl DatabaseManager {
);
let response = self
.ai_service
.summary_database_row(&self.user.workspace_id()?, &row_id, summary_row_content)
.summary_database_row(
&self.user.workspace_id()?,
&Uuid::from_str(&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))
.update_cell_with_changeset(view_id, &row_id, &field_id, BoxAny::new(response))
.await?;
Ok(())
}
@@ -597,11 +608,12 @@ impl DatabaseManager {
#[instrument(level = "debug", skip_all)]
pub async fn translate_row(
&self,
view_id: String,
view_id: &str,
row_id: RowId,
field_id: String,
) -> FlowyResult<()> {
let database = self.get_database_editor_with_view_id(&view_id).await?;
let database = self.get_database_editor_with_view_id(view_id).await?;
let view_id = view_id.to_string();
let mut translate_row_content = TranslateRowContent::new();
let mut language = "english".to_string();
@@ -703,10 +715,13 @@ impl WorkspaceDatabaseCollabServiceImpl {
async fn get_encode_collab(
&self,
object_id: &str,
object_id: &Uuid,
object_ty: CollabType,
) -> Result<Option<EncodedCollab>, DatabaseError> {
let workspace_id = self.user.workspace_id().unwrap();
let workspace_id = self
.user
.workspace_id()
.map_err(|e| DatabaseError::Internal(e.into()))?;
trace!("[Database]: fetch {}:{} from remote", object_id, object_ty);
let encode_collab = self
.cloud_service
@@ -718,7 +733,7 @@ impl WorkspaceDatabaseCollabServiceImpl {
async fn batch_get_encode_collab(
&self,
object_ids: Vec<String>,
object_ids: Vec<Uuid>,
object_ty: CollabType,
) -> Result<EncodeCollabByOid, DatabaseError> {
let workspace_id = self
@@ -730,7 +745,13 @@ impl WorkspaceDatabaseCollabServiceImpl {
.batch_get_database_encode_collab(object_ids, object_ty, &workspace_id)
.await
.map_err(|err| DatabaseError::Internal(err.into()))?;
Ok(updates)
Ok(
updates
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect(),
)
}
fn collab_db(&self) -> Result<Weak<CollabKVDB>, DatabaseError> {
@@ -746,7 +767,7 @@ impl WorkspaceDatabaseCollabServiceImpl {
fn build_collab_object(
&self,
object_id: &str,
object_id: &Uuid,
object_type: CollabType,
) -> Result<CollabObject, DatabaseError> {
let uid = self
@@ -776,8 +797,12 @@ impl DatabaseCollabService for WorkspaceDatabaseCollabServiceImpl {
collab_type: CollabType,
encoded_collab: Option<(EncodedCollab, bool)>,
) -> Result<Collab, DatabaseError> {
let object = self.build_collab_object(object_id, collab_type)?;
let data_source = if self.persistence.is_collab_exist(object_id) {
let object_id = Uuid::parse_str(object_id)?;
let object = self.build_collab_object(&object_id, collab_type)?;
let data_source = if self
.persistence
.is_collab_exist(object_id.to_string().as_str())
{
trace!(
"build collab: {}:{} from local encode collab",
collab_type,
@@ -796,7 +821,7 @@ impl DatabaseCollabService for WorkspaceDatabaseCollabServiceImpl {
object_id,
encoded_collab.is_none(),
);
match self.get_encode_collab(object_id, collab_type).await {
match self.get_encode_collab(&object_id, collab_type).await {
Ok(Some(encode_collab)) => {
info!(
"build collab: {}:{} with remote encode collab, {} bytes",
@@ -837,12 +862,11 @@ impl DatabaseCollabService for WorkspaceDatabaseCollabServiceImpl {
);
self
.persistence
.save_collab(object_id, encoded_collab.clone())?;
.save_collab(object_id.to_string().as_str(), encoded_collab.clone())?;
// TODO(nathan): cover database rows and other database collab type
if matches!(collab_type, CollabType::Database) {
if let Ok(workspace_id) = self.user.workspace_id() {
let object_id = object_id.to_string();
let cloned_encoded_collab = encoded_collab.clone();
let cloud_service = self.cloud_service.clone();
tokio::spawn(async move {
@@ -878,6 +902,7 @@ impl DatabaseCollabService for WorkspaceDatabaseCollabServiceImpl {
if object_ids.is_empty() {
return Ok(EncodeCollabByOid::new());
}
let mut encoded_collab_by_id = EncodeCollabByOid::new();
// 1. Collect local disk collabs into a HashMap
let local_disk_encoded_collab: HashMap<String, EncodedCollab> = object_ids
@@ -900,6 +925,10 @@ impl DatabaseCollabService for WorkspaceDatabaseCollabServiceImpl {
}
if !object_ids.is_empty() {
let object_ids = object_ids
.into_iter()
.flat_map(|v| Uuid::from_str(&v).ok())
.collect::<Vec<_>>();
// 2. Fetch remaining collabs from remote
let remote_collabs = self
.batch_get_encode_collab(object_ids, collab_type)
@@ -927,7 +956,7 @@ pub struct DatabasePersistenceImpl {
}
impl DatabasePersistenceImpl {
fn workspace_id(&self) -> Result<String, DatabaseError> {
fn workspace_id(&self) -> Result<Uuid, DatabaseError> {
let workspace_id = self
.user
.workspace_id()
@@ -947,7 +976,7 @@ impl DatabaseCollabPersistenceService for DatabasePersistenceImpl {
if let Ok((uid, Ok(Some(collab_db)))) = result {
let object_id = collab.object_id().to_string();
let db_read = collab_db.read_txn();
if !db_read.is_exist(uid, &workspace_id, &object_id) {
if !db_read.is_exist(uid, workspace_id.to_string().as_str(), &object_id) {
trace!(
"[Database]: collab:{} not exist in local storage",
object_id
@@ -957,7 +986,12 @@ impl DatabaseCollabPersistenceService for DatabasePersistenceImpl {
trace!("[Database]: start loading collab:{} from disk", object_id);
let mut txn = collab.transact_mut();
match db_read.load_doc_with_txn(uid, &workspace_id, &object_id, &mut txn) {
match db_read.load_doc_with_txn(
uid,
workspace_id.to_string().as_str(),
&object_id,
&mut txn,
) {
Ok(update_count) => {
trace!(
"[Database]: did load collab:{}, update_count:{}",
@@ -976,7 +1010,7 @@ impl DatabaseCollabPersistenceService for DatabasePersistenceImpl {
}
fn get_encoded_collab(&self, object_id: &str, collab_type: CollabType) -> Option<EncodedCollab> {
let workspace_id = self.user.workspace_id().ok()?;
let workspace_id = self.user.workspace_id().ok()?.to_string();
let uid = self.user.user_id().ok()?;
let db = self.user.collab_db(uid).ok()?.upgrade()?;
let read_txn = db.read_txn();
@@ -995,7 +1029,7 @@ impl DatabaseCollabPersistenceService for DatabasePersistenceImpl {
}
fn delete_collab(&self, object_id: &str) -> Result<(), DatabaseError> {
let workspace_id = self.workspace_id()?;
let workspace_id = self.workspace_id()?.to_string();
let uid = self
.user
.user_id()
@@ -1017,7 +1051,7 @@ impl DatabaseCollabPersistenceService for DatabasePersistenceImpl {
object_id: &str,
encoded_collab: EncodedCollab,
) -> Result<(), DatabaseError> {
let workspace_id = self.workspace_id()?;
let workspace_id = self.workspace_id()?.to_string();
let uid = self
.user
.user_id()
@@ -1051,7 +1085,7 @@ impl DatabaseCollabPersistenceService for DatabasePersistenceImpl {
Ok(uid) => {
if let Ok(Some(collab_db)) = self.user.collab_db(uid).map(|weak| weak.upgrade()) {
let read_txn = collab_db.read_txn();
return read_txn.is_exist(uid, workspace_id.as_str(), object_id);
return read_txn.is_exist(uid, workspace_id.to_string().as_str(), object_id);
}
false
},
@@ -1073,7 +1107,8 @@ impl DatabaseCollabPersistenceService for DatabasePersistenceImpl {
let workspace_id = self
.user
.workspace_id()
.map_err(|err| DatabaseError::Internal(err.into()))?;
.map_err(|err| DatabaseError::Internal(err.into()))?
.to_string();
if let Ok(Some(collab_db)) = self.user.collab_db(uid).map(|weak| weak.upgrade()) {
let write_txn = collab_db.write_txn();
@@ -44,6 +44,7 @@ use lib_infra::box_any::BoxAny;
use lib_infra::priority_task::TaskDispatcher;
use lib_infra::util::timestamp;
use std::collections::HashMap;
use std::str::FromStr;
use std::sync::{Arc, Weak};
use std::time::Duration;
use tokio::select;
@@ -53,11 +54,12 @@ use tokio::sync::{broadcast, oneshot};
use tokio::task::yield_now;
use tokio_util::sync::CancellationToken;
use tracing::{debug, error, event, info, instrument, trace, warn};
use uuid::Uuid;
type OpenDatabaseResult = oneshot::Sender<FlowyResult<DatabasePB>>;
pub struct DatabaseEditor {
database_id: String,
database_id: Uuid,
pub(crate) database: Arc<RwLock<Database>>,
pub cell_cache: CellCache,
pub(crate) database_views: Arc<DatabaseViews>,
@@ -117,6 +119,7 @@ impl DatabaseEditor {
.await?,
);
let database_id = Uuid::from_str(&database_id)?;
let collab_object = collab_builder.collab_object(
&user.workspace_id()?,
user.user_id()?,
@@ -130,7 +133,7 @@ impl DatabaseEditor {
database.clone(),
)?;
let this = Arc::new(Self {
database_id: database_id.clone(),
database_id,
user,
database,
cell_cache,
@@ -806,10 +809,11 @@ impl DatabaseEditor {
let is_finalized = self.finalized_rows.get(row_id.as_str()).await.is_some();
if !is_finalized {
trace!("[Database]: finalize database row: {}", row_id);
let row_id = Uuid::from_str(row_id.as_str())?;
let collab_object = self.collab_builder.collab_object(
&self.user.workspace_id()?,
self.user.user_id()?,
row_id,
&row_id,
CollabType::DatabaseRow,
)?;
@@ -16,6 +16,7 @@ use futures::StreamExt;
use std::sync::Arc;
use tracing::{error, trace, warn};
use uuid::Uuid;
pub(crate) async fn observe_sync_state(database_id: &str, database: &Arc<RwLock<Database>>) {
let weak_database = Arc::downgrade(database);
@@ -112,7 +113,7 @@ pub(crate) async fn observe_field_change(database_id: &str, database: &Arc<RwLoc
}
#[allow(dead_code)]
pub(crate) async fn observe_view_change(database_id: &str, database_editor: &Arc<DatabaseEditor>) {
pub(crate) async fn observe_view_change(database_id: &Uuid, database_editor: &Arc<DatabaseEditor>) {
let database_id = database_id.to_string();
let weak_database_editor = Arc::downgrade(database_editor);
let view_change = database_editor
@@ -289,7 +290,7 @@ async fn handle_did_update_row_orders(
}
}
pub(crate) async fn observe_block_event(database_id: &str, database_editor: &Arc<DatabaseEditor>) {
pub(crate) async fn observe_block_event(database_id: &Uuid, database_editor: &Arc<DatabaseEditor>) {
let database_id = database_id.to_string();
let mut block_event_rx = database_editor
.database
@@ -9,5 +9,5 @@ edition = "2021"
lib-infra = { workspace = true }
flowy-error = { workspace = true }
collab-document = { workspace = true }
anyhow.workspace = true
collab = { workspace = true }
collab = { workspace = true }
uuid.workspace = true
@@ -1,8 +1,8 @@
use collab::entity::EncodedCollab;
pub use collab_document::blocks::DocumentData;
use flowy_error::FlowyError;
use lib_infra::async_trait::async_trait;
use uuid::Uuid;
/// A trait for document cloud service.
/// Each kind of server should implement this trait. Check out the [AppFlowyServerProvider] of
@@ -11,27 +11,27 @@ use lib_infra::async_trait::async_trait;
pub trait DocumentCloudService: Send + Sync + 'static {
async fn get_document_doc_state(
&self,
document_id: &str,
workspace_id: &str,
document_id: &Uuid,
workspace_id: &Uuid,
) -> Result<Vec<u8>, FlowyError>;
async fn get_document_snapshots(
&self,
document_id: &str,
document_id: &Uuid,
limit: usize,
workspace_id: &str,
) -> Result<Vec<DocumentSnapshot>, FlowyError>;
async fn get_document_data(
&self,
document_id: &str,
workspace_id: &str,
document_id: &Uuid,
workspace_id: &Uuid,
) -> Result<Option<DocumentData>, FlowyError>;
async fn create_document_collab(
&self,
workspace_id: &str,
document_id: &str,
workspace_id: &Uuid,
document_id: &Uuid,
encoded_collab: EncodedCollab,
) -> Result<(), FlowyError>;
}
@@ -6,9 +6,10 @@ use collab::preclude::Collab;
use collab_document::document::Document;
use futures::StreamExt;
use lib_infra::sync_trace;
use uuid::Uuid;
pub fn subscribe_document_changed(doc_id: &str, document: &mut Document) {
let doc_id_clone_for_block_changed = doc_id.to_owned();
pub fn subscribe_document_changed(doc_id: &Uuid, document: &mut Document) {
let doc_id_clone_for_block_changed = doc_id.to_string();
document.subscribe_block_changed("key", move |events, is_remote| {
sync_trace!(
"[Document] block changed in doc_id: {}, is_remote: {}, events: {:?}",
@@ -35,7 +36,7 @@ pub fn subscribe_document_changed(doc_id: &str, document: &mut Document) {
);
document_notification_builder(
&doc_id_clone_for_awareness_state,
&doc_id_clone_for_awareness_state.to_string(),
DocumentNotification::DidUpdateDocumentAwarenessState,
)
.payload::<DocumentAwarenessStatesPB>(events.into())
@@ -1,5 +1,3 @@
use std::collections::HashMap;
use collab::core::collab_state::SyncState;
use collab_document::{
blocks::{json_str_to_hashmap, Block, BlockAction, DocumentData},
@@ -8,10 +6,12 @@ use collab_document::{
DocumentAwarenessUser,
},
};
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::ErrorCode;
use lib_infra::validator_fn::{required_not_empty_str, required_valid_path};
use std::collections::HashMap;
use std::str::FromStr;
use uuid::Uuid;
use validator::Validate;
use crate::parse::{NotEmptyStr, NotEmptyVec};
@@ -31,7 +31,7 @@ pub struct OpenDocumentPayloadPB {
}
pub struct OpenDocumentParams {
pub document_id: String,
pub document_id: Uuid,
}
impl TryInto<OpenDocumentParams> for OpenDocumentPayloadPB {
@@ -39,9 +39,9 @@ impl TryInto<OpenDocumentParams> for OpenDocumentPayloadPB {
fn try_into(self) -> Result<OpenDocumentParams, Self::Error> {
let document_id =
NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?;
Ok(OpenDocumentParams {
document_id: document_id.0,
})
let document_id = Uuid::from_str(&document_id.0).map_err(|_| ErrorCode::InvalidParams)?;
Ok(OpenDocumentParams { document_id })
}
}
@@ -52,7 +52,7 @@ pub struct DocumentRedoUndoPayloadPB {
}
pub struct DocumentRedoUndoParams {
pub document_id: String,
pub document_id: Uuid,
}
impl TryInto<DocumentRedoUndoParams> for DocumentRedoUndoPayloadPB {
@@ -60,9 +60,8 @@ impl TryInto<DocumentRedoUndoParams> for DocumentRedoUndoPayloadPB {
fn try_into(self) -> Result<DocumentRedoUndoParams, Self::Error> {
let document_id =
NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?;
Ok(DocumentRedoUndoParams {
document_id: document_id.0,
})
let document_id = Uuid::from_str(&document_id.0).map_err(|_| ErrorCode::InvalidParams)?;
Ok(DocumentRedoUndoParams { document_id })
}
}
@@ -132,7 +131,7 @@ pub struct CreateDocumentPayloadPB {
}
pub struct CreateDocumentParams {
pub document_id: String,
pub document_id: Uuid,
pub initial_data: Option<DocumentData>,
}
@@ -141,9 +140,10 @@ impl TryInto<CreateDocumentParams> for CreateDocumentPayloadPB {
fn try_into(self) -> Result<CreateDocumentParams, Self::Error> {
let document_id =
NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?;
let document_id = Uuid::from_str(&document_id.0).map_err(|_| ErrorCode::InvalidParams)?;
let initial_data = self.initial_data.map(|data| data.into());
Ok(CreateDocumentParams {
document_id: document_id.0,
document_id,
initial_data,
})
}
@@ -156,7 +156,7 @@ pub struct CloseDocumentPayloadPB {
}
pub struct CloseDocumentParams {
pub document_id: String,
pub document_id: Uuid,
}
impl TryInto<CloseDocumentParams> for CloseDocumentPayloadPB {
@@ -164,9 +164,8 @@ impl TryInto<CloseDocumentParams> for CloseDocumentPayloadPB {
fn try_into(self) -> Result<CloseDocumentParams, Self::Error> {
let document_id =
NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?;
Ok(CloseDocumentParams {
document_id: document_id.0,
})
let document_id = Uuid::from_str(&document_id.0).map_err(|_| ErrorCode::InvalidParams)?;
Ok(CloseDocumentParams { document_id })
}
}
@@ -180,7 +179,7 @@ pub struct ApplyActionPayloadPB {
}
pub struct ApplyActionParams {
pub document_id: String,
pub document_id: Uuid,
pub actions: Vec<BlockAction>,
}
@@ -189,10 +188,11 @@ impl TryInto<ApplyActionParams> for ApplyActionPayloadPB {
fn try_into(self) -> Result<ApplyActionParams, Self::Error> {
let document_id =
NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?;
let document_id = Uuid::from_str(&document_id.0).map_err(|_| ErrorCode::InvalidParams)?;
let actions = NotEmptyVec::parse(self.actions).map_err(|_| ErrorCode::ApplyActionsIsEmpty)?;
let actions = actions.0.into_iter().map(BlockAction::from).collect();
Ok(ApplyActionParams {
document_id: document_id.0,
document_id,
actions,
})
}
@@ -525,7 +525,7 @@ pub struct TextDeltaPayloadPB {
}
pub struct TextDeltaParams {
pub document_id: String,
pub document_id: Uuid,
pub text_id: String,
pub delta: String,
}
@@ -535,10 +535,11 @@ impl TryInto<TextDeltaParams> for TextDeltaPayloadPB {
fn try_into(self) -> Result<TextDeltaParams, Self::Error> {
let document_id =
NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?;
let document_id = Uuid::from_str(&document_id.0).map_err(|_| ErrorCode::InvalidParams)?;
let text_id = NotEmptyStr::parse(self.text_id).map_err(|_| ErrorCode::TextIdIsEmpty)?;
let delta = self.delta.map_or_else(|| "".to_string(), |delta| delta);
Ok(TextDeltaParams {
document_id: document_id.0,
document_id,
text_id: text_id.0,
delta,
})
@@ -3,7 +3,7 @@
* as well as performing actions on documents. These functions make use of a DocumentManager,
* which you can think of as a higher-level interface to interact with documents.
*/
use std::str::FromStr;
use std::sync::{Arc, Weak};
use collab_document::blocks::{
@@ -11,10 +11,6 @@ use collab_document::blocks::{
DocumentData,
};
use flowy_error::{FlowyError, FlowyResult};
use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult};
use tracing::instrument;
use crate::entities::*;
use crate::parser::document_data_parser::DocumentDataParser;
use crate::parser::external::parser::ExternalDataToNestedJSONParser;
@@ -23,7 +19,11 @@ use crate::parser::parser_entities::{
ConvertDocumentParams, ConvertDocumentPayloadPB, ConvertDocumentResponsePB,
};
use crate::{manager::DocumentManager, parser::json::parser::JsonToDocumentParser};
use flowy_error::{FlowyError, FlowyResult};
use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult};
use lib_infra::sync_trace;
use tracing::instrument;
use uuid::Uuid;
fn upgrade_document(
document_manager: AFPluginState<Weak<DocumentManager>>,
) -> FlowyResult<Arc<DocumentManager>> {
@@ -124,9 +124,7 @@ pub(crate) async fn apply_action_handler(
let doc_id = params.document_id;
let document = manager.editable_document(&doc_id).await?;
let actions = params.actions;
if cfg!(feature = "verbose_log") {
tracing::trace!("{} applying actions: {:?}", doc_id, actions);
}
sync_trace!("{} applying action: {:?}", doc_id, actions);
document.write().await.apply_action(actions)?;
Ok(())
}
@@ -141,6 +139,7 @@ pub(crate) async fn create_text_handler(
let doc_id = params.document_id;
let document = manager.editable_document(&doc_id).await?;
let mut document = document.write().await;
sync_trace!("{} creating text: {:?}", doc_id, params.delta);
document.apply_text_delta(&params.text_id, params.delta);
Ok(())
}
@@ -157,9 +156,7 @@ pub(crate) async fn apply_text_delta_handler(
let text_id = params.text_id;
let delta = params.delta;
let mut document = document.write().await;
if cfg!(feature = "verbose_log") {
tracing::trace!("{} applying delta: {:?}", doc_id, delta);
}
sync_trace!("{} applying delta: {:?}", doc_id, delta);
document.apply_text_delta(&text_id, delta);
Ok(())
}
@@ -499,7 +496,7 @@ pub(crate) async fn set_awareness_local_state_handler(
) -> FlowyResult<()> {
let manager = upgrade_document(manager)?;
let data = data.into_inner();
let doc_id = data.document_id.clone();
let doc_id = Uuid::from_str(&data.document_id)?;
manager
.set_document_awareness_local_state(&doc_id, data)
.await?;
+44 -35
View File
@@ -14,21 +14,21 @@ use collab_document::document_awareness::DocumentAwarenessUser;
use collab_document::document_data::default_document_data;
use collab_entity::CollabType;
use collab_plugins::CollabKVDB;
use dashmap::DashMap;
use lib_infra::util::timestamp;
use tracing::{event, instrument};
use tracing::{info, trace};
use crate::document::{
subscribe_document_changed, subscribe_document_snapshot_state, subscribe_document_sync_state,
};
use collab_integrate::collab_builder::{
AppFlowyCollabBuilder, CollabBuilderConfig, CollabPersistenceImpl,
};
use collab_plugins::CollabKVDB;
use dashmap::DashMap;
use flowy_document_pub::cloud::DocumentCloudService;
use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult};
use flowy_storage_pub::storage::{CreatedUpload, StorageService};
use lib_infra::util::timestamp;
use tracing::{event, instrument};
use tracing::{info, trace};
use uuid::Uuid;
use crate::entities::UpdateDocumentAwarenessStatePB;
use crate::entities::{
@@ -39,7 +39,7 @@ use crate::reminder::DocumentReminderAction;
pub trait DocumentUserService: Send + Sync {
fn user_id(&self) -> Result<i64, FlowyError>;
fn device_id(&self) -> Result<String, FlowyError>;
fn workspace_id(&self) -> Result<String, FlowyError>;
fn workspace_id(&self) -> Result<Uuid, FlowyError>;
fn collab_db(&self, uid: i64) -> Result<Weak<CollabKVDB>, FlowyError>;
}
@@ -54,8 +54,8 @@ pub trait DocumentSnapshotService: Send + Sync {
pub struct DocumentManager {
pub user_service: Arc<dyn DocumentUserService>,
collab_builder: Arc<AppFlowyCollabBuilder>,
documents: Arc<DashMap<String, Arc<RwLock<Document>>>>,
removing_documents: Arc<DashMap<String, Arc<RwLock<Document>>>>,
documents: Arc<DashMap<Uuid, Arc<RwLock<Document>>>>,
removing_documents: Arc<DashMap<Uuid, Arc<RwLock<Document>>>>,
cloud_service: Arc<dyn DocumentCloudService>,
storage_service: Weak<dyn StorageService>,
snapshot_service: Arc<dyn DocumentSnapshotService>,
@@ -81,7 +81,7 @@ impl DocumentManager {
}
/// Get the encoded collab of the document.
pub async fn get_encoded_collab_with_view_id(&self, doc_id: &str) -> FlowyResult<EncodedCollab> {
pub async fn get_encoded_collab_with_view_id(&self, doc_id: &Uuid) -> FlowyResult<EncodedCollab> {
let uid = self.user_service.user_id()?;
let workspace_id = self.user_service.workspace_id()?;
let doc_state =
@@ -139,7 +139,7 @@ impl DocumentManager {
pub async fn create_document(
&self,
_uid: i64,
doc_id: &str,
doc_id: &Uuid,
data: Option<DocumentData>,
) -> FlowyResult<EncodedCollab> {
if self.is_doc_exist(doc_id).await.unwrap_or(false) {
@@ -151,17 +151,17 @@ impl DocumentManager {
let encoded_collab = doc_state_from_document_data(doc_id, data).await?;
self
.persistence()?
.save_collab_to_disk(doc_id, encoded_collab.clone())
.save_collab_to_disk(doc_id.to_string().as_str(), encoded_collab.clone())
.map_err(internal_error)?;
// Send the collab data to server with a background task.
let cloud_service = self.cloud_service.clone();
let cloned_encoded_collab = encoded_collab.clone();
let document_id = doc_id.to_string();
let workspace_id = self.user_service.workspace_id()?;
let doc_id = *doc_id;
tokio::spawn(async move {
let _ = cloud_service
.create_document_collab(&workspace_id, &document_id, cloned_encoded_collab)
.create_document_collab(&workspace_id, &doc_id, cloned_encoded_collab)
.await;
});
Ok(encoded_collab)
@@ -171,7 +171,7 @@ impl DocumentManager {
async fn collab_for_document(
&self,
uid: i64,
doc_id: &str,
doc_id: &Uuid,
data_source: DataSource,
sync_enable: bool,
) -> FlowyResult<Arc<RwLock<Document>>> {
@@ -195,7 +195,7 @@ impl DocumentManager {
}
/// Return a document instance if the document is already opened.
pub async fn editable_document(&self, doc_id: &str) -> FlowyResult<Arc<RwLock<Document>>> {
pub async fn editable_document(&self, doc_id: &Uuid) -> FlowyResult<Arc<RwLock<Document>>> {
if let Some(doc) = self.documents.get(doc_id).map(|item| item.value().clone()) {
return Ok(doc);
}
@@ -213,7 +213,7 @@ impl DocumentManager {
#[tracing::instrument(level = "info", skip(self), err)]
async fn create_document_instance(
&self,
doc_id: &str,
doc_id: &Uuid,
enable_sync: bool,
) -> FlowyResult<Arc<RwLock<Document>>> {
let uid = self.user_service.user_id()?;
@@ -260,7 +260,7 @@ impl DocumentManager {
subscribe_document_snapshot_state(&lock);
subscribe_document_sync_state(&lock);
}
self.documents.insert(doc_id.to_string(), document.clone());
self.documents.insert(*doc_id, document.clone());
}
Ok(document)
},
@@ -273,21 +273,21 @@ impl DocumentManager {
}
}
pub async fn get_document_data(&self, doc_id: &str) -> FlowyResult<DocumentData> {
pub async fn get_document_data(&self, doc_id: &Uuid) -> FlowyResult<DocumentData> {
let document = self.get_document(doc_id).await?;
let document = document.read().await;
document.get_document_data().map_err(internal_error)
}
pub async fn get_document_text(&self, doc_id: &str) -> FlowyResult<String> {
pub async fn get_document_text(&self, doc_id: &Uuid) -> FlowyResult<String> {
let document = self.get_document(doc_id).await?;
let document = document.read().await;
let text = document.to_plain_text(true, false)?;
let text = document.paragraphs().join("\n");
Ok(text)
}
/// Return a document instance.
/// The returned document might or might not be able to sync with the cloud.
async fn get_document(&self, doc_id: &str) -> FlowyResult<Arc<RwLock<Document>>> {
async fn get_document(&self, doc_id: &Uuid) -> FlowyResult<Arc<RwLock<Document>>> {
if let Some(doc) = self.documents.get(doc_id).map(|item| item.value().clone()) {
return Ok(doc);
}
@@ -300,7 +300,7 @@ impl DocumentManager {
Ok(document)
}
pub async fn open_document(&self, doc_id: &str) -> FlowyResult<()> {
pub async fn open_document(&self, doc_id: &Uuid) -> FlowyResult<()> {
if let Some(mutex_document) = self.restore_document_from_removing(doc_id) {
let lock = mutex_document.read().await;
lock.start_init_sync();
@@ -314,7 +314,7 @@ impl DocumentManager {
Ok(())
}
pub async fn close_document(&self, doc_id: &str) -> FlowyResult<()> {
pub async fn close_document(&self, doc_id: &Uuid) -> FlowyResult<()> {
if let Some((doc_id, document)) = self.documents.remove(doc_id) {
{
// clear the awareness state when close the document
@@ -322,7 +322,7 @@ impl DocumentManager {
lock.clean_awareness_local_state();
}
let clone_doc_id = doc_id.clone();
let clone_doc_id = doc_id;
trace!("move document to removing_documents: {}", doc_id);
self.removing_documents.insert(doc_id, document);
@@ -340,11 +340,12 @@ impl DocumentManager {
Ok(())
}
pub async fn delete_document(&self, doc_id: &str) -> FlowyResult<()> {
pub async fn delete_document(&self, doc_id: &Uuid) -> FlowyResult<()> {
let uid = self.user_service.user_id()?;
let workspace_id = self.user_service.workspace_id()?;
if let Some(db) = self.user_service.collab_db(uid)?.upgrade() {
db.delete_doc(uid, &workspace_id, doc_id).await?;
db.delete_doc(uid, &workspace_id.to_string(), &doc_id.to_string())
.await?;
// When deleting a document, we need to remove it from the cache.
self.documents.remove(doc_id);
}
@@ -354,7 +355,7 @@ impl DocumentManager {
#[instrument(level = "debug", skip_all, err)]
pub async fn set_document_awareness_local_state(
&self,
doc_id: &str,
doc_id: &Uuid,
state: UpdateDocumentAwarenessStatePB,
) -> FlowyResult<bool> {
let uid = self.user_service.user_id()?;
@@ -379,12 +380,12 @@ impl DocumentManager {
/// Return the list of snapshots of the document.
pub async fn get_document_snapshot_meta(
&self,
document_id: &str,
document_id: &Uuid,
_limit: usize,
) -> FlowyResult<Vec<DocumentSnapshotMetaPB>> {
let metas = self
.snapshot_service
.get_document_snapshot_metas(document_id)?
.get_document_snapshot_metas(document_id.to_string().as_str())?
.into_iter()
.map(|meta| DocumentSnapshotMetaPB {
snapshot_id: meta.snapshot_id,
@@ -434,11 +435,13 @@ impl DocumentManager {
Ok(())
}
async fn is_doc_exist(&self, doc_id: &str) -> FlowyResult<bool> {
async fn is_doc_exist(&self, doc_id: &Uuid) -> FlowyResult<bool> {
let uid = self.user_service.user_id()?;
let workspace_id = self.user_service.workspace_id()?;
if let Some(collab_db) = self.user_service.collab_db(uid)?.upgrade() {
let is_exist = collab_db.is_exist(uid, &workspace_id, doc_id).await?;
let is_exist = collab_db
.is_exist(uid, &workspace_id.to_string(), &doc_id.to_string())
.await?;
Ok(is_exist)
} else {
Ok(false)
@@ -463,7 +466,7 @@ impl DocumentManager {
&self.storage_service
}
fn restore_document_from_removing(&self, doc_id: &str) -> Option<Arc<RwLock<Document>>> {
fn restore_document_from_removing(&self, doc_id: &Uuid) -> Option<Arc<RwLock<Document>>> {
let (doc_id, doc) = self.removing_documents.remove(doc_id)?;
trace!(
"move document {} from removing_documents to documents",
@@ -475,11 +478,17 @@ impl DocumentManager {
}
async fn doc_state_from_document_data(
doc_id: &str,
doc_id: &Uuid,
data: Option<DocumentData>,
) -> Result<EncodedCollab, FlowyError> {
let doc_id = doc_id.to_string();
let data = data.unwrap_or_else(|| default_document_data(&doc_id));
let data = data.unwrap_or_else(|| {
trace!(
"{} document data is None, use default document data",
doc_id.to_string()
);
default_document_data(&doc_id)
});
// spawn_blocking is used to avoid blocking the tokio thread pool if the document is large.
let encoded_collab = tokio::task::spawn_blocking(move || {
let collab = Collab::new_with_origin(CollabOrigin::Empty, doc_id, vec![], false);
@@ -10,6 +10,7 @@ use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
use uuid::Uuid;
use validator::Validate;
#[derive(Default, ProtoBuf)]
@@ -96,7 +97,7 @@ pub struct ParseType {
}
pub struct ConvertDocumentParams {
pub document_id: String,
pub document_id: Uuid,
pub range: Option<Range>,
pub parse_types: ParseType,
}
@@ -140,10 +141,11 @@ impl TryInto<ConvertDocumentParams> for ConvertDocumentPayloadPB {
fn try_into(self) -> Result<ConvertDocumentParams, Self::Error> {
let document_id =
NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?;
let document_id = Uuid::parse_str(&document_id.0).map_err(|_| ErrorCode::InvalidParams)?;
let range = self.range.map(|data| data.into());
Ok(ConvertDocumentParams {
document_id: document_id.0,
document_id,
range,
parse_types: self.parse_types.into(),
})
@@ -9,8 +9,8 @@ use crate::document::util::{gen_document_id, gen_id, DocumentTest};
async fn undo_redo_test() {
let test = DocumentTest::new();
let doc_id: String = gen_document_id();
let data = default_document_data(&doc_id);
let doc_id = gen_document_id();
let data = default_document_data(&doc_id.to_string());
// create a document
_ = test
@@ -11,8 +11,8 @@ async fn restore_document() {
let test = DocumentTest::new();
// create a document
let doc_id: String = gen_document_id();
let data = default_document_data(&doc_id);
let doc_id = gen_document_id();
let data = default_document_data(&doc_id.to_string());
let uid = test.user_service.user_id().unwrap();
test
.create_document(uid, &doc_id, Some(data.clone()))
@@ -55,8 +55,8 @@ async fn restore_document() {
async fn document_apply_insert_action() {
let test = DocumentTest::new();
let uid = test.user_service.user_id().unwrap();
let doc_id: String = gen_document_id();
let data = default_document_data(&doc_id);
let doc_id = gen_document_id();
let data = default_document_data(&doc_id.to_string());
// create a document
_ = test.create_document(uid, &doc_id, Some(data.clone())).await;
@@ -111,9 +111,9 @@ async fn document_apply_insert_action() {
#[tokio::test]
async fn document_apply_update_page_action() {
let test = DocumentTest::new();
let doc_id: String = gen_document_id();
let doc_id = gen_document_id();
let uid = test.user_service.user_id().unwrap();
let data = default_document_data(&doc_id);
let data = default_document_data(&doc_id.to_string());
// create a document
_ = test.create_document(uid, &doc_id, Some(data.clone())).await;
@@ -158,8 +158,8 @@ async fn document_apply_update_page_action() {
async fn document_apply_update_action() {
let test = DocumentTest::new();
let uid = test.user_service.user_id().unwrap();
let doc_id: String = gen_document_id();
let data = default_document_data(&doc_id);
let doc_id = gen_document_id();
let data = default_document_data(&doc_id.to_string());
// create a document
_ = test.create_document(uid, &doc_id, Some(data.clone())).await;
@@ -1,17 +1,11 @@
use std::ops::Deref;
use std::sync::{Arc, OnceLock};
use anyhow::Error;
use collab::entity::EncodedCollab;
use collab::preclude::CollabPlugin;
use collab_document::blocks::DocumentData;
use collab_document::document::Document;
use collab_document::document_data::default_document_data;
use nanoid::nanoid;
use tempfile::TempDir;
use tokio::sync::RwLock;
use tracing_subscriber::{fmt::Subscriber, util::SubscriberInitExt, EnvFilter};
use collab_integrate::collab_builder::{
AppFlowyCollabBuilder, CollabCloudPluginProvider, CollabPluginProviderContext,
CollabPluginProviderType, WorkspaceCollabIntegrate,
@@ -24,6 +18,11 @@ use flowy_error::{ErrorCode, FlowyError, FlowyResult};
use flowy_storage_pub::storage::{CreatedUpload, FileProgressReceiver, StorageService};
use lib_infra::async_trait::async_trait;
use lib_infra::box_any::BoxAny;
use nanoid::nanoid;
use tempfile::TempDir;
use tokio::sync::RwLock;
use tracing_subscriber::{fmt::Subscriber, util::SubscriberInitExt, EnvFilter};
use uuid::Uuid;
pub struct DocumentTest {
inner: DocumentManager,
@@ -39,7 +38,7 @@ impl DocumentTest {
let builder = Arc::new(AppFlowyCollabBuilder::new(
DefaultCollabStorageProvider(),
WorkspaceCollabIntegrateImpl {
workspace_id: user.workspace_id.clone(),
workspace_id: user.workspace_id,
},
));
@@ -63,7 +62,7 @@ impl Deref for DocumentTest {
}
pub struct FakeUser {
workspace_id: String,
workspace_id: Uuid,
collab_db: Arc<CollabKVDB>,
}
@@ -74,7 +73,7 @@ impl FakeUser {
let tempdir = TempDir::new().unwrap();
let path = tempdir.into_path();
let collab_db = Arc::new(CollabKVDB::open(path).unwrap());
let workspace_id = uuid::Uuid::new_v4().to_string();
let workspace_id = uuid::Uuid::new_v4();
Self {
collab_db,
@@ -88,8 +87,8 @@ impl DocumentUserService for FakeUser {
Ok(1)
}
fn workspace_id(&self) -> Result<String, FlowyError> {
Ok(self.workspace_id.clone())
fn workspace_id(&self) -> Result<Uuid, FlowyError> {
Ok(self.workspace_id)
}
fn collab_db(&self, _uid: i64) -> Result<std::sync::Weak<CollabKVDB>, FlowyError> {
@@ -115,8 +114,8 @@ pub fn setup_log() {
pub async fn create_and_open_empty_document() -> (DocumentTest, Arc<RwLock<Document>>, String) {
let test = DocumentTest::new();
let doc_id: String = gen_document_id();
let data = default_document_data(&doc_id);
let doc_id = gen_document_id();
let data = default_document_data(&doc_id.to_string());
let uid = test.user_service.user_id().unwrap();
// create a document
test
@@ -130,9 +129,8 @@ pub async fn create_and_open_empty_document() -> (DocumentTest, Arc<RwLock<Docum
(test, document, data.page_id)
}
pub fn gen_document_id() -> String {
let uuid = uuid::Uuid::new_v4();
uuid.to_string()
pub fn gen_document_id() -> Uuid {
uuid::Uuid::new_v4()
}
pub fn gen_id() -> String {
@@ -145,8 +143,8 @@ pub struct LocalTestDocumentCloudServiceImpl();
impl DocumentCloudService for LocalTestDocumentCloudServiceImpl {
async fn get_document_doc_state(
&self,
document_id: &str,
_workspace_id: &str,
document_id: &Uuid,
_workspace_id: &Uuid,
) -> Result<Vec<u8>, FlowyError> {
let document_id = document_id.to_string();
Err(FlowyError::new(
@@ -157,7 +155,7 @@ impl DocumentCloudService for LocalTestDocumentCloudServiceImpl {
async fn get_document_snapshots(
&self,
_document_id: &str,
_document_id: &Uuid,
_limit: usize,
_workspace_id: &str,
) -> Result<Vec<DocumentSnapshot>, FlowyError> {
@@ -166,16 +164,16 @@ impl DocumentCloudService for LocalTestDocumentCloudServiceImpl {
async fn get_document_data(
&self,
_document_id: &str,
_workspace_id: &str,
_document_id: &Uuid,
_workspace_id: &Uuid,
) -> Result<Option<DocumentData>, FlowyError> {
Ok(None)
}
async fn create_document_collab(
&self,
_workspace_id: &str,
_document_id: &str,
_workspace_id: &Uuid,
_document_id: &Uuid,
_encoded_collab: EncodedCollab,
) -> Result<(), FlowyError> {
Ok(())
@@ -257,14 +255,14 @@ impl DocumentSnapshotService for DocumentTestSnapshot {
}
struct WorkspaceCollabIntegrateImpl {
workspace_id: String,
workspace_id: Uuid,
}
impl WorkspaceCollabIntegrate for WorkspaceCollabIntegrateImpl {
fn workspace_id(&self) -> Result<String, Error> {
Ok(self.workspace_id.clone())
fn workspace_id(&self) -> Result<Uuid, FlowyError> {
Ok(self.workspace_id)
}
fn device_id(&self) -> Result<String, Error> {
fn device_id(&self) -> Result<String, FlowyError> {
Ok("fake_device_id".to_string())
}
}
+1
View File
@@ -34,6 +34,7 @@ collab-plugins = { workspace = true, optional = true }
collab-folder = { workspace = true, optional = true }
client-api = { workspace = true, optional = true }
tantivy = { version = "0.22.0", optional = true }
uuid.workspace = true
[features]
default = ["impl_from_dispatch_error", "impl_from_serde", "impl_from_reqwest", "impl_from_sqlite"]
@@ -256,3 +256,9 @@ impl From<collab::error::CollabError> for FlowyError {
}
}
}
impl From<uuid::Error> for FlowyError {
fn from(value: uuid::Error) -> Self {
FlowyError::internal().with_context(value)
}
}
+20 -20
View File
@@ -15,7 +15,7 @@ pub trait FolderCloudService: Send + Sync + 'static {
/// Returns error if the cloud service doesn't support multiple workspaces
async fn create_workspace(&self, uid: i64, name: &str) -> Result<Workspace, FlowyError>;
async fn open_workspace(&self, workspace_id: &str) -> Result<(), FlowyError>;
async fn open_workspace(&self, workspace_id: &Uuid) -> Result<(), FlowyError>;
/// Returns all workspaces of the user.
/// Returns vec![] if the cloud service doesn't support multiple workspaces
@@ -23,7 +23,7 @@ pub trait FolderCloudService: Send + Sync + 'static {
async fn get_folder_data(
&self,
workspace_id: &str,
workspace_id: &Uuid,
uid: &i64,
) -> Result<Option<FolderData>, FlowyError>;
@@ -35,21 +35,21 @@ pub trait FolderCloudService: Send + Sync + 'static {
async fn get_folder_doc_state(
&self,
workspace_id: &str,
workspace_id: &Uuid,
uid: i64,
collab_type: CollabType,
object_id: &str,
object_id: &Uuid,
) -> Result<Vec<u8>, FlowyError>;
async fn full_sync_collab_object(
&self,
workspace_id: &str,
workspace_id: &Uuid,
params: FullSyncCollabParams,
) -> Result<(), FlowyError>;
async fn batch_create_folder_collab_objects(
&self,
workspace_id: &str,
workspace_id: &Uuid,
objects: Vec<FolderCollabParams>,
) -> Result<(), FlowyError>;
@@ -57,64 +57,64 @@ pub trait FolderCloudService: Send + Sync + 'static {
async fn publish_view(
&self,
workspace_id: &str,
workspace_id: &Uuid,
payload: Vec<PublishPayload>,
) -> Result<(), FlowyError>;
async fn unpublish_views(
&self,
workspace_id: &str,
view_ids: Vec<String>,
workspace_id: &Uuid,
view_ids: Vec<Uuid>,
) -> Result<(), FlowyError>;
async fn get_publish_info(&self, view_id: &str) -> Result<PublishInfo, FlowyError>;
async fn get_publish_info(&self, view_id: &Uuid) -> Result<PublishInfo, FlowyError>;
async fn set_publish_name(
&self,
workspace_id: &str,
view_id: String,
workspace_id: &Uuid,
view_id: Uuid,
new_name: String,
) -> Result<(), FlowyError>;
async fn set_publish_namespace(
&self,
workspace_id: &str,
workspace_id: &Uuid,
new_namespace: String,
) -> Result<(), FlowyError>;
async fn list_published_views(
&self,
workspace_id: &str,
workspace_id: &Uuid,
) -> Result<Vec<PublishInfoView>, FlowyError>;
async fn get_default_published_view_info(
&self,
workspace_id: &str,
workspace_id: &Uuid,
) -> Result<PublishInfo, FlowyError>;
async fn set_default_published_view(
&self,
workspace_id: &str,
workspace_id: &Uuid,
view_id: uuid::Uuid,
) -> Result<(), FlowyError>;
async fn remove_default_published_view(&self, workspace_id: &str) -> Result<(), FlowyError>;
async fn remove_default_published_view(&self, workspace_id: &Uuid) -> Result<(), FlowyError>;
async fn get_publish_namespace(&self, workspace_id: &str) -> Result<String, FlowyError>;
async fn get_publish_namespace(&self, workspace_id: &Uuid) -> Result<String, FlowyError>;
async fn import_zip(&self, file_path: &str) -> Result<(), FlowyError>;
}
#[derive(Debug)]
pub struct FolderCollabParams {
pub object_id: String,
pub object_id: Uuid,
pub encoded_collab_v1: Vec<u8>,
pub collab_type: CollabType,
}
#[derive(Debug)]
pub struct FullSyncCollabParams {
pub object_id: String,
pub object_id: Uuid,
pub encoded_collab: EncodedCollab,
pub collab_type: CollabType,
}
@@ -3,6 +3,7 @@ use collab_entity::CollabType;
use collab_folder::ViewLayout;
use flowy_error::FlowyResult;
use lib_infra::async_trait::async_trait;
use uuid::Uuid;
pub struct QueryCollab {
pub collab_type: CollabType,
@@ -17,14 +18,14 @@ pub trait FolderQueryService: Send + Sync + 'static {
/// the provided view layout, given that the parent view is not a space
async fn get_surrounding_view_ids_with_view_layout(
&self,
parent_view_id: &str,
parent_view_id: &Uuid,
view_layout: ViewLayout,
) -> Vec<String>;
) -> Vec<Uuid>;
async fn get_collab(&self, object_id: &str, collab_type: CollabType) -> Option<QueryCollab>;
async fn get_collab(&self, object_id: &Uuid, collab_type: CollabType) -> Option<QueryCollab>;
}
#[async_trait]
pub trait FolderViewEdit: Send + Sync + 'static {
async fn set_view_title_if_empty(&self, view_id: &str, title: &str) -> FlowyResult<()>;
async fn set_view_title_if_empty(&self, view_id: &Uuid, title: &str) -> FlowyResult<()>;
}
@@ -4,6 +4,8 @@ use crate::share::{ImportData, ImportItem, ImportParams, ImportType};
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::FlowyError;
use lib_infra::validator_fn::required_not_empty_str;
use std::str::FromStr;
use uuid::Uuid;
use validator::Validate;
#[derive(Clone, Debug, ProtoBuf_Enum)]
@@ -76,6 +78,8 @@ impl TryInto<ImportParams> for ImportPayloadPB {
.map_err(|_| FlowyError::invalid_view_id())?
.0;
let parent_view_id = Uuid::from_str(&parent_view_id)?;
let items = self
.items
.into_iter()
@@ -1,12 +1,13 @@
use collab_folder::{View, ViewIcon, ViewLayout};
use std::collections::HashMap;
use std::convert::TryInto;
use std::ops::{Deref, DerefMut};
use std::sync::Arc;
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::ErrorCode;
use flowy_folder_pub::cloud::gen_view_id;
use std::collections::HashMap;
use std::convert::TryInto;
use std::ops::{Deref, DerefMut};
use std::str::FromStr;
use std::sync::Arc;
use uuid::Uuid;
use crate::entities::icon::ViewIconPB;
use crate::entities::parser::view::{ViewIdentify, ViewName, ViewThumbnail};
@@ -322,10 +323,10 @@ pub struct CreateOrphanViewPayloadPB {
#[derive(Debug, Clone)]
pub struct CreateViewParams {
pub parent_view_id: String,
pub parent_view_id: Uuid,
pub name: String,
pub layout: ViewLayoutPB,
pub view_id: String,
pub view_id: Uuid,
pub initial_data: ViewData,
pub meta: HashMap<String, String>,
// Mark the view as current view after creation.
@@ -346,9 +347,13 @@ impl TryInto<CreateViewParams> for CreateViewPayloadPB {
fn try_into(self) -> Result<CreateViewParams, Self::Error> {
let name = ViewName::parse(self.name)?.0;
let parent_view_id = ViewIdentify::parse(self.parent_view_id)?.0;
let parent_view_id = ViewIdentify::parse(self.parent_view_id)
.and_then(|id| Uuid::from_str(&id.0).map_err(|_| ErrorCode::InvalidParams))?;
// if view_id is not provided, generate a new view_id
let view_id = self.view_id.unwrap_or_else(|| gen_view_id().to_string());
let view_id = self
.view_id
.and_then(|v| Uuid::parse_str(&v).ok())
.unwrap_or_else(gen_view_id);
Ok(CreateViewParams {
parent_view_id,
@@ -371,13 +376,13 @@ impl TryInto<CreateViewParams> for CreateOrphanViewPayloadPB {
fn try_into(self) -> Result<CreateViewParams, Self::Error> {
let name = ViewName::parse(self.name)?.0;
let parent_view_id = ViewIdentify::parse(self.view_id.clone())?.0;
let view_id = Uuid::parse_str(&self.view_id).map_err(|_| ErrorCode::InvalidParams)?;
Ok(CreateViewParams {
parent_view_id,
parent_view_id: view_id,
name,
layout: self.layout,
view_id: self.view_id,
view_id,
initial_data: ViewData::Data(self.initial_data.into()),
meta: Default::default(),
set_as_current: false,
@@ -564,9 +569,9 @@ impl TryInto<MoveViewParams> for MoveViewPayloadPB {
#[derive(Debug)]
pub struct MoveNestedViewParams {
pub view_id: String,
pub new_parent_id: String,
pub prev_view_id: Option<String>,
pub view_id: Uuid,
pub new_parent_id: Uuid,
pub prev_view_id: Option<Uuid>,
pub from_section: Option<ViewSectionPB>,
pub to_section: Option<ViewSectionPB>,
}
@@ -575,9 +580,20 @@ impl TryInto<MoveNestedViewParams> for MoveNestedViewPayloadPB {
type Error = ErrorCode;
fn try_into(self) -> Result<MoveNestedViewParams, Self::Error> {
let view_id = ViewIdentify::parse(self.view_id)?.0;
let view_id = Uuid::from_str(&ViewIdentify::parse(self.view_id)?.0)
.map_err(|_| ErrorCode::InvalidParams)?;
let new_parent_id = ViewIdentify::parse(self.new_parent_id)?.0;
let prev_view_id = self.prev_view_id;
let new_parent_id = Uuid::from_str(&new_parent_id).map_err(|_| ErrorCode::InvalidParams)?;
let prev_view_id = match self.prev_view_id {
Some(prev_view_id) => Some(
Uuid::from_str(&ViewIdentify::parse(prev_view_id)?.0)
.map_err(|_| ErrorCode::InvalidParams)?,
),
None => None,
};
Ok(MoveNestedViewParams {
view_id,
new_parent_id,
@@ -1,8 +1,9 @@
use std::sync::{Arc, Weak};
use tracing::instrument;
use flowy_error::{FlowyError, FlowyResult};
use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult};
use std::str::FromStr;
use std::sync::{Arc, Weak};
use tracing::instrument;
use uuid::Uuid;
use crate::entities::*;
use crate::manager::FolderManager;
@@ -443,7 +444,12 @@ pub(crate) async fn unpublish_views_handler(
) -> Result<(), FlowyError> {
let folder = upgrade_folder(folder)?;
let params = data.into_inner();
folder.unpublish_views(params.view_ids).await?;
let view_ids = params
.view_ids
.into_iter()
.flat_map(|id| Uuid::from_str(&id).ok())
.collect::<Vec<_>>();
folder.unpublish_views(view_ids).await?;
Ok(())
}
@@ -454,6 +460,7 @@ pub(crate) async fn get_publish_info_handler(
) -> DataResult<PublishInfoResponsePB, FlowyError> {
let folder = upgrade_folder(folder)?;
let view_id = data.into_inner().value;
let view_id = Uuid::from_str(&view_id)?;
let info = folder.get_publish_info(&view_id).await?;
data_result_ok(PublishInfoResponsePB::from(info))
}
@@ -465,6 +472,7 @@ pub(crate) async fn set_publish_name_handler(
) -> Result<(), FlowyError> {
let folder = upgrade_folder(folder)?;
let SetPublishNamePB { view_id, new_name } = data.into_inner();
let view_id = Uuid::from_str(&view_id)?;
folder.set_publish_name(view_id, new_name).await?;
Ok(())
}
+94 -74
View File
@@ -44,16 +44,18 @@ use flowy_sqlite::kv::KVStorePreferences;
use futures::future;
use std::collections::HashMap;
use std::fmt::{Display, Formatter};
use std::str::FromStr;
use std::sync::{Arc, Weak};
use tokio::sync::RwLockWriteGuard;
use tracing::{error, info, instrument};
use uuid::Uuid;
pub trait FolderUser: Send + Sync {
fn user_id(&self) -> Result<i64, FlowyError>;
fn workspace_id(&self) -> Result<String, FlowyError>;
fn workspace_id(&self) -> Result<Uuid, FlowyError>;
fn collab_db(&self, uid: i64) -> Result<Weak<CollabKVDB>, FlowyError>;
fn is_folder_exist_on_disk(&self, uid: i64, workspace_id: &str) -> FlowyResult<bool>;
fn is_folder_exist_on_disk(&self, uid: i64, workspace_id: &Uuid) -> FlowyResult<bool>;
}
pub struct FolderManager {
@@ -111,7 +113,7 @@ impl FolderManager {
Ok::<WorkspacePB, FlowyError>(workspace)
};
match folder.get_workspace_info(&workspace_id) {
match folder.get_workspace_info(&workspace_id.to_string()) {
None => Err(FlowyError::record_not_found().with_context("Can not find the workspace")),
Some(workspace) => workspace_pb_from_workspace(workspace, &folder),
}
@@ -127,14 +129,14 @@ impl FolderManager {
.ok_or_else(|| internal_error("The folder is not initialized"))?
.read()
.await
.get_folder_data(&workspace_id)
.get_folder_data(&workspace_id.to_string())
.ok_or_else(|| internal_error("Workspace id not match the id in current folder"))?;
Ok(data)
}
pub async fn gather_publish_encode_collab(
&self,
view_id: &str,
view_id: &Uuid,
layout: &ViewLayout,
) -> FlowyResult<GatherEncodedCollab> {
let handler = self.get_handler(layout)?;
@@ -177,7 +179,7 @@ impl FolderManager {
pub(crate) async fn make_folder<T: Into<Option<FolderNotify>>>(
&self,
uid: i64,
workspace_id: &str,
workspace_id: &Uuid,
collab_db: Weak<CollabKVDB>,
data_source: Option<DataSource>,
folder_notifier: T,
@@ -187,8 +189,7 @@ impl FolderManager {
let config = CollabBuilderConfig::default().sync_enable(true);
let data_source = data_source.unwrap_or_else(|| {
CollabPersistenceImpl::new(collab_db.clone(), uid, workspace_id.to_string())
.into_data_source()
CollabPersistenceImpl::new(collab_db.clone(), uid, *workspace_id).into_data_source()
});
let object_id = workspace_id;
@@ -218,8 +219,11 @@ impl FolderManager {
"Clear the folder data and try to open the folder again due to: {}",
err
);
if let Some(db) = self.user.collab_db(uid).ok().and_then(|a| a.upgrade()) {
let _ = db.delete_doc(uid, workspace_id, workspace_id).await;
let _ = db
.delete_doc(uid, &workspace_id.to_string(), &object_id.to_string())
.await;
}
Err(err.into())
},
@@ -229,7 +233,7 @@ impl FolderManager {
pub(crate) async fn create_folder_with_data(
&self,
uid: i64,
workspace_id: &str,
workspace_id: &Uuid,
collab_db: Weak<CollabKVDB>,
notifier: Option<FolderNotify>,
folder_data: Option<FolderData>,
@@ -240,8 +244,8 @@ impl FolderManager {
.collab_builder
.collab_object(workspace_id, uid, object_id, CollabType::Folder)?;
let doc_state = CollabPersistenceImpl::new(collab_db.clone(), uid, workspace_id.to_string())
.into_data_source();
let doc_state =
CollabPersistenceImpl::new(collab_db.clone(), uid, *workspace_id).into_data_source();
let folder = self
.collab_builder
.create_folder(
@@ -317,7 +321,7 @@ impl FolderManager {
_token: &str,
is_new: bool,
data_source: FolderInitDataSource,
workspace_id: &str,
workspace_id: &Uuid,
) -> FlowyResult<()> {
// Create the default workspace if the user is new
info!("initialize_when_sign_up: is_new: {}", is_new);
@@ -377,7 +381,7 @@ impl FolderManager {
let workspace_id = self.user.workspace_id()?;
let latest_view = self.get_current_view().await;
Ok(WorkspaceSettingPB {
workspace_id,
workspace_id: workspace_id.to_string(),
latest_view,
})
}
@@ -495,7 +499,7 @@ impl FolderManager {
.ok_or_else(|| FlowyError::internal().with_context("folder is not initialized"))?;
let folder = lock.read().await;
let workspace = folder
.get_workspace_info(&workspace_id)
.get_workspace_info(&workspace_id.to_string())
.ok_or_else(|| FlowyError::record_not_found().with_context("Can not find the workspace"))?;
let views = folder
@@ -606,8 +610,9 @@ impl FolderManager {
// Drop the folder lock explicitly to avoid deadlock when following calls contains 'self'
drop(folder);
let view_id = Uuid::from_str(view_id)?;
let handler = self.get_handler(&view.layout)?;
handler.close_view(view_id).await?;
handler.close_view(&view_id).await?;
}
}
Ok(())
@@ -844,24 +849,28 @@ impl FolderManager {
let prev_view_id = params.prev_view_id;
let from_section = params.from_section;
let to_section = params.to_section;
let view = self.get_view_pb(&view_id).await?;
let view = self.get_view_pb(&view_id.to_string()).await?;
// if the view is locked, the view can't be moved
if view.is_locked.unwrap_or(false) {
return Err(FlowyError::view_is_locked());
}
let old_parent_id = view.parent_view_id;
let old_parent_id = Uuid::from_str(&view.parent_view_id)?;
if let Some(lock) = self.mutex_folder.load_full() {
let mut folder = lock.write().await;
folder.move_nested_view(&view_id, &new_parent_id, prev_view_id);
folder.move_nested_view(
&view_id.to_string(),
&new_parent_id.to_string(),
prev_view_id.map(|s| s.to_string()),
);
if from_section != to_section {
if to_section == Some(ViewSectionPB::Private) {
folder.add_private_view_ids(vec![view_id.clone()]);
folder.add_private_view_ids(vec![view_id.to_string()]);
} else {
folder.delete_private_view_ids(vec![view_id.clone()]);
folder.delete_private_view_ids(vec![view_id.to_string()]);
}
}
notify_parent_view_did_change(&workspace_id, &folder, vec![new_parent_id, old_parent_id]);
notify_parent_view_did_change(workspace_id, &folder, vec![new_parent_id, old_parent_id]);
}
Ok(())
}
@@ -912,7 +921,8 @@ impl FolderManager {
if let Some(lock) = self.mutex_folder.load_full() {
let mut folder = lock.write().await;
folder.move_view(view_id, actual_from_index as u32, actual_to_index as u32);
notify_parent_view_did_change(&workspace_id, &folder, vec![parent_view_id]);
let parent_view_id = Uuid::from_str(&parent_view_id)?;
notify_parent_view_did_change(workspace_id, &folder, vec![parent_view_id]);
}
}
}
@@ -1115,7 +1125,8 @@ impl FolderManager {
view.name,
view.layout
);
let view_data = handler.duplicate_view(&view.id).await?;
let view_id = Uuid::from_str(&view.id)?;
let view_data = handler.duplicate_view(&view_id).await?;
let index = self
.get_view_relation(&current_parent_id)
@@ -1151,12 +1162,13 @@ impl FolderManager {
view.name.clone()
};
let parent_view_id = Uuid::from_str(&current_parent_id)?;
let duplicate_params = CreateViewParams {
parent_view_id: current_parent_id.clone(),
parent_view_id,
name,
layout: view.layout.clone().into(),
initial_data: ViewData::DuplicateData(view_data),
view_id: gen_view_id().to_string(),
view_id: gen_view_id(),
meta: Default::default(),
set_as_current: is_source_view && open_after_duplicated,
index,
@@ -1176,7 +1188,7 @@ impl FolderManager {
if sync_after_create {
if let Some(encoded_collab) = encoded_collab {
let object_id = duplicated_view.id.clone();
let object_id = Uuid::from_str(&duplicated_view.id)?;
let collab_type = match duplicated_view.layout {
ViewLayout::Document => CollabType::Document,
ViewLayout::Board | ViewLayout::Grid | ViewLayout::Calendar => CollabType::Database,
@@ -1208,20 +1220,20 @@ impl FolderManager {
is_source_view = false
}
let workspace_id = &self.user.workspace_id()?;
let workspace_id = self.user.workspace_id()?;
let parent_view_id = Uuid::from_str(parent_view_id)?;
// Sync the view to the cloud
if sync_after_create {
self
.cloud_service
.batch_create_folder_collab_objects(workspace_id, objects)
.batch_create_folder_collab_objects(&workspace_id, objects)
.await?;
}
// notify the update here
let folder = lock.read().await;
notify_parent_view_did_change(workspace_id, &folder, vec![parent_view_id.to_string()]);
notify_parent_view_did_change(workspace_id, &folder, vec![parent_view_id]);
let duplicated_view = self.get_view_pb(&new_view_id).await?;
Ok(duplicated_view)
@@ -1242,6 +1254,7 @@ impl FolderManager {
let view_layout: ViewLayout = view.layout.clone().into();
if let Some(handle) = self.operation_handlers.get(&view_layout) {
info!("Open view: {}-{}", view.name, view.id);
let view_id = Uuid::from_str(&view.id)?;
if let Err(err) = handle.open_view(&view_id).await {
error!("Open view error: {:?}", err);
}
@@ -1250,7 +1263,7 @@ impl FolderManager {
let workspace_id = self.user.workspace_id()?;
let setting = WorkspaceSettingPB {
workspace_id,
workspace_id: workspace_id.to_string(),
latest_view: view,
};
send_current_workspace_notification(FolderNotification::DidUpdateWorkspaceSetting, setting);
@@ -1367,18 +1380,18 @@ impl FolderManager {
let workspace_id = self.user.workspace_id()?;
self
.cloud_service
.publish_view(workspace_id.as_str(), payload)
.publish_view(&workspace_id, payload)
.await?;
Ok(())
}
/// Unpublish the view with the given view id.
#[tracing::instrument(level = "debug", skip(self), err)]
pub async fn unpublish_views(&self, view_ids: Vec<String>) -> FlowyResult<()> {
pub async fn unpublish_views(&self, view_ids: Vec<Uuid>) -> FlowyResult<()> {
let workspace_id = self.user.workspace_id()?;
self
.cloud_service
.unpublish_views(workspace_id.as_str(), view_ids)
.unpublish_views(&workspace_id, view_ids)
.await?;
Ok(())
}
@@ -1386,14 +1399,14 @@ impl FolderManager {
/// Get the publish info of the view with the given view id.
/// The publish info contains the namespace and publish_name of the view.
#[tracing::instrument(level = "debug", skip(self))]
pub async fn get_publish_info(&self, view_id: &str) -> FlowyResult<PublishInfo> {
pub async fn get_publish_info(&self, view_id: &Uuid) -> FlowyResult<PublishInfo> {
let publish_info = self.cloud_service.get_publish_info(view_id).await?;
Ok(publish_info)
}
/// Sets the publish name of the view with the given view id.
#[tracing::instrument(level = "debug", skip(self))]
pub async fn set_publish_name(&self, view_id: String, new_name: String) -> FlowyResult<()> {
pub async fn set_publish_name(&self, view_id: Uuid, new_name: String) -> FlowyResult<()> {
let workspace_id = self.user.workspace_id()?;
self
.cloud_service
@@ -1409,7 +1422,7 @@ impl FolderManager {
let workspace_id = self.user.workspace_id()?;
self
.cloud_service
.set_publish_namespace(workspace_id.as_str(), new_namespace)
.set_publish_namespace(&workspace_id, new_namespace)
.await?;
Ok(())
}
@@ -1420,7 +1433,7 @@ impl FolderManager {
let workspace_id = self.user.workspace_id()?;
let namespace = self
.cloud_service
.get_publish_namespace(workspace_id.as_str())
.get_publish_namespace(&workspace_id)
.await?;
Ok(namespace)
}
@@ -1502,7 +1515,7 @@ impl FolderManager {
};
if let Ok(payload) = self
.get_publish_payload(&current_view_id, publish_name, layout)
.get_publish_payload(&Uuid::from_str(&current_view_id)?, publish_name, layout)
.await
{
payloads.push(payload);
@@ -1551,7 +1564,7 @@ impl FolderManager {
async fn get_publish_payload(
&self,
view_id: &str,
view_id: &Uuid,
publish_name: Option<String>,
layout: ViewLayout,
) -> FlowyResult<PublishPayload> {
@@ -1559,18 +1572,20 @@ impl FolderManager {
let encoded_collab_wrapper: GatherEncodedCollab = handler
.gather_publish_encode_collab(&self.user, view_id)
.await?;
let view = self.get_view_pb(view_id).await?;
let view_str_id = view_id.to_string();
let view = self.get_view_pb(&view_str_id).await?;
let publish_name = publish_name.unwrap_or_else(|| generate_publish_name(&view.id, &view.name));
let child_views = self
.build_publish_views(view_id)
.build_publish_views(&view_str_id)
.await
.and_then(|v| v.child_views)
.unwrap_or_default();
let ancestor_views = self
.get_view_ancestors_pb(view_id)
.get_view_ancestors_pb(&view_str_id)
.await?
.iter()
.map(view_pb_to_publish_view)
@@ -1720,8 +1735,9 @@ impl FolderManager {
};
if let Some(view) = view {
let view_id = Uuid::from_str(view_id)?;
if let Ok(handler) = self.get_handler(&view.layout) {
handler.delete_view(view_id).await?;
handler.delete_view(&view_id).await?;
}
}
}
@@ -1733,11 +1749,11 @@ impl FolderManager {
#[instrument(level = "debug", skip_all, err)]
pub(crate) async fn import_single_file(
&self,
parent_view_id: String,
parent_view_id: Uuid,
import_data: ImportItem,
) -> FlowyResult<(View, Vec<(String, CollabType, EncodedCollab)>)> {
let handler = self.get_handler(&import_data.view_layout)?;
let view_id = gen_view_id().to_string();
let view_id = gen_view_id();
let uid = self.user.user_id()?;
let mut encoded_collab = vec![];
@@ -1745,7 +1761,7 @@ impl FolderManager {
match import_data.data {
ImportData::FilePath { file_path } => {
handler
.import_from_file_path(&view_id, &import_data.name, file_path)
.import_from_file_path(&view_id.to_string(), &import_data.name, file_path)
.await?;
},
ImportData::Bytes { bytes } => {
@@ -1799,16 +1815,18 @@ impl FolderManager {
for data in import_data.items {
// Import a single file and get the view and encoded collab data
let (view, encoded_collabs) = self
.import_single_file(import_data.parent_view_id.clone(), data)
.import_single_file(import_data.parent_view_id, data)
.await?;
views.push(view_pb_without_child_views(view));
for (object_id, collab_type, encode_collab) in encoded_collabs {
match self.get_folder_collab_params(object_id, collab_type, encode_collab) {
Ok(params) => objects.push(params),
Err(e) => {
error!("import error {}", e);
},
if let Ok(object_id) = Uuid::from_str(&object_id) {
match self.get_folder_collab_params(object_id, collab_type, encode_collab) {
Ok(params) => objects.push(params),
Err(e) => {
error!("import error {}", e);
},
}
}
}
}
@@ -1822,7 +1840,7 @@ impl FolderManager {
// Notify that the parent view has changed
if let Some(lock) = self.mutex_folder.load_full() {
let folder = lock.read().await;
notify_parent_view_did_change(&workspace_id, &folder, vec![import_data.parent_view_id]);
notify_parent_view_did_change(workspace_id, &folder, vec![import_data.parent_view_id]);
}
Ok(RepeatedViewPB { items: views })
@@ -1887,7 +1905,7 @@ impl FolderManager {
fn get_folder_collab_params(
&self,
object_id: String,
object_id: Uuid,
collab_type: CollabType,
encoded_collab: EncodedCollab,
) -> FlowyResult<FolderCollabParams> {
@@ -1911,18 +1929,20 @@ impl FolderManager {
let folder = lock.read().await;
let view = folder.get_view(view_id)?;
match folder.get_view(&view.parent_view_id) {
None => folder.get_workspace_info(&workspace_id).map(|workspace| {
(
true,
workspace.id,
workspace
.child_views
.items
.into_iter()
.map(|view| view.id)
.collect::<Vec<String>>(),
)
}),
None => folder
.get_workspace_info(&workspace_id.to_string())
.map(|workspace| {
(
true,
workspace.id,
workspace
.child_views
.items
.into_iter()
.map(|view| view.id)
.collect::<Vec<String>>(),
)
}),
Some(parent_view) => Some((
false,
parent_view.id.clone(),
@@ -2031,17 +2051,17 @@ impl FolderManager {
.collect()
}
pub fn remove_indices_for_workspace(&self, workspace_id: String) -> FlowyResult<()> {
pub fn remove_indices_for_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()> {
self
.folder_indexer
.remove_indices_for_workspace(workspace_id)?;
.remove_indices_for_workspace(*workspace_id)?;
Ok(())
}
}
/// Return the views that belong to the workspace. The views are filtered by the trash and all the private views.
pub(crate) fn get_workspace_public_view_pbs(workspace_id: &str, folder: &Folder) -> Vec<ViewPB> {
pub(crate) fn get_workspace_public_view_pbs(workspace_id: &Uuid, folder: &Folder) -> Vec<ViewPB> {
// get the trash ids
let trash_ids = folder
.get_all_trash_sections()
@@ -2056,7 +2076,7 @@ pub(crate) fn get_workspace_public_view_pbs(workspace_id: &str, folder: &Folder)
.map(|view| view.id)
.collect::<Vec<String>>();
let mut views = folder.get_views_belong_to(workspace_id);
let mut views = folder.get_views_belong_to(&workspace_id.to_string());
// filter the views that are in the trash and all the private views
views.retain(|view| !trash_ids.contains(&view.id) && !private_view_ids.contains(&view.id));
@@ -2082,7 +2102,7 @@ fn get_all_child_view_ids(folder: &Folder, view_id: &str) -> Vec<String> {
}
/// Get the current private views of the user.
pub(crate) fn get_workspace_private_view_pbs(workspace_id: &str, folder: &Folder) -> Vec<ViewPB> {
pub(crate) fn get_workspace_private_view_pbs(workspace_id: &Uuid, folder: &Folder) -> Vec<ViewPB> {
// get the trash ids
let trash_ids = folder
.get_all_trash_sections()
@@ -2097,7 +2117,7 @@ pub(crate) fn get_workspace_private_view_pbs(workspace_id: &str, folder: &Folder
.map(|view| view.id)
.collect::<Vec<String>>();
let mut views = folder.get_views_belong_to(workspace_id);
let mut views = folder.get_views_belong_to(&workspace_id.to_string());
// filter the views that are in the trash and not in the private view ids
views.retain(|view| !trash_ids.contains(&view.id) && private_view_ids.contains(&view.id));
@@ -10,6 +10,7 @@ use flowy_error::{FlowyError, FlowyResult};
use std::sync::{Arc, Weak};
use tokio::task::spawn_blocking;
use tracing::{event, info, Level};
use uuid::Uuid;
impl FolderManager {
/// Called immediately after the application launched if the user already sign in/sign up.
@@ -17,7 +18,7 @@ impl FolderManager {
pub async fn initialize(
&self,
uid: i64,
workspace_id: &str,
workspace_id: &Uuid,
initial_data: FolderInitDataSource,
) -> FlowyResult<()> {
// Update the workspace id
@@ -37,7 +38,6 @@ impl FolderManager {
);
}
let workspace_id = workspace_id.to_string();
// Get the collab db for the user with given user id.
let collab_db = self.user.collab_db(uid)?;
@@ -54,33 +54,33 @@ impl FolderManager {
} => {
let is_exist = self
.user
.is_folder_exist_on_disk(uid, &workspace_id)
.is_folder_exist_on_disk(uid, workspace_id)
.unwrap_or(false);
// 1. if the folder exists, open it from local disk
if is_exist {
event!(Level::INFO, "Init folder from local disk");
self
.make_folder(uid, &workspace_id, collab_db, None, folder_notifier)
.make_folder(uid, workspace_id, collab_db, None, folder_notifier)
.await?
} else if create_if_not_exist {
// 2. if the folder doesn't exist and create_if_not_exist is true, create a default folder
// Currently, this branch is only used when the server type is supabase. For appflowy cloud,
// the default workspace is already created when the user sign up.
self
.create_default_folder(uid, &workspace_id, collab_db, folder_notifier)
.create_default_folder(uid, workspace_id, collab_db, folder_notifier)
.await?
} else {
// 3. If the folder doesn't exist and create_if_not_exist is false, try to fetch the folder data from cloud/
// This will happen user can't fetch the folder data when the user sign in.
let doc_state = self
.cloud_service
.get_folder_doc_state(&workspace_id, uid, CollabType::Folder, &workspace_id)
.get_folder_doc_state(workspace_id, uid, CollabType::Folder, workspace_id)
.await?;
self
.make_folder(
uid,
&workspace_id,
workspace_id,
collab_db.clone(),
Some(DataSource::DocStateV1(doc_state)),
folder_notifier.clone(),
@@ -92,14 +92,14 @@ impl FolderManager {
if doc_state.is_empty() {
event!(Level::ERROR, "remote folder data is empty, open from local");
self
.make_folder(uid, &workspace_id, collab_db, None, folder_notifier)
.make_folder(uid, workspace_id, collab_db, None, folder_notifier)
.await?
} else {
event!(Level::INFO, "Restore folder from remote data");
self
.make_folder(
uid,
&workspace_id,
workspace_id,
collab_db.clone(),
Some(DataSource::DocStateV1(doc_state)),
folder_notifier.clone(),
@@ -115,27 +115,23 @@ impl FolderManager {
let index_content_rx = folder.subscribe_index_content();
self
.folder_indexer
.set_index_content_receiver(index_content_rx, workspace_id.clone());
self.handle_index_folder(workspace_id.clone(), &folder);
.set_index_content_receiver(index_content_rx, *workspace_id);
self.handle_index_folder(*workspace_id, &folder);
folder_state_rx
};
self.mutex_folder.store(Some(folder.clone()));
let weak_mutex_folder = Arc::downgrade(&folder);
subscribe_folder_sync_state_changed(
workspace_id.clone(),
folder_state_rx,
Arc::downgrade(&self.user),
);
subscribe_folder_sync_state_changed(*workspace_id, folder_state_rx, Arc::downgrade(&self.user));
subscribe_folder_trash_changed(
workspace_id.clone(),
*workspace_id,
section_change_rx,
weak_mutex_folder.clone(),
Arc::downgrade(&self.user),
);
subscribe_folder_view_changed(
workspace_id.clone(),
*workspace_id,
view_rx,
weak_mutex_folder.clone(),
Arc::downgrade(&self.user),
@@ -147,7 +143,7 @@ impl FolderManager {
async fn create_default_folder(
&self,
uid: i64,
workspace_id: &str,
workspace_id: &Uuid,
collab_db: Weak<CollabKVDB>,
folder_notifier: FolderNotify,
) -> Result<Arc<RwLock<Folder>>, FlowyError> {
@@ -170,24 +166,22 @@ impl FolderManager {
Ok(folder)
}
fn handle_index_folder(&self, workspace_id: String, folder: &Folder) {
fn handle_index_folder(&self, workspace_id: Uuid, folder: &Folder) {
let mut index_all = true;
let encoded_collab = self
.store_preferences
.get_object::<EncodedCollab>(&workspace_id);
.get_object::<EncodedCollab>(workspace_id.to_string().as_str());
if let Some(encoded_collab) = encoded_collab {
if let Ok(changes) = folder.calculate_view_changes(encoded_collab) {
let folder_indexer = self.folder_indexer.clone();
let views = folder.get_all_views();
let wid = workspace_id.clone();
if !changes.is_empty() && !views.is_empty() {
spawn_blocking(move || {
// We index the changes
folder_indexer.index_view_changes(views, changes, wid);
folder_indexer.index_view_changes(views, changes, workspace_id);
});
index_all = false;
}
@@ -197,15 +191,13 @@ impl FolderManager {
if index_all {
let views = folder.get_all_views();
let folder_indexer = self.folder_indexer.clone();
let wid = workspace_id.clone();
// We spawn a blocking task to index all views in the folder
spawn_blocking(move || {
// We remove old indexes just in case
let _ = folder_indexer.remove_indices_for_workspace(wid.clone());
let _ = folder_indexer.remove_indices_for_workspace(workspace_id);
// We index all views from the workspace
folder_indexer.index_all_views(views, wid);
folder_indexer.index_all_views(views, workspace_id);
});
}
@@ -13,14 +13,16 @@ use collab_folder::{
use lib_infra::sync_trace;
use std::collections::HashSet;
use std::str::FromStr;
use std::sync::Weak;
use tokio_stream::wrappers::WatchStream;
use tokio_stream::StreamExt;
use tracing::{event, trace, Level};
use uuid::Uuid;
/// Listen on the [ViewChange] after create/delete/update events happened
pub(crate) fn subscribe_folder_view_changed(
workspace_id: String,
workspace_id: Uuid,
mut rx: ViewChangeReceiver,
weak_mutex_folder: Weak<RwLock<Folder>>,
user: Weak<dyn FolderUser>,
@@ -46,9 +48,10 @@ pub(crate) fn subscribe_folder_view_changed(
ChildViewChangeReason::Create,
);
let folder = lock.read().await;
let parent_view_id = view.parent_view_id.clone();
notify_parent_view_did_change(&workspace_id, &folder, vec![parent_view_id]);
sync_trace!("[Folder] create view: {:?}", view);
if let Ok(parent_view_id) = Uuid::from_str(&view.parent_view_id) {
notify_parent_view_did_change(workspace_id, &folder, vec![parent_view_id]);
sync_trace!("[Folder] create view: {:?}", view);
}
},
ViewChange::DidDeleteView { views } => {
for view in views {
@@ -69,7 +72,9 @@ pub(crate) fn subscribe_folder_view_changed(
ChildViewChangeReason::Update,
);
let folder = lock.read().await;
notify_parent_view_did_change(&workspace_id, &folder, vec![view.parent_view_id]);
if let Ok(parent_view_id) = Uuid::from_str(&view.parent_view_id) {
notify_parent_view_did_change(workspace_id, &folder, vec![parent_view_id]);
}
},
};
}
@@ -78,7 +83,7 @@ pub(crate) fn subscribe_folder_view_changed(
}
pub(crate) fn subscribe_folder_sync_state_changed(
workspace_id: String,
workspace_id: Uuid,
mut folder_sync_state_rx: WatchStream<SyncState>,
user: Weak<dyn FolderUser>,
) {
@@ -93,16 +98,19 @@ pub(crate) fn subscribe_folder_sync_state_changed(
}
}
folder_notification_builder(&workspace_id, FolderNotification::DidUpdateFolderSyncUpdate)
.payload(FolderSyncStatePB::from(state))
.send();
folder_notification_builder(
workspace_id.to_string(),
FolderNotification::DidUpdateFolderSyncUpdate,
)
.payload(FolderSyncStatePB::from(state))
.send();
}
});
}
/// Listen on the [TrashChange]s and notify the frontend some views were changed.
pub(crate) fn subscribe_folder_trash_changed(
workspace_id: String,
workspace_id: Uuid,
mut rx: SectionChangeReceiver,
weak_mutex_folder: Weak<RwLock<Folder>>,
user: Weak<dyn FolderUser>,
@@ -131,7 +139,9 @@ pub(crate) fn subscribe_folder_trash_changed(
let folder = lock.read().await;
let views = folder.get_views(&ids);
for view in views {
unique_ids.insert(view.parent_view_id.clone());
if let Ok(parent_view_id) = Uuid::from_str(&view.parent_view_id) {
unique_ids.insert(parent_view_id);
}
}
let repeated_trash: RepeatedTrashPB = folder.get_my_trash_info().into();
@@ -140,7 +150,7 @@ pub(crate) fn subscribe_folder_trash_changed(
.send();
let parent_view_ids = unique_ids.into_iter().collect();
notify_parent_view_did_change(&workspace_id, &folder, parent_view_ids);
notify_parent_view_did_change(workspace_id, &folder, parent_view_ids);
},
}
}
@@ -150,10 +160,10 @@ pub(crate) fn subscribe_folder_trash_changed(
/// Notify the list of parent view ids that its child views were changed.
#[tracing::instrument(level = "debug", skip(folder, parent_view_ids))]
pub(crate) fn notify_parent_view_did_change<T: AsRef<str>>(
workspace_id: &str,
pub(crate) fn notify_parent_view_did_change(
workspace_id: Uuid,
folder: &Folder,
parent_view_ids: Vec<T>,
parent_view_ids: Vec<Uuid>,
) -> Option<()> {
let trash_ids = folder
.get_all_trash_sections()
@@ -162,24 +172,23 @@ pub(crate) fn notify_parent_view_did_change<T: AsRef<str>>(
.collect::<Vec<String>>();
for parent_view_id in parent_view_ids {
let parent_view_id = parent_view_id.as_ref();
// if the view's parent id equal to workspace id. Then it will fetch the current
// workspace views. Because the workspace is not a view stored in the views map.
if parent_view_id == workspace_id {
notify_did_update_workspace(workspace_id, folder);
notify_did_update_section_views(workspace_id, folder);
notify_did_update_workspace(&workspace_id, folder);
notify_did_update_section_views(&workspace_id, folder);
} else {
// Parent view can contain a list of child views. Currently, only get the first level
// child views.
let parent_view = folder.get_view(parent_view_id)?;
let mut child_views = folder.get_views_belong_to(parent_view_id);
let parent_view_id = parent_view_id.to_string();
let parent_view = folder.get_view(&parent_view_id)?;
let mut child_views = folder.get_views_belong_to(&parent_view_id);
child_views.retain(|view| !trash_ids.contains(&view.id));
event!(Level::DEBUG, child_views_count = child_views.len());
// Post the notification
let parent_view_pb = view_pb_with_child_views(parent_view, child_views);
folder_notification_builder(parent_view_id, FolderNotification::DidUpdateView)
folder_notification_builder(&parent_view_id, FolderNotification::DidUpdateView)
.payload(parent_view_pb)
.send();
}
@@ -188,7 +197,7 @@ pub(crate) fn notify_parent_view_did_change<T: AsRef<str>>(
None
}
pub(crate) fn notify_did_update_section_views(workspace_id: &str, folder: &Folder) {
pub(crate) fn notify_did_update_section_views(workspace_id: &Uuid, folder: &Folder) {
let public_views = get_workspace_public_view_pbs(workspace_id, folder);
let private_views = get_workspace_private_view_pbs(workspace_id, folder);
trace!(
@@ -214,7 +223,7 @@ pub(crate) fn notify_did_update_section_views(workspace_id: &str, folder: &Folde
.send();
}
pub(crate) fn notify_did_update_workspace(workspace_id: &str, folder: &Folder) {
pub(crate) fn notify_did_update_workspace(workspace_id: &Uuid, folder: &Folder) {
let repeated_view: RepeatedViewPB = get_workspace_public_view_pbs(workspace_id, folder).into();
folder_notification_builder(workspace_id, FolderNotification::DidUpdateWorkspaceViews)
.payload(repeated_view)
@@ -1,6 +1,7 @@
use flowy_derive::ProtoBuf_Enum;
use flowy_notification::NotificationBuilder;
use lib_dispatch::prelude::ToBytes;
use tracing::trace;
const FOLDER_OBSERVABLE_SOURCE: &str = "Workspace";
@@ -68,9 +69,14 @@ impl std::convert::From<i32> for FolderNotification {
}
}
#[tracing::instrument(level = "trace")]
pub(crate) fn folder_notification_builder(id: &str, ty: FolderNotification) -> NotificationBuilder {
NotificationBuilder::new(id, ty, FOLDER_OBSERVABLE_SOURCE)
#[tracing::instrument(level = "trace", skip_all)]
pub(crate) fn folder_notification_builder<T: ToString>(
id: T,
ty: FolderNotification,
) -> NotificationBuilder {
let id = id.to_string();
trace!("folder_notification_builder: id = {id}, ty = {ty:?}");
NotificationBuilder::new(&id, ty, FOLDER_OBSERVABLE_SOURCE)
}
/// The [CURRENT_WORKSPACE] represents as the current workspace that opened by the
@@ -1,5 +1,6 @@
use collab_folder::ViewLayout;
use std::fmt::{Display, Formatter};
use uuid::Uuid;
#[derive(Clone, Debug)]
pub enum ImportType {
@@ -35,6 +36,6 @@ impl Display for ImportData {
#[derive(Clone, Debug)]
pub struct ImportParams {
pub parent_view_id: String,
pub parent_view_id: Uuid,
pub items: Vec<ImportItem>,
}
+2 -1
View File
@@ -1,11 +1,12 @@
use crate::entities::UserFolderPB;
use flowy_error::{ErrorCode, FlowyError};
use uuid::Uuid;
pub(crate) fn folder_not_init_error() -> FlowyError {
FlowyError::internal().with_context("Folder not initialized")
}
pub(crate) fn workspace_data_not_sync_error(uid: i64, workspace_id: &str) -> FlowyError {
pub(crate) fn workspace_data_not_sync_error(uid: i64, workspace_id: &Uuid) -> FlowyError {
FlowyError::from(ErrorCode::WorkspaceDataNotSync).with_payload(UserFolderPB {
uid,
workspace_id: workspace_id.to_string(),
@@ -6,11 +6,11 @@ use collab_folder::hierarchy_builder::NestedViewBuilder;
pub use collab_folder::View;
use collab_folder::ViewLayout;
use dashmap::DashMap;
use flowy_error::FlowyError;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use flowy_error::FlowyError;
use uuid::Uuid;
use lib_infra::util::timestamp;
@@ -51,23 +51,23 @@ pub trait FolderOperationHandler: Send + Sync {
Ok(())
}
async fn open_view(&self, view_id: &str) -> Result<(), FlowyError>;
async fn open_view(&self, view_id: &Uuid) -> Result<(), FlowyError>;
/// Closes the view and releases the resources that this view has in
/// the backend
async fn close_view(&self, view_id: &str) -> Result<(), FlowyError>;
async fn close_view(&self, view_id: &Uuid) -> Result<(), FlowyError>;
/// Called when the view is deleted.
/// This will called after the view is deleted from the trash.
async fn delete_view(&self, view_id: &str) -> Result<(), FlowyError>;
async fn delete_view(&self, view_id: &Uuid) -> Result<(), FlowyError>;
/// Returns the [ViewData] that can be used to create the same view.
async fn duplicate_view(&self, view_id: &str) -> Result<Bytes, FlowyError>;
async fn duplicate_view(&self, view_id: &Uuid) -> Result<Bytes, FlowyError>;
/// get the encoded collab data from the disk.
async fn gather_publish_encode_collab(
&self,
_user: &Arc<dyn FolderUser>,
_view_id: &str,
_view_id: &Uuid,
) -> Result<GatherEncodedCollab, FlowyError> {
Err(FlowyError::not_support())
}
@@ -102,8 +102,8 @@ pub trait FolderOperationHandler: Send + Sync {
async fn create_default_view(
&self,
user_id: i64,
parent_view_id: &str,
view_id: &str,
parent_view_id: &Uuid,
view_id: &Uuid,
name: &str,
layout: ViewLayout,
) -> Result<(), FlowyError>;
@@ -114,7 +114,7 @@ pub trait FolderOperationHandler: Send + Sync {
async fn import_from_bytes(
&self,
uid: i64,
view_id: &str,
view_id: &Uuid,
name: &str,
import_type: ImportType,
bytes: Vec<u8>,
@@ -152,8 +152,8 @@ impl From<ViewLayoutPB> for ViewLayout {
pub(crate) fn create_view(uid: i64, params: CreateViewParams, layout: ViewLayout) -> View {
let time = timestamp();
View {
id: params.view_id,
parent_view_id: params.parent_view_id,
id: params.view_id.to_string(),
parent_view_id: params.parent_view_id.to_string(),
name: params.name,
created_at: time,
is_favorite: false,
@@ -11,4 +11,4 @@ collab = { workspace = true }
collab-folder = { workspace = true }
flowy-error = { workspace = true }
client-api = { workspace = true }
futures = { workspace = true }
uuid.workspace = true
@@ -1,12 +1,13 @@
use client_api::entity::search_dto::SearchDocumentResponseItem;
use flowy_error::FlowyError;
use lib_infra::async_trait::async_trait;
use uuid::Uuid;
#[async_trait]
pub trait SearchCloudService: Send + Sync + 'static {
async fn document_search(
&self,
workspace_id: &str,
workspace_id: &Uuid,
query: String,
) -> Result<Vec<SearchDocumentResponseItem>, FlowyError>;
}
@@ -4,44 +4,45 @@ use std::sync::Arc;
use collab::core::collab::IndexContentReceiver;
use collab_folder::{folder_diff::FolderViewChange, View, ViewIcon, ViewLayout};
use flowy_error::FlowyError;
use uuid::Uuid;
pub struct IndexableData {
pub id: String,
pub data: String,
pub icon: Option<ViewIcon>,
pub layout: ViewLayout,
pub workspace_id: String,
pub workspace_id: Uuid,
}
impl IndexableData {
pub fn from_view(view: Arc<View>, workspace_id: String) -> Self {
pub fn from_view(view: Arc<View>, workspace_id: Uuid) -> Self {
IndexableData {
id: view.id.clone(),
data: view.name.clone(),
icon: view.icon.clone(),
layout: view.layout.clone(),
workspace_id: workspace_id.clone(),
workspace_id,
}
}
}
pub trait IndexManager: Send + Sync {
fn set_index_content_receiver(&self, rx: IndexContentReceiver, workspace_id: String);
fn set_index_content_receiver(&self, rx: IndexContentReceiver, workspace_id: Uuid);
fn add_index(&self, data: IndexableData) -> Result<(), FlowyError>;
fn update_index(&self, data: IndexableData) -> Result<(), FlowyError>;
fn remove_indices(&self, ids: Vec<String>) -> Result<(), FlowyError>;
fn remove_indices_for_workspace(&self, workspace_id: String) -> Result<(), FlowyError>;
fn remove_indices_for_workspace(&self, workspace_id: Uuid) -> Result<(), FlowyError>;
fn is_indexed(&self) -> bool;
fn as_any(&self) -> &dyn Any;
}
pub trait FolderIndexManager: IndexManager {
fn index_all_views(&self, views: Vec<Arc<View>>, workspace_id: String);
fn index_all_views(&self, views: Vec<Arc<View>>, workspace_id: Uuid);
fn index_view_changes(
&self,
views: Vec<Arc<View>>,
changes: Vec<FolderViewChange>,
workspace_id: String,
workspace_id: Uuid,
);
}
+1 -7
View File
@@ -18,7 +18,6 @@ flowy-error = { workspace = true, features = [
"impl_from_serde",
] }
flowy-notification.workspace = true
flowy-sqlite.workspace = true
flowy-user.workspace = true
flowy-search-pub.workspace = true
flowy-folder = { workspace = true }
@@ -37,12 +36,7 @@ async-stream = "0.3.4"
strsim = "0.11.0"
strum_macros = "0.26.1"
tantivy = { version = "0.22.0" }
tempfile = "3.9.0"
validator = { workspace = true, features = ["derive"] }
diesel.workspace = true
diesel_derives = { version = "2.1.0", features = ["sqlite", "r2d2"] }
diesel_migrations = { version = "2.1.0", features = ["sqlite"] }
uuid.workspace = true
[build-dependencies]
flowy-codegen.workspace = true
@@ -1,10 +1,11 @@
use std::sync::Arc;
use tracing::{trace, warn};
use flowy_error::FlowyResult;
use flowy_folder::{manager::FolderManager, ViewLayout};
use flowy_search_pub::cloud::SearchCloudService;
use lib_infra::async_trait::async_trait;
use std::str::FromStr;
use std::sync::Arc;
use tracing::{trace, warn};
use uuid::Uuid;
use crate::{
entities::{IndexTypePB, ResultIconPB, ResultIconTypePB, SearchFilterPB, SearchResultPB},
@@ -49,6 +50,7 @@ impl SearchHandler for DocumentSearchHandler {
None => return Ok(vec![]),
};
let workspace_id = Uuid::from_str(&workspace_id)?;
let results = self
.cloud_service
.document_search(&workspace_id, query)
@@ -61,7 +63,7 @@ impl SearchHandler for DocumentSearchHandler {
let mut search_results: Vec<SearchResultPB> = vec![];
for result in results {
if let Some(view) = views.iter().find(|v| v.id == result.object_id) {
if let Some(view) = views.iter().find(|v| v.id == result.object_id.to_string()) {
// If there is no View for the result, we don't add it to the results
// If possible we will extract the icon to display for the result
let icon: Option<ResultIconPB> = match view.icon.clone() {
@@ -77,12 +79,12 @@ impl SearchHandler for DocumentSearchHandler {
search_results.push(SearchResultPB {
index_type: IndexTypePB::Document,
view_id: result.object_id.clone(),
id: result.object_id.clone(),
view_id: result.object_id.to_string(),
id: result.object_id.to_string(),
data: view.name.clone(),
icon,
score: result.score,
workspace_id: result.workspace_id,
workspace_id: result.workspace_id.to_string(),
preview: result.preview,
});
} else {
@@ -20,13 +20,13 @@ use flowy_error::{FlowyError, FlowyResult};
use flowy_search_pub::entities::{FolderIndexManager, IndexManager, IndexableData};
use flowy_user::services::authenticate_user::AuthenticateUser;
use super::entities::FolderIndexData;
use strsim::levenshtein;
use tantivy::{
collector::TopDocs, directory::MmapDirectory, doc, query::QueryParser, schema::Field, Document,
Index, IndexReader, IndexWriter, TantivyDocument, Term,
};
use super::entities::FolderIndexData;
use uuid::Uuid;
#[derive(Clone)]
pub struct FolderIndexManagerImpl {
@@ -139,7 +139,7 @@ impl FolderIndexManagerImpl {
title_field => data.data.clone(),
icon_field => icon.unwrap_or_default(),
icon_ty_field => icon_ty,
workspace_id_field => data.workspace_id.clone(),
workspace_id_field => data.workspace_id.to_string(),
]);
}
@@ -293,9 +293,9 @@ impl IndexManager for FolderIndexManagerImpl {
.unwrap_or(false)
}
fn set_index_content_receiver(&self, mut rx: IndexContentReceiver, workspace_id: String) {
fn set_index_content_receiver(&self, mut rx: IndexContentReceiver, workspace_id: Uuid) {
let indexer = self.clone();
let wid = workspace_id.clone();
let wid = workspace_id;
tokio::spawn(async move {
while let Ok(msg) = rx.recv().await {
match msg {
@@ -306,7 +306,7 @@ impl IndexManager for FolderIndexManagerImpl {
data: view.name,
icon: view.icon,
layout: view.layout,
workspace_id: wid.clone(),
workspace_id: wid,
});
},
Err(err) => tracing::error!("FolderIndexManager error deserialize: {:?}", err),
@@ -318,7 +318,7 @@ impl IndexManager for FolderIndexManagerImpl {
data: view.name,
icon: view.icon,
layout: view.layout,
workspace_id: wid.clone(),
workspace_id: wid,
});
},
Err(err) => tracing::error!("FolderIndexManager error deserialize: {:?}", err),
@@ -352,7 +352,7 @@ impl IndexManager for FolderIndexManagerImpl {
title_field => data.data,
icon_field => icon.unwrap_or_default(),
icon_ty_field => icon_ty,
workspace_id_field => data.workspace_id.clone(),
workspace_id_field => data.workspace_id.to_string(),
]);
index_writer.commit()?;
@@ -389,7 +389,7 @@ impl IndexManager for FolderIndexManagerImpl {
title_field => data.data,
icon_field => icon.unwrap_or_default(),
icon_ty_field => icon_ty,
workspace_id_field => data.workspace_id,
workspace_id_field => data.workspace_id.to_string(),
]);
index_writer.commit()?;
@@ -400,14 +400,14 @@ impl IndexManager for FolderIndexManagerImpl {
/// Removes all indexes that are related by workspace id. This is useful
/// for cleaning indexes when eg. removing/leaving a workspace.
///
fn remove_indices_for_workspace(&self, workspace_id: String) -> Result<(), FlowyError> {
fn remove_indices_for_workspace(&self, workspace_id: Uuid) -> Result<(), FlowyError> {
let mut index_writer = self.get_index_writer()?;
let folder_schema = self.get_folder_schema()?;
let id_field = folder_schema
.schema
.get_field(FOLDER_WORKSPACE_ID_FIELD_NAME)?;
let delete_term = Term::from_field_text(id_field, &workspace_id);
let delete_term = Term::from_field_text(id_field, &workspace_id.to_string());
index_writer.delete_term(delete_term);
index_writer.commit()?;
@@ -421,10 +421,10 @@ impl IndexManager for FolderIndexManagerImpl {
}
impl FolderIndexManager for FolderIndexManagerImpl {
fn index_all_views(&self, views: Vec<Arc<View>>, workspace_id: String) {
fn index_all_views(&self, views: Vec<Arc<View>>, workspace_id: Uuid) {
let indexable_data = views
.into_iter()
.map(|view| IndexableData::from_view(view, workspace_id.clone()))
.map(|view| IndexableData::from_view(view, workspace_id))
.collect();
let _ = self.index_all(indexable_data);
@@ -434,7 +434,7 @@ impl FolderIndexManager for FolderIndexManagerImpl {
&self,
views: Vec<Arc<View>>,
changes: Vec<FolderViewChange>,
workspace_id: String,
workspace_id: Uuid,
) {
let mut views_iter = views.into_iter();
for change in changes {
@@ -442,14 +442,14 @@ impl FolderIndexManager for FolderIndexManagerImpl {
FolderViewChange::Inserted { view_id } => {
let view = views_iter.find(|view| view.id == view_id);
if let Some(view) = view {
let indexable_data = IndexableData::from_view(view, workspace_id.clone());
let indexable_data = IndexableData::from_view(view, workspace_id);
let _ = self.add_index(indexable_data);
}
},
FolderViewChange::Updated { view_id } => {
let view = views_iter.find(|view| view.id == view_id);
if let Some(view) = view {
let indexable_data = IndexableData::from_view(view, workspace_id.clone());
let indexable_data = IndexableData::from_view(view, workspace_id);
let _ = self.update_index(indexable_data);
}
},
@@ -60,7 +60,7 @@ impl AFCloudConfiguration {
let enable_sync_trace = std::env::var(APPFLOWY_ENABLE_SYNC_TRACE)
.map(|v| v == "true" || v == "1")
.unwrap_or(false);
.unwrap_or(true);
Ok(Self {
base_url,
@@ -1,4 +1,5 @@
use flowy_error::FlowyResult;
use uuid::Uuid;
pub const USER_SIGN_IN_URL: &str = "sign_in_url";
pub const USER_UUID: &str = "uuid";
@@ -8,5 +9,5 @@ pub const USER_DEVICE_ID: &str = "device_id";
/// Represents a user that is currently using the server.
pub trait ServerUser: Send + Sync {
/// different user might return different workspace id.
fn workspace_id(&self) -> FlowyResult<String>;
fn workspace_id(&self) -> FlowyResult<Uuid>;
}
@@ -1,3 +1,4 @@
#![allow(unused_variables)]
use crate::af_cloud::AFServer;
use client_api::entity::ai_dto::{
ChatQuestionQuery, CompleteTextParams, RepeatedRelatedQuestion, ResponseFormat,
@@ -18,6 +19,7 @@ use serde_json::Value;
use std::collections::HashMap;
use std::path::Path;
use tracing::trace;
use uuid::Uuid;
pub(crate) struct AFCloudChatCloudServiceImpl<T> {
pub inner: T,
@@ -30,10 +32,10 @@ where
{
async fn create_chat(
&self,
_uid: &i64,
workspace_id: &str,
chat_id: &str,
rag_ids: Vec<String>,
uid: &i64,
workspace_id: &Uuid,
chat_id: &Uuid,
rag_ids: Vec<Uuid>,
) -> Result<(), FlowyError> {
let chat_id = chat_id.to_string();
let try_get_client = self.inner.try_get_client();
@@ -52,13 +54,12 @@ where
async fn create_question(
&self,
workspace_id: &str,
chat_id: &str,
workspace_id: &Uuid,
chat_id: &Uuid,
message: &str,
message_type: ChatMessageType,
metadata: &[ChatMessageMetadata],
) -> Result<ChatMessage, FlowyError> {
let workspace_id = workspace_id.to_string();
let chat_id = chat_id.to_string();
let try_get_client = self.inner.try_get_client();
let params = CreateChatMessageParams {
@@ -68,7 +69,7 @@ where
};
let message = try_get_client?
.create_question(&workspace_id, &chat_id, params)
.create_question(workspace_id, &chat_id, params)
.await
.map_err(FlowyError::from)?;
Ok(message)
@@ -76,8 +77,8 @@ where
async fn create_answer(
&self,
workspace_id: &str,
chat_id: &str,
workspace_id: &Uuid,
chat_id: &Uuid,
message: &str,
question_id: i64,
metadata: Option<serde_json::Value>,
@@ -89,7 +90,7 @@ where
question_message_id: question_id,
};
let message = try_get_client?
.save_answer(workspace_id, chat_id, params)
.save_answer(workspace_id, chat_id.to_string().as_str(), params)
.await
.map_err(FlowyError::from)?;
Ok(message)
@@ -97,8 +98,8 @@ where
async fn stream_answer(
&self,
workspace_id: &str,
chat_id: &str,
workspace_id: &Uuid,
chat_id: &Uuid,
message_id: i64,
format: ResponseFormat,
ai_model: Option<AIModel>,
@@ -129,13 +130,17 @@ where
async fn get_answer(
&self,
workspace_id: &str,
chat_id: &str,
workspace_id: &Uuid,
chat_id: &Uuid,
question_message_id: i64,
) -> Result<ChatMessage, FlowyError> {
let try_get_client = self.inner.try_get_client();
let resp = try_get_client?
.get_answer(workspace_id, chat_id, question_message_id)
.get_answer(
workspace_id,
chat_id.to_string().as_str(),
question_message_id,
)
.await
.map_err(FlowyError::from)?;
Ok(resp)
@@ -143,14 +148,14 @@ where
async fn get_chat_messages(
&self,
workspace_id: &str,
chat_id: &str,
workspace_id: &Uuid,
chat_id: &Uuid,
offset: MessageCursor,
limit: u64,
) -> Result<RepeatedChatMessage, FlowyError> {
let try_get_client = self.inner.try_get_client();
let resp = try_get_client?
.get_chat_messages(workspace_id, chat_id, offset, limit)
.get_chat_messages(workspace_id, chat_id.to_string().as_str(), offset, limit)
.await
.map_err(FlowyError::from)?;
@@ -159,13 +164,17 @@ where
async fn get_question_from_answer_id(
&self,
workspace_id: &str,
chat_id: &str,
workspace_id: &Uuid,
chat_id: &Uuid,
answer_message_id: i64,
) -> Result<ChatMessage, FlowyError> {
let try_get_client = self.inner.try_get_client()?;
let resp = try_get_client
.get_question_message_from_answer_id(workspace_id, chat_id, answer_message_id)
.get_question_message_from_answer_id(
workspace_id,
chat_id.to_string().as_str(),
answer_message_id,
)
.await
.map_err(FlowyError::from)?
.ok_or_else(FlowyError::record_not_found)?;
@@ -175,13 +184,13 @@ where
async fn get_related_message(
&self,
workspace_id: &str,
chat_id: &str,
workspace_id: &Uuid,
chat_id: &Uuid,
message_id: i64,
) -> Result<RepeatedRelatedQuestion, FlowyError> {
let try_get_client = self.inner.try_get_client();
let resp = try_get_client?
.get_chat_related_question(workspace_id, chat_id, message_id)
.get_chat_related_question(workspace_id, chat_id.to_string().as_str(), message_id)
.await
.map_err(FlowyError::from)?;
@@ -190,7 +199,7 @@ where
async fn stream_complete(
&self,
workspace_id: &str,
workspace_id: &Uuid,
params: CompleteTextParams,
ai_model: Option<AIModel>,
) -> Result<StreamComplete, FlowyError> {
@@ -207,10 +216,10 @@ where
async fn embed_file(
&self,
_workspace_id: &str,
_file_path: &Path,
_chat_id: &str,
_metadata: Option<HashMap<String, Value>>,
workspace_id: &Uuid,
file_path: &Path,
chat_id: &Uuid,
metadata: Option<HashMap<String, Value>>,
) -> Result<(), FlowyError> {
return Err(
FlowyError::not_support()
@@ -218,7 +227,7 @@ where
);
}
async fn get_local_ai_config(&self, workspace_id: &str) -> Result<LocalAIConfig, FlowyError> {
async fn get_local_ai_config(&self, workspace_id: &Uuid) -> Result<LocalAIConfig, FlowyError> {
let system = get_operating_system();
let platform = match system {
OperatingSystem::MacOS => "macos",
@@ -232,51 +241,51 @@ where
let config = self
.inner
.try_get_client()?
.get_local_ai_config(workspace_id, platform)
.get_local_ai_config(workspace_id.to_string().as_str(), platform)
.await?;
Ok(config)
}
async fn get_workspace_plan(
&self,
workspace_id: &str,
workspace_id: &Uuid,
) -> Result<Vec<SubscriptionPlan>, FlowyError> {
let plans = self
.inner
.try_get_client()?
.get_active_workspace_subscriptions(workspace_id)
.get_active_workspace_subscriptions(workspace_id.to_string().as_str())
.await?;
Ok(plans)
}
async fn get_chat_settings(
&self,
workspace_id: &str,
chat_id: &str,
workspace_id: &Uuid,
chat_id: &Uuid,
) -> Result<ChatSettings, FlowyError> {
let settings = self
.inner
.try_get_client()?
.get_chat_settings(workspace_id, chat_id)
.get_chat_settings(workspace_id, chat_id.to_string().as_str())
.await?;
Ok(settings)
}
async fn update_chat_settings(
&self,
workspace_id: &str,
chat_id: &str,
workspace_id: &Uuid,
chat_id: &Uuid,
params: UpdateChatParams,
) -> Result<(), FlowyError> {
self
.inner
.try_get_client()?
.update_chat_settings(workspace_id, chat_id, params)
.update_chat_settings(workspace_id, chat_id.to_string().as_str(), params)
.await?;
Ok(())
}
async fn get_available_models(&self, workspace_id: &str) -> Result<ModelList, FlowyError> {
async fn get_available_models(&self, workspace_id: &Uuid) -> Result<ModelList, FlowyError> {
let list = self
.inner
.try_get_client()?
@@ -285,11 +294,11 @@ where
Ok(list)
}
async fn get_workspace_default_model(&self, workspace_id: &str) -> Result<String, FlowyError> {
async fn get_workspace_default_model(&self, workspace_id: &Uuid) -> Result<String, FlowyError> {
let setting = self
.inner
.try_get_client()?
.get_workspace_settings(workspace_id)
.get_workspace_settings(workspace_id.to_string().as_str())
.await?;
Ok(setting.ai_model)
}
@@ -1,3 +1,7 @@
#![allow(unused_variables)]
use crate::af_cloud::define::ServerUser;
use crate::af_cloud::impls::util::check_request_workspace_id_is_match;
use crate::af_cloud::AFServer;
use client_api::entity::ai_dto::{
SummarizeRowData, SummarizeRowParams, TranslateRowData, TranslateRowParams,
};
@@ -6,20 +10,16 @@ use client_api::entity::{CreateCollabParams, QueryCollab, QueryCollabParams};
use client_api::error::ErrorCode::RecordNotFound;
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::{
DatabaseAIService, DatabaseCloudService, DatabaseSnapshot, EncodeCollabByOid, SummaryRowContent,
TranslateRowContent, TranslateRowResponse,
};
use flowy_error::FlowyError;
use lib_infra::async_trait::async_trait;
use crate::af_cloud::define::ServerUser;
use crate::af_cloud::impls::util::check_request_workspace_id_is_match;
use crate::af_cloud::AFServer;
use serde_json::{Map, Value};
use std::sync::Arc;
use tracing::{error, instrument};
use uuid::Uuid;
pub(crate) struct AFCloudDatabaseCloudServiceImpl<T> {
pub inner: T,
@@ -35,23 +35,21 @@ where
#[allow(clippy::blocks_in_conditions)]
async fn get_database_encode_collab(
&self,
object_id: &str,
object_id: &Uuid,
collab_type: CollabType,
workspace_id: &str,
workspace_id: &Uuid,
) -> Result<Option<EncodedCollab>, FlowyError> {
let workspace_id = workspace_id.to_string();
let object_id = object_id.to_string();
let try_get_client = self.inner.try_get_client();
let cloned_user = self.user.clone();
let params = QueryCollabParams {
workspace_id: workspace_id.clone(),
inner: QueryCollab::new(object_id.clone(), collab_type),
workspace_id: *workspace_id,
inner: QueryCollab::new(*object_id, collab_type),
};
let result = try_get_client?.get_collab(params).await;
match result {
Ok(data) => {
check_request_workspace_id_is_match(
&workspace_id,
workspace_id,
&cloned_user,
format!("get database object: {}:{}", object_id, collab_type),
)?;
@@ -71,17 +69,17 @@ where
#[allow(clippy::blocks_in_conditions)]
async fn create_database_encode_collab(
&self,
object_id: &str,
object_id: &Uuid,
collab_type: CollabType,
workspace_id: &str,
workspace_id: &Uuid,
encoded_collab: EncodedCollab,
) -> Result<(), FlowyError> {
let encoded_collab_v1 = encoded_collab
.encode_to_bytes()
.map_err(|err| FlowyError::internal().with_context(err))?;
let params = CreateCollabParams {
workspace_id: workspace_id.to_string(),
object_id: object_id.to_string(),
workspace_id: *workspace_id,
object_id: *object_id,
encoded_collab_v1,
collab_type,
};
@@ -92,11 +90,10 @@ where
#[instrument(level = "debug", skip_all)]
async fn batch_get_database_encode_collab(
&self,
object_ids: Vec<String>,
object_ids: Vec<Uuid>,
object_ty: CollabType,
workspace_id: &str,
workspace_id: &Uuid,
) -> Result<EncodeCollabByOid, FlowyError> {
let workspace_id = workspace_id.to_string();
let try_get_client = self.inner.try_get_client();
let cloned_user = self.user.clone();
let client = try_get_client?;
@@ -104,8 +101,8 @@ where
.into_iter()
.map(|object_id| QueryCollab::new(object_id, object_ty))
.collect();
let results = client.batch_get_collab(&workspace_id, params).await?;
check_request_workspace_id_is_match(&workspace_id, &cloned_user, "batch get database object")?;
let results = client.batch_get_collab(workspace_id, params).await?;
check_request_workspace_id_is_match(workspace_id, &cloned_user, "batch get database object")?;
Ok(
results
.0
@@ -131,8 +128,8 @@ where
async fn get_database_collab_object_snapshots(
&self,
_object_id: &str,
_limit: usize,
object_id: &Uuid,
limit: usize,
) -> Result<Vec<DatabaseSnapshot>, FlowyError> {
Ok(vec![])
}
@@ -145,17 +142,17 @@ where
{
async fn summary_database_row(
&self,
workspace_id: &str,
_object_id: &str,
summary_row: SummaryRowContent,
workspace_id: &Uuid,
_object_id: &Uuid,
_summary_row: SummaryRowContent,
) -> Result<String, FlowyError> {
let try_get_client = self.inner.try_get_client();
let map: Map<String, Value> = summary_row
let map: Map<String, Value> = _summary_row
.into_iter()
.map(|(key, value)| (key, Value::String(value)))
.collect();
let params = SummarizeRowParams {
workspace_id: workspace_id.to_string(),
workspace_id: *workspace_id,
data: SummarizeRowData::Content(map),
};
let data = try_get_client?.summarize_row(params).await?;
@@ -164,19 +161,21 @@ where
async fn translate_database_row(
&self,
workspace_id: &str,
translate_row: TranslateRowContent,
language: &str,
workspace_id: &Uuid,
_translate_row: TranslateRowContent,
_language: &str,
) -> Result<TranslateRowResponse, FlowyError> {
let workspace_id = workspace_id.to_string();
let try_get_client = self.inner.try_get_client();
let data = TranslateRowData {
cells: translate_row,
language: language.to_string(),
cells: _translate_row,
language: _language.to_string(),
include_header: false,
};
let params = TranslateRowParams { workspace_id, data };
let params = TranslateRowParams {
workspace_id: workspace_id.to_string(),
data,
};
let data = try_get_client?.translate_row(params).await?;
Ok(data)
}
@@ -1,3 +1,4 @@
#![allow(unused_variables)]
use client_api::entity::{CreateCollabParams, QueryCollab, QueryCollabParams};
use collab::core::collab::DataSource;
use collab::core::origin::CollabOrigin;
@@ -5,12 +6,12 @@ use collab::entity::EncodedCollab;
use collab::preclude::Collab;
use collab_document::document::Document;
use collab_entity::CollabType;
use std::sync::Arc;
use tracing::instrument;
use flowy_document_pub::cloud::*;
use flowy_error::FlowyError;
use lib_infra::async_trait::async_trait;
use std::sync::Arc;
use tracing::instrument;
use uuid::Uuid;
use crate::af_cloud::define::ServerUser;
use crate::af_cloud::impls::util::check_request_workspace_id_is_match;
@@ -29,12 +30,12 @@ where
#[instrument(level = "debug", skip_all, fields(document_id = %document_id))]
async fn get_document_doc_state(
&self,
document_id: &str,
workspace_id: &str,
document_id: &Uuid,
workspace_id: &Uuid,
) -> Result<Vec<u8>, FlowyError> {
let params = QueryCollabParams {
workspace_id: workspace_id.to_string(),
inner: QueryCollab::new(document_id.to_string(), CollabType::Document),
workspace_id: *workspace_id,
inner: QueryCollab::new(*document_id, CollabType::Document),
};
let doc_state = self
.inner
@@ -57,9 +58,9 @@ where
async fn get_document_snapshots(
&self,
_document_id: &str,
_limit: usize,
_workspace_id: &str,
document_id: &Uuid,
limit: usize,
workspace_id: &str,
) -> Result<Vec<DocumentSnapshot>, FlowyError> {
Ok(vec![])
}
@@ -67,12 +68,12 @@ where
#[instrument(level = "debug", skip_all)]
async fn get_document_data(
&self,
document_id: &str,
workspace_id: &str,
document_id: &Uuid,
workspace_id: &Uuid,
) -> Result<Option<DocumentData>, FlowyError> {
let params = QueryCollabParams {
workspace_id: workspace_id.to_string(),
inner: QueryCollab::new(document_id.to_string(), CollabType::Document),
workspace_id: *workspace_id,
inner: QueryCollab::new(*document_id, CollabType::Document),
};
let doc_state = self
.inner
@@ -89,7 +90,7 @@ where
)?;
let collab = Collab::new_with_source(
CollabOrigin::Empty,
document_id,
document_id.to_string().as_str(),
DataSource::DocStateV1(doc_state),
vec![],
false,
@@ -100,13 +101,13 @@ where
async fn create_document_collab(
&self,
workspace_id: &str,
document_id: &str,
workspace_id: &Uuid,
document_id: &Uuid,
encoded_collab: EncodedCollab,
) -> Result<(), FlowyError> {
let params = CreateCollabParams {
workspace_id: workspace_id.to_string(),
object_id: document_id.to_string(),
workspace_id: *workspace_id,
object_id: *document_id,
encoded_collab_v1: encoded_collab
.encode_to_bytes()
.map_err(|err| FlowyError::internal().with_context(err))?,
Loaded 100 of 125 files, more files were not shown because too many files have changed in this diff. Show more