mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-11 22:18:54 +08:00
feat(editor): add embed doc block extension (#12090)
Closes: BS-3393 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced a new "Embed Doc" block, enabling embedding and rendering of linked and synced documents within cards, including support for banners and note previews. - Added new toolbar and quick search options for inserting embedded linked and synced documents. - **Improvements** - Updated dependencies and internal references to support the new embed doc functionality across related blocks and components. - Enhanced support for edgeless environments with new clipboard and configuration options for embedded docs. - **Refactor** - Streamlined and reorganized embed-related code, moving linked and synced doc logic into a dedicated embed doc module. - Removed obsolete adapter and utility files to simplify maintenance. - **Chores** - Updated project and TypeScript configuration files to include the new embed doc module in builds and references. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
|
||||
import { EmbedLinkedDocHtmlAdapterExtension } from './html.js';
|
||||
import { EmbedLinkedDocMarkdownAdapterExtension } from './markdown.js';
|
||||
import { EmbedLinkedDocBlockPlainTextAdapterExtension } from './plain-text.js';
|
||||
|
||||
export const EmbedLinkedDocBlockAdapterExtensions: ExtensionType[] = [
|
||||
EmbedLinkedDocHtmlAdapterExtension,
|
||||
EmbedLinkedDocMarkdownAdapterExtension,
|
||||
EmbedLinkedDocBlockPlainTextAdapterExtension,
|
||||
];
|
||||
@@ -0,0 +1,63 @@
|
||||
import { EmbedLinkedDocBlockSchema } from '@blocksuite/affine-model';
|
||||
import {
|
||||
AdapterTextUtils,
|
||||
BlockHtmlAdapterExtension,
|
||||
type BlockHtmlAdapterMatcher,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
|
||||
export const embedLinkedDocBlockHtmlAdapterMatcher: BlockHtmlAdapterMatcher = {
|
||||
flavour: EmbedLinkedDocBlockSchema.model.flavour,
|
||||
toMatch: () => false,
|
||||
fromMatch: o => o.node.flavour === EmbedLinkedDocBlockSchema.model.flavour,
|
||||
toBlockSnapshot: {},
|
||||
fromBlockSnapshot: {
|
||||
enter: (o, context) => {
|
||||
const { configs, walkerContext } = context;
|
||||
// Parse as link
|
||||
if (!o.node.props.pageId) {
|
||||
return;
|
||||
}
|
||||
const title = configs.get('title:' + o.node.props.pageId) ?? 'untitled';
|
||||
const url = AdapterTextUtils.generateDocUrl(
|
||||
configs.get('docLinkBaseUrl') ?? '',
|
||||
String(o.node.props.pageId),
|
||||
o.node.props.params ?? Object.create(null)
|
||||
);
|
||||
|
||||
walkerContext
|
||||
.openNode(
|
||||
{
|
||||
type: 'element',
|
||||
tagName: 'div',
|
||||
properties: {
|
||||
className: ['affine-paragraph-block-container'],
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
'children'
|
||||
)
|
||||
.openNode(
|
||||
{
|
||||
type: 'element',
|
||||
tagName: 'a',
|
||||
properties: {
|
||||
href: url,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
value: title,
|
||||
},
|
||||
],
|
||||
},
|
||||
'children'
|
||||
)
|
||||
.closeNode()
|
||||
.closeNode();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const EmbedLinkedDocHtmlAdapterExtension = BlockHtmlAdapterExtension(
|
||||
embedLinkedDocBlockHtmlAdapterMatcher
|
||||
);
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './html.js';
|
||||
export * from './markdown.js';
|
||||
export * from './plain-text.js';
|
||||
@@ -0,0 +1,131 @@
|
||||
import {
|
||||
EmbedLinkedDocBlockSchema,
|
||||
FootNoteReferenceParamsSchema,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
AdapterTextUtils,
|
||||
BlockMarkdownAdapterExtension,
|
||||
type BlockMarkdownAdapterMatcher,
|
||||
FOOTNOTE_DEFINITION_PREFIX,
|
||||
getFootnoteDefinitionText,
|
||||
isFootnoteDefinitionNode,
|
||||
type MarkdownAST,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import { FeatureFlagService } from '@blocksuite/affine-shared/services';
|
||||
import { nanoid } from '@blocksuite/store';
|
||||
|
||||
const isLinkedDocFootnoteDefinitionNode = (node: MarkdownAST) => {
|
||||
if (!isFootnoteDefinitionNode(node)) return false;
|
||||
const footnoteDefinition = getFootnoteDefinitionText(node);
|
||||
try {
|
||||
const footnoteDefinitionJson = FootNoteReferenceParamsSchema.parse(
|
||||
JSON.parse(footnoteDefinition)
|
||||
);
|
||||
return (
|
||||
footnoteDefinitionJson.type === 'doc' && !!footnoteDefinitionJson.docId
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const embedLinkedDocBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher =
|
||||
{
|
||||
flavour: EmbedLinkedDocBlockSchema.model.flavour,
|
||||
toMatch: o => isLinkedDocFootnoteDefinitionNode(o.node),
|
||||
fromMatch: o => o.node.flavour === EmbedLinkedDocBlockSchema.model.flavour,
|
||||
toBlockSnapshot: {
|
||||
enter: (o, context) => {
|
||||
const { provider } = context;
|
||||
let enableCitation = false;
|
||||
try {
|
||||
const featureFlagService = provider?.get(FeatureFlagService);
|
||||
enableCitation = !!featureFlagService?.getFlag('enable_citation');
|
||||
} catch {
|
||||
enableCitation = false;
|
||||
}
|
||||
if (!isFootnoteDefinitionNode(o.node) || !enableCitation) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { walkerContext, configs } = context;
|
||||
const footnoteIdentifier = o.node.identifier;
|
||||
const footnoteDefinitionKey = `${FOOTNOTE_DEFINITION_PREFIX}${footnoteIdentifier}`;
|
||||
const footnoteDefinition = configs.get(footnoteDefinitionKey);
|
||||
if (!footnoteDefinition) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const footnoteDefinitionJson = FootNoteReferenceParamsSchema.parse(
|
||||
JSON.parse(footnoteDefinition)
|
||||
);
|
||||
const { docId } = footnoteDefinitionJson;
|
||||
if (!docId) {
|
||||
return;
|
||||
}
|
||||
walkerContext
|
||||
.openNode(
|
||||
{
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: EmbedLinkedDocBlockSchema.model.flavour,
|
||||
props: {
|
||||
pageId: docId,
|
||||
footnoteIdentifier,
|
||||
style: 'citation',
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
'children'
|
||||
)
|
||||
.closeNode();
|
||||
walkerContext.skipAllChildren();
|
||||
} catch (err) {
|
||||
console.warn('Failed to parse linked doc footnote definition:', err);
|
||||
return;
|
||||
}
|
||||
},
|
||||
},
|
||||
fromBlockSnapshot: {
|
||||
enter: (o, context) => {
|
||||
const { configs, walkerContext } = context;
|
||||
// Parse as link
|
||||
if (!o.node.props.pageId) {
|
||||
return;
|
||||
}
|
||||
const title = configs.get('title:' + o.node.props.pageId) ?? 'untitled';
|
||||
const url = AdapterTextUtils.generateDocUrl(
|
||||
configs.get('docLinkBaseUrl') ?? '',
|
||||
String(o.node.props.pageId),
|
||||
o.node.props.params ?? Object.create(null)
|
||||
);
|
||||
walkerContext
|
||||
.openNode(
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [],
|
||||
},
|
||||
'children'
|
||||
)
|
||||
.openNode(
|
||||
{
|
||||
type: 'link',
|
||||
url,
|
||||
title: o.node.props.caption as string | null,
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
value: title,
|
||||
},
|
||||
],
|
||||
},
|
||||
'children'
|
||||
)
|
||||
.closeNode()
|
||||
.closeNode();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const EmbedLinkedDocMarkdownAdapterExtension =
|
||||
BlockMarkdownAdapterExtension(embedLinkedDocBlockMarkdownAdapterMatcher);
|
||||
@@ -0,0 +1,33 @@
|
||||
import { EmbedLinkedDocBlockSchema } from '@blocksuite/affine-model';
|
||||
import {
|
||||
AdapterTextUtils,
|
||||
BlockPlainTextAdapterExtension,
|
||||
type BlockPlainTextAdapterMatcher,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
|
||||
export const embedLinkedDocBlockPlainTextAdapterMatcher: BlockPlainTextAdapterMatcher =
|
||||
{
|
||||
flavour: EmbedLinkedDocBlockSchema.model.flavour,
|
||||
toMatch: () => false,
|
||||
fromMatch: o => o.node.flavour === EmbedLinkedDocBlockSchema.model.flavour,
|
||||
toBlockSnapshot: {},
|
||||
fromBlockSnapshot: {
|
||||
enter: (o, context) => {
|
||||
const { configs, textBuffer } = context;
|
||||
// Parse as link
|
||||
if (!o.node.props.pageId) {
|
||||
return;
|
||||
}
|
||||
const title = configs.get('title:' + o.node.props.pageId) ?? 'untitled';
|
||||
const url = AdapterTextUtils.generateDocUrl(
|
||||
configs.get('docLinkBaseUrl') ?? '',
|
||||
String(o.node.props.pageId),
|
||||
o.node.props.params ?? Object.create(null)
|
||||
);
|
||||
textBuffer.content += `${title}: ${url}\n`;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const EmbedLinkedDocBlockPlainTextAdapterExtension =
|
||||
BlockPlainTextAdapterExtension(embedLinkedDocBlockPlainTextAdapterMatcher);
|
||||
@@ -0,0 +1 @@
|
||||
export * from './insert-embed-linked-doc';
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import { insertEmbedCard } from '@blocksuite/affine-block-embed';
|
||||
import type { EmbedCardStyle, ReferenceParams } from '@blocksuite/affine-model';
|
||||
import type { Command } from '@blocksuite/std';
|
||||
|
||||
export type LinkableFlavour =
|
||||
| 'affine:bookmark'
|
||||
| 'affine:embed-linked-doc'
|
||||
| 'affine:embed-iframe'
|
||||
| 'affine:embed-figma'
|
||||
| 'affine:embed-github'
|
||||
| 'affine:embed-loom'
|
||||
| 'affine:embed-youtube';
|
||||
|
||||
export type InsertedLinkType = {
|
||||
flavour: LinkableFlavour;
|
||||
} | null;
|
||||
|
||||
export const insertEmbedLinkedDocCommand: Command<
|
||||
{
|
||||
docId: string;
|
||||
params?: ReferenceParams;
|
||||
},
|
||||
{ blockId: string }
|
||||
> = (ctx, next) => {
|
||||
const { docId, params, std } = ctx;
|
||||
const flavour = 'affine:embed-linked-doc';
|
||||
const targetStyle: EmbedCardStyle = 'vertical';
|
||||
const props: Record<string, unknown> = { pageId: docId };
|
||||
if (params) props.params = params;
|
||||
const blockId = insertEmbedCard(std, { flavour, targetStyle, props });
|
||||
if (!blockId) return;
|
||||
next({ blockId });
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
import { EmbedLinkedDocBlockSchema } from '@blocksuite/affine-model';
|
||||
import { insertContent } from '@blocksuite/affine-rich-text';
|
||||
import { REFERENCE_NODE } from '@blocksuite/affine-shared/consts';
|
||||
import { createDefaultDoc } from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
type SlashMenuConfig,
|
||||
SlashMenuConfigIdentifier,
|
||||
} from '@blocksuite/affine-widget-slash-menu';
|
||||
import { LinkedPageIcon, PlusIcon } from '@blocksuite/icons/lit';
|
||||
import { type ExtensionType } from '@blocksuite/store';
|
||||
|
||||
import { LinkDocTooltip, NewDocTooltip } from './tooltips';
|
||||
|
||||
const linkedDocSlashMenuConfig: SlashMenuConfig = {
|
||||
items: [
|
||||
{
|
||||
name: 'New Doc',
|
||||
description: 'Start a new document.',
|
||||
icon: PlusIcon(),
|
||||
tooltip: {
|
||||
figure: NewDocTooltip,
|
||||
caption: 'New Doc',
|
||||
},
|
||||
group: '3_Page@0',
|
||||
when: ({ model }) =>
|
||||
model.doc.schema.flavourSchemaMap.has('affine:embed-linked-doc'),
|
||||
action: ({ std, model }) => {
|
||||
const newDoc = createDefaultDoc(std.host.doc.workspace);
|
||||
insertContent(std, model, REFERENCE_NODE, {
|
||||
reference: {
|
||||
type: 'LinkedPage',
|
||||
pageId: newDoc.id,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Linked Doc',
|
||||
description: 'Link to another document.',
|
||||
icon: LinkedPageIcon(),
|
||||
tooltip: {
|
||||
figure: LinkDocTooltip,
|
||||
caption: 'Link Doc',
|
||||
},
|
||||
searchAlias: ['dual link'],
|
||||
group: '3_Page@1',
|
||||
when: ({ std, model }) => {
|
||||
const root = model.doc.root;
|
||||
if (!root) return false;
|
||||
const linkedDocWidget = std.view.getWidget(
|
||||
'affine-linked-doc-widget',
|
||||
root.id
|
||||
);
|
||||
if (!linkedDocWidget) return false;
|
||||
|
||||
return model.doc.schema.flavourSchemaMap.has('affine:embed-linked-doc');
|
||||
},
|
||||
action: ({ model, std }) => {
|
||||
const root = model.doc.root;
|
||||
if (!root) return;
|
||||
const linkedDocWidget = std.view.getWidget(
|
||||
'affine-linked-doc-widget',
|
||||
root.id
|
||||
);
|
||||
if (!linkedDocWidget) return;
|
||||
// TODO(@L-Sun): make linked-doc-widget as extension
|
||||
// @ts-expect-error same as above
|
||||
linkedDocWidget.show({ addTriggerKey: true });
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const LinkedDocSlashMenuConfigIdentifier = SlashMenuConfigIdentifier(
|
||||
EmbedLinkedDocBlockSchema.model.flavour
|
||||
);
|
||||
|
||||
export const LinkedDocSlashMenuConfigExtension: ExtensionType = {
|
||||
setup: di => {
|
||||
di.addImpl(LinkedDocSlashMenuConfigIdentifier, linkedDocSlashMenuConfig);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,514 @@
|
||||
import { toast } from '@blocksuite/affine-components/toast';
|
||||
import { EditorChevronDown } from '@blocksuite/affine-components/toolbar';
|
||||
import {
|
||||
type EmbedCardStyle,
|
||||
EmbedLinkedDocModel,
|
||||
EmbedLinkedDocStyles,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
EMBED_CARD_HEIGHT,
|
||||
EMBED_CARD_WIDTH,
|
||||
} from '@blocksuite/affine-shared/consts';
|
||||
import {
|
||||
ActionPlacement,
|
||||
DocDisplayMetaProvider,
|
||||
EditorSettingProvider,
|
||||
FeatureFlagService,
|
||||
type LinkEventType,
|
||||
type OpenDocMode,
|
||||
type ToolbarAction,
|
||||
type ToolbarActionGroup,
|
||||
type ToolbarContext,
|
||||
type ToolbarModuleConfig,
|
||||
ToolbarModuleExtension,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import {
|
||||
getBlockProps,
|
||||
referenceToNode,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import { Bound } from '@blocksuite/global/gfx';
|
||||
import {
|
||||
CaptionIcon,
|
||||
CopyIcon,
|
||||
DeleteIcon,
|
||||
DuplicateIcon,
|
||||
ExpandFullIcon,
|
||||
OpenInNewIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { BlockFlavourIdentifier, isGfxBlockComponent } from '@blocksuite/std';
|
||||
import { type ExtensionType, Slice } from '@blocksuite/store';
|
||||
import { computed, signal } from '@preact/signals-core';
|
||||
import { html } from 'lit';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { keyed } from 'lit/directives/keyed.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
import { EmbedLinkedDocBlockComponent } from '../embed-linked-doc-block';
|
||||
|
||||
const trackBaseProps = {
|
||||
category: 'linked doc',
|
||||
type: 'card view',
|
||||
};
|
||||
|
||||
const createOnToggleFn =
|
||||
(
|
||||
ctx: ToolbarContext,
|
||||
name: Extract<
|
||||
LinkEventType,
|
||||
| 'OpenedViewSelector'
|
||||
| 'OpenedCardStyleSelector'
|
||||
| 'OpenedCardScaleSelector'
|
||||
>,
|
||||
control: 'switch view' | 'switch card style' | 'switch card scale'
|
||||
) =>
|
||||
(e: CustomEvent<boolean>) => {
|
||||
e.stopPropagation();
|
||||
const opened = e.detail;
|
||||
if (!opened) return;
|
||||
|
||||
ctx.track(name, { ...trackBaseProps, control });
|
||||
};
|
||||
|
||||
const docTitleAction = {
|
||||
id: 'a.doc-title',
|
||||
content(ctx) {
|
||||
const block = ctx.getCurrentBlockByType(EmbedLinkedDocBlockComponent);
|
||||
if (!block) return null;
|
||||
|
||||
const model = block.model;
|
||||
if (!model.props.title) return null;
|
||||
|
||||
const originalTitle =
|
||||
ctx.std.get(DocDisplayMetaProvider).title(model.props.pageId).value ||
|
||||
'Untitled';
|
||||
const open = (event: MouseEvent) => block.open({ event });
|
||||
|
||||
return html`<affine-linked-doc-title
|
||||
.title=${originalTitle}
|
||||
.open=${open}
|
||||
></affine-linked-doc-title>`;
|
||||
},
|
||||
} as const satisfies ToolbarAction;
|
||||
|
||||
const captionAction = {
|
||||
id: 'd.caption',
|
||||
tooltip: 'Caption',
|
||||
icon: CaptionIcon(),
|
||||
run(ctx) {
|
||||
const block = ctx.getCurrentBlockByType(EmbedLinkedDocBlockComponent);
|
||||
block?.captionEditor?.show();
|
||||
|
||||
ctx.track('OpenedCaptionEditor', {
|
||||
...trackBaseProps,
|
||||
control: 'add caption',
|
||||
});
|
||||
},
|
||||
} as const satisfies ToolbarAction;
|
||||
|
||||
const openDocActions = [
|
||||
{
|
||||
mode: 'open-in-active-view',
|
||||
id: 'a.open-in-active-view',
|
||||
label: 'Open this doc',
|
||||
icon: ExpandFullIcon(),
|
||||
},
|
||||
] as const satisfies (Pick<ToolbarAction, 'id' | 'label' | 'icon'> & {
|
||||
mode: OpenDocMode;
|
||||
})[];
|
||||
|
||||
const openDocActionGroup = {
|
||||
placement: ActionPlacement.Start,
|
||||
id: 'A.open-doc',
|
||||
content(ctx) {
|
||||
const block = ctx.getCurrentBlockByType(EmbedLinkedDocBlockComponent);
|
||||
if (!block) return null;
|
||||
|
||||
const actions = openDocActions.map<ToolbarAction>(action => {
|
||||
const openMode = action.mode;
|
||||
const shouldOpenInActiveView = openMode === 'open-in-active-view';
|
||||
return {
|
||||
...action,
|
||||
disabled: shouldOpenInActiveView
|
||||
? block.model.props.pageId === ctx.store.id
|
||||
: false,
|
||||
when: true,
|
||||
run: (_ctx: ToolbarContext) => block.open({ openMode }),
|
||||
};
|
||||
});
|
||||
|
||||
return html`
|
||||
<editor-menu-button
|
||||
.contentPadding="${'8px'}"
|
||||
.button=${html`
|
||||
<editor-icon-button aria-label="Open doc" .tooltip=${'Open doc'}>
|
||||
${OpenInNewIcon()} ${EditorChevronDown}
|
||||
</editor-icon-button>
|
||||
`}
|
||||
>
|
||||
<div data-size="small" data-orientation="vertical">
|
||||
${repeat(
|
||||
actions,
|
||||
action => action.id,
|
||||
({ label, icon, run, disabled }) => html`
|
||||
<editor-menu-action
|
||||
aria-label=${ifDefined(label)}
|
||||
?disabled=${ifDefined(
|
||||
typeof disabled === 'function' ? disabled(ctx) : disabled
|
||||
)}
|
||||
@click=${() => run?.(ctx)}
|
||||
>
|
||||
${icon}<span class="label">${label}</span>
|
||||
</editor-menu-action>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</editor-menu-button>
|
||||
`;
|
||||
},
|
||||
} as const satisfies ToolbarAction;
|
||||
|
||||
const conversionsActionGroup = {
|
||||
id: 'b.conversions',
|
||||
actions: [
|
||||
{
|
||||
id: 'inline',
|
||||
label: 'Inline view',
|
||||
run(ctx) {
|
||||
const block = ctx.getCurrentBlockByType(EmbedLinkedDocBlockComponent);
|
||||
block?.convertToInline();
|
||||
|
||||
// Clears
|
||||
ctx.select('note');
|
||||
ctx.reset();
|
||||
|
||||
ctx.track('SelectedView', {
|
||||
...trackBaseProps,
|
||||
control: 'select view',
|
||||
type: 'inline view',
|
||||
});
|
||||
},
|
||||
when: ctx => !ctx.hasSelectedSurfaceModels,
|
||||
},
|
||||
{
|
||||
id: 'card',
|
||||
label: 'Card view',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
id: 'embed',
|
||||
label: 'Embed view',
|
||||
disabled(ctx) {
|
||||
const block = ctx.getCurrentBlockByType(EmbedLinkedDocBlockComponent);
|
||||
if (!block) return true;
|
||||
|
||||
if (block.closest('affine-embed-synced-doc-block')) return true;
|
||||
|
||||
const model = block.model;
|
||||
|
||||
// same doc
|
||||
if (model.props.pageId === ctx.store.id) return true;
|
||||
|
||||
// linking to block
|
||||
if (referenceToNode(model.props)) return true;
|
||||
|
||||
return false;
|
||||
},
|
||||
run(ctx) {
|
||||
const block = ctx.getCurrentBlockByType(EmbedLinkedDocBlockComponent);
|
||||
|
||||
if (
|
||||
ctx.std
|
||||
.get(FeatureFlagService)
|
||||
.getFlag('enable_embed_doc_with_alias') &&
|
||||
isGfxBlockComponent(block)
|
||||
) {
|
||||
const editorSetting = ctx.std.getOptional(EditorSettingProvider);
|
||||
editorSetting?.set?.(
|
||||
'docDropCanvasPreferView',
|
||||
'affine:embed-synced-doc'
|
||||
);
|
||||
}
|
||||
|
||||
block?.convertToEmbed();
|
||||
|
||||
ctx.track('SelectedView', {
|
||||
...trackBaseProps,
|
||||
control: 'select view',
|
||||
type: 'embed view',
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
content(ctx) {
|
||||
const model = ctx.getCurrentModelByType(EmbedLinkedDocModel);
|
||||
if (!model) return null;
|
||||
|
||||
const actions = this.actions.map(action => ({ ...action }));
|
||||
const viewType$ = signal('Card view');
|
||||
const onToggle = createOnToggleFn(ctx, 'OpenedViewSelector', 'switch view');
|
||||
|
||||
return html`${keyed(
|
||||
model,
|
||||
html`<affine-view-dropdown-menu
|
||||
@toggle=${onToggle}
|
||||
.actions=${actions}
|
||||
.context=${ctx}
|
||||
.viewType$=${viewType$}
|
||||
></affine-view-dropdown-menu>`
|
||||
)}`;
|
||||
},
|
||||
} as const satisfies ToolbarActionGroup<ToolbarAction>;
|
||||
|
||||
const builtinToolbarConfig = {
|
||||
actions: [
|
||||
docTitleAction,
|
||||
conversionsActionGroup,
|
||||
{
|
||||
id: 'c.style',
|
||||
actions: [
|
||||
{
|
||||
id: 'horizontal',
|
||||
label: 'Large horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'list',
|
||||
label: 'Small horizontal style',
|
||||
},
|
||||
].filter(action =>
|
||||
EmbedLinkedDocStyles.includes(action.id as EmbedCardStyle)
|
||||
),
|
||||
content(ctx) {
|
||||
const model = ctx.getCurrentModelByType(EmbedLinkedDocModel);
|
||||
if (!model) return null;
|
||||
|
||||
const actions = this.actions.map(action => ({
|
||||
...action,
|
||||
run: ({ store }) => {
|
||||
store.updateBlock(model, { style: action.id });
|
||||
|
||||
ctx.track('SelectedCardStyle', {
|
||||
...trackBaseProps,
|
||||
control: 'select card style',
|
||||
type: action.id,
|
||||
});
|
||||
},
|
||||
})) satisfies ToolbarAction[];
|
||||
const onToggle = createOnToggleFn(
|
||||
ctx,
|
||||
'OpenedCardStyleSelector',
|
||||
'switch card style'
|
||||
);
|
||||
|
||||
return html`${keyed(
|
||||
model,
|
||||
html`<affine-card-style-dropdown-menu
|
||||
@toggle=${onToggle}
|
||||
.actions=${actions}
|
||||
.context=${ctx}
|
||||
.style$=${model.props.style$}
|
||||
></affine-card-style-dropdown-menu>`
|
||||
)}`;
|
||||
},
|
||||
} satisfies ToolbarActionGroup<ToolbarAction>,
|
||||
captionAction,
|
||||
{
|
||||
placement: ActionPlacement.More,
|
||||
id: 'a.clipboard',
|
||||
actions: [
|
||||
{
|
||||
id: 'copy',
|
||||
label: 'Copy',
|
||||
icon: CopyIcon(),
|
||||
run(ctx) {
|
||||
const model = ctx.getCurrentModelByType(EmbedLinkedDocModel);
|
||||
if (!model) return;
|
||||
|
||||
const slice = Slice.fromModels(ctx.store, [model]);
|
||||
ctx.clipboard
|
||||
.copySlice(slice)
|
||||
.then(() => toast(ctx.host, 'Copied to clipboard'))
|
||||
.catch(console.error);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'duplicate',
|
||||
label: 'Duplicate',
|
||||
icon: DuplicateIcon(),
|
||||
run(ctx) {
|
||||
const model = ctx.getCurrentModelByType(EmbedLinkedDocModel);
|
||||
if (!model) return;
|
||||
|
||||
const { flavour, parent } = model;
|
||||
const props = getBlockProps(model);
|
||||
const index = parent?.children.indexOf(model);
|
||||
|
||||
ctx.store.addBlock(flavour, props, parent, index);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
placement: ActionPlacement.More,
|
||||
id: 'c.delete',
|
||||
label: 'Delete',
|
||||
icon: DeleteIcon(),
|
||||
variant: 'destructive',
|
||||
run(ctx) {
|
||||
const model = ctx.getCurrentModelByType(EmbedLinkedDocModel);
|
||||
if (!model) return;
|
||||
|
||||
ctx.store.deleteBlock(model);
|
||||
|
||||
// Clears
|
||||
ctx.select('note');
|
||||
ctx.reset();
|
||||
},
|
||||
},
|
||||
],
|
||||
} as const satisfies ToolbarModuleConfig;
|
||||
|
||||
const builtinSurfaceToolbarConfig = {
|
||||
actions: [
|
||||
openDocActionGroup,
|
||||
docTitleAction,
|
||||
conversionsActionGroup,
|
||||
{
|
||||
id: 'c.style',
|
||||
actions: [
|
||||
{
|
||||
id: 'horizontal',
|
||||
label: 'Large horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'list',
|
||||
label: 'Small horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'vertical',
|
||||
label: 'Large vertical style',
|
||||
},
|
||||
{
|
||||
id: 'cube',
|
||||
label: 'Small vertical style',
|
||||
},
|
||||
].filter(action =>
|
||||
EmbedLinkedDocStyles.includes(action.id as EmbedCardStyle)
|
||||
),
|
||||
content(ctx) {
|
||||
const model = ctx.getCurrentModelByType(EmbedLinkedDocModel);
|
||||
if (!model) return null;
|
||||
|
||||
const actions = this.actions.map(action => ({
|
||||
...action,
|
||||
run: ({ store }) => {
|
||||
const style = action.id as EmbedCardStyle;
|
||||
const bounds = Bound.deserialize(model.xywh);
|
||||
bounds.w = EMBED_CARD_WIDTH[style];
|
||||
bounds.h = EMBED_CARD_HEIGHT[style];
|
||||
const xywh = bounds.serialize();
|
||||
|
||||
store.updateBlock(model, { style, xywh });
|
||||
|
||||
ctx.track('SelectedCardStyle', {
|
||||
...trackBaseProps,
|
||||
control: 'select card style',
|
||||
type: style,
|
||||
});
|
||||
},
|
||||
})) satisfies ToolbarAction[];
|
||||
const style$ = model.props.style$;
|
||||
const onToggle = createOnToggleFn(
|
||||
ctx,
|
||||
'OpenedCardStyleSelector',
|
||||
'switch card style'
|
||||
);
|
||||
|
||||
return html`${keyed(
|
||||
model,
|
||||
html`<affine-card-style-dropdown-menu
|
||||
@toggle=${onToggle}
|
||||
.actions=${actions}
|
||||
.context=${ctx}
|
||||
.style$=${style$}
|
||||
></affine-card-style-dropdown-menu>`
|
||||
)}`;
|
||||
},
|
||||
} satisfies ToolbarActionGroup<ToolbarAction>,
|
||||
captionAction,
|
||||
{
|
||||
id: 'e.scale',
|
||||
content(ctx) {
|
||||
const model = ctx.getCurrentBlockByType(
|
||||
EmbedLinkedDocBlockComponent
|
||||
)?.model;
|
||||
if (!model) return null;
|
||||
|
||||
const scale$ = computed(() => {
|
||||
const {
|
||||
xywh$: { value: xywh },
|
||||
} = model;
|
||||
const {
|
||||
style$: { value: style },
|
||||
} = model.props;
|
||||
const bounds = Bound.deserialize(xywh);
|
||||
const height = EMBED_CARD_HEIGHT[style];
|
||||
return Math.round(100 * (bounds.h / height));
|
||||
});
|
||||
const onSelect = (e: CustomEvent<number>) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const scale = e.detail / 100;
|
||||
|
||||
const bounds = Bound.deserialize(model.xywh);
|
||||
const style = model.props.style;
|
||||
bounds.h = EMBED_CARD_HEIGHT[style] * scale;
|
||||
bounds.w = EMBED_CARD_WIDTH[style] * scale;
|
||||
const xywh = bounds.serialize();
|
||||
|
||||
ctx.store.updateBlock(model, { xywh });
|
||||
|
||||
ctx.track('SelectedCardScale', {
|
||||
...trackBaseProps,
|
||||
control: 'select card scale',
|
||||
});
|
||||
};
|
||||
const onToggle = createOnToggleFn(
|
||||
ctx,
|
||||
'OpenedCardScaleSelector',
|
||||
'switch card scale'
|
||||
);
|
||||
const format = (value: number) => `${value}%`;
|
||||
|
||||
return html`${keyed(
|
||||
model,
|
||||
html`<affine-size-dropdown-menu
|
||||
@select=${onSelect}
|
||||
@toggle=${onToggle}
|
||||
.format=${format}
|
||||
.size$=${scale$}
|
||||
></affine-size-dropdown-menu>`
|
||||
)}`;
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
when: ctx => ctx.getSurfaceModelsByType(EmbedLinkedDocModel).length === 1,
|
||||
} as const satisfies ToolbarModuleConfig;
|
||||
|
||||
export const createBuiltinToolbarConfigExtension = (
|
||||
flavour: string
|
||||
): ExtensionType[] => {
|
||||
const name = flavour.split(':').pop();
|
||||
|
||||
return [
|
||||
ToolbarModuleExtension({
|
||||
id: BlockFlavourIdentifier(flavour),
|
||||
config: builtinToolbarConfig,
|
||||
}),
|
||||
|
||||
ToolbarModuleExtension({
|
||||
id: BlockFlavourIdentifier(`affine:surface:${name}`),
|
||||
config: builtinSurfaceToolbarConfig,
|
||||
}),
|
||||
];
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { html } from 'lit';
|
||||
// prettier-ignore
|
||||
export const NewDocTooltip = html`<svg width="170" height="68" viewBox="0 0 170 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
<mask id="mask0_16460_991" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="170" height="68">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_16460_991)">
|
||||
<text fill="#8E8D91" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="22" font-weight="600" letter-spacing="0px"><tspan x="8" y="27.5">Title</tspan></text>
|
||||
<text fill="#8E8D91" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="8" y="44.6364">Type '/' for commands</tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
// prettier-ignore
|
||||
export const LinkDocTooltip = html`<svg width="170" height="68" viewBox="0 0 170 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
<mask id="mask0_16460_998" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="170" height="68">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_16460_998)">
|
||||
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="8" y="15.6364">In a decentralized system, we can have a kaleidoscopic </tspan><tspan x="8" y="27.6364">complexity to our data. </tspan><tspan x="8" y="63.6364">Any user may have a different perspective on what data they </tspan><tspan x="8" y="75.6364">either have, choose to share, or accept. </tspan><tspan x="8" y="111.636">For example, one user’s edits to a document might be on </tspan><tspan x="8" y="123.636">their laptop on an airplane; when the plane lands and the </tspan><tspan x="8" y="135.636">computer reconnects, those changes are distributed to </tspan><tspan x="8" y="147.636">other users. </tspan><tspan x="8" y="183.636">Other users might choose to accept all, some, or none of </tspan><tspan x="8" y="195.636">those changes to their version of the document.</tspan></text>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.125 39C10.125 38.2406 10.7406 37.625 11.5 37.625H16.5C17.2594 37.625 17.875 38.2406 17.875 39V42C17.875 42.2071 17.7071 42.375 17.5 42.375C17.2929 42.375 17.125 42.2071 17.125 42V39C17.125 38.6548 16.8452 38.375 16.5 38.375H11.5C11.1548 38.375 10.875 38.6548 10.875 39V45C10.875 45.3452 11.1548 45.625 11.5 45.625H14C14.2071 45.625 14.375 45.7929 14.375 46C14.375 46.2071 14.2071 46.375 14 46.375H11.5C10.7406 46.375 10.125 45.7594 10.125 45V39ZM12.125 40C12.125 39.7929 12.2929 39.625 12.5 39.625H14C14.2071 39.625 14.375 39.7929 14.375 40C14.375 40.2071 14.2071 40.375 14 40.375H12.5C12.2929 40.375 12.125 40.2071 12.125 40ZM12.5 41.375C12.2929 41.375 12.125 41.5429 12.125 41.75C12.125 41.9571 12.2929 42.125 12.5 42.125H15.5C15.7071 42.125 15.875 41.9571 15.875 41.75C15.875 41.5429 15.7071 41.375 15.5 41.375H12.5ZM12.125 43.5C12.125 43.2929 12.2929 43.125 12.5 43.125H13.75C13.9571 43.125 14.125 43.2929 14.125 43.5C14.125 43.7071 13.9571 43.875 13.75 43.875H12.5C12.2929 43.875 12.125 43.7071 12.125 43.5ZM15.75 43.125C15.5429 43.125 15.375 43.2929 15.375 43.5C15.375 43.7071 15.5429 43.875 15.75 43.875H17.0947L15.2348 45.7348C15.0884 45.8813 15.0884 46.1187 15.2348 46.2652C15.3813 46.4116 15.6187 46.4116 15.7652 46.2652L17.625 44.4053V45.75C17.625 45.9571 17.7929 46.125 18 46.125C18.2071 46.125 18.375 45.9571 18.375 45.75V43.5C18.375 43.4005 18.3355 43.3052 18.2652 43.2348C18.1948 43.1645 18.0995 43.125 18 43.125H15.75Z" fill="#77757D"/>
|
||||
<mask id="path-5-inside-1_16460_998" fill="white">
|
||||
<path d="M24 35H98V49H24V35Z"/>
|
||||
</mask>
|
||||
<path d="M98 48.5H24V49.5H98V48.5Z" fill="#E3E2E4" mask="url(#path-5-inside-1_16460_998)"/>
|
||||
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="24" y="45.6364">What’s AFFiNE?</tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import { EdgelessClipboardConfig } from '@blocksuite/affine-block-surface';
|
||||
import { ReferenceInfoSchema } from '@blocksuite/affine-model';
|
||||
import { type BlockSnapshot } from '@blocksuite/store';
|
||||
|
||||
export class EdgelessClipboardEmbedLinkedDocConfig extends EdgelessClipboardConfig {
|
||||
static override readonly key = 'affine:embed-linked-doc';
|
||||
|
||||
override createBlock(linkedDocEmbed: BlockSnapshot): string | null {
|
||||
if (!this.surface) return null;
|
||||
|
||||
const { xywh, style, caption, pageId, params, title, description } =
|
||||
linkedDocEmbed.props;
|
||||
const referenceInfo = ReferenceInfoSchema.parse({
|
||||
pageId,
|
||||
params,
|
||||
title,
|
||||
description,
|
||||
});
|
||||
|
||||
return this.crud.addBlock(
|
||||
'affine:embed-linked-doc',
|
||||
{
|
||||
xywh,
|
||||
style,
|
||||
caption,
|
||||
...referenceInfo,
|
||||
},
|
||||
this.surface.model.id
|
||||
);
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import { toEdgelessEmbedBlock } from '@blocksuite/affine-block-embed';
|
||||
import {
|
||||
EdgelessCRUDIdentifier,
|
||||
reassociateConnectorsCommand,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import {
|
||||
EMBED_CARD_HEIGHT,
|
||||
EMBED_CARD_WIDTH,
|
||||
} from '@blocksuite/affine-shared/consts';
|
||||
import {
|
||||
cloneReferenceInfoWithoutAliases,
|
||||
isNewTabTrigger,
|
||||
isNewViewTrigger,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import { Bound } from '@blocksuite/global/gfx';
|
||||
|
||||
import { EmbedLinkedDocBlockComponent } from './embed-linked-doc-block.js';
|
||||
|
||||
export class EmbedEdgelessLinkedDocBlockComponent extends toEdgelessEmbedBlock(
|
||||
EmbedLinkedDocBlockComponent
|
||||
) {
|
||||
override convertToEmbed = () => {
|
||||
const { caption, xywh } = this.model.props;
|
||||
const { doc, id } = this.model;
|
||||
|
||||
const style = 'syncedDoc';
|
||||
const bound = Bound.deserialize(xywh);
|
||||
bound.w = EMBED_CARD_WIDTH[style];
|
||||
bound.h = EMBED_CARD_HEIGHT[style];
|
||||
|
||||
const { addBlock } = this.std.get(EdgelessCRUDIdentifier);
|
||||
const surface = this.gfx.surface ?? undefined;
|
||||
const newId = addBlock(
|
||||
'affine:embed-synced-doc',
|
||||
{
|
||||
xywh: bound.serialize(),
|
||||
caption,
|
||||
...cloneReferenceInfoWithoutAliases(this.referenceInfo$.peek()),
|
||||
},
|
||||
surface
|
||||
);
|
||||
|
||||
this.std.command.exec(reassociateConnectorsCommand, {
|
||||
oldId: id,
|
||||
newId,
|
||||
});
|
||||
|
||||
this.gfx.selection.set({
|
||||
editing: false,
|
||||
elements: [newId],
|
||||
});
|
||||
|
||||
doc.deleteBlock(this.model);
|
||||
};
|
||||
|
||||
protected override _handleClick = (evt: MouseEvent): void => {
|
||||
if (isNewTabTrigger(evt)) {
|
||||
this.open({ openMode: 'open-in-new-tab', event: evt });
|
||||
} else if (isNewViewTrigger(evt)) {
|
||||
this.open({ openMode: 'open-in-new-view', event: evt });
|
||||
}
|
||||
};
|
||||
}
|
||||
+608
@@ -0,0 +1,608 @@
|
||||
import {
|
||||
EmbedBlockComponent,
|
||||
RENDER_CARD_THROTTLE_MS,
|
||||
} from '@blocksuite/affine-block-embed';
|
||||
import { SurfaceBlockModel } from '@blocksuite/affine-block-surface';
|
||||
import { isPeekable, Peekable } from '@blocksuite/affine-components/peek';
|
||||
import { RefNodeSlotsProvider } from '@blocksuite/affine-inline-reference';
|
||||
import type {
|
||||
DocMode,
|
||||
EmbedLinkedDocModel,
|
||||
EmbedLinkedDocStyles,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
EMBED_CARD_HEIGHT,
|
||||
EMBED_CARD_WIDTH,
|
||||
REFERENCE_NODE,
|
||||
} from '@blocksuite/affine-shared/consts';
|
||||
import {
|
||||
DocDisplayMetaProvider,
|
||||
DocModeProvider,
|
||||
OpenDocExtensionIdentifier,
|
||||
type OpenDocMode,
|
||||
ThemeProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import {
|
||||
cloneReferenceInfo,
|
||||
cloneReferenceInfoWithoutAliases,
|
||||
isNewTabTrigger,
|
||||
isNewViewTrigger,
|
||||
matchModels,
|
||||
referenceToNode,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import { Bound } from '@blocksuite/global/gfx';
|
||||
import { BlockSelection } from '@blocksuite/std';
|
||||
import { Text } from '@blocksuite/store';
|
||||
import { computed } from '@preact/signals-core';
|
||||
import { html, nothing } from 'lit';
|
||||
import { property, queryAsync, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { when } from 'lit/directives/when.js';
|
||||
import throttle from 'lodash-es/throttle';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import { renderLinkedDocInCard } from '../common/render-linked-doc';
|
||||
import { SyncedDocErrorIcon } from '../embed-synced-doc-block/styles.js';
|
||||
import { styles } from './styles.js';
|
||||
import { getEmbedLinkedDocIcons } from './utils.js';
|
||||
|
||||
@Peekable({
|
||||
enableOn: ({ doc }: EmbedLinkedDocBlockComponent) => !doc.readonly,
|
||||
})
|
||||
export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinkedDocModel> {
|
||||
static override styles = styles;
|
||||
|
||||
private readonly _load = async () => {
|
||||
// If this is a citation linked doc block, we don't need to load the linked doc and render linked doc content in card
|
||||
if (this.isCitation) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
loading = true,
|
||||
isError = false,
|
||||
isBannerEmpty = true,
|
||||
isNoteContentEmpty = true,
|
||||
} = this.getInitialState();
|
||||
|
||||
this._loading = loading;
|
||||
this.isError = isError;
|
||||
this.isBannerEmpty = isBannerEmpty;
|
||||
this.isNoteContentEmpty = isNoteContentEmpty;
|
||||
|
||||
if (!this._loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
const linkedDoc = this.linkedDoc;
|
||||
if (!linkedDoc) {
|
||||
this._loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!linkedDoc.loaded) {
|
||||
try {
|
||||
linkedDoc.load();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.isError = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.isError && !linkedDoc.root) {
|
||||
await new Promise<void>(resolve => {
|
||||
const subscription = linkedDoc.slots.rootAdded.subscribe(() => {
|
||||
subscription.unsubscribe();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this._loading = false;
|
||||
|
||||
// If it is a link to a block or element, the content will not be rendered.
|
||||
if (this._referenceToNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isError) {
|
||||
const cardStyle = this.model.props.style;
|
||||
if (cardStyle === 'horizontal' || cardStyle === 'vertical') {
|
||||
renderLinkedDocInCard(this);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private readonly _selectBlock = () => {
|
||||
const selectionManager = this.std.selection;
|
||||
const blockSelection = selectionManager.create(BlockSelection, {
|
||||
blockId: this.blockId,
|
||||
});
|
||||
selectionManager.setGroup('note', [blockSelection]);
|
||||
};
|
||||
|
||||
private readonly _setDocUpdatedAt = () => {
|
||||
const meta = this.doc.workspace.meta.getDocMeta(this.model.props.pageId);
|
||||
if (meta) {
|
||||
const date = meta.updatedDate || meta.createDate;
|
||||
this._docUpdatedAt = new Date(date);
|
||||
}
|
||||
};
|
||||
|
||||
override _cardStyle: (typeof EmbedLinkedDocStyles)[number] = 'horizontal';
|
||||
|
||||
convertToEmbed = () => {
|
||||
if (this._referenceToNode) return;
|
||||
|
||||
const { caption } = this.model.props;
|
||||
const { parent, doc } = this.model;
|
||||
const index = parent?.children.indexOf(this.model);
|
||||
|
||||
const blockId = doc.addBlock(
|
||||
'affine:embed-synced-doc',
|
||||
{
|
||||
caption,
|
||||
...cloneReferenceInfoWithoutAliases(this.referenceInfo$.peek()),
|
||||
},
|
||||
parent,
|
||||
index
|
||||
);
|
||||
|
||||
doc.deleteBlock(this.model);
|
||||
|
||||
this.std.selection.setGroup('note', [
|
||||
this.std.selection.create(BlockSelection, { blockId }),
|
||||
]);
|
||||
};
|
||||
|
||||
convertToInline = () => {
|
||||
const { doc } = this.model;
|
||||
const parent = doc.getParent(this.model);
|
||||
if (!parent) {
|
||||
return;
|
||||
}
|
||||
const index = parent.children.indexOf(this.model);
|
||||
|
||||
const yText = new Y.Text();
|
||||
yText.insert(0, REFERENCE_NODE);
|
||||
yText.format(0, REFERENCE_NODE.length, {
|
||||
reference: {
|
||||
type: 'LinkedPage',
|
||||
...this.referenceInfo$.peek(),
|
||||
},
|
||||
});
|
||||
const text = new Text(yText);
|
||||
|
||||
doc.addBlock(
|
||||
'affine:paragraph',
|
||||
{
|
||||
text,
|
||||
},
|
||||
parent,
|
||||
index
|
||||
);
|
||||
|
||||
doc.deleteBlock(this.model);
|
||||
};
|
||||
|
||||
referenceInfo$ = computed(() => {
|
||||
const { pageId, params, title$, description$ } = this.model.props;
|
||||
return cloneReferenceInfo({
|
||||
pageId,
|
||||
params,
|
||||
title: title$.value,
|
||||
description: description$.value,
|
||||
});
|
||||
});
|
||||
|
||||
icon$ = computed(() => {
|
||||
const { pageId, params, title } = this.referenceInfo$.value;
|
||||
return this.std
|
||||
.get(DocDisplayMetaProvider)
|
||||
.icon(pageId, { params, title, referenced: true }).value;
|
||||
});
|
||||
|
||||
open = ({
|
||||
openMode,
|
||||
event,
|
||||
}: {
|
||||
openMode?: OpenDocMode;
|
||||
event?: MouseEvent;
|
||||
} = {}) => {
|
||||
this.std.getOptional(RefNodeSlotsProvider)?.docLinkClicked.next({
|
||||
...this.referenceInfo$.peek(),
|
||||
openMode,
|
||||
event,
|
||||
host: this.host,
|
||||
});
|
||||
};
|
||||
|
||||
refreshData = () => {
|
||||
this._load().catch(e => {
|
||||
console.error(e);
|
||||
this.isError = true;
|
||||
});
|
||||
};
|
||||
|
||||
title$ = computed(() => {
|
||||
const { pageId, params, title } = this.referenceInfo$.value;
|
||||
return (
|
||||
this.std
|
||||
.get(DocDisplayMetaProvider)
|
||||
.title(pageId, { params, title, referenced: true }) || title
|
||||
);
|
||||
});
|
||||
|
||||
get docTitle() {
|
||||
return this.model.props.title || this.linkedDoc?.meta?.title || 'Untitled';
|
||||
}
|
||||
|
||||
get editorMode() {
|
||||
return this._linkedDocMode;
|
||||
}
|
||||
|
||||
get linkedDoc() {
|
||||
const doc = this.std.workspace.getDoc(this.model.props.pageId);
|
||||
return doc?.getStore({ id: this.model.props.pageId });
|
||||
}
|
||||
|
||||
get readonly() {
|
||||
return this.doc.readonly;
|
||||
}
|
||||
|
||||
get isCitation() {
|
||||
return (
|
||||
!!this.model.props.footnoteIdentifier &&
|
||||
this.model.props.style === 'citation'
|
||||
);
|
||||
}
|
||||
|
||||
private _handleDoubleClick(event: MouseEvent) {
|
||||
event.stopPropagation();
|
||||
const openDocService = this.std.get(OpenDocExtensionIdentifier);
|
||||
const shouldOpenInPeek =
|
||||
openDocService.isAllowed('open-in-center-peek') && isPeekable(this);
|
||||
this.open({
|
||||
openMode: shouldOpenInPeek
|
||||
? 'open-in-center-peek'
|
||||
: 'open-in-active-view',
|
||||
event,
|
||||
});
|
||||
}
|
||||
|
||||
private _isDocEmpty() {
|
||||
const linkedDoc = this.linkedDoc;
|
||||
if (!linkedDoc) {
|
||||
return false;
|
||||
}
|
||||
return !!linkedDoc && this.isNoteContentEmpty && this.isBannerEmpty;
|
||||
}
|
||||
|
||||
protected _handleClick = (event: MouseEvent) => {
|
||||
if (isNewTabTrigger(event)) {
|
||||
this.open({ openMode: 'open-in-new-tab', event });
|
||||
} else if (isNewViewTrigger(event)) {
|
||||
this.open({ openMode: 'open-in-new-view', event });
|
||||
}
|
||||
|
||||
if (this.readonly) {
|
||||
return;
|
||||
}
|
||||
this._selectBlock();
|
||||
};
|
||||
|
||||
private readonly _renderCitationView = () => {
|
||||
const { footnoteIdentifier } = this.model.props;
|
||||
return html`<div
|
||||
draggable="${this.blockDraggable ? 'true' : 'false'}"
|
||||
class=${classMap({
|
||||
'embed-block-container': true,
|
||||
...this.selectedStyle$?.value,
|
||||
})}
|
||||
style=${styleMap({
|
||||
...this.embedContainerStyle,
|
||||
})}
|
||||
>
|
||||
<affine-citation-card
|
||||
.icon=${this.icon$.value}
|
||||
.citationTitle=${this.title$.value}
|
||||
.citationIdentifier=${footnoteIdentifier}
|
||||
.active=${this.selected$.value}
|
||||
.onClickCallback=${this._handleClick}
|
||||
></affine-citation-card>
|
||||
</div> `;
|
||||
};
|
||||
|
||||
private readonly _renderEmbedView = () => {
|
||||
const linkedDoc = this.linkedDoc;
|
||||
const isDeleted = !linkedDoc;
|
||||
const isLoading = this._loading;
|
||||
const isError = this.isError;
|
||||
const isEmpty = this._isDocEmpty() && this.isBannerEmpty;
|
||||
const inCanvas = matchModels(this.model.parent, [SurfaceBlockModel]);
|
||||
|
||||
const cardClassMap = classMap({
|
||||
loading: isLoading,
|
||||
error: isError,
|
||||
deleted: isDeleted,
|
||||
empty: isEmpty,
|
||||
'banner-empty': this.isBannerEmpty,
|
||||
'note-empty': this.isNoteContentEmpty,
|
||||
'in-canvas': inCanvas,
|
||||
[this._cardStyle]: true,
|
||||
});
|
||||
|
||||
const theme = this.std.get(ThemeProvider).theme;
|
||||
const {
|
||||
LoadingIcon,
|
||||
ReloadIcon,
|
||||
LinkedDocDeletedBanner,
|
||||
LinkedDocEmptyBanner,
|
||||
SyncedDocErrorBanner,
|
||||
} = getEmbedLinkedDocIcons(theme, this._linkedDocMode, this._cardStyle);
|
||||
|
||||
const icon = isError
|
||||
? SyncedDocErrorIcon
|
||||
: isLoading
|
||||
? LoadingIcon
|
||||
: this.icon$.value;
|
||||
const title = isLoading ? 'Loading...' : this.title$;
|
||||
const description = this.model.props.description$;
|
||||
|
||||
const showDefaultNoteContent = isError || isLoading || isDeleted || isEmpty;
|
||||
const defaultNoteContent = isError
|
||||
? 'This linked doc failed to load.'
|
||||
: isLoading
|
||||
? ''
|
||||
: isDeleted
|
||||
? 'This linked doc is deleted.'
|
||||
: isEmpty
|
||||
? 'Preview of the doc will be displayed here.'
|
||||
: '';
|
||||
|
||||
const dateText =
|
||||
this._cardStyle === 'cube'
|
||||
? this._docUpdatedAt.toLocaleTimeString()
|
||||
: this._docUpdatedAt.toLocaleString();
|
||||
|
||||
const showDefaultBanner = isError || isLoading || isDeleted || isEmpty;
|
||||
|
||||
const defaultBanner = isError
|
||||
? SyncedDocErrorBanner
|
||||
: isLoading
|
||||
? LinkedDocEmptyBanner
|
||||
: isDeleted
|
||||
? LinkedDocDeletedBanner
|
||||
: LinkedDocEmptyBanner;
|
||||
|
||||
const hasDescriptionAlias = Boolean(description.value);
|
||||
|
||||
return this.renderEmbed(
|
||||
() => html`
|
||||
<div
|
||||
class="affine-embed-linked-doc-block ${cardClassMap}"
|
||||
style=${styleMap({
|
||||
transform: `scale(${this._scale})`,
|
||||
transformOrigin: '0 0',
|
||||
})}
|
||||
@click=${this._handleClick}
|
||||
@dblclick=${this._handleDoubleClick}
|
||||
>
|
||||
<div class="affine-embed-linked-doc-content">
|
||||
<div class="affine-embed-linked-doc-content-title">
|
||||
<div class="affine-embed-linked-doc-content-title-icon">
|
||||
${icon}
|
||||
</div>
|
||||
|
||||
<div class="affine-embed-linked-doc-content-title-text">
|
||||
${title}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${when(
|
||||
hasDescriptionAlias,
|
||||
() =>
|
||||
html`<div class="affine-embed-linked-doc-content-note alias">
|
||||
${repeat(
|
||||
(description.value ?? '').split('\n'),
|
||||
text => html`<p>${text}</p>`
|
||||
)}
|
||||
</div>`,
|
||||
() =>
|
||||
when(
|
||||
showDefaultNoteContent,
|
||||
() => html`
|
||||
<div class="affine-embed-linked-doc-content-note default">
|
||||
${defaultNoteContent}
|
||||
</div>
|
||||
`,
|
||||
() => html`
|
||||
<div
|
||||
class="affine-embed-linked-doc-content-note render"
|
||||
></div>
|
||||
`
|
||||
)
|
||||
)}
|
||||
${isError
|
||||
? html`
|
||||
<div class="affine-embed-linked-doc-card-content-reload">
|
||||
<div
|
||||
class="affine-embed-linked-doc-card-content-reload-button"
|
||||
@click=${this.refreshData}
|
||||
>
|
||||
${ReloadIcon} <span>Reload</span>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="affine-embed-linked-doc-content-date">
|
||||
<span>Updated</span>
|
||||
|
||||
<span>${dateText}</span>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
|
||||
${showDefaultBanner
|
||||
? html`
|
||||
<div class="affine-embed-linked-doc-banner default">
|
||||
${defaultBanner}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`
|
||||
);
|
||||
};
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
this._cardStyle = this.model.props.style;
|
||||
this._referenceToNode = referenceToNode(this.model.props);
|
||||
|
||||
this._load().catch(e => {
|
||||
console.error(e);
|
||||
this.isError = true;
|
||||
});
|
||||
|
||||
const linkedDoc = this.linkedDoc;
|
||||
if (linkedDoc) {
|
||||
this.disposables.add(
|
||||
linkedDoc.workspace.slots.docListUpdated.subscribe(() => {
|
||||
this._load().catch(e => {
|
||||
console.error(e);
|
||||
this.isError = true;
|
||||
});
|
||||
})
|
||||
);
|
||||
// Should throttle the blockUpdated event to avoid too many re-renders
|
||||
// Because the blockUpdated event is triggered too frequently at some cases
|
||||
this.disposables.add(
|
||||
linkedDoc.slots.blockUpdated.subscribe(
|
||||
throttle(payload => {
|
||||
if (
|
||||
payload.type === 'update' &&
|
||||
['', 'caption', 'xywh'].includes(payload.props.key)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.type === 'add' && payload.init) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._load().catch(e => {
|
||||
console.error(e);
|
||||
this.isError = true;
|
||||
});
|
||||
}, RENDER_CARD_THROTTLE_MS)
|
||||
)
|
||||
);
|
||||
|
||||
this._setDocUpdatedAt();
|
||||
this.disposables.add(
|
||||
this.doc.workspace.slots.docListUpdated.subscribe(() => {
|
||||
this._setDocUpdatedAt();
|
||||
})
|
||||
);
|
||||
|
||||
if (this._referenceToNode) {
|
||||
this._linkedDocMode = this.model.props.params?.mode ?? 'page';
|
||||
} else {
|
||||
const docMode = this.std.get(DocModeProvider);
|
||||
this._linkedDocMode = docMode.getPrimaryMode(this.model.props.pageId);
|
||||
this.disposables.add(
|
||||
docMode.onPrimaryModeChange(mode => {
|
||||
this._linkedDocMode = mode;
|
||||
}, this.model.props.pageId)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.disposables.add(
|
||||
this.model.propsUpdated.subscribe(({ key }) => {
|
||||
if (key === 'style') {
|
||||
this._cardStyle = this.model.props.style;
|
||||
}
|
||||
if (key === 'pageId' || key === 'style') {
|
||||
this._load().catch(e => {
|
||||
console.error(e);
|
||||
this.isError = true;
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getInitialState(): {
|
||||
loading?: boolean;
|
||||
isError?: boolean;
|
||||
isNoteContentEmpty?: boolean;
|
||||
isBannerEmpty?: boolean;
|
||||
} {
|
||||
return {};
|
||||
}
|
||||
|
||||
override renderBlock() {
|
||||
return this.isCitation
|
||||
? this._renderCitationView()
|
||||
: this._renderEmbedView();
|
||||
}
|
||||
|
||||
override updated() {
|
||||
if (this.readonly) {
|
||||
return;
|
||||
}
|
||||
// update card style when linked doc deleted
|
||||
const linkedDoc = this.linkedDoc;
|
||||
const { xywh, style } = this.model.props;
|
||||
const bound = Bound.deserialize(xywh);
|
||||
if (linkedDoc && style === 'horizontalThin') {
|
||||
bound.w = EMBED_CARD_WIDTH.horizontal;
|
||||
bound.h = EMBED_CARD_HEIGHT.horizontal;
|
||||
this.doc.withoutTransact(() => {
|
||||
this.doc.updateBlock(this.model, {
|
||||
xywh: bound.serialize(),
|
||||
style: 'horizontal',
|
||||
});
|
||||
});
|
||||
} else if (!linkedDoc && style === 'horizontal') {
|
||||
bound.w = EMBED_CARD_WIDTH.horizontalThin;
|
||||
bound.h = EMBED_CARD_HEIGHT.horizontalThin;
|
||||
this.doc.withoutTransact(() => {
|
||||
this.doc.updateBlock(this.model, {
|
||||
xywh: bound.serialize(),
|
||||
style: 'horizontalThin',
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@state()
|
||||
private accessor _docUpdatedAt: Date = new Date();
|
||||
|
||||
@state()
|
||||
private accessor _linkedDocMode: DocMode = 'page';
|
||||
|
||||
@state()
|
||||
private accessor _loading = false;
|
||||
|
||||
// reference to block/element
|
||||
@state()
|
||||
private accessor _referenceToNode = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor isBannerEmpty = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor isError = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor isNoteContentEmpty = false;
|
||||
|
||||
@queryAsync('.affine-embed-linked-doc-content-note.render')
|
||||
accessor noteContainer!: Promise<HTMLDivElement | null>;
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import { EmbedLinkedDocBlockSchema } from '@blocksuite/affine-model';
|
||||
import { BlockViewExtension } from '@blocksuite/std';
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
import { literal } from 'lit/static-html.js';
|
||||
|
||||
import { EmbedLinkedDocBlockAdapterExtensions } from './adapters/extension';
|
||||
import { LinkedDocSlashMenuConfigExtension } from './configs/slash-menu';
|
||||
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
|
||||
|
||||
const flavour = EmbedLinkedDocBlockSchema.model.flavour;
|
||||
|
||||
export const EmbedLinkedDocBlockSpec: ExtensionType[] = [
|
||||
BlockViewExtension(flavour, model => {
|
||||
return model.parent?.flavour === 'affine:surface'
|
||||
? literal`affine-embed-edgeless-linked-doc-block`
|
||||
: literal`affine-embed-linked-doc-block`;
|
||||
}),
|
||||
EmbedLinkedDocBlockAdapterExtensions,
|
||||
createBuiltinToolbarConfigExtension(flavour),
|
||||
LinkedDocSlashMenuConfigExtension,
|
||||
].flat();
|
||||
|
||||
export const EmbedLinkedDocViewExtensions: ExtensionType[] = [
|
||||
BlockViewExtension(flavour, model => {
|
||||
return model.parent?.flavour === 'affine:surface'
|
||||
? literal`affine-embed-edgeless-linked-doc-block`
|
||||
: literal`affine-embed-linked-doc-block`;
|
||||
}),
|
||||
createBuiltinToolbarConfigExtension(flavour),
|
||||
LinkedDocSlashMenuConfigExtension,
|
||||
].flat();
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from './adapters';
|
||||
export * from './commands';
|
||||
export { LinkedDocSlashMenuConfigIdentifier } from './configs/slash-menu';
|
||||
export * from './edgeless-clipboard-config';
|
||||
export * from './embed-linked-doc-block';
|
||||
export * from './embed-linked-doc-spec';
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,115 @@
|
||||
import {
|
||||
EmbedEdgelessIcon,
|
||||
EmbedPageIcon,
|
||||
getLoadingIconWith,
|
||||
ReloadIcon,
|
||||
} from '@blocksuite/affine-components/icons';
|
||||
import {
|
||||
ColorScheme,
|
||||
type EmbedLinkedDocStyles,
|
||||
} from '@blocksuite/affine-model';
|
||||
import type { TemplateResult } from 'lit';
|
||||
|
||||
import {
|
||||
DarkSyncedDocErrorBanner,
|
||||
LightSyncedDocErrorBanner,
|
||||
} from '../embed-synced-doc-block/styles.js';
|
||||
import {
|
||||
DarkLinkedEdgelessDeletedLargeBanner,
|
||||
DarkLinkedEdgelessDeletedSmallBanner,
|
||||
DarkLinkedEdgelessEmptyLargeBanner,
|
||||
DarkLinkedEdgelessEmptySmallBanner,
|
||||
DarkLinkedPageDeletedLargeBanner,
|
||||
DarkLinkedPageDeletedSmallBanner,
|
||||
DarkLinkedPageEmptyLargeBanner,
|
||||
DarkLinkedPageEmptySmallBanner,
|
||||
LightLinkedEdgelessDeletedLargeBanner,
|
||||
LightLinkedEdgelessDeletedSmallBanner,
|
||||
LightLinkedEdgelessEmptyLargeBanner,
|
||||
LightLinkedEdgelessEmptySmallBanner,
|
||||
LightLinkedPageDeletedLargeBanner,
|
||||
LightLinkedPageDeletedSmallBanner,
|
||||
LightLinkedPageEmptyLargeBanner,
|
||||
LightLinkedPageEmptySmallBanner,
|
||||
LinkedDocDeletedIcon,
|
||||
} from './styles.js';
|
||||
|
||||
type EmbedCardImages = {
|
||||
LoadingIcon: TemplateResult<1>;
|
||||
ReloadIcon: TemplateResult<1>;
|
||||
LinkedDocIcon: TemplateResult<1>;
|
||||
LinkedDocDeletedIcon: TemplateResult<1>;
|
||||
LinkedDocEmptyBanner: TemplateResult<1>;
|
||||
LinkedDocDeletedBanner: TemplateResult<1>;
|
||||
SyncedDocErrorBanner: TemplateResult<1>;
|
||||
};
|
||||
|
||||
export function getEmbedLinkedDocIcons(
|
||||
theme: ColorScheme,
|
||||
editorMode: 'page' | 'edgeless',
|
||||
style: (typeof EmbedLinkedDocStyles)[number]
|
||||
): EmbedCardImages {
|
||||
const small = style !== 'vertical';
|
||||
const LoadingIcon = getLoadingIconWith(theme);
|
||||
if (editorMode === 'page') {
|
||||
if (theme === ColorScheme.Light) {
|
||||
return {
|
||||
LoadingIcon,
|
||||
ReloadIcon,
|
||||
LinkedDocIcon: EmbedPageIcon,
|
||||
LinkedDocDeletedIcon,
|
||||
LinkedDocEmptyBanner: small
|
||||
? LightLinkedPageEmptySmallBanner
|
||||
: LightLinkedPageEmptyLargeBanner,
|
||||
LinkedDocDeletedBanner: small
|
||||
? LightLinkedPageDeletedSmallBanner
|
||||
: LightLinkedPageDeletedLargeBanner,
|
||||
SyncedDocErrorBanner: LightSyncedDocErrorBanner,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
ReloadIcon,
|
||||
LoadingIcon,
|
||||
LinkedDocIcon: EmbedPageIcon,
|
||||
LinkedDocDeletedIcon,
|
||||
LinkedDocEmptyBanner: small
|
||||
? DarkLinkedPageEmptySmallBanner
|
||||
: DarkLinkedPageEmptyLargeBanner,
|
||||
LinkedDocDeletedBanner: small
|
||||
? DarkLinkedPageDeletedSmallBanner
|
||||
: DarkLinkedPageDeletedLargeBanner,
|
||||
SyncedDocErrorBanner: DarkSyncedDocErrorBanner,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
if (theme === ColorScheme.Light) {
|
||||
return {
|
||||
ReloadIcon,
|
||||
LoadingIcon,
|
||||
LinkedDocIcon: EmbedEdgelessIcon,
|
||||
LinkedDocDeletedIcon,
|
||||
LinkedDocEmptyBanner: small
|
||||
? LightLinkedEdgelessEmptySmallBanner
|
||||
: LightLinkedEdgelessEmptyLargeBanner,
|
||||
LinkedDocDeletedBanner: small
|
||||
? LightLinkedEdgelessDeletedSmallBanner
|
||||
: LightLinkedEdgelessDeletedLargeBanner,
|
||||
SyncedDocErrorBanner: LightSyncedDocErrorBanner,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
ReloadIcon,
|
||||
LoadingIcon,
|
||||
LinkedDocIcon: EmbedEdgelessIcon,
|
||||
LinkedDocDeletedIcon,
|
||||
LinkedDocEmptyBanner: small
|
||||
? DarkLinkedEdgelessEmptySmallBanner
|
||||
: DarkLinkedEdgelessEmptyLargeBanner,
|
||||
LinkedDocDeletedBanner: small
|
||||
? DarkLinkedEdgelessDeletedSmallBanner
|
||||
: DarkLinkedEdgelessDeletedLargeBanner,
|
||||
SyncedDocErrorBanner: DarkSyncedDocErrorBanner,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user