import { addSiblingAttachmentBlocks } from '@blocksuite/affine-block-attachment'; import { insertDatabaseBlockCommand } from '@blocksuite/affine-block-database'; import { insertImagesCommand } from '@blocksuite/affine-block-image'; import { insertLatexBlockCommand } from '@blocksuite/affine-block-latex'; import { canDedentListCommand, canIndentListCommand, dedentListCommand, indentListCommand, } from '@blocksuite/affine-block-list'; import { updateBlockType } from '@blocksuite/affine-block-note'; import { canDedentParagraphCommand, canIndentParagraphCommand, dedentParagraphCommand, indentParagraphCommand, } from '@blocksuite/affine-block-paragraph'; import { getSurfaceBlock } from '@blocksuite/affine-block-surface'; import { insertSurfaceRefBlockCommand } from '@blocksuite/affine-block-surface-ref'; import { toggleEmbedCardCreateModal } from '@blocksuite/affine-components/embed-card-modal'; import { toast } from '@blocksuite/affine-components/toast'; import type { FrameBlockModel } from '@blocksuite/affine-model'; import { formatBlockCommand, formatNativeCommand, formatTextCommand, getInlineEditorByModel, getTextStyle, insertContent, insertInlineLatex, toggleBold, toggleCode, toggleItalic, toggleLink, toggleStrike, toggleUnderline, } from '@blocksuite/affine-rich-text'; import { copySelectedModelsCommand, deleteSelectedModelsCommand, draftSelectedModelsCommand, duplicateSelectedModelsCommand, getBlockSelectionsCommand, getSelectedModelsCommand, getTextSelectionCommand, } from '@blocksuite/affine-shared/commands'; import { REFERENCE_NODE } from '@blocksuite/affine-shared/consts'; import { FileSizeLimitService } from '@blocksuite/affine-shared/services'; import type { AffineTextAttributes } from '@blocksuite/affine-shared/types'; import { createDefaultDoc, openFileOrFiles, type Signal, } from '@blocksuite/affine-shared/utils'; import type { BlockStdScope } from '@blocksuite/block-std'; import { viewPresets } from '@blocksuite/data-view/view-presets'; import { assertType } from '@blocksuite/global/utils'; import { AttachmentIcon, BoldIcon, BulletedListIcon, CheckBoxCheckLinearIcon, CloseIcon, CodeBlockIcon, CodeIcon, CollapseTabIcon, CopyIcon, DatabaseKanbanViewIcon, DatabaseTableViewIcon, DeleteIcon, DividerIcon, DuplicateIcon, FontIcon, FrameIcon, GithubIcon, GroupIcon, ImageIcon, ItalicIcon, LinkedPageIcon, LinkIcon, LoomLogoIcon, NewPageIcon, NowIcon, NumberedListIcon, PlusIcon, QuoteIcon, RedoIcon, RightTabIcon, StrikeThroughIcon, TeXIcon, TextIcon, TodayIcon, TomorrowIcon, UnderLineIcon, UndoIcon, YesterdayIcon, YoutubeDuotoneIcon, } from '@blocksuite/icons/lit'; import { computed } from '@preact/signals-core'; import { cssVarV2 } from '@toeverything/theme/v2'; import type { TemplateResult } from 'lit'; import type { PageRootBlockComponent } from '../../page/page-root-block.js'; import { formatDate, formatTime } from '../../utils/misc.js'; import type { AffineLinkedDocWidget } from '../linked-doc/index.js'; import { FigmaDuotoneIcon, HeadingIcon, HighLightDuotoneIcon, TextBackgroundDuotoneIcon, TextColorIcon, } from './icons.js'; export type KeyboardToolbarConfig = { items: KeyboardToolbarItem[]; }; export type KeyboardToolbarItem = | KeyboardToolbarActionItem | KeyboardSubToolbarConfig | KeyboardToolPanelConfig; export type KeyboardIconType = | TemplateResult | ((ctx: KeyboardToolbarContext) => TemplateResult); export type KeyboardToolbarActionItem = { name: string; icon: KeyboardIconType; background?: string | ((ctx: KeyboardToolbarContext) => string | undefined); /** * @default true * @description Whether to show the item in the toolbar. */ showWhen?: (ctx: KeyboardToolbarContext) => boolean; /** * @default false * @description Whether to set the item as disabled status. */ disableWhen?: (ctx: KeyboardToolbarContext) => boolean; /** * @description The action to be executed when the item is clicked. */ action?: (ctx: KeyboardToolbarContext) => void | Promise; }; export type KeyboardSubToolbarConfig = { icon: KeyboardIconType; items: KeyboardToolbarItem[]; /** * It will enter this sub-toolbar when the condition is met. */ autoShow?: (ctx: KeyboardToolbarContext) => Signal; }; export type KeyboardToolbarContext = { std: BlockStdScope; rootComponent: PageRootBlockComponent; /** * Close tool bar, and blur the focus if blur is true, default is false */ closeToolbar: (blur?: boolean) => void; /** * Close current tool panel and show virtual keyboard */ closeToolPanel: () => void; }; export type KeyboardToolPanelConfig = { icon: KeyboardIconType; activeIcon?: KeyboardIconType; activeBackground?: string; groups: (KeyboardToolPanelGroup | DynamicKeyboardToolPanelGroup)[]; }; export type KeyboardToolPanelGroup = { name: string; items: KeyboardToolbarActionItem[]; }; export type DynamicKeyboardToolPanelGroup = ( ctx: KeyboardToolbarContext ) => KeyboardToolPanelGroup | null; const textToolActionItems: KeyboardToolbarActionItem[] = [ { name: 'Text', icon: TextIcon(), showWhen: ({ std }) => std.store.schema.flavourSchemaMap.has('affine:paragraph'), action: ({ std }) => { std.command.exec(updateBlockType, { flavour: 'affine:paragraph', props: { type: 'text' }, }); }, }, ...([1, 2, 3, 4, 5, 6] as const).map(i => ({ name: `Heading ${i}`, icon: HeadingIcon(i), showWhen: ({ std }: KeyboardToolbarContext) => std.store.schema.flavourSchemaMap.has('affine:paragraph'), action: ({ std }: KeyboardToolbarContext) => { std.command.exec(updateBlockType, { flavour: 'affine:paragraph', props: { type: `h${i}` }, }); }, })), { name: 'CodeBlock', showWhen: ({ std }) => std.store.schema.flavourSchemaMap.has('affine:code'), icon: CodeBlockIcon(), action: ({ std }) => { std.command.exec(updateBlockType, { flavour: 'affine:code', }); }, }, { name: 'Quote', showWhen: ({ std }) => std.store.schema.flavourSchemaMap.has('affine:paragraph'), icon: QuoteIcon(), action: ({ std }) => { std.command.exec(updateBlockType, { flavour: 'affine:paragraph', props: { type: 'quote' }, }); }, }, { name: 'Divider', icon: DividerIcon(), showWhen: ({ std }) => std.store.schema.flavourSchemaMap.has('affine:divider'), action: ({ std }) => { std.command.exec(updateBlockType, { flavour: 'affine:divider', props: { type: 'divider' }, }); }, }, { name: 'Inline equation', icon: TeXIcon(), showWhen: ({ std }) => std.store.schema.flavourSchemaMap.has('affine:paragraph'), action: ({ std }) => { std.command .chain() .pipe(getTextSelectionCommand) .pipe(insertInlineLatex) .run(); }, }, ]; const listToolActionItems: KeyboardToolbarActionItem[] = [ { name: 'BulletedList', icon: BulletedListIcon(), showWhen: ({ std }) => std.store.schema.flavourSchemaMap.has('affine:list'), action: ({ std }) => { std.command.exec(updateBlockType, { flavour: 'affine:list', props: { type: 'bulleted', }, }); }, }, { name: 'NumberedList', icon: NumberedListIcon(), showWhen: ({ std }) => std.store.schema.flavourSchemaMap.has('affine:list'), action: ({ std }) => { std.command.exec(updateBlockType, { flavour: 'affine:list', props: { type: 'numbered', }, }); }, }, { name: 'CheckBox', icon: CheckBoxCheckLinearIcon(), showWhen: ({ std }) => std.store.schema.flavourSchemaMap.has('affine:list'), action: ({ std }) => { std.command.exec(updateBlockType, { flavour: 'affine:list', props: { type: 'todo', }, }); }, }, ]; const pageToolGroup: KeyboardToolPanelGroup = { name: 'Page', items: [ { name: 'NewPage', icon: NewPageIcon(), showWhen: ({ std }) => std.store.schema.flavourSchemaMap.has('affine:embed-linked-doc'), action: ({ std }) => { std.command .chain() .pipe(getSelectedModelsCommand) .pipe(({ selectedModels }) => { const newDoc = createDefaultDoc(std.store.workspace); if (!selectedModels?.length) return; insertContent(std.host, selectedModels[0], REFERENCE_NODE, { reference: { type: 'LinkedPage', pageId: newDoc.id, }, }); }) .run(); }, }, { name: 'LinkedPage', icon: LinkedPageIcon(), showWhen: ({ std, rootComponent }) => { const linkedDocWidget = std.view.getWidget( 'affine-linked-doc-widget', rootComponent.model.id ); if (!linkedDocWidget) return false; return std.store.schema.flavourSchemaMap.has('affine:embed-linked-doc'); }, action: ({ rootComponent, closeToolPanel }) => { const { std } = rootComponent; const linkedDocWidget = std.view.getWidget( 'affine-linked-doc-widget', rootComponent.model.id ); if (!linkedDocWidget) return; assertType(linkedDocWidget); const triggerKey = linkedDocWidget.config.triggerKeys[0]; std.command .chain() .pipe(getSelectedModelsCommand) .pipe(ctx => { const { selectedModels } = ctx; if (!selectedModels?.length) return; const currentModel = selectedModels[0]; insertContent(std.host, currentModel, triggerKey); const inlineEditor = getInlineEditorByModel(std.host, currentModel); // Wait for range to be updated inlineEditor?.slots.inlineRangeSync.once(() => { linkedDocWidget.show({ mode: 'mobile', addTriggerKey: true, }); closeToolPanel(); }); }) .run(); }, }, ], }; const contentMediaToolGroup: KeyboardToolPanelGroup = { name: 'Content & Media', items: [ { name: 'Image', icon: ImageIcon(), showWhen: ({ std }) => std.store.schema.flavourSchemaMap.has('affine:image'), action: ({ std }) => { std.command .chain() .pipe(getSelectedModelsCommand) .pipe(insertImagesCommand, { removeEmptyLine: true }) .run(); }, }, { name: 'Link', icon: LinkIcon(), showWhen: ({ std }) => std.store.schema.flavourSchemaMap.has('affine:bookmark'), action: async ({ std }) => { const [_, { selectedModels }] = std.command.exec( getSelectedModelsCommand ); const model = selectedModels?.[0]; if (!model) return; const parentModel = std.store.getParent(model); if (!parentModel) return; const index = parentModel.children.indexOf(model) + 1; await toggleEmbedCardCreateModal( std.host, 'Links', 'The added link will be displayed as a card view.', { mode: 'page', parentModel, index } ); if (model.text?.length === 0) { std.store.deleteBlock(model); } }, }, { name: 'Attachment', icon: AttachmentIcon(), showWhen: ({ std }) => std.store.schema.flavourSchemaMap.has('affine:attachment'), action: async ({ std }) => { const [_, { selectedModels }] = std.command.exec( getSelectedModelsCommand ); const model = selectedModels?.[0]; if (!model) return; const file = await openFileOrFiles(); if (!file) return; const maxFileSize = std.store.get(FileSizeLimitService).maxFileSize; await addSiblingAttachmentBlocks(std.host, [file], maxFileSize, model); if (model.text?.length === 0) { std.store.deleteBlock(model); } }, }, { name: 'Youtube', icon: YoutubeDuotoneIcon({ style: `color: white`, }), showWhen: ({ std }) => std.store.schema.flavourSchemaMap.has('affine:embed-youtube'), action: async ({ std }) => { const [_, { selectedModels }] = std.command.exec( getSelectedModelsCommand ); const model = selectedModels?.[0]; if (!model) return; const parentModel = std.store.getParent(model); if (!parentModel) return; const index = parentModel.children.indexOf(model) + 1; await toggleEmbedCardCreateModal( std.host, 'YouTube', 'The added YouTube video link will be displayed as an embed view.', { mode: 'page', parentModel, index } ); if (model.text?.length === 0) { std.store.deleteBlock(model); } }, }, { name: 'Github', icon: GithubIcon({ style: `color: black` }), showWhen: ({ std }) => std.store.schema.flavourSchemaMap.has('affine:embed-github'), action: async ({ std }) => { const [_, { selectedModels }] = std.command.exec( getSelectedModelsCommand ); const model = selectedModels?.[0]; if (!model) return; const parentModel = std.store.getParent(model); if (!parentModel) return; const index = parentModel.children.indexOf(model) + 1; await toggleEmbedCardCreateModal( std.host, 'GitHub', 'The added GitHub issue or pull request link will be displayed as a card view.', { mode: 'page', parentModel, index } ); if (model.text?.length === 0) { std.store.deleteBlock(model); } }, }, { name: 'Figma', icon: FigmaDuotoneIcon, showWhen: ({ std }) => std.store.schema.flavourSchemaMap.has('affine:embed-figma'), action: async ({ std }) => { const [_, { selectedModels }] = std.command.exec( getSelectedModelsCommand ); const model = selectedModels?.[0]; if (!model) return; const parentModel = std.store.getParent(model); if (!parentModel) { return; } const index = parentModel.children.indexOf(model) + 1; await toggleEmbedCardCreateModal( std.host, 'Figma', 'The added Figma link will be displayed as an embed view.', { mode: 'page', parentModel, index } ); if (model.text?.length === 0) { std.store.deleteBlock(model); } }, }, { name: 'Loom', icon: LoomLogoIcon({ style: `color: #625DF5` }), showWhen: ({ std }) => std.store.schema.flavourSchemaMap.has('affine:embed-loom'), action: async ({ std }) => { const [_, { selectedModels }] = std.command.exec( getSelectedModelsCommand ); const model = selectedModels?.[0]; if (!model) return; const parentModel = std.store.getParent(model); if (!parentModel) return; const index = parentModel.children.indexOf(model) + 1; await toggleEmbedCardCreateModal( std.host, 'Loom', 'The added Loom video link will be displayed as an embed view.', { mode: 'page', parentModel, index } ); if (model.text?.length === 0) { std.store.deleteBlock(model); } }, }, { name: 'Equation', icon: TeXIcon(), showWhen: ({ std }) => std.store.schema.flavourSchemaMap.has('affine:latex'), action: ({ std }) => { std.command .chain() .pipe(getSelectedModelsCommand) .pipe(insertLatexBlockCommand, { place: 'after', removeEmptyLine: true, }) .run(); }, }, ], }; const documentGroupFrameToolGroup: DynamicKeyboardToolPanelGroup = ({ std, }) => { const { store } = std; const frameModels = store .getBlocksByFlavour('affine:frame') .map(block => block.model) as FrameBlockModel[]; const frameItems = frameModels.map(frameModel => ({ name: 'Frame: ' + frameModel.title.toString(), icon: FrameIcon(), action: ({ std }) => { std.command .chain() .pipe(getSelectedModelsCommand) .pipe(insertSurfaceRefBlockCommand, { reference: frameModel.id, place: 'after', removeEmptyLine: true, }) .run(); }, })); const surfaceModel = getSurfaceBlock(store); const groupElements = surfaceModel ? surfaceModel.getElementsByType('group') : []; const groupItems = groupElements.map(group => ({ name: 'Group: ' + group.title.toString(), icon: GroupIcon(), action: ({ std }) => { std.command .chain() .pipe(getSelectedModelsCommand) .pipe(insertSurfaceRefBlockCommand, { reference: group.id, place: 'after', removeEmptyLine: true, }) .run(); }, })); const items = [...frameItems, ...groupItems]; if (items.length === 0) return null; return { name: 'Document Group&Frame', items, }; }; const dateToolGroup: KeyboardToolPanelGroup = { name: 'Date', items: [ { name: 'Today', icon: TodayIcon(), action: ({ std }) => { const [_, { selectedModels }] = std.command.exec( getSelectedModelsCommand ); const model = selectedModels?.[0]; if (!model) return; insertContent(std.host, model, formatDate(new Date())); }, }, { name: 'Tomorrow', icon: TomorrowIcon(), action: ({ std }) => { const [_, { selectedModels }] = std.command.exec( getSelectedModelsCommand ); const model = selectedModels?.[0]; if (!model) return; const tomorrow = new Date(); tomorrow.setDate(tomorrow.getDate() + 1); insertContent(std.host, model, formatDate(tomorrow)); }, }, { name: 'Yesterday', icon: YesterdayIcon(), action: ({ std }) => { const [_, { selectedModels }] = std.command.exec( getSelectedModelsCommand ); const model = selectedModels?.[0]; if (!model) return; const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); insertContent(std.host, model, formatDate(yesterday)); }, }, { name: 'Now', icon: NowIcon(), action: ({ std }) => { const [_, { selectedModels }] = std.command.exec( getSelectedModelsCommand ); const model = selectedModels?.[0]; if (!model) return; insertContent(std.host, model, formatTime(new Date())); }, }, ], }; const databaseToolGroup: KeyboardToolPanelGroup = { name: 'Database', items: [ { name: 'Table view', icon: DatabaseTableViewIcon(), showWhen: ({ std }) => std.store.schema.flavourSchemaMap.has('affine:database'), action: ({ std }) => { std.command .chain() .pipe(getSelectedModelsCommand) .pipe(insertDatabaseBlockCommand, { viewType: viewPresets.tableViewMeta.type, place: 'after', removeEmptyLine: true, }) .run(); }, }, { name: 'Kanban view', icon: DatabaseKanbanViewIcon(), showWhen: ({ std }) => std.store.schema.flavourSchemaMap.has('affine:database'), action: ({ std }) => { std.command .chain() .pipe(getSelectedModelsCommand) .pipe(insertDatabaseBlockCommand, { viewType: viewPresets.kanbanViewMeta.type, place: 'after', removeEmptyLine: true, }) .run(); }, }, ], }; const moreToolPanel: KeyboardToolPanelConfig = { icon: PlusIcon(), activeIcon: CloseIcon({ style: `color: ${cssVarV2('icon/activated')}`, }), activeBackground: cssVarV2('edgeless/selection/selectionMarqueeBackground'), groups: [ { name: 'Basic', items: textToolActionItems }, { name: 'List', items: listToolActionItems }, pageToolGroup, contentMediaToolGroup, documentGroupFrameToolGroup, dateToolGroup, databaseToolGroup, ], }; const textToolPanel: KeyboardToolPanelConfig = { icon: TextIcon(), groups: [ { name: 'Turn into', items: textToolActionItems, }, ], }; const textStyleToolItems: KeyboardToolbarItem[] = [ { name: 'Bold', icon: BoldIcon(), background: ({ std }) => { const [_, { textStyle }] = std.command.exec(getTextStyle); return textStyle?.bold ? '#00000012' : ''; }, action: ({ std }) => { std.command.exec(toggleBold); }, }, { name: 'Italic', icon: ItalicIcon(), background: ({ std }) => { const [_, { textStyle }] = std.command.exec(getTextStyle); return textStyle?.italic ? '#00000012' : ''; }, action: ({ std }) => { std.command.exec(toggleItalic); }, }, { name: 'UnderLine', icon: UnderLineIcon(), background: ({ std }) => { const [_, { textStyle }] = std.command.exec(getTextStyle); return textStyle?.underline ? '#00000012' : ''; }, action: ({ std }) => { std.command.exec(toggleUnderline); }, }, { name: 'StrikeThrough', icon: StrikeThroughIcon(), background: ({ std }) => { const [_, { textStyle }] = std.command.exec(getTextStyle); return textStyle?.strike ? '#00000012' : ''; }, action: ({ std }) => { std.command.exec(toggleStrike); }, }, { name: 'Code', icon: CodeIcon(), background: ({ std }) => { const [_, { textStyle }] = std.command.exec(getTextStyle); return textStyle?.code ? '#00000012' : ''; }, action: ({ std }) => { std.command.exec(toggleCode); }, }, { name: 'Link', icon: LinkIcon(), background: ({ std }) => { const [_, { textStyle }] = std.command.exec(getTextStyle); return textStyle?.link ? '#00000012' : ''; }, action: ({ std }) => { std.command.exec(toggleLink); }, }, ]; const highlightToolPanel: KeyboardToolPanelConfig = { icon: ({ std }) => { const [_, { textStyle }] = std.command.exec(getTextStyle); if (textStyle?.color) { return HighLightDuotoneIcon(textStyle.color); } else { return HighLightDuotoneIcon(cssVarV2('icon/primary')); } }, groups: [ { name: 'Color', items: [ { name: 'Default Color', icon: TextColorIcon(cssVarV2('text/highlight/fg/orange')), }, ...( [ 'red', 'orange', 'yellow', 'green', 'teal', 'blue', 'purple', 'grey', ] as const ).map(color => ({ name: color.charAt(0).toUpperCase() + color.slice(1), icon: TextColorIcon(cssVarV2(`text/highlight/fg/${color}`)), action: ({ std }) => { const payload = { styles: { color: cssVarV2(`text/highlight/fg/${color}`), } satisfies AffineTextAttributes, }; std.command .chain() .try(chain => [ chain .pipe(getTextSelectionCommand) .pipe(formatTextCommand, payload), chain .pipe(getBlockSelectionsCommand) .pipe(formatBlockCommand, payload), chain.pipe(formatNativeCommand, payload), ]) .run(); }, })), ], }, { name: 'Background', items: [ { name: 'Default Color', icon: TextBackgroundDuotoneIcon(cssVarV2('text/highlight/bg/orange')), }, ...( [ 'red', 'orange', 'yellow', 'green', 'teal', 'blue', 'purple', 'grey', ] as const ).map(color => ({ name: color.charAt(0).toUpperCase() + color.slice(1), icon: TextBackgroundDuotoneIcon( cssVarV2(`text/highlight/bg/${color}`) ), action: ({ std }) => { const payload = { styles: { background: cssVarV2(`text/highlight/bg/${color}`), } satisfies AffineTextAttributes, }; std.command .chain() .try(chain => [ chain .pipe(getTextSelectionCommand) .pipe(formatTextCommand, payload), chain .pipe(getBlockSelectionsCommand) .pipe(formatBlockCommand, payload), chain.pipe(formatNativeCommand, payload), ]) .run(); }, })), ], }, ], }; const textSubToolbarConfig: KeyboardSubToolbarConfig = { icon: FontIcon(), items: [ textToolPanel, ...textStyleToolItems, { name: 'InlineTex', icon: TeXIcon(), action: ({ std }) => { std.command .chain() .pipe(getTextSelectionCommand) .pipe(insertInlineLatex) .run(); }, }, highlightToolPanel, ], autoShow: ({ std }) => { return computed(() => { const [_, { currentTextSelection: selection }] = std.command.exec( getTextSelectionCommand ); return selection ? !selection.isCollapsed() : false; }); }, }; export const defaultKeyboardToolbarConfig: KeyboardToolbarConfig = { items: [ moreToolPanel, // TODO(@L-Sun): add ai function in AFFiNE side // { icon: AiIcon(iconStyle) }, textSubToolbarConfig, { name: 'Image', icon: ImageIcon(), showWhen: ({ std }) => std.store.schema.flavourSchemaMap.has('affine:image'), action: ({ std }) => { std.command .chain() .pipe(getSelectedModelsCommand) .pipe(insertImagesCommand, { removeEmptyLine: true }) .run(); }, }, { name: 'Attachment', icon: AttachmentIcon(), showWhen: ({ std }) => std.store.schema.flavourSchemaMap.has('affine:attachment'), action: async ({ std }) => { const [_, { selectedModels }] = std.command.exec( getSelectedModelsCommand ); const model = selectedModels?.[0]; if (!model) return; const file = await openFileOrFiles(); if (!file) return; const maxFileSize = std.store.get(FileSizeLimitService).maxFileSize; await addSiblingAttachmentBlocks(std.host, [file], maxFileSize, model); if (model.text?.length === 0) { std.store.deleteBlock(model); } }, }, { name: 'Undo', icon: UndoIcon(), disableWhen: ({ std }) => !std.store.canUndo, action: ({ std }) => { std.store.undo(); }, }, { name: 'Redo', icon: RedoIcon(), disableWhen: ({ std }) => !std.store.canRedo, action: ({ std }) => { std.store.redo(); }, }, { name: 'RightTab', icon: RightTabIcon(), disableWhen: ({ std }) => { const [success] = std.command .chain() .tryAll(chain => [ chain.pipe(canIndentParagraphCommand), chain.pipe(canIndentListCommand), ]) .run(); return !success; }, action: ({ std }) => { std.command .chain() .tryAll(chain => [ chain.pipe(canIndentParagraphCommand).pipe(indentParagraphCommand), chain.pipe(canIndentListCommand).pipe(indentListCommand), ]) .run(); }, }, ...listToolActionItems, ...textToolActionItems.filter(({ name }) => name === 'Divider'), { name: 'CollapseTab', icon: CollapseTabIcon(), disableWhen: ({ std }) => { const [success] = std.command .chain() .tryAll(chain => [ chain.pipe(canDedentParagraphCommand), chain.pipe(canDedentListCommand), ]) .run(); return !success; }, action: ({ std }) => { std.command .chain() .tryAll(chain => [ chain.pipe(canDedentParagraphCommand).pipe(dedentParagraphCommand), chain.pipe(canDedentListCommand).pipe(dedentListCommand), ]) .run(); }, }, { name: 'Copy', icon: CopyIcon(), action: ({ std }) => { std.command .chain() .pipe(getSelectedModelsCommand) .with({ onCopy: () => { toast(std.host, 'Copied to clipboard'); }, }) .pipe(draftSelectedModelsCommand) .pipe(copySelectedModelsCommand) .run(); }, }, { name: 'Duplicate', icon: DuplicateIcon(), action: ({ std }) => { std.command .chain() .pipe(getSelectedModelsCommand) .pipe(draftSelectedModelsCommand) .pipe(duplicateSelectedModelsCommand) .run(); }, }, { name: 'Delete', icon: DeleteIcon(), action: ({ std }) => { std.command .chain() .pipe(getSelectedModelsCommand) .pipe(deleteSelectedModelsCommand) .run(); }, }, ], };