snapshot of https://github.com/toeverything/AFFiNE.git (canary, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Build & Test / Analyze (javascript, blocksuite) (push) Failing after 3s
Build & Test / Rust test filter (push) Failing after 3s
Build & Test / E2E BlockSuite Test (2) (push) Failing after 3s
Build & Test / E2E BlockSuite Test (3) (push) Failing after 4s
Build & Test / E2E BlockSuite Test (4) (push) Failing after 3s
Build & Test / E2E BlockSuite Cross Browser Test (push) Failing after 3s
Build & Test / E2E Mobile Test (1) (push) Failing after 3s
Build & Test / E2E Mobile Test (2) (push) Failing after 3s
Build & Test / Build AFFiNE native (x86_64-unknown-linux-gnu) (push) Failing after 4s
Build & Test / Unit Test (1) (push) Has been skipped
Build & Test / Unit Test (2) (push) Has been skipped
Build & Test / Unit Test (3) (push) Has been skipped
Build & Test / Build Server native (push) Failing after 3s
Build & Test / Check Git Status (push) Has been skipped
Build & Test / Server Test (0, 4) (push) Has been skipped
Build & Test / Server Test (1, 4) (push) Has been skipped
Build & Test / Analyze (javascript, affine) (push) Failing after 3s
Build & Test / Analyze (typescript, affine) (push) Failing after 3s
Build & Test / Analyze (typescript, blocksuite) (push) Failing after 3s
Build & Test / Mobile native build filter (push) Failing after 3s
Build & Test / Check yarn binary (push) Failing after 3s
Build & Test / E2E BlockSuite Test (1) (push) Failing after 4s
Build & Test / E2E BlockSuite Test (5) (push) Failing after 3s
Build & Test / Unit Test (4) (push) Has been skipped
Build & Test / Unit Test (5) (push) Has been skipped
Build & Test / Native Unit Test (push) Has been skipped
Build & Test / Server Test (2, 4) (push) Has been skipped
Build & Test / Server Test (3, 4) (push) Has been skipped
Build & Test / Server Test with Elasticsearch (push) Has been skipped
Build & Test / Server Test with Embedded Search (push) Has been skipped
Build & Test / Server Test with Manticore Search (push) Has been skipped
Build & Test / Server E2E Test (push) Has been skipped
Build & Test / Cloud Desktop E2E Test (push) Has been skipped
Build & Test / Cloud E2E Test 1/2 (push) Has been skipped
Build & Test / Cloud E2E Test 2/2 (push) Has been skipped
Build & Test / Build @affine/electron renderer (push) Failing after 3s
Build & Test / Copilot test filter (push) Failing after 4s
Build & Test / Lint Rust (push) Has been skipped
Build & Test / Build Android app (push) Has been skipped
Build & Test / Test Android app (push) Has been skipped
Build & Test / Run native tests (push) Has been skipped
Build & Test / Server Copilot Api Test (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (3, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (4, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (1, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (2, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (5, 5) (push) Has been skipped
Build & Test / Desktop Test (ubuntu-latest, linux, x64, x86_64-unknown-linux-gnu, true) (push) Has been cancelled
Build & Test / Desktop Test (windows-latest, windows, x64, x86_64-pc-windows-msvc, true) (push) Has been cancelled
Build & Test / 3, 2, 1 Launch (push) Has been cancelled
Build & Test / Lint (push) Has been cancelled
Build & Test / Typecheck (push) Has been cancelled
Build & Test / Build iOS app (push) Has been cancelled
Build & Test / Build AFFiNE native (aarch64-apple-darwin) (push) Has been cancelled
Build & Test / E2E Test (1) (push) Has been cancelled
Build & Test / E2E Test (2) (push) Has been cancelled
Build & Test / E2E Test (3) (push) Has been cancelled
Build & Test / E2E Test (4) (push) Has been cancelled
Build & Test / E2E Test (5) (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-apple-darwin) (push) Has been cancelled
Build & Test / Build AFFiNE native (aarch64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, arm64, aarch64-apple-darwin, true) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, x64, x86_64-apple-darwin, false) (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-16 00:31:07 +05:00
commit 50f788e799
7907 changed files with 988785 additions and 0 deletions

View File

@@ -0,0 +1,65 @@
import { DefaultTool } from '@blocksuite/affine-block-surface';
import { toggleEmbedCardCreateModal } from '@blocksuite/affine-components/embed-card-modal';
import { BookmarkBlockSchema } from '@blocksuite/affine-model';
import {
type SlashMenuConfig,
SlashMenuConfigIdentifier,
} from '@blocksuite/affine-widget-slash-menu';
import { LinkIcon } from '@blocksuite/icons/lit';
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
import type { ExtensionType } from '@blocksuite/store';
import { LinkTooltip } from './tooltips';
const bookmarkSlashMenuConfig: SlashMenuConfig = {
items: [
{
name: 'Link',
description: 'Add a bookmark for reference.',
icon: LinkIcon(),
tooltip: {
figure: LinkTooltip,
caption: 'Link',
},
group: '4_Content & Media@2',
when: ({ model }) =>
model.store.schema.flavourSchemaMap.has('affine:bookmark'),
action: ({ std, model }) => {
const { host } = std;
const parentModel = host.store.getParent(model);
if (!parentModel) {
return;
}
const index = parentModel.children.indexOf(model) + 1;
toggleEmbedCardCreateModal(
host,
'Links',
'The added link will be displayed as a card view.',
{ mode: 'page', parentModel, index },
({ mode }) => {
if (mode === 'edgeless') {
const gfx = std.get(GfxControllerIdentifier);
gfx.tool.setTool(DefaultTool);
}
}
)
.then(() => {
if (model.text?.length === 0) {
model.store.deleteBlock(model);
}
})
.catch(console.error);
},
},
],
};
export const BookmarkSlashMenuConfigIdentifier = SlashMenuConfigIdentifier(
BookmarkBlockSchema.model.flavour
);
export const BookmarkSlashMenuConfigExtension: ExtensionType = {
setup: di => {
di.addImpl(BookmarkSlashMenuConfigIdentifier, bookmarkSlashMenuConfig);
},
};

View File

@@ -0,0 +1,620 @@
import {
canEmbedAsEmbedBlock,
canEmbedAsIframe,
EMBED_IFRAME_DEFAULT_HEIGHT_IN_SURFACE,
EMBED_IFRAME_DEFAULT_WIDTH_IN_SURFACE,
} from '@blocksuite/affine-block-embed';
import { reassociateConnectorsCommand } from '@blocksuite/affine-block-surface';
import { toast } from '@blocksuite/affine-components/toast';
import {
BookmarkBlockModel,
BookmarkStyles,
type EmbedCardStyle,
} from '@blocksuite/affine-model';
import {
EMBED_CARD_HEIGHT,
EMBED_CARD_WIDTH,
} from '@blocksuite/affine-shared/consts';
import {
ActionPlacement,
EmbedIframeService,
EmbedOptionProvider,
type LinkEventType,
type ToolbarAction,
type ToolbarActionGroup,
type ToolbarContext,
type ToolbarModuleConfig,
ToolbarModuleExtension,
} from '@blocksuite/affine-shared/services';
import { getBlockProps } from '@blocksuite/affine-shared/utils';
import { Bound } from '@blocksuite/global/gfx';
import {
CaptionIcon,
CopyIcon,
DeleteIcon,
DuplicateIcon,
ResetIcon,
} from '@blocksuite/icons/lit';
import { BlockFlavourIdentifier, BlockSelection } from '@blocksuite/std';
import { type ExtensionType, Slice, Text } from '@blocksuite/store';
import { computed, signal } from '@preact/signals-core';
import { html } from 'lit';
import { keyed } from 'lit/directives/keyed.js';
import * as Y from 'yjs';
import { BookmarkBlockComponent } from '../bookmark-block';
const trackBaseProps = {
category: 'bookmark',
type: 'card view',
};
const previewAction = {
id: 'a.preview',
content(ctx) {
const model = ctx.getCurrentModelByType(BookmarkBlockModel);
if (!model) return null;
const { url } = model.props;
return html`<affine-link-preview .url=${url}></affine-link-preview>`;
},
} satisfies ToolbarAction;
const captionAction = {
id: 'd.caption',
tooltip: 'Caption',
icon: CaptionIcon(),
run(ctx) {
const block = ctx.getCurrentBlockByType(BookmarkBlockComponent);
block?.captionEditor?.show();
ctx.track('OpenedCaptionEditor', {
...trackBaseProps,
control: 'add caption',
});
},
} satisfies ToolbarAction;
const createOnToggleFn =
(
ctx: ToolbarContext,
name: Extract<
LinkEventType,
| 'OpenedViewSelector'
| 'OpenedCardStyleSelector'
| 'OpenedCardScaleSelector'
>,
control: string
) =>
(e: CustomEvent<boolean>) => {
e.stopPropagation();
const opened = e.detail;
if (!opened) return;
ctx.track(name, {
...trackBaseProps,
control,
});
};
const builtinToolbarConfig = {
actions: [
previewAction,
{
id: 'b.conversions',
actions: [
{
id: 'inline',
label: 'Inline view',
run(ctx) {
const model = ctx.getCurrentModelByType(BookmarkBlockModel);
if (!model) return;
const { title, caption, url } = model.props;
const { parent } = model;
const index = parent?.children.indexOf(model);
const yText = new Y.Text();
const insert = title || caption || url;
yText.insert(0, insert);
yText.format(0, insert.length, { link: url });
const text = new Text(yText);
ctx.store.addBlock('affine:paragraph', { text }, parent, index);
ctx.store.deleteBlock(model);
// Clears
ctx.reset();
ctx.select('note');
ctx.track('SelectedView', {
...trackBaseProps,
control: 'select view',
type: 'inline view',
});
},
},
{
id: 'card',
label: 'Card view',
disabled: true,
},
{
id: 'embed',
label: 'Embed view',
disabled(ctx) {
const model = ctx.getCurrentModelByType(BookmarkBlockModel);
if (!model) return true;
const url = model.props.url;
return (
!canEmbedAsIframe(ctx.std, url) &&
!canEmbedAsEmbedBlock(ctx.std, url)
);
},
run(ctx) {
const model = ctx.getCurrentModelByType(BookmarkBlockModel);
if (!model) return;
const { caption, url, title, description, style } = model.props;
const { parent } = model;
const index = parent?.children.indexOf(model);
if (!parent) return;
let blockId: string | undefined;
// first try to embed as a custom embed block
if (canEmbedAsEmbedBlock(ctx.std, url)) {
const options = ctx.std
.get(EmbedOptionProvider)
.getEmbedBlockOptions(url);
if (!options) return;
const { flavour, styles } = options;
const newStyle = styles.includes(style)
? style
: styles.find(s => s !== 'vertical' && s !== 'cube');
blockId = ctx.store.addBlock(
flavour,
{
url,
caption,
title,
description,
style: newStyle,
},
parent,
index
);
} else if (canEmbedAsIframe(ctx.std, url)) {
const embedIframeService = ctx.std.get(EmbedIframeService);
blockId = embedIframeService.addEmbedIframeBlock(
{ url, caption, title, description },
parent.id,
index
);
}
if (!blockId) return;
ctx.store.deleteBlock(model);
// Selects new block
ctx.select('note', [
ctx.selection.create(BlockSelection, { blockId }),
]);
ctx.track('SelectedView', {
...trackBaseProps,
control: 'select view',
type: 'embed view',
});
},
},
],
content(ctx) {
const model = ctx.getCurrentModelByType(BookmarkBlockModel);
if (!model) return null;
const actions = this.actions.map(action => ({ ...action }));
const viewType$ = signal(actions[1].label);
const onToggle = createOnToggleFn(
ctx,
'OpenedViewSelector',
'switch view'
);
return html`${keyed(
model,
html`<affine-view-dropdown-menu
@toggle=${onToggle}
.actions=${actions}
.context=${ctx}
.viewTypeSignal=${viewType$}
></affine-view-dropdown-menu>`
)}`;
},
} satisfies ToolbarActionGroup<ToolbarAction>,
{
id: 'c.style',
actions: [
{
id: 'horizontal',
label: 'Large horizontal style',
},
{
id: 'list',
label: 'Small horizontal style',
},
],
content(ctx) {
const model = ctx.getCurrentModelByType(BookmarkBlockModel);
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}
.styleSignal=${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(BookmarkBlockModel);
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(BookmarkBlockModel);
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: 'b.refresh',
label: 'Reload',
icon: ResetIcon(),
run(ctx) {
const block = ctx.getCurrentBlockByType(BookmarkBlockComponent);
block?.refreshData();
},
},
{
placement: ActionPlacement.More,
id: 'c.delete',
label: 'Delete',
icon: DeleteIcon(),
variant: 'destructive',
run(ctx) {
const model = ctx.getCurrentModelByType(BookmarkBlockModel);
if (!model) return;
ctx.store.deleteBlock(model);
// Clears
ctx.select('note');
ctx.reset();
},
},
],
} as const satisfies ToolbarModuleConfig;
const builtinSurfaceToolbarConfig = {
actions: [
previewAction,
{
id: 'b.conversions',
actions: [
{
id: 'card',
label: 'Card view',
disabled: true,
},
{
id: 'embed',
label: 'Embed view',
run(ctx) {
const model = ctx.getCurrentModelByType(BookmarkBlockModel);
if (!model) return;
const { id: oldId, xywh, parent } = model;
const { url, caption, title, description } = model.props;
let newId: string | undefined;
// first try to embed as a custom embed block
if (canEmbedAsEmbedBlock(ctx.std, url)) {
const options = ctx.std
.get(EmbedOptionProvider)
.getEmbedBlockOptions(url);
if (options?.viewType !== 'embed') return;
const { flavour, styles } = options;
let style: EmbedCardStyle = model.props.style;
if (!styles.includes(style)) {
style = styles[0];
}
const bound = Bound.deserialize(xywh);
bound.w = EMBED_CARD_WIDTH[style];
bound.h = EMBED_CARD_HEIGHT[style];
newId = ctx.store.addBlock(
flavour,
{
url,
caption,
title,
description,
style,
xywh: bound.serialize(),
},
parent
);
} else if (canEmbedAsIframe(ctx.std, url)) {
const embedIframeService = ctx.std.get(EmbedIframeService);
const config = embedIframeService.getConfig(url);
if (!config) {
return;
}
const bound = Bound.deserialize(xywh);
const options = config.options;
const { widthInSurface, heightInSurface } = options ?? {};
bound.w = widthInSurface ?? EMBED_IFRAME_DEFAULT_WIDTH_IN_SURFACE;
bound.h =
heightInSurface ?? EMBED_IFRAME_DEFAULT_HEIGHT_IN_SURFACE;
newId = ctx.store.addBlock(
'affine:embed-iframe',
{ url, caption, title, description, xywh: bound.serialize() },
parent
);
}
if (!newId) return;
ctx.command.exec(reassociateConnectorsCommand, { oldId, newId });
ctx.store.deleteBlock(model);
// Selects new block
ctx.gfx.selection.set({ editing: false, elements: [newId] });
ctx.track('SelectedView', {
...trackBaseProps,
control: 'select view',
type: 'embed view',
});
},
},
],
when(ctx) {
const model = ctx.getCurrentModelByType(BookmarkBlockModel);
if (!model) return false;
const { url } = model.props;
return (
canEmbedAsIframe(ctx.std, url) || canEmbedAsEmbedBlock(ctx.std, url)
);
},
content(ctx) {
const model = ctx.getCurrentModelByType(BookmarkBlockModel);
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}
.viewTypeSignal=${viewType$}
></affine-view-dropdown-menu>`
)}`;
},
} satisfies ToolbarActionGroup<ToolbarAction>,
{
id: 'b.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',
},
] as const
).filter(action => BookmarkStyles.includes(action.id)),
content(ctx) {
const model = ctx.getCurrentModelByType(BookmarkBlockModel);
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}
.styleSignal=${style$}
></affine-card-style-dropdown-menu>`
)}`;
},
} satisfies ToolbarActionGroup<ToolbarAction>,
{
...captionAction,
id: 'c.caption',
},
{
id: 'd.scale',
content(ctx) {
const model = ctx.getCurrentModelByType(BookmarkBlockModel);
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.w = EMBED_CARD_WIDTH[style] * scale;
bounds.h = EMBED_CARD_HEIGHT[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}
.sizeSignal=${scale$}
></affine-size-dropdown-menu>`
)}`;
},
},
],
when: ctx => ctx.getSurfaceModelsByType(BookmarkBlockModel).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,
}),
];
};

View File

@@ -0,0 +1,13 @@
import { html } from 'lit';
// prettier-ignore
export const LinkTooltip = 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_1007" 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_1007)">
<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.&#10;</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.&#10;</tspan><tspan x="8" y="111.636">For example, one user&#x2019;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.&#10;</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>
<text fill="#1E67AF" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="11" letter-spacing="0em"><tspan x="8" y="45.5">Learn about AFFiNE</tspan></text>
</g>
</svg>
`;