feat: support Inline page reference #2196 (#2898)

This commit is contained in:
Muhammad Rizwan 2023-06-29 07:04:24 +05:00 committed by GitHub
parent a17d6b7eec
commit 30b52a29fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 590 additions and 43 deletions

View File

@ -1,4 +1,5 @@
import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart';
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
@ -56,21 +57,21 @@ void main() {
);
});
// testWidgets('insert a referenced calendar', (tester) async {
// await tester.initializeAppFlowy();
// await tester.tapGoButton();
testWidgets('insert a referenced calendar', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// await insertReferenceDatabase(tester, ViewLayoutPB.Calendar);
await insertReferenceDatabase(tester, ViewLayoutPB.Calendar);
// // validate the referenced grid is inserted
// expect(
// find.descendant(
// of: find.byType(AppFlowyEditor),
// matching: find.byType(CalendarPage),
// ),
// findsOneWidget,
// );
// });
// validate the referenced grid is inserted
expect(
find.descendant(
of: find.byType(AppFlowyEditor),
matching: find.byType(CalendarPage),
),
findsOneWidget,
);
});
});
}
@ -93,7 +94,7 @@ Future<void> insertReferenceDatabase(
);
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
// insert a referenced grid
// insert a referenced view
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
layout.referencedMenuName,

View File

@ -0,0 +1,129 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flowy_infra_ui/widget/error_page.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('inline page view in document', () {
const location = 'inline_page';
setUp(() async {
await TestFolder.cleanTestLocation(location);
await TestFolder.setTestLocation(location);
});
tearDown(() async {
await TestFolder.cleanTestLocation(null);
});
testWidgets('insert a inline page - grid', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await insertingInlinePage(tester, ViewLayoutPB.Grid);
final mentionBlock = find.byType(MentionPageBlock);
expect(mentionBlock, findsOneWidget);
await tester.tapButton(mentionBlock);
});
testWidgets('insert a inline page - board', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await insertingInlinePage(tester, ViewLayoutPB.Board);
final mentionBlock = find.byType(MentionPageBlock);
expect(mentionBlock, findsOneWidget);
await tester.tapButton(mentionBlock);
});
testWidgets('insert a inline page - calendar', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await insertingInlinePage(tester, ViewLayoutPB.Calendar);
final mentionBlock = find.byType(MentionPageBlock);
expect(mentionBlock, findsOneWidget);
await tester.tapButton(mentionBlock);
});
testWidgets('insert a inline page - document', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await insertingInlinePage(tester, ViewLayoutPB.Document);
final mentionBlock = find.byType(MentionPageBlock);
expect(mentionBlock, findsOneWidget);
await tester.tapButton(mentionBlock);
});
testWidgets('insert a inline page and rename it', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
final pageName = await insertingInlinePage(tester, ViewLayoutPB.Document);
// rename
await tester.hoverOnPageName(pageName);
const newName = 'RenameToNewPageName';
await tester.renamePage(newName);
final finder = find.descendant(
of: find.byType(MentionPageBlock),
matching: find.findTextInFlowyText(newName),
);
expect(finder, findsOneWidget);
});
testWidgets('insert a inline page and delete it', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
final pageName = await insertingInlinePage(tester, ViewLayoutPB.Grid);
// rename
await tester.hoverOnPageName(pageName);
await tester.tapDeletePageButton();
final finder = find.descendant(
of: find.byType(MentionPageBlock),
matching: find.findTextInFlowyText(pageName),
);
expect(finder, findsOneWidget);
await tester.tapButton(finder);
expect(find.byType(FlowyErrorPage), findsOneWidget);
});
});
}
/// Insert a referenced database of [layout] into the document
Future<String> insertingInlinePage(
WidgetTester tester,
ViewLayoutPB layout,
) async {
// create a new grid
final id = uuid();
final name = '${layout.name}_$id';
await tester.createNewPageWithName(
layout,
name,
);
// create a new document
await tester.createNewPageWithName(
ViewLayoutPB.Document,
'insert_a_inline_page_${layout.name}',
);
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
// insert a inline page
await tester.editor.showAtMenu();
await tester.editor.tapAtMenuItemWithName(name);
return name;
}

