From 30b52a29fdc8fee2beabc954c95e0f2e5ab3e3a4 Mon Sep 17 00:00:00 2001 From: Muhammad Rizwan <47111784+rizwan3395@users.noreply.github.com> Date: Thu, 29 Jun 2023 07:04:24 +0500 Subject: [PATCH] feat: support Inline page reference #2196 (#2898) --- .../document/document_with_database_test.dart | 29 ++-- .../document_with_inline_page_test.dart | 129 ++++++++++++++ .../util/editor_test_operations.dart | 17 ++ .../integration_test/util/ime.dart | 32 ++-- .../document/presentation/editor_page.dart | 19 ++ .../base/link_to_page_widget.dart | 3 +- .../inline_page/inline_page_reference.dart | 164 ++++++++++++++++++ .../editor_plugins/mention/mention_block.dart | 22 +++ .../mention/mention_page_block.dart | 143 +++++++++++++++ .../document/presentation/editor_style.dart | 29 +++- .../workspace/application/view/view_ext.dart | 10 +- .../application/view/view_service.dart | 15 +- frontend/appflowy_flutter/pubspec.lock | 6 +- frontend/appflowy_flutter/pubspec.yaml | 5 +- frontend/rust-lib/Cargo.lock | 10 ++ 15 files changed, 590 insertions(+), 43 deletions(-) create mode 100644 frontend/appflowy_flutter/integration_test/document/document_with_inline_page_test.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_page/inline_page_reference.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_block.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart diff --git a/frontend/appflowy_flutter/integration_test/document/document_with_database_test.dart b/frontend/appflowy_flutter/integration_test/document/document_with_database_test.dart index 5f6cea1d18..5cae41e478 100644 --- a/frontend/appflowy_flutter/integration_test/document/document_with_database_test.dart +++ b/frontend/appflowy_flutter/integration_test/document/document_with_database_test.dart @@ -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 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, diff --git a/frontend/appflowy_flutter/integration_test/document/document_with_inline_page_test.dart b/frontend/appflowy_flutter/integration_test/document/document_with_inline_page_test.dart new file mode 100644 index 0000000000..c9a0bead19 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/document/document_with_inline_page_test.dart @@ -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 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; +} diff --git a/frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart b/frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart index 9d2c14abb5..5d558ca469 100644 --- a/frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart +++ b/frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart @@ -32,6 +32,7 @@ class EditorOperations { Future 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 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 tapAtMenuItemWithName(String name) async { + final atMenuItem = find.descendant( + of: find.byType(SelectionMenuWidget), + matching: find.text(name, findRichText: true), + ); + await tester.tapButton(atMenuItem); + } } diff --git a/frontend/appflowy_flutter/integration_test/util/ime.dart b/frontend/appflowy_flutter/integration_test/util/ime.dart index 30b1388e0d..963caf9779 100644 --- a/frontend/appflowy_flutter/integration_test/util/ime.dart +++ b/frontend/appflowy_flutter/integration_test/util/ime.dart @@ -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 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, - selection: TextSelection.collapsed( - offset: value.selection.baseOffset + 1, - ), - composing: TextRange.empty, + 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, ), - ]; - client.updateEditingValueWithDeltas(deltas); + composing: TextRange.empty, + ); + 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; } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index f92918b464..c94ea11342 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -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 { late final ScrollController effectiveScrollController; + final inlinePageReferenceService = InlinePageReferenceService(); + final List commandShortcutEvents = [ ...codeBlockCommands, ...standardCommandShortcutEvents, @@ -69,7 +72,11 @@ class _AppFlowyEditorPageState extends State { late final Map blockComponentBuilders = _customAppFlowyBlockComponentBuilders(); + List get characterShortcutEvents => [ + // inline page reference list + ...inlinePageReferenceShortcuts, + // code block ...codeBlockCharacterEvents, @@ -88,6 +95,18 @@ class _AppFlowyEditorPageState extends State { ), // 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, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart index 902a551f65..944b76e1d7 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart @@ -87,7 +87,8 @@ class _LinkToPageMenuState extends State { final Map _items = {}; Future)>> 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) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_page/inline_page_reference.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_page/inline_page_reference.dart new file mode 100644 index 0000000000..16e0a12e2d --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_page/inline_page_reference.dart @@ -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 _showPageSelectionMenu( + EditorState editorState, + List 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> generatePageItems(String character) async { + final service = ViewBackendService(); + final List<(ViewPB, List)> pbViews = await service.fetchViews( + (_, __) => true, + ); + if (pbViews.isEmpty) { + return []; + } + final List pages = []; + final List 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; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_block.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_block.dart new file mode 100644 index 0000000000..f88e0bdf43 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_block.dart @@ -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(); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart new file mode 100644 index 0000000000..d0337dd5e3 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart @@ -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 = {}; + +class MentionPageBlock extends StatefulWidget { + const MentionPageBlock({ + super.key, + required this.pageId, + }); + + final String pageId; + + @override + State createState() => _MentionPageBlockState(); +} + +class _MentionPageBlockState extends State { + late final EditorState editorState; + late final Future viewPBFuture; + ViewListener? viewListener; + + @override + void initState() { + super.initState(); + + editorState = context.read(); + 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().state.fontSize; + return FutureBuilder( + 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().latestOpenView = view; + } + + Future 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, + ); + }); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart index 39240591a4..14f951af35 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart @@ -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; + } } diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart index 065b55d8ff..4614ca033e 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart @@ -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'); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart index 6e8f71eb14..a577493fd9 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart @@ -154,8 +154,19 @@ class ViewBackendService { return FolderEventMoveView(payload).send(); } + Future)>> fetchViewsWithLayoutType( + ViewLayoutPB? layoutType, + ) async { + return fetchViews((workspace, view) { + if (layoutType != null) { + return view.layout == layoutType; + } + return true; + }); + } + Future)>> fetchViews( - ViewLayoutPB layoutType, + bool Function(WorkspaceSettingPB workspace, ViewPB view) filter, ) async { final result = <(ViewPB, List)>[]; return FolderEventGetCurrentWorkspace().send().then((value) async { @@ -166,7 +177,7 @@ class ViewBackendService { final childViews = await getChildViews(viewId: view.id).then( (value) => value .getLeftOrNull>() - ?.where((e) => e.layout == layoutType) + ?.where((e) => filter(workspaces, e)) .toList(), ); if (childViews != null && childViews.isNotEmpty) { diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index ea8db53286..272b0a1123 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -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: diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 8db6661592..b2bc5486e5 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -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 diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index a63ddb76d4..842e939b6d 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -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",