mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-10-29 17:04:52 +00:00
feat: observe mention block change and support block navigation (#6568)
* feat: observe mentioned block changes and navigate to block * test: add delete mentioned block test * chore: update editor version * feat: navigate block in same page * fix: sometimes turn into menu doesn't work * test: add test * fix: integration test
This commit is contained in:
parent
7cad04bbf4
commit
0413100e2b
@ -1,9 +1,12 @@
|
|||||||
import 'package:appflowy/env/cloud_env.dart';
|
import 'package:appflowy/env/cloud_env.dart';
|
||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/plugins/document/document_page.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.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/document/presentation/editor_plugins/mention/mention_page_block.dart';
|
||||||
import 'package:appflowy/shared/patterns/common_patterns.dart';
|
import 'package:appflowy/shared/patterns/common_patterns.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
@ -99,6 +102,27 @@ void main() {
|
|||||||
// tap the mention block to jump to the page
|
// tap the mention block to jump to the page
|
||||||
await tester.tapButton(find.byType(MentionPageBlock));
|
await tester.tapButton(find.byType(MentionPageBlock));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// expect to go to the getting started page
|
||||||
|
final documentPage = find.byType(DocumentPage);
|
||||||
|
expect(documentPage, findsOneWidget);
|
||||||
|
expect(
|
||||||
|
tester.widget<DocumentPage>(documentPage).view.name,
|
||||||
|
Constants.gettingStartedPageName,
|
||||||
|
);
|
||||||
|
// and the block is selected
|
||||||
|
expect(
|
||||||
|
tester.widget<DocumentPage>(documentPage).initialBlockId,
|
||||||
|
mention[MentionBlockKeys.blockId],
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
tester.editor.getCurrentEditorState().selection,
|
||||||
|
Selection.collapsed(
|
||||||
|
Position(
|
||||||
|
path: [0],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('copy link to block(same page) and paste it in doc',
|
testWidgets('copy link to block(same page) and paste it in doc',
|
||||||
@ -168,6 +192,84 @@ void main() {
|
|||||||
),
|
),
|
||||||
findsNWidgets(2),
|
findsNWidgets(2),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// tap the mention block
|
||||||
|
await tester.tapButton(find.byType(MentionPageBlock));
|
||||||
|
expect(
|
||||||
|
tester.editor.getCurrentEditorState().selection,
|
||||||
|
Selection.collapsed(
|
||||||
|
Position(
|
||||||
|
path: [0],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('''1. copy link to block from another page
|
||||||
|
2. paste the link to the new page
|
||||||
|
3. delete the original page
|
||||||
|
4. check the content of the block, it should be no access to the page
|
||||||
|
''', (tester) async {
|
||||||
|
await tester.initializeAppFlowy(
|
||||||
|
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||||
|
);
|
||||||
|
await tester.tapGoogleLoginInButton();
|
||||||
|
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||||
|
|
||||||
|
// open getting started page
|
||||||
|
await tester.openPage(Constants.gettingStartedPageName);
|
||||||
|
await tester.editor.copyLinkToBlock([0]);
|
||||||
|
|
||||||
|
// create a new page and paste it
|
||||||
|
const pageName = 'copy link to block';
|
||||||
|
await tester.createNewPageInSpace(
|
||||||
|
spaceName: Constants.generalSpaceName,
|
||||||
|
layout: ViewLayoutPB.Document,
|
||||||
|
pageName: pageName,
|
||||||
|
);
|
||||||
|
|
||||||
|
// paste the link to the new page
|
||||||
|
await tester.editor.tapLineOfEditorAt(0);
|
||||||
|
await tester.editor.paste();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// tap the mention block to jump to the page
|
||||||
|
await tester.tapButton(find.byType(MentionPageBlock));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// expect to go to the getting started page
|
||||||
|
final documentPage = find.byType(DocumentPage);
|
||||||
|
expect(documentPage, findsOneWidget);
|
||||||
|
expect(
|
||||||
|
tester.widget<DocumentPage>(documentPage).view.name,
|
||||||
|
Constants.gettingStartedPageName,
|
||||||
|
);
|
||||||
|
// delete the getting started page
|
||||||
|
await tester.hoverOnPageName(
|
||||||
|
Constants.gettingStartedPageName,
|
||||||
|
onHover: () async => tester.tapDeletePageButton(),
|
||||||
|
);
|
||||||
|
tester.expectToSeeDocumentBanner();
|
||||||
|
tester.expectNotToSeePageName(gettingStarted);
|
||||||
|
|
||||||
|
// delete the page permanently
|
||||||
|
await tester.tapDeletePermanentlyButton();
|
||||||
|
|
||||||
|
// go back the page
|
||||||
|
await tester.openPage(pageName);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// check the content of the block
|
||||||
|
// it should be no access to the page
|
||||||
|
expect(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byType(AppFlowyEditor),
|
||||||
|
matching: find.findTextInFlowyText(
|
||||||
|
LocaleKeys.document_mention_noAccess.tr(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,6 +51,7 @@ class DocumentPlugin extends Plugin {
|
|||||||
required ViewPB view,
|
required ViewPB view,
|
||||||
required PluginType pluginType,
|
required PluginType pluginType,
|
||||||
this.initialSelection,
|
this.initialSelection,
|
||||||
|
this.initialBlockId,
|
||||||
}) : notifier = ViewPluginNotifier(view: view) {
|
}) : notifier = ViewPluginNotifier(view: view) {
|
||||||
_pluginType = pluginType;
|
_pluginType = pluginType;
|
||||||
}
|
}
|
||||||
@ -61,13 +62,18 @@ class DocumentPlugin extends Plugin {
|
|||||||
@override
|
@override
|
||||||
final ViewPluginNotifier notifier;
|
final ViewPluginNotifier notifier;
|
||||||
|
|
||||||
|
// the initial selection of the document
|
||||||
final Selection? initialSelection;
|
final Selection? initialSelection;
|
||||||
|
|
||||||
|
// the initial block id of the document
|
||||||
|
final String? initialBlockId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
PluginWidgetBuilder get widgetBuilder => DocumentPluginWidgetBuilder(
|
PluginWidgetBuilder get widgetBuilder => DocumentPluginWidgetBuilder(
|
||||||
bloc: _viewInfoBloc,
|
bloc: _viewInfoBloc,
|
||||||
notifier: notifier,
|
notifier: notifier,
|
||||||
initialSelection: initialSelection,
|
initialSelection: initialSelection,
|
||||||
|
initialBlockId: initialBlockId,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -95,6 +101,7 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
|
|||||||
required this.bloc,
|
required this.bloc,
|
||||||
required this.notifier,
|
required this.notifier,
|
||||||
this.initialSelection,
|
this.initialSelection,
|
||||||
|
this.initialBlockId,
|
||||||
});
|
});
|
||||||
|
|
||||||
final ViewInfoBloc bloc;
|
final ViewInfoBloc bloc;
|
||||||
@ -102,6 +109,7 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
|
|||||||
ViewPB get view => notifier.view;
|
ViewPB get view => notifier.view;
|
||||||
int? deletedViewIndex;
|
int? deletedViewIndex;
|
||||||
final Selection? initialSelection;
|
final Selection? initialSelection;
|
||||||
|
final String? initialBlockId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
EdgeInsets get contentPadding => EdgeInsets.zero;
|
EdgeInsets get contentPadding => EdgeInsets.zero;
|
||||||
@ -129,6 +137,7 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
|
|||||||
view: view,
|
view: view,
|
||||||
onDeleted: () => context.onDeleted?.call(view, deletedViewIndex),
|
onDeleted: () => context.onDeleted?.call(view, deletedViewIndex),
|
||||||
initialSelection: initialSelection,
|
initialSelection: initialSelection,
|
||||||
|
initialBlockId: initialBlockId,
|
||||||
fixedTitle: fixedTitle,
|
fixedTitle: fixedTitle,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
|
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
|
||||||
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
|
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
|
||||||
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
||||||
@ -21,6 +19,7 @@ import 'package:appflowy/workspace/application/view/prelude.dart';
|
|||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:universal_platform/universal_platform.dart';
|
import 'package:universal_platform/universal_platform.dart';
|
||||||
@ -31,12 +30,14 @@ class DocumentPage extends StatefulWidget {
|
|||||||
required this.view,
|
required this.view,
|
||||||
required this.onDeleted,
|
required this.onDeleted,
|
||||||
this.initialSelection,
|
this.initialSelection,
|
||||||
|
this.initialBlockId,
|
||||||
this.fixedTitle,
|
this.fixedTitle,
|
||||||
});
|
});
|
||||||
|
|
||||||
final ViewPB view;
|
final ViewPB view;
|
||||||
final VoidCallback onDeleted;
|
final VoidCallback onDeleted;
|
||||||
final Selection? initialSelection;
|
final Selection? initialSelection;
|
||||||
|
final String? initialBlockId;
|
||||||
final String? fixedTitle;
|
final String? fixedTitle;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -124,13 +125,18 @@ class _DocumentPageState extends State<DocumentPage>
|
|||||||
BuildContext context,
|
BuildContext context,
|
||||||
DocumentState state,
|
DocumentState state,
|
||||||
) {
|
) {
|
||||||
|
final editorState = state.editorState;
|
||||||
|
if (editorState == null) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
final width = context.read<DocumentAppearanceCubit>().state.width;
|
final width = context.read<DocumentAppearanceCubit>().state.width;
|
||||||
|
|
||||||
final Widget child;
|
final Widget child;
|
||||||
if (UniversalPlatform.isMobile) {
|
if (UniversalPlatform.isMobile) {
|
||||||
child = BlocBuilder<DocumentPageStyleBloc, DocumentPageStyleState>(
|
child = BlocBuilder<DocumentPageStyleBloc, DocumentPageStyleState>(
|
||||||
builder: (context, styleState) => AppFlowyEditorPage(
|
builder: (context, styleState) => AppFlowyEditorPage(
|
||||||
editorState: state.editorState!,
|
editorState: editorState,
|
||||||
// if the view's name is empty, focus on the title
|
// if the view's name is empty, focus on the title
|
||||||
autoFocus: widget.view.name.isEmpty ? false : null,
|
autoFocus: widget.view.name.isEmpty ? false : null,
|
||||||
styleCustomizer: EditorStyleCustomizer(
|
styleCustomizer: EditorStyleCustomizer(
|
||||||
@ -145,10 +151,10 @@ class _DocumentPageState extends State<DocumentPage>
|
|||||||
} else {
|
} else {
|
||||||
child = EditorDropHandler(
|
child = EditorDropHandler(
|
||||||
viewId: widget.view.id,
|
viewId: widget.view.id,
|
||||||
editorState: state.editorState!,
|
editorState: editorState,
|
||||||
isLocalMode: context.read<DocumentBloc>().isLocalMode,
|
isLocalMode: context.read<DocumentBloc>().isLocalMode,
|
||||||
child: AppFlowyEditorPage(
|
child: AppFlowyEditorPage(
|
||||||
editorState: state.editorState!,
|
editorState: editorState,
|
||||||
// if the view's name is empty, focus on the title
|
// if the view's name is empty, focus on the title
|
||||||
autoFocus: widget.view.name.isEmpty ? false : null,
|
autoFocus: widget.view.name.isEmpty ? false : null,
|
||||||
styleCustomizer: EditorStyleCustomizer(
|
styleCustomizer: EditorStyleCustomizer(
|
||||||
@ -157,7 +163,7 @@ class _DocumentPageState extends State<DocumentPage>
|
|||||||
padding: EditorStyleCustomizer.documentPadding,
|
padding: EditorStyleCustomizer.documentPadding,
|
||||||
),
|
),
|
||||||
header: buildCoverAndIcon(context, state),
|
header: buildCoverAndIcon(context, state),
|
||||||
initialSelection: widget.initialSelection,
|
initialSelection: _calculateInitialSelection(editorState),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -242,16 +248,54 @@ class _DocumentPageState extends State<DocumentPage>
|
|||||||
BuildContext context,
|
BuildContext context,
|
||||||
ActionNavigationState state,
|
ActionNavigationState state,
|
||||||
) {
|
) {
|
||||||
if (state.action != null && state.action!.type == ActionType.jumpToBlock) {
|
final action = state.action;
|
||||||
final path = state.action?.arguments?[ActionArgumentKeys.nodePath];
|
if (action == null ||
|
||||||
|
action.type != ActionType.jumpToBlock ||
|
||||||
|
action.objectId != widget.view.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final editorState = context.read<DocumentBloc>().state.editorState;
|
final editorState = context.read<DocumentBloc>().state.editorState;
|
||||||
if (editorState != null && widget.view.id == state.action?.objectId) {
|
if (editorState == null) {
|
||||||
editorState.updateSelectionWithReason(
|
return;
|
||||||
Selection.collapsed(Position(path: [path])),
|
}
|
||||||
);
|
|
||||||
|
final Path? path = _getPathFromAction(action, editorState);
|
||||||
|
if (path != null) {
|
||||||
|
debugPrint('jump to block: $path');
|
||||||
|
editorState.updateSelectionWithReason(
|
||||||
|
Selection.collapsed(Position(path: path)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Path? _getPathFromAction(NavigationAction action, EditorState editorState) {
|
||||||
|
Path? path = action.arguments?[ActionArgumentKeys.nodePath];
|
||||||
|
if (path == null || path.isEmpty) {
|
||||||
|
final blockId = action.arguments?[ActionArgumentKeys.blockId];
|
||||||
|
if (blockId != null) {
|
||||||
|
path = _findNodePathByBlockId(editorState, blockId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
Path? _findNodePathByBlockId(EditorState editorState, String blockId) {
|
||||||
|
final document = editorState.document;
|
||||||
|
final startNode = document.root.children.firstOrNull;
|
||||||
|
if (startNode == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final nodeIterator = NodeIterator(document: document, startNode: startNode);
|
||||||
|
while (nodeIterator.moveNext()) {
|
||||||
|
final node = nodeIterator.current;
|
||||||
|
if (node.id == blockId) {
|
||||||
|
return node.path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool shouldRebuildDocument(DocumentState previous, DocumentState current) {
|
bool shouldRebuildDocument(DocumentState previous, DocumentState current) {
|
||||||
@ -362,4 +406,24 @@ class _DocumentPageState extends State<DocumentPage>
|
|||||||
isPaste = false;
|
isPaste = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Selection? _calculateInitialSelection(EditorState editorState) {
|
||||||
|
if (widget.initialSelection != null) {
|
||||||
|
return widget.initialSelection;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (widget.initialBlockId != null) {
|
||||||
|
final path = _findNodePathByBlockId(editorState, widget.initialBlockId!);
|
||||||
|
if (path != null) {
|
||||||
|
editorState.selectionType = SelectionType.block;
|
||||||
|
return Selection.collapsed(
|
||||||
|
Position(
|
||||||
|
path: path,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -177,8 +177,14 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage>
|
|||||||
focusManager = AFFocusManager.maybeOf(context);
|
focusManager = AFFocusManager.maybeOf(context);
|
||||||
focusManager?.loseFocusNotifier.addListener(_loseFocus);
|
focusManager?.loseFocusNotifier.addListener(_loseFocus);
|
||||||
|
|
||||||
if (widget.initialSelection != null) {
|
final initialSelection = widget.initialSelection;
|
||||||
widget.editorState.updateSelectionWithReason(widget.initialSelection);
|
final path = initialSelection?.start.path;
|
||||||
|
if (initialSelection != null && path != null && path.isNotEmpty) {
|
||||||
|
editorScrollController.itemScrollController.jumpTo(
|
||||||
|
index: path.first,
|
||||||
|
alignment: 0.5,
|
||||||
|
);
|
||||||
|
widget.editorState.updateSelectionWithReason(initialSelection);
|
||||||
}
|
}
|
||||||
|
|
||||||
widget.editorState.service.keyboardService?.registerInterceptor(
|
widget.editorState.service.keyboardService?.registerInterceptor(
|
||||||
|
|||||||
@ -12,12 +12,14 @@ class BlockActionButton extends StatelessWidget {
|
|||||||
required this.richMessage,
|
required this.richMessage,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
this.showTooltip = true,
|
this.showTooltip = true,
|
||||||
|
this.onPointerDown,
|
||||||
});
|
});
|
||||||
|
|
||||||
final FlowySvgData svg;
|
final FlowySvgData svg;
|
||||||
final bool showTooltip;
|
final bool showTooltip;
|
||||||
final InlineSpan richMessage;
|
final InlineSpan richMessage;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
|
final VoidCallback? onPointerDown;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -26,6 +28,7 @@ class BlockActionButton extends StatelessWidget {
|
|||||||
? SystemMouseCursors.click
|
? SystemMouseCursors.click
|
||||||
: SystemMouseCursors.grab,
|
: SystemMouseCursors.grab,
|
||||||
child: IgnoreParentGestureWidget(
|
child: IgnoreParentGestureWidget(
|
||||||
|
onPress: onPointerDown,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
behavior: HitTestBehavior.deferToChild,
|
behavior: HitTestBehavior.deferToChild,
|
||||||
|
|||||||
@ -324,6 +324,11 @@ class _OptionButtonState extends State<_OptionButton> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
onPointerDown: () {
|
||||||
|
if (widget.editorState.selection != null) {
|
||||||
|
beforeSelection = widget.editorState.selection;
|
||||||
|
}
|
||||||
|
},
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (widget.editorState.selection != null) {
|
if (widget.editorState.selection != null) {
|
||||||
beforeSelection = widget.editorState.selection;
|
beforeSelection = widget.editorState.selection;
|
||||||
@ -369,6 +374,7 @@ class _OptionButtonState extends State<_OptionButton> {
|
|||||||
} else {
|
} else {
|
||||||
beforeSelection = null;
|
beforeSelection = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,12 +7,24 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/me
|
|||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mobile_page_selector_sheet.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mobile_page_selector_sheet.dart';
|
||||||
import 'package:appflowy/plugins/trash/application/trash_service.dart';
|
import 'package:appflowy/plugins/trash/application/trash_service.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart';
|
||||||
|
import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart';
|
||||||
import 'package:appflowy/workspace/application/view/prelude.dart';
|
import 'package:appflowy/workspace/application/view/prelude.dart';
|
||||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart'
|
import 'package:appflowy_editor/appflowy_editor.dart'
|
||||||
show Delta, EditorState, Node, TextInsert, TextTransaction, paragraphNode;
|
show
|
||||||
|
Delta,
|
||||||
|
EditorState,
|
||||||
|
Node,
|
||||||
|
TextInsert,
|
||||||
|
TextTransaction,
|
||||||
|
paragraphNode,
|
||||||
|
NodeIterator,
|
||||||
|
Path,
|
||||||
|
Selection,
|
||||||
|
Position,
|
||||||
|
SelectionType;
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
@ -116,14 +128,37 @@ class _MentionPageBlockState extends State<MentionPageBlock> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> handleTap(ViewPB view) async {
|
Future<void> handleTap(ViewPB view) async {
|
||||||
|
final blockId = widget.blockId;
|
||||||
|
final currentViewId = context.read<DocumentBloc>().documentId;
|
||||||
|
if (currentViewId == widget.pageId && blockId != null) {
|
||||||
|
// same page
|
||||||
|
final path = _findNodePathByBlockId(editorState, blockId);
|
||||||
|
if (path != null) {
|
||||||
|
editorState.scrollService?.jumpTo(path.first);
|
||||||
|
await editorState.updateSelectionWithReason(
|
||||||
|
Selection.collapsed(Position(path: path)),
|
||||||
|
customSelectionType: SelectionType.block,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (UniversalPlatform.isMobile) {
|
if (UniversalPlatform.isMobile) {
|
||||||
final currentViewId = context.read<DocumentBloc>().documentId;
|
|
||||||
if (mounted && currentViewId != widget.pageId) {
|
if (mounted && currentViewId != widget.pageId) {
|
||||||
await context.pushView(view);
|
await context.pushView(view);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
getIt<TabsBloc>().add(
|
final action = NavigationAction(
|
||||||
TabsEvent.openPlugin(plugin: view.plugin(), view: view),
|
objectId: view.id,
|
||||||
|
arguments: {
|
||||||
|
ActionArgumentKeys.view: view,
|
||||||
|
ActionArgumentKeys.blockId: blockId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
getIt<ActionNavigationBloc>().add(
|
||||||
|
ActionNavigationEvent.performAction(
|
||||||
|
action: action,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -188,6 +223,27 @@ class _MentionPageBlockState extends State<MentionPageBlock> {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Path? _findNodePathByBlockId(EditorState editorState, String blockId) {
|
||||||
|
final document = editorState.document;
|
||||||
|
final startNode = document.root.children.firstOrNull;
|
||||||
|
if (startNode == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final nodeIterator = NodeIterator(
|
||||||
|
document: document,
|
||||||
|
startNode: startNode,
|
||||||
|
);
|
||||||
|
while (nodeIterator.moveNext()) {
|
||||||
|
final node = nodeIterator.current;
|
||||||
|
if (node.id == blockId) {
|
||||||
|
return node.path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MentionPageBlockContent extends StatelessWidget {
|
class _MentionPageBlockContent extends StatelessWidget {
|
||||||
@ -225,12 +281,15 @@ class _MentionPageBlockContent extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
const HSpace(2),
|
const HSpace(2),
|
||||||
FlowyText(
|
Flexible(
|
||||||
text,
|
child: FlowyText(
|
||||||
decoration: TextDecoration.underline,
|
text,
|
||||||
fontSize: textStyle?.fontSize,
|
decoration: TextDecoration.underline,
|
||||||
fontWeight: textStyle?.fontWeight,
|
fontSize: textStyle?.fontSize,
|
||||||
lineHeight: textStyle?.height,
|
fontWeight: textStyle?.fontWeight,
|
||||||
|
lineHeight: textStyle?.height,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const HSpace(4),
|
const HSpace(4),
|
||||||
],
|
],
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
|||||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||||
import 'package:appflowy/workspace/presentation/command_palette/command_palette.dart';
|
import 'package:appflowy/workspace/presentation/command_palette/command_palette.dart';
|
||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra/theme.dart';
|
import 'package:flowy_infra/theme.dart';
|
||||||
@ -176,12 +177,17 @@ class _ApplicationWidgetState extends State<ApplicationWidget> {
|
|||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (action?.type == ActionType.openView &&
|
if (action?.type == ActionType.openView &&
|
||||||
UniversalPlatform.isDesktop) {
|
UniversalPlatform.isDesktop) {
|
||||||
final view = action!.arguments?[ActionArgumentKeys.view];
|
final view =
|
||||||
|
action!.arguments?[ActionArgumentKeys.view] as ViewPB?;
|
||||||
final nodePath = action.arguments?[ActionArgumentKeys.nodePath];
|
final nodePath = action.arguments?[ActionArgumentKeys.nodePath];
|
||||||
|
final blockId = action.arguments?[ActionArgumentKeys.blockId];
|
||||||
if (view != null) {
|
if (view != null) {
|
||||||
getIt<TabsBloc>().openPlugin(
|
getIt<TabsBloc>().openPlugin(
|
||||||
view.plugin(),
|
view,
|
||||||
arguments: {PluginArgumentKeys.selection: nodePath},
|
arguments: {
|
||||||
|
PluginArgumentKeys.selection: nodePath,
|
||||||
|
PluginArgumentKeys.blockId: blockId,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (action?.type == ActionType.openRow &&
|
} else if (action?.type == ActionType.openRow &&
|
||||||
|
|||||||
@ -7,6 +7,7 @@ enum ActionType {
|
|||||||
class ActionArgumentKeys {
|
class ActionArgumentKeys {
|
||||||
static String view = "view";
|
static String view = "view";
|
||||||
static String nodePath = "node_path";
|
static String nodePath = "node_path";
|
||||||
|
static String blockId = "block_id";
|
||||||
static String rowId = "row_id";
|
static String rowId = "row_id";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import 'package:flutter/material.dart';
|
|||||||
class PluginArgumentKeys {
|
class PluginArgumentKeys {
|
||||||
static String selection = "selection";
|
static String selection = "selection";
|
||||||
static String rowId = "row_id";
|
static String rowId = "row_id";
|
||||||
|
static String blockId = "block_id";
|
||||||
}
|
}
|
||||||
|
|
||||||
class ViewExtKeys {
|
class ViewExtKeys {
|
||||||
@ -90,11 +91,13 @@ extension ViewExtension on ViewPB {
|
|||||||
case ViewLayoutPB.Document:
|
case ViewLayoutPB.Document:
|
||||||
final Selection? initialSelection =
|
final Selection? initialSelection =
|
||||||
arguments[PluginArgumentKeys.selection];
|
arguments[PluginArgumentKeys.selection];
|
||||||
|
final String? initialBlockId = arguments[PluginArgumentKeys.blockId];
|
||||||
|
|
||||||
return DocumentPlugin(
|
return DocumentPlugin(
|
||||||
view: this,
|
view: this,
|
||||||
pluginType: pluginType,
|
pluginType: pluginType,
|
||||||
initialSelection: initialSelection,
|
initialSelection: initialSelection,
|
||||||
|
initialBlockId: initialBlockId,
|
||||||
);
|
);
|
||||||
case ViewLayoutPB.Chat:
|
case ViewLayoutPB.Chat:
|
||||||
return AIChatPagePlugin(view: this);
|
return AIChatPagePlugin(view: this);
|
||||||
|
|||||||
@ -219,6 +219,11 @@ class HomeSideBar extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final blockId = action.arguments?[ActionArgumentKeys.blockId];
|
||||||
|
if (blockId != null) {
|
||||||
|
arguments[PluginArgumentKeys.blockId] = blockId;
|
||||||
|
}
|
||||||
|
|
||||||
final rowId = action.arguments?[ActionArgumentKeys.rowId];
|
final rowId = action.arguments?[ActionArgumentKeys.rowId];
|
||||||
if (rowId != null) {
|
if (rowId != null) {
|
||||||
arguments[PluginArgumentKeys.rowId] = rowId;
|
arguments[PluginArgumentKeys.rowId] = rowId;
|
||||||
|
|||||||
@ -78,10 +78,8 @@ class _AccountDeletionButtonState extends State<AccountDeletionButton> {
|
|||||||
radius: Corners.s8Border,
|
radius: Corners.s8Border,
|
||||||
hoverColor: Theme.of(context).colorScheme.error.withOpacity(0.1),
|
hoverColor: Theme.of(context).colorScheme.error.withOpacity(0.1),
|
||||||
fontColor: Theme.of(context).colorScheme.error,
|
fontColor: Theme.of(context).colorScheme.error,
|
||||||
fontHoverColor: Colors.white,
|
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
isDangerous: true,
|
isDangerous: true,
|
||||||
lineHeight: 18.0 / 12.0,
|
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
isCheckedNotifier.value = false;
|
isCheckedNotifier.value = false;
|
||||||
textEditingController.clear();
|
textEditingController.clear();
|
||||||
|
|||||||
@ -53,8 +53,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: bcd1208
|
ref: "4509123"
|
||||||
resolved-ref: bcd12082fea75cdbbea8b090bf938aae7dc9f4ad
|
resolved-ref: "450912305a2cdf98776b09ad4dbdb11498bdf9ce"
|
||||||
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
|
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
|
||||||
source: git
|
source: git
|
||||||
version: "4.0.0"
|
version: "4.0.0"
|
||||||
|
|||||||
@ -171,7 +171,7 @@ dependency_overrides:
|
|||||||
appflowy_editor:
|
appflowy_editor:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
||||||
ref: "bcd1208"
|
ref: "4509123"
|
||||||
|
|
||||||
appflowy_editor_plugins:
|
appflowy_editor_plugins:
|
||||||
git:
|
git:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user