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/calendar/presentation/calendar_page.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
|
||||
@ -56,21 +57,21 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
// testWidgets('insert a referenced calendar', (tester) async {
|
||||
// await tester.initializeAppFlowy();
|
||||
// await tester.tapGoButton();
|
||||
testWidgets('insert a referenced calendar', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// await insertReferenceDatabase(tester, ViewLayoutPB.Calendar);
|
||||
await insertReferenceDatabase(tester, ViewLayoutPB.Calendar);
|
||||
|
||||
// // validate the referenced grid is inserted
|
||||
// expect(
|
||||
// find.descendant(
|
||||
// of: find.byType(AppFlowyEditor),
|
||||
// matching: find.byType(CalendarPage),
|
||||
// ),
|
||||
// findsOneWidget,
|
||||
// );
|
||||
// });
|
||||
// validate the referenced grid is inserted
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(AppFlowyEditor),
|
||||
matching: find.byType(CalendarPage),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -93,7 +94,7 @@ Future<void> insertReferenceDatabase(
|
||||
);
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
// insert a referenced grid
|
||||
// insert a referenced view
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
layout.referencedMenuName,
|
||||
|
@ -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 {
|
||||
final textBlocks = find.byType(TextBlockComponentWidget);
|
||||
await tester.tapAt(tester.getTopRight(textBlocks.at(index)));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
/// Hover on cover plugin button above the document
|
||||
@ -114,6 +115,11 @@ class EditorOperations {
|
||||
await tester.ime.insertCharacter('/');
|
||||
}
|
||||
|
||||
/// trigger the slash command (selection menu)
|
||||
Future<void> showAtMenu() async {
|
||||
await tester.ime.insertCharacter('@');
|
||||
}
|
||||
|
||||
/// Tap the slash menu item with [name]
|
||||
///
|
||||
/// Must call [showSlashMenu] first.
|
||||
@ -121,4 +127,15 @@ class EditorOperations {
|
||||
final slashMenuItem = find.text(name, findRichText: true);
|
||||
await tester.tapButton(slashMenuItem);
|
||||
}
|
||||
|
||||
/// Tap the at menu item with [name]
|
||||
///
|
||||
/// Must call [showAtMenu] first.
|
||||
Future<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 {
|
||||
IMESimulator(this.tester) {
|
||||
client = findDeltaTextInputClient();
|
||||
client = findTextInputClient();
|
||||
}
|
||||
|
||||
final WidgetTester tester;
|
||||
late final DeltaTextInputClient client;
|
||||
late final TextInputClient client;
|
||||
|
||||
Future<void> insertText(String text) async {
|
||||
for (final c in text.characters) {
|
||||
@ -27,28 +27,22 @@ class IMESimulator {
|
||||
assert(false);
|
||||
return;
|
||||
}
|
||||
final deltas = [
|
||||
TextEditingDeltaInsertion(
|
||||
textInserted: character,
|
||||
oldText: value.text.replaceRange(
|
||||
value.selection.start,
|
||||
value.selection.end,
|
||||
'',
|
||||
),
|
||||
insertionOffset: value.selection.baseOffset,
|
||||
selection: TextSelection.collapsed(
|
||||
offset: value.selection.baseOffset + 1,
|
||||
),
|
||||
composing: TextRange.empty,
|
||||
final text = value.text
|
||||
.replaceRange(value.selection.start, value.selection.end, character);
|
||||
final textEditingValue = TextEditingValue(
|
||||
text: text,
|
||||
selection: TextSelection.collapsed(
|
||||
offset: value.selection.baseOffset + 1,
|
||||
),
|
||||
];
|
||||
client.updateEditingValueWithDeltas(deltas);
|
||||
composing: TextRange.empty,
|
||||
);
|
||||
client.updateEditingValue(textEditingValue);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
DeltaTextInputClient findDeltaTextInputClient() {
|
||||
TextInputClient findTextInputClient() {
|
||||
final finder = find.byType(KeyboardServiceWidget);
|
||||
final KeyboardServiceWidgetState state = tester.state(finder);
|
||||
return state.textInputService as DeltaTextInputClient;
|
||||
return state.textInputService as TextInputClient;
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/bl
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/database/referenced_database_menu_tem.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/inline_page/inline_page_reference.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -35,6 +36,8 @@ class AppFlowyEditorPage extends StatefulWidget {
|
||||
class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
late final ScrollController effectiveScrollController;
|
||||
|
||||
final inlinePageReferenceService = InlinePageReferenceService();
|
||||
|
||||
final List<CommandShortcutEvent> commandShortcutEvents = [
|
||||
...codeBlockCommands,
|
||||
...standardCommandShortcutEvents,
|
||||
@ -69,7 +72,11 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
|
||||
late final Map<String, BlockComponentBuilder> blockComponentBuilders =
|
||||
_customAppFlowyBlockComponentBuilders();
|
||||
|
||||
List<CharacterShortcutEvent> get characterShortcutEvents => [
|
||||
// inline page reference list
|
||||
...inlinePageReferenceShortcuts,
|
||||
|
||||
// code block
|
||||
...codeBlockCharacterEvents,
|
||||
|
||||
@ -88,6 +95,18 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
), // remove the default slash command.
|
||||
];
|
||||
|
||||
late final inlinePageReferenceShortcuts = [
|
||||
inlinePageReferenceService.customPageLinkMenu(
|
||||
character: '@',
|
||||
style: styleCustomizer.selectionMenuStyleBuilder(),
|
||||
),
|
||||
// uncomment this to enable the inline page reference list
|
||||
// inlinePageReferenceService.customPageLinkMenu(
|
||||
// character: '+',
|
||||
// style: styleCustomizer.selectionMenuStyleBuilder(),
|
||||
// ),
|
||||
];
|
||||
|
||||
late final showSlashMenu = customSlashCommand(
|
||||
slashMenuItems,
|
||||
shouldInsertSlash: false,
|
||||
|
@ -87,7 +87,8 @@ class _LinkToPageMenuState extends State<LinkToPageMenu> {
|
||||
final Map<int, (ViewPB, ViewPB)> _items = {};
|
||||
|
||||
Future<List<(ViewPB, List<ViewPB>)>> fetchItems() async {
|
||||
final items = await ViewBackendService().fetchViews(widget.layoutType);
|
||||
final items =
|
||||
await ViewBackendService().fetchViewsWithLayoutType(widget.layoutType);
|
||||
|
||||
int index = 0;
|
||||
for (final (app, children) in items) {
|
||||
|
@ -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_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide FlowySvg, Log;
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@ -57,6 +59,7 @@ class EditorStyleCustomizer {
|
||||
),
|
||||
),
|
||||
),
|
||||
textSpanDecorator: customizeAttributeDecorator,
|
||||
);
|
||||
}
|
||||
|
||||
@ -142,4 +145,28 @@ class EditorStyleCustomizer {
|
||||
backgroundColor: theme.colorScheme.onTertiary,
|
||||
);
|
||||
}
|
||||
|
||||
InlineSpan customizeAttributeDecorator(
|
||||
TextInsert textInsert,
|
||||
TextSpan textSpan,
|
||||
) {
|
||||
final attributes = textInsert.attributes;
|
||||
if (attributes == null) {
|
||||
return textSpan;
|
||||
}
|
||||
final mention = attributes[MentionBlockKeys.mention] as Map?;
|
||||
if (mention != null) {
|
||||
final type = mention[MentionBlockKeys.type];
|
||||
if (type == MentionType.page.name) {
|
||||
return WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: MentionBlock(
|
||||
key: ValueKey(mention[MentionBlockKeys.pageId]),
|
||||
mention: mention,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return textSpan;
|
||||
}
|
||||
}
|
||||
|
@ -94,13 +94,21 @@ extension ViewExtension on ViewPB {
|
||||
}
|
||||
|
||||
String get iconName {
|
||||
switch (layout) {
|
||||
return layout.iconName;
|
||||
}
|
||||
}
|
||||
|
||||
extension ViewLayoutExtension on ViewLayoutPB {
|
||||
String get iconName {
|
||||
switch (this) {
|
||||
case ViewLayoutPB.Grid:
|
||||
return 'editor/grid';
|
||||
case ViewLayoutPB.Board:
|
||||
return 'editor/board';
|
||||
case ViewLayoutPB.Calendar:
|
||||
return 'editor/calendar';
|
||||
case ViewLayoutPB.Document:
|
||||
return 'editor/documents';
|
||||
default:
|
||||
throw Exception('Unknown layout type');
|
||||
}
|
||||
|
@ -154,8 +154,19 @@ class ViewBackendService {
|
||||
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(
|
||||
ViewLayoutPB layoutType,
|
||||
bool Function(WorkspaceSettingPB workspace, ViewPB view) filter,
|
||||
) async {
|
||||
final result = <(ViewPB, List<ViewPB>)>[];
|
||||
return FolderEventGetCurrentWorkspace().send().then((value) async {
|
||||
@ -166,7 +177,7 @@ class ViewBackendService {
|
||||
final childViews = await getChildViews(viewId: view.id).then(
|
||||
(value) => value
|
||||
.getLeftOrNull<List<ViewPB>>()
|
||||
?.where((e) => e.layout == layoutType)
|
||||
?.where((e) => filter(workspaces, e))
|
||||
.toList(),
|
||||
);
|
||||
if (childViews != null && childViews.isNotEmpty) {
|
||||
|
@ -53,9 +53,9 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: cd0f67a
|
||||
resolved-ref: cd0f67a48e40188114800fae9a0f59cafe15b0f2
|
||||
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
|
||||
ref: "250b1a5"
|
||||
resolved-ref: "250b1a59856b337fc2d4b26a1dabdec265e80acf"
|
||||
url: "https://github.com/AppFlowy-IO/appflowy-editor"
|
||||
source: git
|
||||
version: "1.0.4"
|
||||
appflowy_popover:
|
||||
|
@ -45,8 +45,9 @@ dependencies:
|
||||
# appflowy_editor: ^1.0.4
|
||||
appflowy_editor:
|
||||
git:
|
||||
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
||||
ref: cd0f67a
|
||||
url: https://github.com/AppFlowy-IO/appflowy-editor
|
||||
ref: 250b1a5
|
||||
|
||||
appflowy_popover:
|
||||
path: packages/appflowy_popover
|
||||
|
||||
|
10
frontend/rust-lib/Cargo.lock
generated
10
frontend/rust-lib/Cargo.lock
generated
@ -85,6 +85,7 @@ checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4"
|
||||
[[package]]
|
||||
name = "appflowy-integrate"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collab",
|
||||
@ -886,6 +887,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@ -903,6 +905,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-client-ws"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"collab-sync",
|
||||
@ -920,6 +923,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-database"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -945,6 +949,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-derive"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -956,6 +961,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-document"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collab",
|
||||
@ -973,6 +979,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-folder"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@ -992,6 +999,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-persistence"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"chrono",
|
||||
@ -1011,6 +1019,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-plugins"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -1041,6 +1050,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-sync"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"collab",
|
||||
|
Loading…
Reference in New Issue
Block a user