Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1dff84018 | ||
|
|
9de76b17a6 | ||
|
|
b791d6af70 | ||
|
|
0975f6c8c0 | ||
|
|
e870481ef9 | ||
|
|
c1f6a0efea | ||
|
|
5c19c08cb3 | ||
|
|
210ea0c2ac | ||
|
|
ab70dccc8e | ||
|
|
eb22d86f3e | ||
|
|
a7d5c389b0 | ||
|
|
71a6ba660f |
No files matched your search
+41
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
+37
@@ -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);
|
||||
|
||||
+11
-6
@@ -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(
|
||||
|
||||
+6
-3
@@ -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) {
|
||||
|
||||
+52
-10
@@ -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 {
|
||||
|
||||
+20
-1
@@ -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) {
|
||||
|
||||
+187
-1
@@ -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(),
|
||||
|
||||
+2
-4
@@ -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(),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
||||
+4
@@ -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,
|
||||
|
||||
+2
-2
@@ -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),
|
||||
]);
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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',
|
||||
|
||||
+508
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+33
-55
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
¤t_workspace.id,
|
||||
&chat_id,
|
||||
&Uuid::from_str(¤t_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(
|
||||
¤t_workspace.id,
|
||||
&chat_id,
|
||||
&Uuid::from_str(¤t_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);
|
||||
|
||||
+13
-12
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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(¶ms.chat_id).await?;
|
||||
let ai_model = self.get_active_model(¶ms.chat_id).await;
|
||||
let ai_model = self.get_active_model(¶ms.chat_id.to_string()).await;
|
||||
let question = chat.stream_chat_message(¶ms, 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)
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(¶ms.chat_id.value, params.rag_ids)
|
||||
.await?;
|
||||
let chat_id = Uuid::from_str(¶ms.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)
|
||||
}
|
||||
@@ -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"] }
|
||||
|
||||
|
||||
@@ -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>,
|
||||
|
||||
+41
-27
@@ -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, ¶ms.view_id)
|
||||
.duplicate_database(&duplicated_view_id, ¶ms.view_id.to_string())
|
||||
.await?;
|
||||
Ok(Some(encoded_collab))
|
||||
},
|
||||
ViewData::Data(data) => {
|
||||
let encoded_collab = self
|
||||
.0
|
||||
.create_database_with_data(¶ms.view_id, data.to_vec())
|
||||
.create_database_with_data(¶ms.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
|
||||
|
||||
@@ -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(¶ms.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?;
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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(¤t_parent_id)
|
||||
@@ -1151,12 +1162,13 @@ impl FolderManager {
|
||||
view.name.clone()
|
||||
};
|
||||
|
||||
let parent_view_id = Uuid::from_str(¤t_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(¤t_view_id, publish_name, layout)
|
||||
.get_publish_payload(&Uuid::from_str(¤t_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>,
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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
Reference in new issue
Block a user