diff --git a/frontend/appflowy_flutter/integration_test/document/document_inline_page_reference_test.dart b/frontend/appflowy_flutter/integration_test/document/document_inline_page_reference_test.dart new file mode 100644 index 0000000000..49d067f248 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/document/document_inline_page_reference_test.dart @@ -0,0 +1,136 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; +import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart'; +import 'package:flowy_infra/uuid.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../util/keyboard.dart'; +import '../util/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('insert inline document reference', () { + testWidgets('insert by slash menu', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + final name = await createDocumentToReference(tester); + + await tester.editor.tapLineOfEditorAt(0); + await tester.pumpAndSettle(); + + await triggerReferenceDocumentBySlashMenu(tester); + + // Search for prefix of document + await enterDocumentText(tester); + + // Select result + final optionFinder = find.descendant( + of: find.byType(LinkToPageMenu), + matching: find.text(name), + ); + + await tester.tap(optionFinder); + await tester.pumpAndSettle(); + + final mentionBlock = find.byType(MentionPageBlock); + expect(mentionBlock, findsOneWidget); + }); + + testWidgets('insert by `[[` character shortcut', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + final name = await createDocumentToReference(tester); + + await tester.editor.tapLineOfEditorAt(0); + await tester.pumpAndSettle(); + + await tester.ime.insertText('[['); + await tester.pumpAndSettle(); + + // Select result + await tester.editor.tapAtMenuItemWithName(name); + await tester.pumpAndSettle(); + + final mentionBlock = find.byType(MentionPageBlock); + expect(mentionBlock, findsOneWidget); + }); + + testWidgets('insert by `+` character shortcut', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + final name = await createDocumentToReference(tester); + + await tester.editor.tapLineOfEditorAt(0); + await tester.pumpAndSettle(); + + await tester.ime.insertText('+'); + await tester.pumpAndSettle(); + + // Select result + await tester.editor.tapAtMenuItemWithName(name); + await tester.pumpAndSettle(); + + final mentionBlock = find.byType(MentionPageBlock); + expect(mentionBlock, findsOneWidget); + }); + }); +} + +Future createDocumentToReference(WidgetTester tester) async { + final name = 'document_${uuid()}'; + + await tester.createNewPageWithName( + name: name, + layout: ViewLayoutPB.Document, + openAfterCreated: false, + ); + + // This is a workaround since the openAfterCreated + // option does not work in createNewPageWithName method + await tester.tap(find.byType(SingleInnerViewItem).first); + await tester.pumpAndSettle(); + + return name; +} + +Future triggerReferenceDocumentBySlashMenu(WidgetTester tester) async { + await tester.editor.showSlashMenu(); + await tester.pumpAndSettle(); + + // Search for referenced document action + await enterDocumentText(tester); + + // Select item + await FlowyTestKeyboard.simulateKeyDownEvent( + [ + LogicalKeyboardKey.enter, + ], + tester: tester, + ); + + await tester.pumpAndSettle(); +} + +Future enterDocumentText(WidgetTester tester) async { + await FlowyTestKeyboard.simulateKeyDownEvent( + [ + LogicalKeyboardKey.keyD, + LogicalKeyboardKey.keyO, + LogicalKeyboardKey.keyC, + LogicalKeyboardKey.keyU, + LogicalKeyboardKey.keyM, + LogicalKeyboardKey.keyE, + LogicalKeyboardKey.keyN, + LogicalKeyboardKey.keyT, + ], + tester: tester, + ); + await tester.pumpAndSettle(); +} diff --git a/frontend/appflowy_flutter/integration_test/document/document_test_runner.dart b/frontend/appflowy_flutter/integration_test/document/document_test_runner.dart index 2e9ca77f5e..42462c2658 100644 --- a/frontend/appflowy_flutter/integration_test/document/document_test_runner.dart +++ b/frontend/appflowy_flutter/integration_test/document/document_test_runner.dart @@ -16,6 +16,8 @@ import 'document_with_inline_page_test.dart' as document_with_inline_page_test; import 'document_with_outline_block_test.dart' as document_with_outline_block; import 'document_with_toggle_list_test.dart' as document_with_toggle_list_test; import 'edit_document_test.dart' as document_edit_test; +import 'document_inline_page_reference_test.dart' + as document_inline_page_reference_test; void startTesting() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -35,4 +37,5 @@ void startTesting() { document_text_direction_test.main(); document_option_action_test.main(); document_with_image_block_test.main(); + document_inline_page_reference_test.main(); } 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 index e071f3ee36..f33ce370ff 100644 --- 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 @@ -15,7 +15,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - await insertingInlinePage(tester, ViewLayoutPB.Grid); + await insertInlinePage(tester, ViewLayoutPB.Grid); final mentionBlock = find.byType(MentionPageBlock); expect(mentionBlock, findsOneWidget); @@ -26,7 +26,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - await insertingInlinePage(tester, ViewLayoutPB.Board); + await insertInlinePage(tester, ViewLayoutPB.Board); final mentionBlock = find.byType(MentionPageBlock); expect(mentionBlock, findsOneWidget); @@ -37,7 +37,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - await insertingInlinePage(tester, ViewLayoutPB.Calendar); + await insertInlinePage(tester, ViewLayoutPB.Calendar); final mentionBlock = find.byType(MentionPageBlock); expect(mentionBlock, findsOneWidget); @@ -48,7 +48,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - await insertingInlinePage(tester, ViewLayoutPB.Document); + await insertInlinePage(tester, ViewLayoutPB.Document); final mentionBlock = find.byType(MentionPageBlock); expect(mentionBlock, findsOneWidget); @@ -59,7 +59,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - final pageName = await insertingInlinePage(tester, ViewLayoutPB.Document); + final pageName = await insertInlinePage(tester, ViewLayoutPB.Document); // rename const newName = 'RenameToNewPageName'; @@ -78,7 +78,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - final pageName = await insertingInlinePage(tester, ViewLayoutPB.Grid); + final pageName = await insertInlinePage(tester, ViewLayoutPB.Grid); // rename await tester.hoverOnPageName( @@ -98,7 +98,7 @@ void main() { } /// Insert a referenced database of [layout] into the document -Future insertingInlinePage( +Future insertInlinePage( WidgetTester tester, ViewLayoutPB layout, ) async { @@ -110,15 +110,19 @@ Future insertingInlinePage( layout: layout, openAfterCreated: false, ); + // create a new document await tester.createNewPageWithName( name: 'insert_a_inline_page_${layout.name}', layout: ViewLayoutPB.Document, ); + // 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/document/edit_document_test.dart b/frontend/appflowy_flutter/integration_test/document/edit_document_test.dart index b120b5e332..280ff24577 100644 --- a/frontend/appflowy_flutter/integration_test/document/edit_document_test.dart +++ b/frontend/appflowy_flutter/integration_test/document/edit_document_test.dart @@ -113,9 +113,9 @@ const _sample = r''' [] Type followed by bullet or num to create a list. -[x] Click `+ New Page` button at the bottom of your sidebar to add a new page. +[x] Click `New Page` button at the bottom of your sidebar to add a new page. -[] Click `+` next to any page title in the sidebar to quickly add a new subpage, `Document`, `Grid`, or `Kanban Board`. +[] Click the plus sign next to any page title in the sidebar to quickly add a new subpage, `Document`, `Grid`, or `Kanban Board`. --- * bulleted list 1 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 51e99ceddf..7236a4b94b 100644 --- a/frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart +++ b/frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart @@ -160,7 +160,7 @@ class EditorOperations { await tester.ime.insertCharacter('/'); } - /// trigger the slash command (selection menu) + /// trigger the mention (@) command Future showAtMenu() async { await tester.ime.insertCharacter('@'); } 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 5f63347c5e..99f338bf80 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -1,6 +1,7 @@ import 'package:appflowy/plugins/document/application/doc_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_configuration.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/background_color/theme_background_color.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/base/page_reference_commands.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/i18n/editor_i18n.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/slash_menu_items.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; @@ -139,6 +140,21 @@ class _AppFlowyEditorPageState extends State { inlineActionsService, style: styleCustomizer.inlineActionsMenuStyleBuilder(), ), + + /// Inline page menu + /// - Using `[[` + pageReferenceShortcutBrackets( + context, + documentBloc.view.id, + styleCustomizer.inlineActionsMenuStyleBuilder(), + ), + + /// - Using `+` + pageReferenceShortcutPlusSign( + context, + documentBloc.view.id, + styleCustomizer.inlineActionsMenuStyleBuilder(), + ), ]; EditorStyleCustomizer get styleCustomizer => widget.styleCustomizer; @@ -322,6 +338,7 @@ class _AppFlowyEditorPageState extends State { referencedBoardMenuItem, inlineCalendarMenuItem(documentBloc), referencedCalendarMenuItem, + referencedDocumentMenuItem, calloutItem, outlineItem, mathEquationItem, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart index debf00f4e1..fba3c8644c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart @@ -1,5 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database_view/application/database_view_service.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; @@ -34,6 +35,7 @@ extension InsertDatabase on EditorState { Future insertReferencePage( ViewPB childView, + ViewLayoutPB viewType, ) async { final selection = this.selection; if (selection == null || !selection.isCollapsed) { @@ -50,22 +52,63 @@ extension InsertDatabase on EditorState { ); } + late Transaction transaction; + if (viewType == ViewLayoutPB.Document) { + transaction = await _insertDocumentReference( + childView, + selection, + node, + ); + } else { + transaction = await _insertDatabaseReference( + childView, + selection.end.path, + ); + } + + await apply(transaction); + } + + Future _insertDocumentReference( + ViewPB view, + Selection selection, + Node node, + ) async { + return transaction + ..replaceText( + node, + selection.end.offset, + 0, + r'$', + attributes: { + MentionBlockKeys.mention: { + MentionBlockKeys.type: MentionType.page.name, + MentionBlockKeys.pageId: view.id, + } + }, + ); + } + + Future _insertDatabaseReference( + ViewPB view, + List path, + ) async { // get the database id that the view is associated with - final databaseId = await DatabaseViewBackendService(viewId: childView.id) + final databaseId = await DatabaseViewBackendService(viewId: view.id) .getDatabaseId() .then((value) => value.swap().toOption().toNullable()); if (databaseId == null) { throw StateError( - 'The database associated with ${childView.id} could not be found while attempting to create a referenced ${childView.layout.name}.', + 'The database associated with ${view.id} could not be found while attempting to create a referenced ${view.layout.name}.', ); } - final prefix = _referencedDatabasePrefix(childView.layout); + final prefix = _referencedDatabasePrefix(view.layout); final ref = await ViewBackendService.createDatabaseLinkedView( - parentViewId: childView.id, - name: "$prefix ${childView.name}", - layoutType: childView.layout, + parentViewId: view.id, + name: "$prefix ${view.name}", + layoutType: view.layout, databaseId: databaseId, ).then((value) => value.swap().toOption().toNullable()); @@ -76,18 +119,17 @@ extension InsertDatabase on EditorState { ); } - final transaction = this.transaction; - transaction.insertNode( - selection.end.path, - Node( - type: _convertPageType(childView), - attributes: { - DatabaseBlockKeys.parentID: childView.id, - DatabaseBlockKeys.viewID: ref.id, - }, - ), - ); - await apply(transaction); + return transaction + ..insertNode( + path, + Node( + type: _convertPageType(view), + attributes: { + DatabaseBlockKeys.parentID: view.id, + DatabaseBlockKeys.viewID: ref.id, + }, + ), + ); } String _referencedDatabasePrefix(ViewLayoutPB layout) { 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 ff5ae9984a..0a87e2e189 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 @@ -1,4 +1,3 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/insert_page_command.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; @@ -42,7 +41,7 @@ void showLinkToPageMenu( hintText: pageType.toHintText(), onSelected: (appPB, viewPB) async { try { - await editorState.insertReferencePage(viewPB); + await editorState.insertReferencePage(viewPB, pageType); linkToPageMenuEntry.remove(); } on FlowyError catch (e) { Dialogs.show( @@ -188,6 +187,7 @@ class _LinkToPageMenuState extends State { ) { int index = 0; return FutureBuilder>( + future: items, builder: (context, snapshot) { if (snapshot.hasData && snapshot.connectionState == ConnectionState.done) { @@ -208,10 +208,7 @@ class _LinkToPageMenuState extends State { children.add( FlowyButton( isSelected: index == _selectedIndex, - leftIcon: FlowySvg( - view.iconData, - color: Theme.of(context).iconTheme.color, - ), + leftIcon: view.defaultIcon(), text: FlowyText.regular(view.name), onTap: () => widget.onSelected(view, view), ), @@ -229,7 +226,6 @@ class _LinkToPageMenuState extends State { return const Center(child: CircularProgressIndicator()); }, - future: items, ); } } @@ -239,13 +235,14 @@ extension on ViewLayoutPB { switch (this) { case ViewLayoutPB.Grid: return LocaleKeys.document_slashMenu_grid_selectAGridToLinkTo.tr(); - case ViewLayoutPB.Board: return LocaleKeys.document_slashMenu_board_selectABoardToLinkTo.tr(); - case ViewLayoutPB.Calendar: return LocaleKeys.document_slashMenu_calendar_selectACalendarToLinkTo .tr(); + case ViewLayoutPB.Document: + return LocaleKeys.document_slashMenu_document_selectADocumentToLinkTo + .tr(); default: throw Exception('Unknown layout type'); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/page_reference_commands.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/page_reference_commands.dart new file mode 100644 index 0000000000..0df49665a0 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/page_reference_commands.dart @@ -0,0 +1,117 @@ +import 'package:appflowy/plugins/inline_actions/handlers/inline_page_reference.dart'; +import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart'; +import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart'; +import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; + +const _bracketChar = '['; +const _plusChar = '+'; + +CharacterShortcutEvent pageReferenceShortcutBrackets( + BuildContext context, + String viewId, + InlineActionsMenuStyle style, +) => + CharacterShortcutEvent( + key: 'show the inline page reference menu by [', + character: _bracketChar, + handler: (editorState) => inlinePageReferenceCommandHandler( + _bracketChar, + context, + viewId, + editorState, + style, + previousChar: _bracketChar, + ), + ); + +CharacterShortcutEvent pageReferenceShortcutPlusSign( + BuildContext context, + String viewId, + InlineActionsMenuStyle style, +) => + CharacterShortcutEvent( + key: 'show the inline page reference menu by +', + character: _plusChar, + handler: (editorState) => inlinePageReferenceCommandHandler( + _plusChar, + context, + viewId, + editorState, + style, + ), + ); + +InlineActionsMenuService? selectionMenuService; +Future inlinePageReferenceCommandHandler( + String character, + BuildContext context, + String currentViewId, + EditorState editorState, + InlineActionsMenuStyle style, { + String? previousChar, +}) async { + final selection = editorState.selection; + if (PlatformExtension.isMobile || selection == null) { + return false; + } + + if (!selection.isCollapsed) { + await editorState.deleteSelection(selection); + } + + // Check for previous character + if (previousChar != null) { + final node = editorState.getNodeAtPath(selection.end.path); + final delta = node?.delta; + if (node == null || delta == null || delta.isEmpty) { + return false; + } + + if (selection.end.offset > 0) { + final plain = delta.toPlainText(); + + final previousCharacter = plain[selection.end.offset - 1]; + if (previousCharacter != _bracketChar) { + return false; + } + } + } + + // ignore: use_build_context_synchronously + final service = InlineActionsService( + context: context, + handlers: [ + InlinePageReferenceService( + currentViewId: currentViewId, + ).inlinePageReferenceDelegate, + ], + ); + + await editorState.insertTextAtPosition(character, position: selection.start); + + final List initialResults = []; + for (final handler in service.handlers) { + final group = await handler(); + + if (group.results.isNotEmpty) { + initialResults.add(group); + } + } + + if (service.context != null) { + selectionMenuService = InlineActionsMenu( + context: service.context!, + editorState: editorState, + service: service, + initialResults: initialResults, + style: style, + startCharAmount: previousChar != null ? 2 : 1, + ); + + selectionMenuService?.show(); + } + + return true; +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/referenced_database_menu_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/referenced_database_menu_item.dart index 94d20d479c..976a0186dd 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/referenced_database_menu_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/referenced_database_menu_item.dart @@ -7,6 +7,28 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +// Document Reference + +SelectionMenuItem referencedDocumentMenuItem = SelectionMenuItem( + name: LocaleKeys.document_plugins_referencedDocument.tr(), + icon: (editorState, onSelected, style) => SelectableSvgWidget( + data: FlowySvgs.documents_s, + isSelected: onSelected, + style: style, + ), + keywords: ['page', 'notes', 'referenced page', 'referenced document'], + handler: (editorState, menuService, context) { + showLinkToPageMenu( + Overlay.of(context), + editorState, + menuService, + ViewLayoutPB.Document, + ); + }, +); + +// Database References + SelectionMenuItem referencedGridMenuItem = SelectionMenuItem( name: LocaleKeys.document_plugins_referencedGrid.tr(), icon: (editorState, onSelected, style) => SelectableSvgWidget( 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 d7f925d00f..402e4a5c9c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart @@ -179,7 +179,7 @@ class EditorStyleCustomizer { backgroundColor: theme.cardColor, groupTextColor: theme.colorScheme.onBackground.withOpacity(.8), menuItemTextColor: theme.colorScheme.onBackground, - menuItemSelectedColor: theme.hoverColor, + menuItemSelectedColor: theme.colorScheme.secondary, menuItemSelectedTextColor: theme.colorScheme.onSurface, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart index 3bfb4b2a0b..4657f4d46f 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart @@ -4,16 +4,20 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; class InlinePageReferenceService { - InlinePageReferenceService({required this.currentViewId}) { + InlinePageReferenceService({ + required this.currentViewId, + }) { init(); } final Completer _initCompleter = Completer(); + final String currentViewId; late final ViewBackendService service; @@ -79,6 +83,7 @@ class InlinePageReferenceService { final pageSelectionMenuItem = InlineActionsMenuItem( keywords: [view.name.toLowerCase()], label: view.name, + icon: (onSelected) => view.defaultIcon(), onSelected: (context, editorState, menuService, replace) async { final selection = editorState.selection; if (selection == null || !selection.isCollapsed) { diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_menu.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_menu.dart index 1dffa7edd0..3db1fa29a9 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_menu.dart @@ -18,6 +18,7 @@ class InlineActionsMenu extends InlineActionsMenuService { required this.service, required this.initialResults, required this.style, + this.startCharAmount = 1, }); final BuildContext context; @@ -28,6 +29,8 @@ class InlineActionsMenu extends InlineActionsMenuService { @override final InlineActionsMenuStyle style; + final int startCharAmount; + OverlayEntry? _menuEntry; bool selectionChangedByMenu = false; @@ -130,6 +133,7 @@ class InlineActionsMenu extends InlineActionsMenuService { onDismiss: dismiss, onSelectionUpdate: _onSelectionUpdate, style: style, + startCharAmount: startCharAmount, ), ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_handler.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_handler.dart index 9aa6f7ba01..af58ea17cc 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_handler.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_handler.dart @@ -1,5 +1,4 @@ import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/inline_actions/inline_actions_command.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart'; @@ -51,6 +50,7 @@ class InlineActionsHandler extends StatefulWidget { required this.onDismiss, required this.onSelectionUpdate, required this.style, + this.startCharAmount = 1, }); final InlineActionsService service; @@ -60,6 +60,7 @@ class InlineActionsHandler extends StatefulWidget { final VoidCallback onDismiss; final VoidCallback onSelectionUpdate; final InlineActionsMenuStyle style; + final int startCharAmount; @override State createState() => _InlineActionsHandlerState(); @@ -99,10 +100,7 @@ class _InlineActionsHandlerState extends State { _resetSelection(); newResults.sortByStartsWithKeyword(_search); - - setState(() { - results = newResults; - }); + setState(() => results = newResults); } void _resetSelection() { @@ -116,10 +114,9 @@ class _InlineActionsHandlerState extends State { @override void initState() { super.initState(); - - WidgetsBinding.instance.addPostFrameCallback((_) { - _focusNode.requestFocus(); - }); + WidgetsBinding.instance.addPostFrameCallback( + (_) => _focusNode.requestFocus(), + ); startOffset = widget.editorState.selection?.endIndex ?? 0; } @@ -163,6 +160,8 @@ class _InlineActionsHandlerState extends State { isGroupSelected: _selectedGroup == index, selectedIndex: _selectedIndex, onSelected: widget.onDismiss, + startOffset: startOffset - widget.startCharAmount, + endOffset: _search.length + widget.startCharAmount, ), ) .toList(), @@ -200,7 +199,10 @@ class _InlineActionsHandlerState extends State { context, widget.editorState, widget.menuService, - (startOffset - 1, _search.length + 1), + ( + startOffset - widget.startCharAmount, + _search.length + widget.startCharAmount + ), ); widget.onDismiss(); @@ -212,7 +214,7 @@ class _InlineActionsHandlerState extends State { } else if (event.logicalKey == LogicalKeyboardKey.backspace) { if (_search.isEmpty) { widget.onDismiss(); - widget.editorState.deleteBackward(); // Delete '@' + widget.editorState.deleteBackward(); } else { widget.onSelectionUpdate(); widget.editorState.deleteBackward(); @@ -282,16 +284,12 @@ class _InlineActionsHandlerState extends State { return; } - /// Grab index of the first character in command (right after @) - final startIndex = - delta.toPlainText().lastIndexOf(inlineActionCharacter) + 1; - search = widget.editorState .getTextInSelection( selection.copyWith( - start: selection.start.copyWith(offset: startIndex), + start: selection.start.copyWith(offset: startOffset), end: selection.start - .copyWith(offset: startIndex + _search.length + 1), + .copyWith(offset: startOffset + _search.length + 1), ), ) .join(); @@ -331,8 +329,9 @@ class _InlineActionsHandlerState extends State { return; } - search = delta - .toPlainText() - .substring(startOffset, startOffset - 1 + _search.length); + search = delta.toPlainText().substring( + startOffset, + startOffset - 1 + _search.length, + ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_menu_group.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_menu_group.dart index 1f568e8f3e..498e91bb9e 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_menu_group.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_menu_group.dart @@ -2,6 +2,7 @@ import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:collection/collection.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; @@ -13,6 +14,8 @@ class InlineActionsGroup extends StatelessWidget { required this.menuService, required this.style, required this.onSelected, + required this.startOffset, + required this.endOffset, this.isGroupSelected = false, this.selectedIndex = 0, }); @@ -22,6 +25,8 @@ class InlineActionsGroup extends StatelessWidget { final InlineActionsMenuService menuService; final InlineActionsMenuStyle style; final VoidCallback onSelected; + final int startOffset; + final int endOffset; final bool isGroupSelected; final int selectedIndex; @@ -43,6 +48,8 @@ class InlineActionsGroup extends StatelessWidget { isSelected: isGroupSelected && index == selectedIndex, style: style, onSelected: onSelected, + startOffset: startOffset, + endOffset: endOffset, ), ), ], @@ -60,6 +67,8 @@ class InlineActionsWidget extends StatefulWidget { required this.isSelected, required this.style, required this.onSelected, + required this.startOffset, + required this.endOffset, }); final InlineActionsMenuItem item; @@ -68,57 +77,26 @@ class InlineActionsWidget extends StatefulWidget { final bool isSelected; final InlineActionsMenuStyle style; final VoidCallback onSelected; + final int startOffset; + final int endOffset; @override State createState() => _InlineActionsWidgetState(); } class _InlineActionsWidgetState extends State { - bool isHovering = false; - @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 2), child: SizedBox( width: 200, - child: widget.item.icon != null - ? TextButton.icon( - onPressed: _onPressed, - style: ButtonStyle( - alignment: Alignment.centerLeft, - backgroundColor: widget.isSelected - ? MaterialStateProperty.all( - widget.style.menuItemSelectedColor, - ) - : MaterialStateProperty.all(Colors.transparent), - ), - icon: widget.item.icon!.call(widget.isSelected || isHovering), - label: FlowyText.regular( - widget.item.label, - color: widget.isSelected - ? widget.style.menuItemSelectedTextColor - : widget.style.menuItemTextColor, - ), - ) - : TextButton( - onPressed: _onPressed, - style: ButtonStyle( - alignment: Alignment.centerLeft, - backgroundColor: widget.isSelected - ? MaterialStateProperty.all( - widget.style.menuItemSelectedColor, - ) - : MaterialStateProperty.all(Colors.transparent), - ), - onHover: (value) => setState(() => isHovering = value), - child: FlowyText.regular( - widget.item.label, - color: widget.isSelected - ? widget.style.menuItemSelectedTextColor - : widget.style.menuItemTextColor, - ), - ), + child: FlowyButton( + isSelected: widget.isSelected, + leftIcon: widget.item.icon?.call(widget.isSelected), + text: FlowyText.regular(widget.item.label), + onTap: _onPressed, + ), ), ); } @@ -129,7 +107,7 @@ class _InlineActionsWidgetState extends State { context, widget.editorState, widget.menuService, - (0, 0), + (widget.startOffset, widget.endOffset), ); } } diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index fff0e8ec08..09ff958b53 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -579,6 +579,9 @@ "calendar": { "selectACalendarToLinkTo": "Select a Calendar to link to", "createANewCalendar": "Create a new Calendar" + }, + "document": { + "selectADocumentToLinkTo": "Select a Document to link to" } }, "selectionMenu": { @@ -589,6 +592,7 @@ "referencedBoard": "Referenced Board", "referencedGrid": "Referenced Grid", "referencedCalendar": "Referenced Calendar", + "referencedDocument": "Referenced Document", "autoGeneratorMenuItemName": "OpenAI Writer", "autoGeneratorTitleName": "OpenAI: Ask AI to write anything...", "autoGeneratorLearnMore": "Learn more", @@ -1030,4 +1034,4 @@ "noFavorite": "No favorite page", "noFavoriteHintText": "Swipe the page to the left to add it to your favorites" } -} \ No newline at end of file +}