View File

@ -32,6 +32,7 @@ class EditorOperations {
Future<void> tapLineOfEditorAt(int index) async {
final textBlocks = find.byType(TextBlockComponentWidget);
await tester.tapAt(tester.getTopRight(textBlocks.at(index)));
await tester.pumpAndSettle();
}
/// Hover on cover plugin button above the document
@ -114,6 +115,11 @@ class EditorOperations {
await tester.ime.insertCharacter('/');
}
/// trigger the slash command (selection menu)
Future<void> showAtMenu() async {
await tester.ime.insertCharacter('@');
}
/// Tap the slash menu item with [name]
///
/// Must call [showSlashMenu] first.
@ -121,4 +127,15 @@ class EditorOperations {
final slashMenuItem = find.text(name, findRichText: true);
await tester.tapButton(slashMenuItem);
}
/// Tap the at menu item with [name]
///
/// Must call [showAtMenu] first.
Future<void> tapAtMenuItemWithName(String name) async {
final atMenuItem = find.descendant(
of: find.byType(SelectionMenuWidget),
matching: find.text(name, findRichText: true),
);
await tester.tapButton(atMenuItem);
}
}

View File

@ -9,11 +9,11 @@ extension IME on WidgetTester {
class IMESimulator {
IMESimulator(this.tester) {
client = findDeltaTextInputClient();
client = findTextInputClient();
}
final WidgetTester tester;
late final DeltaTextInputClient client;
late final TextInputClient client;
Future<void> insertText(String text) async {
for (final c in text.characters) {
@ -27,28 +27,22 @@ class IMESimulator {
assert(false);
return;
}
final deltas = [
TextEditingDeltaInsertion(
textInserted: character,
oldText: value.text.replaceRange(
value.selection.start,
value.selection.end,
'',
),
insertionOffset: value.selection.baseOffset,
final text = value.text
.replaceRange(value.selection.start, value.selection.end, character);
final textEditingValue = TextEditingValue(
text: text,
selection: TextSelection.collapsed(
offset: value.selection.baseOffset + 1,
),
composing: TextRange.empty,
),
];
client.updateEditingValueWithDeltas(deltas);
);
client.updateEditingValue(textEditingValue);
await tester.pumpAndSettle();
}
DeltaTextInputClient findDeltaTextInputClient() {
TextInputClient findTextInputClient() {
final finder = find.byType(KeyboardServiceWidget);
final KeyboardServiceWidgetState state = tester.state(finder);
return state.textInputService as DeltaTextInputClient;
return state.textInputService as TextInputClient;
}
}

View File

@ -4,6 +4,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/bl
import 'package:appflowy/plugins/document/presentation/editor_plugins/database/referenced_database_menu_tem.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/inline_page/inline_page_reference.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@ -35,6 +36,8 @@ class AppFlowyEditorPage extends StatefulWidget {
class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
late final ScrollController effectiveScrollController;
final inlinePageReferenceService = InlinePageReferenceService();
final List<CommandShortcutEvent> commandShortcutEvents = [
...codeBlockCommands,
...standardCommandShortcutEvents,
@ -69,7 +72,11 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
late final Map<String, BlockComponentBuilder> blockComponentBuilders =
_customAppFlowyBlockComponentBuilders();
List<CharacterShortcutEvent> get characterShortcutEvents => [
// inline page reference list
...inlinePageReferenceShortcuts,
// code block
...codeBlockCharacterEvents,
@ -88,6 +95,18 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
), // remove the default slash command.
];
late final inlinePageReferenceShortcuts = [
inlinePageReferenceService.customPageLinkMenu(
character: '@',
style: styleCustomizer.selectionMenuStyleBuilder(),
),
// uncomment this to enable the inline page reference list
// inlinePageReferenceService.customPageLinkMenu(
// character: '+',
// style: styleCustomizer.selectionMenuStyleBuilder(),
// ),
];
late final showSlashMenu = customSlashCommand(
slashMenuItems,
shouldInsertSlash: false,

View File

@ -87,7 +87,8 @@ class _LinkToPageMenuState extends State<LinkToPageMenu> {
final Map<int, (ViewPB, ViewPB)> _items = {};
Future<List<(ViewPB, List<ViewPB>)>> fetchItems() async {
final items = await ViewBackendService().fetchViews(widget.layoutType);
final items =
await ViewBackendService().fetchViewsWithLayoutType(widget.layoutType);
int index = 0;
for (final (app, children) in items) {

View File

@ -0,0 +1,164 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
enum MentionType {
page;
static MentionType fromString(String value) {
switch (value) {
case 'page':
return page;
default:
throw UnimplementedError();
}
}
}
class MentionBlockKeys {
const MentionBlockKeys._();
static const mention = 'mention';
static const type = 'type'; // MentionType, String
static const pageId = 'page_id';
static const pageType = 'page_type';
static const pageName = 'page_name';
}
class InlinePageReferenceService {
customPageLinkMenu({
bool shouldInsertKeyword = false,
SelectionMenuStyle style = SelectionMenuStyle.light,
String character = '@',
}) {
return CharacterShortcutEvent(
key: 'show page link menu',
character: character,
handler: (editorState) async {
final items = await generatePageItems(character);
return _showPageSelectionMenu(
editorState,
items,
shouldInsertKeyword: shouldInsertKeyword,
style: style,
character: character,
);
},
);
}
SelectionMenuService? _selectionMenuService;
Future<bool> _showPageSelectionMenu(
EditorState editorState,
List<SelectionMenuItem> items, {
bool shouldInsertKeyword = true,
SelectionMenuStyle style = SelectionMenuStyle.light,
String character = '@',
}) async {
if (PlatformExtension.isMobile) {
return false;
}
final selection = editorState.selection;
if (selection == null) {
return false;
}
// delete the selection
await editorState.deleteSelection(selection);
final afterSelection = editorState.selection;
if (afterSelection == null || !afterSelection.isCollapsed) {
assert(false, 'the selection should be collapsed');
return true;
}
await editorState.insertTextAtPosition(
character,
position: selection.start,
);
() {
final context = editorState.getNodeAtPath(selection.start.path)?.context;
if (context != null) {
_selectionMenuService = SelectionMenu(
context: context,
editorState: editorState,
selectionMenuItems: items,
deleteSlashByDefault: false,
style: style,
itemCountFilter: 5,
);
_selectionMenuService?.show();
}
}();
return true;
}
Future<List<SelectionMenuItem>> generatePageItems(String character) async {
final service = ViewBackendService();
final List<(ViewPB, List<ViewPB>)> pbViews = await service.fetchViews(
(_, __) => true,
);
if (pbViews.isEmpty) {
return [];
}
final List<SelectionMenuItem> pages = [];
final List<ViewPB> views = [];
for (final element in pbViews) {
views.addAll(element.$2);
}
views.sort(((a, b) => b.createTime.compareTo(a.createTime)));
for (final view in views) {
final SelectionMenuItem pageSelectionMenuItem = SelectionMenuItem(
icon: (editorState, isSelected, style) => SelectableSvgWidget(
name: view.iconName,
isSelected: isSelected,
style: style,
),
keywords: [
view.name.toLowerCase(),
],
name: view.name,
handler: (editorState, menuService, context) async {
final selection = editorState.selection;
if (selection == null || !selection.isCollapsed) {
return;
}
final node = editorState.getNodeAtPath(selection.end.path);
final delta = node?.delta;
if (node == null || delta == null) {
return;
}
final index = selection.endIndex;
final lastKeywordIndex =
delta.toPlainText().substring(0, index).lastIndexOf(character);
// @page name -> $
// preload the page infos
pageMemorizer[view.id] = view;
final transaction = editorState.transaction
..replaceText(
node,
lastKeywordIndex,
index - lastKeywordIndex,
'\$',
attributes: {
MentionBlockKeys.mention: {
MentionBlockKeys.type: MentionType.page.name,
MentionBlockKeys.pageId: view.id,
}
},
);
await editorState.apply(transaction);
},
);
pages.add(pageSelectionMenuItem);
}
return pages;
}
}

View File

@ -0,0 +1,22 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/inline_page/inline_page_reference.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
import 'package:flutter/material.dart';
class MentionBlock extends StatelessWidget {
const MentionBlock({
super.key,
required this.mention,
});
final Map mention;
@override
Widget build(BuildContext context) {
final type = MentionType.fromString(mention[MentionBlockKeys.type]);
if (type == MentionType.page) {
final pageId = mention[MentionBlockKeys.pageId];
return MentionPageBlock(key: ValueKey(pageId), pageId: pageId);
}
throw UnimplementedError();
}
}

View File

@ -0,0 +1,143 @@
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
import 'package:appflowy/plugins/trash/application/trash_service.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/home/menu/menu.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart'
show EditorState, SelectionUpdateReason;
import 'package:collection/collection.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
final pageMemorizer = <String, ViewPB?>{};
class MentionPageBlock extends StatefulWidget {
const MentionPageBlock({
super.key,
required this.pageId,
});
final String pageId;
@override
State<MentionPageBlock> createState() => _MentionPageBlockState();
}
class _MentionPageBlockState extends State<MentionPageBlock> {
late final EditorState editorState;
late final Future<ViewPB?> viewPBFuture;
ViewListener? viewListener;
@override
void initState() {
super.initState();
editorState = context.read<EditorState>();
viewPBFuture = fetchView(widget.pageId);
viewListener = ViewListener(viewId: widget.pageId)
..start(
onViewUpdated: (p0) {
pageMemorizer[p0.id] = p0;
editorState.reload();
},
);
}
@override
void dispose() {
viewListener?.stop();
super.dispose();
}
@override
Widget build(BuildContext context) {
final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;
return FutureBuilder<ViewPB?>(
initialData: pageMemorizer[widget.pageId],
future: viewPBFuture,
builder: (context, state) {
final view = state.data;
// memorize the result
pageMemorizer[widget.pageId] = view;
if (view == null) {
return const SizedBox.shrink();
}
updateSelection();
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 2),
child: FlowyHover(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () => openPage(widget.pageId),
behavior: HitTestBehavior.translucent,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const HSpace(4),
FlowySvg(
name: view.layout.iconName,
size: const Size.square(18.0),
),
const HSpace(2),
FlowyText(
view.name,
decoration: TextDecoration.underline,
fontSize: fontSize,
),
const HSpace(2),
],
),
),
),
);
},
);
}
void openPage(String pageId) async {
final view = await fetchView(pageId);
if (view == null) {
Log.error('Page($pageId) not found');
return;
}
getIt<MenuSharedState>().latestOpenView = view;
}
Future<ViewPB?> fetchView(String pageId) async {
final views = await ViewBackendService().fetchViews((_, __) => true);
final flattenViews = views.expand((e) => [e.$1, ...e.$2]).toList();
final view = flattenViews.firstWhereOrNull(
(element) => element.id == pageId,
);
if (view == null) {
// try to fetch from trash
final trashViews = await TrashService().readTrash();
final trash = trashViews.fold(
(l) => l.items.firstWhereOrNull((element) => element.id == pageId),
(r) => null,
);
if (trash != null) {
return ViewPB()
..id = trash.id
..name = trash.name;
}
}
return view;
}
void updateSelection() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
editorState.updateSelectionWithReason(
editorState.selection,
reason: SelectionUpdateReason.transaction,
);
});
}
}

