refactor(core): use manual upgrade to replace auto migration when web setup (#5022)
1. Split logic in `packages/common/infra/src/blocksuite/index.ts` to multiple single files 2. Move migration logic from setup to upgrade module, to prevent auto migration problems and loading problem
This commit is contained in:
@@ -1,750 +1,17 @@
|
|||||||
import type { Page, PageMeta, Workspace } from '@blocksuite/store';
|
export * from './initialization';
|
||||||
import { createIndexeddbStorage } from '@blocksuite/store';
|
export * from './migration/blob';
|
||||||
import type { createStore, WritableAtom } from 'jotai/vanilla';
|
export { migratePages as forceUpgradePages } from './migration/blocksuite'; // campatible with electron
|
||||||
import type { Doc } from 'yjs';
|
export * from './migration/fixing';
|
||||||
import { Array as YArray, Doc as YDoc, Map as YMap, transact } from 'yjs';
|
export { migrateToSubdoc } from './migration/subdoc';
|
||||||
|
export * from './migration/workspace';
|
||||||
export async function initEmptyPage(page: Page, title?: string) {
|
|
||||||
await page.waitForLoaded();
|
|
||||||
const pageBlockId = page.addBlock('affine:page', {
|
|
||||||
title: new page.Text(title ?? ''),
|
|
||||||
});
|
|
||||||
page.addBlock('affine:surface', {}, pageBlockId);
|
|
||||||
const noteBlockId = page.addBlock('affine:note', {}, pageBlockId);
|
|
||||||
page.addBlock('affine:paragraph', {}, noteBlockId);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function buildEmptyBlockSuite(workspace: Workspace) {
|
|
||||||
const page = workspace.createPage();
|
|
||||||
await initEmptyPage(page);
|
|
||||||
workspace.setPageMeta(page.id, {
|
|
||||||
jumpOnce: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function buildShowcaseWorkspace(
|
|
||||||
workspace: Workspace,
|
|
||||||
options: {
|
|
||||||
schema: Schema;
|
|
||||||
atoms: {
|
|
||||||
pageMode: WritableAtom<
|
|
||||||
undefined,
|
|
||||||
[pageId: string, mode: 'page' | 'edgeless'],
|
|
||||||
void
|
|
||||||
>;
|
|
||||||
};
|
|
||||||
store: ReturnType<typeof createStore>;
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
const prototypes = {
|
|
||||||
tags: {
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
id: 'icg1n5UdkP',
|
|
||||||
value: 'Travel',
|
|
||||||
color: 'var(--affine-tag-gray)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Oe5dSe1DDJ',
|
|
||||||
value: 'Quick summary',
|
|
||||||
color: 'var(--affine-tag-green)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'g1L5dXKctL',
|
|
||||||
value: 'OKR',
|
|
||||||
color: 'var(--affine-tag-purple)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'q3mceOl_zi',
|
|
||||||
value: 'Streamline your workflow',
|
|
||||||
color: 'var(--affine-tag-teal)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'ze07JVwBu4',
|
|
||||||
value: 'Plan',
|
|
||||||
color: 'var(--affine-tag-teal)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '8qcYPCTK0h',
|
|
||||||
value: 'Review',
|
|
||||||
color: 'var(--affine-tag-orange)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'wg-fBtd2eI',
|
|
||||||
value: 'Engage',
|
|
||||||
color: 'var(--affine-tag-pink)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'QYFD_HeQc-',
|
|
||||||
value: 'Create',
|
|
||||||
color: 'var(--affine-tag-blue)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'ZHBa2NtdSo',
|
|
||||||
value: 'Learn',
|
|
||||||
color: 'var(--affine-tag-yellow)',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
workspace.meta.setProperties(prototypes);
|
|
||||||
const edgelessPage1 = nanoid();
|
|
||||||
const edgelessPage2 = nanoid();
|
|
||||||
const edgelessPage3 = nanoid();
|
|
||||||
const { store, atoms } = options;
|
|
||||||
[edgelessPage1, edgelessPage2, edgelessPage3].forEach(pageId => {
|
|
||||||
store.set(atoms.pageMode, pageId, 'edgeless');
|
|
||||||
});
|
|
||||||
|
|
||||||
const pageMetas = {
|
|
||||||
'9f6f3c04-cf32-470c-9648-479dc838f10e': {
|
|
||||||
createDate: 1691548231530,
|
|
||||||
tags: ['ZHBa2NtdSo', 'QYFD_HeQc-', 'wg-fBtd2eI'],
|
|
||||||
updatedDate: 1691676331623,
|
|
||||||
favorite: true,
|
|
||||||
jumpOnce: true,
|
|
||||||
},
|
|
||||||
'0773e198-5de0-45d4-a35e-de22ea72b96b': {
|
|
||||||
createDate: 1691548220794,
|
|
||||||
tags: [],
|
|
||||||
updatedDate: 1691676775642,
|
|
||||||
favorite: false,
|
|
||||||
},
|
|
||||||
'59b140eb-4449-488f-9eeb-42412dcc044e': {
|
|
||||||
createDate: 1691551731225,
|
|
||||||
tags: [],
|
|
||||||
updatedDate: 1691654611175,
|
|
||||||
favorite: false,
|
|
||||||
},
|
|
||||||
'7217fbe2-61db-4a91-93c6-ad5c800e5a43': {
|
|
||||||
createDate: 1691552082822,
|
|
||||||
tags: [],
|
|
||||||
updatedDate: 1691654606912,
|
|
||||||
favorite: false,
|
|
||||||
},
|
|
||||||
'6eb43ea8-8c11-456d-bb1d-5193937961ab': {
|
|
||||||
createDate: 1691552090989,
|
|
||||||
tags: [],
|
|
||||||
updatedDate: 1691646748171,
|
|
||||||
favorite: false,
|
|
||||||
},
|
|
||||||
'3ddc8a4f-62c7-4fd4-8064-9ed9f61e437a': {
|
|
||||||
createDate: 1691564303138,
|
|
||||||
tags: [],
|
|
||||||
updatedDate: 1691646845195,
|
|
||||||
},
|
|
||||||
'512b1cb3-d22d-4b20-a7aa-58e2afcb1238': {
|
|
||||||
createDate: 1691574743531,
|
|
||||||
tags: ['icg1n5UdkP'],
|
|
||||||
updatedDate: 1691647117761,
|
|
||||||
},
|
|
||||||
'22163830-8252-43fe-b62d-fd9bbeaa4caa': {
|
|
||||||
createDate: 1691574859042,
|
|
||||||
tags: [],
|
|
||||||
updatedDate: 1691648159371,
|
|
||||||
},
|
|
||||||
'b7a9e1bc-e205-44aa-8dad-7e328269d00b': {
|
|
||||||
createDate: 1691575011078,
|
|
||||||
tags: ['8qcYPCTK0h'],
|
|
||||||
updatedDate: 1691645074511,
|
|
||||||
favorite: false,
|
|
||||||
},
|
|
||||||
'646305d9-93e0-48df-bb92-d82944ceb5a3': {
|
|
||||||
createDate: 1691634722239,
|
|
||||||
tags: ['ze07JVwBu4'],
|
|
||||||
updatedDate: 1691647069662,
|
|
||||||
favorite: false,
|
|
||||||
},
|
|
||||||
'0350509d-8702-4797-b4d7-168f5e9359c7': {
|
|
||||||
createDate: 1691635388447,
|
|
||||||
tags: ['Oe5dSe1DDJ'],
|
|
||||||
updatedDate: 1691645873930,
|
|
||||||
},
|
|
||||||
'aa02af3c-5c5c-4856-b7ce-947ad17331f3': {
|
|
||||||
createDate: 1691636192263,
|
|
||||||
tags: ['q3mceOl_zi', 'g1L5dXKctL'],
|
|
||||||
updatedDate: 1691645102104,
|
|
||||||
},
|
|
||||||
'9d6e716e-a071-45a2-88ac-2f2f6eec0109': {
|
|
||||||
createDate: 1691574743531,
|
|
||||||
tags: ['icg1n5UdkP'],
|
|
||||||
updatedDate: 1691574743531,
|
|
||||||
},
|
|
||||||
} satisfies Record<string, Partial<PageMeta>>;
|
|
||||||
const data = [
|
|
||||||
[
|
|
||||||
'9f6f3c04-cf32-470c-9648-479dc838f10e',
|
|
||||||
import('@affine/templates/v1/getting-started.json'),
|
|
||||||
nanoid(),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'0773e198-5de0-45d4-a35e-de22ea72b96b',
|
|
||||||
import('@affine/templates/v1/preloading.json'),
|
|
||||||
edgelessPage1,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'59b140eb-4449-488f-9eeb-42412dcc044e',
|
|
||||||
import('@affine/templates/v1/template-galleries.json'),
|
|
||||||
nanoid(),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'7217fbe2-61db-4a91-93c6-ad5c800e5a43',
|
|
||||||
import('@affine/templates/v1/personal-home.json'),
|
|
||||||
nanoid(),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'6eb43ea8-8c11-456d-bb1d-5193937961ab',
|
|
||||||
import('@affine/templates/v1/working-home.json'),
|
|
||||||
nanoid(),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'3ddc8a4f-62c7-4fd4-8064-9ed9f61e437a',
|
|
||||||
import('@affine/templates/v1/personal-project-management.json'),
|
|
||||||
nanoid(),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'512b1cb3-d22d-4b20-a7aa-58e2afcb1238',
|
|
||||||
import('@affine/templates/v1/travel-plan.json'),
|
|
||||||
edgelessPage2,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'22163830-8252-43fe-b62d-fd9bbeaa4caa',
|
|
||||||
import('@affine/templates/v1/personal-knowledge-management.json'),
|
|
||||||
nanoid(),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'b7a9e1bc-e205-44aa-8dad-7e328269d00b',
|
|
||||||
import('@affine/templates/v1/annual-performance-review.json'),
|
|
||||||
nanoid(),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'646305d9-93e0-48df-bb92-d82944ceb5a3',
|
|
||||||
import('@affine/templates/v1/brief-event-planning.json'),
|
|
||||||
nanoid(),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'0350509d-8702-4797-b4d7-168f5e9359c7',
|
|
||||||
import('@affine/templates/v1/meeting-summary.json'),
|
|
||||||
nanoid(),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'aa02af3c-5c5c-4856-b7ce-947ad17331f3',
|
|
||||||
import('@affine/templates/v1/okr-template.json'),
|
|
||||||
nanoid(),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'9d6e716e-a071-45a2-88ac-2f2f6eec0109',
|
|
||||||
import('@affine/templates/v1/travel-note.json'),
|
|
||||||
edgelessPage3,
|
|
||||||
],
|
|
||||||
] as const;
|
|
||||||
const idMap = await Promise.all(data).then(async data => {
|
|
||||||
return data.reduce<Record<string, string>>(
|
|
||||||
(record, currentValue) => {
|
|
||||||
const [oldId, _, newId] = currentValue;
|
|
||||||
record[oldId] = newId;
|
|
||||||
return record;
|
|
||||||
},
|
|
||||||
{} as Record<string, string>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
await Promise.all(
|
|
||||||
data.map(async ([id, promise, newId]) => {
|
|
||||||
const { default: template } = await promise;
|
|
||||||
let json = JSON.stringify(template);
|
|
||||||
Object.entries(idMap).forEach(([oldId, newId]) => {
|
|
||||||
json = json.replaceAll(oldId, newId);
|
|
||||||
});
|
|
||||||
json = JSON.parse(json);
|
|
||||||
await workspace
|
|
||||||
.importPageSnapshot(structuredClone(json), newId)
|
|
||||||
.catch(error => {
|
|
||||||
console.error('error importing page', id, error);
|
|
||||||
});
|
|
||||||
const page = workspace.getPage(newId);
|
|
||||||
assertExists(page);
|
|
||||||
await page.waitForLoaded();
|
|
||||||
workspace.schema.upgradePage(
|
|
||||||
0,
|
|
||||||
{
|
|
||||||
'affine:note': 1,
|
|
||||||
'affine:bookmark': 1,
|
|
||||||
'affine:database': 2,
|
|
||||||
'affine:divider': 1,
|
|
||||||
'affine:image': 1,
|
|
||||||
'affine:list': 1,
|
|
||||||
'affine:code': 1,
|
|
||||||
'affine:page': 2,
|
|
||||||
'affine:paragraph': 1,
|
|
||||||
'affine:surface': 3,
|
|
||||||
},
|
|
||||||
page.spaceDoc
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
Object.entries(pageMetas).forEach(([oldId, meta]) => {
|
|
||||||
const newId = idMap[oldId];
|
|
||||||
workspace.setPageMeta(newId, meta);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
|
|
||||||
|
|
||||||
const migrationOrigin = 'affine-migration';
|
|
||||||
|
|
||||||
import { assertExists } from '@blocksuite/global/utils';
|
|
||||||
import type { Schema } from '@blocksuite/store';
|
|
||||||
import { nanoid } from 'nanoid';
|
|
||||||
|
|
||||||
type XYWH = [number, number, number, number];
|
|
||||||
|
|
||||||
function deserializeXYWH(xywh: string): XYWH {
|
|
||||||
return JSON.parse(xywh) as XYWH;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getLatestVersions = (schema: Schema): Record<string, number> => {
|
|
||||||
return [...schema.flavourSchemaMap.entries()].reduce(
|
|
||||||
(record, [flavour, schema]) => {
|
|
||||||
record[flavour] = schema.version;
|
|
||||||
return record;
|
|
||||||
},
|
|
||||||
{} as Record<string, number>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function migrateDatabase(data: YMap<unknown>) {
|
|
||||||
data.delete('prop:mode');
|
|
||||||
data.set('prop:views', new YArray());
|
|
||||||
const columns = (data.get('prop:columns') as YArray<unknown>).toJSON() as {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
hide: boolean;
|
|
||||||
type: string;
|
|
||||||
width: number;
|
|
||||||
selection?: unknown[];
|
|
||||||
}[];
|
|
||||||
const views = [
|
|
||||||
{
|
|
||||||
id: 'default',
|
|
||||||
name: 'Table',
|
|
||||||
columns: columns.map(col => ({
|
|
||||||
id: col.id,
|
|
||||||
width: col.width,
|
|
||||||
hide: col.hide,
|
|
||||||
})),
|
|
||||||
filter: { type: 'group', op: 'and', conditions: [] },
|
|
||||||
mode: 'table',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const cells = (data.get('prop:cells') as YMap<unknown>).toJSON() as Record<
|
|
||||||
string,
|
|
||||||
Record<
|
|
||||||
string,
|
|
||||||
{
|
|
||||||
id: string;
|
|
||||||
value: unknown;
|
|
||||||
}
|
|
||||||
>
|
|
||||||
>;
|
|
||||||
const convertColumn = (
|
|
||||||
id: string,
|
|
||||||
update: (cell: { id: string; value: unknown }) => void
|
|
||||||
) => {
|
|
||||||
Object.values(cells).forEach(row => {
|
|
||||||
if (row[id] != null) {
|
|
||||||
update(row[id]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const newColumns = columns.map(v => {
|
|
||||||
let data: Record<string, unknown> = {};
|
|
||||||
if (v.type === 'select' || v.type === 'multi-select') {
|
|
||||||
data = { options: v.selection };
|
|
||||||
if (v.type === 'select') {
|
|
||||||
convertColumn(v.id, cell => {
|
|
||||||
if (Array.isArray(cell.value)) {
|
|
||||||
cell.value = cell.value[0]?.id;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
convertColumn(v.id, cell => {
|
|
||||||
if (Array.isArray(cell.value)) {
|
|
||||||
cell.value = cell.value.map(v => v.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (v.type === 'number') {
|
|
||||||
convertColumn(v.id, cell => {
|
|
||||||
if (typeof cell.value === 'string') {
|
|
||||||
cell.value = Number.parseFloat(cell.value.toString());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
id: v.id,
|
|
||||||
type: v.type,
|
|
||||||
name: v.name,
|
|
||||||
data,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
data.set('prop:columns', newColumns);
|
|
||||||
data.set('prop:views', views);
|
|
||||||
data.set('prop:cells', cells);
|
|
||||||
}
|
|
||||||
|
|
||||||
function runBlockMigration(
|
|
||||||
flavour: string,
|
|
||||||
data: YMap<unknown>,
|
|
||||||
version: number
|
|
||||||
) {
|
|
||||||
if (flavour === 'affine:frame') {
|
|
||||||
data.set('sys:flavour', 'affine:note');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (flavour === 'affine:surface' && version <= 3) {
|
|
||||||
if (data.has('elements')) {
|
|
||||||
const elements = data.get('elements') as YMap<unknown>;
|
|
||||||
migrateSurface(elements);
|
|
||||||
data.set('prop:elements', elements.clone());
|
|
||||||
data.delete('elements');
|
|
||||||
} else {
|
|
||||||
data.set('prop:elements', new YMap());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (flavour === 'affine:embed') {
|
|
||||||
data.set('sys:flavour', 'affine:image');
|
|
||||||
data.delete('prop:type');
|
|
||||||
}
|
|
||||||
if (flavour === 'affine:database' && version < 2) {
|
|
||||||
migrateDatabase(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function migrateSurface(data: YMap<unknown>) {
|
|
||||||
for (const [, value] of <IterableIterator<[string, YMap<unknown>]>>(
|
|
||||||
data.entries()
|
|
||||||
)) {
|
|
||||||
if (value.get('type') === 'connector') {
|
|
||||||
migrateSurfaceConnector(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function migrateSurfaceConnector(data: YMap<any>) {
|
|
||||||
let id = data.get('startElement')?.id;
|
|
||||||
const controllers = data.get('controllers');
|
|
||||||
const length = controllers.length;
|
|
||||||
const xywh = deserializeXYWH(data.get('xywh'));
|
|
||||||
if (id) {
|
|
||||||
data.set('source', { id });
|
|
||||||
} else {
|
|
||||||
data.set('source', {
|
|
||||||
position: [controllers[0].x + xywh[0], controllers[0].y + xywh[1]],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
id = data.get('endElement')?.id;
|
|
||||||
if (id) {
|
|
||||||
data.set('target', { id });
|
|
||||||
} else {
|
|
||||||
data.set('target', {
|
|
||||||
position: [
|
|
||||||
controllers[length - 1].x + xywh[0],
|
|
||||||
controllers[length - 1].y + xywh[1],
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const width = data.get('lineWidth') ?? 4;
|
|
||||||
data.set('strokeWidth', width);
|
|
||||||
const color = data.get('color');
|
|
||||||
data.set('stroke', color);
|
|
||||||
|
|
||||||
data.delete('startElement');
|
|
||||||
data.delete('endElement');
|
|
||||||
data.delete('controllers');
|
|
||||||
data.delete('lineWidth');
|
|
||||||
data.delete('color');
|
|
||||||
data.delete('xywh');
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateBlockVersions(versions: YMap<number>) {
|
|
||||||
const frameVersion = versions.get('affine:frame');
|
|
||||||
if (frameVersion !== undefined) {
|
|
||||||
versions.set('affine:note', frameVersion);
|
|
||||||
versions.delete('affine:frame');
|
|
||||||
}
|
|
||||||
const embedVersion = versions.get('affine:embed');
|
|
||||||
if (embedVersion !== undefined) {
|
|
||||||
versions.set('affine:image', embedVersion);
|
|
||||||
versions.delete('affine:embed');
|
|
||||||
}
|
|
||||||
const databaseVersion = versions.get('affine:database');
|
|
||||||
if (databaseVersion !== undefined && databaseVersion < 2) {
|
|
||||||
versions.set('affine:database', 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function migrateMeta(
|
|
||||||
oldDoc: YDoc,
|
|
||||||
newDoc: YDoc,
|
|
||||||
idMap: Record<string, string>
|
|
||||||
) {
|
|
||||||
const originalMeta = oldDoc.getMap('space:meta');
|
|
||||||
const originalVersions = originalMeta.get('versions') as YMap<number>;
|
|
||||||
const originalPages = originalMeta.get('pages') as YArray<YMap<unknown>>;
|
|
||||||
const meta = newDoc.getMap('meta');
|
|
||||||
const pages = new YArray();
|
|
||||||
const blockVersions = originalVersions.clone();
|
|
||||||
|
|
||||||
meta.set('workspaceVersion', 1);
|
|
||||||
meta.set('blockVersions', blockVersions);
|
|
||||||
meta.set('pages', pages);
|
|
||||||
meta.set('name', originalMeta.get('name') as string);
|
|
||||||
|
|
||||||
updateBlockVersions(blockVersions);
|
|
||||||
const mapList = originalPages.map(page => {
|
|
||||||
const map = new YMap();
|
|
||||||
Array.from(page.entries())
|
|
||||||
.filter(([key]) => key !== 'subpageIds')
|
|
||||||
.forEach(([key, value]) => {
|
|
||||||
if (key === 'id') {
|
|
||||||
idMap[value] = nanoid();
|
|
||||||
map.set(key, idMap[value]);
|
|
||||||
} else {
|
|
||||||
map.set(key, value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return map;
|
|
||||||
});
|
|
||||||
pages.push(mapList);
|
|
||||||
}
|
|
||||||
|
|
||||||
function migrateBlocks(
|
|
||||||
oldDoc: YDoc,
|
|
||||||
newDoc: YDoc,
|
|
||||||
idMap: Record<string, string>
|
|
||||||
) {
|
|
||||||
const spaces = newDoc.getMap('spaces');
|
|
||||||
const originalMeta = oldDoc.getMap('space:meta');
|
|
||||||
const originalVersions = originalMeta.get('versions') as YMap<number>;
|
|
||||||
const originalPages = originalMeta.get('pages') as YArray<YMap<unknown>>;
|
|
||||||
originalPages.forEach(page => {
|
|
||||||
const id = page.get('id') as string;
|
|
||||||
const newId = idMap[id];
|
|
||||||
const spaceId = id.startsWith('space:') ? id : `space:${id}`;
|
|
||||||
const originalBlocks = oldDoc.getMap(spaceId) as YMap<unknown>;
|
|
||||||
const subdoc = new YDoc();
|
|
||||||
spaces.set(newId, subdoc);
|
|
||||||
subdoc.guid = id;
|
|
||||||
const blocks = subdoc.getMap('blocks');
|
|
||||||
Array.from(originalBlocks.entries()).forEach(([key, value]) => {
|
|
||||||
const blockData = value.clone();
|
|
||||||
blocks.set(key, blockData);
|
|
||||||
const flavour = blockData.get('sys:flavour') as string;
|
|
||||||
const version = originalVersions.get(flavour);
|
|
||||||
if (version !== undefined) {
|
|
||||||
runBlockMigration(flavour, blockData, version);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function migrateToSubdoc(oldDoc: YDoc): YDoc {
|
|
||||||
const needMigration =
|
|
||||||
Array.from(oldDoc.getMap('space:meta').keys()).length > 0;
|
|
||||||
if (!needMigration) {
|
|
||||||
return oldDoc;
|
|
||||||
}
|
|
||||||
const newDoc = new YDoc();
|
|
||||||
const idMap = {} as Record<string, string>;
|
|
||||||
migrateMeta(oldDoc, newDoc, idMap);
|
|
||||||
migrateBlocks(oldDoc, newDoc, idMap);
|
|
||||||
return newDoc;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type UpgradeOptions = {
|
|
||||||
getCurrentRootDoc: () => Promise<YDoc>;
|
|
||||||
createWorkspace: () => Promise<Workspace>;
|
|
||||||
getSchema: () => Schema;
|
|
||||||
};
|
|
||||||
|
|
||||||
const upgradeV1ToV2 = async (options: UpgradeOptions) => {
|
|
||||||
const oldDoc = await options.getCurrentRootDoc();
|
|
||||||
const newDoc = migrateToSubdoc(oldDoc);
|
|
||||||
const newWorkspace = await options.createWorkspace();
|
|
||||||
applyUpdate(newWorkspace.doc, encodeStateAsUpdate(newDoc), migrationOrigin);
|
|
||||||
newDoc.getSubdocs().forEach(subdoc => {
|
|
||||||
newWorkspace.doc.getSubdocs().forEach(newDoc => {
|
|
||||||
if (subdoc.guid === newDoc.guid) {
|
|
||||||
applyUpdate(newDoc, encodeStateAsUpdate(subdoc), migrationOrigin);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return newWorkspace;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Force upgrade block schema to the latest.
|
* @deprecated
|
||||||
* Don't force to upgrade the pages without the check.
|
* Use workspace meta data to determine the workspace version.
|
||||||
*
|
|
||||||
* Please note that this function will not upgrade the workspace version.
|
|
||||||
*
|
|
||||||
* @returns true if any schema is upgraded.
|
|
||||||
* @returns false if no schema is upgraded.
|
|
||||||
*/
|
*/
|
||||||
export async function forceUpgradePages(
|
|
||||||
options: Omit<UpgradeOptions, 'createWorkspace'>
|
|
||||||
): Promise<boolean> {
|
|
||||||
const rootDoc = await options.getCurrentRootDoc();
|
|
||||||
guidCompatibilityFix(rootDoc);
|
|
||||||
|
|
||||||
const spaces = rootDoc.getMap('spaces') as YMap<any>;
|
|
||||||
const meta = rootDoc.getMap('meta') as YMap<unknown>;
|
|
||||||
const versions = meta.get('blockVersions') as YMap<number>;
|
|
||||||
const schema = options.getSchema();
|
|
||||||
const oldVersions = versions?.toJSON() ?? {};
|
|
||||||
spaces.forEach((space: Doc) => {
|
|
||||||
try {
|
|
||||||
schema.upgradePage(0, oldVersions, space);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(`page ${space.guid} upgrade failed`, e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const newVersions = getLatestVersions(schema);
|
|
||||||
meta.set('blockVersions', new YMap(Object.entries(newVersions)));
|
|
||||||
return Object.entries(oldVersions).some(
|
|
||||||
([flavour, version]) => newVersions[flavour] !== version
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// database from 2 to 3
|
|
||||||
async function upgradeV2ToV3(options: UpgradeOptions): Promise<boolean> {
|
|
||||||
const rootDoc = await options.getCurrentRootDoc();
|
|
||||||
const spaces = rootDoc.getMap('spaces') as YMap<any>;
|
|
||||||
const meta = rootDoc.getMap('meta') as YMap<unknown>;
|
|
||||||
const versions = meta.get('blockVersions') as YMap<number>;
|
|
||||||
const schema = options.getSchema();
|
|
||||||
guidCompatibilityFix(rootDoc);
|
|
||||||
spaces.forEach((space: Doc) => {
|
|
||||||
schema.upgradePage(
|
|
||||||
0,
|
|
||||||
{
|
|
||||||
'affine:note': 1,
|
|
||||||
'affine:bookmark': 1,
|
|
||||||
'affine:database': 2,
|
|
||||||
'affine:divider': 1,
|
|
||||||
'affine:image': 1,
|
|
||||||
'affine:list': 1,
|
|
||||||
'affine:code': 1,
|
|
||||||
'affine:page': 2,
|
|
||||||
'affine:paragraph': 1,
|
|
||||||
'affine:surface': 3,
|
|
||||||
},
|
|
||||||
space
|
|
||||||
);
|
|
||||||
});
|
|
||||||
if ('affine:database' in versions) {
|
|
||||||
meta.set(
|
|
||||||
'blockVersions',
|
|
||||||
new YMap(Object.entries(getLatestVersions(schema)))
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
Object.entries(getLatestVersions(schema)).map(([flavour, version]) =>
|
|
||||||
versions.set(flavour, version)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// patch root doc's space guid compatibility issue
|
|
||||||
//
|
|
||||||
// in version 0.10, page id in spaces no longer has prefix "space:"
|
|
||||||
// The data flow for fetching a doc's updates is:
|
|
||||||
// - page id in `meta.pages` -> find `${page-id}` in `doc.spaces` -> `doc` -> `doc.guid`
|
|
||||||
// if `doc` is not found in `doc.spaces`, a new doc will be created and its `doc.guid` is the same with its pageId
|
|
||||||
// - because of guid logic change, the doc that previously prefixed with "space:" will not be found in `doc.spaces`
|
|
||||||
// - when fetching the rows of this doc using the doc id === page id,
|
|
||||||
// it will return empty since there is no updates associated with the page id
|
|
||||||
export function guidCompatibilityFix(rootDoc: YDoc) {
|
|
||||||
let changed = false;
|
|
||||||
transact(rootDoc, () => {
|
|
||||||
const meta = rootDoc.getMap('meta') as YMap<unknown>;
|
|
||||||
const pages = meta.get('pages') as YArray<YMap<unknown>>;
|
|
||||||
pages?.forEach(page => {
|
|
||||||
const pageId = page.get('id') as string | undefined;
|
|
||||||
if (pageId?.includes(':')) {
|
|
||||||
// remove the prefix "space:" from page id
|
|
||||||
page.set('id', pageId.split(':').at(-1));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const spaces = rootDoc.getMap('spaces') as YMap<YDoc>;
|
|
||||||
spaces?.forEach((doc: YDoc, pageId: string) => {
|
|
||||||
if (pageId.includes(':')) {
|
|
||||||
const newPageId = pageId.split(':').at(-1) ?? pageId;
|
|
||||||
const newDoc = new YDoc();
|
|
||||||
// clone the original doc. yjs is not happy to use the same doc instance
|
|
||||||
applyUpdate(newDoc, encodeStateAsUpdate(doc));
|
|
||||||
newDoc.guid = doc.guid;
|
|
||||||
spaces.set(newPageId, newDoc);
|
|
||||||
// should remove the old doc, otherwise we will do it again in the next run
|
|
||||||
spaces.delete(pageId);
|
|
||||||
changed = true;
|
|
||||||
console.debug(
|
|
||||||
`fixed space id ${pageId} -> ${newPageId}, doc id: ${doc.guid}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return changed;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum WorkspaceVersion {
|
export enum WorkspaceVersion {
|
||||||
// v1 is treated as undefined
|
// v1 is treated as undefined
|
||||||
SubDoc = 2,
|
SubDoc = 2,
|
||||||
DatabaseV3 = 3,
|
DatabaseV3 = 3,
|
||||||
Surface = 4,
|
Surface = 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* If returns false, it means no migration is needed.
|
|
||||||
* If returns true, it means migration is done.
|
|
||||||
* If returns Workspace, it means new workspace is created,
|
|
||||||
* and the old workspace should be deleted.
|
|
||||||
*/
|
|
||||||
export async function migrateWorkspace(
|
|
||||||
currentVersion: WorkspaceVersion | undefined,
|
|
||||||
options: UpgradeOptions
|
|
||||||
): Promise<Workspace | boolean> {
|
|
||||||
if (currentVersion === undefined) {
|
|
||||||
const workspace = await upgradeV1ToV2(options);
|
|
||||||
await upgradeV2ToV3({
|
|
||||||
...options,
|
|
||||||
getCurrentRootDoc: () => Promise.resolve(workspace.doc),
|
|
||||||
});
|
|
||||||
return workspace;
|
|
||||||
}
|
|
||||||
if (currentVersion === WorkspaceVersion.SubDoc) {
|
|
||||||
return upgradeV2ToV3(options);
|
|
||||||
} else if (currentVersion === WorkspaceVersion.DatabaseV3) {
|
|
||||||
// surface from 3 to 5
|
|
||||||
return forceUpgradePages(options);
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function migrateLocalBlobStorage(from: string, to: string) {
|
|
||||||
const fromStorage = createIndexeddbStorage(from);
|
|
||||||
const toStorage = createIndexeddbStorage(to);
|
|
||||||
const keys = await fromStorage.crud.list();
|
|
||||||
for (const key of keys) {
|
|
||||||
const value = await fromStorage.crud.get(key);
|
|
||||||
if (!value) {
|
|
||||||
console.warn('cannot find blob:', key);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
await toStorage.crud.set(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
291
packages/common/infra/src/blocksuite/initialization/index.ts
Normal file
291
packages/common/infra/src/blocksuite/initialization/index.ts
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
|
import type { Page, PageMeta, Workspace } from '@blocksuite/store';
|
||||||
|
import type { createStore, WritableAtom } from 'jotai/vanilla';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
|
import { migratePages } from '../migration/blocksuite';
|
||||||
|
|
||||||
|
export async function initEmptyPage(page: Page, title?: string) {
|
||||||
|
await page.load(() => {
|
||||||
|
const pageBlockId = page.addBlock('affine:page', {
|
||||||
|
title: new page.Text(title ?? ''),
|
||||||
|
});
|
||||||
|
page.addBlock('affine:surface', {}, pageBlockId);
|
||||||
|
const noteBlockId = page.addBlock('affine:note', {}, pageBlockId);
|
||||||
|
page.addBlock('affine:paragraph', {}, noteBlockId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FIXME: Use exported json data to instead of building data.
|
||||||
|
*/
|
||||||
|
export async function buildShowcaseWorkspace(
|
||||||
|
workspace: Workspace,
|
||||||
|
options: {
|
||||||
|
atoms: {
|
||||||
|
pageMode: WritableAtom<
|
||||||
|
undefined,
|
||||||
|
[pageId: string, mode: 'page' | 'edgeless'],
|
||||||
|
void
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
store: ReturnType<typeof createStore>;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const prototypes = {
|
||||||
|
tags: {
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
id: 'icg1n5UdkP',
|
||||||
|
value: 'Travel',
|
||||||
|
color: 'var(--affine-tag-gray)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Oe5dSe1DDJ',
|
||||||
|
value: 'Quick summary',
|
||||||
|
color: 'var(--affine-tag-green)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'g1L5dXKctL',
|
||||||
|
value: 'OKR',
|
||||||
|
color: 'var(--affine-tag-purple)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'q3mceOl_zi',
|
||||||
|
value: 'Streamline your workflow',
|
||||||
|
color: 'var(--affine-tag-teal)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ze07JVwBu4',
|
||||||
|
value: 'Plan',
|
||||||
|
color: 'var(--affine-tag-teal)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '8qcYPCTK0h',
|
||||||
|
value: 'Review',
|
||||||
|
color: 'var(--affine-tag-orange)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'wg-fBtd2eI',
|
||||||
|
value: 'Engage',
|
||||||
|
color: 'var(--affine-tag-pink)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'QYFD_HeQc-',
|
||||||
|
value: 'Create',
|
||||||
|
color: 'var(--affine-tag-blue)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ZHBa2NtdSo',
|
||||||
|
value: 'Learn',
|
||||||
|
color: 'var(--affine-tag-yellow)',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
workspace.meta.setProperties(prototypes);
|
||||||
|
const edgelessPage1 = nanoid();
|
||||||
|
const edgelessPage2 = nanoid();
|
||||||
|
const edgelessPage3 = nanoid();
|
||||||
|
const { store, atoms } = options;
|
||||||
|
[edgelessPage1, edgelessPage2, edgelessPage3].forEach(pageId => {
|
||||||
|
store.set(atoms.pageMode, pageId, 'edgeless');
|
||||||
|
});
|
||||||
|
|
||||||
|
const pageMetas = {
|
||||||
|
'9f6f3c04-cf32-470c-9648-479dc838f10e': {
|
||||||
|
createDate: 1691548231530,
|
||||||
|
tags: ['ZHBa2NtdSo', 'QYFD_HeQc-', 'wg-fBtd2eI'],
|
||||||
|
updatedDate: 1691676331623,
|
||||||
|
favorite: true,
|
||||||
|
jumpOnce: true,
|
||||||
|
},
|
||||||
|
'0773e198-5de0-45d4-a35e-de22ea72b96b': {
|
||||||
|
createDate: 1691548220794,
|
||||||
|
tags: [],
|
||||||
|
updatedDate: 1691676775642,
|
||||||
|
favorite: false,
|
||||||
|
},
|
||||||
|
'59b140eb-4449-488f-9eeb-42412dcc044e': {
|
||||||
|
createDate: 1691551731225,
|
||||||
|
tags: [],
|
||||||
|
updatedDate: 1691654611175,
|
||||||
|
favorite: false,
|
||||||
|
},
|
||||||
|
'7217fbe2-61db-4a91-93c6-ad5c800e5a43': {
|
||||||
|
createDate: 1691552082822,
|
||||||
|
tags: [],
|
||||||
|
updatedDate: 1691654606912,
|
||||||
|
favorite: false,
|
||||||
|
},
|
||||||
|
'6eb43ea8-8c11-456d-bb1d-5193937961ab': {
|
||||||
|
createDate: 1691552090989,
|
||||||
|
tags: [],
|
||||||
|
updatedDate: 1691646748171,
|
||||||
|
favorite: false,
|
||||||
|
},
|
||||||
|
'3ddc8a4f-62c7-4fd4-8064-9ed9f61e437a': {
|
||||||
|
createDate: 1691564303138,
|
||||||
|
tags: [],
|
||||||
|
updatedDate: 1691646845195,
|
||||||
|
},
|
||||||
|
'512b1cb3-d22d-4b20-a7aa-58e2afcb1238': {
|
||||||
|
createDate: 1691574743531,
|
||||||
|
tags: ['icg1n5UdkP'],
|
||||||
|
updatedDate: 1691647117761,
|
||||||
|
},
|
||||||
|
'22163830-8252-43fe-b62d-fd9bbeaa4caa': {
|
||||||
|
createDate: 1691574859042,
|
||||||
|
tags: [],
|
||||||
|
updatedDate: 1691648159371,
|
||||||
|
},
|
||||||
|
'b7a9e1bc-e205-44aa-8dad-7e328269d00b': {
|
||||||
|
createDate: 1691575011078,
|
||||||
|
tags: ['8qcYPCTK0h'],
|
||||||
|
updatedDate: 1691645074511,
|
||||||
|
favorite: false,
|
||||||
|
},
|
||||||
|
'646305d9-93e0-48df-bb92-d82944ceb5a3': {
|
||||||
|
createDate: 1691634722239,
|
||||||
|
tags: ['ze07JVwBu4'],
|
||||||
|
updatedDate: 1691647069662,
|
||||||
|
favorite: false,
|
||||||
|
},
|
||||||
|
'0350509d-8702-4797-b4d7-168f5e9359c7': {
|
||||||
|
createDate: 1691635388447,
|
||||||
|
tags: ['Oe5dSe1DDJ'],
|
||||||
|
updatedDate: 1691645873930,
|
||||||
|
},
|
||||||
|
'aa02af3c-5c5c-4856-b7ce-947ad17331f3': {
|
||||||
|
createDate: 1691636192263,
|
||||||
|
tags: ['q3mceOl_zi', 'g1L5dXKctL'],
|
||||||
|
updatedDate: 1691645102104,
|
||||||
|
},
|
||||||
|
'9d6e716e-a071-45a2-88ac-2f2f6eec0109': {
|
||||||
|
createDate: 1691574743531,
|
||||||
|
tags: ['icg1n5UdkP'],
|
||||||
|
updatedDate: 1691574743531,
|
||||||
|
},
|
||||||
|
} satisfies Record<string, Partial<PageMeta>>;
|
||||||
|
const data = [
|
||||||
|
[
|
||||||
|
'9f6f3c04-cf32-470c-9648-479dc838f10e',
|
||||||
|
import('@affine/templates/v1/getting-started.json'),
|
||||||
|
nanoid(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'0773e198-5de0-45d4-a35e-de22ea72b96b',
|
||||||
|
import('@affine/templates/v1/preloading.json'),
|
||||||
|
edgelessPage1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'59b140eb-4449-488f-9eeb-42412dcc044e',
|
||||||
|
import('@affine/templates/v1/template-galleries.json'),
|
||||||
|
nanoid(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'7217fbe2-61db-4a91-93c6-ad5c800e5a43',
|
||||||
|
import('@affine/templates/v1/personal-home.json'),
|
||||||
|
nanoid(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'6eb43ea8-8c11-456d-bb1d-5193937961ab',
|
||||||
|
import('@affine/templates/v1/working-home.json'),
|
||||||
|
nanoid(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'3ddc8a4f-62c7-4fd4-8064-9ed9f61e437a',
|
||||||
|
import('@affine/templates/v1/personal-project-management.json'),
|
||||||
|
nanoid(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'512b1cb3-d22d-4b20-a7aa-58e2afcb1238',
|
||||||
|
import('@affine/templates/v1/travel-plan.json'),
|
||||||
|
edgelessPage2,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'22163830-8252-43fe-b62d-fd9bbeaa4caa',
|
||||||
|
import('@affine/templates/v1/personal-knowledge-management.json'),
|
||||||
|
nanoid(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'b7a9e1bc-e205-44aa-8dad-7e328269d00b',
|
||||||
|
import('@affine/templates/v1/annual-performance-review.json'),
|
||||||
|
nanoid(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'646305d9-93e0-48df-bb92-d82944ceb5a3',
|
||||||
|
import('@affine/templates/v1/brief-event-planning.json'),
|
||||||
|
nanoid(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'0350509d-8702-4797-b4d7-168f5e9359c7',
|
||||||
|
import('@affine/templates/v1/meeting-summary.json'),
|
||||||
|
nanoid(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'aa02af3c-5c5c-4856-b7ce-947ad17331f3',
|
||||||
|
import('@affine/templates/v1/okr-template.json'),
|
||||||
|
nanoid(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'9d6e716e-a071-45a2-88ac-2f2f6eec0109',
|
||||||
|
import('@affine/templates/v1/travel-note.json'),
|
||||||
|
edgelessPage3,
|
||||||
|
],
|
||||||
|
] as const;
|
||||||
|
const idMap = await Promise.all(data).then(async data => {
|
||||||
|
return data.reduce<Record<string, string>>(
|
||||||
|
(record, currentValue) => {
|
||||||
|
const [oldId, _, newId] = currentValue;
|
||||||
|
record[oldId] = newId;
|
||||||
|
return record;
|
||||||
|
},
|
||||||
|
{} as Record<string, string>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await Promise.all(
|
||||||
|
data.map(async ([id, promise, newId]) => {
|
||||||
|
const { default: template } = await promise;
|
||||||
|
let json = JSON.stringify(template);
|
||||||
|
Object.entries(idMap).forEach(([oldId, newId]) => {
|
||||||
|
json = json.replaceAll(oldId, newId);
|
||||||
|
});
|
||||||
|
json = JSON.parse(json);
|
||||||
|
await workspace
|
||||||
|
.importPageSnapshot(structuredClone(json), newId)
|
||||||
|
.catch(error => {
|
||||||
|
console.error('error importing page', id, error);
|
||||||
|
});
|
||||||
|
const page = workspace.getPage(newId);
|
||||||
|
assertExists(page);
|
||||||
|
await page.load();
|
||||||
|
workspace.schema.upgradePage(
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
'affine:note': 1,
|
||||||
|
'affine:bookmark': 1,
|
||||||
|
'affine:database': 2,
|
||||||
|
'affine:divider': 1,
|
||||||
|
'affine:image': 1,
|
||||||
|
'affine:list': 1,
|
||||||
|
'affine:code': 1,
|
||||||
|
'affine:page': 2,
|
||||||
|
'affine:paragraph': 1,
|
||||||
|
'affine:surface': 3,
|
||||||
|
},
|
||||||
|
page.spaceDoc
|
||||||
|
);
|
||||||
|
|
||||||
|
// The showcase building will create multiple pages once, and may skip the version writing.
|
||||||
|
// https://github.com/toeverything/blocksuite/blob/master/packages/store/src/workspace/page.ts#L662
|
||||||
|
if (!workspace.meta.blockVersions) {
|
||||||
|
await migratePages(workspace.doc, workspace.schema);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
Object.entries(pageMetas).forEach(([oldId, meta]) => {
|
||||||
|
const newId = idMap[oldId];
|
||||||
|
workspace.setPageMeta(newId, meta);
|
||||||
|
});
|
||||||
|
}
|
||||||
15
packages/common/infra/src/blocksuite/migration/blob.ts
Normal file
15
packages/common/infra/src/blocksuite/migration/blob.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { createIndexeddbStorage } from '@blocksuite/store';
|
||||||
|
|
||||||
|
export async function migrateLocalBlobStorage(from: string, to: string) {
|
||||||
|
const fromStorage = createIndexeddbStorage(from);
|
||||||
|
const toStorage = createIndexeddbStorage(to);
|
||||||
|
const keys = await fromStorage.crud.list();
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = await fromStorage.crud.get(key);
|
||||||
|
if (!value) {
|
||||||
|
console.warn('cannot find blob:', key);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
await toStorage.crud.set(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
36
packages/common/infra/src/blocksuite/migration/blocksuite.ts
Normal file
36
packages/common/infra/src/blocksuite/migration/blocksuite.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import type { Schema } from '@blocksuite/store';
|
||||||
|
import type { Doc as YDoc } from 'yjs';
|
||||||
|
import { Map as YMap } from 'yjs';
|
||||||
|
|
||||||
|
const getLatestVersions = (schema: Schema): Record<string, number> => {
|
||||||
|
return [...schema.flavourSchemaMap.entries()].reduce(
|
||||||
|
(record, [flavour, schema]) => {
|
||||||
|
record[flavour] = schema.version;
|
||||||
|
return record;
|
||||||
|
},
|
||||||
|
{} as Record<string, number>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function migratePages(
|
||||||
|
rootDoc: YDoc,
|
||||||
|
schema: Schema
|
||||||
|
): Promise<boolean> {
|
||||||
|
const spaces = rootDoc.getMap('spaces') as YMap<any>;
|
||||||
|
const meta = rootDoc.getMap('meta') as YMap<unknown>;
|
||||||
|
const versions = meta.get('blockVersions') as YMap<number>;
|
||||||
|
const oldVersions = versions?.toJSON() ?? {};
|
||||||
|
spaces.forEach((space: YDoc) => {
|
||||||
|
try {
|
||||||
|
schema.upgradePage(0, oldVersions, space);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`page ${space.guid} upgrade failed`, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const newVersions = getLatestVersions(schema);
|
||||||
|
meta.set('blockVersions', new YMap(Object.entries(newVersions)));
|
||||||
|
return Object.entries(oldVersions).some(
|
||||||
|
([flavour, version]) => newVersions[flavour] !== version
|
||||||
|
);
|
||||||
|
}
|
||||||
45
packages/common/infra/src/blocksuite/migration/fixing.ts
Normal file
45
packages/common/infra/src/blocksuite/migration/fixing.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import type { Array as YArray, Map as YMap } from 'yjs';
|
||||||
|
import { Doc as YDoc, transact } from 'yjs';
|
||||||
|
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
|
||||||
|
|
||||||
|
// patch root doc's space guid compatibility issue
|
||||||
|
//
|
||||||
|
// in version 0.10, page id in spaces no longer has prefix "space:"
|
||||||
|
// The data flow for fetching a doc's updates is:
|
||||||
|
// - page id in `meta.pages` -> find `${page-id}` in `doc.spaces` -> `doc` -> `doc.guid`
|
||||||
|
// if `doc` is not found in `doc.spaces`, a new doc will be created and its `doc.guid` is the same with its pageId
|
||||||
|
// - because of guid logic change, the doc that previously prefixed with "space:" will not be found in `doc.spaces`
|
||||||
|
// - when fetching the rows of this doc using the doc id === page id,
|
||||||
|
// it will return empty since there is no updates associated with the page id
|
||||||
|
export function guidCompatibilityFix(rootDoc: YDoc) {
|
||||||
|
let changed = false;
|
||||||
|
transact(rootDoc, () => {
|
||||||
|
const meta = rootDoc.getMap('meta') as YMap<unknown>;
|
||||||
|
const pages = meta.get('pages') as YArray<YMap<unknown>>;
|
||||||
|
pages?.forEach(page => {
|
||||||
|
const pageId = page.get('id') as string | undefined;
|
||||||
|
if (pageId?.includes(':')) {
|
||||||
|
// remove the prefix "space:" from page id
|
||||||
|
page.set('id', pageId.split(':').at(-1));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const spaces = rootDoc.getMap('spaces') as YMap<YDoc>;
|
||||||
|
spaces?.forEach((doc: YDoc, pageId: string) => {
|
||||||
|
if (pageId.includes(':')) {
|
||||||
|
const newPageId = pageId.split(':').at(-1) ?? pageId;
|
||||||
|
const newDoc = new YDoc();
|
||||||
|
// clone the original doc. yjs is not happy to use the same doc instance
|
||||||
|
applyUpdate(newDoc, encodeStateAsUpdate(doc));
|
||||||
|
newDoc.guid = doc.guid;
|
||||||
|
spaces.set(newPageId, newDoc);
|
||||||
|
// should remove the old doc, otherwise we will do it again in the next run
|
||||||
|
spaces.delete(pageId);
|
||||||
|
changed = true;
|
||||||
|
console.debug(
|
||||||
|
`fixed space id ${pageId} -> ${newPageId}, doc id: ${doc.guid}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
281
packages/common/infra/src/blocksuite/migration/subdoc.ts
Normal file
281
packages/common/infra/src/blocksuite/migration/subdoc.ts
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
import type { Workspace } from '@blocksuite/store';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
import { Array as YArray, Doc as YDoc, Map as YMap } from 'yjs';
|
||||||
|
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
|
||||||
|
|
||||||
|
const migrationOrigin = 'affine-migration';
|
||||||
|
|
||||||
|
type XYWH = [number, number, number, number];
|
||||||
|
|
||||||
|
function deserializeXYWH(xywh: string): XYWH {
|
||||||
|
return JSON.parse(xywh) as XYWH;
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrateDatabase(data: YMap<unknown>) {
|
||||||
|
data.delete('prop:mode');
|
||||||
|
data.set('prop:views', new YArray());
|
||||||
|
const columns = (data.get('prop:columns') as YArray<unknown>).toJSON() as {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
hide: boolean;
|
||||||
|
type: string;
|
||||||
|
width: number;
|
||||||
|
selection?: unknown[];
|
||||||
|
}[];
|
||||||
|
const views = [
|
||||||
|
{
|
||||||
|
id: 'default',
|
||||||
|
name: 'Table',
|
||||||
|
columns: columns.map(col => ({
|
||||||
|
id: col.id,
|
||||||
|
width: col.width,
|
||||||
|
hide: col.hide,
|
||||||
|
})),
|
||||||
|
filter: { type: 'group', op: 'and', conditions: [] },
|
||||||
|
mode: 'table',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const cells = (data.get('prop:cells') as YMap<unknown>).toJSON() as Record<
|
||||||
|
string,
|
||||||
|
Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
id: string;
|
||||||
|
value: unknown;
|
||||||
|
}
|
||||||
|
>
|
||||||
|
>;
|
||||||
|
const convertColumn = (
|
||||||
|
id: string,
|
||||||
|
update: (cell: { id: string; value: unknown }) => void
|
||||||
|
) => {
|
||||||
|
Object.values(cells).forEach(row => {
|
||||||
|
if (row[id] != null) {
|
||||||
|
update(row[id]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const newColumns = columns.map(v => {
|
||||||
|
let data: Record<string, unknown> = {};
|
||||||
|
if (v.type === 'select' || v.type === 'multi-select') {
|
||||||
|
data = { options: v.selection };
|
||||||
|
if (v.type === 'select') {
|
||||||
|
convertColumn(v.id, cell => {
|
||||||
|
if (Array.isArray(cell.value)) {
|
||||||
|
cell.value = cell.value[0]?.id;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
convertColumn(v.id, cell => {
|
||||||
|
if (Array.isArray(cell.value)) {
|
||||||
|
cell.value = cell.value.map(v => v.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (v.type === 'number') {
|
||||||
|
convertColumn(v.id, cell => {
|
||||||
|
if (typeof cell.value === 'string') {
|
||||||
|
cell.value = Number.parseFloat(cell.value.toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: v.id,
|
||||||
|
type: v.type,
|
||||||
|
name: v.name,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
data.set('prop:columns', newColumns);
|
||||||
|
data.set('prop:views', views);
|
||||||
|
data.set('prop:cells', cells);
|
||||||
|
}
|
||||||
|
|
||||||
|
function runBlockMigration(
|
||||||
|
flavour: string,
|
||||||
|
data: YMap<unknown>,
|
||||||
|
version: number
|
||||||
|
) {
|
||||||
|
if (flavour === 'affine:frame') {
|
||||||
|
data.set('sys:flavour', 'affine:note');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (flavour === 'affine:surface' && version <= 3) {
|
||||||
|
if (data.has('elements')) {
|
||||||
|
const elements = data.get('elements') as YMap<unknown>;
|
||||||
|
migrateSurface(elements);
|
||||||
|
data.set('prop:elements', elements.clone());
|
||||||
|
data.delete('elements');
|
||||||
|
} else {
|
||||||
|
data.set('prop:elements', new YMap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (flavour === 'affine:embed') {
|
||||||
|
data.set('sys:flavour', 'affine:image');
|
||||||
|
data.delete('prop:type');
|
||||||
|
}
|
||||||
|
if (flavour === 'affine:database' && version < 2) {
|
||||||
|
migrateDatabase(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrateSurface(data: YMap<unknown>) {
|
||||||
|
for (const [, value] of <IterableIterator<[string, YMap<unknown>]>>(
|
||||||
|
data.entries()
|
||||||
|
)) {
|
||||||
|
if (value.get('type') === 'connector') {
|
||||||
|
migrateSurfaceConnector(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrateSurfaceConnector(data: YMap<any>) {
|
||||||
|
let id = data.get('startElement')?.id;
|
||||||
|
const controllers = data.get('controllers');
|
||||||
|
const length = controllers.length;
|
||||||
|
const xywh = deserializeXYWH(data.get('xywh'));
|
||||||
|
if (id) {
|
||||||
|
data.set('source', { id });
|
||||||
|
} else {
|
||||||
|
data.set('source', {
|
||||||
|
position: [controllers[0].x + xywh[0], controllers[0].y + xywh[1]],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
id = data.get('endElement')?.id;
|
||||||
|
if (id) {
|
||||||
|
data.set('target', { id });
|
||||||
|
} else {
|
||||||
|
data.set('target', {
|
||||||
|
position: [
|
||||||
|
controllers[length - 1].x + xywh[0],
|
||||||
|
controllers[length - 1].y + xywh[1],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const width = data.get('lineWidth') ?? 4;
|
||||||
|
data.set('strokeWidth', width);
|
||||||
|
const color = data.get('color');
|
||||||
|
data.set('stroke', color);
|
||||||
|
|
||||||
|
data.delete('startElement');
|
||||||
|
data.delete('endElement');
|
||||||
|
data.delete('controllers');
|
||||||
|
data.delete('lineWidth');
|
||||||
|
data.delete('color');
|
||||||
|
data.delete('xywh');
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateBlockVersions(versions: YMap<number>) {
|
||||||
|
const frameVersion = versions.get('affine:frame');
|
||||||
|
if (frameVersion !== undefined) {
|
||||||
|
versions.set('affine:note', frameVersion);
|
||||||
|
versions.delete('affine:frame');
|
||||||
|
}
|
||||||
|
const embedVersion = versions.get('affine:embed');
|
||||||
|
if (embedVersion !== undefined) {
|
||||||
|
versions.set('affine:image', embedVersion);
|
||||||
|
versions.delete('affine:embed');
|
||||||
|
}
|
||||||
|
const databaseVersion = versions.get('affine:database');
|
||||||
|
if (databaseVersion !== undefined && databaseVersion < 2) {
|
||||||
|
versions.set('affine:database', 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrateMeta(
|
||||||
|
oldDoc: YDoc,
|
||||||
|
newDoc: YDoc,
|
||||||
|
idMap: Record<string, string>
|
||||||
|
) {
|
||||||
|
const originalMeta = oldDoc.getMap('space:meta');
|
||||||
|
const originalVersions = originalMeta.get('versions') as YMap<number>;
|
||||||
|
const originalPages = originalMeta.get('pages') as YArray<YMap<unknown>>;
|
||||||
|
const meta = newDoc.getMap('meta');
|
||||||
|
const pages = new YArray();
|
||||||
|
const blockVersions = originalVersions.clone();
|
||||||
|
|
||||||
|
meta.set('workspaceVersion', 1);
|
||||||
|
meta.set('blockVersions', blockVersions);
|
||||||
|
meta.set('pages', pages);
|
||||||
|
meta.set('name', originalMeta.get('name') as string);
|
||||||
|
|
||||||
|
updateBlockVersions(blockVersions);
|
||||||
|
const mapList = originalPages.map(page => {
|
||||||
|
const map = new YMap();
|
||||||
|
Array.from(page.entries())
|
||||||
|
.filter(([key]) => key !== 'subpageIds')
|
||||||
|
.forEach(([key, value]) => {
|
||||||
|
if (key === 'id') {
|
||||||
|
idMap[value] = nanoid();
|
||||||
|
map.set(key, idMap[value]);
|
||||||
|
} else {
|
||||||
|
map.set(key, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
});
|
||||||
|
pages.push(mapList);
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrateBlocks(
|
||||||
|
oldDoc: YDoc,
|
||||||
|
newDoc: YDoc,
|
||||||
|
idMap: Record<string, string>
|
||||||
|
) {
|
||||||
|
const spaces = newDoc.getMap('spaces');
|
||||||
|
const originalMeta = oldDoc.getMap('space:meta');
|
||||||
|
const originalVersions = originalMeta.get('versions') as YMap<number>;
|
||||||
|
const originalPages = originalMeta.get('pages') as YArray<YMap<unknown>>;
|
||||||
|
originalPages.forEach(page => {
|
||||||
|
const id = page.get('id') as string;
|
||||||
|
const newId = idMap[id];
|
||||||
|
const spaceId = id.startsWith('space:') ? id : `space:${id}`;
|
||||||
|
const originalBlocks = oldDoc.getMap(spaceId) as YMap<unknown>;
|
||||||
|
const subdoc = new YDoc();
|
||||||
|
spaces.set(newId, subdoc);
|
||||||
|
subdoc.guid = id;
|
||||||
|
const blocks = subdoc.getMap('blocks');
|
||||||
|
Array.from(originalBlocks.entries()).forEach(([key, value]) => {
|
||||||
|
const blockData = value.clone();
|
||||||
|
blocks.set(key, blockData);
|
||||||
|
const flavour = blockData.get('sys:flavour') as string;
|
||||||
|
const version = originalVersions.get(flavour);
|
||||||
|
if (version !== undefined) {
|
||||||
|
runBlockMigration(flavour, blockData, version);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function migrateToSubdoc(oldDoc: YDoc): YDoc {
|
||||||
|
const needMigration =
|
||||||
|
Array.from(oldDoc.getMap('space:meta').keys()).length > 0;
|
||||||
|
if (!needMigration) {
|
||||||
|
return oldDoc;
|
||||||
|
}
|
||||||
|
const newDoc = new YDoc();
|
||||||
|
const idMap = {} as Record<string, string>;
|
||||||
|
migrateMeta(oldDoc, newDoc, idMap);
|
||||||
|
migrateBlocks(oldDoc, newDoc, idMap);
|
||||||
|
return newDoc;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const upgradeV1ToV2 = async (
|
||||||
|
oldDoc: YDoc,
|
||||||
|
createWorkspace: () => Promise<Workspace>
|
||||||
|
) => {
|
||||||
|
const newDoc = migrateToSubdoc(oldDoc);
|
||||||
|
const newWorkspace = await createWorkspace();
|
||||||
|
applyUpdate(newWorkspace.doc, encodeStateAsUpdate(newDoc), migrationOrigin);
|
||||||
|
newDoc.getSubdocs().forEach(subdoc => {
|
||||||
|
newWorkspace.doc.getSubdocs().forEach(newDoc => {
|
||||||
|
if (subdoc.guid === newDoc.guid) {
|
||||||
|
applyUpdate(newDoc, encodeStateAsUpdate(subdoc), migrationOrigin);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return newWorkspace;
|
||||||
|
};
|
||||||
77
packages/common/infra/src/blocksuite/migration/workspace.ts
Normal file
77
packages/common/infra/src/blocksuite/migration/workspace.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import type { Workspace } from '@blocksuite/store';
|
||||||
|
import type { Schema } from '@blocksuite/store';
|
||||||
|
import type { Doc as YDoc } from 'yjs';
|
||||||
|
|
||||||
|
import { migratePages } from './blocksuite';
|
||||||
|
import { upgradeV1ToV2 } from './subdoc';
|
||||||
|
|
||||||
|
interface MigrationOptions {
|
||||||
|
doc: YDoc;
|
||||||
|
schema: Schema;
|
||||||
|
createWorkspace: () => Promise<Workspace>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMigrationQueue(options: MigrationOptions) {
|
||||||
|
return [
|
||||||
|
async (doc: YDoc) => {
|
||||||
|
const newWorkspace = await upgradeV1ToV2(doc, options.createWorkspace);
|
||||||
|
return newWorkspace.doc;
|
||||||
|
},
|
||||||
|
async (doc: YDoc) => {
|
||||||
|
await migratePages(doc, options.schema);
|
||||||
|
return doc;
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For split migrate function from MigrationQueue.
|
||||||
|
*/
|
||||||
|
export enum MigrationPoint {
|
||||||
|
SubDoc = 1,
|
||||||
|
BlockVersion = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function migrateWorkspace(
|
||||||
|
point: MigrationPoint,
|
||||||
|
options: MigrationOptions
|
||||||
|
) {
|
||||||
|
const migrationQueue = createMigrationQueue(options);
|
||||||
|
const migrationFns = migrationQueue.slice(point - 1);
|
||||||
|
|
||||||
|
let doc = options.doc;
|
||||||
|
for (const migrate of migrationFns) {
|
||||||
|
doc = await migrate(doc);
|
||||||
|
}
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkWorkspaceCompatibility(
|
||||||
|
workspace: Workspace
|
||||||
|
): MigrationPoint | null {
|
||||||
|
const workspaceDocJSON = workspace.doc.toJSON();
|
||||||
|
const spaceMetaObj = workspaceDocJSON['space:meta'];
|
||||||
|
const docKeys = Object.keys(workspaceDocJSON);
|
||||||
|
const haveSpaceMeta = !!spaceMetaObj && Object.keys(spaceMetaObj).length > 0;
|
||||||
|
const haveLegacySpace = docKeys.some(key => key.startsWith('space:'));
|
||||||
|
if (haveSpaceMeta || haveLegacySpace) {
|
||||||
|
return MigrationPoint.SubDoc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sometimes, blocksuite will not write blockVersions to meta.
|
||||||
|
// Just fix it when user open the workspace.
|
||||||
|
const blockVersions = workspace.meta.blockVersions;
|
||||||
|
if (!blockVersions) {
|
||||||
|
return MigrationPoint.BlockVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
// From v2, we depend on blocksuite to check and migrate data.
|
||||||
|
for (const [flavour, version] of Object.entries(blockVersions)) {
|
||||||
|
const schema = workspace.schema.flavourSchemaMap.get(flavour);
|
||||||
|
if (schema?.version !== version) {
|
||||||
|
return MigrationPoint.BlockVersion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -13,10 +13,7 @@ import {
|
|||||||
CRUD,
|
CRUD,
|
||||||
saveWorkspaceToLocalStorage,
|
saveWorkspaceToLocalStorage,
|
||||||
} from '@affine/workspace/local/crud';
|
} from '@affine/workspace/local/crud';
|
||||||
import {
|
import { getOrCreateWorkspace } from '@affine/workspace/manager';
|
||||||
getOrCreateWorkspace,
|
|
||||||
globalBlockSuiteSchema,
|
|
||||||
} from '@affine/workspace/manager';
|
|
||||||
import { getBlockSuiteWorkspaceAtom } from '@toeverything/infra/__internal__/workspace';
|
import { getBlockSuiteWorkspaceAtom } from '@toeverything/infra/__internal__/workspace';
|
||||||
import { getCurrentStore } from '@toeverything/infra/atom';
|
import { getCurrentStore } from '@toeverything/infra/atom';
|
||||||
import { initEmptyPage } from '@toeverything/infra/blocksuite';
|
import { initEmptyPage } from '@toeverything/infra/blocksuite';
|
||||||
@@ -47,7 +44,6 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
|
|||||||
blockSuiteWorkspace.meta.setName(DEFAULT_WORKSPACE_NAME);
|
blockSuiteWorkspace.meta.setName(DEFAULT_WORKSPACE_NAME);
|
||||||
if (runtimeConfig.enablePreloading) {
|
if (runtimeConfig.enablePreloading) {
|
||||||
buildShowcaseWorkspace(blockSuiteWorkspace, {
|
buildShowcaseWorkspace(blockSuiteWorkspace, {
|
||||||
schema: globalBlockSuiteSchema,
|
|
||||||
store: getCurrentStore(),
|
store: getCurrentStore(),
|
||||||
atoms: {
|
atoms: {
|
||||||
pageMode: setPageModeAtom,
|
pageMode: setPageModeAtom,
|
||||||
|
|||||||
@@ -1,129 +1,18 @@
|
|||||||
import { setupGlobal } from '@affine/env/global';
|
import { setupGlobal } from '@affine/env/global';
|
||||||
import type { WorkspaceAdapter } from '@affine/env/workspace';
|
import type { WorkspaceAdapter } from '@affine/env/workspace';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import type { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
|
|
||||||
import {
|
import {
|
||||||
type RootWorkspaceMetadataV2,
|
type RootWorkspaceMetadataV2,
|
||||||
rootWorkspacesMetadataAtom,
|
rootWorkspacesMetadataAtom,
|
||||||
workspaceAdaptersAtom,
|
workspaceAdaptersAtom,
|
||||||
} from '@affine/workspace/atom';
|
} from '@affine/workspace/atom';
|
||||||
import {
|
|
||||||
getOrCreateWorkspace,
|
|
||||||
globalBlockSuiteSchema,
|
|
||||||
} from '@affine/workspace/manager';
|
|
||||||
import { assertExists } from '@blocksuite/global/utils';
|
|
||||||
import {
|
|
||||||
migrateLocalBlobStorage,
|
|
||||||
migrateWorkspace,
|
|
||||||
WorkspaceVersion,
|
|
||||||
} from '@toeverything/infra/blocksuite';
|
|
||||||
import { downloadBinary, overwriteBinary } from '@toeverything/y-indexeddb';
|
|
||||||
import type { createStore } from 'jotai/vanilla';
|
import type { createStore } from 'jotai/vanilla';
|
||||||
import { nanoid } from 'nanoid';
|
|
||||||
import { applyUpdate, Doc as YDoc, encodeStateAsUpdate } from 'yjs';
|
|
||||||
|
|
||||||
import { WorkspaceAdapters } from '../adapters/workspace';
|
import { WorkspaceAdapters } from '../adapters/workspace';
|
||||||
import { performanceLogger } from '../shared';
|
import { performanceLogger } from '../shared';
|
||||||
|
|
||||||
const performanceSetupLogger = performanceLogger.namespace('setup');
|
const performanceSetupLogger = performanceLogger.namespace('setup');
|
||||||
|
|
||||||
async function tryMigration() {
|
|
||||||
const value = localStorage.getItem('jotai-workspaces');
|
|
||||||
if (value) {
|
|
||||||
try {
|
|
||||||
const metadata = JSON.parse(value) as RootWorkspaceMetadata[];
|
|
||||||
const promises: Promise<void>[] = [];
|
|
||||||
const newMetadata = [...metadata];
|
|
||||||
metadata.forEach(oldMeta => {
|
|
||||||
if (oldMeta.flavour === WorkspaceFlavour.LOCAL) {
|
|
||||||
let doc: YDoc;
|
|
||||||
const options = {
|
|
||||||
getCurrentRootDoc: async () => {
|
|
||||||
doc = new YDoc({
|
|
||||||
guid: oldMeta.id,
|
|
||||||
});
|
|
||||||
const downloadWorkspace = async (doc: YDoc): Promise<void> => {
|
|
||||||
const binary = await downloadBinary(doc.guid);
|
|
||||||
if (binary) {
|
|
||||||
applyUpdate(doc, binary);
|
|
||||||
}
|
|
||||||
await Promise.all(
|
|
||||||
[...doc.subdocs.values()].map(subdoc =>
|
|
||||||
downloadWorkspace(subdoc)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
await downloadWorkspace(doc);
|
|
||||||
return doc;
|
|
||||||
},
|
|
||||||
createWorkspace: async () =>
|
|
||||||
getOrCreateWorkspace(nanoid(), WorkspaceFlavour.LOCAL),
|
|
||||||
getSchema: () => globalBlockSuiteSchema,
|
|
||||||
};
|
|
||||||
promises.push(
|
|
||||||
migrateWorkspace(
|
|
||||||
'version' in oldMeta ? oldMeta.version : undefined,
|
|
||||||
options
|
|
||||||
).then(async status => {
|
|
||||||
if (typeof status !== 'boolean') {
|
|
||||||
const adapter = WorkspaceAdapters[oldMeta.flavour];
|
|
||||||
const oldWorkspace = await adapter.CRUD.get(oldMeta.id);
|
|
||||||
const newId = await adapter.CRUD.create(status);
|
|
||||||
assertExists(
|
|
||||||
oldWorkspace,
|
|
||||||
'workspace should exist after migrate'
|
|
||||||
);
|
|
||||||
await adapter.CRUD.delete(oldWorkspace.blockSuiteWorkspace);
|
|
||||||
const index = newMetadata.findIndex(
|
|
||||||
meta => meta.id === oldMeta.id
|
|
||||||
);
|
|
||||||
newMetadata[index] = {
|
|
||||||
...oldMeta,
|
|
||||||
id: newId,
|
|
||||||
version: WorkspaceVersion.Surface,
|
|
||||||
};
|
|
||||||
await migrateLocalBlobStorage(status.id, newId);
|
|
||||||
console.log('workspace migrated', oldMeta.id, newId);
|
|
||||||
} else if (status) {
|
|
||||||
const index = newMetadata.findIndex(
|
|
||||||
meta => meta.id === oldMeta.id
|
|
||||||
);
|
|
||||||
newMetadata[index] = {
|
|
||||||
...oldMeta,
|
|
||||||
version: WorkspaceVersion.Surface,
|
|
||||||
};
|
|
||||||
const overWrite = async (doc: YDoc): Promise<void> => {
|
|
||||||
await overwriteBinary(doc.guid, encodeStateAsUpdate(doc));
|
|
||||||
return Promise.all(
|
|
||||||
[...doc.subdocs.values()].map(subdoc => overWrite(subdoc))
|
|
||||||
).then();
|
|
||||||
};
|
|
||||||
await overWrite(doc);
|
|
||||||
console.log('workspace migrated', oldMeta.id);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all(promises)
|
|
||||||
.then(() => {
|
|
||||||
console.log('migration done');
|
|
||||||
})
|
|
||||||
.catch(e => {
|
|
||||||
console.error('migration failed', e);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
localStorage.setItem('jotai-workspaces', JSON.stringify(newMetadata));
|
|
||||||
window.dispatchEvent(new CustomEvent('migration-done'));
|
|
||||||
window.$migrationDone = true;
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.error('error when migrating data', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createFirstAppData(store: ReturnType<typeof createStore>) {
|
export function createFirstAppData(store: ReturnType<typeof createStore>) {
|
||||||
const createFirst = (): RootWorkspaceMetadataV2[] => {
|
const createFirst = (): RootWorkspaceMetadataV2[] => {
|
||||||
const Plugins = Object.values(WorkspaceAdapters).sort(
|
const Plugins = Object.values(WorkspaceAdapters).sort(
|
||||||
@@ -136,7 +25,6 @@ export function createFirstAppData(store: ReturnType<typeof createStore>) {
|
|||||||
<RootWorkspaceMetadataV2>{
|
<RootWorkspaceMetadataV2>{
|
||||||
id,
|
id,
|
||||||
flavour: Plugin.flavour,
|
flavour: Plugin.flavour,
|
||||||
version: WorkspaceVersion.DatabaseV3,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}).filter((ids): ids is RootWorkspaceMetadataV2 => !!ids);
|
}).filter((ids): ids is RootWorkspaceMetadataV2 => !!ids);
|
||||||
@@ -163,9 +51,6 @@ export async function setup(store: ReturnType<typeof createStore>) {
|
|||||||
performanceSetupLogger.info('setup global');
|
performanceSetupLogger.info('setup global');
|
||||||
setupGlobal();
|
setupGlobal();
|
||||||
|
|
||||||
performanceSetupLogger.info('try migration');
|
|
||||||
await tryMigration();
|
|
||||||
|
|
||||||
performanceSetupLogger.info('get root workspace meta');
|
performanceSetupLogger.info('get root workspace meta');
|
||||||
// do not read `rootWorkspacesMetadataAtom` before migration
|
// do not read `rootWorkspacesMetadataAtom` before migration
|
||||||
await store.get(rootWorkspacesMetadataAtom);
|
await store.get(rootWorkspacesMetadataAtom);
|
||||||
|
|||||||
@@ -1,42 +1,122 @@
|
|||||||
import { forceUpgradePages } from '@toeverything/infra/blocksuite';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { useCallback, useState } from 'react';
|
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
|
||||||
|
import { getOrCreateWorkspace } from '@affine/workspace/manager';
|
||||||
|
import type { Workspace } from '@blocksuite/store';
|
||||||
|
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||||
|
import { getBlockSuiteWorkspaceAtom } from '@toeverything/infra/__internal__/workspace';
|
||||||
|
import { getCurrentStore } from '@toeverything/infra/atom';
|
||||||
|
import type { MigrationPoint } from '@toeverything/infra/blocksuite';
|
||||||
|
import {
|
||||||
|
migrateLocalBlobStorage,
|
||||||
|
migrateWorkspace,
|
||||||
|
} from '@toeverything/infra/blocksuite';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { applyUpdate, Doc as YDoc, encodeStateAsUpdate } from 'yjs';
|
||||||
|
|
||||||
|
import { WorkspaceAdapters } from '../../adapters/workspace';
|
||||||
import { useCurrentSyncEngine } from '../../hooks/current/use-current-sync-engine';
|
import { useCurrentSyncEngine } from '../../hooks/current/use-current-sync-engine';
|
||||||
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
|
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
|
||||||
|
|
||||||
export type UpgradeState = 'pending' | 'upgrading' | 'done' | 'error';
|
export type UpgradeState = 'pending' | 'upgrading' | 'done' | 'error';
|
||||||
|
|
||||||
export function useUpgradeWorkspace() {
|
function applyDoc(target: YDoc, result: YDoc) {
|
||||||
|
applyUpdate(target, encodeStateAsUpdate(result));
|
||||||
|
for (const targetSubDoc of target.subdocs.values()) {
|
||||||
|
const resultSubDocs = Array.from(result.subdocs.values());
|
||||||
|
const resultSubDoc = resultSubDocs.find(
|
||||||
|
item => item.guid === targetSubDoc.guid
|
||||||
|
);
|
||||||
|
if (resultSubDoc) {
|
||||||
|
applyDoc(targetSubDoc, resultSubDoc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpgradeWorkspace(migration: MigrationPoint) {
|
||||||
const [state, setState] = useState<UpgradeState>('pending');
|
const [state, setState] = useState<UpgradeState>('pending');
|
||||||
const [error, setError] = useState<Error | null>(null);
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
const [newWorkspaceId, setNewWorkspaceId] = useState<string | null>(null);
|
||||||
|
|
||||||
const [workspace] = useCurrentWorkspace();
|
const [workspace] = useCurrentWorkspace();
|
||||||
const syncEngine = useCurrentSyncEngine();
|
const syncEngine = useCurrentSyncEngine();
|
||||||
|
const rootStore = getCurrentStore();
|
||||||
|
|
||||||
const upgradeWorkspace = useCallback(() => {
|
const upgradeWorkspace = useAsyncCallback(async () => {
|
||||||
setState('upgrading');
|
setState('upgrading');
|
||||||
setError(null);
|
setError(null);
|
||||||
|
try {
|
||||||
(async () => {
|
// Migration need to wait for root doc and all subdocs loaded.
|
||||||
await syncEngine?.waitForSynced();
|
await syncEngine?.waitForSynced();
|
||||||
await forceUpgradePages({
|
|
||||||
getCurrentRootDoc: async () => workspace.blockSuiteWorkspace.doc,
|
// Clone a new doc to prevent change events.
|
||||||
getSchema: () => workspace.blockSuiteWorkspace.schema,
|
const clonedDoc = new YDoc({
|
||||||
|
guid: workspace.blockSuiteWorkspace.doc.guid,
|
||||||
});
|
});
|
||||||
|
applyDoc(clonedDoc, workspace.blockSuiteWorkspace.doc);
|
||||||
|
const schema = workspace.blockSuiteWorkspace.schema;
|
||||||
|
let newWorkspace: Workspace | null = null;
|
||||||
|
|
||||||
|
const resultDoc = await migrateWorkspace(migration, {
|
||||||
|
doc: clonedDoc,
|
||||||
|
schema,
|
||||||
|
createWorkspace: () => {
|
||||||
|
// Migrate to subdoc version need to create a new workspace.
|
||||||
|
// It will only happened for old local workspace.
|
||||||
|
newWorkspace = getOrCreateWorkspace(nanoid(), WorkspaceFlavour.LOCAL);
|
||||||
|
return Promise.resolve(newWorkspace);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (newWorkspace) {
|
||||||
|
const localMetaString =
|
||||||
|
localStorage.getItem('jotai-workspaces') ?? '[]';
|
||||||
|
const localMetadataList = JSON.parse(
|
||||||
|
localMetaString
|
||||||
|
) as RootWorkspaceMetadata[];
|
||||||
|
const currentLocalMetadata = localMetadataList.find(
|
||||||
|
item => item.id === workspace.id
|
||||||
|
);
|
||||||
|
const flavour = currentLocalMetadata?.flavour ?? WorkspaceFlavour.LOCAL;
|
||||||
|
|
||||||
|
// Legacy logic moved from `setup.ts`.
|
||||||
|
// It works well before, should be refactor or remove in the future.
|
||||||
|
const adapter = WorkspaceAdapters[flavour];
|
||||||
|
const newId = await adapter.CRUD.create(newWorkspace);
|
||||||
|
const [workspaceAtom] = getBlockSuiteWorkspaceAtom(newId);
|
||||||
|
await rootStore.get(workspaceAtom); // Trigger provider sync to persist data.
|
||||||
|
|
||||||
|
await adapter.CRUD.delete(workspace.blockSuiteWorkspace);
|
||||||
|
await migrateLocalBlobStorage(workspace.id, newId);
|
||||||
|
setNewWorkspaceId(newId);
|
||||||
|
|
||||||
|
const index = localMetadataList.findIndex(
|
||||||
|
meta => meta.id === workspace.id
|
||||||
|
);
|
||||||
|
localMetadataList[index] = {
|
||||||
|
...currentLocalMetadata,
|
||||||
|
id: newId,
|
||||||
|
flavour,
|
||||||
|
};
|
||||||
|
localStorage.setItem(
|
||||||
|
'jotai-workspaces',
|
||||||
|
JSON.stringify(localMetadataList)
|
||||||
|
);
|
||||||
|
localStorage.setItem('last_workspace_id', newId);
|
||||||
|
localStorage.removeItem('last_page_id');
|
||||||
|
} else {
|
||||||
|
applyDoc(workspace.blockSuiteWorkspace.doc, resultDoc);
|
||||||
|
}
|
||||||
|
|
||||||
await syncEngine?.waitForSynced();
|
await syncEngine?.waitForSynced();
|
||||||
|
|
||||||
setState('done');
|
setState('done');
|
||||||
})().catch((e: any) => {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
setError(e);
|
setError(e);
|
||||||
setState('error');
|
setState('error');
|
||||||
});
|
}
|
||||||
}, [
|
}, [rootStore, workspace, syncEngine, migration]);
|
||||||
workspace.blockSuiteWorkspace.doc,
|
|
||||||
workspace.blockSuiteWorkspace.schema,
|
|
||||||
syncEngine,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return [state, error, upgradeWorkspace] as const;
|
return [state, error, upgradeWorkspace, newWorkspaceId] as const;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { AffineShapeIcon } from '@affine/component/page-list'; // TODO: import from page-list temporarily, need to defined common svg icon/images management.
|
import { AffineShapeIcon } from '@affine/component/page-list'; // TODO: import from page-list temporarily, need to defined common svg icon/images management.
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { Button } from '@toeverything/components/button';
|
import { Button } from '@toeverything/components/button';
|
||||||
|
import type { MigrationPoint } from '@toeverything/infra/blocksuite';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { pathGenerator } from '../../shared';
|
||||||
import * as styles from './upgrade.css';
|
import * as styles from './upgrade.css';
|
||||||
import { type UpgradeState, useUpgradeWorkspace } from './upgrade-hooks';
|
import { type UpgradeState, useUpgradeWorkspace } from './upgrade-hooks';
|
||||||
import { ArrowCircleIcon, HeartBreakIcon } from './upgrade-icon';
|
import { ArrowCircleIcon, HeartBreakIcon } from './upgrade-icon';
|
||||||
@@ -32,11 +34,18 @@ function UpgradeIcon({ upgradeState }: { upgradeState: UpgradeState }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface WorkspaceUpgradeProps {
|
||||||
|
migration: MigrationPoint;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO: Help info is not implemented yet.
|
* TODO: Help info is not implemented yet.
|
||||||
*/
|
*/
|
||||||
export const WorkspaceUpgrade = function MigrationFallback() {
|
export const WorkspaceUpgrade = function WorkspaceUpgrade(
|
||||||
const [upgradeState, , upgradeWorkspace] = useUpgradeWorkspace();
|
props: WorkspaceUpgradeProps
|
||||||
|
) {
|
||||||
|
const [upgradeState, , upgradeWorkspace, newWorkspaceId] =
|
||||||
|
useUpgradeWorkspace(props.migration);
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
|
|
||||||
const refreshPage = useCallback(() => {
|
const refreshPage = useCallback(() => {
|
||||||
@@ -45,6 +54,12 @@ export const WorkspaceUpgrade = function MigrationFallback() {
|
|||||||
|
|
||||||
const onButtonClick = useMemo(() => {
|
const onButtonClick = useMemo(() => {
|
||||||
if (upgradeState === 'done') {
|
if (upgradeState === 'done') {
|
||||||
|
if (newWorkspaceId) {
|
||||||
|
return () => {
|
||||||
|
window.location.replace(pathGenerator.all(newWorkspaceId));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return refreshPage;
|
return refreshPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +68,7 @@ export const WorkspaceUpgrade = function MigrationFallback() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}, [upgradeState, upgradeWorkspace, refreshPage]);
|
}, [upgradeState, upgradeWorkspace, refreshPage, newWorkspaceId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.layout}>
|
<div className={styles.layout}>
|
||||||
|
|||||||
@@ -2,10 +2,7 @@ import { DebugLogger } from '@affine/debug';
|
|||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||||
import { saveWorkspaceToLocalStorage } from '@affine/workspace/local/crud';
|
import { saveWorkspaceToLocalStorage } from '@affine/workspace/local/crud';
|
||||||
import {
|
import { getOrCreateWorkspace } from '@affine/workspace/manager';
|
||||||
getOrCreateWorkspace,
|
|
||||||
globalBlockSuiteSchema,
|
|
||||||
} from '@affine/workspace/manager';
|
|
||||||
import { getWorkspace } from '@toeverything/infra/__internal__/workspace';
|
import { getWorkspace } from '@toeverything/infra/__internal__/workspace';
|
||||||
import { getCurrentStore } from '@toeverything/infra/atom';
|
import { getCurrentStore } from '@toeverything/infra/atom';
|
||||||
import {
|
import {
|
||||||
@@ -76,7 +73,6 @@ export function useAppHelper() {
|
|||||||
WorkspaceFlavour.LOCAL
|
WorkspaceFlavour.LOCAL
|
||||||
);
|
);
|
||||||
await buildShowcaseWorkspace(blockSuiteWorkspace, {
|
await buildShowcaseWorkspace(blockSuiteWorkspace, {
|
||||||
schema: globalBlockSuiteSchema,
|
|
||||||
store: getCurrentStore(),
|
store: getCurrentStore(),
|
||||||
atoms: {
|
atoms: {
|
||||||
pageMode: setPageModeAtom,
|
pageMode: setPageModeAtom,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
} from '@dnd-kit/core';
|
} from '@dnd-kit/core';
|
||||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||||
import { currentWorkspaceIdAtom } from '@toeverything/infra/atom';
|
import { currentWorkspaceIdAtom } from '@toeverything/infra/atom';
|
||||||
|
import type { MigrationPoint } from '@toeverything/infra/blocksuite';
|
||||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
||||||
import type { PropsWithChildren, ReactNode } from 'react';
|
import type { PropsWithChildren, ReactNode } from 'react';
|
||||||
import { lazy, Suspense, useCallback, useEffect, useState } from 'react';
|
import { lazy, Suspense, useCallback, useEffect, useState } from 'react';
|
||||||
@@ -112,12 +113,12 @@ export const CurrentWorkspaceContext = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
type WorkspaceLayoutProps = {
|
type WorkspaceLayoutProps = {
|
||||||
incompatible?: boolean;
|
migration?: MigrationPoint;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WorkspaceLayout = function WorkspacesSuspense({
|
export const WorkspaceLayout = function WorkspacesSuspense({
|
||||||
children,
|
children,
|
||||||
incompatible = false,
|
migration,
|
||||||
}: PropsWithChildren<WorkspaceLayoutProps>) {
|
}: PropsWithChildren<WorkspaceLayoutProps>) {
|
||||||
return (
|
return (
|
||||||
<AdapterProviderWrapper>
|
<AdapterProviderWrapper>
|
||||||
@@ -128,7 +129,7 @@ export const WorkspaceLayout = function WorkspacesSuspense({
|
|||||||
<CurrentWorkspaceModals />
|
<CurrentWorkspaceModals />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<Suspense fallback={<WorkspaceFallback />}>
|
<Suspense fallback={<WorkspaceFallback />}>
|
||||||
<WorkspaceLayoutInner incompatible={incompatible}>
|
<WorkspaceLayoutInner migration={migration}>
|
||||||
{children}
|
{children}
|
||||||
</WorkspaceLayoutInner>
|
</WorkspaceLayoutInner>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
@@ -139,7 +140,7 @@ export const WorkspaceLayout = function WorkspacesSuspense({
|
|||||||
|
|
||||||
export const WorkspaceLayoutInner = ({
|
export const WorkspaceLayoutInner = ({
|
||||||
children,
|
children,
|
||||||
incompatible = false,
|
migration,
|
||||||
}: PropsWithChildren<WorkspaceLayoutProps>) => {
|
}: PropsWithChildren<WorkspaceLayoutProps>) => {
|
||||||
const [currentWorkspace] = useCurrentWorkspace();
|
const [currentWorkspace] = useCurrentWorkspace();
|
||||||
const { openPage } = useNavigateHelper();
|
const { openPage } = useNavigateHelper();
|
||||||
@@ -262,7 +263,11 @@ export const WorkspaceLayoutInner = ({
|
|||||||
padding={appSettings.clientBorder}
|
padding={appSettings.clientBorder}
|
||||||
inTrashPage={inTrashPage}
|
inTrashPage={inTrashPage}
|
||||||
>
|
>
|
||||||
{incompatible ? <WorkspaceUpgrade /> : children}
|
{migration ? (
|
||||||
|
<WorkspaceUpgrade migration={migration} />
|
||||||
|
) : (
|
||||||
|
children
|
||||||
|
)}
|
||||||
<ToolContainer inTrashPage={inTrashPage}>
|
<ToolContainer inTrashPage={inTrashPage}>
|
||||||
<RootBlockHub />
|
<RootBlockHub />
|
||||||
<HelpIsland showList={pageId ? undefined : showList} />
|
<HelpIsland showList={pageId ? undefined : showList} />
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||||
import { getBlockSuiteWorkspaceAtom } from '@toeverything/infra/__internal__/workspace';
|
import { getBlockSuiteWorkspaceAtom } from '@toeverything/infra/__internal__/workspace';
|
||||||
import {
|
import {
|
||||||
@@ -6,7 +5,11 @@ import {
|
|||||||
currentWorkspaceIdAtom,
|
currentWorkspaceIdAtom,
|
||||||
getCurrentStore,
|
getCurrentStore,
|
||||||
} from '@toeverything/infra/atom';
|
} from '@toeverything/infra/atom';
|
||||||
import { guidCompatibilityFix } from '@toeverything/infra/blocksuite';
|
import type { MigrationPoint } from '@toeverything/infra/blocksuite';
|
||||||
|
import {
|
||||||
|
checkWorkspaceCompatibility,
|
||||||
|
guidCompatibilityFix,
|
||||||
|
} from '@toeverything/infra/blocksuite';
|
||||||
import { useSetAtom } from 'jotai';
|
import { useSetAtom } from 'jotai';
|
||||||
import { type ReactElement, useEffect } from 'react';
|
import { type ReactElement, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
@@ -49,22 +52,9 @@ export const loader: LoaderFunction = async args => {
|
|||||||
|
|
||||||
const workspace = await rootStore.get(workspaceAtom);
|
const workspace = await rootStore.get(workspaceAtom);
|
||||||
workspaceLoaderLogger.info('workspace loaded');
|
workspaceLoaderLogger.info('workspace loaded');
|
||||||
if (currentMetadata.flavour === WorkspaceFlavour.AFFINE_CLOUD) {
|
|
||||||
return (() => {
|
guidCompatibilityFix(workspace.doc);
|
||||||
guidCompatibilityFix(workspace.doc);
|
return checkWorkspaceCompatibility(workspace);
|
||||||
const blockVersions = workspace.meta.blockVersions;
|
|
||||||
if (!blockVersions) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
for (const [flavour, schema] of workspace.schema.flavourSchemaMap) {
|
|
||||||
if (blockVersions[flavour] !== schema.version) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Component = (): ReactElement => {
|
export const Component = (): ReactElement => {
|
||||||
@@ -81,10 +71,10 @@ export const Component = (): ReactElement => {
|
|||||||
}
|
}
|
||||||
}, [params, setCurrentWorkspaceId]);
|
}, [params, setCurrentWorkspaceId]);
|
||||||
|
|
||||||
const incompatible = useLoaderData();
|
const migration = useLoaderData() as MigrationPoint | undefined;
|
||||||
return (
|
return (
|
||||||
<AffineErrorBoundary height="100vh">
|
<AffineErrorBoundary height="100vh">
|
||||||
<WorkspaceLayout incompatible={!!incompatible}>
|
<WorkspaceLayout migration={migration}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</WorkspaceLayout>
|
</WorkspaceLayout>
|
||||||
</AffineErrorBoundary>
|
</AffineErrorBoundary>
|
||||||
|
|||||||
@@ -77,10 +77,7 @@ export const migrateToLatest = async (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
await downloadBinary(rootDoc, true);
|
await downloadBinary(rootDoc, true);
|
||||||
const result = await forceUpgradePages({
|
const result = await forceUpgradePages(rootDoc, schema);
|
||||||
getSchema: () => schema,
|
|
||||||
getCurrentRootDoc: () => Promise.resolve(rootDoc),
|
|
||||||
});
|
|
||||||
equal(result, true, 'migrateWorkspace should return boolean value');
|
equal(result, true, 'migrateWorkspace should return boolean value');
|
||||||
const uploadBinary = async (doc: YDoc, isRoot: boolean) => {
|
const uploadBinary = async (doc: YDoc, isRoot: boolean) => {
|
||||||
await connection.replaceUpdates(doc.guid, [
|
await connection.replaceUpdates(doc.guid, [
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ export type AsyncErrorHandler = (error: Error) => void;
|
|||||||
/**
|
/**
|
||||||
* App should provide a global error handler for async callback in the root.
|
* App should provide a global error handler for async callback in the root.
|
||||||
*/
|
*/
|
||||||
export const AsyncCallbackContext = React.createContext<AsyncErrorHandler>(e =>
|
export const AsyncCallbackContext = React.createContext<AsyncErrorHandler>(
|
||||||
console.error(e)
|
e => {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -74,15 +74,12 @@ const rootWorkspacesMetadataPrimitiveAtom = atom<Promise<
|
|||||||
|
|
||||||
type Getter = <Value>(atom: Atom<Value>) => Value;
|
type Getter = <Value>(atom: Atom<Value>) => Value;
|
||||||
|
|
||||||
type FetchMetadata = (
|
type FetchMetadata = (get: Getter) => Promise<RootWorkspaceMetadata[]>;
|
||||||
get: Getter,
|
|
||||||
options: { signal: AbortSignal }
|
|
||||||
) => Promise<RootWorkspaceMetadata[]>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
const fetchMetadata: FetchMetadata = async (get, { signal }) => {
|
const fetchMetadata: FetchMetadata = async get => {
|
||||||
performanceJotaiLogger.info('fetch metadata start');
|
performanceJotaiLogger.info('fetch metadata start');
|
||||||
|
|
||||||
const WorkspaceAdapters = get(workspaceAdaptersAtom);
|
const WorkspaceAdapters = get(workspaceAdaptersAtom);
|
||||||
@@ -111,23 +108,6 @@ const fetchMetadata: FetchMetadata = async (get, { signal }) => {
|
|||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
const maybeMetadata = loadFromLocalStorage();
|
|
||||||
|
|
||||||
// migration step, only data in `METADATA_STORAGE_KEY` will be migrated
|
|
||||||
if (
|
|
||||||
maybeMetadata.some(meta => !('version' in meta)) &&
|
|
||||||
!window.$migrationDone
|
|
||||||
) {
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
|
||||||
signal.addEventListener('abort', () => reject(), { once: true });
|
|
||||||
window.addEventListener('migration-done', () => resolve(), {
|
|
||||||
once: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
performanceJotaiLogger.info('migration done');
|
|
||||||
}
|
|
||||||
|
|
||||||
metadata.push(...loadFromLocalStorage());
|
metadata.push(...loadFromLocalStorage());
|
||||||
}
|
}
|
||||||
// step 2: fetch from adapters
|
// step 2: fetch from adapters
|
||||||
@@ -211,14 +191,14 @@ const fetchMetadata: FetchMetadata = async (get, { signal }) => {
|
|||||||
|
|
||||||
const rootWorkspacesMetadataPromiseAtom = atom<
|
const rootWorkspacesMetadataPromiseAtom = atom<
|
||||||
Promise<RootWorkspaceMetadata[]>
|
Promise<RootWorkspaceMetadata[]>
|
||||||
>(async (get, { signal }) => {
|
>(async get => {
|
||||||
const primitiveMetadata = get(rootWorkspacesMetadataPrimitiveAtom);
|
const primitiveMetadata = get(rootWorkspacesMetadataPrimitiveAtom);
|
||||||
assertEquals(
|
assertEquals(
|
||||||
primitiveMetadata,
|
primitiveMetadata,
|
||||||
null,
|
null,
|
||||||
'rootWorkspacesMetadataPrimitiveAtom should be null'
|
'rootWorkspacesMetadataPrimitiveAtom should be null'
|
||||||
);
|
);
|
||||||
return fetchMetadata(get, { signal });
|
return fetchMetadata(get);
|
||||||
});
|
});
|
||||||
|
|
||||||
type SetStateAction<Value> = Value | ((prev: Value) => Value);
|
type SetStateAction<Value> = Value | ((prev: Value) => Value);
|
||||||
@@ -276,11 +256,7 @@ export const rootWorkspacesMetadataAtom = atom<
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const refreshRootMetadataAtom = atom(null, (get, set) => {
|
export const refreshRootMetadataAtom = atom(null, (get, set) => {
|
||||||
const abortController = new AbortController();
|
set(rootWorkspacesMetadataPrimitiveAtom, fetchMetadata(get));
|
||||||
set(
|
|
||||||
rootWorkspacesMetadataPrimitiveAtom,
|
|
||||||
fetchMetadata(get, { signal: abortController.signal })
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// blocksuite atoms,
|
// blocksuite atoms,
|
||||||
|
|||||||
@@ -7,3 +7,6 @@ BUILD_TYPE=canary yarn run build
|
|||||||
cd tests/affine-migration
|
cd tests/affine-migration
|
||||||
yarn run e2e
|
yarn run e2e
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> Tips:
|
||||||
|
> Run `yarn dev` to start dev server in 8080 could make debugging more quickly.
|
||||||
|
|||||||
@@ -34,26 +34,21 @@ test('v1 to v4', async ({ page }) => {
|
|||||||
|
|
||||||
await page.goto(coreUrl);
|
await page.goto(coreUrl);
|
||||||
await clickSideBarAllPageButton(page);
|
await clickSideBarAllPageButton(page);
|
||||||
await page.getByText('hello').click();
|
|
||||||
|
|
||||||
//#region fixme(himself65): blocksuite issue, data cannot be loaded to store
|
await expect(page.getByTestId('upgrade-workspace-button')).toBeVisible();
|
||||||
const url = page.url();
|
await page.getByTestId('upgrade-workspace-button').click();
|
||||||
await page.waitForTimeout(5000);
|
|
||||||
await page.goto(url);
|
await expect(page.getByText('Refresh Current Page')).toBeVisible();
|
||||||
//#endregion
|
await page.getByTestId('upgrade-workspace-button').click();
|
||||||
|
|
||||||
|
await expect(page.getByTestId('page-list-item')).toHaveCount(2);
|
||||||
|
await page
|
||||||
|
.getByTestId('page-list-item-title-text')
|
||||||
|
.getByText('hello')
|
||||||
|
.click();
|
||||||
|
|
||||||
await waitForEditorLoad(page);
|
await waitForEditorLoad(page);
|
||||||
expect(await page.locator('v-line').nth(0).textContent()).toBe('hello');
|
await expect(page.locator('v-line').nth(0)).toHaveText('hello');
|
||||||
|
|
||||||
const changedLocalStorageData = await page.evaluate(() =>
|
|
||||||
window.readAffineLocalStorage()
|
|
||||||
);
|
|
||||||
const workspaces = JSON.parse(
|
|
||||||
changedLocalStorageData['jotai-workspaces']
|
|
||||||
) as any[];
|
|
||||||
for (const workspace of workspaces) {
|
|
||||||
expect(workspace.version).toBe(4);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('v2 to v4, database migration', async ({ page }) => {
|
test('v2 to v4, database migration', async ({ page }) => {
|
||||||
@@ -62,99 +57,70 @@ test('v2 to v4, database migration', async ({ page }) => {
|
|||||||
'0.8.0-canary.7'
|
'0.8.0-canary.7'
|
||||||
);
|
);
|
||||||
|
|
||||||
//#region fixme(himself65): blocksuite issue, data cannot be loaded to store
|
|
||||||
const allPagePath = `${coreUrl}/workspace/${localStorageData.last_workspace_id}/all`;
|
|
||||||
await page.goto(allPagePath);
|
|
||||||
await page.waitForTimeout(5000);
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
const detailPagePath = `${coreUrl}/workspace/${localStorageData.last_workspace_id}/${localStorageData.last_page_id}`;
|
const detailPagePath = `${coreUrl}/workspace/${localStorageData.last_workspace_id}/${localStorageData.last_page_id}`;
|
||||||
await page.goto(detailPagePath);
|
await page.goto(detailPagePath);
|
||||||
|
|
||||||
|
await expect(page.getByTestId('upgrade-workspace-button')).toBeVisible();
|
||||||
|
await page.getByTestId('upgrade-workspace-button').click();
|
||||||
|
|
||||||
|
await expect(page.getByText('Refresh Current Page')).toBeVisible();
|
||||||
|
await page.getByTestId('upgrade-workspace-button').click();
|
||||||
await waitForEditorLoad(page);
|
await waitForEditorLoad(page);
|
||||||
|
|
||||||
// check page mode is correct
|
// check page mode is correct
|
||||||
expect(await page.locator('v-line').nth(0).textContent()).toBe('hello');
|
await expect(page.locator('v-line').nth(0)).toHaveText('hello');
|
||||||
expect(await page.locator('affine-database').isVisible()).toBe(true);
|
await expect(page.locator('affine-database')).toBeVisible();
|
||||||
|
|
||||||
// check edgeless mode is correct
|
// check edgeless mode is correct
|
||||||
await clickEdgelessModeButton(page);
|
await clickEdgelessModeButton(page);
|
||||||
await page.waitForTimeout(200);
|
await expect(page.locator('affine-database')).toBeVisible();
|
||||||
expect(await page.locator('affine-database').isVisible()).toBe(true);
|
|
||||||
|
|
||||||
const changedLocalStorageData = await page.evaluate(() =>
|
|
||||||
window.readAffineLocalStorage()
|
|
||||||
);
|
|
||||||
const workspaces = JSON.parse(
|
|
||||||
changedLocalStorageData['jotai-workspaces']
|
|
||||||
) as any[];
|
|
||||||
for (const workspace of workspaces) {
|
|
||||||
expect(workspace.version).toBe(4);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('v3 to v4, surface migration', async ({ page }) => {
|
test('v3 to v4, surface migration', async ({ page }) => {
|
||||||
const { localStorageData } = await open404PageToInitData(page, '0.8.4');
|
const { localStorageData } = await open404PageToInitData(page, '0.8.4');
|
||||||
|
|
||||||
//#region fixme(himself65): blocksuite issue, data cannot be loaded to store
|
|
||||||
const allPagePath = `${coreUrl}/workspace/${localStorageData.last_workspace_id}/all`;
|
|
||||||
await page.goto(allPagePath);
|
|
||||||
await page.waitForTimeout(5000);
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
const detailPagePath = `${coreUrl}/workspace/${localStorageData.last_workspace_id}/${localStorageData.last_page_id}`;
|
const detailPagePath = `${coreUrl}/workspace/${localStorageData.last_workspace_id}/${localStorageData.last_page_id}`;
|
||||||
await page.goto(detailPagePath);
|
await page.goto(detailPagePath);
|
||||||
|
|
||||||
|
await expect(page.getByTestId('upgrade-workspace-button')).toBeVisible();
|
||||||
|
await page.getByTestId('upgrade-workspace-button').click();
|
||||||
|
|
||||||
|
await expect(page.getByText('Refresh Current Page')).toBeVisible();
|
||||||
|
await page.getByTestId('upgrade-workspace-button').click();
|
||||||
await waitForEditorLoad(page);
|
await waitForEditorLoad(page);
|
||||||
|
|
||||||
// check edgeless mode is correct
|
// check edgeless mode is correct
|
||||||
await clickEdgelessModeButton(page);
|
await clickEdgelessModeButton(page);
|
||||||
await expect(page.locator('edgeless-toolbar')).toBeVisible();
|
await expect(page.locator('edgeless-toolbar')).toBeVisible();
|
||||||
await expect(page.locator('affine-edgeless-page')).toBeVisible();
|
await expect(page.locator('affine-edgeless-page')).toBeVisible();
|
||||||
|
|
||||||
const changedLocalStorageData = await page.evaluate(() =>
|
|
||||||
window.readAffineLocalStorage()
|
|
||||||
);
|
|
||||||
const workspaces = JSON.parse(
|
|
||||||
changedLocalStorageData['jotai-workspaces']
|
|
||||||
) as any[];
|
|
||||||
for (const workspace of workspaces) {
|
|
||||||
expect(workspace.version).toBe(4);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('v0 to v4, subdoc migration', async ({ page }) => {
|
test('v0 to v4, subdoc migration', async ({ page }) => {
|
||||||
await open404PageToInitData(page, '0.6.1-beta.1');
|
await open404PageToInitData(page, '0.6.1-beta.1');
|
||||||
|
|
||||||
await page.goto(coreUrl);
|
await page.goto(coreUrl);
|
||||||
await page.waitForTimeout(5000);
|
|
||||||
|
|
||||||
// go to all page
|
|
||||||
await clickSideBarAllPageButton(page);
|
await clickSideBarAllPageButton(page);
|
||||||
|
|
||||||
// find if page name with "hello" exists and click it
|
await expect(page.getByTestId('upgrade-workspace-button')).toBeVisible();
|
||||||
|
await page.getByTestId('upgrade-workspace-button').click();
|
||||||
|
|
||||||
|
await expect(page.getByText('Refresh Current Page')).toBeVisible();
|
||||||
|
await page.getByTestId('upgrade-workspace-button').click();
|
||||||
|
|
||||||
|
await expect(page.getByTestId('page-list-item')).toHaveCount(2);
|
||||||
await page
|
await page
|
||||||
.locator('[data-testid="page-list-item-title-text"]:has-text("hello")')
|
.getByTestId('page-list-item-title-text')
|
||||||
|
.getByText('hello')
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
await waitForEditorLoad(page);
|
await waitForEditorLoad(page);
|
||||||
|
|
||||||
// check if content is correct
|
// check page mode is correct
|
||||||
expect(await page.locator('v-line').nth(0).textContent()).toBe('hello');
|
await expect(page.locator('v-line').nth(0)).toHaveText('hello');
|
||||||
expect(await page.locator('v-line').nth(1).textContent()).toBe(
|
await expect(page.locator('v-line').nth(1)).toHaveText('TEST CONTENT');
|
||||||
'TEST CONTENT'
|
|
||||||
);
|
|
||||||
|
|
||||||
// check edgeless mode is correct
|
// check edgeless mode is correct
|
||||||
await clickEdgelessModeButton(page);
|
await clickEdgelessModeButton(page);
|
||||||
await expect(page.locator('edgeless-toolbar')).toBeVisible();
|
await expect(page.locator('edgeless-toolbar')).toBeVisible();
|
||||||
await expect(page.locator('affine-edgeless-page')).toBeVisible();
|
await expect(page.locator('affine-edgeless-page')).toBeVisible();
|
||||||
|
|
||||||
const changedLocalStorageData = await page.evaluate(() =>
|
|
||||||
window.readAffineLocalStorage()
|
|
||||||
);
|
|
||||||
const workspaces = JSON.parse(
|
|
||||||
changedLocalStorageData['jotai-workspaces']
|
|
||||||
) as any[];
|
|
||||||
for (const workspace of workspaces) {
|
|
||||||
expect(workspace.version).toBe(4);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -30,10 +30,13 @@ export async function createLocalWorkspace(
|
|||||||
await page.getByPlaceholder('Set a Workspace name').fill(params.name);
|
await page.getByPlaceholder('Set a Workspace name').fill(params.name);
|
||||||
|
|
||||||
// click create button
|
// click create button
|
||||||
await page.getByRole('button', { name: 'Create' }).click({
|
await page.getByTestId('create-workspace-create-button').click({
|
||||||
delay: 500,
|
delay: 500,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByTestId('create-workspace-create-button')
|
||||||
|
).not.toBeAttached();
|
||||||
await waitForEditorLoad(page);
|
await waitForEditorLoad(page);
|
||||||
|
|
||||||
await expect(page.getByTestId('workspace-name')).toHaveText(params.name);
|
await expect(page.getByTestId('workspace-name')).toHaveText(params.name);
|
||||||
|
|||||||
5
tools/@types/env/__all.d.ts
vendored
5
tools/@types/env/__all.d.ts
vendored
@@ -44,11 +44,6 @@ declare global {
|
|||||||
): this;
|
): this;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
$migrationDone: boolean | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WindowEventMap {
|
|
||||||
'migration-done': CustomEvent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-var
|
// eslint-disable-next-line no-var
|
||||||
|
|||||||
Reference in New Issue
Block a user