refactor(editor): edgeless toolbar more actions (#10882)

This commit is contained in:
fundon
2025-03-20 02:08:18 +00:00
parent 39704aac66
commit a004a2cfab
7 changed files with 491 additions and 119 deletions

View File

@@ -30,8 +30,8 @@ import { EditIcon, PageIcon, UngroupIcon } from '@blocksuite/icons/lit';
import type { ExtensionType } from '@blocksuite/store'; import type { ExtensionType } from '@blocksuite/store';
import { html } from 'lit'; import { html } from 'lit';
import { EdgelessRootBlockComponent } from '../..';
import { mountFrameTitleEditor } from '../../utils/text'; import { mountFrameTitleEditor } from '../../utils/text';
import { getEdgelessWith } from './utils';
const builtinSurfaceToolbarConfig = { const builtinSurfaceToolbarConfig = {
actions: [ actions: [
@@ -87,15 +87,8 @@ const builtinSurfaceToolbarConfig = {
const model = ctx.getCurrentModelByType(FrameBlockModel); const model = ctx.getCurrentModelByType(FrameBlockModel);
if (!model) return; if (!model) return;
const rootModel = ctx.store.root; const edgeless = getEdgelessWith(ctx);
if (!rootModel) return; if (!edgeless) return;
// TODO(@fundon): it should be simple
const edgeless = ctx.view.getBlock(rootModel.id);
if (!ctx.matchBlock(edgeless, EdgelessRootBlockComponent)) {
console.error('edgeless view is not found.');
return;
}
mountFrameTitleEditor(model, edgeless); mountFrameTitleEditor(model, edgeless);
}, },
@@ -108,15 +101,8 @@ const builtinSurfaceToolbarConfig = {
const models = ctx.getSurfaceModelsByType(FrameBlockModel); const models = ctx.getSurfaceModelsByType(FrameBlockModel);
if (!models.length) return; if (!models.length) return;
const rootModel = ctx.store.root; const edgeless = getEdgelessWith(ctx);
if (!rootModel) return; if (!edgeless) return;
// TODO(@fundon): it should be simple
const edgeless = ctx.view.getBlock(rootModel.id);
if (!ctx.matchBlock(edgeless, EdgelessRootBlockComponent)) {
console.error('edgeless view is not found.');
return;
}
ctx.store.captureSync(); ctx.store.captureSync();

View File

@@ -12,8 +12,8 @@ import { matchModels } from '@blocksuite/affine-shared/utils';
import { Bound } from '@blocksuite/global/gfx'; import { Bound } from '@blocksuite/global/gfx';
import { EditIcon, PageIcon, UngroupIcon } from '@blocksuite/icons/lit'; import { EditIcon, PageIcon, UngroupIcon } from '@blocksuite/icons/lit';
import { EdgelessRootBlockComponent } from '../..';
import { mountGroupTitleEditor } from '../../utils/text'; import { mountGroupTitleEditor } from '../../utils/text';
import { getEdgelessWith } from './utils';
export const builtinGroupToolbarConfig = { export const builtinGroupToolbarConfig = {
actions: [ actions: [
@@ -69,15 +69,8 @@ export const builtinGroupToolbarConfig = {
const model = ctx.getCurrentModelByType(GroupElementModel); const model = ctx.getCurrentModelByType(GroupElementModel);
if (!model) return; if (!model) return;
const rootModel = ctx.store.root; const edgeless = getEdgelessWith(ctx);
if (!rootModel) return; if (!edgeless) return;
// TODO(@fundon): it should be simple
const edgeless = ctx.view.getBlock(rootModel.id);
if (!ctx.matchBlock(edgeless, EdgelessRootBlockComponent)) {
console.error('edgeless view is not found.');
return;
}
mountGroupTitleEditor(model, edgeless); mountGroupTitleEditor(model, edgeless);
}, },
@@ -90,15 +83,8 @@ export const builtinGroupToolbarConfig = {
const models = ctx.getSurfaceModelsByType(GroupElementModel); const models = ctx.getSurfaceModelsByType(GroupElementModel);
if (!models.length) return; if (!models.length) return;
const rootModel = ctx.store.root; const edgeless = getEdgelessWith(ctx);
if (!rootModel) return; if (!edgeless) return;
// TODO(@fundon): it should be simple
const edgeless = ctx.view.getBlock(rootModel.id);
if (!ctx.matchBlock(edgeless, EdgelessRootBlockComponent)) {
console.error('edgeless view is not found.');
return;
}
for (const model of models) { for (const model of models) {
edgeless.service.ungroup(model); edgeless.service.ungroup(model);

View File

@@ -25,8 +25,9 @@ import {
} from '@blocksuite/icons/lit'; } from '@blocksuite/icons/lit';
import { html } from 'lit'; import { html } from 'lit';
import { EdgelessRootBlockComponent } from '../..';
import { renderAlignmentMenu } from './alignment'; import { renderAlignmentMenu } from './alignment';
import { moreActions } from './more';
import { getEdgelessWith } from './utils';
export const builtinMiscToolbarConfig = { export const builtinMiscToolbarConfig = {
actions: [ actions: [
@@ -86,15 +87,8 @@ export const builtinMiscToolbarConfig = {
const models = ctx.getSurfaceModels(); const models = ctx.getSurfaceModels();
if (models.length < 2) return; if (models.length < 2) return;
const rootModel = ctx.store.root; const edgeless = getEdgelessWith(ctx);
if (!rootModel) return; if (!edgeless) return;
// TODO(@fundon): it should be simple
const edgeless = ctx.view.getBlock(rootModel.id);
if (!ctx.matchBlock(edgeless, EdgelessRootBlockComponent)) {
console.error('edgeless view is not found.');
return;
}
const frame = edgeless.service.frame.createFrameOnSelected(); const frame = edgeless.service.frame.createFrameOnSelected();
if (!frame) return; if (!frame) return;
@@ -135,15 +129,8 @@ export const builtinMiscToolbarConfig = {
const models = ctx.getSurfaceModels(); const models = ctx.getSurfaceModels();
if (models.length < 2) return; if (models.length < 2) return;
const rootModel = ctx.store.root; const edgeless = getEdgelessWith(ctx);
if (!rootModel) return; if (!edgeless) return;
// TODO(@fundon): it should be simple
const edgeless = ctx.view.getBlock(rootModel.id);
if (!ctx.matchBlock(edgeless, EdgelessRootBlockComponent)) {
console.error('edgeless view is not found.');
return;
}
// TODO(@fundon): should be a command // TODO(@fundon): should be a command
edgeless.service.createGroupFromSelected(); edgeless.service.createGroupFromSelected();
@@ -155,7 +142,6 @@ export const builtinMiscToolbarConfig = {
when(ctx) { when(ctx) {
const models = ctx.getSurfaceModels(); const models = ctx.getSurfaceModels();
if (models.length < 2) return false; if (models.length < 2) return false;
if (models.some(model => model.isLocked())) return false;
if (models.some(model => model.group instanceof MindmapElementModel)) if (models.some(model => model.group instanceof MindmapElementModel))
return false; return false;
if ( if (
@@ -227,15 +213,8 @@ export const builtinMiscToolbarConfig = {
const models = ctx.getSurfaceModels(); const models = ctx.getSurfaceModels();
if (!models.length) return; if (!models.length) return;
const rootModel = ctx.store.root; const edgeless = getEdgelessWith(ctx);
if (!rootModel) return; if (!edgeless) return;
// TODO(@fundon): it should be simple
const edgeless = ctx.view.getBlock(rootModel.id);
if (!ctx.matchBlock(edgeless, EdgelessRootBlockComponent)) {
console.error('edgeless view is not found.');
return;
}
// get most top selected elements(*) from tree, like in a tree below // get most top selected elements(*) from tree, like in a tree below
// G0 // G0
@@ -318,6 +297,12 @@ export const builtinMiscToolbarConfig = {
}); });
}, },
}, },
// More actions
...moreActions.map(action => ({
...action,
placement: ActionPlacement.More,
})),
], ],
when(ctx) { when(ctx) {
const models = ctx.getSurfaceModels(); const models = ctx.getSurfaceModels();
@@ -336,15 +321,8 @@ export const builtinLockedToolbarConfig = {
const models = ctx.getSurfaceModels(); const models = ctx.getSurfaceModels();
if (!models.length) return; if (!models.length) return;
const rootModel = ctx.store.root; const edgeless = getEdgelessWith(ctx);
if (!rootModel) return; if (!edgeless) return;
// TODO(@fundon): it should be simple
const edgeless = ctx.view.getBlock(rootModel.id);
if (!ctx.matchBlock(edgeless, EdgelessRootBlockComponent)) {
console.error('edgeless view is not found.');
return;
}
const elements = new Set( const elements = new Set(
models.map(model => models.map(model =>

View File

@@ -0,0 +1,416 @@
import { AttachmentBlockComponent } from '@blocksuite/affine-block-attachment';
import { BookmarkBlockComponent } from '@blocksuite/affine-block-bookmark';
import {
isExternalEmbedBlockComponent,
notifyDocCreated,
promptDocTitle,
} from '@blocksuite/affine-block-embed';
import { EdgelessFrameManagerIdentifier } from '@blocksuite/affine-block-frame';
import { ImageBlockComponent } from '@blocksuite/affine-block-image';
import { EdgelessCRUDIdentifier } from '@blocksuite/affine-block-surface';
import {
AttachmentBlockModel,
BookmarkBlockModel,
EmbedLinkedDocBlockSchema,
EmbedLinkedDocModel,
EmbedSyncedDocBlockSchema,
EmbedSyncedDocModel,
FrameBlockModel,
ImageBlockModel,
isExternalEmbedModel,
NoteBlockModel,
} from '@blocksuite/affine-model';
import type {
ToolbarActions,
ToolbarContext,
} from '@blocksuite/affine-shared/services';
import { type ReorderingType } from '@blocksuite/affine-shared/utils';
import type { BlockComponent } from '@blocksuite/block-std';
import { GfxBlockElementModel, type GfxModel } from '@blocksuite/block-std/gfx';
import { Bound, getCommonBoundWithRotation } from '@blocksuite/global/gfx';
import {
ArrowDownBigBottomIcon,
ArrowDownBigIcon,
ArrowUpBigIcon,
ArrowUpBigTopIcon,
CopyIcon,
DeleteIcon,
DuplicateIcon,
FrameIcon,
GroupIcon,
LinkedPageIcon,
ResetIcon,
} from '@blocksuite/icons/lit';
import {
createLinkedDocFromEdgelessElements,
createLinkedDocFromNote,
} from '../../../widgets/element-toolbar/more-menu/render-linked-doc';
import { duplicate } from '../../utils/clipboard-utils';
import { getSortedCloneElements } from '../../utils/clone-utils';
import { moveConnectors } from '../../utils/connector';
import { deleteElements } from '../../utils/crud';
import { getEdgelessWith } from './utils';
export const moreActions = [
// Selection Group: frame & group
{
id: 'Z.a.selection',
actions: [
{
id: 'a.create-frame',
label: 'Frame section',
icon: FrameIcon(),
run(ctx) {
const frame = ctx.std
.get(EdgelessFrameManagerIdentifier)
.createFrameOnSelected();
if (!frame) return;
const edgeless = getEdgelessWith(ctx);
if (!edgeless) return;
edgeless.surface.fitToViewport(Bound.deserialize(frame.xywh));
ctx.track('CanvasElementAdded', {
control: 'context-menu',
type: 'frame',
});
},
},
{
id: 'b.create-group',
label: 'Group section',
icon: GroupIcon(),
when(ctx) {
const models = ctx.getSurfaceModels();
if (models.length === 0) return false;
return !models.some(model => ctx.matchModel(model, FrameBlockModel));
},
run(ctx) {
const edgeless = getEdgelessWith(ctx);
if (!edgeless) return;
edgeless.service.createGroupFromSelected();
},
},
],
},
// Reordering Group
{
id: 'Z.b.reordering',
actions: [
{
id: 'a.bring-to-front',
label: 'Bring to Front',
icon: ArrowUpBigTopIcon(),
run(ctx) {
const models = ctx.getSurfaceModels();
reorderElements(ctx, models, 'front');
},
},
{
id: 'b.bring-forward',
label: 'Bring Forward',
icon: ArrowUpBigIcon(),
run(ctx) {
const models = ctx.getSurfaceModels();
reorderElements(ctx, models, 'forward');
},
},
{
id: 'c.send-backward',
label: 'Send Backward',
icon: ArrowDownBigIcon(),
run(ctx) {
const models = ctx.getSurfaceModels();
reorderElements(ctx, models, 'backward');
},
},
{
id: 'c.send-to-back',
label: 'Send to Back',
icon: ArrowDownBigBottomIcon(),
run(ctx) {
const models = ctx.getSurfaceModels();
reorderElements(ctx, models, 'back');
},
},
],
},
// Clipboard Group
// Uses the same `ID` for both page and edgeless modes.
{
id: 'a.clipboard',
actions: [
{
id: 'copy',
label: 'Copy',
icon: CopyIcon(),
run(ctx) {
const models = ctx.getSurfaceModels();
if (!models.length) return;
const edgeless = getEdgelessWith(ctx);
if (!edgeless) return;
edgeless.clipboardController.copy();
},
},
{
id: 'duplicate',
label: 'Duplicate',
icon: DuplicateIcon(),
run(ctx) {
const models = ctx.getSurfaceModels();
if (!models.length) return;
const edgeless = getEdgelessWith(ctx);
if (!edgeless) return;
duplicate(edgeless, models).catch(console.error);
},
},
{
id: 'reload',
label: 'Reload',
icon: ResetIcon(),
when(ctx) {
const models = ctx.getSurfaceModels();
if (models.length === 0) return false;
return models.every(isRefreshableModel);
},
run(ctx) {
const blocks = ctx
.getSurfaceModels()
.map(model => ctx.view.getBlock(model.id))
.filter(isRefreshableBlock);
if (!blocks.length) return;
for (const block of blocks) {
block.refreshData();
}
},
},
],
},
// Conversions Group
{
id: 'd.conversions',
actions: [
{
id: 'a.turn-into-linked-doc',
label: 'Turn into linked doc',
icon: LinkedPageIcon(),
when(ctx) {
const models = ctx.getSurfaceModels();
if (models.length !== 1) return false;
return ctx.matchModel(models[0], NoteBlockModel);
},
run(ctx) {
const model = ctx.getCurrentModelByType(NoteBlockModel);
if (!model) return;
const create = async () => {
const title = await promptDocTitle(ctx.std);
if (title === null) return;
const edgeless = getEdgelessWith(ctx);
if (!edgeless) return;
const surfaceId = edgeless.surfaceBlockModel.id;
if (!surfaceId) return;
const linkedDoc = createLinkedDocFromNote(ctx.store, model, title);
// Inserts linked doc card
const cardId = ctx.std.get(EdgelessCRUDIdentifier).addBlock(
EmbedSyncedDocBlockSchema.model.flavour,
{
xywh: model.xywh,
style: 'syncedDoc',
pageId: linkedDoc.id,
index: model.index,
},
surfaceId
);
ctx.track('CanvasElementAdded', {
control: 'context-menu',
type: 'embed-synced-doc',
});
ctx.track('DocCreated', {
control: 'turn into linked doc',
type: 'embed-linked-doc',
});
ctx.track('LinkedDocCreated', {
control: 'turn into linked doc',
type: 'embed-linked-doc',
other: 'new doc',
});
moveConnectors(model.id, cardId, edgeless.service);
// Deletes selected note
ctx.store.transact(() => {
ctx.store.deleteBlock(model);
});
ctx.gfx.selection.set({
elements: [cardId],
editing: false,
});
};
create().catch(console.error);
},
},
{
id: 'b.create-linked-doc',
label: 'Create linked doc',
icon: LinkedPageIcon(),
when(ctx) {
const models = ctx.getSurfaceModels();
if (models.length === 0) return false;
if (models.length === 1) {
return ![
NoteBlockModel,
EmbedLinkedDocModel,
EmbedSyncedDocModel,
].some(k => ctx.matchModel(models[0], k));
}
return true;
},
run(ctx) {
const models = ctx.getSurfaceModels();
if (!models.length) return;
const create = async () => {
const edgeless = getEdgelessWith(ctx);
if (!edgeless) return;
const surfaceId = edgeless.surfaceBlockModel.id;
if (!surfaceId) return;
const title = await promptDocTitle(ctx.std);
if (title === null) return;
const clonedModels = getSortedCloneElements(models);
const linkedDoc = createLinkedDocFromEdgelessElements(
ctx.host,
clonedModels,
title
);
ctx.store.transact(() => {
deleteElements(edgeless, clonedModels);
});
// Inserts linked doc card
const width = 364;
const height = 390;
const bound = getCommonBoundWithRotation(clonedModels);
const cardId = ctx.std.get(EdgelessCRUDIdentifier).addBlock(
EmbedLinkedDocBlockSchema.model.flavour,
{
xywh: `[${bound.center[0] - width / 2}, ${bound.center[1] - height / 2}, ${width}, ${height}]`,
style: 'vertical',
pageId: linkedDoc.id,
},
surfaceId
);
ctx.gfx.selection.set({
elements: [cardId],
editing: false,
});
ctx.track('CanvasElementAdded', {
control: 'context-menu',
type: 'embed-linked-doc',
});
ctx.track('DocCreated', {
control: 'create linked doc',
type: 'embed-linked-doc',
});
ctx.track('LinkedDocCreated', {
control: 'create linked doc',
type: 'embed-linked-doc',
other: 'new doc',
});
notifyDocCreated(ctx.std, ctx.store);
};
create().catch(console.error);
},
},
],
},
// Deleting Group
{
id: 'e.delete',
label: 'Delete',
icon: DeleteIcon(),
variant: 'destructive',
run(ctx) {
const models = ctx.getSurfaceModels();
if (!models.length) return;
const edgeless = getEdgelessWith(ctx);
if (!edgeless) return;
ctx.store.captureSync();
deleteElements(edgeless, models);
// Clears
ctx.select('surface');
ctx.reset();
},
},
] as const satisfies ToolbarActions;
function reorderElements(
ctx: ToolbarContext,
models: GfxModel[],
type: ReorderingType
) {
if (!models.length) return;
for (const model of models) {
const index = ctx.gfx.layer.getReorderedIndex(model, type);
// block should be updated in transaction
if (model instanceof GfxBlockElementModel) {
ctx.store.transact(() => {
model.index = index;
});
} else {
model.index = index;
}
}
}
function isRefreshableModel(model: GfxModel) {
return (
model instanceof AttachmentBlockModel ||
model instanceof BookmarkBlockModel ||
model instanceof ImageBlockModel ||
isExternalEmbedModel(model)
);
}
function isRefreshableBlock(block: BlockComponent | null) {
return (
!!block &&
(block instanceof AttachmentBlockComponent ||
block instanceof BookmarkBlockComponent ||
block instanceof ImageBlockComponent ||
isExternalEmbedBlockComponent(block))
);
}

View File

@@ -35,7 +35,7 @@ import { AddTextIcon, ShapeIcon } from '@blocksuite/icons/lit';
import { html } from 'lit'; import { html } from 'lit';
import isEqual from 'lodash-es/isEqual'; import isEqual from 'lodash-es/isEqual';
import { EdgelessRootBlockComponent, type ShapeToolOption } from '../..'; import type { ShapeToolOption } from '../..';
import { ShapeComponentConfig } from '../../components/toolbar/shape/shape-menu-config'; import { ShapeComponentConfig } from '../../components/toolbar/shape/shape-menu-config';
import { mountShapeTextEditor } from '../../utils/text'; import { mountShapeTextEditor } from '../../utils/text';
import { LINE_STYLE_LIST } from './consts'; import { LINE_STYLE_LIST } from './consts';
@@ -44,7 +44,7 @@ import {
createMindmapStyleActionMenu, createMindmapStyleActionMenu,
} from './mindmap'; } from './mindmap';
import { createTextActions } from './text-common'; import { createTextActions } from './text-common';
import { renderMenu } from './utils'; import { getEdgelessWith, renderMenu } from './utils';
export const builtinShapeToolbarConfig = { export const builtinShapeToolbarConfig = {
actions: [ actions: [
@@ -318,15 +318,8 @@ export const builtinShapeToolbarConfig = {
const model = ctx.getCurrentModelByType(ShapeElementModel); const model = ctx.getCurrentModelByType(ShapeElementModel);
if (!model) return; if (!model) return;
const rootModel = ctx.store.root; const edgeless = getEdgelessWith(ctx);
if (!rootModel) return; if (!edgeless) return;
// TODO(@fundon): it should be simple
const edgeless = ctx.view.getBlock(rootModel.id);
if (!ctx.matchBlock(edgeless, EdgelessRootBlockComponent)) {
console.error('edgeless view is not found.');
return;
}
mountShapeTextEditor(model, edgeless); mountShapeTextEditor(model, edgeless);
}, },

View File

@@ -1,8 +1,10 @@
import type { ToolbarContext } from '@blocksuite/affine-shared/services';
import { ArrowDownSmallIcon } from '@blocksuite/icons/lit'; import { ArrowDownSmallIcon } from '@blocksuite/icons/lit';
import { html } from 'lit'; import { html } from 'lit';
import { ifDefined } from 'lit/directives/if-defined.js'; import { ifDefined } from 'lit/directives/if-defined.js';
import { repeat } from 'lit/directives/repeat.js'; import { repeat } from 'lit/directives/repeat.js';
import { EdgelessRootBlockComponent } from '../..';
import type { Menu, MenuItem } from './types'; import type { Menu, MenuItem } from './types';
export function renderCurrentMenuItemWith<T, F extends keyof MenuItem<T>>( export function renderCurrentMenuItemWith<T, F extends keyof MenuItem<T>>(
@@ -60,3 +62,17 @@ export function renderMenuItems<T>(
` `
); );
} }
// TODO(@fundon): it should be simple
export function getEdgelessWith(ctx: ToolbarContext) {
const rootModel = ctx.store.root;
if (!rootModel) return;
const edgeless = ctx.view.getBlock(rootModel.id);
if (!ctx.matchBlock(edgeless, EdgelessRootBlockComponent)) {
console.error('edgeless view is not found.');
return;
}
return edgeless;
}

View File

@@ -26,7 +26,7 @@ import {
offset, offset,
shift, shift,
} from '@floating-ui/dom'; } from '@floating-ui/dom';
import { html, render, type TemplateResult } from 'lit'; import { html, render } from 'lit';
import { ifDefined } from 'lit/directives/if-defined.js'; import { ifDefined } from 'lit/directives/if-defined.js';
import { join } from 'lit/directives/join.js'; import { join } from 'lit/directives/join.js';
import { keyed } from 'lit/directives/keyed.js'; import { keyed } from 'lit/directives/keyed.js';
@@ -223,32 +223,31 @@ export function renderToolbar(
context, context,
renderMenuActionItem renderMenuActionItem
); );
// if (moreMenuItems.length) { if (moreMenuItems.length) {
// TODO(@fundon): edgeless case needs to be considered const key = `${context.getCurrentModel()?.id}`;
const key = `${context.getCurrentModel()?.id}`;
primaryActionGroup.push({ primaryActionGroup.push({
id: 'more', id: 'more',
content: html`${keyed( content: html`${keyed(
`${flavour}:${key}`, `${flavour}:${key}`,
html` html`
<editor-menu-button <editor-menu-button
class="more-menu" class="more-menu"
.contentPadding="${'8px'}" .contentPadding="${'8px'}"
.button=${html` .button=${html`
<editor-icon-button aria-label="More" .tooltip="${'More'}"> <editor-icon-button aria-label="More" .tooltip="${'More'}">
${MoreVerticalIcon()} ${MoreVerticalIcon()}
</editor-icon-button> </editor-icon-button>
`} `}
> >
<div data-size="large" data-orientation="vertical"> <div data-size="large" data-orientation="vertical">
${join(moreMenuItems, renderToolbarSeparator('horizontal'))} ${join(moreMenuItems, renderToolbarSeparator('horizontal'))}
</div> </div>
</editor-menu-button> </editor-menu-button>
` `
)}`, )}`,
}); });
// } }
} }
render( render(
@@ -264,20 +263,18 @@ function renderActions(
) { ) {
return actions return actions
.map(action => { .map(action => {
let content: TemplateResult | null = null;
if ('content' in action) { if ('content' in action) {
if (typeof action.content === 'function') { if (typeof action.content === 'function') {
content = action.content(context); return action.content(context);
} else { } else {
content = action.content ?? null; return action.content ?? null;
} }
return content;
} }
if ('actions' in action && action.actions.length) { if ('actions' in action && action.actions.length) {
const combined = combine(action.actions, context); const combined = combine(action.actions, context);
if (!combined.length) return content; if (!combined.length) return null;
const ordered = orderBy(combined, ['id', 'score'], ['asc', 'asc']); const ordered = orderBy(combined, ['id', 'score'], ['asc', 'asc']);
@@ -301,7 +298,7 @@ function renderActions(
return render(action, context); return render(action, context);
} }
return content; return null;
}) })
.filter(action => action !== null); .filter(action => action !== null);
} }