refactor(editor): job should not rely on doc collection directly (#9488)

This commit is contained in:
Saul-Mirone
2025-01-02 10:50:15 +00:00
parent f2906bc6d0
commit edb5e1d87a
34 changed files with 565 additions and 337 deletions
@@ -27,7 +27,7 @@ export const embedSyncedDocBlockHtmlAdapterMatcher: BlockHtmlAdapterMatcher = {
if (type === 'content') {
const syncedDocId = o.node.props.pageId as string;
const syncedDoc = job.collection.getDoc(syncedDocId);
const syncedDoc = job.docCRUD.get(syncedDocId);
walkerContext.setGlobalContext('hast:html-root-doc', false);
if (!syncedDoc) return;
@@ -29,7 +29,7 @@ export const embedSyncedDocBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatc
if (type === 'content') {
const syncedDocId = o.node.props.pageId as string;
const syncedDoc = job.collection.getDoc(syncedDocId);
const syncedDoc = job.docCRUD.get(syncedDocId);
if (!syncedDoc) return;
if (counter === 1) {
@@ -31,7 +31,7 @@ export const embedSyncedDocBlockPlainTextAdapterMatcher: BlockPlainTextAdapterMa
if (type === 'content') {
const syncedDocId = o.node.props.pageId as string;
const syncedDoc = job.collection.getDoc(syncedDocId);
const syncedDoc = job.docCRUD.get(syncedDocId);
if (!syncedDoc) return;
if (counter === 1) {
@@ -4,7 +4,7 @@ import {
StdIdentifier,
} from '@blocksuite/block-std';
import { type Container, createIdentifier } from '@blocksuite/global/di';
import { Job, Slice, type SliceSnapshot } from '@blocksuite/store';
import { Slice, type SliceSnapshot } from '@blocksuite/store';
export const DndApiExtensionIdentifier = createIdentifier<DNDAPIExtension>(
'AffineDndApiIdentifier'
@@ -40,7 +40,7 @@ export class DNDAPIExtension extends Extension {
const { docId, flavour = 'affine:embed-linked-doc', blockId } = options;
const slice = Slice.fromModels(this.std.doc, []);
const job = new Job({ collection: this.std.collection });
const job = this.std.getJob();
const snapshot = job.sliceToSnapshot(slice);
if (!snapshot) {
console.error('Failed to convert slice to snapshot');
@@ -3,14 +3,14 @@ import type { JobMiddleware } from '@blocksuite/store';
export const newIdCrossDoc =
(std: BlockStdScope): JobMiddleware =>
({ slots, collection }) => {
({ slots }) => {
let samePage = false;
slots.beforeImport.on(payload => {
if (payload.type === 'slice') {
samePage = payload.snapshot.pageId === std.doc.id;
}
if (payload.type === 'block' && !samePage) {
payload.snapshot.id = collection.idGenerator();
payload.snapshot.id = std.collection.idGenerator();
}
});
};
@@ -3,7 +3,7 @@ import type { JobMiddleware } from '@blocksuite/store';
export const surfaceRefToEmbed =
(std: BlockStdScope): JobMiddleware =>
({ slots, collection }) => {
({ slots }) => {
let pageId: string | null = null;
slots.beforeImport.on(payload => {
if (payload.type === 'slice') {
@@ -18,7 +18,7 @@ export const surfaceRefToEmbed =
!std.doc.hasBlock(payload.snapshot.id)
) {
const id = payload.snapshot.id;
payload.snapshot.id = collection.idGenerator();
payload.snapshot.id = std.collection.idGenerator();
payload.snapshot.flavour = 'affine:embed-linked-doc';
payload.snapshot.props = {
blockId: id,
@@ -30,7 +30,7 @@ import {
} from '@blocksuite/block-std';
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
import { Bound, Point } from '@blocksuite/global/utils';
import { Job, Slice, type SliceSnapshot } from '@blocksuite/store';
import { Slice, type SliceSnapshot } from '@blocksuite/store';
import { DropIndicator } from '../components/drop-indicator.js';
import { AFFINE_DRAG_HANDLE_WIDGET } from '../consts.js';
@@ -544,14 +544,11 @@ export class DragEventWatcher {
private _getJob() {
const std = this._std;
return new Job({
collection: std.collection,
middlewares: [
newIdCrossDoc(std),
reorderList(std),
surfaceRefToEmbed(std),
],
});
return std.getJob([
newIdCrossDoc(std),
reorderList(std),
surfaceRefToEmbed(std),
]);
}
private _serializeData(slice: Slice, state: DndEventState) {
@@ -27,5 +27,14 @@ export function createJob(middlewares?: JobMiddleware[]) {
const schema = new Schema().register(AffineSchemas);
const docCollection = new DocCollection({ schema });
docCollection.meta.initialize();
return new Job({ collection: docCollection, middlewares: testMiddlewares });
return new Job({
schema,
blobCRUD: docCollection.blobSync,
middlewares: testMiddlewares,
docCRUD: {
create: (id: string) => docCollection.createDoc({ id }),
get: (id: string) => docCollection.getDoc(id),
delete: (id: string) => docCollection.removeDoc(id),
},
});
}
@@ -45,8 +45,17 @@ const provider = container.provider();
*/
async function exportDoc(doc: Doc) {
const job = new Job({
collection: doc.collection,
middlewares: [docLinkBaseURLMiddleware, titleMiddleware],
schema: doc.schema,
blobCRUD: doc.blobSync,
docCRUD: {
create: (id: string) => doc.collection.createDoc({ id }),
get: (id: string) => doc.collection.getDoc(id),
delete: (id: string) => doc.collection.removeDoc(id),
},
middlewares: [
docLinkBaseURLMiddleware(doc.collection.id),
titleMiddleware(doc.collection.meta.docMetas),
],
});
const snapshot = job.docToSnapshot(doc);
const adapter = new HtmlAdapter(job, provider);
@@ -91,11 +100,17 @@ async function importHTMLToDoc({
fileName,
}: ImportHTMLToDocOptions) {
const job = new Job({
collection,
schema: collection.schema,
blobCRUD: collection.blobSync,
docCRUD: {
create: (id: string) => collection.createDoc({ id }),
get: (id: string) => collection.getDoc(id),
delete: (id: string) => collection.removeDoc(id),
},
middlewares: [
defaultImageProxyMiddleware,
fileNameMiddleware(fileName),
docLinkBaseURLMiddleware,
docLinkBaseURLMiddleware(collection.id),
],
});
const htmlAdapter = new HtmlAdapter(job, provider);
@@ -147,11 +162,17 @@ async function importHTMLZip({ collection, imported }: ImportHTMLZipOptions) {
htmlBlobs.map(async ([fileName, blob]) => {
const fileNameWithoutExt = fileName.replace(/\.[^/.]+$/, '');
const job = new Job({
collection,
schema: collection.schema,
blobCRUD: collection.blobSync,
docCRUD: {
create: (id: string) => collection.createDoc({ id }),
get: (id: string) => collection.getDoc(id),
delete: (id: string) => collection.removeDoc(id),
},
middlewares: [
defaultImageProxyMiddleware,
fileNameMiddleware(fileNameWithoutExt),
docLinkBaseURLMiddleware,
docLinkBaseURLMiddleware(collection.id),
],
});
const assets = job.assets;
@@ -51,8 +51,17 @@ type ImportMarkdownZipOptions = {
*/
async function exportDoc(doc: Doc) {
const job = new Job({
collection: doc.collection,
middlewares: [docLinkBaseURLMiddleware, titleMiddleware],
schema: doc.schema,
blobCRUD: doc.blobSync,
docCRUD: {
create: (id: string) => doc.collection.createDoc({ id }),
get: (id: string) => doc.collection.getDoc(id),
delete: (id: string) => doc.collection.removeDoc(id),
},
middlewares: [
docLinkBaseURLMiddleware(doc.collection.id),
titleMiddleware(doc.collection.meta.docMetas),
],
});
const snapshot = job.docToSnapshot(doc);
@@ -101,8 +110,17 @@ async function importMarkdownToBlock({
blockId,
}: ImportMarkdownToBlockOptions) {
const job = new Job({
collection: doc.collection,
middlewares: [defaultImageProxyMiddleware, docLinkBaseURLMiddleware],
schema: doc.schema,
blobCRUD: doc.blobSync,
docCRUD: {
create: (id: string) => doc.collection.createDoc({ id }),
get: (id: string) => doc.collection.getDoc(id),
delete: (id: string) => doc.collection.removeDoc(id),
},
middlewares: [
defaultImageProxyMiddleware,
docLinkBaseURLMiddleware(doc.collection.id),
],
});
const adapter = new MarkdownAdapter(job, provider);
const snapshot = await adapter.toSliceSnapshot({
@@ -137,11 +155,17 @@ async function importMarkdownToDoc({
fileName,
}: ImportMarkdownToDocOptions) {
const job = new Job({
collection,
schema: collection.schema,
blobCRUD: collection.blobSync,
docCRUD: {
create: (id: string) => collection.createDoc({ id }),
get: (id: string) => collection.getDoc(id),
delete: (id: string) => collection.removeDoc(id),
},
middlewares: [
defaultImageProxyMiddleware,
fileNameMiddleware(fileName),
docLinkBaseURLMiddleware,
docLinkBaseURLMiddleware(collection.id),
],
});
const mdAdapter = new MarkdownAdapter(job, provider);
@@ -195,11 +219,17 @@ async function importMarkdownZip({
markdownBlobs.map(async ([fileName, blob]) => {
const fileNameWithoutExt = fileName.replace(/\.[^/.]+$/, '');
const job = new Job({
collection,
schema: collection.schema,
blobCRUD: collection.blobSync,
docCRUD: {
create: (id: string) => collection.createDoc({ id }),
get: (id: string) => collection.getDoc(id),
delete: (id: string) => collection.removeDoc(id),
},
middlewares: [
defaultImageProxyMiddleware,
fileNameMiddleware(fileNameWithoutExt),
docLinkBaseURLMiddleware,
docLinkBaseURLMiddleware(collection.id),
],
});
const assets = job.assets;
@@ -8,193 +8,197 @@ import type {
} from '@blocksuite/affine-model';
import { DEFAULT_IMAGE_PROXY_ENDPOINT } from '@blocksuite/affine-shared/consts';
import { assertExists } from '@blocksuite/global/utils';
import type { DeltaOperation, JobMiddleware } from '@blocksuite/store';
import type { DeltaOperation, DocMeta, JobMiddleware } from '@blocksuite/store';
export const replaceIdMiddleware: JobMiddleware = ({ slots, collection }) => {
const idMap = new Map<string, string>();
slots.afterImport.on(payload => {
if (
payload.type === 'block' &&
payload.snapshot.flavour === 'affine:database'
) {
const model = payload.model as DatabaseBlockModel;
Object.keys(model.cells).forEach(cellId => {
if (idMap.has(cellId)) {
model.cells[idMap.get(cellId)!] = model.cells[cellId];
delete model.cells[cellId];
}
});
}
// replace LinkedPage pageId with new id in paragraph blocks
if (
payload.type === 'block' &&
['affine:list', 'affine:paragraph'].includes(payload.snapshot.flavour)
) {
const model = payload.model as ParagraphBlockModel | ListBlockModel;
let prev = 0;
const delta: DeltaOperation[] = [];
for (const d of model.text.toDelta()) {
if (d.attributes?.reference?.pageId) {
const newId = idMap.get(d.attributes.reference.pageId);
if (!newId) {
prev += d.insert?.length ?? 0;
continue;
}
if (prev > 0) {
delta.push({ retain: prev });
}
delta.push({
retain: d.insert?.length ?? 0,
attributes: {
reference: {
...d.attributes.reference,
pageId: newId,
},
},
});
prev = 0;
} else {
prev += d.insert?.length ?? 0;
}
}
if (delta.length > 0) {
model.text.applyDelta(delta);
}
}
if (
payload.type === 'block' &&
payload.snapshot.flavour === 'affine:surface-ref'
) {
const model = payload.model as SurfaceRefBlockModel;
const original = model.reference;
// If there exists a replacement, replace the reference with the new id.
// Otherwise,
// 1. If the reference is an affine:frame not in doc, generate a new id.
// 2. If the reference is graph, keep the original id.
if (idMap.has(original)) {
model.reference = idMap.get(original)!;
} else if (
model.refFlavour === 'affine:frame' &&
!model.doc.hasBlock(original)
export const replaceIdMiddleware =
(idGenerator: () => string): JobMiddleware =>
({ slots, docCRUD }) => {
const idMap = new Map<string, string>();
slots.afterImport.on(payload => {
if (
payload.type === 'block' &&
payload.snapshot.flavour === 'affine:database'
) {
const newId = collection.idGenerator();
idMap.set(original, newId);
model.reference = newId;
const model = payload.model as DatabaseBlockModel;
Object.keys(model.cells).forEach(cellId => {
if (idMap.has(cellId)) {
model.cells[idMap.get(cellId)!] = model.cells[cellId];
delete model.cells[cellId];
}
});
}
}
// TODO(@fundon): process linked block/element
if (
payload.type === 'block' &&
['affine:embed-linked-doc', 'affine:embed-synced-doc'].includes(
payload.snapshot.flavour
)
) {
const model = payload.model as EmbedLinkedDocModel | EmbedSyncedDocModel;
const original = model.pageId;
// If the pageId is not in the doc, generate a new id.
// If we already have a replacement, use it.
if (!collection.getDoc(original)) {
if (idMap.has(original)) {
model.pageId = idMap.get(original)!;
} else {
const newId = collection.idGenerator();
idMap.set(original, newId);
model.pageId = newId;
// replace LinkedPage pageId with new id in paragraph blocks
if (
payload.type === 'block' &&
['affine:list', 'affine:paragraph'].includes(payload.snapshot.flavour)
) {
const model = payload.model as ParagraphBlockModel | ListBlockModel;
let prev = 0;
const delta: DeltaOperation[] = [];
for (const d of model.text.toDelta()) {
if (d.attributes?.reference?.pageId) {
const newId = idMap.get(d.attributes.reference.pageId);
if (!newId) {
prev += d.insert?.length ?? 0;
continue;
}
if (prev > 0) {
delta.push({ retain: prev });
}
delta.push({
retain: d.insert?.length ?? 0,
attributes: {
reference: {
...d.attributes.reference,
pageId: newId,
},
},
});
prev = 0;
} else {
prev += d.insert?.length ?? 0;
}
}
if (delta.length > 0) {
model.text.applyDelta(delta);
}
}
}
});
slots.beforeImport.on(payload => {
if (payload.type === 'page') {
if (idMap.has(payload.snapshot.meta.id)) {
payload.snapshot.meta.id = idMap.get(payload.snapshot.meta.id)!;
if (
payload.type === 'block' &&
payload.snapshot.flavour === 'affine:surface-ref'
) {
const model = payload.model as SurfaceRefBlockModel;
const original = model.reference;
// If there exists a replacement, replace the reference with the new id.
// Otherwise,
// 1. If the reference is an affine:frame not in doc, generate a new id.
// 2. If the reference is graph, keep the original id.
if (idMap.has(original)) {
model.reference = idMap.get(original)!;
} else if (
model.refFlavour === 'affine:frame' &&
!model.doc.hasBlock(original)
) {
const newId = idGenerator();
idMap.set(original, newId);
model.reference = newId;
}
}
// TODO(@fundon): process linked block/element
if (
payload.type === 'block' &&
['affine:embed-linked-doc', 'affine:embed-synced-doc'].includes(
payload.snapshot.flavour
)
) {
const model = payload.model as
| EmbedLinkedDocModel
| EmbedSyncedDocModel;
const original = model.pageId;
// If the pageId is not in the doc, generate a new id.
// If we already have a replacement, use it.
if (!docCRUD.get(original)) {
if (idMap.has(original)) {
model.pageId = idMap.get(original)!;
} else {
const newId = idGenerator();
idMap.set(original, newId);
model.pageId = newId;
}
}
}
});
slots.beforeImport.on(payload => {
if (payload.type === 'page') {
if (idMap.has(payload.snapshot.meta.id)) {
payload.snapshot.meta.id = idMap.get(payload.snapshot.meta.id)!;
return;
}
const newId = idGenerator();
idMap.set(payload.snapshot.meta.id, newId);
payload.snapshot.meta.id = newId;
return;
}
const newId = collection.idGenerator();
idMap.set(payload.snapshot.meta.id, newId);
payload.snapshot.meta.id = newId;
return;
}
if (payload.type === 'block') {
const { snapshot } = payload;
if (snapshot.flavour === 'affine:page') {
const index = snapshot.children.findIndex(
c => c.flavour === 'affine:surface'
);
if (index !== -1) {
const [surface] = snapshot.children.splice(index, 1);
snapshot.children.push(surface);
if (payload.type === 'block') {
const { snapshot } = payload;
if (snapshot.flavour === 'affine:page') {
const index = snapshot.children.findIndex(
c => c.flavour === 'affine:surface'
);
if (index !== -1) {
const [surface] = snapshot.children.splice(index, 1);
snapshot.children.push(surface);
}
}
const original = snapshot.id;
let newId: string;
if (idMap.has(original)) {
newId = idMap.get(original)!;
} else {
newId = idGenerator();
idMap.set(original, newId);
}
snapshot.id = newId;
if (snapshot.flavour === 'affine:surface') {
// Generate new IDs for images and frames in advance.
snapshot.children.forEach(child => {
const original = child.id;
if (idMap.has(original)) {
newId = idMap.get(original)!;
} else {
newId = idGenerator();
idMap.set(original, newId);
}
});
Object.entries(
snapshot.props.elements as Record<string, Record<string, unknown>>
).forEach(([_, value]) => {
switch (value.type) {
case 'connector': {
let connection = value.source as Record<string, string>;
if (idMap.has(connection.id)) {
const newId = idMap.get(connection.id);
assertExists(newId, 'reference id must exist');
connection.id = newId;
}
connection = value.target as Record<string, string>;
if (idMap.has(connection.id)) {
const newId = idMap.get(connection.id);
assertExists(newId, 'reference id must exist');
connection.id = newId;
}
break;
}
case 'group': {
const json = (value.children as Record<string, unknown>)
.json as Record<string, unknown>;
Object.entries(json).forEach(([key, value]) => {
if (idMap.has(key)) {
delete json[key];
const newKey = idMap.get(key);
assertExists(newKey, 'reference id must exist');
json[newKey] = value;
}
});
break;
}
default:
break;
}
});
}
}
const original = snapshot.id;
let newId: string;
if (idMap.has(original)) {
newId = idMap.get(original)!;
} else {
newId = collection.idGenerator();
idMap.set(original, newId);
}
snapshot.id = newId;
if (snapshot.flavour === 'affine:surface') {
// Generate new IDs for images and frames in advance.
snapshot.children.forEach(child => {
const original = child.id;
if (idMap.has(original)) {
newId = idMap.get(original)!;
} else {
newId = collection.idGenerator();
idMap.set(original, newId);
}
});
Object.entries(
snapshot.props.elements as Record<string, Record<string, unknown>>
).forEach(([_, value]) => {
switch (value.type) {
case 'connector': {
let connection = value.source as Record<string, string>;
if (idMap.has(connection.id)) {
const newId = idMap.get(connection.id);
assertExists(newId, 'reference id must exist');
connection.id = newId;
}
connection = value.target as Record<string, string>;
if (idMap.has(connection.id)) {
const newId = idMap.get(connection.id);
assertExists(newId, 'reference id must exist');
connection.id = newId;
}
break;
}
case 'group': {
const json = (value.children as Record<string, unknown>)
.json as Record<string, unknown>;
Object.entries(json).forEach(([key, value]) => {
if (idMap.has(key)) {
delete json[key];
const newKey = idMap.get(key);
assertExists(newKey, 'reference id must exist');
json[newKey] = value;
}
});
break;
}
default:
break;
}
});
}
}
});
};
});
};
export const customImageProxyMiddleware = (
imageProxyURL: string
@@ -204,46 +208,52 @@ export const customImageProxyMiddleware = (
};
};
const customDocLinkBaseUrlMiddleware = (baseUrl: string): JobMiddleware => {
return ({ adapterConfigs, collection }) => {
const customDocLinkBaseUrlMiddleware = (
baseUrl: string,
collectionId: string
): JobMiddleware => {
return ({ adapterConfigs }) => {
const docLinkBaseUrl = baseUrl
? `${baseUrl}/workspace/${collection.id}`
? `${baseUrl}/workspace/${collectionId}`
: '';
adapterConfigs.set('docLinkBaseUrl', docLinkBaseUrl);
};
};
export const titleMiddleware: JobMiddleware = ({
slots,
collection,
adapterConfigs,
}) => {
slots.beforeExport.on(() => {
for (const meta of collection.meta.docMetas) {
adapterConfigs.set('title:' + meta.id, meta.title);
}
});
};
export const titleMiddleware =
(metas: DocMeta[]): JobMiddleware =>
({ slots, adapterConfigs }) => {
slots.beforeExport.on(() => {
for (const meta of metas) {
adapterConfigs.set('title:' + meta.id, meta.title);
}
});
};
export const docLinkBaseURLMiddlewareBuilder = (baseUrl: string) => {
let middleware = customDocLinkBaseUrlMiddleware(baseUrl);
export const docLinkBaseURLMiddlewareBuilder = (
baseUrl: string,
collectionId: string
) => {
let middleware = customDocLinkBaseUrlMiddleware(baseUrl, collectionId);
return {
get: () => middleware,
set: (url: string) => {
middleware = customDocLinkBaseUrlMiddleware(url);
middleware = customDocLinkBaseUrlMiddleware(url, collectionId);
},
};
};
const defaultDocLinkBaseURLMiddlewareBuilder = docLinkBaseURLMiddlewareBuilder(
typeof window !== 'undefined' ? window.location.origin : '.'
);
const defaultDocLinkBaseURLMiddlewareBuilder = (collectionId: string) =>
docLinkBaseURLMiddlewareBuilder(
typeof window !== 'undefined' ? window.location.origin : '.',
collectionId
);
export const docLinkBaseURLMiddleware =
defaultDocLinkBaseURLMiddlewareBuilder.get();
export const docLinkBaseURLMiddleware = (collectionId: string) =>
defaultDocLinkBaseURLMiddlewareBuilder(collectionId).get();
export const setDocLinkBaseURLMiddleware =
defaultDocLinkBaseURLMiddlewareBuilder.set;
export const setDocLinkBaseURLMiddleware = (collectionId: string) =>
defaultDocLinkBaseURLMiddlewareBuilder(collectionId).set;
const imageProxyMiddlewareBuilder = () => {
let middleware = customImageProxyMiddleware(DEFAULT_IMAGE_PROXY_ENDPOINT);
@@ -119,7 +119,13 @@ async function importNotionZip({
}
const pagePromises = Array.from(pagePaths).map(async path => {
const job = new Job({
collection: collection,
schema: collection.schema,
blobCRUD: collection.blobSync,
docCRUD: {
create: (id: string) => collection.createDoc({ id }),
get: (id: string) => collection.getDoc(id),
delete: (id: string) => collection.removeDoc(id),
},
middlewares: [defaultImageProxyMiddleware],
});
const htmlAdapter = new NotionHtmlAdapter(job, provider);
@@ -7,12 +7,21 @@ import { replaceIdMiddleware, titleMiddleware } from './middlewares.js';
async function exportDocs(collection: DocCollection, docs: Doc[]) {
const zip = new Zip();
const job = new Job({ collection });
const job = new Job({
schema: collection.schema,
blobCRUD: collection.blobSync,
docCRUD: {
create: (id: string) => collection.createDoc({ id }),
get: (id: string) => collection.getDoc(id),
delete: (id: string) => collection.removeDoc(id),
},
middlewares: [
replaceIdMiddleware(collection.idGenerator),
titleMiddleware(collection.meta.docMetas),
],
});
const snapshots = await Promise.all(docs.map(job.docToSnapshot));
const collectionInfo = job.collectionInfoToSnapshot();
await zip.file('info.json', JSON.stringify(collectionInfo, null, 2));
await Promise.all(
snapshots
.filter((snapshot): snapshot is DocSnapshot => !!snapshot)
@@ -69,8 +78,17 @@ async function importDocs(collection: DocCollection, imported: Blob) {
}
const job = new Job({
collection,
middlewares: [replaceIdMiddleware, titleMiddleware],
schema: collection.schema,
blobCRUD: collection.blobSync,
docCRUD: {
create: (id: string) => collection.createDoc({ id }),
get: (id: string) => collection.getDoc(id),
delete: (id: string) => collection.removeDoc(id),
},
middlewares: [
replaceIdMiddleware(collection.idGenerator),
titleMiddleware(collection.meta.docMetas),
],
});
const assetsMap = job.assets;
@@ -59,8 +59,12 @@ export class PageClipboard {
const paste = pasteMiddleware(this._std);
this._std.clipboard.use(copy);
this._std.clipboard.use(paste);
this._std.clipboard.use(replaceIdMiddleware);
this._std.clipboard.use(titleMiddleware);
this._std.clipboard.use(
replaceIdMiddleware(this._std.doc.collection.idGenerator)
);
this._std.clipboard.use(
titleMiddleware(this._std.doc.collection.meta.docMetas)
);
this._std.clipboard.use(defaultImageProxyMiddleware);
this._disposables.add({
@@ -80,8 +84,12 @@ export class PageClipboard {
this._std.clipboard.unregisterAdapter('*/*');
this._std.clipboard.unuse(copy);
this._std.clipboard.unuse(paste);
this._std.clipboard.unuse(replaceIdMiddleware);
this._std.clipboard.unuse(titleMiddleware);
this._std.clipboard.unuse(
replaceIdMiddleware(this._std.doc.collection.idGenerator)
);
this._std.clipboard.unuse(
titleMiddleware(this._std.doc.collection.meta.docMetas)
);
this._std.clipboard.unuse(defaultImageProxyMiddleware);
},
});
@@ -369,7 +369,15 @@ export class EdgelessClipboardController extends PageClipboard {
if (mayBeSurfaceDataJson !== undefined) {
const elementsRawData = JSON.parse(mayBeSurfaceDataJson);
const { snapshot, blobs } = elementsRawData;
const job = new Job({ collection: this.std.collection });
const job = new Job({
schema: this.std.collection.schema,
blobCRUD: this.std.collection.blobSync,
docCRUD: {
create: (id: string) => this.std.collection.createDoc({ id }),
get: (id: string) => this.std.collection.getDoc(id),
delete: (id: string) => this.std.collection.removeDoc(id),
},
});
const map = job.assetsManager.getAssets();
decodeClipboardBlobs(blobs, map);
for (const blobId of map.keys()) {
@@ -1366,7 +1374,13 @@ export async function prepareClipboardData(
std: BlockStdScope
) {
const job = new Job({
collection: std.collection,
schema: std.collection.schema,
blobCRUD: std.collection.blobSync,
docCRUD: {
create: (id: string) => std.collection.createDoc({ id }),
get: (id: string) => std.collection.getDoc(id),
delete: (id: string) => std.collection.removeDoc(id),
},
});
const selected = await Promise.all(
selectedAll.map(async selected => {
@@ -87,7 +87,16 @@ export class TemplateJob {
type: TemplateType;
constructor({ model, type, middlewares }: TemplateJobConfig) {
this.job = new Job({ collection: model.doc.collection, middlewares: [] });
this.job = new Job({
schema: model.doc.collection.schema,
blobCRUD: model.doc.collection.blobSync,
docCRUD: {
create: (id: string) => model.doc.collection.createDoc({ id }),
get: (id: string) => model.doc.collection.getDoc(id),
delete: (id: string) => model.doc.collection.removeDoc(id),
},
middlewares: [],
});
this.model = model;
this.type = TEMPLATE_TYPES.includes(type as TemplateType)
? (type as TemplateType)
@@ -40,7 +40,13 @@ export function getSortedCloneElements(elements: GfxModel[]) {
export function prepareCloneData(elements: GfxModel[], std: BlockStdScope) {
elements = sortEdgelessElements(elements);
const job = new Job({
collection: std.collection,
schema: std.collection.schema,
blobCRUD: std.collection.blobSync,
docCRUD: {
create: (id: string) => std.collection.createDoc({ id }),
get: (id: string) => std.collection.getDoc(id),
delete: (id: string) => std.collection.removeDoc(id),
},
});
const res = elements.map(element => {
const data = serializeElement(element, elements, job);
@@ -247,7 +247,15 @@ export const markdownToMindmap = (
provider: ServiceProvider
) => {
let result: Node | null = null;
const job = new Job({ collection: doc.collection });
const job = new Job({
schema: doc.collection.schema,
blobCRUD: doc.collection.blobSync,
docCRUD: {
create: (id: string) => doc.collection.createDoc({ id }),
get: (id: string) => doc.collection.getDoc(id),
delete: (id: string) => doc.collection.removeDoc(id),
},
});
const markdown = new MarkdownAdapter(job, provider);
const ast: Root = markdown['_markdownToAst'](answer);
const traverse = (
@@ -4,10 +4,10 @@ import type {
BaseAdapter,
BlockSnapshot,
Doc,
Job,
JobMiddleware,
Slice,
} from '@blocksuite/store';
import { Job } from '@blocksuite/store';
import DOMPurify from 'dompurify';
import type { RootContentMap } from 'hast';
import * as lz from 'lz-string';
@@ -286,10 +286,7 @@ export class Clipboard extends LifeCycleWatcher {
}
private _getJob() {
return new Job({
middlewares: this._jobMiddlewares,
collection: this.std.collection,
});
return this.std.getJob(this._jobMiddlewares);
}
readFromClipboard(clipboardData: DataTransfer) {
@@ -1,7 +1,7 @@
import type { ServiceProvider } from '@blocksuite/global/di';
import { Container } from '@blocksuite/global/di';
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import type { Doc } from '@blocksuite/store';
import { type Doc, Job, type JobMiddleware } from '@blocksuite/store';
import { Clipboard } from '../clipboard/index.js';
import { CommandManager } from '../command/index.js';
@@ -168,6 +168,19 @@ export class BlockStdScope {
return this.getOptional(BlockViewIdentifier(flavour));
}
getJob(middlewares: JobMiddleware[] = []) {
return new Job({
schema: this.collection.schema,
blobCRUD: this.collection.blobSync,
docCRUD: {
create: (id: string) => this.collection.createDoc({ id }),
get: (id: string) => this.collection.getDoc(id),
delete: (id: string) => this.collection.removeDoc(id),
},
middlewares,
});
}
mount() {
this._lifeCycleWatchers.forEach(watcher => {
watcher.mounted.call(watcher);
@@ -1,11 +1,6 @@
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
interface BlobCRUD {
get: (key: string) => Promise<Blob | null> | Blob | null;
set: (key: string, value: Blob) => Promise<string> | string;
delete: (key: string) => Promise<void> | void;
list: () => Promise<string[]> | string[];
}
import type { BlobCRUD } from './type.js';
type AssetsManagerConfig = {
blob: BlobCRUD;
@@ -1,8 +1,8 @@
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import { nextTick, Slot } from '@blocksuite/global/utils';
import type { BlockModel, BlockSchemaType } from '../schema/index.js';
import type { Doc, DocCollection, DocMeta } from '../store/index.js';
import type { BlockModel, BlockSchemaType, Schema } from '../schema/index.js';
import type { Doc } from '../store/index.js';
import { AssetsManager } from './assets.js';
import { BaseBlockTransformer } from './base.js';
import type { DraftModel } from './draft.js';
@@ -15,20 +15,22 @@ import type {
} from './middleware.js';
import { Slice } from './slice.js';
import type {
BlobCRUD,
BlockSnapshot,
CollectionInfoSnapshot,
DocCRUD,
DocSnapshot,
SliceSnapshot,
} from './type.js';
import {
BlockSnapshotSchema,
CollectionInfoSnapshotSchema,
DocSnapshotSchema,
SliceSnapshotSchema,
} from './type.js';
export type JobConfig = {
collection: DocCollection;
schema: Schema;
blobCRUD: BlobCRUD;
docCRUD: DocCRUD;
middlewares?: JobMiddleware[];
};
@@ -52,7 +54,9 @@ export class Job {
private readonly _assetsManager: AssetsManager;
private readonly _collection: DocCollection;
private readonly _schema: Schema;
private readonly _docCRUD: DocCRUD;
private readonly _slots: JobSlots = {
beforeImport: new Slot<BeforeImportPayload>(),
@@ -74,31 +78,6 @@ export class Job {
}
};
collectionInfoToSnapshot = (): CollectionInfoSnapshot | undefined => {
try {
this._slots.beforeExport.emit({
type: 'info',
});
const collectionMeta = this._getCollectionMeta();
const snapshot: CollectionInfoSnapshot = {
type: 'info',
id: this._collection.id,
...collectionMeta,
};
this._slots.afterExport.emit({
type: 'info',
snapshot,
});
CollectionInfoSnapshotSchema.parse(snapshot);
return snapshot;
} catch (error) {
console.error(`Error when transforming collection info to snapshot:`);
console.error(error);
return;
}
};
docToSnapshot = (doc: Doc): DocSnapshot | undefined => {
try {
this._slots.beforeExport.emit({
@@ -199,7 +178,7 @@ export class Job {
});
DocSnapshotSchema.parse(snapshot);
const { meta, blocks } = snapshot;
const doc = this._collection.createDoc({ id: meta.id });
const doc = this.docCRUD.create(meta.id);
doc.load();
await this.snapshotToBlock(blocks, doc);
this._slots.afterImport.emit({
@@ -328,19 +307,24 @@ export class Job {
return this._assetsManager;
}
get collection() {
return this._collection;
get schema() {
return this._schema;
}
constructor({ collection, middlewares = [] }: JobConfig) {
this._collection = collection;
this._assetsManager = new AssetsManager({ blob: collection.blobSync });
get docCRUD() {
return this._docCRUD;
}
constructor({ blobCRUD, schema, docCRUD, middlewares = [] }: JobConfig) {
this._assetsManager = new AssetsManager({ blob: blobCRUD });
this._schema = schema;
this._docCRUD = docCRUD;
middlewares.forEach(middleware => {
middleware({
slots: this._slots,
docCRUD: this._docCRUD,
assetsManager: this._assetsManager,
collection: this._collection,
adapterConfigs: this._adapterConfigs,
});
});
@@ -465,20 +449,8 @@ export class Job {
}
}
private _getCollectionMeta() {
const { meta } = this._collection;
const { docs } = meta;
if (!docs) {
throw new BlockSuiteError(ErrorCode.TransformerError, 'Docs not found');
}
return {
properties: {}, // for backward compatibility
pages: JSON.parse(JSON.stringify(docs)) as DocMeta[],
};
}
private _getSchema(flavour: string) {
const schema = this._collection.schema.flavourSchemaMap.get(flavour);
const schema = this.schema.flavourSchemaMap.get(flavour);
if (!schema) {
throw new BlockSuiteError(
ErrorCode.TransformerError,
@@ -1,12 +1,13 @@
import type { Slot } from '@blocksuite/global/utils';
import type { Doc, DocCollection } from '../store/index.js';
import type { Doc } from '../store/index.js';
import type { AssetsManager } from './assets.js';
import type { DraftModel } from './draft.js';
import type { Slice } from './slice.js';
import type {
BlockSnapshot,
CollectionInfoSnapshot,
DocCRUD,
DocSnapshot,
SliceSnapshot,
} from './type.js';
@@ -79,9 +80,9 @@ export type JobSlots = {
};
type JobMiddlewareOptions = {
collection: DocCollection;
assetsManager: AssetsManager;
slots: JobSlots;
docCRUD: DocCRUD;
adapterConfigs: Map<string, string>;
};
@@ -1,5 +1,6 @@
import { z } from 'zod';
import type { Doc } from '../store/doc/doc.js';
import type { DocMeta, DocsPropertiesMeta } from '../store/meta.js';
export type BlockSnapshot = {
@@ -65,3 +66,16 @@ export const DocSnapshotSchema: z.ZodType<DocSnapshot> = z.object({
meta: DocMetaSchema,
blocks: BlockSnapshotSchema,
});
export interface BlobCRUD {
get: (key: string) => Promise<Blob | null> | Blob | null;
set: (key: string, value: Blob) => Promise<string> | string;
delete: (key: string) => Promise<void> | void;
list: () => Promise<string[]> | string[];
}
export interface DocCRUD {
create: (id: string) => Doc;
get: (id: string) => Doc | null;
delete: (id: string) => void;
}
@@ -102,10 +102,19 @@ export class AdaptersPanel extends WithDisposable(ShadowlessElement) {
private _createJob() {
return new Job({
collection: this.doc.collection,
schema: this.doc.collection.schema,
blobCRUD: this.doc.collection.blobSync,
docCRUD: {
create: (id: string) => this.doc.collection.createDoc({ id }),
get: (id: string) => this.doc.collection.getDoc(id),
delete: (id: string) => this.doc.collection.removeDoc(id),
},
middlewares: [
docLinkBaseURLMiddlewareBuilder('https://example.com').get(),
titleMiddleware,
docLinkBaseURLMiddlewareBuilder(
'https://example.com',
this.doc.collection.id
).get(),
titleMiddleware(this.doc.collection.meta.docMetas),
embedSyncedDocMiddleware('content'),
defaultImageProxyMiddleware,
],
@@ -242,8 +242,17 @@ export class StarterDebugMenu extends ShadowlessElement {
private async _exportFile(config: AdapterConfig) {
const doc = this.editor.doc;
const job = new Job({
collection: this.editor.doc.collection,
middlewares: [docLinkBaseURLMiddleware, titleMiddleware],
schema: this.collection.schema,
blobCRUD: this.collection.blobSync,
docCRUD: {
create: (id: string) => this.collection.createDoc({ id }),
get: (id: string) => this.collection.getDoc(id),
delete: (id: string) => this.collection.removeDoc(id),
},
middlewares: [
docLinkBaseURLMiddleware(this.collection.id),
titleMiddleware(this.collection.meta.docMetas),
],
});
const adapterFactory = this.editor.std.provider.get(config.identifier);
@@ -432,7 +441,13 @@ export class StarterDebugMenu extends ShadowlessElement {
});
if (!file) return;
const job = new Job({
collection: this.collection,
schema: this.collection.schema,
blobCRUD: this.collection.blobSync,
docCRUD: {
create: (id: string) => this.collection.createDoc({ id }),
get: (id: string) => this.collection.getDoc(id),
delete: (id: string) => this.collection.removeDoc(id),
},
middlewares: [defaultImageProxyMiddleware],
});
const htmlAdapter = new NotionHtmlAdapter(job, this.editor.std.provider);
@@ -84,7 +84,15 @@ export async function createDefaultDocCollection() {
// debug info
window.collection = collection;
window.blockSchemas = AffineSchemas;
window.job = new Job({ collection });
window.job = new Job({
schema: collection.schema,
blobCRUD: collection.blobSync,
docCRUD: {
create: (id: string) => collection.createDoc({ id }),
get: (id: string) => collection.getDoc(id),
delete: (id: string) => collection.removeDoc(id),
},
});
window.Y = DocCollection.Y;
return collection;
@@ -83,7 +83,15 @@ export function createStarterDocCollection() {
// debug info
window.collection = collection;
window.blockSchemas = AffineSchemas;
window.job = new Job({ collection: collection });
window.job = new Job({
schema: collection.schema,
blobCRUD: collection.blobSync,
docCRUD: {
create: (id: string) => collection.createDoc({ id }),
get: (id: string) => collection.getDoc(id),
delete: (id: string) => collection.removeDoc(id),
},
});
window.Y = DocCollection.Y;
window.testUtils = new TestUtils();
@@ -6,8 +6,14 @@ export async function importFromSnapshot(
snapshot: DocSnapshot
) {
const job = new Job({
collection,
middlewares: [replaceIdMiddleware],
schema: collection.schema,
blobCRUD: collection.blobSync,
docCRUD: {
create: (id: string) => collection.createDoc({ id }),
get: (id: string) => collection.getDoc(id),
delete: (id: string) => collection.removeDoc(id),
},
middlewares: [replaceIdMiddleware(collection.idGenerator)],
});
return job.snapshotToDoc(snapshot);
+11 -2
View File
@@ -174,8 +174,17 @@ const frameworkProvider = framework.provider();
const blockSuiteDoc = doc.blockSuiteDoc;
const job = new Job({
collection: blockSuiteDoc.collection,
middlewares: [docLinkBaseURLMiddleware, titleMiddleware],
schema: blockSuiteDoc.collection.schema,
blobCRUD: blockSuiteDoc.collection.blobSync,
docCRUD: {
create: (id: string) => blockSuiteDoc.collection.createDoc({ id }),
get: (id: string) => blockSuiteDoc.collection.getDoc(id),
delete: (id: string) => blockSuiteDoc.collection.removeDoc(id),
},
middlewares: [
docLinkBaseURLMiddleware(blockSuiteDoc.collection.id),
titleMiddleware(blockSuiteDoc.collection.meta.docMetas),
],
});
const snapshot = job.docToSnapshot(blockSuiteDoc);
@@ -78,8 +78,17 @@ export async function getContentFromSlice(
type: 'markdown' | 'plain-text' = 'markdown'
) {
const job = new Job({
collection: host.std.doc.collection,
middlewares: [titleMiddleware, embedSyncedDocMiddleware('content')],
schema: host.std.doc.collection.schema,
blobCRUD: host.std.doc.collection.blobSync,
docCRUD: {
create: (id: string) => host.std.doc.collection.createDoc({ id }),
get: (id: string) => host.std.doc.collection.getDoc(id),
delete: (id: string) => host.std.doc.collection.removeDoc(id),
},
middlewares: [
titleMiddleware(host.std.doc.collection.meta.docMetas),
embedSyncedDocMiddleware('content'),
],
});
const snapshot = job.sliceToSnapshot(slice);
if (!snapshot) {
@@ -99,8 +108,14 @@ export async function getContentFromSlice(
export async function getPlainTextFromSlice(host: EditorHost, slice: Slice) {
const job = new Job({
collection: host.std.doc.collection,
middlewares: [titleMiddleware],
schema: host.std.doc.collection.schema,
blobCRUD: host.std.doc.collection.blobSync,
docCRUD: {
create: (id: string) => host.std.doc.collection.createDoc({ id }),
get: (id: string) => host.std.doc.collection.getDoc(id),
delete: (id: string) => host.std.doc.collection.removeDoc(id),
},
middlewares: [titleMiddleware(host.std.doc.collection.meta.docMetas)],
});
const snapshot = job.sliceToSnapshot(slice);
if (!snapshot) {
@@ -120,7 +135,13 @@ export const markdownToSnapshot = async (
host: EditorHost
) => {
const job = new Job({
collection: host.std.doc.collection,
schema: host.std.doc.collection.schema,
blobCRUD: host.std.doc.collection.blobSync,
docCRUD: {
create: (id: string) => host.std.doc.collection.createDoc({ id }),
get: (id: string) => host.std.doc.collection.getDoc(id),
delete: (id: string) => host.std.doc.collection.removeDoc(id),
},
middlewares: [defaultImageProxyMiddleware, pasteMiddleware(host.std)],
});
const markdownAdapter = new MixTextAdapter(job, host.std.provider);
@@ -204,7 +225,13 @@ export async function markDownToDoc(
middlewares.push(...additionalMiddlewares);
}
const job = new Job({
collection,
schema: collection.schema,
blobCRUD: collection.blobSync,
docCRUD: {
create: (id: string) => collection.createDoc({ id }),
get: (id: string) => collection.getDoc(id),
delete: (id: string) => collection.removeDoc(id),
},
middlewares,
});
const mdAdapter = new MarkdownAdapter(job, provider);
@@ -55,10 +55,16 @@ interface AdapterConfig {
async function exportDoc(doc: Doc, std: BlockStdScope, config: AdapterConfig) {
const job = new Job({
collection: doc.collection,
schema: doc.collection.schema,
blobCRUD: doc.collection.blobSync,
docCRUD: {
create: (id: string) => doc.collection.createDoc({ id }),
get: (id: string) => doc.collection.getDoc(id),
delete: (id: string) => doc.collection.removeDoc(id),
},
middlewares: [
docLinkBaseURLMiddleware,
titleMiddleware,
docLinkBaseURLMiddleware(doc.collection.id),
titleMiddleware(doc.collection.meta.docMetas),
embedSyncedDocMiddleware('content'),
],
});
@@ -79,7 +79,13 @@ async function initDoc(name: DocName) {
const snapshot = (await loaders[name]()) as DocSnapshot;
const collection = await getCollection();
const job = new Job({
collection,
schema: collection.schema,
blobCRUD: collection.blobSync,
docCRUD: {
create: (id: string) => collection.createDoc({ id }),
get: (id: string) => collection.getDoc(id),
delete: (id: string) => collection.removeDoc(id),
},
middlewares: [],
});
@@ -192,7 +192,13 @@ function generateMarkdownPreviewBuilder(
const provider = container.provider();
const markdownAdapter = new MarkdownAdapter(
new Job({
collection: markdownPreviewDocCollection,
schema: markdownPreviewDocCollection.schema,
blobCRUD: markdownPreviewDocCollection.blobSync,
docCRUD: {
create: (id: string) => markdownPreviewDocCollection.createDoc({ id }),
get: (id: string) => markdownPreviewDocCollection.getDoc(id),
delete: (id: string) => markdownPreviewDocCollection.removeDoc(id),
},
middlewares: [docLinkBaseURLMiddleware, titleMiddleware],
}),
provider