mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
parent
a17d6b7eec
commit
30b52a29fd
@ -1,4 +1,5 @@
|
|||||||
import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart';
|
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/database_view/grid/presentation/grid_page.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
|
||||||
@ -56,21 +57,21 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// testWidgets('insert a referenced calendar', (tester) async {
|
testWidgets('insert a referenced calendar', (tester) async {
|
||||||
// await tester.initializeAppFlowy();
|
await tester.initializeAppFlowy();
|
||||||
// await tester.tapGoButton();
|
await tester.tapGoButton();
|
||||||
|
|
||||||
// await insertReferenceDatabase(tester, ViewLayoutPB.Calendar);
|
await insertReferenceDatabase(tester, ViewLayoutPB.Calendar);
|
||||||
|
|
||||||
// // validate the referenced grid is inserted
|
// validate the referenced grid is inserted
|
||||||
// expect(
|
expect(
|
||||||
// find.descendant(
|
find.descendant(
|
||||||
// of: find.byType(AppFlowyEditor),
|
of: find.byType(AppFlowyEditor),
|
||||||
// matching: find.byType(CalendarPage),
|
matching: find.byType(CalendarPage),
|
||||||
// ),
|
),
|
||||||
// findsOneWidget,
|
findsOneWidget,
|
||||||
// );
|
);
|
||||||
// });
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,7 +94,7 @@ Future<void> insertReferenceDatabase(
|
|||||||
);
|
);
|
||||||
// tap the first line of the document
|
// tap the first line of the document
|
||||||
await tester.editor.tapLineOfEditorAt(0);
|
await tester.editor.tapLineOfEditorAt(0);
|
||||||
// insert a referenced grid
|
// insert a referenced view
|
||||||
await tester.editor.showSlashMenu();
|
await tester.editor.showSlashMenu();
|
||||||
await tester.editor.tapSlashMenuItemWithName(
|
await tester.editor.tapSlashMenuItemWithName(
|
||||||
layout.referencedMenuName,
|
layout.referencedMenuName,
|
||||||
|
@ -0,0 +1,129 @@
|
|||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
|
||||||
|
import 'package:flowy_infra/uuid.dart';
|
||||||
|
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
|
||||||
|
import '../util/util.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
group('inline page view in document', () {
|
||||||
|
const location = 'inline_page';
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
await TestFolder.cleanTestLocation(location);
|
||||||
|
await TestFolder.setTestLocation(location);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
await TestFolder.cleanTestLocation(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('insert a inline page - grid', (tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
await insertingInlinePage(tester, ViewLayoutPB.Grid);
|
||||||
|
|
||||||
|
final mentionBlock = find.byType(MentionPageBlock);
|
||||||
|
expect(mentionBlock, findsOneWidget);
|
||||||
|
await tester.tapButton(mentionBlock);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('insert a inline page - board', (tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
await insertingInlinePage(tester, ViewLayoutPB.Board);
|
||||||
|
|
||||||
|
final mentionBlock = find.byType(MentionPageBlock);
|
||||||
|
expect(mentionBlock, findsOneWidget);
|
||||||
|
await tester.tapButton(mentionBlock);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('insert a inline page - calendar', (tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
await insertingInlinePage(tester, ViewLayoutPB.Calendar);
|
||||||
|
|
||||||
|
final mentionBlock = find.byType(MentionPageBlock);
|
||||||
|
expect(mentionBlock, findsOneWidget);
|
||||||
|
await tester.tapButton(mentionBlock);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('insert a inline page - document', (tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
await insertingInlinePage(tester, ViewLayoutPB.Document);
|
||||||
|
|
||||||
|
final mentionBlock = find.byType(MentionPageBlock);
|
||||||
|
expect(mentionBlock, findsOneWidget);
|
||||||
|
await tester.tapButton(mentionBlock);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('insert a inline page and rename it', (tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
final pageName = await insertingInlinePage(tester, ViewLayoutPB.Document);
|
||||||
|
|
||||||
|
// rename
|
||||||
|
await tester.hoverOnPageName(pageName);
|
||||||
|
const newName = 'RenameToNewPageName';
|
||||||
|
await tester.renamePage(newName);
|
||||||
|
final finder = find.descendant(
|
||||||
|
of: find.byType(MentionPageBlock),
|
||||||
|
matching: find.findTextInFlowyText(newName),
|
||||||
|
);
|
||||||
|
expect(finder, findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('insert a inline page and delete it', (tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
final pageName = await insertingInlinePage(tester, ViewLayoutPB.Grid);
|
||||||
|
|
||||||
|
// rename
|
||||||
|
await tester.hoverOnPageName(pageName);
|
||||||
|
await tester.tapDeletePageButton();
|
||||||
|
final finder = find.descendant(
|
||||||
|
of: find.byType(MentionPageBlock),
|
||||||
|
matching: find.findTextInFlowyText(pageName),
|
||||||
|
);
|
||||||
|
expect(finder, findsOneWidget);
|
||||||
|
await tester.tapButton(finder);
|
||||||
|
expect(find.byType(FlowyErrorPage), findsOneWidget);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert a referenced database of [layout] into the document
|
||||||
|
Future<String> insertingInlinePage(
|
||||||
|
WidgetTester tester,
|
||||||
|
ViewLayoutPB layout,
|
||||||
|
) async {
|
||||||
|
// create a new grid
|
||||||
|
final id = uuid();
|
||||||
|
final name = '${layout.name}_$id';
|
||||||
|
await tester.createNewPageWithName(
|
||||||
|
layout,
|
||||||
|
name,
|
||||||
|
);
|
||||||
|
// create a new document
|
||||||
|
await tester.createNewPageWithName(
|
||||||
|
ViewLayoutPB.Document,
|
||||||
|
'insert_a_inline_page_${layout.name}',
|
||||||
|
);
|
||||||
|
// tap the first line of the document
|
||||||
|
await tester.editor.tapLineOfEditorAt(0);
|
||||||
|
// insert a inline page
|
||||||
|
await tester.editor.showAtMenu();
|
||||||
|
await tester.editor.tapAtMenuItemWithName(name);
|
||||||
|
return name;
|
||||||
|
}
|
@ -32,6 +32,7 @@ class EditorOperations {
|
|||||||
Future<void> tapLineOfEditorAt(int index) async {
|
Future<void> tapLineOfEditorAt(int index) async {
|
||||||
final textBlocks = find.byType(TextBlockComponentWidget);
|
final textBlocks = find.byType(TextBlockComponentWidget);
|
||||||
await tester.tapAt(tester.getTopRight(textBlocks.at(index)));
|
await tester.tapAt(tester.getTopRight(textBlocks.at(index)));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hover on cover plugin button above the document
|
/// Hover on cover plugin button above the document
|
||||||
@ -114,6 +115,11 @@ class EditorOperations {
|
|||||||
await tester.ime.insertCharacter('/');
|
await tester.ime.insertCharacter('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// trigger the slash command (selection menu)
|
||||||
|
Future<void> showAtMenu() async {
|
||||||
|
await tester.ime.insertCharacter('@');
|
||||||
|
}
|
||||||
|
|
||||||
/// Tap the slash menu item with [name]
|
/// Tap the slash menu item with [name]
|
||||||
///
|
///
|
||||||
/// Must call [showSlashMenu] first.
|
/// Must call [showSlashMenu] first.
|
||||||
@ -121,4 +127,15 @@ class EditorOperations {
|
|||||||
final slashMenuItem = find.text(name, findRichText: true);
|
final slashMenuItem = find.text(name, findRichText: true);
|
||||||
await tester.tapButton(slashMenuItem);
|
await tester.tapButton(slashMenuItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Tap the at menu item with [name]
|
||||||
|
///
|
||||||
|
/// Must call [showAtMenu] first.
|
||||||
|
Future<void> tapAtMenuItemWithName(String name) async {
|
||||||
|
final atMenuItem = find.descendant(
|
||||||
|
of: find.byType(SelectionMenuWidget),
|
||||||
|
matching: find.text(name, findRichText: true),
|
||||||
|
);
|
||||||
|
await tester.tapButton(atMenuItem);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,11 +9,11 @@ extension IME on WidgetTester {
|
|||||||
|
|
||||||
class IMESimulator {
|
class IMESimulator {
|
||||||
IMESimulator(this.tester) {
|
IMESimulator(this.tester) {
|
||||||
client = findDeltaTextInputClient();
|
client = findTextInputClient();
|
||||||
}
|
}
|
||||||
|
|
||||||
final WidgetTester tester;
|
final WidgetTester tester;
|
||||||
late final DeltaTextInputClient client;
|
late final TextInputClient client;
|
||||||
|
|
||||||
Future<void> insertText(String text) async {
|
Future<void> insertText(String text) async {
|
||||||
for (final c in text.characters) {
|
for (final c in text.characters) {
|
||||||
@ -27,28 +27,22 @@ class IMESimulator {
|
|||||||
assert(false);
|
assert(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final deltas = [
|
final text = value.text
|
||||||
TextEditingDeltaInsertion(
|
.replaceRange(value.selection.start, value.selection.end, character);
|
||||||
textInserted: character,
|
final textEditingValue = TextEditingValue(
|
||||||
oldText: value.text.replaceRange(
|
text: text,
|
||||||
value.selection.start,
|
selection: TextSelection.collapsed(
|
||||||
value.selection.end,
|
offset: value.selection.baseOffset + 1,
|
||||||
'',
|
|
||||||
),
|
|
||||||
insertionOffset: value.selection.baseOffset,
|
|
||||||
selection: TextSelection.collapsed(
|
|
||||||
offset: value.selection.baseOffset + 1,
|
|
||||||
),
|
|
||||||
composing: TextRange.empty,
|
|
||||||
),
|
),
|
||||||
];
|
composing: TextRange.empty,
|
||||||
client.updateEditingValueWithDeltas(deltas);
|
);
|
||||||
|
client.updateEditingValue(textEditingValue);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
}
|
}
|
||||||
|
|
||||||
DeltaTextInputClient findDeltaTextInputClient() {
|
TextInputClient findTextInputClient() {
|
||||||
final finder = find.byType(KeyboardServiceWidget);
|
final finder = find.byType(KeyboardServiceWidget);
|
||||||
final KeyboardServiceWidgetState state = tester.state(finder);
|
final KeyboardServiceWidgetState state = tester.state(finder);
|
||||||
return state.textInputService as DeltaTextInputClient;
|
return state.textInputService as TextInputClient;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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/database/referenced_database_menu_tem.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.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_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:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -35,6 +36,8 @@ class AppFlowyEditorPage extends StatefulWidget {
|
|||||||
class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||||
late final ScrollController effectiveScrollController;
|
late final ScrollController effectiveScrollController;
|
||||||
|
|
||||||
|
final inlinePageReferenceService = InlinePageReferenceService();
|
||||||
|
|
||||||
final List<CommandShortcutEvent> commandShortcutEvents = [
|
final List<CommandShortcutEvent> commandShortcutEvents = [
|
||||||
...codeBlockCommands,
|
...codeBlockCommands,
|
||||||
...standardCommandShortcutEvents,
|
...standardCommandShortcutEvents,
|
||||||
@ -69,7 +72,11 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
|||||||
|
|
||||||
late final Map<String, BlockComponentBuilder> blockComponentBuilders =
|
late final Map<String, BlockComponentBuilder> blockComponentBuilders =
|
||||||
_customAppFlowyBlockComponentBuilders();
|
_customAppFlowyBlockComponentBuilders();
|
||||||
|
|
||||||
List<CharacterShortcutEvent> get characterShortcutEvents => [
|
List<CharacterShortcutEvent> get characterShortcutEvents => [
|
||||||
|
// inline page reference list
|
||||||
|
...inlinePageReferenceShortcuts,
|
||||||
|
|
||||||
// code block
|
// code block
|
||||||
...codeBlockCharacterEvents,
|
...codeBlockCharacterEvents,
|
||||||
|
|
||||||
@ -88,6 +95,18 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
|||||||
), // remove the default slash command.
|
), // 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(
|
late final showSlashMenu = customSlashCommand(
|
||||||
slashMenuItems,
|
slashMenuItems,
|
||||||
shouldInsertSlash: false,
|
shouldInsertSlash: false,
|
||||||
|
@ -87,7 +87,8 @@ class _LinkToPageMenuState extends State<LinkToPageMenu> {
|
|||||||
final Map<int, (ViewPB, ViewPB)> _items = {};
|
final Map<int, (ViewPB, ViewPB)> _items = {};
|
||||||
|
|
||||||
Future<List<(ViewPB, List<ViewPB>)>> fetchItems() async {
|
Future<List<(ViewPB, List<ViewPB>)>> fetchItems() async {
|
||||||
final items = await ViewBackendService().fetchViews(widget.layoutType);
|
final items =
|
||||||
|
await ViewBackendService().fetchViewsWithLayoutType(widget.layoutType);
|
||||||
|
|
||||||
int index = 0;
|
int index = 0;
|
||||||
for (final (app, children) in items) {
|
for (final (app, children) in items) {
|
||||||
|
@ -0,0 +1,164 @@
|
|||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
|
||||||
|
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||||
|
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
|
||||||
|
enum MentionType {
|
||||||
|
page;
|
||||||
|
|
||||||
|
static MentionType fromString(String value) {
|
||||||
|
switch (value) {
|
||||||
|
case 'page':
|
||||||
|
return page;
|
||||||
|
default:
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MentionBlockKeys {
|
||||||
|
const MentionBlockKeys._();
|
||||||
|
|
||||||
|
static const mention = 'mention';
|
||||||
|
static const type = 'type'; // MentionType, String
|
||||||
|
static const pageId = 'page_id';
|
||||||
|
static const pageType = 'page_type';
|
||||||
|
static const pageName = 'page_name';
|
||||||
|
}
|
||||||
|
|
||||||
|
class InlinePageReferenceService {
|
||||||
|
customPageLinkMenu({
|
||||||
|
bool shouldInsertKeyword = false,
|
||||||
|
SelectionMenuStyle style = SelectionMenuStyle.light,
|
||||||
|
String character = '@',
|
||||||
|
}) {
|
||||||
|
return CharacterShortcutEvent(
|
||||||
|
key: 'show page link menu',
|
||||||
|
character: character,
|
||||||
|
handler: (editorState) async {
|
||||||
|
final items = await generatePageItems(character);
|
||||||
|
return _showPageSelectionMenu(
|
||||||
|
editorState,
|
||||||
|
items,
|
||||||
|
shouldInsertKeyword: shouldInsertKeyword,
|
||||||
|
style: style,
|
||||||
|
character: character,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectionMenuService? _selectionMenuService;
|
||||||
|
Future<bool> _showPageSelectionMenu(
|
||||||
|
EditorState editorState,
|
||||||
|
List<SelectionMenuItem> items, {
|
||||||
|
bool shouldInsertKeyword = true,
|
||||||
|
SelectionMenuStyle style = SelectionMenuStyle.light,
|
||||||
|
String character = '@',
|
||||||
|
}) async {
|
||||||
|
if (PlatformExtension.isMobile) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final selection = editorState.selection;
|
||||||
|
if (selection == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete the selection
|
||||||
|
await editorState.deleteSelection(selection);
|
||||||
|
|
||||||
|
final afterSelection = editorState.selection;
|
||||||
|
if (afterSelection == null || !afterSelection.isCollapsed) {
|
||||||
|
assert(false, 'the selection should be collapsed');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
await editorState.insertTextAtPosition(
|
||||||
|
character,
|
||||||
|
position: selection.start,
|
||||||
|
);
|
||||||
|
|
||||||
|
() {
|
||||||
|
final context = editorState.getNodeAtPath(selection.start.path)?.context;
|
||||||
|
if (context != null) {
|
||||||
|
_selectionMenuService = SelectionMenu(
|
||||||
|
context: context,
|
||||||
|
editorState: editorState,
|
||||||
|
selectionMenuItems: items,
|
||||||
|
deleteSlashByDefault: false,
|
||||||
|
style: style,
|
||||||
|
itemCountFilter: 5,
|
||||||
|
);
|
||||||
|
_selectionMenuService?.show();
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<SelectionMenuItem>> generatePageItems(String character) async {
|
||||||
|
final service = ViewBackendService();
|
||||||
|
final List<(ViewPB, List<ViewPB>)> pbViews = await service.fetchViews(
|
||||||
|
(_, __) => true,
|
||||||
|
);
|
||||||
|
if (pbViews.isEmpty) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
final List<SelectionMenuItem> pages = [];
|
||||||
|
final List<ViewPB> views = [];
|
||||||
|
for (final element in pbViews) {
|
||||||
|
views.addAll(element.$2);
|
||||||
|
}
|
||||||
|
views.sort(((a, b) => b.createTime.compareTo(a.createTime)));
|
||||||
|
|
||||||
|
for (final view in views) {
|
||||||
|
final SelectionMenuItem pageSelectionMenuItem = SelectionMenuItem(
|
||||||
|
icon: (editorState, isSelected, style) => SelectableSvgWidget(
|
||||||
|
name: view.iconName,
|
||||||
|
isSelected: isSelected,
|
||||||
|
style: style,
|
||||||
|
),
|
||||||
|
keywords: [
|
||||||
|
view.name.toLowerCase(),
|
||||||
|
],
|
||||||
|
name: view.name,
|
||||||
|
handler: (editorState, menuService, context) async {
|
||||||
|
final selection = editorState.selection;
|
||||||
|
if (selection == null || !selection.isCollapsed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final node = editorState.getNodeAtPath(selection.end.path);
|
||||||
|
final delta = node?.delta;
|
||||||
|
if (node == null || delta == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final index = selection.endIndex;
|
||||||
|
final lastKeywordIndex =
|
||||||
|
delta.toPlainText().substring(0, index).lastIndexOf(character);
|
||||||
|
// @page name -> $
|
||||||
|
// preload the page infos
|
||||||
|
pageMemorizer[view.id] = view;
|
||||||
|
final transaction = editorState.transaction
|
||||||
|
..replaceText(
|
||||||
|
node,
|
||||||
|
lastKeywordIndex,
|
||||||
|
index - lastKeywordIndex,
|
||||||
|
'\$',
|
||||||
|
attributes: {
|
||||||
|
MentionBlockKeys.mention: {
|
||||||
|
MentionBlockKeys.type: MentionType.page.name,
|
||||||
|
MentionBlockKeys.pageId: view.id,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await editorState.apply(transaction);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
pages.add(pageSelectionMenuItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pages;
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,143 @@
|
|||||||
|
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
|
||||||
|
import 'package:appflowy/plugins/trash/application/trash_service.dart';
|
||||||
|
import 'package:appflowy/startup/startup.dart';
|
||||||
|
import 'package:appflowy/workspace/application/view/prelude.dart';
|
||||||
|
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/home/menu/menu.dart';
|
||||||
|
import 'package:appflowy_backend/log.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart'
|
||||||
|
show EditorState, SelectionUpdateReason;
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flowy_infra/image.dart';
|
||||||
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
final pageMemorizer = <String, ViewPB?>{};
|
||||||
|
|
||||||
|
class MentionPageBlock extends StatefulWidget {
|
||||||
|
const MentionPageBlock({
|
||||||
|
super.key,
|
||||||
|
required this.pageId,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String pageId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MentionPageBlock> createState() => _MentionPageBlockState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MentionPageBlockState extends State<MentionPageBlock> {
|
||||||
|
late final EditorState editorState;
|
||||||
|
late final Future<ViewPB?> viewPBFuture;
|
||||||
|
ViewListener? viewListener;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
editorState = context.read<EditorState>();
|
||||||
|
viewPBFuture = fetchView(widget.pageId);
|
||||||
|
viewListener = ViewListener(viewId: widget.pageId)
|
||||||
|
..start(
|
||||||
|
onViewUpdated: (p0) {
|
||||||
|
pageMemorizer[p0.id] = p0;
|
||||||
|
editorState.reload();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
viewListener?.stop();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;
|
||||||
|
return FutureBuilder<ViewPB?>(
|
||||||
|
initialData: pageMemorizer[widget.pageId],
|
||||||
|
future: viewPBFuture,
|
||||||
|
builder: (context, state) {
|
||||||
|
final view = state.data;
|
||||||
|
// memorize the result
|
||||||
|
pageMemorizer[widget.pageId] = view;
|
||||||
|
if (view == null) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
updateSelection();
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 2),
|
||||||
|
child: FlowyHover(
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => openPage(widget.pageId),
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const HSpace(4),
|
||||||
|
FlowySvg(
|
||||||
|
name: view.layout.iconName,
|
||||||
|
size: const Size.square(18.0),
|
||||||
|
),
|
||||||
|
const HSpace(2),
|
||||||
|
FlowyText(
|
||||||
|
view.name,
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
fontSize: fontSize,
|
||||||
|
),
|
||||||
|
const HSpace(2),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void openPage(String pageId) async {
|
||||||
|
final view = await fetchView(pageId);
|
||||||
|
if (view == null) {
|
||||||
|
Log.error('Page($pageId) not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
getIt<MenuSharedState>().latestOpenView = view;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ViewPB?> fetchView(String pageId) async {
|
||||||
|
final views = await ViewBackendService().fetchViews((_, __) => true);
|
||||||
|
final flattenViews = views.expand((e) => [e.$1, ...e.$2]).toList();
|
||||||
|
final view = flattenViews.firstWhereOrNull(
|
||||||
|
(element) => element.id == pageId,
|
||||||
|
);
|
||||||
|
if (view == null) {
|
||||||
|
// try to fetch from trash
|
||||||
|
final trashViews = await TrashService().readTrash();
|
||||||
|
final trash = trashViews.fold(
|
||||||
|
(l) => l.items.firstWhereOrNull((element) => element.id == pageId),
|
||||||
|
(r) => null,
|
||||||
|
);
|
||||||
|
if (trash != null) {
|
||||||
|
return ViewPB()
|
||||||
|
..id = trash.id
|
||||||
|
..name = trash.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateSelection() {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||||
|
editorState.updateSelectionWithReason(
|
||||||
|
editorState.selection,
|
||||||
|
reason: SelectionUpdateReason.transaction,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -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/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:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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,
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -94,13 +94,21 @@ extension ViewExtension on ViewPB {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String get iconName {
|
String get iconName {
|
||||||
switch (layout) {
|
return layout.iconName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ViewLayoutExtension on ViewLayoutPB {
|
||||||
|
String get iconName {
|
||||||
|
switch (this) {
|
||||||
case ViewLayoutPB.Grid:
|
case ViewLayoutPB.Grid:
|
||||||
return 'editor/grid';
|
return 'editor/grid';
|
||||||
case ViewLayoutPB.Board:
|
case ViewLayoutPB.Board:
|
||||||
return 'editor/board';
|
return 'editor/board';
|
||||||
case ViewLayoutPB.Calendar:
|
case ViewLayoutPB.Calendar:
|
||||||
return 'editor/calendar';
|
return 'editor/calendar';
|
||||||
|
case ViewLayoutPB.Document:
|
||||||
|
return 'editor/documents';
|
||||||
default:
|
default:
|
||||||
throw Exception('Unknown layout type');
|
throw Exception('Unknown layout type');
|
||||||
}
|
}
|
||||||
|
@ -154,8 +154,19 @@ class ViewBackendService {
|
|||||||
return FolderEventMoveView(payload).send();
|
return FolderEventMoveView(payload).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<(ViewPB, List<ViewPB>)>> fetchViewsWithLayoutType(
|
||||||
|
ViewLayoutPB? layoutType,
|
||||||
|
) async {
|
||||||
|
return fetchViews((workspace, view) {
|
||||||
|
if (layoutType != null) {
|
||||||
|
return view.layout == layoutType;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<(ViewPB, List<ViewPB>)>> fetchViews(
|
Future<List<(ViewPB, List<ViewPB>)>> fetchViews(
|
||||||
ViewLayoutPB layoutType,
|
bool Function(WorkspaceSettingPB workspace, ViewPB view) filter,
|
||||||
) async {
|
) async {
|
||||||
final result = <(ViewPB, List<ViewPB>)>[];
|
final result = <(ViewPB, List<ViewPB>)>[];
|
||||||
return FolderEventGetCurrentWorkspace().send().then((value) async {
|
return FolderEventGetCurrentWorkspace().send().then((value) async {
|
||||||
@ -166,7 +177,7 @@ class ViewBackendService {
|
|||||||
final childViews = await getChildViews(viewId: view.id).then(
|
final childViews = await getChildViews(viewId: view.id).then(
|
||||||
(value) => value
|
(value) => value
|
||||||
.getLeftOrNull<List<ViewPB>>()
|
.getLeftOrNull<List<ViewPB>>()
|
||||||
?.where((e) => e.layout == layoutType)
|
?.where((e) => filter(workspaces, e))
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
if (childViews != null && childViews.isNotEmpty) {
|
if (childViews != null && childViews.isNotEmpty) {
|
||||||
|
@ -53,9 +53,9 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: cd0f67a
|
ref: "250b1a5"
|
||||||
resolved-ref: cd0f67a48e40188114800fae9a0f59cafe15b0f2
|
resolved-ref: "250b1a59856b337fc2d4b26a1dabdec265e80acf"
|
||||||
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
|
url: "https://github.com/AppFlowy-IO/appflowy-editor"
|
||||||
source: git
|
source: git
|
||||||
version: "1.0.4"
|
version: "1.0.4"
|
||||||
appflowy_popover:
|
appflowy_popover:
|
||||||
|
@ -45,8 +45,9 @@ dependencies:
|
|||||||
# appflowy_editor: ^1.0.4
|
# appflowy_editor: ^1.0.4
|
||||||
appflowy_editor:
|
appflowy_editor:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
url: https://github.com/AppFlowy-IO/appflowy-editor
|
||||||
ref: cd0f67a
|
ref: 250b1a5
|
||||||
|
|
||||||
appflowy_popover:
|
appflowy_popover:
|
||||||
path: packages/appflowy_popover
|
path: packages/appflowy_popover
|
||||||
|
|
||||||
|
10
frontend/rust-lib/Cargo.lock
generated
10
frontend/rust-lib/Cargo.lock
generated
@ -85,6 +85,7 @@ checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "appflowy-integrate"
|
name = "appflowy-integrate"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"collab",
|
"collab",
|
||||||
@ -886,6 +887,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab"
|
name = "collab"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@ -903,6 +905,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-client-ws"
|
name = "collab-client-ws"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"collab-sync",
|
"collab-sync",
|
||||||
@ -920,6 +923,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-database"
|
name = "collab-database"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@ -945,6 +949,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-derive"
|
name = "collab-derive"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -956,6 +961,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-document"
|
name = "collab-document"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"collab",
|
"collab",
|
||||||
@ -973,6 +979,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-folder"
|
name = "collab-folder"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
@ -992,6 +999,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-persistence"
|
name = "collab-persistence"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bincode",
|
"bincode",
|
||||||
"chrono",
|
"chrono",
|
||||||
@ -1011,6 +1019,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-plugins"
|
name = "collab-plugins"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@ -1041,6 +1050,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-sync"
|
name = "collab-sync"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"collab",
|
"collab",
|
||||||
|
Loading…
Reference in New Issue
Block a user