Skip to content

Commit 332a677

Browse files
authored
feat: improve reference menus (#4301)
* feat: improve reference menus * fix: limit page results in reference menus * fix: custom title for specific type refs * fix: insert pages * fix: enable scrolling on item focus change * fix: enable shift+tab to navigate * fix: properly offset menu * fix: review comments * fix: remove bottom padding on last group
1 parent 75d394f commit 332a677

File tree

12 files changed

+355
-381
lines changed

12 files changed

+355
-381
lines changed

frontend/appflowy_flutter/integration_test/document/document_inline_page_reference_test.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart';
1+
import 'package:flutter/services.dart';
2+
23
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
4+
import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';
35
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
46
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
57
import 'package:flowy_infra/uuid.dart';
6-
import 'package:flutter/services.dart';
78
import 'package:flutter_test/flutter_test.dart';
89
import 'package:integration_test/integration_test.dart';
910

@@ -30,7 +31,7 @@ void main() {
3031

3132
// Select result
3233
final optionFinder = find.descendant(
33-
of: find.byType(LinkToPageMenu),
34+
of: find.byType(InlineActionsHandler),
3435
matching: find.text(name),
3536
);
3637

frontend/appflowy_flutter/integration_test/document/document_with_database_test.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import 'package:appflowy/generated/locale_keys.g.dart';
2+
import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';
23
import 'package:appflowy/plugins/database/board/presentation/board_page.dart';
34
import 'package:appflowy/plugins/database/calendar/presentation/calendar_page.dart';
45
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
56
import 'package:appflowy/plugins/database/widgets/row/cells/text_cell/text_cell.dart';
6-
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart';
77
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
88
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
99
import 'package:appflowy_editor/appflowy_editor.dart';
@@ -155,7 +155,7 @@ Future<void> insertReferenceDatabase(
155155
layout.referencedMenuName,
156156
);
157157

158-
final linkToPageMenu = find.byType(LinkToPageMenu);
158+
final linkToPageMenu = find.byType(InlineActionsHandler);
159159
expect(linkToPageMenu, findsOneWidget);
160160
final referencedDatabase = find.descendant(
161161
of: linkToPageMenu,

frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
7373
handlers: [
7474
InlinePageReferenceService(
7575
currentViewId: documentBloc.view.id,
76+
limitResults: 5,
7677
).inlinePageReferenceDelegate,
7778
DateReferenceService(context).dateReferenceDelegate,
7879
ReminderReferenceService(context).reminderReferenceDelegate,

frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ extension InsertDatabase on EditorState {
1414
if (selection == null || !selection.isCollapsed) {
1515
return;
1616
}
17+
1718
final node = getNodeAtPath(selection.end.path);
1819
if (node == null) {
1920
return;
@@ -52,19 +53,9 @@ extension InsertDatabase on EditorState {
5253
);
5354
}
5455

55-
late Transaction transaction;
56-
if (viewType == ViewLayoutPB.Document) {
57-
transaction = await _insertDocumentReference(
58-
childView,
59-
selection,
60-
node,
61-
);
62-
} else {
63-
transaction = await _insertDatabaseReference(
64-
childView,
65-
selection.end.path,
66-
);
67-
}
56+
final Transaction transaction = viewType == ViewLayoutPB.Document
57+
? await _insertDocumentReference(childView, selection, node)
58+
: await _insertDatabaseReference(childView, selection.end.path);
6859

6960
await apply(transaction);
7061
}
Lines changed: 52 additions & 233 deletions
Original file line numberDiff line numberDiff line change
@@ -1,252 +1,71 @@
1+
import 'package:flutter/material.dart';
2+
13
import 'package:appflowy/generated/locale_keys.g.dart';
2-
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/insert_page_command.dart';
3-
import 'package:appflowy/workspace/application/view/view_ext.dart';
4-
import 'package:appflowy/workspace/application/view/view_service.dart';
5-
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
6-
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
4+
import 'package:appflowy/plugins/inline_actions/handlers/inline_page_reference.dart';
5+
import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';
6+
import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';
7+
import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart';
78
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
89
import 'package:appflowy_editor/appflowy_editor.dart';
910
import 'package:easy_localization/easy_localization.dart';
10-
import 'package:flowy_infra_ui/style_widget/button.dart';
11-
import 'package:flowy_infra_ui/style_widget/text.dart';
12-
import 'package:flowy_infra_ui/widget/error_page.dart';
13-
import 'package:flutter/material.dart';
14-
import 'package:flutter/services.dart';
1511

16-
void showLinkToPageMenu(
17-
OverlayState container,
12+
InlineActionsMenuService? _actionsMenuService;
13+
Future<void> showLinkToPageMenu(
1814
EditorState editorState,
1915
SelectionMenuService menuService,
2016
ViewLayoutPB pageType,
21-
) {
17+
) async {
2218
menuService.dismiss();
19+
_actionsMenuService?.dismiss();
2320

24-
final alignment = menuService.alignment;
25-
final offset = menuService.offset;
26-
final top = alignment == Alignment.topLeft ? offset.dy : null;
27-
final bottom = alignment == Alignment.bottomLeft ? offset.dy : null;
28-
29-
keepEditorFocusNotifier.increase();
30-
late OverlayEntry linkToPageMenuEntry;
31-
linkToPageMenuEntry = FullScreenOverlayEntry(
32-
top: top,
33-
bottom: bottom,
34-
left: offset.dx,
35-
dismissCallback: () => keepEditorFocusNotifier.decrease(),
36-
builder: (context) => Material(
37-
color: Colors.transparent,
38-
child: LinkToPageMenu(
39-
editorState: editorState,
40-
layoutType: pageType,
41-
hintText: pageType.toHintText(),
42-
onSelected: (appPB, viewPB) async {
43-
try {
44-
await editorState.insertReferencePage(viewPB, pageType);
45-
linkToPageMenuEntry.remove();
46-
} on FlowyError catch (e) {
47-
if (context.mounted) {
48-
Dialogs.show(
49-
child: FlowyErrorPage.message(
50-
e.msg,
51-
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
52-
),
53-
context,
54-
);
55-
}
56-
}
57-
},
58-
),
59-
),
60-
).build();
61-
container.insert(linkToPageMenuEntry);
62-
}
63-
64-
class LinkToPageMenu extends StatefulWidget {
65-
const LinkToPageMenu({
66-
super.key,
67-
required this.editorState,
68-
required this.layoutType,
69-
required this.hintText,
70-
required this.onSelected,
71-
});
72-
73-
final EditorState editorState;
74-
final ViewLayoutPB layoutType;
75-
final String hintText;
76-
final void Function(ViewPB view, ViewPB childView) onSelected;
77-
78-
@override
79-
State<LinkToPageMenu> createState() => _LinkToPageMenuState();
80-
}
81-
82-
class _LinkToPageMenuState extends State<LinkToPageMenu> {
83-
final _focusNode = FocusNode(debugLabel: 'reference_list_widget');
84-
EditorStyle get style => widget.editorState.editorStyle;
85-
int _selectedIndex = 0;
86-
final int _totalItems = 0;
87-
Future<List<ViewPB>>? _availableLayout;
88-
final List<ViewPB> _items = [];
89-
90-
Future<List<ViewPB>> fetchItems() async {
91-
final items =
92-
await ViewBackendService().fetchViewsWithLayoutType(widget.layoutType);
93-
_items
94-
..clear()
95-
..addAll(items);
96-
return items;
97-
}
98-
99-
@override
100-
void initState() {
101-
_availableLayout = fetchItems();
102-
WidgetsBinding.instance.addPostFrameCallback((_) {
103-
_focusNode.requestFocus();
104-
});
105-
super.initState();
21+
final rootContext = editorState.document.root.context;
22+
if (rootContext == null) {
23+
return;
10624
}
10725

108-
@override
109-
void dispose() {
110-
_focusNode.dispose();
111-
super.dispose();
112-
}
113-
114-
@override
115-
Widget build(BuildContext context) {
116-
final theme = Theme.of(context);
117-
return Focus(
118-
focusNode: _focusNode,
119-
onKey: _onKey,
120-
child: Container(
121-
width: 300,
122-
padding: const EdgeInsets.fromLTRB(10, 6, 10, 6),
123-
decoration: BoxDecoration(
124-
color: theme.cardColor,
125-
boxShadow: [
126-
BoxShadow(
127-
blurRadius: 5,
128-
spreadRadius: 1,
129-
color: Colors.black.withOpacity(0.1),
130-
),
131-
],
132-
borderRadius: BorderRadius.circular(6.0),
133-
),
134-
child: _buildListWidget(
135-
context,
136-
_selectedIndex,
137-
_availableLayout,
138-
),
139-
),
140-
);
141-
}
142-
143-
KeyEventResult _onKey(FocusNode node, RawKeyEvent event) {
144-
if (event is! RawKeyDownEvent ||
145-
_availableLayout == null ||
146-
_items.isEmpty) {
147-
return KeyEventResult.ignored;
148-
}
149-
150-
final acceptedKeys = [
151-
LogicalKeyboardKey.arrowUp,
152-
LogicalKeyboardKey.arrowDown,
153-
LogicalKeyboardKey.tab,
154-
LogicalKeyboardKey.enter,
155-
];
156-
157-
if (!acceptedKeys.contains(event.logicalKey)) {
158-
return KeyEventResult.handled;
26+
final service = InlineActionsService(
27+
context: rootContext,
28+
handlers: [
29+
InlinePageReferenceService(
30+
currentViewId: "",
31+
viewLayout: pageType,
32+
customTitle: titleFromPageType(pageType),
33+
insertPage: pageType != ViewLayoutPB.Document,
34+
limitResults: 15,
35+
).inlinePageReferenceDelegate,
36+
],
37+
);
38+
39+
final List<InlineActionsResult> initialResults = [];
40+
for (final handler in service.handlers) {
41+
final group = await handler();
42+
43+
if (group.results.isNotEmpty) {
44+
initialResults.add(group);
15945
}
160-
161-
var newSelectedIndex = _selectedIndex;
162-
if (event.logicalKey == LogicalKeyboardKey.arrowDown &&
163-
newSelectedIndex != _totalItems - 1) {
164-
newSelectedIndex += 1;
165-
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp &&
166-
newSelectedIndex != 0) {
167-
newSelectedIndex -= 1;
168-
} else if (event.logicalKey == LogicalKeyboardKey.tab) {
169-
newSelectedIndex += 1;
170-
newSelectedIndex %= _totalItems;
171-
} else if (event.logicalKey == LogicalKeyboardKey.enter) {
172-
widget.onSelected(
173-
_items[_selectedIndex],
174-
_items[_selectedIndex],
175-
);
176-
}
177-
178-
setState(() {
179-
_selectedIndex = newSelectedIndex;
180-
});
181-
182-
return KeyEventResult.handled;
18346
}
18447

185-
Widget _buildListWidget(
186-
BuildContext context,
187-
int selectedIndex,
188-
Future<List<ViewPB>>? items,
189-
) {
190-
int index = 0;
191-
return FutureBuilder<List<ViewPB>>(
192-
future: items,
193-
builder: (context, snapshot) {
194-
if (snapshot.hasData &&
195-
snapshot.connectionState == ConnectionState.done) {
196-
final views = snapshot.data;
197-
final List<Widget> children = [
198-
Padding(
199-
padding: const EdgeInsets.symmetric(vertical: 4),
200-
child: FlowyText.regular(
201-
widget.hintText,
202-
fontSize: 10,
203-
color: Colors.grey,
204-
),
205-
),
206-
];
207-
208-
if (views != null && views.isNotEmpty) {
209-
for (final view in views) {
210-
children.add(
211-
FlowyButton(
212-
isSelected: index == _selectedIndex,
213-
leftIcon: view.defaultIcon(),
214-
text: FlowyText.regular(view.name),
215-
onTap: () => widget.onSelected(view, view),
216-
),
217-
);
218-
219-
index += 1;
220-
}
221-
}
222-
223-
return Column(
224-
crossAxisAlignment: CrossAxisAlignment.stretch,
225-
children: children,
226-
);
227-
}
228-
229-
return const Center(child: CircularProgressIndicator());
230-
},
48+
if (rootContext.mounted) {
49+
_actionsMenuService = InlineActionsMenu(
50+
context: rootContext,
51+
editorState: editorState,
52+
service: service,
53+
initialResults: initialResults,
54+
style: Theme.of(editorState.document.root.context!).brightness ==
55+
Brightness.light
56+
? const InlineActionsMenuStyle.light()
57+
: const InlineActionsMenuStyle.dark(),
58+
startCharAmount: 0,
23159
);
232-
}
233-
}
23460

235-
extension on ViewLayoutPB {
236-
String toHintText() {
237-
switch (this) {
238-
case ViewLayoutPB.Grid:
239-
return LocaleKeys.document_slashMenu_grid_selectAGridToLinkTo.tr();
240-
case ViewLayoutPB.Board:
241-
return LocaleKeys.document_slashMenu_board_selectABoardToLinkTo.tr();
242-
case ViewLayoutPB.Calendar:
243-
return LocaleKeys.document_slashMenu_calendar_selectACalendarToLinkTo
244-
.tr();
245-
case ViewLayoutPB.Document:
246-
return LocaleKeys.document_slashMenu_document_selectADocumentToLinkTo
247-
.tr();
248-
default:
249-
throw Exception('Unknown layout type');
250-
}
61+
_actionsMenuService?.show();
25162
}
25263
}
64+
65+
String titleFromPageType(ViewLayoutPB layout) => switch (layout) {
66+
ViewLayoutPB.Grid => LocaleKeys.inlineActions_gridReference.tr(),
67+
ViewLayoutPB.Document => LocaleKeys.inlineActions_docReference.tr(),
68+
ViewLayoutPB.Board => LocaleKeys.inlineActions_boardReference.tr(),
69+
ViewLayoutPB.Calendar => LocaleKeys.inlineActions_calReference.tr(),
70+
_ => LocaleKeys.inlineActions_pageReference.tr(),
71+
};

0 commit comments

Comments
 (0)