mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: inline page reference (#3859)
* feat: more methods of inserting page reference * test: add tests for inserting document reference * chore: remove unused import * chore: update editor ref * tests: fix test with an interim solution
This commit is contained in:
parent
bc502c9c5b
commit
b35d6131d4
@ -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<String> 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<void> 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<void> 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();
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
@ -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<String> insertingInlinePage(
|
||||
Future<String> insertInlinePage(
|
||||
WidgetTester tester,
|
||||
ViewLayoutPB layout,
|
||||
) async {
|
||||
@ -110,15 +110,19 @@ Future<String> 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;
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -160,7 +160,7 @@ class EditorOperations {
|
||||
await tester.ime.insertCharacter('/');
|
||||
}
|
||||
|
||||
/// trigger the slash command (selection menu)
|
||||
/// trigger the mention (@) command
|
||||
Future<void> showAtMenu() async {
|
||||
await tester.ime.insertCharacter('@');
|
||||
}
|
||||
|
@ -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<AppFlowyEditorPage> {
|
||||
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<AppFlowyEditorPage> {
|
||||
referencedBoardMenuItem,
|
||||
inlineCalendarMenuItem(documentBloc),
|
||||
referencedCalendarMenuItem,
|
||||
referencedDocumentMenuItem,
|
||||
calloutItem,
|
||||
outlineItem,
|
||||
mathEquationItem,
|
||||
|
@ -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<void> 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<Transaction> _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<Transaction> _insertDatabaseReference(
|
||||
ViewPB view,
|
||||
List<int> 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) {
|
||||
|
@ -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<LinkToPageMenu> {
|
||||
) {
|
||||
int index = 0;
|
||||
return FutureBuilder<List<ViewPB>>(
|
||||
future: items,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData &&
|
||||
snapshot.connectionState == ConnectionState.done) {
|
||||
@ -208,10 +208,7 @@ class _LinkToPageMenuState extends State<LinkToPageMenu> {
|
||||
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<LinkToPageMenu> {
|
||||
|
||||
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');
|
||||
}
|
||||
|
@ -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<bool> 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<InlineActionsResult> 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;
|
||||
}
|
@ -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(
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
@ -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<void>();
|
||||
|
||||
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) {
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -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<InlineActionsHandler> createState() => _InlineActionsHandlerState();
|
||||
@ -99,10 +100,7 @@ class _InlineActionsHandlerState extends State<InlineActionsHandler> {
|
||||
_resetSelection();
|
||||
|
||||
newResults.sortByStartsWithKeyword(_search);
|
||||
|
||||
setState(() {
|
||||
results = newResults;
|
||||
});
|
||||
setState(() => results = newResults);
|
||||
}
|
||||
|
||||
void _resetSelection() {
|
||||
@ -116,10 +114,9 @@ class _InlineActionsHandlerState extends State<InlineActionsHandler> {
|
||||
@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<InlineActionsHandler> {
|
||||
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<InlineActionsHandler> {
|
||||
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<InlineActionsHandler> {
|
||||
} 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<InlineActionsHandler> {
|
||||
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<InlineActionsHandler> {
|
||||
return;
|
||||
}
|
||||
|
||||
search = delta
|
||||
.toPlainText()
|
||||
.substring(startOffset, startOffset - 1 + _search.length);
|
||||
search = delta.toPlainText().substring(
|
||||
startOffset,
|
||||
startOffset - 1 + _search.length,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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<InlineActionsWidget> createState() => _InlineActionsWidgetState();
|
||||
}
|
||||
|
||||
class _InlineActionsWidgetState extends State<InlineActionsWidget> {
|
||||
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<InlineActionsWidget> {
|
||||
context,
|
||||
widget.editorState,
|
||||
widget.menuService,
|
||||
(0, 0),
|
||||
(widget.startOffset, widget.endOffset),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user