View File

@ -1,5 +1,7 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/inline_page/inline_page_reference.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide FlowySvg, Log;
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@ -57,6 +59,7 @@ class EditorStyleCustomizer {
),
),
),
textSpanDecorator: customizeAttributeDecorator,
);
}
@ -142,4 +145,28 @@ class EditorStyleCustomizer {
backgroundColor: theme.colorScheme.onTertiary,
);
}
InlineSpan customizeAttributeDecorator(
TextInsert textInsert,
TextSpan textSpan,
) {
final attributes = textInsert.attributes;
if (attributes == null) {
return textSpan;
}
final mention = attributes[MentionBlockKeys.mention] as Map?;
if (mention != null) {
final type = mention[MentionBlockKeys.type];
if (type == MentionType.page.name) {
return WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: MentionBlock(
key: ValueKey(mention[MentionBlockKeys.pageId]),
mention: mention,
),
);
}
}
return textSpan;
}
}

View File

@ -94,13 +94,21 @@ extension ViewExtension on ViewPB {
}
String get iconName {
switch (layout) {
return layout.iconName;
}
}
extension ViewLayoutExtension on ViewLayoutPB {
String get iconName {
switch (this) {
case ViewLayoutPB.Grid:
return 'editor/grid';
case ViewLayoutPB.Board:
return 'editor/board';
case ViewLayoutPB.Calendar:
return 'editor/calendar';
case ViewLayoutPB.Document:
return 'editor/documents';
default:
throw Exception('Unknown layout type');
}

View File

@ -154,8 +154,19 @@ class ViewBackendService {
return FolderEventMoveView(payload).send();
}
Future<List<(ViewPB, List<ViewPB>)>> fetchViewsWithLayoutType(
ViewLayoutPB? layoutType,
) async {
return fetchViews((workspace, view) {
if (layoutType != null) {
return view.layout == layoutType;
}
return true;
});
}
Future<List<(ViewPB, List<ViewPB>)>> fetchViews(
ViewLayoutPB layoutType,
bool Function(WorkspaceSettingPB workspace, ViewPB view) filter,
) async {
final result = <(ViewPB, List<ViewPB>)>[];
return FolderEventGetCurrentWorkspace().send().then((value) async {
@ -166,7 +177,7 @@ class ViewBackendService {
final childViews = await getChildViews(viewId: view.id).then(
(value) => value
.getLeftOrNull<List<ViewPB>>()
?.where((e) => e.layout == layoutType)
?.where((e) => filter(workspaces, e))
.toList(),
);
if (childViews != null && childViews.isNotEmpty) {

View File

@ -53,9 +53,9 @@ packages:
dependency: "direct main"
description:
path: "."
ref: cd0f67a
resolved-ref: cd0f67a48e40188114800fae9a0f59cafe15b0f2
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
ref: "250b1a5"
resolved-ref: "250b1a59856b337fc2d4b26a1dabdec265e80acf"
url: "https://github.com/AppFlowy-IO/appflowy-editor"
source: git
version: "1.0.4"
appflowy_popover:

View File

@ -45,8 +45,9 @@ dependencies:
# appflowy_editor: ^1.0.4
appflowy_editor:
git:
url: https://github.com/AppFlowy-IO/appflowy-editor.git
ref: cd0f67a
url: https://github.com/AppFlowy-IO/appflowy-editor
ref: 250b1a5
appflowy_popover:
path: packages/appflowy_popover

View File

@ -85,6 +85,7 @@ checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4"
[[package]]
name = "appflowy-integrate"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
dependencies = [
"anyhow",
"collab",
@ -886,6 +887,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
dependencies = [
"anyhow",
"bytes",
@ -903,6 +905,7 @@ dependencies = [
[[package]]
name = "collab-client-ws"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
dependencies = [
"bytes",
"collab-sync",
@ -920,6 +923,7 @@ dependencies = [
[[package]]
name = "collab-database"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
dependencies = [
"anyhow",
"async-trait",
@ -945,6 +949,7 @@ dependencies = [
[[package]]
name = "collab-derive"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
dependencies = [
"proc-macro2",
"quote",
@ -956,6 +961,7 @@ dependencies = [
[[package]]
name = "collab-document"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
dependencies = [
"anyhow",
"collab",
@ -973,6 +979,7 @@ dependencies = [
[[package]]
name = "collab-folder"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
dependencies = [
"anyhow",
"chrono",
@ -992,6 +999,7 @@ dependencies = [
[[package]]
name = "collab-persistence"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
dependencies = [
"bincode",
"chrono",
@ -1011,6 +1019,7 @@ dependencies = [
[[package]]
name = "collab-plugins"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
dependencies = [
"anyhow",
"async-trait",
@ -1041,6 +1050,7 @@ dependencies = [
[[package]]
name = "collab-sync"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
dependencies = [
"bytes",
"collab",