mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: show block in notification item (#3736)
* feat: show block in notification item * feat: jump to block action * fix: conflict after merge main * fix: missing import after merge main
This commit is contained in:
parent
aa27c4e6d4
commit
74d9d427bd
@ -130,7 +130,7 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
final result = await UserBackendService.getCurrentUserProfile().then(
|
||||
(value) async => value.andThen(
|
||||
// open the document
|
||||
await _documentService.openDocument(view: view),
|
||||
await _documentService.openDocument(viewId: view.id),
|
||||
),
|
||||
);
|
||||
return state.copyWith(
|
||||
|
@ -9,7 +9,7 @@ class DocumentService {
|
||||
Future<Either<FlowyError, Unit>> createDocument({
|
||||
required ViewPB view,
|
||||
}) async {
|
||||
final canOpen = await openDocument(view: view);
|
||||
final canOpen = await openDocument(viewId: view.id);
|
||||
if (canOpen.isRight()) {
|
||||
return const Right(unit);
|
||||
}
|
||||
@ -19,13 +19,30 @@ class DocumentService {
|
||||
}
|
||||
|
||||
Future<Either<FlowyError, DocumentDataPB>> openDocument({
|
||||
required ViewPB view,
|
||||
required String viewId,
|
||||
}) async {
|
||||
final payload = OpenDocumentPayloadPB()..documentId = view.id;
|
||||
final payload = OpenDocumentPayloadPB()..documentId = viewId;
|
||||
final result = await DocumentEventOpenDocument(payload).send();
|
||||
return result.swap();
|
||||
}
|
||||
|
||||
Future<Either<FlowyError, BlockPB>> getBlockFromDocument({
|
||||
required DocumentDataPB document,
|
||||
required String blockId,
|
||||
}) async {
|
||||
final block = document.blocks[blockId];
|
||||
|
||||
if (block != null) {
|
||||
return right(block);
|
||||
}
|
||||
|
||||
return left(
|
||||
FlowyError(
|
||||
msg: 'Block($blockId) not found in Document(${document.pageId})',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Either<FlowyError, Unit>> closeDocument({
|
||||
required ViewPB view,
|
||||
}) async {
|
||||
|
@ -10,6 +10,8 @@ import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/export_page_widget.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/base64_string.dart';
|
||||
import 'package:appflowy/workspace/application/notifications/notification_action.dart';
|
||||
import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart'
|
||||
@ -60,8 +62,13 @@ class _DocumentPageState extends State<DocumentPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: documentBloc,
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(value: getIt<NotificationActionBloc>()),
|
||||
BlocProvider.value(value: documentBloc),
|
||||
],
|
||||
child: BlocListener<NotificationActionBloc, NotificationActionState>(
|
||||
listener: _onNotificationAction,
|
||||
child: BlocBuilder<DocumentBloc, DocumentState>(
|
||||
builder: (context, state) {
|
||||
return state.loadingState.when(
|
||||
@ -94,6 +101,7 @@ class _DocumentPageState extends State<DocumentPage> {
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -112,9 +120,7 @@ class _DocumentPageState extends State<DocumentPage> {
|
||||
return Column(
|
||||
children: [
|
||||
if (state.isDeleted) _buildBanner(context),
|
||||
Expanded(
|
||||
child: appflowyEditorPage,
|
||||
),
|
||||
Expanded(child: appflowyEditorPage),
|
||||
],
|
||||
);
|
||||
}
|
||||
@ -151,4 +157,22 @@ class _DocumentPageState extends State<DocumentPage> {
|
||||
showSnackBarMessage(context, 'Export success to $path');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onNotificationAction(
|
||||
BuildContext context,
|
||||
NotificationActionState state,
|
||||
) async {
|
||||
if (state.action != null && state.action!.type == ActionType.jumpToBlock) {
|
||||
final path = state.action?.arguments?[ActionArgumentKeys.nodePath.name];
|
||||
|
||||
if (editorState != null && widget.view.id == state.action?.objectId) {
|
||||
editorState!.updateSelectionWithReason(
|
||||
Selection.collapsed(
|
||||
Position(path: [path]),
|
||||
),
|
||||
reason: SelectionUpdateReason.transaction,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,231 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Map<String, BlockComponentBuilder> getEditorBuilderMap({
|
||||
required BuildContext context,
|
||||
required EditorState editorState,
|
||||
required EditorStyleCustomizer styleCustomizer,
|
||||
List<SelectionMenuItem>? slashMenuItems,
|
||||
bool editable = true,
|
||||
}) {
|
||||
final standardActions = [
|
||||
OptionAction.delete,
|
||||
OptionAction.duplicate,
|
||||
// OptionAction.divider,
|
||||
// OptionAction.moveUp,
|
||||
// OptionAction.moveDown,
|
||||
];
|
||||
|
||||
final calloutBGColor = AFThemeExtension.of(context).calloutBGColor;
|
||||
|
||||
final configuration = BlockComponentConfiguration(
|
||||
padding: (_) => const EdgeInsets.symmetric(vertical: 5.0),
|
||||
);
|
||||
|
||||
final customBlockComponentBuilderMap = {
|
||||
PageBlockKeys.type: PageBlockComponentBuilder(),
|
||||
ParagraphBlockKeys.type: ParagraphBlockComponentBuilder(
|
||||
configuration: configuration,
|
||||
),
|
||||
TodoListBlockKeys.type: TodoListBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
placeholderText: (_) => 'To-do',
|
||||
),
|
||||
),
|
||||
BulletedListBlockKeys.type: BulletedListBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
placeholderText: (_) => 'List',
|
||||
),
|
||||
),
|
||||
NumberedListBlockKeys.type: NumberedListBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
placeholderText: (_) => 'List',
|
||||
),
|
||||
),
|
||||
QuoteBlockKeys.type: QuoteBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
placeholderText: (_) => 'Quote',
|
||||
),
|
||||
),
|
||||
HeadingBlockKeys.type: HeadingBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
padding: (_) => const EdgeInsets.only(top: 12.0, bottom: 4.0),
|
||||
placeholderText: (node) =>
|
||||
'Heading ${node.attributes[HeadingBlockKeys.level]}',
|
||||
),
|
||||
textStyleBuilder: (level) => styleCustomizer.headingStyleBuilder(level),
|
||||
),
|
||||
ImageBlockKeys.type: CustomImageBlockComponentBuilder(
|
||||
configuration: configuration,
|
||||
showMenu: true,
|
||||
menuBuilder: (Node node, CustomImageBlockComponentState state) =>
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 10,
|
||||
child: ImageMenu(
|
||||
node: node,
|
||||
state: state,
|
||||
),
|
||||
),
|
||||
),
|
||||
TableBlockKeys.type: TableBlockComponentBuilder(
|
||||
menuBuilder: (node, editorState, position, dir, onBuild, onClose) =>
|
||||
TableMenu(
|
||||
node: node,
|
||||
editorState: editorState,
|
||||
position: position,
|
||||
dir: dir,
|
||||
onBuild: onBuild,
|
||||
onClose: onClose,
|
||||
),
|
||||
),
|
||||
TableCellBlockKeys.type: TableCellBlockComponentBuilder(
|
||||
menuBuilder: (node, editorState, position, dir, onBuild, onClose) =>
|
||||
TableMenu(
|
||||
node: node,
|
||||
editorState: editorState,
|
||||
position: position,
|
||||
dir: dir,
|
||||
onBuild: onBuild,
|
||||
onClose: onClose,
|
||||
),
|
||||
),
|
||||
DatabaseBlockKeys.gridType: DatabaseViewBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
padding: (_) => const EdgeInsets.symmetric(vertical: 10),
|
||||
),
|
||||
),
|
||||
DatabaseBlockKeys.boardType: DatabaseViewBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
padding: (_) => const EdgeInsets.symmetric(vertical: 10),
|
||||
),
|
||||
),
|
||||
DatabaseBlockKeys.calendarType: DatabaseViewBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
padding: (_) => const EdgeInsets.symmetric(vertical: 10),
|
||||
),
|
||||
),
|
||||
CalloutBlockKeys.type: CalloutBlockComponentBuilder(
|
||||
configuration: configuration,
|
||||
defaultColor: calloutBGColor,
|
||||
),
|
||||
DividerBlockKeys.type: DividerBlockComponentBuilder(
|
||||
configuration: configuration,
|
||||
height: 28.0,
|
||||
),
|
||||
MathEquationBlockKeys.type: MathEquationBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
padding: (_) => const EdgeInsets.symmetric(vertical: 20),
|
||||
),
|
||||
),
|
||||
CodeBlockKeys.type: CodeBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
textStyle: (_) => styleCustomizer.codeBlockStyleBuilder(),
|
||||
placeholderTextStyle: (_) => styleCustomizer.codeBlockStyleBuilder(),
|
||||
),
|
||||
padding: const EdgeInsets.only(
|
||||
left: 30,
|
||||
right: 30,
|
||||
bottom: 36,
|
||||
),
|
||||
),
|
||||
AutoCompletionBlockKeys.type: AutoCompletionBlockComponentBuilder(),
|
||||
SmartEditBlockKeys.type: SmartEditBlockComponentBuilder(),
|
||||
ToggleListBlockKeys.type: ToggleListBlockComponentBuilder(
|
||||
configuration: configuration,
|
||||
),
|
||||
OutlineBlockKeys.type: OutlineBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
placeholderTextStyle: (_) =>
|
||||
styleCustomizer.outlineBlockPlaceholderStyleBuilder(),
|
||||
),
|
||||
),
|
||||
errorBlockComponentBuilderKey: ErrorBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
padding: (_) => const EdgeInsets.symmetric(vertical: 10),
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
final builders = {
|
||||
...standardBlockComponentBuilderMap,
|
||||
...customBlockComponentBuilderMap,
|
||||
};
|
||||
|
||||
if (editable) {
|
||||
// customize the action builder. actually, we can customize them in their own builder. Put them here just for convenience.
|
||||
for (final entry in builders.entries) {
|
||||
if (entry.key == PageBlockKeys.type) {
|
||||
continue;
|
||||
}
|
||||
final builder = entry.value;
|
||||
|
||||
// customize the action builder.
|
||||
final supportColorBuilderTypes = [
|
||||
ParagraphBlockKeys.type,
|
||||
HeadingBlockKeys.type,
|
||||
BulletedListBlockKeys.type,
|
||||
NumberedListBlockKeys.type,
|
||||
QuoteBlockKeys.type,
|
||||
TodoListBlockKeys.type,
|
||||
CalloutBlockKeys.type,
|
||||
OutlineBlockKeys.type,
|
||||
ToggleListBlockKeys.type,
|
||||
];
|
||||
|
||||
final supportAlignBuilderType = [
|
||||
ImageBlockKeys.type,
|
||||
];
|
||||
|
||||
final colorAction = [
|
||||
OptionAction.divider,
|
||||
OptionAction.color,
|
||||
];
|
||||
|
||||
final alignAction = [
|
||||
OptionAction.divider,
|
||||
OptionAction.align,
|
||||
];
|
||||
|
||||
final List<OptionAction> actions = [
|
||||
...standardActions,
|
||||
if (supportColorBuilderTypes.contains(entry.key)) ...colorAction,
|
||||
if (supportAlignBuilderType.contains(entry.key)) ...alignAction,
|
||||
];
|
||||
|
||||
if (PlatformExtension.isDesktop) {
|
||||
builder.showActions =
|
||||
(node) => node.parent?.type != TableCellBlockKeys.type;
|
||||
|
||||
builder.actionBuilder = (context, state) {
|
||||
final top = builder.configuration.padding(context.node).top;
|
||||
final padding = context.node.type == HeadingBlockKeys.type
|
||||
? EdgeInsets.only(top: top + 8.0)
|
||||
: EdgeInsets.only(top: top + 2.0);
|
||||
return Padding(
|
||||
padding: padding,
|
||||
child: BlockActionList(
|
||||
blockComponentContext: context,
|
||||
blockComponentState: state,
|
||||
editorState: editorState,
|
||||
actions: actions,
|
||||
showSlashMenu: slashMenuItems != null
|
||||
? () => customSlashCommand(
|
||||
slashMenuItems,
|
||||
shouldInsertSlash: false,
|
||||
style: styleCustomizer.selectionMenuStyleBuilder(),
|
||||
).handler.call(editorState)
|
||||
: () {},
|
||||
),
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return builders;
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import 'package:appflowy/plugins/document/application/doc_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_configuration.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/background_color/theme_background_color.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/i18n/editor_i18n.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/slash_menu_items.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
||||
@ -10,6 +10,9 @@ import 'package:appflowy/plugins/inline_actions/handlers/inline_page_reference.d
|
||||
import 'package:appflowy/plugins/inline_actions/handlers/reminder_reference.dart';
|
||||
import 'package:appflowy/plugins/inline_actions/inline_actions_command.dart';
|
||||
import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/application/notifications/notification_action.dart';
|
||||
import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||
import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart';
|
||||
@ -104,7 +107,11 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
late final List<SelectionMenuItem> slashMenuItems;
|
||||
|
||||
late final Map<String, BlockComponentBuilder> blockComponentBuilders =
|
||||
_customAppFlowyBlockComponentBuilders();
|
||||
getEditorBuilderMap(
|
||||
context: context,
|
||||
editorState: widget.editorState,
|
||||
styleCustomizer: widget.styleCustomizer,
|
||||
);
|
||||
|
||||
List<CharacterShortcutEvent> get characterShortcutEvents => [
|
||||
// code block
|
||||
@ -227,6 +234,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
);
|
||||
|
||||
final editorState = widget.editorState;
|
||||
_setInitialSelection(editorScrollController);
|
||||
|
||||
if (PlatformExtension.isMobile) {
|
||||
return Column(
|
||||
@ -280,216 +288,19 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, BlockComponentBuilder> _customAppFlowyBlockComponentBuilders() {
|
||||
final standardActions = [
|
||||
OptionAction.delete,
|
||||
OptionAction.duplicate,
|
||||
// OptionAction.divider,
|
||||
// OptionAction.moveUp,
|
||||
// OptionAction.moveDown,
|
||||
];
|
||||
void _setInitialSelection(EditorScrollController scrollController) {
|
||||
final action = getIt<NotificationActionBloc>().state.action;
|
||||
final viewId = action?.objectId;
|
||||
final nodePath =
|
||||
action?.arguments?[ActionArgumentKeys.nodePath.name] as int?;
|
||||
|
||||
const calloutBGColor = Colors.black;
|
||||
// AFThemeExtension.of(context).calloutBGColor;
|
||||
|
||||
final configuration = BlockComponentConfiguration(
|
||||
padding: (_) => const EdgeInsets.symmetric(vertical: 5.0),
|
||||
);
|
||||
|
||||
final customBlockComponentBuilderMap = {
|
||||
PageBlockKeys.type: PageBlockComponentBuilder(),
|
||||
ParagraphBlockKeys.type: ParagraphBlockComponentBuilder(
|
||||
configuration: configuration,
|
||||
),
|
||||
TodoListBlockKeys.type: TodoListBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
placeholderText: (_) => 'To-do',
|
||||
),
|
||||
),
|
||||
BulletedListBlockKeys.type: BulletedListBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
placeholderText: (_) => 'List',
|
||||
),
|
||||
),
|
||||
NumberedListBlockKeys.type: NumberedListBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
placeholderText: (_) => 'List',
|
||||
),
|
||||
),
|
||||
QuoteBlockKeys.type: QuoteBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
placeholderText: (_) => 'Quote',
|
||||
),
|
||||
),
|
||||
HeadingBlockKeys.type: HeadingBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
padding: (_) => const EdgeInsets.only(top: 12.0, bottom: 4.0),
|
||||
placeholderText: (node) =>
|
||||
'Heading ${node.attributes[HeadingBlockKeys.level]}',
|
||||
),
|
||||
textStyleBuilder: (level) => styleCustomizer.headingStyleBuilder(level),
|
||||
),
|
||||
ImageBlockKeys.type: CustomImageBlockComponentBuilder(
|
||||
configuration: configuration,
|
||||
showMenu: true,
|
||||
menuBuilder: (Node node, CustomImageBlockComponentState state) =>
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 10,
|
||||
child: ImageMenu(
|
||||
node: node,
|
||||
state: state,
|
||||
),
|
||||
),
|
||||
),
|
||||
TableBlockKeys.type: TableBlockComponentBuilder(
|
||||
menuBuilder: (node, editorState, position, dir, onBuild, onClose) =>
|
||||
TableMenu(
|
||||
node: node,
|
||||
editorState: editorState,
|
||||
position: position,
|
||||
dir: dir,
|
||||
onBuild: onBuild,
|
||||
onClose: onClose,
|
||||
),
|
||||
),
|
||||
TableCellBlockKeys.type: TableCellBlockComponentBuilder(
|
||||
menuBuilder: (node, editorState, position, dir, onBuild, onClose) =>
|
||||
TableMenu(
|
||||
node: node,
|
||||
editorState: editorState,
|
||||
position: position,
|
||||
dir: dir,
|
||||
onBuild: onBuild,
|
||||
onClose: onClose,
|
||||
),
|
||||
),
|
||||
DatabaseBlockKeys.gridType: DatabaseViewBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
padding: (_) => const EdgeInsets.symmetric(vertical: 10),
|
||||
),
|
||||
),
|
||||
DatabaseBlockKeys.boardType: DatabaseViewBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
padding: (_) => const EdgeInsets.symmetric(vertical: 10),
|
||||
),
|
||||
),
|
||||
DatabaseBlockKeys.calendarType: DatabaseViewBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
padding: (_) => const EdgeInsets.symmetric(vertical: 10),
|
||||
),
|
||||
),
|
||||
CalloutBlockKeys.type: CalloutBlockComponentBuilder(
|
||||
configuration: configuration,
|
||||
defaultColor: calloutBGColor,
|
||||
),
|
||||
DividerBlockKeys.type: DividerBlockComponentBuilder(
|
||||
configuration: configuration,
|
||||
height: 28.0,
|
||||
),
|
||||
MathEquationBlockKeys.type: MathEquationBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
padding: (_) => const EdgeInsets.symmetric(vertical: 20),
|
||||
),
|
||||
),
|
||||
CodeBlockKeys.type: CodeBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
textStyle: (_) => styleCustomizer.codeBlockStyleBuilder(),
|
||||
placeholderTextStyle: (_) => styleCustomizer.codeBlockStyleBuilder(),
|
||||
),
|
||||
padding: const EdgeInsets.only(
|
||||
left: 30,
|
||||
right: 30,
|
||||
bottom: 36,
|
||||
),
|
||||
),
|
||||
AutoCompletionBlockKeys.type: AutoCompletionBlockComponentBuilder(),
|
||||
SmartEditBlockKeys.type: SmartEditBlockComponentBuilder(),
|
||||
ToggleListBlockKeys.type: ToggleListBlockComponentBuilder(
|
||||
configuration: configuration,
|
||||
),
|
||||
OutlineBlockKeys.type: OutlineBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
placeholderTextStyle: (_) =>
|
||||
styleCustomizer.outlineBlockPlaceholderStyleBuilder(),
|
||||
),
|
||||
),
|
||||
errorBlockComponentBuilderKey: ErrorBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
padding: (_) => const EdgeInsets.symmetric(vertical: 10),
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
final builders = {
|
||||
...standardBlockComponentBuilderMap,
|
||||
...customBlockComponentBuilderMap,
|
||||
};
|
||||
|
||||
// customize the action builder. actually, we can customize them in their own builder. Put them here just for convenience.
|
||||
for (final entry in builders.entries) {
|
||||
if (entry.key == PageBlockKeys.type) {
|
||||
continue;
|
||||
if (viewId != null && viewId == documentBloc.view.id && nodePath != null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
scrollController.itemScrollController.jumpTo(index: nodePath);
|
||||
widget.editorState.selection =
|
||||
Selection.collapsed(Position(path: [nodePath]));
|
||||
});
|
||||
}
|
||||
final builder = entry.value;
|
||||
|
||||
// customize the action builder.
|
||||
final supportColorBuilderTypes = [
|
||||
ParagraphBlockKeys.type,
|
||||
HeadingBlockKeys.type,
|
||||
BulletedListBlockKeys.type,
|
||||
NumberedListBlockKeys.type,
|
||||
QuoteBlockKeys.type,
|
||||
TodoListBlockKeys.type,
|
||||
CalloutBlockKeys.type,
|
||||
OutlineBlockKeys.type,
|
||||
ToggleListBlockKeys.type,
|
||||
];
|
||||
|
||||
final supportAlignBuilderType = [
|
||||
ImageBlockKeys.type,
|
||||
];
|
||||
|
||||
final colorAction = [
|
||||
OptionAction.divider,
|
||||
OptionAction.color,
|
||||
];
|
||||
|
||||
final alignAction = [
|
||||
OptionAction.divider,
|
||||
OptionAction.align,
|
||||
];
|
||||
|
||||
final List<OptionAction> actions = [
|
||||
...standardActions,
|
||||
if (supportColorBuilderTypes.contains(entry.key)) ...colorAction,
|
||||
if (supportAlignBuilderType.contains(entry.key)) ...alignAction,
|
||||
];
|
||||
|
||||
// only show the ... and + button on the desktop platform.
|
||||
if (PlatformExtension.isDesktop) {
|
||||
builder.showActions =
|
||||
(node) => node.parent?.type != TableCellBlockKeys.type;
|
||||
builder.actionBuilder = (context, state) {
|
||||
final top = builder.configuration.padding(context.node).top;
|
||||
final padding = context.node.type == HeadingBlockKeys.type
|
||||
? EdgeInsets.only(top: top + 8.0)
|
||||
: EdgeInsets.only(top: top + 2.0);
|
||||
return Padding(
|
||||
padding: padding,
|
||||
child: BlockActionList(
|
||||
blockComponentContext: context,
|
||||
blockComponentState: state,
|
||||
editorState: widget.editorState,
|
||||
actions: actions,
|
||||
showSlashMenu: () => showSlashMenu(widget.editorState),
|
||||
),
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return builders;
|
||||
}
|
||||
|
||||
List<SelectionMenuItem> _customSlashMenuItems() {
|
||||
|
@ -39,6 +39,8 @@ class MentionDateBlock extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final editorState = context.read<EditorState>();
|
||||
|
||||
DateTime? parsedDate = DateTime.tryParse(date);
|
||||
if (parsedDate == null) {
|
||||
return const SizedBox.shrink();
|
||||
@ -108,10 +110,12 @@ class MentionDateBlock extends StatelessWidget {
|
||||
);
|
||||
|
||||
return GestureDetector(
|
||||
onTapDown: (details) => DatePickerMenu(
|
||||
onTapDown: editorState.editable
|
||||
? (details) => DatePickerMenu(
|
||||
context: context,
|
||||
editorState: context.read<EditorState>(),
|
||||
).show(details.globalPosition, options: options),
|
||||
).show(details.globalPosition, options: options)
|
||||
: null,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: MouseRegion(
|
||||
|
@ -137,9 +137,7 @@ class ReminderReferenceService {
|
||||
}
|
||||
|
||||
final viewId = context.read<DocumentBloc>().view.id;
|
||||
final reminder = _reminderFromDate(date, viewId);
|
||||
|
||||
context.read<ReminderBloc>().add(ReminderEvent.add(reminder: reminder));
|
||||
final reminder = _reminderFromDate(date, viewId, node);
|
||||
|
||||
final transaction = editorState.transaction
|
||||
..replaceText(
|
||||
@ -157,6 +155,10 @@ class ReminderReferenceService {
|
||||
);
|
||||
|
||||
await editorState.apply(transaction);
|
||||
|
||||
if (context.mounted) {
|
||||
context.read<ReminderBloc>().add(ReminderEvent.add(reminder: reminder));
|
||||
}
|
||||
}
|
||||
|
||||
void _setOptions() {
|
||||
@ -204,7 +206,7 @@ class ReminderReferenceService {
|
||||
);
|
||||
}
|
||||
|
||||
ReminderPB _reminderFromDate(DateTime date, String viewId) {
|
||||
ReminderPB _reminderFromDate(DateTime date, String viewId, Node node) {
|
||||
return ReminderPB(
|
||||
id: nanoid(),
|
||||
objectId: viewId,
|
||||
@ -212,6 +214,7 @@ class ReminderReferenceService {
|
||||
message: LocaleKeys.reminderNotification_message.tr(),
|
||||
meta: {
|
||||
ReminderMetaKeys.includeTime.name: false.toString(),
|
||||
ReminderMetaKeys.blockId.name: node.id,
|
||||
},
|
||||
scheduledAt: Int64(date.millisecondsSinceEpoch ~/ 1000),
|
||||
isAck: date.isBefore(DateTime.now()),
|
||||
|
@ -111,7 +111,7 @@ class ReminderBloc extends Bloc<ReminderEvent, ReminderState> {
|
||||
},
|
||||
);
|
||||
},
|
||||
pressReminder: (reminderId) {
|
||||
pressReminder: (reminderId, path) {
|
||||
final reminder =
|
||||
state.reminders.firstWhereOrNull((r) => r.id == reminderId);
|
||||
|
||||
@ -120,12 +120,22 @@ class ReminderBloc extends Bloc<ReminderEvent, ReminderState> {
|
||||
}
|
||||
|
||||
add(
|
||||
ReminderEvent.update(ReminderUpdate(id: reminderId, isRead: true)),
|
||||
ReminderEvent.update(
|
||||
ReminderUpdate(
|
||||
id: reminderId,
|
||||
isRead: state.pastReminders.contains(reminder),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
actionBloc.add(
|
||||
NotificationActionEvent.performAction(
|
||||
action: NotificationAction(objectId: reminder.objectId),
|
||||
action: NotificationAction(
|
||||
objectId: reminder.objectId,
|
||||
arguments: {
|
||||
ActionArgumentKeys.nodePath.name: path,
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@ -191,8 +201,10 @@ class ReminderEvent with _$ReminderEvent {
|
||||
// Mark all unread reminders as read
|
||||
const factory ReminderEvent.markAllRead() = _MarkAllRead;
|
||||
|
||||
const factory ReminderEvent.pressReminder({required String reminderId}) =
|
||||
_PressReminder;
|
||||
const factory ReminderEvent.pressReminder({
|
||||
required String reminderId,
|
||||
@Default(null) int? path,
|
||||
}) = _PressReminder;
|
||||
}
|
||||
|
||||
/// Object used to merge updates with
|
||||
|
@ -1,7 +1,8 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
|
||||
enum ReminderMetaKeys {
|
||||
includeTime("include_time");
|
||||
includeTime("include_time"),
|
||||
blockId("block_id");
|
||||
|
||||
const ReminderMetaKeys(this.name);
|
||||
|
||||
|
@ -34,7 +34,7 @@ class DocumentExporter {
|
||||
|
||||
Future<Either<FlowyError, String>> export(DocumentExportType type) async {
|
||||
final documentService = DocumentService();
|
||||
final result = await documentService.openDocument(view: view);
|
||||
final result = await documentService.openDocument(viewId: view.id);
|
||||
return result.fold((error) => left(error), (r) {
|
||||
final document = r.toDocument();
|
||||
if (document == null) {
|
||||
|
@ -1,5 +1,6 @@
|
||||
enum ActionType {
|
||||
openView,
|
||||
jumpToBlock,
|
||||
}
|
||||
|
||||
/// A [NotificationAction] is used to communicate with the
|
||||
@ -10,10 +11,31 @@ enum ActionType {
|
||||
class NotificationAction {
|
||||
const NotificationAction({
|
||||
this.type = ActionType.openView,
|
||||
this.arguments,
|
||||
required this.objectId,
|
||||
});
|
||||
|
||||
final ActionType type;
|
||||
|
||||
final String objectId;
|
||||
final Map<String, dynamic>? arguments;
|
||||
|
||||
NotificationAction copyWith({
|
||||
ActionType? type,
|
||||
String? objectId,
|
||||
Map<String, dynamic>? arguments,
|
||||
}) =>
|
||||
NotificationAction(
|
||||
type: type ?? this.type,
|
||||
objectId: objectId ?? this.objectId,
|
||||
arguments: arguments ?? this.arguments,
|
||||
);
|
||||
}
|
||||
|
||||
enum ActionArgumentKeys {
|
||||
nodePath('node_path');
|
||||
|
||||
final String name;
|
||||
|
||||
const ActionArgumentKeys(this.name);
|
||||
}
|
||||
|
@ -135,3 +135,22 @@ extension ViewLayoutExtension on ViewLayoutPB {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ViewFinder on List<ViewPB> {
|
||||
ViewPB? findView(String id) {
|
||||
for (final view in this) {
|
||||
if (view.id == id) {
|
||||
return view;
|
||||
}
|
||||
|
||||
if (view.childViews.isNotEmpty) {
|
||||
final v = view.childViews.findView(id);
|
||||
if (v != null) {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,6 @@ import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
|
||||
show UserProfilePB;
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@ -64,23 +63,7 @@ class HomeSideBar extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
BlocListener<NotificationActionBloc, NotificationActionState>(
|
||||
listener: (context, state) {
|
||||
final action = state.action;
|
||||
if (action != null) {
|
||||
switch (action.type) {
|
||||
case ActionType.openView:
|
||||
final view = context
|
||||
.read<MenuBloc>()
|
||||
.state
|
||||
.views
|
||||
.firstWhereOrNull((view) => action.objectId == view.id);
|
||||
|
||||
if (view != null) {
|
||||
context.read<TabsBloc>().openPlugin(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
listener: _onNotificationAction,
|
||||
),
|
||||
],
|
||||
child: Builder(
|
||||
@ -145,4 +128,31 @@ class HomeSideBar extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onNotificationAction(
|
||||
BuildContext context,
|
||||
NotificationActionState state,
|
||||
) {
|
||||
final action = state.action;
|
||||
if (action != null) {
|
||||
if (action.type == ActionType.openView) {
|
||||
final view =
|
||||
context.read<MenuBloc>().state.views.findView(action.objectId);
|
||||
|
||||
if (view != null) {
|
||||
context.read<TabsBloc>().openPlugin(view);
|
||||
|
||||
final nodePath =
|
||||
action.arguments?[ActionArgumentKeys.nodePath.name] as int?;
|
||||
if (nodePath != null) {
|
||||
context.read<NotificationActionBloc>().add(
|
||||
NotificationActionEvent.performAction(
|
||||
action: action.copyWith(type: ActionType.jumpToBlock),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -121,17 +121,9 @@ class _NotificationDialogState extends State<NotificationDialog>
|
||||
);
|
||||
}
|
||||
|
||||
void _onAction(ReminderPB reminder) {
|
||||
final view = widget.views.firstWhereOrNull(
|
||||
(view) => view.id == reminder.objectId,
|
||||
);
|
||||
|
||||
if (view == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
void _onAction(ReminderPB reminder, int? path) {
|
||||
_reminderBloc.add(
|
||||
ReminderEvent.pressReminder(reminderId: reminder.id),
|
||||
ReminderEvent.pressReminder(reminderId: reminder.id, path: path),
|
||||
);
|
||||
|
||||
widget.mutex.close();
|
||||
|
@ -31,7 +31,7 @@ class NotificationButton extends StatelessWidget {
|
||||
child: AppFlowyPopover(
|
||||
mutex: mutex,
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
constraints: const BoxConstraints(maxHeight: 250, maxWidth: 400),
|
||||
constraints: const BoxConstraints(maxHeight: 250, maxWidth: 425),
|
||||
windowPadding: EdgeInsets.zero,
|
||||
margin: EdgeInsets.zero,
|
||||
popupBuilder: (_) =>
|
||||
|
@ -1,10 +1,14 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_configuration.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||
import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -18,6 +22,8 @@ class NotificationItem extends StatefulWidget {
|
||||
required this.scheduled,
|
||||
required this.body,
|
||||
required this.isRead,
|
||||
this.path,
|
||||
this.block,
|
||||
this.includeTime = false,
|
||||
this.readOnly = false,
|
||||
this.onAction,
|
||||
@ -29,11 +35,20 @@ class NotificationItem extends StatefulWidget {
|
||||
final String title;
|
||||
final Int64 scheduled;
|
||||
final String body;
|
||||
final bool isRead;
|
||||
final Future<int?>? path;
|
||||
|
||||
/// If [block] is provided, then [body] will be shown only if
|
||||
/// [block] fails to fetch.
|
||||
///
|
||||
/// [block] is rendered as a result of a [FutureBuilder].
|
||||
///
|
||||
final Future<Node?>? block;
|
||||
|
||||
final bool includeTime;
|
||||
final bool readOnly;
|
||||
final bool isRead;
|
||||
|
||||
final VoidCallback? onAction;
|
||||
final void Function(int? path)? onAction;
|
||||
final VoidCallback? onDelete;
|
||||
final void Function(bool isRead)? onReadChanged;
|
||||
|
||||
@ -44,6 +59,13 @@ class NotificationItem extends StatefulWidget {
|
||||
class _NotificationItemState extends State<NotificationItem> {
|
||||
final PopoverMutex mutex = PopoverMutex();
|
||||
bool _isHovering = false;
|
||||
int? path;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.path?.then((p) => path = p);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -56,7 +78,8 @@ class _NotificationItemState extends State<NotificationItem> {
|
||||
child: Stack(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: widget.onAction,
|
||||
onTap: () => widget.onAction?.call(path),
|
||||
child: AbsorbPointer(
|
||||
child: Opacity(
|
||||
opacity: widget.isRead && !widget.readOnly ? 0.5 : 1,
|
||||
child: DecoratedBox(
|
||||
@ -90,7 +113,7 @@ class _NotificationItemState extends State<NotificationItem> {
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
FlowyText.semibold(
|
||||
widget.title,
|
||||
@ -106,7 +129,54 @@ class _NotificationItemState extends State<NotificationItem> {
|
||||
fontSize: 10,
|
||||
),
|
||||
const VSpace(5),
|
||||
FlowyText.regular(widget.body, maxLines: 4),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: Corners.s8Border,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
child: FutureBuilder<Node?>(
|
||||
future: widget.block,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError ||
|
||||
!snapshot.hasData ||
|
||||
snapshot.data == null) {
|
||||
return FlowyText.regular(
|
||||
widget.body,
|
||||
maxLines: 4,
|
||||
);
|
||||
}
|
||||
|
||||
final EditorState editorState = EditorState(
|
||||
document: Document(root: snapshot.data!),
|
||||
);
|
||||
|
||||
final EditorStyleCustomizer
|
||||
styleCustomizer = EditorStyleCustomizer(
|
||||
context: context,
|
||||
padding: EdgeInsets.zero,
|
||||
);
|
||||
|
||||
return Transform.scale(
|
||||
scale: .9,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: AppFlowyEditor(
|
||||
editorState: editorState,
|
||||
editorStyle: styleCustomizer.style(),
|
||||
editable: false,
|
||||
shrinkWrap: true,
|
||||
blockComponentBuilders:
|
||||
getEditorBuilderMap(
|
||||
context: context,
|
||||
editorState: editorState,
|
||||
styleCustomizer: styleCustomizer,
|
||||
editable: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -116,6 +186,7 @@ class _NotificationItemState extends State<NotificationItem> {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_isHovering && !widget.readOnly)
|
||||
Positioned(
|
||||
right: 4,
|
||||
|
@ -1,9 +1,15 @@
|
||||
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
|
||||
import 'package:appflowy/plugins/document/application/prelude.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_extension.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_item.dart';
|
||||
import 'package:appflowy/workspace/presentation/notifications/widgets/notifications_hub_empty.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class NotificationsView extends StatelessWidget {
|
||||
@ -23,7 +29,7 @@ class NotificationsView extends StatelessWidget {
|
||||
final ReminderBloc reminderBloc;
|
||||
final List<ViewPB> views;
|
||||
final bool isUpcoming;
|
||||
final Function(ReminderPB reminder)? onAction;
|
||||
final Function(ReminderPB reminder, int? path)? onAction;
|
||||
final Function(ReminderPB reminder)? onDelete;
|
||||
final Function(ReminderPB reminder, bool isRead)? onReadChanged;
|
||||
final Widget? actionBar;
|
||||
@ -47,19 +53,35 @@ class NotificationsView extends StatelessWidget {
|
||||
if (actionBar != null) actionBar!,
|
||||
...shownReminders.map(
|
||||
(ReminderPB reminder) {
|
||||
final blockId = reminder.meta[ReminderMetaKeys.blockId.name];
|
||||
|
||||
final documentService = DocumentService();
|
||||
final documentFuture = documentService.openDocument(
|
||||
viewId: reminder.objectId,
|
||||
);
|
||||
|
||||
Future<Node?>? nodeBuilder;
|
||||
Future<int?>? pathFinder;
|
||||
if (blockId != null) {
|
||||
nodeBuilder = _getNodeFromDocument(documentFuture, blockId);
|
||||
pathFinder = _getPathFromDocument(documentFuture, blockId);
|
||||
}
|
||||
|
||||
return NotificationItem(
|
||||
reminderId: reminder.id,
|
||||
key: ValueKey(reminder.id),
|
||||
title: reminder.title,
|
||||
scheduled: reminder.scheduledAt,
|
||||
body: reminder.message,
|
||||
path: pathFinder,
|
||||
block: nodeBuilder,
|
||||
isRead: reminder.isRead,
|
||||
includeTime: reminder.includeTime ?? false,
|
||||
readOnly: isUpcoming,
|
||||
onReadChanged: (isRead) =>
|
||||
onReadChanged?.call(reminder, isRead),
|
||||
onDelete: () => onDelete?.call(reminder),
|
||||
onAction: () => onAction?.call(reminder),
|
||||
onAction: (path) => onAction?.call(reminder, path),
|
||||
);
|
||||
},
|
||||
),
|
||||
@ -67,4 +89,69 @@ class NotificationsView extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Node?> _getNodeFromDocument(
|
||||
Future<Either<FlowyError, DocumentDataPB>> documentFuture,
|
||||
String blockId,
|
||||
) async {
|
||||
final document = (await documentFuture).fold(
|
||||
(l) => null,
|
||||
(document) => document,
|
||||
);
|
||||
|
||||
if (document == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final blockOrFailure = await DocumentService().getBlockFromDocument(
|
||||
document: document,
|
||||
blockId: blockId,
|
||||
);
|
||||
|
||||
return blockOrFailure.fold(
|
||||
(_) => null,
|
||||
(block) => block.toNode(meta: MetaPB()),
|
||||
);
|
||||
}
|
||||
|
||||
Future<int?> _getPathFromDocument(
|
||||
Future<Either<FlowyError, DocumentDataPB>> documentFuture,
|
||||
String blockId,
|
||||
) async {
|
||||
final document = (await documentFuture).fold(
|
||||
(l) => null,
|
||||
(document) => document,
|
||||
);
|
||||
|
||||
if (document == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final rootNode = document.toDocument()?.root;
|
||||
if (rootNode == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return _searchById(rootNode, blockId)?.path.first;
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively iterates a [Node] and compares by its [id]
|
||||
///
|
||||
Node? _searchById(Node current, String id) {
|
||||
if (current.id == id) {
|
||||
return current;
|
||||
}
|
||||
|
||||
if (current.children.isNotEmpty) {
|
||||
for (final child in current.children) {
|
||||
final node = _searchById(child, id);
|
||||
|
||||
if (node != null) {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user