refactor: workspace manager (#5060)

This commit is contained in:
EYHN
2023-12-15 07:20:50 +00:00
parent af15aa06d4
commit fe2851d3e9
217 changed files with 3605 additions and 4244 deletions

View File

@@ -1,5 +1,6 @@
/// <reference types="@blocksuite/global" /> /// <reference types="@blocksuite/global" />
import { assertEquals } from '@blocksuite/global/utils'; import { assertEquals } from '@blocksuite/global/utils';
import type { Workspace } from '@blocksuite/store';
import { z } from 'zod'; import { z } from 'zod';
import { isDesktop, isServer } from './constant.js'; import { isDesktop, isServer } from './constant.js';
@@ -149,3 +150,12 @@ export function setupGlobal() {
globalThis.$AFFINE_SETUP = true; globalThis.$AFFINE_SETUP = true;
} }
export function setupEditorFlags(workspace: Workspace) {
Object.entries(runtimeConfig.editorFlags).forEach(([key, value]) => {
workspace.awarenessStore.setFlag(
key as keyof BlockSuiteFeatureFlags,
value
);
});
}

View File

@@ -1,10 +1,4 @@
import type {
ActiveDocProvider,
PassiveDocProvider,
Workspace as BlockSuiteWorkspace,
} from '@blocksuite/store';
import type { PropsWithChildren, ReactNode } from 'react'; import type { PropsWithChildren, ReactNode } from 'react';
import type { DataSourceAdapter } from 'y-provider';
export enum WorkspaceSubPath { export enum WorkspaceSubPath {
ALL = 'all', ALL = 'all',
@@ -14,73 +8,6 @@ export enum WorkspaceSubPath {
SHARED = 'shared', SHARED = 'shared',
} }
export interface AffineDownloadProvider extends PassiveDocProvider {
flavour: 'affine-download';
}
/**
* Download the first binary from local IndexedDB
*/
export interface BroadCastChannelProvider extends PassiveDocProvider {
flavour: 'broadcast-channel';
}
/**
* Long polling provider with local IndexedDB
*/
export interface LocalIndexedDBBackgroundProvider
extends DataSourceAdapter,
PassiveDocProvider {
flavour: 'local-indexeddb-background';
}
export interface LocalIndexedDBDownloadProvider extends ActiveDocProvider {
flavour: 'local-indexeddb';
}
export interface SQLiteProvider extends PassiveDocProvider, DataSourceAdapter {
flavour: 'sqlite';
}
export interface SQLiteDBDownloadProvider extends ActiveDocProvider {
flavour: 'sqlite-download';
}
export interface AffineSocketIOProvider
extends PassiveDocProvider,
DataSourceAdapter {
flavour: 'affine-socket-io';
}
type BaseWorkspace = {
flavour: string;
id: string;
blockSuiteWorkspace: BlockSuiteWorkspace;
};
export interface AffineCloudWorkspace extends BaseWorkspace {
flavour: WorkspaceFlavour.AFFINE_CLOUD;
id: string;
blockSuiteWorkspace: BlockSuiteWorkspace;
}
export interface LocalWorkspace extends BaseWorkspace {
flavour: WorkspaceFlavour.LOCAL;
id: string;
blockSuiteWorkspace: BlockSuiteWorkspace;
}
export interface AffinePublicWorkspace extends BaseWorkspace {
flavour: WorkspaceFlavour.AFFINE_PUBLIC;
id: string;
blockSuiteWorkspace: BlockSuiteWorkspace;
}
export type AffineOfficialWorkspace =
| AffineCloudWorkspace
| LocalWorkspace
| AffinePublicWorkspace;
export enum ReleaseType { export enum ReleaseType {
// if workspace is not released yet, we will not show it in the workspace list // if workspace is not released yet, we will not show it in the workspace list
UNRELEASED = 'unreleased', UNRELEASED = 'unreleased',
@@ -99,7 +26,6 @@ export enum WorkspaceFlavour {
*/ */
AFFINE_CLOUD = 'affine-cloud', AFFINE_CLOUD = 'affine-cloud',
LOCAL = 'local', LOCAL = 'local',
AFFINE_PUBLIC = 'affine-public',
} }
export const settingPanel = { export const settingPanel = {
@@ -112,68 +38,30 @@ export const settingPanel = {
export const settingPanelValues = Object.values(settingPanel); export const settingPanelValues = Object.values(settingPanel);
export type SettingPanel = (typeof settingPanel)[keyof typeof settingPanel]; export type SettingPanel = (typeof settingPanel)[keyof typeof settingPanel];
// built-in workspaces export type WorkspaceHeaderProps = {
export interface WorkspaceRegistry { rightSlot?: ReactNode;
[WorkspaceFlavour.LOCAL]: LocalWorkspace; currentEntry:
[WorkspaceFlavour.AFFINE_PUBLIC]: AffinePublicWorkspace; | {
[WorkspaceFlavour.AFFINE_CLOUD]: AffineCloudWorkspace; subPath: WorkspaceSubPath;
} }
| {
export interface WorkspaceCRUD<Flavour extends keyof WorkspaceRegistry> { pageId: string;
create: (blockSuiteWorkspace: BlockSuiteWorkspace) => Promise<string>; };
delete: (blockSuiteWorkspace: BlockSuiteWorkspace) => Promise<void>;
get: (workspaceId: string) => Promise<WorkspaceRegistry[Flavour] | null>;
// not supported yet
// update: (workspace: FlavourToWorkspace[Flavour]) => Promise<void>;
list: () => Promise<WorkspaceRegistry[Flavour][]>;
}
type UIBaseProps<_Flavour extends keyof WorkspaceRegistry> = {
currentWorkspaceId: string;
}; };
type NewSettingProps<Flavour extends keyof WorkspaceRegistry> =
UIBaseProps<Flavour> & {
onDeleteLocalWorkspace: () => void;
onDeleteCloudWorkspace: () => void;
onLeaveWorkspace: () => void;
onTransformWorkspace: <
From extends keyof WorkspaceRegistry,
To extends keyof WorkspaceRegistry,
>(
from: From,
to: To,
workspace: WorkspaceRegistry[From]
) => void;
};
interface FC<P> { interface FC<P> {
(props: P): ReactNode; (props: P): ReactNode;
} }
export interface WorkspaceUISchema<Flavour extends keyof WorkspaceRegistry> { export interface WorkspaceUISchema {
NewSettingsDetail: FC<NewSettingProps<Flavour>>;
Provider: FC<PropsWithChildren>; Provider: FC<PropsWithChildren>;
LoginCard?: FC<object>; LoginCard?: FC<object>;
} }
export interface AppEvents {
// event there is no workspace
// usually used to initialize workspace adapter
'app:init': () => string[];
// event if you have access to workspace adapter
'app:access': () => Promise<boolean>;
'service:start': () => void;
'service:stop': () => void;
}
export interface WorkspaceAdapter<Flavour extends WorkspaceFlavour> { export interface WorkspaceAdapter<Flavour extends WorkspaceFlavour> {
releaseType: ReleaseType; releaseType: ReleaseType;
flavour: Flavour; flavour: Flavour;
// The Adapter will be loaded according to the priority // The Adapter will be loaded according to the priority
loadPriority: LoadPriority; loadPriority: LoadPriority;
Events: Partial<AppEvents>; UI: WorkspaceUISchema;
// Fetch necessary data for the first render
CRUD: WorkspaceCRUD<Flavour>;
UI: WorkspaceUISchema<Flavour>;
} }

View File

@@ -1,79 +0,0 @@
import type { ActiveDocProvider, Workspace } from '@blocksuite/store';
import type { PassiveDocProvider } from '@blocksuite/store';
import type { Atom } from 'jotai/vanilla';
import { atom } from 'jotai/vanilla';
import { atomEffect } from 'jotai-effect';
/**
* Map: guid -> Workspace
*/
export const INTERNAL_BLOCKSUITE_HASH_MAP = new Map<string, Workspace>([]);
const workspaceActiveAtomWeakMap = new WeakMap<
Workspace,
Atom<Promise<Workspace>>
>();
const workspaceActiveWeakMap = new WeakMap<Workspace, boolean>();
const workspaceEffectAtomWeakMap = new WeakMap<Workspace, Atom<void>>();
export async function waitForWorkspace(workspace: Workspace) {
if (workspaceActiveWeakMap.get(workspace) !== true) {
const providers = workspace.providers.filter(
(provider): provider is ActiveDocProvider =>
'active' in provider && provider.active === true
);
for (const provider of providers) {
provider.sync();
// we will wait for the necessary providers to be ready
await provider.whenReady;
}
// timeout is INFINITE
workspaceActiveWeakMap.set(workspace, true);
}
}
export function getWorkspace(id: string) {
if (!INTERNAL_BLOCKSUITE_HASH_MAP.has(id)) {
throw new Error('Workspace not found');
}
return INTERNAL_BLOCKSUITE_HASH_MAP.get(id) as Workspace;
}
export function getBlockSuiteWorkspaceAtom(
id: string
): [workspaceAtom: Atom<Promise<Workspace>>, workspaceEffectAtom: Atom<void>] {
if (!INTERNAL_BLOCKSUITE_HASH_MAP.has(id)) {
throw new Error('Workspace not found');
}
const workspace = INTERNAL_BLOCKSUITE_HASH_MAP.get(id) as Workspace;
if (!workspaceActiveAtomWeakMap.has(workspace)) {
const baseAtom = atom(async () => {
await waitForWorkspace(workspace);
return workspace;
});
workspaceActiveAtomWeakMap.set(workspace, baseAtom);
}
if (!workspaceEffectAtomWeakMap.has(workspace)) {
const effectAtom = atomEffect(() => {
const providers = workspace.providers.filter(
(provider): provider is PassiveDocProvider =>
'passive' in provider && provider.passive === true
);
providers.forEach(provider => {
provider.connect();
});
return () => {
providers.forEach(provider => {
provider.disconnect();
});
};
});
workspaceEffectAtomWeakMap.set(workspace, effectAtom);
}
return [
workspaceActiveAtomWeakMap.get(workspace) as Atom<Promise<Workspace>>,
workspaceEffectAtomWeakMap.get(workspace) as Atom<void>,
];
}

View File

@@ -1,55 +0,0 @@
/**
* @vitest-environment happy-dom
*/
import { Schema, Workspace } from '@blocksuite/store';
import { waitFor } from '@testing-library/react';
import { getDefaultStore } from 'jotai/vanilla';
import { expect, test, vi } from 'vitest';
import {
getBlockSuiteWorkspaceAtom,
INTERNAL_BLOCKSUITE_HASH_MAP,
} from '../__internal__/workspace.js';
test('blocksuite atom', async () => {
const sync = vi.fn();
let connected = false;
const connect = vi.fn(() => (connected = true));
const workspace = new Workspace({
schema: new Schema(),
id: '1',
providerCreators: [
() => ({
flavour: 'fake',
active: true,
sync,
get whenReady(): Promise<void> {
return Promise.resolve();
},
}),
() => ({
flavour: 'fake-2',
passive: true,
get connected() {
return connected;
},
connect,
disconnect: vi.fn(),
}),
],
});
INTERNAL_BLOCKSUITE_HASH_MAP.set('1', workspace);
{
const [atom, effectAtom] = getBlockSuiteWorkspaceAtom('1');
const store = getDefaultStore();
const result = await store.get(atom);
expect(result).toBe(workspace);
expect(sync).toBeCalledTimes(1);
expect(connect).not.toHaveBeenCalled();
store.sub(effectAtom, vi.fn());
await waitFor(() => expect(connect).toBeCalledTimes(1));
expect(connected).toBe(true);
}
});

View File

@@ -1,14 +1,3 @@
import { assertExists } from '@blocksuite/global/utils';
import type { Workspace } from '@blocksuite/store';
import { atom } from 'jotai'; import { atom } from 'jotai';
import { getBlockSuiteWorkspaceAtom } from '../__internal__/workspace';
export const currentWorkspaceIdAtom = atom<string | null>(null);
export const currentPageIdAtom = atom<string | null>(null); export const currentPageIdAtom = atom<string | null>(null);
export const currentWorkspaceAtom = atom<Promise<Workspace>>(async get => {
const workspaceId = get(currentWorkspaceIdAtom);
assertExists(workspaceId);
const [currentWorkspaceAtom] = getBlockSuiteWorkspaceAtom(workspaceId);
return get(currentWorkspaceAtom);
});

View File

@@ -1,8 +1,10 @@
export * from './initialization'; export * from './initialization';
export * from './migration/blob'; export {
export { migratePages as forceUpgradePages } from './migration/blocksuite'; // campatible with electron migratePages as forceUpgradePages,
migrateGuidCompatibility,
} from './migration/blocksuite'; // campatible with electron
export * from './migration/fixing'; export * from './migration/fixing';
export { migrateToSubdoc } from './migration/subdoc'; export { migrateToSubdoc, upgradeV1ToV2 } from './migration/subdoc';
export * from './migration/workspace'; export * from './migration/workspace';
/** /**

View File

@@ -2,12 +2,9 @@ import { assertExists } from '@blocksuite/global/utils';
import type { Page, PageMeta, Workspace } from '@blocksuite/store'; import type { Page, PageMeta, Workspace } from '@blocksuite/store';
import type { createStore, WritableAtom } from 'jotai/vanilla'; import type { createStore, WritableAtom } from 'jotai/vanilla';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { Map as YMap } from 'yjs';
import { migratePages } from '../migration/blocksuite'; import { getLatestVersions } from '../migration/blocksuite';
import {
checkWorkspaceCompatibility,
MigrationPoint,
} from '../migration/workspace';
export async function initEmptyPage(page: Page, title?: string) { export async function initEmptyPage(page: Page, title?: string) {
await page.load(() => { await page.load(() => {
@@ -261,10 +258,11 @@ export async function buildShowcaseWorkspace(
// The showcase building will create multiple pages once, and may skip the version writing. // 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 // https://github.com/toeverything/blocksuite/blob/master/packages/store/src/workspace/page.ts#L662
const compatibilityResult = checkWorkspaceCompatibility(workspace); workspace.doc.getMap('meta').set('pageVersion', 2);
if (compatibilityResult === MigrationPoint.BlockVersion) { const newVersions = getLatestVersions(workspace.schema);
await migratePages(workspace.doc, workspace.schema); workspace.doc
} .getMap('meta')
.set('blockVersions', new YMap(Object.entries(newVersions)));
Object.entries(pageMetas).forEach(([oldId, meta]) => { Object.entries(pageMetas).forEach(([oldId, meta]) => {
const newId = idMap[oldId]; const newId = idMap[oldId];

View File

@@ -1,15 +0,0 @@
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);
}
}

View File

@@ -1,8 +1,14 @@
import type { Schema } from '@blocksuite/store'; import type { Schema } from '@blocksuite/store';
import type { Doc as YDoc } from 'yjs'; import type { Array as YArray } from 'yjs';
import { Map as YMap } from 'yjs'; import {
applyUpdate,
Doc as YDoc,
encodeStateAsUpdate,
Map as YMap,
transact,
} from 'yjs';
const getLatestVersions = (schema: Schema): Record<string, number> => { export const getLatestVersions = (schema: Schema): Record<string, number> => {
return [...schema.flavourSchemaMap.entries()].reduce( return [...schema.flavourSchemaMap.entries()].reduce(
(record, [flavour, schema]) => { (record, [flavour, schema]) => {
record[flavour] = schema.version; record[flavour] = schema.version;
@@ -28,14 +34,62 @@ export async function migratePages(
// Hard code to upgrade page version to 2. // Hard code to upgrade page version to 2.
// Let e2e to ensure the data version is correct. // Let e2e to ensure the data version is correct.
const pageVersion = meta.get('pageVersion'); return transact(
if (typeof pageVersion !== 'number' || pageVersion < 2) { rootDoc,
meta.set('pageVersion', 2); () => {
} const pageVersion = meta.get('pageVersion');
if (typeof pageVersion !== 'number' || pageVersion < 2) {
meta.set('pageVersion', 2);
}
const newVersions = getLatestVersions(schema); const newVersions = getLatestVersions(schema);
meta.set('blockVersions', new YMap(Object.entries(newVersions))); meta.set('blockVersions', new YMap(Object.entries(newVersions)));
return Object.entries(oldVersions).some( return Object.entries(oldVersions).some(
([flavour, version]) => newVersions[flavour] !== version ([flavour, version]) => newVersions[flavour] !== version
);
},
'migratePages',
/**
* transact as remote update, because blocksuite will skip local changes.
* https://github.com/toeverything/blocksuite/blob/9c2df3f7aa5617c050e0dccdd73e99bb67e0c0f7/packages/store/src/reactive/utils.ts#L143
*/
false
); );
} }
// 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 migrateGuidCompatibility(rootDoc: YDoc) {
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);
console.debug(
`fixed space id ${pageId} -> ${newPageId}, doc id: ${doc.guid}`
);
}
});
}

View File

@@ -1,48 +1,5 @@
import type { Array as YArray, Map as YMap } from 'yjs'; import type { Doc as YDoc, Map as YMap } from 'yjs';
import { Doc as YDoc, transact } from 'yjs'; import { 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;
}
/** /**
* Hard code to fix workspace version to be compatible with legacy data. * Hard code to fix workspace version to be compatible with legacy data.
@@ -55,13 +12,35 @@ export function fixWorkspaceVersion(rootDoc: YDoc) {
* It doesn't matter to upgrade workspace version from 1 or undefined to 2. * It doesn't matter to upgrade workspace version from 1 or undefined to 2.
* Blocksuite just set the value, do nothing else. * Blocksuite just set the value, do nothing else.
*/ */
const workspaceVersion = meta.get('workspaceVersion'); function doFix() {
if (typeof workspaceVersion !== 'number' || workspaceVersion < 2) { const workspaceVersion = meta.get('workspaceVersion');
meta.set('workspaceVersion', 2); if (typeof workspaceVersion !== 'number' || workspaceVersion < 2) {
transact(
rootDoc,
() => {
meta.set('workspaceVersion', 2);
},
'fixWorkspaceVersion',
// transact as remote update, because blocksuite will skip local changes.
false
);
}
const pageVersion = meta.get('pageVersion'); const pageVersion = meta.get('pageVersion');
if (typeof pageVersion !== 'number') { if (typeof pageVersion !== 'number' || pageVersion < 2) {
meta.set('pageVersion', 1); transact(
rootDoc,
() => {
meta.set('pageVersion', 2);
},
'fixPageVersion',
// transact as remote update, because blocksuite will skip local changes.
false
);
} }
} }
doFix();
// do fix every time when meta changed
meta.observe(() => doFix());
} }

View File

@@ -1,4 +1,3 @@
import type { Workspace } from '@blocksuite/store';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { Array as YArray, Doc as YDoc, Map as YMap } from 'yjs'; import { Array as YArray, Doc as YDoc, Map as YMap } from 'yjs';
import { applyUpdate, encodeStateAsUpdate } from 'yjs'; import { applyUpdate, encodeStateAsUpdate } from 'yjs';
@@ -264,19 +263,17 @@ export function migrateToSubdoc(oldDoc: YDoc): YDoc {
return newDoc; return newDoc;
} }
export const upgradeV1ToV2 = async ( /**
oldDoc: YDoc, * upgrade oldDoc to v2, write to targetDoc
createWorkspace: () => Promise<Workspace> */
) => { export const upgradeV1ToV2 = async (oldDoc: YDoc, targetDoc: YDoc) => {
const newDoc = migrateToSubdoc(oldDoc); const newDoc = migrateToSubdoc(oldDoc);
const newWorkspace = await createWorkspace(); applyUpdate(targetDoc, encodeStateAsUpdate(newDoc), migrationOrigin);
applyUpdate(newWorkspace.doc, encodeStateAsUpdate(newDoc), migrationOrigin);
newDoc.getSubdocs().forEach(subdoc => { newDoc.getSubdocs().forEach(subdoc => {
newWorkspace.doc.getSubdocs().forEach(newDoc => { targetDoc.getSubdocs().forEach(newDoc => {
if (subdoc.guid === newDoc.guid) { if (subdoc.guid === newDoc.guid) {
applyUpdate(newDoc, encodeStateAsUpdate(subdoc), migrationOrigin); applyUpdate(newDoc, encodeStateAsUpdate(subdoc), migrationOrigin);
} }
}); });
}); });
return newWorkspace;
}; };

View File

@@ -1,63 +1,50 @@
import type { Workspace } from '@blocksuite/store'; import type { Workspace } from '@blocksuite/store';
import type { Schema } from '@blocksuite/store'; import type { Array as YArray, Doc as YDoc, Map as YMap } from 'yjs';
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. * For split migrate function from MigrationQueue.
*/ */
export enum MigrationPoint { export enum MigrationPoint {
SubDoc = 1, SubDoc = 1,
BlockVersion = 2, GuidFix = 2,
} BlockVersion = 3,
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( export function checkWorkspaceCompatibility(
workspace: Workspace workspace: Workspace
): MigrationPoint | null { ): MigrationPoint | null {
const workspaceDocJSON = workspace.doc.toJSON(); // check if there is any key starts with 'space:' on root doc
const spaceMetaObj = workspaceDocJSON['space:meta']; const spaceMetaObj = workspace.doc.share.get('space:meta') as
const docKeys = Object.keys(workspaceDocJSON); | YMap<any>
const haveSpaceMeta = !!spaceMetaObj && Object.keys(spaceMetaObj).length > 0; | undefined;
const docKeys = Array.from(workspace.doc.share.keys());
const haveSpaceMeta = !!spaceMetaObj && spaceMetaObj.size > 0;
const haveLegacySpace = docKeys.some(key => key.startsWith('space:')); const haveLegacySpace = docKeys.some(key => key.startsWith('space:'));
if (haveSpaceMeta || haveLegacySpace) { if (haveSpaceMeta || haveLegacySpace) {
return MigrationPoint.SubDoc; return MigrationPoint.SubDoc;
} }
// exit if no pages
if (!workspace.meta.pages?.length) {
return null;
}
// check guid compatibility
const meta = workspace.doc.getMap('meta') as YMap<unknown>;
const pages = meta.get('pages') as YArray<YMap<unknown>>;
for (const page of pages) {
const pageId = page.get('id') as string | undefined;
if (pageId?.includes(':')) {
return MigrationPoint.GuidFix;
}
}
const spaces = workspace.doc.getMap('spaces') as YMap<YDoc>;
for (const [pageId, _] of spaces) {
if (pageId.includes(':')) {
return MigrationPoint.GuidFix;
}
}
const hasVersion = workspace.meta.hasVersion; const hasVersion = workspace.meta.hasVersion;
if (!hasVersion) { if (!hasVersion) {
return MigrationPoint.BlockVersion; return MigrationPoint.BlockVersion;

View File

@@ -18,10 +18,6 @@ export default defineConfig({
type: resolve(root, 'src/type.ts'), type: resolve(root, 'src/type.ts'),
'core/event-emitter': resolve(root, 'src/core/event-emitter.ts'), 'core/event-emitter': resolve(root, 'src/core/event-emitter.ts'),
'preload/electron': resolve(root, 'src/preload/electron.ts'), 'preload/electron': resolve(root, 'src/preload/electron.ts'),
'__internal__/workspace': resolve(
root,
'src/__internal__/workspace.ts'
),
'__internal__/plugin': resolve(root, 'src/__internal__/plugin.ts'), '__internal__/plugin': resolve(root, 'src/__internal__/plugin.ts'),
}, },
formats: ['es', 'cjs'], formats: ['es', 'cjs'],

View File

@@ -1,11 +1,10 @@
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { RootWorkspaceMetadata } from '@affine/workspace/atom'; import type { WorkspaceMetadata } from '@affine/workspace';
import { CollaborationIcon, SettingsIcon } from '@blocksuite/icons'; import { CollaborationIcon, SettingsIcon } from '@blocksuite/icons';
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url'; import { useWorkspaceBlobObjectUrl } from '@toeverything/hooks/use-workspace-blob';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name'; import { useWorkspaceInfo } from '@toeverything/hooks/use-workspace-info';
import { getBlockSuiteWorkspaceAtom } from '@toeverything/infra/__internal__/workspace';
import { useAtomValue } from 'jotai/react';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { Avatar } from '../../../ui/avatar'; import { Avatar } from '../../../ui/avatar';
@@ -68,10 +67,10 @@ const WorkspaceType = ({ flavour, isOwner }: WorkspaceTypeProps) => {
}; };
export interface WorkspaceCardProps { export interface WorkspaceCardProps {
currentWorkspaceId: string | null; currentWorkspaceId?: string | null;
meta: RootWorkspaceMetadata; meta: WorkspaceMetadata;
onClick: (workspaceId: string) => void; onClick: (metadata: WorkspaceMetadata) => void;
onSettingClick: (workspaceId: string) => void; onSettingClick: (metadata: WorkspaceMetadata) => void;
isOwner?: boolean; isOwner?: boolean;
} }
@@ -98,29 +97,31 @@ export const WorkspaceCard = ({
meta, meta,
isOwner = true, isOwner = true,
}: WorkspaceCardProps) => { }: WorkspaceCardProps) => {
const [workspaceAtom] = getBlockSuiteWorkspaceAtom(meta.id); const information = useWorkspaceInfo(meta);
const workspace = useAtomValue(workspaceAtom); const avatarUrl = useWorkspaceBlobObjectUrl(meta, information?.avatar);
const [name] = useBlockSuiteWorkspaceName(workspace);
const [workspaceAvatar] = useBlockSuiteWorkspaceAvatarUrl(workspace); const name = information?.name ?? UNTITLED_WORKSPACE_NAME;
return ( return (
<StyledCard <StyledCard
data-testid="workspace-card" data-testid="workspace-card"
onClick={useCallback(() => { onClick={useCallback(() => {
onClick(meta.id); onClick(meta);
}, [onClick, meta.id])} }, [onClick, meta])}
active={workspace.id === currentWorkspaceId} active={meta.id === currentWorkspaceId}
> >
<Avatar size={28} url={workspaceAvatar} name={name} colorfulFallback /> <Avatar size={28} url={avatarUrl} name={name} colorfulFallback />
<StyledWorkspaceInfo> <StyledWorkspaceInfo>
<StyledWorkspaceTitleArea style={{ display: 'flex' }}> <StyledWorkspaceTitleArea style={{ display: 'flex' }}>
<StyledWorkspaceTitle>{name}</StyledWorkspaceTitle> <StyledWorkspaceTitle>
{information?.name ?? UNTITLED_WORKSPACE_NAME}
</StyledWorkspaceTitle>
<StyledSettingLink <StyledSettingLink
size="small" size="small"
className="setting-entry" className="setting-entry"
onClick={e => { onClick={e => {
e.stopPropagation(); e.stopPropagation();
onSettingClick(meta.id); onSettingClick(meta);
}} }}
withoutHoverStyle={true} withoutHoverStyle={true}
> >

View File

@@ -12,7 +12,7 @@ import { expect, test } from 'vitest';
import { createDefaultFilter, vars } from '../filter/vars'; import { createDefaultFilter, vars } from '../filter/vars';
import { import {
type CollectionsCRUDAtom, type CollectionsCRUD,
useCollectionManager, useCollectionManager,
} from '../use-collection-manager'; } from '../use-collection-manager';
@@ -27,18 +27,18 @@ const baseAtom = atomWithObservable<Collection[]>(
} }
); );
const mockAtom: CollectionsCRUDAtom = atom(get => { const mockAtom = atom(get => {
return { return {
collections: get(baseAtom), collections: get(baseAtom),
addCollection: async (...collections) => { addCollection: (...collections) => {
const prev = collectionsSubject.value; const prev = collectionsSubject.value;
collectionsSubject.next([...collections, ...prev]); collectionsSubject.next([...collections, ...prev]);
}, },
deleteCollection: async (...ids) => { deleteCollection: (...ids) => {
const prev = collectionsSubject.value; const prev = collectionsSubject.value;
collectionsSubject.next(prev.filter(v => !ids.includes(v.id))); collectionsSubject.next(prev.filter(v => !ids.includes(v.id)));
}, },
updateCollection: async (id, updater) => { updateCollection: (id, updater) => {
const prev = collectionsSubject.value; const prev = collectionsSubject.value;
collectionsSubject.next( collectionsSubject.next(
prev.map(v => { prev.map(v => {
@@ -49,14 +49,14 @@ const mockAtom: CollectionsCRUDAtom = atom(get => {
}) })
); );
}, },
}; } satisfies CollectionsCRUD;
}); });
test('useAllPageSetting', async () => { test('useAllPageSetting', async () => {
const settingHook = renderHook(() => useCollectionManager(mockAtom)); const settingHook = renderHook(() => useCollectionManager(mockAtom));
const prevCollection = settingHook.result.current.currentCollection; const prevCollection = settingHook.result.current.currentCollection;
expect(settingHook.result.current.savedCollections).toEqual([]); expect(settingHook.result.current.savedCollections).toEqual([]);
await settingHook.result.current.updateCollection({ settingHook.result.current.updateCollection({
...settingHook.result.current.currentCollection, ...settingHook.result.current.currentCollection,
filterList: [createDefaultFilter(vars[0], defaultMeta)], filterList: [createDefaultFilter(vars[0], defaultMeta)],
}); });
@@ -66,7 +66,7 @@ test('useAllPageSetting', async () => {
expect(nextCollection.filterList).toEqual([ expect(nextCollection.filterList).toEqual([
createDefaultFilter(vars[0], defaultMeta), createDefaultFilter(vars[0], defaultMeta),
]); ]);
await settingHook.result.current.createCollection({ settingHook.result.current.createCollection({
...settingHook.result.current.currentCollection, ...settingHook.result.current.currentCollection,
id: '1', id: '1',
}); });

View File

@@ -33,22 +33,21 @@ export const currentCollectionAtom = atomWithReset<string>(NIL);
export type Updater<T> = (value: T) => T; export type Updater<T> = (value: T) => T;
export type CollectionUpdater = Updater<Collection>; export type CollectionUpdater = Updater<Collection>;
export type CollectionsCRUD = { export type CollectionsCRUD = {
addCollection: (...collections: Collection[]) => Promise<void>; addCollection: (...collections: Collection[]) => void;
collections: Collection[]; collections: Collection[];
updateCollection: (id: string, updater: CollectionUpdater) => Promise<void>; updateCollection: (id: string, updater: CollectionUpdater) => void;
deleteCollection: ( deleteCollection: (info: DeleteCollectionInfo, ...ids: string[]) => void;
info: DeleteCollectionInfo,
...ids: string[]
) => Promise<void>;
}; };
export type CollectionsCRUDAtom = Atom<CollectionsCRUD>; export type CollectionsCRUDAtom = Atom<
Promise<CollectionsCRUD> | CollectionsCRUD
>;
export const useSavedCollections = (collectionAtom: CollectionsCRUDAtom) => { export const useSavedCollections = (collectionAtom: CollectionsCRUDAtom) => {
const [{ collections, addCollection, deleteCollection, updateCollection }] = const [{ collections, addCollection, deleteCollection, updateCollection }] =
useAtom(collectionAtom); useAtom(collectionAtom);
const addPage = useCallback( const addPage = useCallback(
async (collectionId: string, pageId: string) => { (collectionId: string, pageId: string) => {
await updateCollection(collectionId, old => { updateCollection(collectionId, old => {
return { return {
...old, ...old,
allowList: [pageId, ...(old.allowList ?? [])], allowList: [pageId, ...(old.allowList ?? [])],
@@ -79,11 +78,11 @@ export const useCollectionManager = (collectionsAtom: CollectionsCRUDAtom) => {
defaultCollectionAtom defaultCollectionAtom
); );
const update = useCallback( const update = useCallback(
async (collection: Collection) => { (collection: Collection) => {
if (collection.id === NIL) { if (collection.id === NIL) {
updateDefaultCollection(collection); updateDefaultCollection(collection);
} else { } else {
await updateCollection(collection.id, () => collection); updateCollection(collection.id, () => collection);
} }
}, },
[updateDefaultCollection, updateCollection] [updateDefaultCollection, updateCollection]

View File

@@ -35,14 +35,10 @@ export const CollectionList = ({
const [collection, setCollection] = useState<Collection>(); const [collection, setCollection] = useState<Collection>();
const onChange = useCallback( const onChange = useCallback(
(filterList: Filter[]) => { (filterList: Filter[]) => {
setting setting.updateCollection({
.updateCollection({ ...setting.currentCollection,
...setting.currentCollection, filterList,
filterList, });
})
.catch(err => {
console.error(err);
});
}, },
[setting] [setting]
); );
@@ -53,8 +49,8 @@ export const CollectionList = ({
}, []); }, []);
const onConfirm = useCallback( const onConfirm = useCallback(
async (view: Collection) => { (view: Collection) => {
await setting.updateCollection(view); setting.updateCollection(view);
closeUpdateCollectionModal(false); closeUpdateCollectionModal(false);
}, },
[closeUpdateCollectionModal, setting] [closeUpdateCollectionModal, setting]

View File

@@ -107,9 +107,7 @@ export const CollectionOperations = ({
), ),
name: t['Delete'](), name: t['Delete'](),
click: () => { click: () => {
setting.deleteCollection(info, collection.id).catch(err => { setting.deleteCollection(info, collection.id);
console.error(err);
});
}, },
type: 'danger', type: 'danger',
}, },

View File

@@ -10,7 +10,7 @@ export interface CreateCollectionModalProps {
title?: string; title?: string;
onConfirmText?: string; onConfirmText?: string;
init: string; init: string;
onConfirm: (title: string) => Promise<void>; onConfirm: (title: string) => void;
open: boolean; open: boolean;
showTips?: boolean; showTips?: boolean;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
@@ -27,13 +27,8 @@ export const CreateCollectionModal = ({
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const onConfirmTitle = useCallback( const onConfirmTitle = useCallback(
(title: string) => { (title: string) => {
onConfirm(title) onConfirm(title);
.then(() => { onOpenChange(false);
onOpenChange(false);
})
.catch(err => {
console.error(err);
});
}, },
[onConfirm, onOpenChange] [onConfirm, onOpenChange]
); );

View File

@@ -19,7 +19,7 @@ export interface EditCollectionModalProps {
open: boolean; open: boolean;
mode?: EditCollectionMode; mode?: EditCollectionMode;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
onConfirm: (view: Collection) => Promise<void>; onConfirm: (view: Collection) => void;
allPageListConfig: AllPageListConfig; allPageListConfig: AllPageListConfig;
} }
@@ -45,13 +45,7 @@ export const EditCollectionModal = ({
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const onConfirmOnCollection = useCallback( const onConfirmOnCollection = useCallback(
(view: Collection) => { (view: Collection) => {
onConfirm(view) onConfirm(view);
.then(() => {
onOpenChange(false);
})
.catch(err => {
console.error(err);
});
onOpenChange(false); onOpenChange(false);
}, },
[onConfirm, onOpenChange] [onConfirm, onOpenChange]

View File

@@ -9,7 +9,7 @@ import { createEmptyCollection } from '../use-collection-manager';
import { useEditCollectionName } from './use-edit-collection'; import { useEditCollectionName } from './use-edit-collection';
interface SaveAsCollectionButtonProps { interface SaveAsCollectionButtonProps {
onConfirm: (collection: Collection) => Promise<void>; onConfirm: (collection: Collection) => void;
} }
export const SaveAsCollectionButton = ({ export const SaveAsCollectionButton = ({

View File

@@ -40,9 +40,7 @@ export const useActions = ({
name: 'delete', name: 'delete',
tooltip: t['com.affine.collection-bar.action.tooltip.delete'](), tooltip: t['com.affine.collection-bar.action.tooltip.delete'](),
click: () => { click: () => {
setting.deleteCollection(info, collection.id).catch(err => { setting.deleteCollection(info, collection.id);
console.error(err);
});
}, },
}, },
]; ];

View File

@@ -12,7 +12,7 @@ export const useEditCollection = (config: AllPageListConfig) => {
const [data, setData] = useState<{ const [data, setData] = useState<{
collection: Collection; collection: Collection;
mode?: 'page' | 'rule'; mode?: 'page' | 'rule';
onConfirm: (collection: Collection) => Promise<void>; onConfirm: (collection: Collection) => void;
}>(); }>();
const close = useCallback(() => setData(undefined), []); const close = useCallback(() => setData(undefined), []);
@@ -35,7 +35,7 @@ export const useEditCollection = (config: AllPageListConfig) => {
setData({ setData({
collection, collection,
mode, mode,
onConfirm: async collection => { onConfirm: collection => {
res(collection); res(collection);
}, },
}); });
@@ -52,7 +52,7 @@ export const useEditCollectionName = ({
}) => { }) => {
const [data, setData] = useState<{ const [data, setData] = useState<{
name: string; name: string;
onConfirm: (name: string) => Promise<void>; onConfirm: (name: string) => void;
}>(); }>();
const close = useCallback(() => setData(undefined), []); const close = useCallback(() => setData(undefined), []);
@@ -71,7 +71,7 @@ export const useEditCollectionName = ({
new Promise<string>(res => { new Promise<string>(res => {
setData({ setData({
name, name,
onConfirm: async collection => { onConfirm: collection => {
res(collection); res(collection);
}, },
}); });

View File

@@ -1,8 +1,4 @@
import type { import type { WorkspaceMetadata } from '@affine/workspace';
AffineCloudWorkspace,
LocalWorkspace,
} from '@affine/env/workspace';
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
import type { DragEndEvent } from '@dnd-kit/core'; import type { DragEndEvent } from '@dnd-kit/core';
import { import {
DndContext, DndContext,
@@ -26,17 +22,17 @@ import { workspaceItemStyle } from './index.css';
export interface WorkspaceListProps { export interface WorkspaceListProps {
disabled?: boolean; disabled?: boolean;
currentWorkspaceId: string | null; currentWorkspaceId?: string | null;
items: (AffineCloudWorkspace | LocalWorkspace)[]; items: WorkspaceMetadata[];
onClick: (workspaceId: string) => void; onClick: (workspaceMetadata: WorkspaceMetadata) => void;
onSettingClick: (workspaceId: string) => void; onSettingClick: (workspaceMetadata: WorkspaceMetadata) => void;
onDragEnd: (event: DragEndEvent) => void; onDragEnd: (event: DragEndEvent) => void;
useIsWorkspaceOwner?: (workspaceId: string) => boolean; useIsWorkspaceOwner?: (workspaceMetadata: WorkspaceMetadata) => boolean;
} }
interface SortableWorkspaceItemProps extends Omit<WorkspaceListProps, 'items'> { interface SortableWorkspaceItemProps extends Omit<WorkspaceListProps, 'items'> {
item: RootWorkspaceMetadata; item: WorkspaceMetadata;
useIsWorkspaceOwner?: (workspaceId: string) => boolean; useIsWorkspaceOwner?: (workspaceMetadata: WorkspaceMetadata) => boolean;
} }
const SortableWorkspaceItem = ({ const SortableWorkspaceItem = ({
@@ -62,7 +58,7 @@ const SortableWorkspaceItem = ({
}), }),
[disabled, transform, transition] [disabled, transform, transition]
); );
const isOwner = useIsWorkspaceOwner?.(item.id); const isOwner = useIsWorkspaceOwner?.(item);
return ( return (
<div <div
className={workspaceItemStyle} className={workspaceItemStyle}

View File

@@ -59,6 +59,7 @@
"foxact": "^0.2.20", "foxact": "^0.2.20",
"graphql": "^16.8.1", "graphql": "^16.8.1",
"idb": "^8.0.0", "idb": "^8.0.0",
"image-blob-reduce": "^4.1.0",
"intl-segmenter-polyfill-rs": "^0.1.6", "intl-segmenter-polyfill-rs": "^0.1.6",
"jotai": "^2.5.1", "jotai": "^2.5.1",
"jotai-devtools": "^0.7.0", "jotai-devtools": "^0.7.0",
@@ -93,6 +94,7 @@
"@swc/core": "^1.3.93", "@swc/core": "^1.3.93",
"@testing-library/react": "^14.0.0", "@testing-library/react": "^14.0.0",
"@types/bytes": "^3.1.3", "@types/bytes": "^3.1.3",
"@types/image-blob-reduce": "^4.1.3",
"@types/lodash-es": "^4.17.9", "@types/lodash-es": "^4.17.9",
"@types/uuid": "^9.0.5", "@types/uuid": "^9.0.5",
"@types/webpack-env": "^1.18.2", "@types/webpack-env": "^1.18.2",

View File

@@ -18,7 +18,7 @@ import { bootstrapPluginSystem } from '../bootstrap/register-plugins';
async function main() { async function main() {
const { setup } = await import('../bootstrap/setup'); const { setup } = await import('../bootstrap/setup');
const rootStore = getCurrentStore(); const rootStore = getCurrentStore();
await setup(rootStore); setup();
const { _pluginNestedImportsMap } = createSetup(rootStore); const { _pluginNestedImportsMap } = createSetup(rootStore);
const pluginRegisterPromise = bootstrapPluginSystem(rootStore); const pluginRegisterPromise = bootstrapPluginSystem(rootStore);
const root = document.getElementById('app'); const root = document.getElementById('app');

View File

@@ -1,11 +1,7 @@
import type { import type { WorkspaceUISchema } from '@affine/env/workspace';
WorkspaceFlavour,
WorkspaceUISchema,
} from '@affine/env/workspace';
import { lazy } from 'react'; import { lazy } from 'react';
import { useIsWorkspaceOwner } from '../../hooks/affine/use-is-workspace-owner'; import { Provider } from '../shared';
import { NewWorkspaceSettingDetail, Provider } from '../shared';
const LoginCard = lazy(() => const LoginCard = lazy(() =>
import('../../components/cloud/login-card').then(({ LoginCard }) => ({ import('../../components/cloud/login-card').then(({ LoginCard }) => ({
@@ -16,23 +12,4 @@ const LoginCard = lazy(() =>
export const UI = { export const UI = {
Provider, Provider,
LoginCard, LoginCard,
NewSettingsDetail: ({ } satisfies WorkspaceUISchema;
currentWorkspaceId,
onTransformWorkspace,
onDeleteLocalWorkspace,
onDeleteCloudWorkspace,
onLeaveWorkspace,
}) => {
const isOwner = useIsWorkspaceOwner(currentWorkspaceId);
return (
<NewWorkspaceSettingDetail
onDeleteLocalWorkspace={onDeleteLocalWorkspace}
onDeleteCloudWorkspace={onDeleteCloudWorkspace}
onLeaveWorkspace={onLeaveWorkspace}
workspaceId={currentWorkspaceId}
onTransferWorkspace={onTransformWorkspace}
isOwner={isOwner}
/>
);
},
} satisfies WorkspaceUISchema<WorkspaceFlavour.AFFINE_CLOUD>;

View File

@@ -1,81 +1,17 @@
import { DebugLogger } from '@affine/debug';
import { DEFAULT_WORKSPACE_NAME } from '@affine/env/constant';
import type { WorkspaceAdapter } from '@affine/env/workspace'; import type { WorkspaceAdapter } from '@affine/env/workspace';
import { import {
LoadPriority, LoadPriority,
ReleaseType, ReleaseType,
WorkspaceFlavour, WorkspaceFlavour,
} from '@affine/env/workspace'; } from '@affine/env/workspace';
import {
CRUD,
saveWorkspaceToLocalStorage,
} from '@affine/workspace/local/crud';
import { getOrCreateWorkspace } from '@affine/workspace/manager';
import { getCurrentStore } from '@toeverything/infra/atom';
import { initEmptyPage } from '@toeverything/infra/blocksuite';
import { buildShowcaseWorkspace } from '@toeverything/infra/blocksuite';
import { nanoid } from 'nanoid';
import { setPageModeAtom } from '../../atoms'; import { Provider } from '../shared';
import { NewWorkspaceSettingDetail, Provider } from '../shared';
const logger = new DebugLogger('use-create-first-workspace');
export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = { export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
releaseType: ReleaseType.STABLE, releaseType: ReleaseType.STABLE,
flavour: WorkspaceFlavour.LOCAL, flavour: WorkspaceFlavour.LOCAL,
loadPriority: LoadPriority.LOW, loadPriority: LoadPriority.LOW,
Events: {
'app:access': async () => true,
'app:init': () => {
const blockSuiteWorkspace = getOrCreateWorkspace(
nanoid(),
WorkspaceFlavour.LOCAL
);
blockSuiteWorkspace.meta.setName(DEFAULT_WORKSPACE_NAME);
if (runtimeConfig.enablePreloading) {
buildShowcaseWorkspace(blockSuiteWorkspace, {
store: getCurrentStore(),
atoms: {
pageMode: setPageModeAtom,
},
}).catch(err => {
logger.error('init page with preloading failed', err);
});
} else {
const page = blockSuiteWorkspace.createPage();
blockSuiteWorkspace.setPageMeta(page.id, {
jumpOnce: true,
});
initEmptyPage(page).catch(error => {
logger.error('init page with empty failed', error);
});
}
saveWorkspaceToLocalStorage(blockSuiteWorkspace.id);
logger.debug('create first workspace');
return [blockSuiteWorkspace.id];
},
},
CRUD,
UI: { UI: {
Provider, Provider,
NewSettingsDetail: ({
currentWorkspaceId,
onTransformWorkspace,
onDeleteLocalWorkspace,
onDeleteCloudWorkspace,
onLeaveWorkspace,
}) => {
return (
<NewWorkspaceSettingDetail
onDeleteLocalWorkspace={onDeleteLocalWorkspace}
onDeleteCloudWorkspace={onDeleteCloudWorkspace}
onLeaveWorkspace={onLeaveWorkspace}
workspaceId={currentWorkspaceId}
onTransferWorkspace={onTransformWorkspace}
isOwner={true}
/>
);
},
}, },
}; };

View File

@@ -1,11 +0,0 @@
import type { WorkspaceFlavour } from '@affine/env/workspace';
import { type WorkspaceUISchema } from '@affine/env/workspace';
import { Provider } from '../shared';
export const UI = {
Provider,
NewSettingsDetail: () => {
throw new Error('Not implemented');
},
} satisfies WorkspaceUISchema<WorkspaceFlavour.AFFINE_PUBLIC>;

View File

@@ -1,6 +1,5 @@
import { Unreachable } from '@affine/env/constant'; import { Unreachable } from '@affine/env/constant';
import type { import type {
AppEvents,
WorkspaceAdapter, WorkspaceAdapter,
WorkspaceUISchema, WorkspaceUISchema,
} from '@affine/env/workspace'; } from '@affine/env/workspace';
@@ -9,19 +8,9 @@ import {
ReleaseType, ReleaseType,
WorkspaceFlavour, WorkspaceFlavour,
} from '@affine/env/workspace'; } from '@affine/env/workspace';
import { CRUD as CloudCRUD } from '@affine/workspace/affine/crud';
import { UI as CloudUI } from './cloud/ui'; import { UI as CloudUI } from './cloud/ui';
import { LocalAdapter } from './local'; import { LocalAdapter } from './local';
import { UI as PublicCloudUI } from './public-cloud/ui';
const unimplemented = () => {
throw new Error('Not implemented');
};
const bypassList = async () => {
return [];
};
export const WorkspaceAdapters = { export const WorkspaceAdapters = {
[WorkspaceFlavour.LOCAL]: LocalAdapter, [WorkspaceFlavour.LOCAL]: LocalAdapter,
@@ -29,43 +18,16 @@ export const WorkspaceAdapters = {
releaseType: ReleaseType.UNRELEASED, releaseType: ReleaseType.UNRELEASED,
flavour: WorkspaceFlavour.AFFINE_CLOUD, flavour: WorkspaceFlavour.AFFINE_CLOUD,
loadPriority: LoadPriority.HIGH, loadPriority: LoadPriority.HIGH,
Events: {
'app:access': async () => {
try {
const { getSession } = await import('next-auth/react');
const session = await getSession();
return !!session;
} catch (e) {
console.error('failed to get session', e);
return false;
}
},
} as Partial<AppEvents>,
CRUD: CloudCRUD,
UI: CloudUI, UI: CloudUI,
}, },
[WorkspaceFlavour.AFFINE_PUBLIC]: {
releaseType: ReleaseType.UNRELEASED,
flavour: WorkspaceFlavour.AFFINE_PUBLIC,
loadPriority: LoadPriority.LOW,
Events: {} as Partial<AppEvents>,
// todo: implement this
CRUD: {
get: unimplemented,
list: bypassList,
delete: unimplemented,
create: unimplemented,
},
UI: PublicCloudUI,
},
} satisfies { } satisfies {
[Key in WorkspaceFlavour]: WorkspaceAdapter<Key>; [Key in WorkspaceFlavour]: WorkspaceAdapter<Key>;
}; };
export function getUIAdapter<Flavour extends WorkspaceFlavour>( export function getUIAdapter<Flavour extends WorkspaceFlavour>(
flavour: Flavour flavour: Flavour
): WorkspaceUISchema<Flavour> { ): WorkspaceUISchema {
const ui = WorkspaceAdapters[flavour].UI as WorkspaceUISchema<Flavour>; const ui = WorkspaceAdapters[flavour].UI as WorkspaceUISchema;
if (!ui) { if (!ui) {
throw new Unreachable(); throw new Unreachable();
} }

View File

@@ -5,9 +5,9 @@ import { AffineContext } from '@affine/component/context';
import { GlobalLoading } from '@affine/component/global-loading'; import { GlobalLoading } from '@affine/component/global-loading';
import { NotificationCenter } from '@affine/component/notification-center'; import { NotificationCenter } from '@affine/component/notification-center';
import { WorkspaceFallback } from '@affine/component/workspace'; import { WorkspaceFallback } from '@affine/component/workspace';
import { createI18n, setUpLanguage } from '@affine/i18n';
import { CacheProvider } from '@emotion/react'; import { CacheProvider } from '@emotion/react';
import { getCurrentStore } from '@toeverything/infra/atom'; import { getCurrentStore } from '@toeverything/infra/atom';
import { use } from 'foxact/use';
import type { PropsWithChildren, ReactElement } from 'react'; import type { PropsWithChildren, ReactElement } from 'react';
import { lazy, memo, Suspense } from 'react'; import { lazy, memo, Suspense } from 'react';
import { RouterProvider } from 'react-router-dom'; import { RouterProvider } from 'react-router-dom';
@@ -41,7 +41,6 @@ async function loadLanguage() {
if (environment.isBrowser) { if (environment.isBrowser) {
performanceI18nLogger.info('start'); performanceI18nLogger.info('start');
const { createI18n, setUpLanguage } = await import('@affine/i18n');
const i18n = createI18n(); const i18n = createI18n();
document.documentElement.lang = i18n.language; document.documentElement.lang = i18n.language;
@@ -51,12 +50,15 @@ async function loadLanguage() {
} }
} }
const languageLoadingPromise = loadLanguage().catch(console.error); let languageLoadingPromise: Promise<void> | null = null;
export const App = memo(function App() { export const App = memo(function App() {
performanceRenderLogger.info('App'); performanceRenderLogger.info('App');
use(languageLoadingPromise); if (!languageLoadingPromise) {
languageLoadingPromise = loadLanguage().catch(console.error);
}
return ( return (
<CacheProvider value={cache}> <CacheProvider value={cache}>
<AffineContext store={getCurrentStore()}> <AffineContext store={getCurrentStore()}>

View File

@@ -1,12 +1,18 @@
import type { CollectionsCRUDAtom } from '@affine/component/page-list'; import type {
CollectionsCRUD,
CollectionsCRUDAtom,
} from '@affine/component/page-list';
import type { Collection, DeprecatedCollection } from '@affine/env/filter'; import type { Collection, DeprecatedCollection } from '@affine/env/filter';
import {
currentWorkspaceAtom,
waitForCurrentWorkspaceAtom,
} from '@affine/workspace/atom';
import { DisposableGroup } from '@blocksuite/global/utils'; import { DisposableGroup } from '@blocksuite/global/utils';
import type { Workspace } from '@blocksuite/store'; import type { Workspace } from '@blocksuite/store';
import { currentWorkspaceAtom } from '@toeverything/infra/atom';
import { type DBSchema, openDB } from 'idb'; import { type DBSchema, openDB } from 'idb';
import { atom } from 'jotai'; import { atom } from 'jotai';
import { atomWithObservable } from 'jotai/utils'; import { atomWithObservable } from 'jotai/utils';
import { Observable } from 'rxjs'; import { Observable, of } from 'rxjs';
import { getUserSetting } from '../utils/user-setting'; import { getUserSetting } from '../utils/user-setting';
import { getWorkspaceSetting } from '../utils/workspace-setting'; import { getWorkspaceSetting } from '../utils/workspace-setting';
@@ -95,7 +101,11 @@ type BaseCollectionsDataType = {
export const pageCollectionBaseAtom = export const pageCollectionBaseAtom =
atomWithObservable<BaseCollectionsDataType>( atomWithObservable<BaseCollectionsDataType>(
get => { get => {
const currentWorkspacePromise = get(currentWorkspaceAtom); const currentWorkspace = get(currentWorkspaceAtom);
if (!currentWorkspace) {
return of({ loading: true, collections: [] });
}
const session = get(sessionAtom); const session = get(sessionAtom);
const userId = session?.data?.user.id ?? null; const userId = session?.data?.user.id ?? null;
const migrateCollectionsFromIdbData = async ( const migrateCollectionsFromIdbData = async (
@@ -149,48 +159,44 @@ export const pageCollectionBaseAtom =
return new Observable<BaseCollectionsDataType>(subscriber => { return new Observable<BaseCollectionsDataType>(subscriber => {
const group = new DisposableGroup(); const group = new DisposableGroup();
currentWorkspacePromise const workspaceSetting = getWorkspaceSetting(
.then(async currentWorkspace => { currentWorkspace.blockSuiteWorkspace
const workspaceSetting = getWorkspaceSetting(currentWorkspace); );
migrateCollectionsFromIdbData(currentWorkspace) migrateCollectionsFromIdbData(currentWorkspace.blockSuiteWorkspace)
.then(collections => { .then(collections => {
if (collections.length) { if (collections.length) {
workspaceSetting.addCollection(...collections); workspaceSetting.addCollection(...collections);
}
})
.catch(error => {
console.error(error);
});
migrateCollectionsFromUserData(currentWorkspace)
.then(collections => {
if (collections.length) {
workspaceSetting.addCollection(...collections);
}
})
.catch(error => {
console.error(error);
});
subscriber.next({
loading: false,
collections: workspaceSetting.collections,
});
if (group.disposed) {
return;
} }
const fn = () => {
subscriber.next({
loading: false,
collections: workspaceSetting.collections,
});
};
workspaceSetting.collectionsYArray.observe(fn);
group.add(() => {
workspaceSetting.collectionsYArray.unobserve(fn);
});
}) })
.catch(error => { .catch(error => {
subscriber.error(error); console.error(error);
}); });
migrateCollectionsFromUserData(currentWorkspace.blockSuiteWorkspace)
.then(collections => {
if (collections.length) {
workspaceSetting.addCollection(...collections);
}
})
.catch(error => {
console.error(error);
});
subscriber.next({
loading: false,
collections: workspaceSetting.collections,
});
if (group.disposed) {
return;
}
const fn = () => {
subscriber.next({
loading: false,
collections: workspaceSetting.collections,
});
};
workspaceSetting.collectionsYArray.observe(fn);
group.add(() => {
workspaceSetting.collectionsYArray.unobserve(fn);
});
return () => { return () => {
group.dispose(); group.dispose();
@@ -199,21 +205,27 @@ export const pageCollectionBaseAtom =
}, },
{ initialValue: { loading: true, collections: [] } } { initialValue: { loading: true, collections: [] } }
); );
export const collectionsCRUDAtom: CollectionsCRUDAtom = atom(get => {
const workspacePromise = get(currentWorkspaceAtom); export const collectionsCRUDAtom: CollectionsCRUDAtom = atom(async get => {
const workspace = await get(waitForCurrentWorkspaceAtom);
return { return {
addCollection: async (...collections) => { addCollection: (...collections) => {
const workspace = await workspacePromise; getWorkspaceSetting(workspace.blockSuiteWorkspace).addCollection(
getWorkspaceSetting(workspace).addCollection(...collections); ...collections
);
}, },
collections: get(pageCollectionBaseAtom).collections, collections: get(pageCollectionBaseAtom).collections,
updateCollection: async (id, updater) => { updateCollection: (id, updater) => {
const workspace = await workspacePromise; getWorkspaceSetting(workspace.blockSuiteWorkspace).updateCollection(
getWorkspaceSetting(workspace).updateCollection(id, updater); id,
updater
);
}, },
deleteCollection: async (info, ...ids) => { deleteCollection: (info, ...ids) => {
const workspace = await workspacePromise; getWorkspaceSetting(workspace.blockSuiteWorkspace).deleteCollection(
getWorkspaceSetting(workspace).deleteCollection(info, ...ids); info,
...ids
);
}, },
}; } satisfies CollectionsCRUD;
}); });

View File

@@ -14,13 +14,15 @@ export const openOnboardingModalAtom = atom(false);
export const openSignOutModalAtom = atom(false); export const openSignOutModalAtom = atom(false);
export const openPaymentDisableAtom = atom(false); export const openPaymentDisableAtom = atom(false);
export type SettingAtom = Pick<SettingProps, 'activeTab' | 'workspaceId'> & { export type SettingAtom = Pick<
SettingProps,
'activeTab' | 'workspaceMetadata'
> & {
open: boolean; open: boolean;
}; };
export const openSettingModalAtom = atom<SettingAtom>({ export const openSettingModalAtom = atom<SettingAtom>({
activeTab: 'appearance', activeTab: 'appearance',
workspaceId: null,
open: false, open: false,
}); });

View File

@@ -0,0 +1,43 @@
import { DebugLogger } from '@affine/debug';
import { DEFAULT_WORKSPACE_NAME } from '@affine/env/constant';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { workspaceManager } from '@affine/workspace';
import { getCurrentStore } from '@toeverything/infra/atom';
import {
buildShowcaseWorkspace,
initEmptyPage,
} from '@toeverything/infra/blocksuite';
import { setPageModeAtom } from '../atoms';
const logger = new DebugLogger('affine:first-app-data');
export async function createFirstAppData() {
if (localStorage.getItem('is-first-open') !== null) {
return;
}
localStorage.setItem('is-first-open', 'false');
const workspaceId = await workspaceManager.createWorkspace(
WorkspaceFlavour.LOCAL,
async workspace => {
workspace.meta.setName(DEFAULT_WORKSPACE_NAME);
if (runtimeConfig.enablePreloading) {
await buildShowcaseWorkspace(workspace, {
store: getCurrentStore(),
atoms: {
pageMode: setPageModeAtom,
},
});
} else {
const page = workspace.createPage();
workspace.setPageMeta(page.id, {
jumpOnce: true,
});
await initEmptyPage(page);
}
logger.debug('create first workspace');
}
);
console.info('create first workspace', workspaceId);
return workspaceId;
}

View File

@@ -14,11 +14,7 @@ import {
pluginSettingAtom, pluginSettingAtom,
pluginWindowAtom, pluginWindowAtom,
} from '@toeverything/infra/__internal__/plugin'; } from '@toeverything/infra/__internal__/plugin';
import { import { contentLayoutAtom, currentPageIdAtom } from '@toeverything/infra/atom';
contentLayoutAtom,
currentPageIdAtom,
currentWorkspaceAtom,
} from '@toeverything/infra/atom';
import { atom } from 'jotai'; import { atom } from 'jotai';
import { Provider } from 'jotai/react'; import { Provider } from 'jotai/react';
import type { createStore } from 'jotai/vanilla'; import type { createStore } from 'jotai/vanilla';
@@ -149,7 +145,6 @@ function createSetupImpl(rootStore: ReturnType<typeof createStore>) {
'@blocksuite/inline': import('@blocksuite/inline'), '@blocksuite/inline': import('@blocksuite/inline'),
'@affine/sdk/entry': { '@affine/sdk/entry': {
rootStore, rootStore,
currentWorkspaceAtom: currentWorkspaceAtom,
currentPageIdAtom: currentPageIdAtom, currentPageIdAtom: currentPageIdAtom,
pushLayoutAtom: pushLayoutAtom, pushLayoutAtom: pushLayoutAtom,
deleteLayoutAtom: deleteLayoutAtom, deleteLayoutAtom: deleteLayoutAtom,

View File

@@ -1,15 +1,7 @@
import './register-blocksuite-components'; import './register-blocksuite-components';
import { setupGlobal } from '@affine/env/global'; import { setupGlobal } from '@affine/env/global';
import type { WorkspaceAdapter } from '@affine/env/workspace';
import type { WorkspaceFlavour } from '@affine/env/workspace';
import {
type RootWorkspaceMetadataV2,
rootWorkspacesMetadataAtom,
workspaceAdaptersAtom,
} from '@affine/workspace/atom';
import * as Sentry from '@sentry/react'; import * as Sentry from '@sentry/react';
import type { createStore } from 'jotai/vanilla';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { import {
createRoutesFromChildren, createRoutesFromChildren,
@@ -18,45 +10,12 @@ import {
useNavigationType, useNavigationType,
} from 'react-router-dom'; } from 'react-router-dom';
import { WorkspaceAdapters } from '../adapters/workspace';
import { performanceLogger } from '../shared'; import { performanceLogger } from '../shared';
const performanceSetupLogger = performanceLogger.namespace('setup'); const performanceSetupLogger = performanceLogger.namespace('setup');
export function createFirstAppData(store: ReturnType<typeof createStore>) { export function setup() {
const createFirst = (): RootWorkspaceMetadataV2[] => {
const Plugins = Object.values(WorkspaceAdapters).sort(
(a, b) => a.loadPriority - b.loadPriority
);
return Plugins.flatMap(Plugin => {
return Plugin.Events['app:init']?.().map(
id =>
<RootWorkspaceMetadataV2>{
id,
flavour: Plugin.flavour,
}
);
}).filter((ids): ids is RootWorkspaceMetadataV2 => !!ids);
};
if (localStorage.getItem('is-first-open') !== null) {
return;
}
const result = createFirst();
console.info('create first workspace', result);
localStorage.setItem('is-first-open', 'false');
store.set(rootWorkspacesMetadataAtom, result);
}
export async function setup(store: ReturnType<typeof createStore>) {
performanceSetupLogger.info('start'); performanceSetupLogger.info('start');
store.set(
workspaceAdaptersAtom,
WorkspaceAdapters as Record<
WorkspaceFlavour,
WorkspaceAdapter<WorkspaceFlavour>
>
);
performanceSetupLogger.info('setup global'); performanceSetupLogger.info('setup global');
setupGlobal(); setupGlobal();
@@ -88,9 +47,5 @@ export async function setup(store: ReturnType<typeof createStore>) {
}); });
} }
performanceSetupLogger.info('get root workspace meta');
// do not read `rootWorkspacesMetadataAtom` before migration
await store.get(rootWorkspacesMetadataAtom);
performanceSetupLogger.info('done'); performanceSetupLogger.info('done');
} }

View File

@@ -34,7 +34,7 @@ export function registerAffineHelpCommands({
store.set(openSettingModalAtom, { store.set(openSettingModalAtom, {
open: true, open: true,
activeTab: 'about', activeTab: 'about',
workspaceId: null, workspaceMetadata: null,
}); });
}, },
}) })

View File

@@ -94,7 +94,6 @@ export function registerAffineNavigationCommands({
run() { run() {
store.set(openSettingModalAtom, { store.set(openSettingModalAtom, {
activeTab: 'appearance', activeTab: 'appearance',
workspaceId: null,
open: true, open: true,
}); });
}, },

View File

@@ -1,11 +1,12 @@
import { waitForCurrentWorkspaceAtom } from '@affine/workspace/atom';
import { assertExists } from '@blocksuite/global/utils'; import { assertExists } from '@blocksuite/global/utils';
import { useAtomValue } from 'jotai';
import type { FC, PropsWithChildren } from 'react'; import type { FC, PropsWithChildren } from 'react';
import { WorkspaceAdapters } from '../adapters/workspace'; import { WorkspaceAdapters } from '../adapters/workspace';
import { useCurrentWorkspace } from '../hooks/current/use-current-workspace';
export const AdapterProviderWrapper: FC<PropsWithChildren> = ({ children }) => { export const AdapterProviderWrapper: FC<PropsWithChildren> = ({ children }) => {
const [currentWorkspace] = useCurrentWorkspace(); const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const Provider = WorkspaceAdapters[currentWorkspace.flavour].UI.Provider; const Provider = WorkspaceAdapters[currentWorkspace.flavour].UI.Provider;
assertExists(Provider); assertExists(Provider);

View File

@@ -1,8 +1,8 @@
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { import {
currentPageIdAtom, currentWorkspaceAtom,
currentWorkspaceIdAtom, workspaceListAtom,
} from '@toeverything/infra/atom'; } from '@affine/workspace/atom';
import { currentPageIdAtom } from '@toeverything/infra/atom';
import { useAtomValue } from 'jotai/react'; import { useAtomValue } from 'jotai/react';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useLocation, useParams } from 'react-router-dom'; import { useLocation, useParams } from 'react-router-dom';
@@ -13,8 +13,8 @@ export interface DumpInfoProps {
export const DumpInfo = (_props: DumpInfoProps) => { export const DumpInfo = (_props: DumpInfoProps) => {
const location = useLocation(); const location = useLocation();
const metadata = useAtomValue(rootWorkspacesMetadataAtom); const workspaceList = useAtomValue(workspaceListAtom);
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom); const currentWorkspace = useAtomValue(currentWorkspaceAtom);
const currentPageId = useAtomValue(currentPageIdAtom); const currentPageId = useAtomValue(currentPageIdAtom);
const path = location.pathname; const path = location.pathname;
const query = useParams(); const query = useParams();
@@ -22,10 +22,10 @@ export const DumpInfo = (_props: DumpInfoProps) => {
console.info('DumpInfo', { console.info('DumpInfo', {
path, path,
query, query,
currentWorkspaceId, currentWorkspaceId: currentWorkspace?.id,
currentPageId, currentPageId,
metadata, workspaceList,
}); });
}, [path, query, currentWorkspaceId, currentPageId, metadata]); }, [path, query, currentWorkspace, currentPageId, workspaceList]);
return null; return null;
}; };

View File

@@ -20,7 +20,6 @@ const UserPlanButtonWithData = () => {
setSettingModalAtom({ setSettingModalAtom({
open: true, open: true,
activeTab: 'plans', activeTab: 'plans',
workspaceId: null,
}); });
}, },
[setSettingModalAtom] [setSettingModalAtom]

View File

@@ -1,26 +1,33 @@
import { waitForCurrentWorkspaceAtom } from '@affine/workspace/atom';
import { useAtomValue } from 'jotai';
import { Suspense, useEffect } from 'react'; import { Suspense, useEffect } from 'react';
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status'; import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
import { useCurrentUser } from '../../../hooks/affine/use-current-user'; import { useCurrentUser } from '../../../hooks/affine/use-current-user';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
const SyncAwarenessInnerLoggedIn = () => { const SyncAwarenessInnerLoggedIn = () => {
const currentUser = useCurrentUser(); const currentUser = useCurrentUser();
const [{ blockSuiteWorkspace: workspace }] = useCurrentWorkspace(); const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
useEffect(() => { useEffect(() => {
if (currentUser && workspace) { if (currentUser && currentWorkspace) {
workspace.awarenessStore.awareness.setLocalStateField('user', { currentWorkspace.blockSuiteWorkspace.awarenessStore.awareness.setLocalStateField(
name: currentUser.name, 'user',
// todo: add avatar? {
}); name: currentUser.name,
// todo: add avatar?
}
);
return () => { return () => {
workspace.awarenessStore.awareness.setLocalStateField('user', null); currentWorkspace.blockSuiteWorkspace.awarenessStore.awareness.setLocalStateField(
'user',
null
);
}; };
} }
return; return;
}, [currentUser, workspace]); }, [currentUser, currentWorkspace]);
return null; return null;
}; };

View File

@@ -1,27 +1,26 @@
import { Input, toast } from '@affine/component'; import { Input, toast } from '@affine/component';
import { Button } from '@affine/component/ui/button';
import { import {
ConfirmModal, ConfirmModal,
type ConfirmModalProps, type ConfirmModalProps,
Modal, Modal,
} from '@affine/component/ui/modal'; } from '@affine/component/ui/modal';
import { Tooltip } from '@affine/component/ui/tooltip';
import { DebugLogger } from '@affine/debug'; import { DebugLogger } from '@affine/debug';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { HelpIcon } from '@blocksuite/icons'; import { workspaceManagerAtom } from '@affine/workspace/atom';
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks'; import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
import type { import { getCurrentStore } from '@toeverything/infra/atom';
LoadDBFileResult, import {
SelectDBFileLocationResult, buildShowcaseWorkspace,
} from '@toeverything/infra/type'; initEmptyPage,
import { useSetAtom } from 'jotai'; } from '@toeverything/infra/blocksuite';
import type { LoadDBFileResult } from '@toeverything/infra/type';
import { useAtomValue } from 'jotai';
import type { KeyboardEvent } from 'react'; import type { KeyboardEvent } from 'react';
import { useEffect } from 'react';
import { useLayoutEffect } from 'react'; import { useLayoutEffect } from 'react';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { openDisableCloudAlertModalAtom } from '../../../atoms'; import { setPageModeAtom } from '../../../atoms';
import { useAppHelper } from '../../../hooks/use-workspaces';
import * as style from './index.css'; import * as style from './index.css';
type CreateWorkspaceStep = type CreateWorkspaceStep =
@@ -94,159 +93,14 @@ const NameWorkspaceContent = ({
); );
}; };
interface SetDBLocationContentProps {
onConfirmLocation: (dir?: string) => void;
}
const useDefaultDBLocation = () => {
const [defaultDBLocation, setDefaultDBLocation] = useState('');
useEffect(() => {
window.apis?.db
.getDefaultStorageLocation()
.then(dir => {
setDefaultDBLocation(dir);
})
.catch(err => {
console.error(err);
});
}, []);
return defaultDBLocation;
};
const SetDBLocationContent = ({
onConfirmLocation,
}: SetDBLocationContentProps) => {
const t = useAFFiNEI18N();
const defaultDBLocation = useDefaultDBLocation();
const [opening, setOpening] = useState(false);
const handleSelectDBFileLocation = useCallback(() => {
if (opening) {
return;
}
setOpening(true);
(async function () {
const result: SelectDBFileLocationResult =
await window.apis?.dialog.selectDBFileLocation();
setOpening(false);
if (result?.filePath) {
onConfirmLocation(result.filePath);
} else if (result?.error) {
toast(t[result.error]());
}
})().catch(err => {
logger.error(err);
});
}, [onConfirmLocation, opening, t]);
return (
<div className={style.content}>
<div className={style.contentTitle}>
{t['com.affine.setDBLocation.title']()}
</div>
<p>{t['com.affine.setDBLocation.description']()}</p>
<div className={style.buttonGroup}>
<Button
disabled={opening}
data-testid="create-workspace-customize-button"
type="primary"
onClick={handleSelectDBFileLocation}
>
{t['com.affine.setDBLocation.button.customize']()}
</Button>
<Tooltip
content={t['com.affine.setDBLocation.tooltip.defaultLocation']({
location: defaultDBLocation,
})}
>
<Button
data-testid="create-workspace-default-location-button"
type="primary"
onClick={() => {
onConfirmLocation();
}}
icon={<HelpIcon />}
iconPosition="end"
>
{t['com.affine.setDBLocation.button.defaultLocation']()}
</Button>
</Tooltip>
</div>
</div>
);
};
interface SetSyncingModeContentProps {
mode: CreateWorkspaceMode;
onConfirmMode: (enableCloudSyncing: boolean) => void;
}
const SetSyncingModeContent = ({
mode,
onConfirmMode,
}: SetSyncingModeContentProps) => {
const t = useAFFiNEI18N();
const [enableCloudSyncing, setEnableCloudSyncing] = useState(false);
return (
<div className={style.content}>
<div className={style.contentTitle}>
{mode === 'new'
? t['com.affine.setSyncingMode.title.created']()
: t['com.affine.setSyncingMode.title.added']()}
</div>
<div className={style.radioGroup}>
<label onClick={() => setEnableCloudSyncing(false)}>
<input
className={style.radio}
type="radio"
readOnly
checked={!enableCloudSyncing}
/>
{t['com.affine.setSyncingMode.deviceOnly']()}
</label>
<label onClick={() => setEnableCloudSyncing(true)}>
<input
className={style.radio}
type="radio"
readOnly
checked={enableCloudSyncing}
/>
{t['com.affine.setSyncingMode.cloud']()}
</label>
</div>
<div className={style.buttonGroup}>
<Button
data-testid="create-workspace-continue-button"
type="primary"
onClick={() => {
onConfirmMode(enableCloudSyncing);
}}
>
{t['com.affine.setSyncingMode.button.continue']()}
</Button>
</div>
</div>
);
};
export const CreateWorkspaceModal = ({ export const CreateWorkspaceModal = ({
mode, mode,
onClose, onClose,
onCreate, onCreate,
}: ModalProps) => { }: ModalProps) => {
const { createLocalWorkspace, addLocalWorkspace } = useAppHelper();
const [step, setStep] = useState<CreateWorkspaceStep>(); const [step, setStep] = useState<CreateWorkspaceStep>();
const [addedId, setAddedId] = useState<string>();
const [workspaceName, setWorkspaceName] = useState<string>();
const [dbFileLocation, setDBFileLocation] = useState<string>();
const setOpenDisableCloudAlertModal = useSetAtom(
openDisableCloudAlertModalAtom
);
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const workspaceManager = useAtomValue(workspaceManagerAtom);
// todo: maybe refactor using xstate? // todo: maybe refactor using xstate?
useLayoutEffect(() => { useLayoutEffect(() => {
@@ -265,9 +119,8 @@ export const CreateWorkspaceModal = ({
setStep(undefined); setStep(undefined);
const result: LoadDBFileResult = await window.apis.dialog.loadDBFile(); const result: LoadDBFileResult = await window.apis.dialog.loadDBFile();
if (result.workspaceId && !canceled) { if (result.workspaceId && !canceled) {
setAddedId(result.workspaceId); workspaceManager._addLocalWorkspace(result.workspaceId);
const newWorkspaceId = await addLocalWorkspace(result.workspaceId); onCreate(result.workspaceId);
onCreate(newWorkspaceId);
} else if (result.error || result.canceled) { } else if (result.error || result.canceled) {
if (result.error) { if (result.error) {
toast(t[result.error]()); toast(t[result.error]());
@@ -285,77 +138,38 @@ export const CreateWorkspaceModal = ({
return () => { return () => {
canceled = true; canceled = true;
}; };
}, [addLocalWorkspace, mode, onClose, onCreate, t]); }, [mode, onClose, onCreate, t, workspaceManager]);
const onConfirmEnableCloudSyncing = useCallback(
(enableCloudSyncing: boolean) => {
(async function () {
if (!runtimeConfig.enableCloud && enableCloudSyncing) {
setOpenDisableCloudAlertModal(true);
} else {
let id = addedId;
// syncing mode is also the last step
if (addedId && mode === 'add') {
await addLocalWorkspace(addedId);
} else if (mode === 'new' && workspaceName) {
id = await createLocalWorkspace(workspaceName);
// if dbFileLocation is set, move db file to that location
if (dbFileLocation) {
await window.apis?.dialog.moveDBFile(id, dbFileLocation);
}
} else {
logger.error('invalid state');
return;
}
if (id) {
onCreate(id);
}
}
})().catch(e => {
logger.error(e);
});
},
[
addLocalWorkspace,
addedId,
createLocalWorkspace,
dbFileLocation,
mode,
onCreate,
setOpenDisableCloudAlertModal,
workspaceName,
]
);
const onConfirmName = useAsyncCallback( const onConfirmName = useAsyncCallback(
async (name: string) => { async (name: string) => {
setWorkspaceName(name);
// this will be the last step for web for now // this will be the last step for web for now
// fix me later // fix me later
const id = await createLocalWorkspace(name); const id = await workspaceManager.createWorkspace(
WorkspaceFlavour.LOCAL,
async workspace => {
workspace.meta.setName(name);
if (runtimeConfig.enablePreloading) {
await buildShowcaseWorkspace(workspace, {
store: getCurrentStore(),
atoms: {
pageMode: setPageModeAtom,
},
});
} else {
const page = workspace.createPage();
workspace.setPageMeta(page.id, {
jumpOnce: true,
});
await initEmptyPage(page);
}
logger.debug('create first workspace');
}
);
onCreate(id); onCreate(id);
}, },
[createLocalWorkspace, onCreate] [onCreate, workspaceManager]
); );
const setDBLocationNode =
step === 'set-db-location' ? (
<SetDBLocationContent
onConfirmLocation={dir => {
setDBFileLocation(dir);
setStep('name-workspace');
}}
/>
) : null;
const setSyncingModeNode =
step === 'set-syncing-mode' ? (
<SetSyncingModeContent
mode={mode}
onConfirmMode={onConfirmEnableCloudSyncing}
/>
) : null;
const onOpenChange = useCallback( const onOpenChange = useCallback(
(open: boolean) => { (open: boolean) => {
if (!open) { if (!open) {
@@ -384,8 +198,6 @@ export const CreateWorkspaceModal = ({
}} }}
> >
<div className={style.header}></div> <div className={style.header}></div>
{setDBLocationNode}
{setSyncingModeNode}
</Modal> </Modal>
); );
}; };

View File

@@ -3,28 +3,28 @@ import {
ConfirmModal, ConfirmModal,
type ConfirmModalProps, type ConfirmModalProps,
} from '@affine/component/ui/modal'; } from '@affine/component/ui/modal';
import type { AffineOfficialWorkspace } from '@affine/env/workspace'; import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { Trans } from '@affine/i18n'; import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name'; import type { WorkspaceMetadata } from '@affine/workspace/metadata';
import { useWorkspaceInfo } from '@toeverything/hooks/use-workspace-info';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import * as styles from './style.css'; import * as styles from './style.css';
interface WorkspaceDeleteProps extends ConfirmModalProps { interface WorkspaceDeleteProps extends ConfirmModalProps {
workspace: AffineOfficialWorkspace; workspaceMetadata: WorkspaceMetadata;
} }
export const WorkspaceDeleteModal = ({ export const WorkspaceDeleteModal = ({
workspace, workspaceMetadata,
...props ...props
}: WorkspaceDeleteProps) => { }: WorkspaceDeleteProps) => {
const { onConfirm } = props; const { onConfirm } = props;
const [workspaceName] = useBlockSuiteWorkspaceName(
workspace.blockSuiteWorkspace
);
const [deleteStr, setDeleteStr] = useState<string>(''); const [deleteStr, setDeleteStr] = useState<string>('');
const info = useWorkspaceInfo(workspaceMetadata);
const workspaceName = info?.name ?? UNTITLED_WORKSPACE_NAME;
const allowDelete = deleteStr === workspaceName; const allowDelete = deleteStr === workspaceName;
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
@@ -46,7 +46,7 @@ export const WorkspaceDeleteModal = ({
}} }}
{...props} {...props}
> >
{workspace.flavour === WorkspaceFlavour.LOCAL ? ( {workspaceMetadata.flavour === WorkspaceFlavour.LOCAL ? (
<Trans i18nKey="com.affine.workspaceDelete.description"> <Trans i18nKey="com.affine.workspaceDelete.description">
Deleting ( Deleting (
<span className={styles.workspaceName}> <span className={styles.workspaceName}>

View File

@@ -1,29 +1,44 @@
import { pushNotificationAtom } from '@affine/component/notification-center';
import { SettingRow } from '@affine/component/setting-components'; import { SettingRow } from '@affine/component/setting-components';
import { ConfirmModal } from '@affine/component/ui/modal'; import { ConfirmModal } from '@affine/component/ui/modal';
import type { AffineOfficialWorkspace } from '@affine/env/workspace'; import { WorkspaceSubPath } from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
currentWorkspaceAtom,
workspaceListAtom,
workspaceManagerAtom,
} from '@affine/workspace/atom';
import { ArrowRightSmallIcon } from '@blocksuite/icons'; import { ArrowRightSmallIcon } from '@blocksuite/icons';
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
import { useAtomValue, useSetAtom } from 'jotai';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { openSettingModalAtom } from '../../../../atoms';
import {
RouteLogic,
useNavigateHelper,
} from '../../../../hooks/use-navigate-helper';
import type { WorkspaceSettingDetailProps } from '../types'; import type { WorkspaceSettingDetailProps } from '../types';
import { WorkspaceDeleteModal } from './delete'; import { WorkspaceDeleteModal } from './delete';
export interface DeleteLeaveWorkspaceProps extends WorkspaceSettingDetailProps { export interface DeleteLeaveWorkspaceProps
workspace: AffineOfficialWorkspace; extends WorkspaceSettingDetailProps {}
}
export const DeleteLeaveWorkspace = ({ export const DeleteLeaveWorkspace = ({
workspace, workspaceMetadata,
onDeleteCloudWorkspace,
onDeleteLocalWorkspace,
onLeaveWorkspace,
isOwner, isOwner,
}: DeleteLeaveWorkspaceProps) => { }: DeleteLeaveWorkspaceProps) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const { jumpToSubPath, jumpToIndex } = useNavigateHelper();
// fixme: cloud regression // fixme: cloud regression
const [showDelete, setShowDelete] = useState(false); const [showDelete, setShowDelete] = useState(false);
const [showLeave, setShowLeave] = useState(false); const [showLeave, setShowLeave] = useState(false);
const setSettingModal = useSetAtom(openSettingModalAtom);
const workspaceManager = useAtomValue(workspaceManagerAtom);
const workspaceList = useAtomValue(workspaceListAtom);
const currentWorkspace = useAtomValue(currentWorkspaceAtom);
const pushNotification = useSetAtom(pushNotificationAtom);
const onLeaveOrDelete = useCallback(() => { const onLeaveOrDelete = useCallback(() => {
if (isOwner) { if (isOwner) {
@@ -33,18 +48,41 @@ export const DeleteLeaveWorkspace = ({
} }
}, [isOwner]); }, [isOwner]);
const onLeaveConfirm = useCallback(() => { const onDeleteConfirm = useAsyncCallback(async () => {
return onLeaveWorkspace(); setSettingModal(prev => ({ ...prev, open: false, workspaceId: null }));
}, [onLeaveWorkspace]);
const onDeleteConfirm = useCallback(() => { if (currentWorkspace?.id === workspaceMetadata.id) {
if (workspace.flavour === WorkspaceFlavour.LOCAL) { const backWorkspace = workspaceList.find(
return onDeleteLocalWorkspace(); ws => ws.id !== workspaceMetadata.id
);
// TODO: if there is no workspace, jump to a new page(wait for design)
if (backWorkspace) {
jumpToSubPath(
backWorkspace?.id || '',
WorkspaceSubPath.ALL,
RouteLogic.REPLACE
);
} else {
jumpToIndex(RouteLogic.REPLACE);
}
} }
if (workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD) {
return onDeleteCloudWorkspace(); await workspaceManager.deleteWorkspace(workspaceMetadata);
} pushNotification({
}, [onDeleteCloudWorkspace, onDeleteLocalWorkspace, workspace.flavour]); title: t['Successfully deleted'](),
type: 'success',
});
}, [
currentWorkspace?.id,
jumpToIndex,
jumpToSubPath,
pushNotification,
setSettingModal,
t,
workspaceList,
workspaceManager,
workspaceMetadata,
]);
return ( return (
<> <>
@@ -68,13 +106,13 @@ export const DeleteLeaveWorkspace = ({
onConfirm={onDeleteConfirm} onConfirm={onDeleteConfirm}
open={showDelete} open={showDelete}
onOpenChange={setShowDelete} onOpenChange={setShowDelete}
workspace={workspace} workspaceMetadata={workspaceMetadata}
/> />
) : ( ) : (
<ConfirmModal <ConfirmModal
open={showLeave} open={showLeave}
cancelText={t['com.affine.confirmModal.button.cancel']()} cancelText={t['com.affine.confirmModal.button.cancel']()}
onConfirm={onLeaveConfirm} onConfirm={onDeleteConfirm}
onOpenChange={setShowLeave} onOpenChange={setShowLeave}
title={`${t['com.affine.deleteLeaveWorkspace.leave']()}?`} title={`${t['com.affine.deleteLeaveWorkspace.leave']()}?`}
description={t['com.affine.deleteLeaveWorkspace.leaveDescription']()} description={t['com.affine.deleteLeaveWorkspace.leaveDescription']()}

View File

@@ -0,0 +1,90 @@
import { SettingRow } from '@affine/component/setting-components';
import { Button } from '@affine/component/ui/button';
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
import { WorkspaceFlavour, WorkspaceSubPath } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { Workspace } from '@affine/workspace';
import { workspaceManagerAtom } from '@affine/workspace/atom';
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
import { useWorkspaceInfo } from '@toeverything/hooks/use-workspace-info';
import { useAtomValue, useSetAtom } from 'jotai';
import { useState } from 'react';
import { openSettingModalAtom } from '../../../atoms';
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
import { EnableAffineCloudModal } from '../enable-affine-cloud-modal';
import { TmpDisableAffineCloudModal } from '../tmp-disable-affine-cloud-modal';
import type { WorkspaceSettingDetailProps } from './types';
export interface PublishPanelProps extends WorkspaceSettingDetailProps {
workspace: Workspace | null;
}
export const EnableCloudPanel = ({
workspaceMetadata,
workspace,
}: PublishPanelProps) => {
const t = useAFFiNEI18N();
const { openPage } = useNavigateHelper();
const workspaceManager = useAtomValue(workspaceManagerAtom);
const workspaceInfo = useWorkspaceInfo(workspaceMetadata);
const setSettingModal = useSetAtom(openSettingModalAtom);
const [open, setOpen] = useState(false);
const handleEnableCloud = useAsyncCallback(async () => {
if (!workspace) {
return;
}
const { id: newId } =
await workspaceManager.transformLocalToCloud(workspace);
openPage(newId, WorkspaceSubPath.ALL);
setOpen(false);
setSettingModal(settings => ({
...settings,
open: false,
}));
}, [openPage, setSettingModal, workspace, workspaceManager]);
if (workspaceMetadata.flavour !== WorkspaceFlavour.LOCAL) {
return null;
}
return (
<>
<SettingRow
name={t['Workspace saved locally']({
name: workspaceInfo?.name ?? UNTITLED_WORKSPACE_NAME,
})}
desc={t['Enable cloud hint']()}
spreadCol={false}
style={{
padding: '10px',
background: 'var(--affine-background-secondary-color)',
}}
>
<Button
data-testid="publish-enable-affine-cloud-button"
type="primary"
onClick={() => {
setOpen(true);
}}
style={{ marginTop: '12px' }}
>
{t['Enable AFFiNE Cloud']()}
</Button>
</SettingRow>
{runtimeConfig.enableCloud ? (
<EnableAffineCloudModal
open={open}
onOpenChange={setOpen}
onConfirm={handleEnableCloud}
/>
) : (
<TmpDisableAffineCloudModal open={open} onOpenChange={setOpen} />
)}
</>
);
};

View File

@@ -1,69 +1,35 @@
import { pushNotificationAtom } from '@affine/component/notification-center'; import { pushNotificationAtom } from '@affine/component/notification-center';
import { SettingRow } from '@affine/component/setting-components'; import { SettingRow } from '@affine/component/setting-components';
import { Button } from '@affine/component/ui/button'; import { Button } from '@affine/component/ui/button';
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { Workspace, WorkspaceMetadata } from '@affine/workspace';
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks'; import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
import type { SaveDBFileResult } from '@toeverything/infra/type'; import type { SaveDBFileResult } from '@toeverything/infra/type';
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import { useState } from 'react'; import { useState } from 'react';
import type { Doc } from 'yjs';
import { encodeStateAsUpdate } from 'yjs';
async function syncBlobsToSqliteDb(workspace: AffineOfficialWorkspace) {
if (window.apis && environment.isDesktop) {
const bs = workspace.blockSuiteWorkspace.blob;
const blobsInDb = await window.apis.db.getBlobKeys(workspace.id);
const blobsInStorage = await bs.list();
const blobsToSync = blobsInStorage.filter(
blob => !blobsInDb.includes(blob)
);
await Promise.all(
blobsToSync.map(async blobKey => {
const blob = await bs.get(blobKey);
if (blob) {
const bin = new Uint8Array(await blob.arrayBuffer());
await window.apis.db.addBlob(workspace.id, blobKey, bin);
}
})
);
}
}
async function syncDocsToSqliteDb(workspace: AffineOfficialWorkspace) {
if (window.apis && environment.isDesktop) {
const workspaceId = workspace.blockSuiteWorkspace.doc.guid;
const syncDoc = async (doc: Doc) => {
await window.apis.db.applyDocUpdate(
workspace.id,
encodeStateAsUpdate(doc),
doc.guid === workspaceId ? undefined : doc.guid
);
await Promise.all([...doc.subdocs].map(subdoc => syncDoc(subdoc)));
};
return syncDoc(workspace.blockSuiteWorkspace.doc);
}
}
interface ExportPanelProps { interface ExportPanelProps {
workspace: AffineOfficialWorkspace; workspaceMetadata: WorkspaceMetadata;
workspace: Workspace | null;
} }
export const ExportPanel = ({ workspace }: ExportPanelProps) => { export const ExportPanel = ({
const workspaceId = workspace.id; workspaceMetadata,
workspace,
}: ExportPanelProps) => {
const workspaceId = workspaceMetadata.id;
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const [syncing, setSyncing] = useState(false); const [saving, setSaving] = useState(false);
const pushNotification = useSetAtom(pushNotificationAtom); const pushNotification = useSetAtom(pushNotificationAtom);
const onExport = useAsyncCallback(async () => { const onExport = useAsyncCallback(async () => {
if (syncing) { if (saving || !workspace) {
return; return;
} }
setSyncing(true); setSaving(true);
try { try {
await syncBlobsToSqliteDb(workspace); await workspace.engine.sync.waitForSynced();
await syncDocsToSqliteDb(workspace); await workspace.engine.blob.sync();
const result: SaveDBFileResult = const result: SaveDBFileResult =
await window.apis?.dialog.saveDBFileAs(workspaceId); await window.apis?.dialog.saveDBFileAs(workspaceId);
if (result?.error) { if (result?.error) {
@@ -81,16 +47,16 @@ export const ExportPanel = ({ workspace }: ExportPanelProps) => {
message: e.message, message: e.message,
}); });
} finally { } finally {
setSyncing(false); setSaving(false);
} }
}, [pushNotification, syncing, t, workspace, workspaceId]); }, [pushNotification, saving, t, workspace, workspaceId]);
return ( return (
<SettingRow name={t['Export']()} desc={t['Export Description']()}> <SettingRow name={t['Export']()} desc={t['Export Description']()}>
<Button <Button
data-testid="export-affine-backup" data-testid="export-affine-backup"
onClick={onExport} onClick={onExport}
disabled={syncing} disabled={saving}
> >
{t['Export']()} {t['Export']()}
</Button> </Button>

View File

@@ -3,47 +3,38 @@ import {
SettingRow, SettingRow,
SettingWrapper, SettingWrapper,
} from '@affine/component/setting-components'; } from '@affine/component/setting-components';
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name'; import { useWorkspace } from '@toeverything/hooks/use-workspace';
import { useMemo } from 'react'; import { useWorkspaceInfo } from '@toeverything/hooks/use-workspace-info';
import { useSelfHosted } from '../../../hooks/affine/use-server-flavor'; import { useSelfHosted } from '../../../hooks/affine/use-server-flavor';
import { useWorkspace } from '../../../hooks/use-workspace';
import { DeleteLeaveWorkspace } from './delete-leave-workspace'; import { DeleteLeaveWorkspace } from './delete-leave-workspace';
import { EnableCloudPanel } from './enable-cloud';
import { ExportPanel } from './export'; import { ExportPanel } from './export';
import { LabelsPanel } from './labels'; import { LabelsPanel } from './labels';
import { MembersPanel } from './members'; import { MembersPanel } from './members';
import { ProfilePanel } from './profile'; import { ProfilePanel } from './profile';
import { PublishPanel } from './publish';
import { StoragePanel } from './storage'; import { StoragePanel } from './storage';
import type { WorkspaceSettingDetailProps } from './types'; import type { WorkspaceSettingDetailProps } from './types';
export const WorkspaceSettingDetail = (props: WorkspaceSettingDetailProps) => { export const WorkspaceSettingDetail = (props: WorkspaceSettingDetailProps) => {
const { workspaceId } = props;
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const isSelfHosted = useSelfHosted(); const isSelfHosted = useSelfHosted();
const workspace = useWorkspace(workspaceId); const workspaceMetadata = props.workspaceMetadata;
const [name] = useBlockSuiteWorkspaceName(workspace.blockSuiteWorkspace);
const storageAndExportSetting = useMemo(() => { // useWorkspace hook is a vary heavy operation here, but we need syncing name and avatar changes here,
if (environment.isDesktop) { // we don't have a better way to do this now
return ( const workspace = useWorkspace(workspaceMetadata);
<SettingWrapper title={t['Storage and Export']()}>
{runtimeConfig.enableMoveDatabase ? ( const workspaceInfo = useWorkspaceInfo(workspaceMetadata);
<StoragePanel workspace={workspace} />
) : null}
<ExportPanel workspace={workspace} />
</SettingWrapper>
);
} else {
return null;
}
}, [t, workspace]);
return ( return (
<> <>
<SettingHeader <SettingHeader
title={t[`Workspace Settings with name`]({ name })} title={t[`Workspace Settings with name`]({
name: workspaceInfo?.name ?? UNTITLED_WORKSPACE_NAME,
})}
subtitle={t['com.affine.settings.workspace.description']()} subtitle={t['com.affine.settings.workspace.description']()}
/> />
<SettingWrapper title={t['Info']()}> <SettingWrapper title={t['Info']()}>
@@ -53,20 +44,26 @@ export const WorkspaceSettingDetail = (props: WorkspaceSettingDetailProps) => {
spreadCol={false} spreadCol={false}
> >
<ProfilePanel workspace={workspace} {...props} /> <ProfilePanel workspace={workspace} {...props} />
<LabelsPanel workspace={workspace} {...props} /> <LabelsPanel {...props} />
</SettingRow> </SettingRow>
</SettingWrapper> </SettingWrapper>
<SettingWrapper title={t['com.affine.brand.affineCloud']()}> <SettingWrapper title={t['com.affine.brand.affineCloud']()}>
<PublishPanel workspace={workspace} {...props} /> <EnableCloudPanel workspace={workspace} {...props} />
<MembersPanel <MembersPanel upgradable={!isSelfHosted} {...props} />
workspace={workspace}
upgradable={!isSelfHosted}
{...props}
/>
</SettingWrapper> </SettingWrapper>
{storageAndExportSetting} {environment.isDesktop && (
<SettingWrapper title={t['Storage and Export']()}>
{runtimeConfig.enableMoveDatabase ? (
<StoragePanel workspaceMetadata={workspaceMetadata} />
) : null}
<ExportPanel
workspace={workspace}
workspaceMetadata={workspaceMetadata}
/>
</SettingWrapper>
)}
<SettingWrapper> <SettingWrapper>
<DeleteLeaveWorkspace workspace={workspace} {...props} /> <DeleteLeaveWorkspace {...props} />
</SettingWrapper> </SettingWrapper>
</> </>
); );

View File

@@ -1,12 +1,9 @@
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
import { useMemo } from 'react'; import { useMemo } from 'react';
import * as style from './style.css'; import * as style from './style.css';
import type { WorkspaceSettingDetailProps } from './types'; import type { WorkspaceSettingDetailProps } from './types';
export interface LabelsPanelProps extends WorkspaceSettingDetailProps { export interface LabelsPanelProps extends WorkspaceSettingDetailProps {}
workspace: AffineOfficialWorkspace;
}
type WorkspaceStatus = type WorkspaceStatus =
| 'local' | 'local'
@@ -38,7 +35,10 @@ const Label = ({ value, background }: LabelProps) => {
</div> </div>
); );
}; };
export const LabelsPanel = ({ workspace, isOwner }: LabelsPanelProps) => { export const LabelsPanel = ({
workspaceMetadata,
isOwner,
}: LabelsPanelProps) => {
const labelMap: LabelMap = useMemo( const labelMap: LabelMap = useMemo(
() => ({ () => ({
local: { local: {
@@ -74,11 +74,10 @@ export const LabelsPanel = ({ workspace, isOwner }: LabelsPanelProps) => {
); );
const labelConditions: labelConditionsProps[] = [ const labelConditions: labelConditionsProps[] = [
{ condition: !isOwner, label: 'joinedWorkspace' }, { condition: !isOwner, label: 'joinedWorkspace' },
{ condition: workspace.flavour === 'local', label: 'local' }, { condition: workspaceMetadata.flavour === 'local', label: 'local' },
{ condition: workspace.flavour === 'affine-cloud', label: 'syncCloud' },
{ {
condition: workspace.flavour === 'affine-public', condition: workspaceMetadata.flavour === 'affine-cloud',
label: 'publishedToWeb', label: 'syncCloud',
}, },
//TODO: add these labels //TODO: add these labels
// { status==="synced", label: 'availableOffline' } // { status==="synced", label: 'availableOffline' }

View File

@@ -13,7 +13,6 @@ import { Button, IconButton } from '@affine/component/ui/button';
import { Loading } from '@affine/component/ui/loading'; import { Loading } from '@affine/component/ui/loading';
import { Menu, MenuItem } from '@affine/component/ui/menu'; import { Menu, MenuItem } from '@affine/component/ui/menu';
import { Tooltip } from '@affine/component/ui/tooltip'; import { Tooltip } from '@affine/component/ui/tooltip';
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { Permission } from '@affine/graphql'; import { Permission } from '@affine/graphql';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
@@ -45,7 +44,6 @@ import type { WorkspaceSettingDetailProps } from './types';
const COUNT_PER_PAGE = 8; const COUNT_PER_PAGE = 8;
export interface MembersPanelProps extends WorkspaceSettingDetailProps { export interface MembersPanelProps extends WorkspaceSettingDetailProps {
upgradable: boolean; upgradable: boolean;
workspace: AffineOfficialWorkspace;
} }
type OnRevoke = (memberId: string) => void; type OnRevoke = (memberId: string) => void;
const MembersPanelLocal = () => { const MembersPanelLocal = () => {
@@ -62,11 +60,11 @@ const MembersPanelLocal = () => {
}; };
export const CloudWorkspaceMembersPanel = ({ export const CloudWorkspaceMembersPanel = ({
workspace,
isOwner, isOwner,
upgradable, upgradable,
workspaceMetadata,
}: MembersPanelProps) => { }: MembersPanelProps) => {
const workspaceId = workspace.id; const workspaceId = workspaceMetadata.id;
const memberCount = useMemberCount(workspaceId); const memberCount = useMemberCount(workspaceId);
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
@@ -138,7 +136,6 @@ export const CloudWorkspaceMembersPanel = ({
setSettingModalAtom({ setSettingModalAtom({
open: true, open: true,
activeTab: 'plans', activeTab: 'plans',
workspaceId: null,
}); });
}, [setSettingModalAtom]); }, [setSettingModalAtom]);
@@ -345,7 +342,7 @@ const MemberItem = ({
}; };
export const MembersPanel = (props: MembersPanelProps): ReactElement | null => { export const MembersPanel = (props: MembersPanelProps): ReactElement | null => {
if (props.workspace.flavour === WorkspaceFlavour.LOCAL) { if (props.workspaceMetadata.flavour === WorkspaceFlavour.LOCAL) {
return <MembersPanelLocal />; return <MembersPanelLocal />;
} }
return ( return (

View File

@@ -2,51 +2,120 @@ import { FlexWrapper, Input, Wrapper } from '@affine/component';
import { pushNotificationAtom } from '@affine/component/notification-center'; import { pushNotificationAtom } from '@affine/component/notification-center';
import { Avatar } from '@affine/component/ui/avatar'; import { Avatar } from '@affine/component/ui/avatar';
import { Button } from '@affine/component/ui/button'; import { Button } from '@affine/component/ui/button';
import type { AffineOfficialWorkspace } from '@affine/env/workspace'; import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { Workspace } from '@affine/workspace';
import { SyncPeerStep } from '@affine/workspace';
import { CameraIcon } from '@blocksuite/icons'; import { CameraIcon } from '@blocksuite/icons';
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url'; import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name'; import { useWorkspaceBlobObjectUrl } from '@toeverything/hooks/use-workspace-blob';
import { useWorkspaceStatus } from '@toeverything/hooks/use-workspace-status';
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import { import {
type KeyboardEvent, type KeyboardEvent,
type MouseEvent, type MouseEvent,
startTransition, startTransition,
useCallback, useCallback,
useEffect,
useState, useState,
} from 'react'; } from 'react';
import { validateAndReduceImage } from '../../../utils/reduce-image';
import { Upload } from '../../pure/file-upload'; import { Upload } from '../../pure/file-upload';
import * as style from './style.css'; import * as style from './style.css';
import type { WorkspaceSettingDetailProps } from './types'; import type { WorkspaceSettingDetailProps } from './types';
export interface ProfilePanelProps extends WorkspaceSettingDetailProps { export interface ProfilePanelProps extends WorkspaceSettingDetailProps {
workspace: AffineOfficialWorkspace; workspace: Workspace | null;
} }
export const ProfilePanel = ({ workspace, isOwner }: ProfilePanelProps) => { export const ProfilePanel = ({ isOwner, workspace }: ProfilePanelProps) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const pushNotification = useSetAtom(pushNotificationAtom); const pushNotification = useSetAtom(pushNotificationAtom);
const [workspaceAvatar, update] = useBlockSuiteWorkspaceAvatarUrl( const workspaceIsLoading =
workspace.blockSuiteWorkspace useWorkspaceStatus(
workspace,
status =>
!status.engine.sync.local ||
status.engine.sync.local?.step <= SyncPeerStep.LoadingRootDoc
) ?? true;
const [avatarBlob, setAvatarBlob] = useState<string | null>(null);
const [name, setName] = useState('');
const avatarUrl = useWorkspaceBlobObjectUrl(workspace?.meta, avatarBlob);
useEffect(() => {
if (workspace?.blockSuiteWorkspace) {
setAvatarBlob(workspace.blockSuiteWorkspace.meta.avatar ?? null);
setName(
workspace.blockSuiteWorkspace.meta.name ?? UNTITLED_WORKSPACE_NAME
);
const dispose = workspace.blockSuiteWorkspace.meta.commonFieldsUpdated.on(
() => {
setAvatarBlob(workspace.blockSuiteWorkspace.meta.avatar ?? null);
setName(
workspace.blockSuiteWorkspace.meta.name ?? UNTITLED_WORKSPACE_NAME
);
}
);
return () => {
dispose.dispose();
};
} else {
setAvatarBlob(null);
setName(UNTITLED_WORKSPACE_NAME);
}
return;
}, [workspace]);
const setWorkspaceAvatar = useCallback(
async (file: File | null) => {
if (!workspace) {
return;
}
if (!file) {
workspace.blockSuiteWorkspace.meta.setAvatar('');
return;
}
try {
const reducedFile = await validateAndReduceImage(file);
const blobs = workspace.blockSuiteWorkspace.blob;
const blobId = await blobs.set(reducedFile);
workspace.blockSuiteWorkspace.meta.setAvatar(blobId);
} catch (error) {
console.error(error);
throw error;
}
},
[workspace]
); );
const [name, setName] = useBlockSuiteWorkspaceName( const setWorkspaceName = useCallback(
workspace.blockSuiteWorkspace (name: string) => {
if (!workspace) {
return;
}
workspace.blockSuiteWorkspace.meta.setName(name);
},
[workspace]
); );
const [input, setInput] = useState<string>(name); const [input, setInput] = useState<string>('');
useEffect(() => {
setInput(name);
}, [name]);
const handleUpdateWorkspaceName = useCallback( const handleUpdateWorkspaceName = useCallback(
(name: string) => { (name: string) => {
setName(name); setWorkspaceName(name);
pushNotification({ pushNotification({
title: t['Update workspace name success'](), title: t['Update workspace name success'](),
type: 'success', type: 'success',
}); });
}, },
[pushNotification, setName, t] [pushNotification, setWorkspaceName, t]
); );
const handleSetInput = useCallback((value: string) => { const handleSetInput = useCallback((value: string) => {
@@ -68,17 +137,17 @@ export const ProfilePanel = ({ workspace, isOwner }: ProfilePanelProps) => {
handleUpdateWorkspaceName(input); handleUpdateWorkspaceName(input);
}, [handleUpdateWorkspaceName, input]); }, [handleUpdateWorkspaceName, input]);
const handleRemoveUserAvatar = useCallback( const handleRemoveUserAvatar = useAsyncCallback(
async (e: MouseEvent<HTMLButtonElement>) => { async (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation(); e.stopPropagation();
await update(null); await setWorkspaceAvatar(null);
}, },
[update] [setWorkspaceAvatar]
); );
const handleUploadAvatar = useCallback( const handleUploadAvatar = useCallback(
(file: File) => { (file: File) => {
update(file) setWorkspaceAvatar(file)
.then(() => { .then(() => {
pushNotification({ pushNotification({
title: 'Update workspace avatar success', title: 'Update workspace avatar success',
@@ -93,10 +162,10 @@ export const ProfilePanel = ({ workspace, isOwner }: ProfilePanelProps) => {
}); });
}); });
}, },
[pushNotification, update] [pushNotification, setWorkspaceAvatar]
); );
const canAdjustAvatar = workspaceAvatar && isOwner; const canAdjustAvatar = !workspaceIsLoading && avatarUrl && isOwner;
return ( return (
<div className={style.profileWrapper}> <div className={style.profileWrapper}>
@@ -108,7 +177,7 @@ export const ProfilePanel = ({ workspace, isOwner }: ProfilePanelProps) => {
> >
<Avatar <Avatar
size={56} size={56}
url={workspaceAvatar} url={avatarUrl}
name={name} name={name}
colorfulFallback colorfulFallback
hoverIcon={isOwner ? <CameraIcon /> : undefined} hoverIcon={isOwner ? <CameraIcon /> : undefined}
@@ -132,10 +201,10 @@ export const ProfilePanel = ({ workspace, isOwner }: ProfilePanelProps) => {
<div className={style.label}>{t['Workspace Name']()}</div> <div className={style.label}>{t['Workspace Name']()}</div>
<FlexWrapper alignItems="center" flexGrow="1"> <FlexWrapper alignItems="center" flexGrow="1">
<Input <Input
disabled={!isOwner} disabled={workspaceIsLoading || !isOwner}
width={280} width={280}
height={32} height={32}
defaultValue={input} value={input}
data-testid="workspace-name-input" data-testid="workspace-name-input"
placeholder={t['Workspace Name']()} placeholder={t['Workspace Name']()}
maxLength={64} maxLength={64}
@@ -143,7 +212,7 @@ export const ProfilePanel = ({ workspace, isOwner }: ProfilePanelProps) => {
onChange={handleSetInput} onChange={handleSetInput}
onKeyUp={handleKeyUp} onKeyUp={handleKeyUp}
/> />
{input === workspace.blockSuiteWorkspace.meta.name ? null : ( {input === name ? null : (
<Button <Button
data-testid="save-workspace-name" data-testid="save-workspace-name"
onClick={handleClick} onClick={handleClick}

View File

@@ -1,169 +0,0 @@
import { FlexWrapper, Input, Switch } from '@affine/component';
import { SettingRow } from '@affine/component/setting-components';
import { Button } from '@affine/component/ui/button';
import { Tooltip } from '@affine/component/ui/tooltip';
import { Unreachable } from '@affine/env/constant';
import type {
AffineCloudWorkspace,
AffinePublicWorkspace,
LocalWorkspace,
} from '@affine/env/workspace';
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import { noop } from 'foxact/noop';
import { useEffect, useMemo, useState } from 'react';
import { toast } from '../../../utils';
import { EnableAffineCloudModal } from '../enable-affine-cloud-modal';
import { TmpDisableAffineCloudModal } from '../tmp-disable-affine-cloud-modal';
import * as style from './style.css';
import type { WorkspaceSettingDetailProps } from './types';
export interface PublishPanelProps
extends Omit<WorkspaceSettingDetailProps, 'workspaceId'> {
workspace: AffineOfficialWorkspace;
}
export interface PublishPanelLocalProps
extends Omit<WorkspaceSettingDetailProps, 'workspaceId'> {
workspace: LocalWorkspace;
}
export interface PublishPanelAffineProps
extends Omit<WorkspaceSettingDetailProps, 'workspaceId'> {
workspace: AffineCloudWorkspace | AffinePublicWorkspace;
}
const PublishPanelAffine = (props: PublishPanelAffineProps) => {
const { workspace } = props;
const t = useAFFiNEI18N();
// const toggleWorkspacePublish = useToggleWorkspacePublish(workspace);
const isPublic = useMemo(() => {
return workspace.flavour === WorkspaceFlavour.AFFINE_PUBLIC;
}, [workspace]);
const [origin, setOrigin] = useState('');
const shareUrl = origin + '/public-workspace/' + workspace.id;
useEffect(() => {
setOrigin(
typeof window !== 'undefined' && window.location.origin
? window.location.origin
: ''
);
}, []);
const copyUrl = useAsyncCallback(async () => {
await navigator.clipboard.writeText(shareUrl);
toast(t['Copied link to clipboard']());
}, [shareUrl, t]);
return (
<div style={{ display: 'none' }}>
<SettingRow
name={t['Publish']()}
desc={isPublic ? t['Unpublished hint']() : t['Published hint']()}
style={{
marginBottom: isPublic ? '12px' : '25px',
}}
>
<Switch checked={isPublic} />
</SettingRow>
{isPublic ? (
<FlexWrapper justifyContent="space-between" marginBottom={25}>
<Input value={shareUrl} disabled />
<Button
onClick={copyUrl}
style={{
marginLeft: '20px',
}}
>
{t['Copy']()}
</Button>
</FlexWrapper>
) : null}
</div>
);
};
interface FakePublishPanelAffineProps {
workspace: AffineOfficialWorkspace;
}
const FakePublishPanelAffine = (_props: FakePublishPanelAffineProps) => {
const t = useAFFiNEI18N();
return (
<Tooltip content={t['com.affine.settings.workspace.publish-tooltip']()}>
<div className={style.fakeWrapper}>
<SettingRow name={t['Publish']()} desc={t['Unpublished hint']()}>
<Switch checked={false} onChange={noop} />
</SettingRow>
</div>
</Tooltip>
);
};
const PublishPanelLocal = ({
workspace,
onTransferWorkspace,
}: PublishPanelLocalProps) => {
const t = useAFFiNEI18N();
const [name] = useBlockSuiteWorkspaceName(workspace.blockSuiteWorkspace);
const [open, setOpen] = useState(false);
return (
<>
<SettingRow
name={t['Workspace saved locally']({ name })}
desc={t['Enable cloud hint']()}
spreadCol={false}
style={{
padding: '10px',
background: 'var(--affine-background-secondary-color)',
}}
>
<Button
data-testid="publish-enable-affine-cloud-button"
type="primary"
onClick={() => {
setOpen(true);
}}
style={{ marginTop: '12px' }}
>
{t['Enable AFFiNE Cloud']()}
</Button>
</SettingRow>
<FakePublishPanelAffine workspace={workspace} />
{runtimeConfig.enableCloud ? (
<EnableAffineCloudModal
open={open}
onOpenChange={setOpen}
onConfirm={() => {
onTransferWorkspace(
WorkspaceFlavour.LOCAL,
WorkspaceFlavour.AFFINE_CLOUD,
workspace
);
setOpen(false);
}}
/>
) : (
<TmpDisableAffineCloudModal open={open} onOpenChange={setOpen} />
)}
</>
);
};
export const PublishPanel = (props: PublishPanelProps) => {
if (
props.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD ||
props.workspace.flavour === WorkspaceFlavour.AFFINE_PUBLIC
) {
return <PublishPanelAffine {...props} workspace={props.workspace} />;
} else if (props.workspace.flavour === WorkspaceFlavour.LOCAL) {
return <PublishPanelLocal {...props} workspace={props.workspace} />;
}
throw new Unreachable();
};

View File

@@ -2,8 +2,8 @@ import { FlexWrapper, toast } from '@affine/component';
import { SettingRow } from '@affine/component/setting-components'; import { SettingRow } from '@affine/component/setting-components';
import { Button } from '@affine/component/ui/button'; import { Button } from '@affine/component/ui/button';
import { Tooltip } from '@affine/component/ui/tooltip'; import { Tooltip } from '@affine/component/ui/tooltip';
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { WorkspaceMetadata } from '@affine/workspace/metadata';
import type { MoveDBFileResult } from '@toeverything/infra/type'; import type { MoveDBFileResult } from '@toeverything/infra/type';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
@@ -33,11 +33,11 @@ const useDBFileSecondaryPath = (workspaceId: string) => {
}; };
interface StoragePanelProps { interface StoragePanelProps {
workspace: AffineOfficialWorkspace; workspaceMetadata: WorkspaceMetadata;
} }
export const StoragePanel = ({ workspace }: StoragePanelProps) => { export const StoragePanel = ({ workspaceMetadata }: StoragePanelProps) => {
const workspaceId = workspace.id; const workspaceId = workspaceMetadata.id;
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const secondaryPath = useDBFileSecondaryPath(workspaceId); const secondaryPath = useDBFileSecondaryPath(workspaceId);

View File

@@ -1,20 +1,6 @@
import type { import type { WorkspaceMetadata } from '@affine/workspace/metadata';
WorkspaceFlavour,
WorkspaceRegistry,
} from '@affine/env/workspace';
export interface WorkspaceSettingDetailProps { export interface WorkspaceSettingDetailProps {
workspaceId: string;
isOwner: boolean; isOwner: boolean;
onDeleteLocalWorkspace: () => void; workspaceMetadata: WorkspaceMetadata;
onDeleteCloudWorkspace: () => void;
onLeaveWorkspace: () => void;
onTransferWorkspace: <
From extends WorkspaceFlavour,
To extends WorkspaceFlavour,
>(
from: From,
to: To,
workspace: WorkspaceRegistry[From]
) => void;
} }

View File

@@ -5,13 +5,15 @@ import {
listHistoryQuery, listHistoryQuery,
recoverDocMutation, recoverDocMutation,
} from '@affine/graphql'; } from '@affine/graphql';
import {
createAffineCloudBlobStorage,
globalBlockSuiteSchema,
} from '@affine/workspace';
import { import {
useMutateQueryResource, useMutateQueryResource,
useMutation, useMutation,
useQueryInfinite, useQueryInfinite,
} from '@affine/workspace/affine/gql'; } from '@affine/workspace/affine/gql';
import { createAffineCloudBlobEngine } from '@affine/workspace/blob';
import { globalBlockSuiteSchema } from '@affine/workspace/manager';
import { assertEquals } from '@blocksuite/global/utils'; import { assertEquals } from '@blocksuite/global/utils';
import { Workspace } from '@blocksuite/store'; import { Workspace } from '@blocksuite/store';
import { usePageMetaHelper } from '@toeverything/hooks/use-block-suite-page-meta'; import { usePageMetaHelper } from '@toeverything/hooks/use-block-suite-page-meta';
@@ -107,27 +109,13 @@ const workspaceMap = new Map<string, Workspace>();
const getOrCreateWorkspace = (workspaceId: string) => { const getOrCreateWorkspace = (workspaceId: string) => {
let workspace = workspaceMap.get(workspaceId); let workspace = workspaceMap.get(workspaceId);
if (!workspace) { if (!workspace) {
const blobEngine = createAffineCloudBlobEngine(workspaceId); const blobStorage = createAffineCloudBlobStorage(workspaceId);
workspace = new Workspace({ workspace = new Workspace({
id: workspaceId, id: workspaceId,
providerCreators: [], providerCreators: [],
blobStorages: [ blobStorages: [
() => ({ () => ({
crud: { crud: blobStorage,
async get(key) {
return (await blobEngine.get(key)) ?? null;
},
async set(key, value) {
await blobEngine.set(key, value);
return key;
},
async delete(key) {
return blobEngine.delete(key);
},
async list() {
return blobEngine.list();
},
},
}), }),
], ],
schema: globalBlockSuiteSchema, schema: globalBlockSuiteSchema,

View File

@@ -7,6 +7,7 @@ import { Button } from '@affine/component/ui/button';
import { ConfirmModal, Modal } from '@affine/component/ui/modal'; import { ConfirmModal, Modal } from '@affine/component/ui/modal';
import type { PageMode } from '@affine/core/atoms'; import type { PageMode } from '@affine/core/atoms';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { waitForCurrentWorkspaceAtom } from '@affine/workspace/atom';
import type { Workspace } from '@blocksuite/store'; import type { Workspace } from '@blocksuite/store';
import type { DialogContentProps } from '@radix-ui/react-dialog'; import type { DialogContentProps } from '@radix-ui/react-dialog';
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks'; import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
@@ -22,7 +23,6 @@ import {
import { currentModeAtom } from '../../../atoms/mode'; import { currentModeAtom } from '../../../atoms/mode';
import { pageHistoryModalAtom } from '../../../atoms/page-history'; import { pageHistoryModalAtom } from '../../../atoms/page-history';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { StyledEditorModeSwitch } from '../../blocksuite/block-suite-mode-switch/style'; import { StyledEditorModeSwitch } from '../../blocksuite/block-suite-mode-switch/style';
import { import {
EdgelessSwitchItem, EdgelessSwitchItem,
@@ -423,7 +423,7 @@ export const PageHistoryModal = ({
export const GlobalPageHistoryModal = () => { export const GlobalPageHistoryModal = () => {
const [{ open, pageId }, setState] = useAtom(pageHistoryModalAtom); const [{ open, pageId }, setState] = useAtom(pageHistoryModalAtom);
const [workspace] = useCurrentWorkspace(); const workspace = useAtomValue(waitForCurrentWorkspaceAtom);
const handleOpenChange = useCallback( const handleOpenChange = useCallback(
(open: boolean) => { (open: boolean) => {

View File

@@ -17,7 +17,6 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useMutation, useQuery } from '@affine/workspace/affine/gql'; import { useMutation, useQuery } from '@affine/workspace/affine/gql';
import { ArrowRightSmallIcon, CameraIcon } from '@blocksuite/icons'; import { ArrowRightSmallIcon, CameraIcon } from '@blocksuite/icons';
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks'; import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
import { validateAndReduceImage } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
import bytes from 'bytes'; import bytes from 'bytes';
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import { import {
@@ -37,6 +36,7 @@ import {
import { useCurrentUser } from '../../../../hooks/affine/use-current-user'; import { useCurrentUser } from '../../../../hooks/affine/use-current-user';
import { useSelfHosted } from '../../../../hooks/affine/use-server-flavor'; import { useSelfHosted } from '../../../../hooks/affine/use-server-flavor';
import { useUserSubscription } from '../../../../hooks/use-subscription'; import { useUserSubscription } from '../../../../hooks/use-subscription';
import { validateAndReduceImage } from '../../../../utils/reduce-image';
import { Upload } from '../../../pure/file-upload'; import { Upload } from '../../../pure/file-upload';
import * as style from './style.css'; import * as style from './style.css';
@@ -187,7 +187,6 @@ const StoragePanel = () => {
setSettingModalAtom({ setSettingModalAtom({
open: true, open: true,
activeTab: 'plans', activeTab: 'plans',
workspaceId: null,
}); });
}, [setSettingModalAtom]); }, [setSettingModalAtom]);

View File

@@ -119,7 +119,6 @@ const SubscriptionSettings = () => {
setOpenSettingModalAtom({ setOpenSettingModalAtom({
open: true, open: true,
activeTab: 'plans', activeTab: 'plans',
workspaceId: null,
}); });
}, [setOpenSettingModalAtom]); }, [setOpenSettingModalAtom]);

View File

@@ -1,6 +1,7 @@
import { WorkspaceDetailSkeleton } from '@affine/component/setting-components'; import { WorkspaceDetailSkeleton } from '@affine/component/setting-components';
import { Modal, type ModalProps } from '@affine/component/ui/modal'; import { Modal, type ModalProps } from '@affine/component/ui/modal';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { WorkspaceMetadata } from '@affine/workspace/metadata';
import { ContactWithUsIcon } from '@blocksuite/icons'; import { ContactWithUsIcon } from '@blocksuite/icons';
import { debounce } from 'lodash-es'; import { debounce } from 'lodash-es';
import { Suspense, useCallback, useLayoutEffect, useRef } from 'react'; import { Suspense, useCallback, useLayoutEffect, useRef } from 'react';
@@ -20,16 +21,16 @@ type ActiveTab = GeneralSettingKeys | 'workspace' | 'account';
export interface SettingProps extends ModalProps { export interface SettingProps extends ModalProps {
activeTab: ActiveTab; activeTab: ActiveTab;
workspaceId: string | null; workspaceMetadata?: WorkspaceMetadata | null;
onSettingClick: (params: { onSettingClick: (params: {
activeTab: ActiveTab; activeTab: ActiveTab;
workspaceId: string | null; workspaceMetadata: WorkspaceMetadata | null;
}) => void; }) => void;
} }
export const SettingModal = ({ export const SettingModal = ({
activeTab = 'appearance', activeTab = 'appearance',
workspaceId = null, workspaceMetadata = null,
onSettingClick, onSettingClick,
...modalProps ...modalProps
}: SettingProps) => { }: SettingProps) => {
@@ -75,22 +76,22 @@ export const SettingModal = ({
(key: GeneralSettingKeys) => { (key: GeneralSettingKeys) => {
onSettingClick({ onSettingClick({
activeTab: key, activeTab: key,
workspaceId: null, workspaceMetadata: null,
}); });
}, },
[onSettingClick] [onSettingClick]
); );
const onWorkspaceSettingClick = useCallback( const onWorkspaceSettingClick = useCallback(
(workspaceId: string) => { (workspaceMetadata: WorkspaceMetadata) => {
onSettingClick({ onSettingClick({
activeTab: 'workspace', activeTab: 'workspace',
workspaceId, workspaceMetadata,
}); });
}, },
[onSettingClick] [onSettingClick]
); );
const onAccountSettingClick = useCallback(() => { const onAccountSettingClick = useCallback(() => {
onSettingClick({ activeTab: 'account', workspaceId: null }); onSettingClick({ activeTab: 'account', workspaceMetadata: null });
}, [onSettingClick]); }, [onSettingClick]);
return ( return (
@@ -114,7 +115,7 @@ export const SettingModal = ({
onGeneralSettingClick={onGeneralSettingClick} onGeneralSettingClick={onGeneralSettingClick}
onWorkspaceSettingClick={onWorkspaceSettingClick} onWorkspaceSettingClick={onWorkspaceSettingClick}
selectedGeneralKey={activeTab} selectedGeneralKey={activeTab}
selectedWorkspaceId={workspaceId} selectedWorkspaceId={workspaceMetadata?.id ?? null}
onAccountSettingClick={onAccountSettingClick} onAccountSettingClick={onAccountSettingClick}
/> />
@@ -125,9 +126,12 @@ export const SettingModal = ({
> >
<div ref={modalContentRef} className={style.centerContainer}> <div ref={modalContentRef} className={style.centerContainer}>
<div className={style.content}> <div className={style.content}>
{activeTab === 'workspace' && workspaceId ? ( {activeTab === 'workspace' && workspaceMetadata ? (
<Suspense fallback={<WorkspaceDetailSkeleton />}> <Suspense fallback={<WorkspaceDetailSkeleton />}>
<WorkspaceSetting key={workspaceId} workspaceId={workspaceId} /> <WorkspaceSetting
key={workspaceMetadata.id}
workspaceMetadata={workspaceMetadata}
/>
</Suspense> </Suspense>
) : null} ) : null}
{generalSettingList.some(v => v.key === activeTab) ? ( {generalSettingList.some(v => v.key === activeTab) ? (

View File

@@ -4,22 +4,23 @@ import {
} from '@affine/component/setting-components'; } from '@affine/component/setting-components';
import { Avatar } from '@affine/component/ui/avatar'; import { Avatar } from '@affine/component/ui/avatar';
import { Tooltip } from '@affine/component/ui/tooltip'; import { Tooltip } from '@affine/component/ui/tooltip';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { RootWorkspaceMetadata } from '@affine/workspace/atom'; import type { WorkspaceMetadata } from '@affine/workspace';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom'; import {
waitForCurrentWorkspaceAtom,
workspaceListAtom,
} from '@affine/workspace/atom';
import { Logo1Icon } from '@blocksuite/icons'; import { Logo1Icon } from '@blocksuite/icons';
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url'; import { useWorkspaceBlobObjectUrl } from '@toeverything/hooks/use-workspace-blob';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name'; import { useWorkspaceInfo } from '@toeverything/hooks/use-workspace-info';
import { getBlockSuiteWorkspaceAtom } from '@toeverything/infra/__internal__/workspace';
import clsx from 'clsx'; import clsx from 'clsx';
import { useAtom, useAtomValue } from 'jotai/react'; import { useAtom, useAtomValue } from 'jotai/react';
import { type ReactElement, Suspense, useCallback, useMemo } from 'react'; import { type ReactElement, Suspense, useCallback } from 'react';
import { authAtom } from '../../../../atoms'; import { authAtom } from '../../../../atoms';
import { useCurrentLoginStatus } from '../../../../hooks/affine/use-current-login-status'; import { useCurrentLoginStatus } from '../../../../hooks/affine/use-current-login-status';
import { useCurrentUser } from '../../../../hooks/affine/use-current-user'; import { useCurrentUser } from '../../../../hooks/affine/use-current-user';
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
import { UserPlanButton } from '../../auth/user-plan-button'; import { UserPlanButton } from '../../auth/user-plan-button';
import type { import type {
GeneralSettingKeys, GeneralSettingKeys,
@@ -109,7 +110,7 @@ export const SettingSidebar = ({
}: { }: {
generalSettingList: GeneralSettingList; generalSettingList: GeneralSettingList;
onGeneralSettingClick: (key: GeneralSettingKeys) => void; onGeneralSettingClick: (key: GeneralSettingKeys) => void;
onWorkspaceSettingClick: (workspaceId: string) => void; onWorkspaceSettingClick: (workspaceMetadata: WorkspaceMetadata) => void;
selectedWorkspaceId: string | null; selectedWorkspaceId: string | null;
selectedGeneralKey: string | null; selectedGeneralKey: string | null;
onAccountSettingClick: () => void; onAccountSettingClick: () => void;
@@ -182,25 +183,20 @@ export const WorkspaceList = ({
onWorkspaceSettingClick, onWorkspaceSettingClick,
selectedWorkspaceId, selectedWorkspaceId,
}: { }: {
onWorkspaceSettingClick: (workspaceId: string) => void; onWorkspaceSettingClick: (workspaceMetadata: WorkspaceMetadata) => void;
selectedWorkspaceId: string | null; selectedWorkspaceId: string | null;
}) => { }) => {
const workspaces = useAtomValue(rootWorkspacesMetadataAtom); const workspaces = useAtomValue(workspaceListAtom);
const [currentWorkspace] = useCurrentWorkspace(); const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const workspaceList = useMemo(() => {
return workspaces.filter(
({ flavour }) => flavour !== WorkspaceFlavour.AFFINE_PUBLIC
);
}, [workspaces]);
return ( return (
<> <>
{workspaceList.map(workspace => { {workspaces.map(workspace => {
return ( return (
<Suspense key={workspace.id} fallback={<WorkspaceListItemSkeleton />}> <Suspense key={workspace.id} fallback={<WorkspaceListItemSkeleton />}>
<WorkspaceListItem <WorkspaceListItem
meta={workspace} meta={workspace}
onClick={() => { onClick={() => {
onWorkspaceSettingClick(workspace.id); onWorkspaceSettingClick(workspace);
}} }}
isCurrent={workspace.id === currentWorkspace.id} isCurrent={workspace.id === currentWorkspace.id}
isActive={workspace.id === selectedWorkspaceId} isActive={workspace.id === selectedWorkspaceId}
@@ -218,33 +214,34 @@ const WorkspaceListItem = ({
isCurrent, isCurrent,
isActive, isActive,
}: { }: {
meta: RootWorkspaceMetadata; meta: WorkspaceMetadata;
onClick: () => void; onClick: () => void;
isCurrent: boolean; isCurrent: boolean;
isActive: boolean; isActive: boolean;
}) => { }) => {
const [workspaceAtom] = getBlockSuiteWorkspaceAtom(meta.id); const information = useWorkspaceInfo(meta);
const workspace = useAtomValue(workspaceAtom);
const [workspaceAvatar] = useBlockSuiteWorkspaceAvatarUrl(workspace); const avatarUrl = useWorkspaceBlobObjectUrl(meta, information?.avatar);
const [workspaceName] = useBlockSuiteWorkspaceName(workspace);
const name = information?.name ?? UNTITLED_WORKSPACE_NAME;
return ( return (
<div <div
className={clsx(sidebarSelectItem, { active: isActive })} className={clsx(sidebarSelectItem, { active: isActive })}
title={workspaceName} title={name}
onClick={onClick} onClick={onClick}
data-testid="workspace-list-item" data-testid="workspace-list-item"
> >
<Avatar <Avatar
size={14} size={14}
url={workspaceAvatar} url={avatarUrl}
name={workspaceName} name={name}
colorfulFallback colorfulFallback
style={{ style={{
marginRight: '10px', marginRight: '10px',
}} }}
/> />
<span className="setting-name">{workspaceName}</span> <span className="setting-name">{name}</span>
{isCurrent ? ( {isCurrent ? (
<Tooltip content="Current" side="top"> <Tooltip content="Current" side="top">
<div <div

View File

@@ -1,113 +1,18 @@
import { pushNotificationAtom } from '@affine/component/notification-center'; import type { WorkspaceMetadata } from '@affine/workspace/metadata';
import { WorkspaceSubPath } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import { useSetAtom } from 'jotai';
import { useAtomValue } from 'jotai';
import { useCallback } from 'react';
import { getUIAdapter } from '../../../../adapters/workspace'; import { NewWorkspaceSettingDetail } from '../../../../adapters/shared';
import { openSettingModalAtom } from '../../../../atoms'; import { useIsWorkspaceOwner } from '../../../../hooks/affine/use-is-workspace-owner';
import { useLeaveWorkspace } from '../../../../hooks/affine/use-leave-workspace';
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
import { useOnTransformWorkspace } from '../../../../hooks/root/use-on-transform-workspace';
import {
RouteLogic,
useNavigateHelper,
} from '../../../../hooks/use-navigate-helper';
import { useWorkspace } from '../../../../hooks/use-workspace';
import { useAppHelper } from '../../../../hooks/use-workspaces';
export const WorkspaceSetting = ({ workspaceId }: { workspaceId: string }) => {
const t = useAFFiNEI18N();
const { jumpToSubPath, jumpToIndex } = useNavigateHelper();
const [currentWorkspace] = useCurrentWorkspace();
const workspace = useWorkspace(workspaceId);
const [workspaceName] = useBlockSuiteWorkspaceName(
workspace.blockSuiteWorkspace
);
const workspaces = useAtomValue(rootWorkspacesMetadataAtom);
const pushNotification = useSetAtom(pushNotificationAtom);
const leaveWorkspace = useLeaveWorkspace();
const setSettingModal = useSetAtom(openSettingModalAtom);
const { deleteWorkspace } = useAppHelper();
const { NewSettingsDetail } = getUIAdapter(workspace.flavour);
const closeAndJumpOut = useCallback(() => {
setSettingModal(prev => ({ ...prev, open: false, workspaceId: null }));
if (currentWorkspace.id === workspaceId) {
const backWorkspace = workspaces.find(ws => ws.id !== workspaceId);
// TODO: if there is no workspace, jump to a new page(wait for design)
if (backWorkspace) {
jumpToSubPath(
backWorkspace?.id || '',
WorkspaceSubPath.ALL,
RouteLogic.REPLACE
);
} else {
setTimeout(() => {
jumpToIndex(RouteLogic.REPLACE);
}, 100);
}
}
}, [
currentWorkspace.id,
jumpToIndex,
jumpToSubPath,
setSettingModal,
workspaceId,
workspaces,
]);
const handleDeleteWorkspace = useAsyncCallback(async () => {
closeAndJumpOut();
await deleteWorkspace(workspaceId);
pushNotification({
title: t['Successfully deleted'](),
type: 'success',
});
}, [closeAndJumpOut, deleteWorkspace, pushNotification, t, workspaceId]);
const handleLeaveWorkspace = useAsyncCallback(async () => {
closeAndJumpOut();
await leaveWorkspace(workspaceId, workspaceName);
pushNotification({
title: 'Successfully leave',
type: 'success',
});
}, [
closeAndJumpOut,
leaveWorkspace,
pushNotification,
workspaceId,
workspaceName,
]);
const onTransformWorkspace = useOnTransformWorkspace();
// const handleDelete = useCallback(async () => {
// await onDeleteWorkspace();
// toast(t['Successfully deleted'](), {
// portal: document.body,
// });
// onClose();
// }, [onClose, onDeleteWorkspace, t, workspace.id]);
export const WorkspaceSetting = ({
workspaceMetadata,
}: {
workspaceMetadata: WorkspaceMetadata;
}) => {
const isOwner = useIsWorkspaceOwner(workspaceMetadata);
return ( return (
<NewSettingsDetail <NewWorkspaceSettingDetail
onDeleteCloudWorkspace={handleDeleteWorkspace} workspaceMetadata={workspaceMetadata}
onDeleteLocalWorkspace={handleDeleteWorkspace} isOwner={isOwner}
onLeaveWorkspace={handleLeaveWorkspace}
onTransformWorkspace={onTransformWorkspace}
currentWorkspaceId={workspaceId}
/> />
); );
}; };

View File

@@ -1,39 +1,41 @@
import { import { WorkspaceFlavour } from '@affine/env/workspace';
type AffineOfficialWorkspace, import type { Workspace } from '@affine/workspace';
WorkspaceFlavour, import { workspaceManagerAtom } from '@affine/workspace/atom';
} from '@affine/env/workspace';
import type { Page } from '@blocksuite/store'; import type { Page } from '@blocksuite/store';
import { useCallback, useState } from 'react'; import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
import { useAtomValue } from 'jotai';
import { useState } from 'react';
import { useOnTransformWorkspace } from '../../../hooks/root/use-on-transform-workspace'; import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
import { EnableAffineCloudModal } from '../enable-affine-cloud-modal'; import { EnableAffineCloudModal } from '../enable-affine-cloud-modal';
import { ShareMenu } from './share-menu'; import { ShareMenu } from './share-menu';
type SharePageModalProps = { type SharePageModalProps = {
workspace: AffineOfficialWorkspace; workspace: Workspace;
page: Page; page: Page;
}; };
export const SharePageButton = ({ workspace, page }: SharePageModalProps) => { export const SharePageButton = ({ workspace, page }: SharePageModalProps) => {
const onTransformWorkspace = useOnTransformWorkspace();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const handleConfirm = useCallback(() => { const { openPage } = useNavigateHelper();
const workspaceManager = useAtomValue(workspaceManagerAtom);
const handleConfirm = useAsyncCallback(async () => {
if (workspace.flavour !== WorkspaceFlavour.LOCAL) { if (workspace.flavour !== WorkspaceFlavour.LOCAL) {
return; return;
} }
onTransformWorkspace( const { id: newId } =
WorkspaceFlavour.LOCAL, await workspaceManager.transformLocalToCloud(workspace);
WorkspaceFlavour.AFFINE_CLOUD, openPage(newId, page.id);
workspace
);
setOpen(false); setOpen(false);
}, [onTransformWorkspace, workspace]); }, [openPage, page.id, workspace, workspaceManager]);
return ( return (
<> <>
<ShareMenu <ShareMenu
workspace={workspace} workspaceMetadata={workspace.meta}
currentPage={page} currentPage={page}
onEnableAffineCloud={() => setOpen(true)} onEnableAffineCloud={() => setOpen(true)}
/> />

View File

@@ -10,7 +10,10 @@ import * as styles from './index.css';
import type { ShareMenuProps } from './share-menu'; import type { ShareMenuProps } from './share-menu';
import { useSharingUrl } from './use-share-url'; import { useSharingUrl } from './use-share-url';
export const ShareExport = ({ workspace, currentPage }: ShareMenuProps) => { export const ShareExport = ({
workspaceMetadata: workspace,
currentPage,
}: ShareMenuProps) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const workspaceId = workspace.id; const workspaceId = workspace.id;
const pageId = currentPage.id; const pageId = currentPage.id;

View File

@@ -1,14 +1,9 @@
import { Button } from '@affine/component/ui/button'; import { Button } from '@affine/component/ui/button';
import { Divider } from '@affine/component/ui/divider'; import { Divider } from '@affine/component/ui/divider';
import { Menu } from '@affine/component/ui/menu'; import { Menu } from '@affine/component/ui/menu';
import { import { WorkspaceFlavour } from '@affine/env/workspace';
type AffineCloudWorkspace,
type AffineOfficialWorkspace,
type AffinePublicWorkspace,
type LocalWorkspace,
WorkspaceFlavour,
} from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { WorkspaceMetadata } from '@affine/workspace';
import { WebIcon } from '@blocksuite/icons'; import { WebIcon } from '@blocksuite/icons';
import type { Page } from '@blocksuite/store'; import type { Page } from '@blocksuite/store';
@@ -17,13 +12,8 @@ import * as styles from './index.css';
import { ShareExport } from './share-export'; import { ShareExport } from './share-export';
import { SharePage } from './share-page'; import { SharePage } from './share-page';
export interface ShareMenuProps< export interface ShareMenuProps {
Workspace extends AffineOfficialWorkspace = workspaceMetadata: WorkspaceMetadata;
| AffineCloudWorkspace
| LocalWorkspace
| AffinePublicWorkspace,
> {
workspace: Workspace;
currentPage: Page; currentPage: Page;
onEnableAffineCloud: () => void; onEnableAffineCloud: () => void;
} }
@@ -70,7 +60,7 @@ const LocalShareMenu = (props: ShareMenuProps) => {
const CloudShareMenu = (props: ShareMenuProps) => { const CloudShareMenu = (props: ShareMenuProps) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const { const {
workspace: { id: workspaceId }, workspaceMetadata: { id: workspaceId },
currentPage, currentPage,
} = props; } = props;
const { isSharedPage } = useIsSharedPage(workspaceId, currentPage.id); const { isSharedPage } = useIsSharedPage(workspaceId, currentPage.id);
@@ -96,9 +86,9 @@ const CloudShareMenu = (props: ShareMenuProps) => {
}; };
export const ShareMenu = (props: ShareMenuProps) => { export const ShareMenu = (props: ShareMenuProps) => {
const { workspace } = props; const { workspaceMetadata } = props;
if (workspace.flavour === WorkspaceFlavour.LOCAL) { if (workspaceMetadata.flavour === WorkspaceFlavour.LOCAL) {
return <LocalShareMenu {...props} />; return <LocalShareMenu {...props} />;
} }
return <CloudShareMenu {...props} />; return <CloudShareMenu {...props} />;

View File

@@ -69,7 +69,7 @@ export const LocalSharePage = (props: ShareMenuProps) => {
export const AffineSharePage = (props: ShareMenuProps) => { export const AffineSharePage = (props: ShareMenuProps) => {
const { const {
workspace: { id: workspaceId }, workspaceMetadata: { id: workspaceId },
currentPage, currentPage,
} = props; } = props;
const pageId = currentPage.id; const pageId = currentPage.id;
@@ -239,9 +239,11 @@ export const AffineSharePage = (props: ShareMenuProps) => {
}; };
export const SharePage = (props: ShareMenuProps) => { export const SharePage = (props: ShareMenuProps) => {
if (props.workspace.flavour === WorkspaceFlavour.LOCAL) { if (props.workspaceMetadata.flavour === WorkspaceFlavour.LOCAL) {
return <LocalSharePage {...props} />; return <LocalSharePage {...props} />;
} else if (props.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD) { } else if (
props.workspaceMetadata.flavour === WorkspaceFlavour.AFFINE_CLOUD
) {
return <AffineSharePage {...props} />; return <AffineSharePage {...props} />;
} }
throw new Error('Unreachable'); throw new Error('Unreachable');

View File

@@ -1,4 +1,4 @@
import type { AffineOfficialWorkspace } from '@affine/env/workspace'; import type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
import { import {
useBlockSuitePageMeta, useBlockSuitePageMeta,
usePageMetaHelper, usePageMetaHelper,
@@ -18,7 +18,7 @@ import { PageHeaderMenuButton } from './operation-menu';
import * as styles from './styles.css'; import * as styles from './styles.css';
export interface BlockSuiteHeaderTitleProps { export interface BlockSuiteHeaderTitleProps {
workspace: AffineOfficialWorkspace; blockSuiteWorkspace: BlockSuiteWorkspace;
pageId: string; pageId: string;
isPublic?: boolean; isPublic?: boolean;
publicMode?: PageMode; publicMode?: PageMode;
@@ -53,7 +53,7 @@ const EditableTitle = ({
}; };
const StableTitle = ({ const StableTitle = ({
workspace, blockSuiteWorkspace: workspace,
pageId, pageId,
onRename, onRename,
isPublic, isPublic,
@@ -61,8 +61,8 @@ const StableTitle = ({
}: BlockSuiteHeaderTitleProps & { }: BlockSuiteHeaderTitleProps & {
onRename?: () => void; onRename?: () => void;
}) => { }) => {
const currentPage = workspace.blockSuiteWorkspace.getPage(pageId); const currentPage = workspace.getPage(pageId);
const pageMeta = useBlockSuitePageMeta(workspace.blockSuiteWorkspace).find( const pageMeta = useBlockSuitePageMeta(workspace).find(
meta => meta.id === currentPage?.id meta => meta.id === currentPage?.id
); );
@@ -77,7 +77,7 @@ const StableTitle = ({
return ( return (
<div className={styles.headerTitleContainer}> <div className={styles.headerTitleContainer}>
<EditorModeSwitch <EditorModeSwitch
blockSuiteWorkspace={workspace.blockSuiteWorkspace} blockSuiteWorkspace={workspace}
pageId={pageId} pageId={pageId}
isPublic={isPublic} isPublic={isPublic}
publicMode={publicMode} publicMode={publicMode}
@@ -97,12 +97,12 @@ const StableTitle = ({
}; };
const BlockSuiteTitleWithRename = (props: BlockSuiteHeaderTitleProps) => { const BlockSuiteTitleWithRename = (props: BlockSuiteHeaderTitleProps) => {
const { workspace, pageId } = props; const { blockSuiteWorkspace: workspace, pageId } = props;
const currentPage = workspace.blockSuiteWorkspace.getPage(pageId); const currentPage = workspace.getPage(pageId);
const pageMeta = useBlockSuitePageMeta(workspace.blockSuiteWorkspace).find( const pageMeta = useBlockSuitePageMeta(workspace).find(
meta => meta.id === currentPage?.id meta => meta.id === currentPage?.id
); );
const pageTitleMeta = usePageMetaHelper(workspace.blockSuiteWorkspace); const pageTitleMeta = usePageMetaHelper(workspace);
const [isEditable, setIsEditable] = useState(false); const [isEditable, setIsEditable] = useState(false);
const [title, setPageTitle] = useState(pageMeta?.title || 'Untitled'); const [title, setPageTitle] = useState(pageMeta?.title || 'Untitled');

View File

@@ -7,6 +7,7 @@ import {
} from '@affine/component/ui/menu'; } from '@affine/component/ui/menu';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { waitForCurrentWorkspaceAtom } from '@affine/workspace/atom';
import { assertExists } from '@blocksuite/global/utils'; import { assertExists } from '@blocksuite/global/utils';
import { import {
DuplicateIcon, DuplicateIcon,
@@ -18,7 +19,6 @@ import {
ImportIcon, ImportIcon,
PageIcon, PageIcon,
} from '@blocksuite/icons'; } from '@blocksuite/icons';
import type { PageMeta } from '@blocksuite/store';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta'; import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
@@ -27,7 +27,6 @@ import { currentModeAtom } from '../../../atoms/mode';
import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper'; import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper';
import { useExportPage } from '../../../hooks/affine/use-export-page'; import { useExportPage } from '../../../hooks/affine/use-export-page';
import { useTrashModalHelper } from '../../../hooks/affine/use-trash-modal-helper'; import { useTrashModalHelper } from '../../../hooks/affine/use-trash-modal-helper';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { toast } from '../../../utils'; import { toast } from '../../../utils';
import { PageHistoryModal } from '../../affine/page-history-modal/history-modal'; import { PageHistoryModal } from '../../affine/page-history-modal/history-modal';
import { HeaderDropDownButton } from '../../pure/header-drop-down-button'; import { HeaderDropDownButton } from '../../pure/header-drop-down-button';
@@ -42,16 +41,16 @@ export const PageHeaderMenuButton = ({ rename, pageId }: PageMenuProps) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
// fixme(himself65): remove these hooks ASAP // fixme(himself65): remove these hooks ASAP
const [workspace] = useCurrentWorkspace(); const workspace = useAtomValue(waitForCurrentWorkspaceAtom);
const blockSuiteWorkspace = workspace.blockSuiteWorkspace; const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
const currentPage = blockSuiteWorkspace.getPage(pageId); const currentPage = blockSuiteWorkspace.getPage(pageId);
assertExists(currentPage); assertExists(currentPage);
const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find( const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
meta => meta.id === pageId meta => meta.id === pageId
) as PageMeta; );
const currentMode = useAtomValue(currentModeAtom); const currentMode = useAtomValue(currentModeAtom);
const favorite = pageMeta.favorite ?? false; const favorite = pageMeta?.favorite ?? false;
const { togglePageMode, toggleFavorite, duplicate } = const { togglePageMode, toggleFavorite, duplicate } =
useBlockSuiteMetaHelper(blockSuiteWorkspace); useBlockSuiteMetaHelper(blockSuiteWorkspace);
@@ -65,12 +64,15 @@ export const PageHeaderMenuButton = ({ rename, pageId }: PageMenuProps) => {
}, []); }, []);
const handleOpenTrashModal = useCallback(() => { const handleOpenTrashModal = useCallback(() => {
if (!pageMeta) {
return;
}
setTrashModal({ setTrashModal({
open: true, open: true,
pageIds: [pageId], pageIds: [pageId],
pageTitles: [pageMeta.title], pageTitles: [pageMeta.title],
}); });
}, [pageId, pageMeta.title, setTrashModal]); }, [pageId, pageMeta, setTrashModal]);
const handleFavorite = useCallback(() => { const handleFavorite = useCallback(() => {
toggleFavorite(pageId); toggleFavorite(pageId);
@@ -205,7 +207,7 @@ export const PageHeaderMenuButton = ({ rename, pageId }: PageMenuProps) => {
/> />
</> </>
); );
if (pageMeta.trash) { if (pageMeta?.trash) {
return null; return null;
} }
return ( return (

View File

@@ -1,6 +1,5 @@
import { Tooltip } from '@affine/component/ui/tooltip'; import { Tooltip } from '@affine/component/ui/tooltip';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { assertExists } from '@blocksuite/global/utils';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta'; import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import type { CSSProperties } from 'react'; import type { CSSProperties } from 'react';
@@ -44,8 +43,7 @@ export const EditorModeSwitch = ({
const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find( const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
meta => meta.id === pageId meta => meta.id === pageId
); );
assertExists(pageMeta); const trash = pageMeta?.trash ?? false;
const { trash } = pageMeta;
const { togglePageMode, switchToEdgelessMode, switchToPageMode } = const { togglePageMode, switchToEdgelessMode, switchToPageMode } =
useBlockSuiteMetaHelper(blockSuiteWorkspace); useBlockSuiteMetaHelper(blockSuiteWorkspace);

View File

@@ -1,10 +1,8 @@
import './page-detail-editor.css'; import './page-detail-editor.css';
import { PageNotFoundError } from '@affine/env/constant';
import { assertExists, DisposableGroup } from '@blocksuite/global/utils'; import { assertExists, DisposableGroup } from '@blocksuite/global/utils';
import type { AffineEditorContainer } from '@blocksuite/presets'; import type { AffineEditorContainer } from '@blocksuite/presets';
import type { Page, Workspace } from '@blocksuite/store'; import type { Page, Workspace } from '@blocksuite/store';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page'; import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page';
import { pluginEditorAtom } from '@toeverything/infra/__internal__/plugin'; import { pluginEditorAtom } from '@toeverything/infra/__internal__/plugin';
import { getCurrentStore } from '@toeverything/infra/atom'; import { getCurrentStore } from '@toeverything/infra/atom';
@@ -51,10 +49,6 @@ const PageDetailEditorMain = memo(function PageDetailEditorMain({
isPublic, isPublic,
publishMode, publishMode,
}: PageDetailEditorProps & { page: Page }) { }: PageDetailEditorProps & { page: Page }) {
const meta = useBlockSuitePageMeta(workspace).find(
meta => meta.id === pageId
);
const { switchToEdgelessMode, switchToPageMode } = const { switchToEdgelessMode, switchToPageMode } =
useBlockSuiteMetaHelper(workspace); useBlockSuiteMetaHelper(workspace);
@@ -73,7 +67,6 @@ const PageDetailEditorMain = memo(function PageDetailEditorMain({
const { appSettings } = useAppSettingHelper(); const { appSettings } = useAppSettingHelper();
assertExists(meta);
const value = useMemo(() => { const value = useMemo(() => {
const fontStyle = fontStyleOptions.find( const fontStyle = fontStyleOptions.find(
option => option.key === appSettings.fontStyle option => option.key === appSettings.fontStyle
@@ -171,9 +164,8 @@ export const PageDetailEditor = (props: PageDetailEditorProps) => {
const { workspace, pageId } = props; const { workspace, pageId } = props;
const page = useBlockSuiteWorkspacePage(workspace, pageId); const page = useBlockSuiteWorkspacePage(workspace, pageId);
if (!page) { if (!page) {
throw new PageNotFoundError(workspace, pageId); return null;
} }
return ( return (
<Suspense> <Suspense>
<PageDetailEditorMain {...props} page={page} /> <PageDetailEditorMain {...props} page={page} />

View File

@@ -2,21 +2,17 @@ import { commandScore } from '@affine/cmdk';
import { useCollectionManager } from '@affine/component/page-list'; import { useCollectionManager } from '@affine/component/page-list';
import type { Collection } from '@affine/env/filter'; import type { Collection } from '@affine/env/filter';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
currentWorkspaceAtom,
waitForCurrentWorkspaceAtom,
} from '@affine/workspace/atom';
import { EdgelessIcon, PageIcon, ViewLayersIcon } from '@blocksuite/icons'; import { EdgelessIcon, PageIcon, ViewLayersIcon } from '@blocksuite/icons';
import type { Page, PageMeta } from '@blocksuite/store'; import type { Page, PageMeta } from '@blocksuite/store';
import { import {
useBlockSuitePageMeta, useBlockSuitePageMeta,
usePageMetaHelper, usePageMetaHelper,
} from '@toeverything/hooks/use-block-suite-page-meta'; } from '@toeverything/hooks/use-block-suite-page-meta';
import { import { currentPageIdAtom, getCurrentStore } from '@toeverything/infra/atom';
getWorkspace,
waitForWorkspace,
} from '@toeverything/infra/__internal__/workspace';
import {
currentPageIdAtom,
currentWorkspaceIdAtom,
getCurrentStore,
} from '@toeverything/infra/atom';
import { import {
type AffineCommand, type AffineCommand,
AffineCommandRegistry, AffineCommandRegistry,
@@ -33,7 +29,6 @@ import {
recentPageIdsBaseAtom, recentPageIdsBaseAtom,
} from '../../../atoms'; } from '../../../atoms';
import { collectionsCRUDAtom } from '../../../atoms/collections'; import { collectionsCRUDAtom } from '../../../atoms/collections';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { useNavigateHelper } from '../../../hooks/use-navigate-helper'; import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
import { WorkspaceSubPath } from '../../../shared'; import { WorkspaceSubPath } from '../../../shared';
import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils'; import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils';
@@ -53,8 +48,8 @@ export const cmdkValueAtom = atom('');
// like currentWorkspaceAtom, but not throw error // like currentWorkspaceAtom, but not throw error
const safeCurrentPageAtom = atom<Promise<Page | undefined>>(async get => { const safeCurrentPageAtom = atom<Promise<Page | undefined>>(async get => {
const currentWorkspaceId = get(currentWorkspaceIdAtom); const currentWorkspace = get(currentWorkspaceAtom);
if (!currentWorkspaceId) { if (!currentWorkspace) {
return; return;
} }
@@ -64,9 +59,7 @@ const safeCurrentPageAtom = atom<Promise<Page | undefined>>(async get => {
return; return;
} }
const workspace = getWorkspace(currentWorkspaceId); const page = currentWorkspace.blockSuiteWorkspace.getPage(currentPageId);
await waitForWorkspace(workspace);
const page = workspace.getPage(currentPageId);
if (!page) { if (!page) {
return; return;
@@ -132,7 +125,7 @@ export const filteredAffineCommands = atom(async get => {
}); });
const useWorkspacePages = () => { const useWorkspacePages = () => {
const [currentWorkspace] = useCurrentWorkspace(); const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const pages = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace); const pages = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace);
return pages; return pages;
}; };
@@ -166,7 +159,7 @@ export const pageToCommand = (
blockId?: string blockId?: string
): CMDKCommand => { ): CMDKCommand => {
const pageMode = store.get(pageSettingsAtom)?.[page.id]?.mode; const pageMode = store.get(pageSettingsAtom)?.[page.id]?.mode;
const currentWorkspaceId = store.get(currentWorkspaceIdAtom); const currentWorkspace = store.get(currentWorkspaceAtom);
const title = page.title || t['Untitled'](); const title = page.title || t['Untitled']();
const commandLabel = label || { const commandLabel = label || {
@@ -191,18 +184,18 @@ export const pageToCommand = (
originalValue: title, originalValue: title,
category: category, category: category,
run: () => { run: () => {
if (!currentWorkspaceId) { if (!currentWorkspace) {
console.error('current workspace not found'); console.error('current workspace not found');
return; return;
} }
if (blockId) { if (blockId) {
return navigationHelper.jumpToPageBlock( return navigationHelper.jumpToPageBlock(
currentWorkspaceId, currentWorkspace.id,
page.id, page.id,
blockId blockId
); );
} }
return navigationHelper.jumpToPage(currentWorkspaceId, page.id); return navigationHelper.jumpToPage(currentWorkspace.id, page.id);
}, },
icon: pageMode === 'edgeless' ? <EdgelessIcon /> : <PageIcon />, icon: pageMode === 'edgeless' ? <EdgelessIcon /> : <PageIcon />,
timestamp: page.updatedDate, timestamp: page.updatedDate,
@@ -217,7 +210,7 @@ export const usePageCommands = () => {
const recentPages = useRecentPages(); const recentPages = useRecentPages();
const pages = useWorkspacePages(); const pages = useWorkspacePages();
const store = getCurrentStore(); const store = getCurrentStore();
const [workspace] = useCurrentWorkspace(); const workspace = useAtomValue(waitForCurrentWorkspaceAtom);
const pageHelper = usePageHelper(workspace.blockSuiteWorkspace); const pageHelper = usePageHelper(workspace.blockSuiteWorkspace);
const pageMetaHelper = usePageMetaHelper(workspace.blockSuiteWorkspace); const pageMetaHelper = usePageMetaHelper(workspace.blockSuiteWorkspace);
const query = useAtomValue(cmdkQueryAtom); const query = useAtomValue(cmdkQueryAtom);
@@ -359,7 +352,7 @@ export const collectionToCommand = (
selectCollection: (id: string) => void, selectCollection: (id: string) => void,
t: ReturnType<typeof useAFFiNEI18N> t: ReturnType<typeof useAFFiNEI18N>
): CMDKCommand => { ): CMDKCommand => {
const currentWorkspaceId = store.get(currentWorkspaceIdAtom); const currentWorkspace = store.get(currentWorkspaceAtom);
const label = collection.name || t['Untitled'](); const label = collection.name || t['Untitled']();
const category = 'affine:collections'; const category = 'affine:collections';
return { return {
@@ -377,11 +370,11 @@ export const collectionToCommand = (
originalValue: label, originalValue: label,
category: category, category: category,
run: () => { run: () => {
if (!currentWorkspaceId) { if (!currentWorkspace) {
console.error('current workspace not found'); console.error('current workspace not found');
return; return;
} }
navigationHelper.jumpToSubPath(currentWorkspaceId, WorkspaceSubPath.ALL); navigationHelper.jumpToSubPath(currentWorkspace.id, WorkspaceSubPath.ALL);
selectCollection(collection.id); selectCollection(collection.id);
}, },
icon: <ViewLayersIcon />, icon: <ViewLayersIcon />,
@@ -395,7 +388,7 @@ export const useCollectionsCommands = () => {
const query = useAtomValue(cmdkQueryAtom); const query = useAtomValue(cmdkQueryAtom);
const navigationHelper = useNavigateHelper(); const navigationHelper = useNavigateHelper();
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const [workspace] = useCurrentWorkspace(); const workspace = useAtomValue(waitForCurrentWorkspaceAtom);
const selectCollection = useCallback( const selectCollection = useCallback(
(id: string) => { (id: string) => {
navigationHelper.jumpToCollection(workspace.id, id); navigationHelper.jumpToCollection(workspace.id, id);

View File

@@ -40,7 +40,6 @@ export const HelpIsland = () => {
setOpenSettingModalAtom({ setOpenSettingModalAtom({
open: true, open: true,
activeTab: tab, activeTab: tab,
workspaceId: null,
}); });
}, },
[setOpenSettingModalAtom] [setOpenSettingModalAtom]

View File

@@ -3,21 +3,21 @@ import { ConfirmModal } from '@affine/component/ui/modal';
import { Tooltip } from '@affine/component/ui/tooltip'; import { Tooltip } from '@affine/component/ui/tooltip';
import { WorkspaceSubPath } from '@affine/env/workspace'; import { WorkspaceSubPath } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { waitForCurrentWorkspaceAtom } from '@affine/workspace/atom';
import { assertExists } from '@blocksuite/global/utils'; import { assertExists } from '@blocksuite/global/utils';
import { DeleteIcon, ResetIcon } from '@blocksuite/icons'; import { DeleteIcon, ResetIcon } from '@blocksuite/icons';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta'; import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useAtomValue } from 'jotai';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { useAppSettingHelper } from '../../../hooks/affine/use-app-setting-helper'; import { useAppSettingHelper } from '../../../hooks/affine/use-app-setting-helper';
import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper'; import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { useNavigateHelper } from '../../../hooks/use-navigate-helper'; import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
import { toast } from '../../../utils'; import { toast } from '../../../utils';
import * as styles from './styles.css'; import * as styles from './styles.css';
export const TrashPageFooter = ({ pageId }: { pageId: string }) => { export const TrashPageFooter = ({ pageId }: { pageId: string }) => {
// fixme(himself65): remove these hooks ASAP const workspace = useAtomValue(waitForCurrentWorkspaceAtom);
const [workspace] = useCurrentWorkspace();
assertExists(workspace); assertExists(workspace);
const blockSuiteWorkspace = workspace.blockSuiteWorkspace; const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find( const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find(

View File

@@ -15,7 +15,6 @@ import { MoreHorizontalIcon, ViewLayersIcon } from '@blocksuite/icons';
import type { PageMeta, Workspace } from '@blocksuite/store'; import type { PageMeta, Workspace } from '@blocksuite/store';
import { useDroppable } from '@dnd-kit/core'; import { useDroppable } from '@dnd-kit/core';
import * as Collapsible from '@radix-ui/react-collapsible'; import * as Collapsible from '@radix-ui/react-collapsible';
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta'; import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
@@ -44,9 +43,9 @@ const CollectionRenderer = ({
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const dragItemId = getDropItemId('collections', collection.id); const dragItemId = getDropItemId('collections', collection.id);
const removeFromAllowList = useAsyncCallback( const removeFromAllowList = useCallback(
async (id: string) => { (id: string) => {
await setting.updateCollection({ setting.updateCollection({
...collection, ...collection,
allowList: collection.allowList?.filter(v => v !== id), allowList: collection.allowList?.filter(v => v !== id),
}); });
@@ -66,9 +65,7 @@ const CollectionRenderer = ({
} else { } else {
toast(t['com.affine.collection.addPage.success']()); toast(t['com.affine.collection.addPage.success']());
} }
setting.addPage(collection.id, id).catch(err => { setting.addPage(collection.id, id);
console.error(err);
});
}, },
}, },
}); });
@@ -90,9 +87,9 @@ const CollectionRenderer = ({
const currentPath = location.pathname.split('?')[0]; const currentPath = location.pathname.split('?')[0];
const path = `/workspace/${workspace.id}/collection/${collection.id}`; const path = `/workspace/${workspace.id}/collection/${collection.id}`;
const onRename = useAsyncCallback( const onRename = useCallback(
async (name: string) => { (name: string) => {
await setting.updateCollection({ setting.updateCollection({
...collection, ...collection,
name, name,
}); });

View File

@@ -1,12 +1,16 @@
import { Divider } from '@affine/component/ui/divider'; import { Divider } from '@affine/component/ui/divider';
import { MenuItem } from '@affine/component/ui/menu'; import { MenuItem } from '@affine/component/ui/menu';
import { Unreachable } from '@affine/env/constant';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom'; import {
workspaceListAtom,
workspaceManagerAtom,
} from '@affine/workspace/atom';
import { Logo1Icon } from '@blocksuite/icons'; import { Logo1Icon } from '@blocksuite/icons';
import { useAtomValue, useSetAtom } from 'jotai'; import { useAtomValue, useSetAtom } from 'jotai';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports // eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { useSession } from 'next-auth/react'; import { useSession } from 'next-auth/react';
import { useCallback, useMemo } from 'react'; import { useCallback, useEffect, useMemo } from 'react';
import { import {
authAtom, authAtom,
@@ -81,9 +85,16 @@ export const UserWithWorkspaceList = ({
onEventEnd?.(); onEventEnd?.();
}, [onEventEnd, setOpenCreateWorkspaceModal]); }, [onEventEnd, setOpenCreateWorkspaceModal]);
const workspaces = useAtomValue(rootWorkspacesMetadataAtom, { const workspaces = useAtomValue(workspaceListAtom);
delay: 0,
}); const workspaceManager = useAtomValue(workspaceManagerAtom);
// revalidate workspace list when mounted
useEffect(() => {
workspaceManager.list.revalidate().catch(err => {
throw new Unreachable('revlidate should never throw, ' + err);
});
}, [workspaceManager]);
return ( return (
<div className={styles.workspaceListWrapper}> <div className={styles.workspaceListWrapper}>

View File

@@ -1,39 +1,29 @@
import { ScrollableContainer } from '@affine/component'; import { ScrollableContainer } from '@affine/component';
import { Divider } from '@affine/component/ui/divider'; import { Divider } from '@affine/component/ui/divider';
import { WorkspaceList } from '@affine/component/workspace-list'; import { WorkspaceList } from '@affine/component/workspace-list';
import type {
AffineCloudWorkspace,
LocalWorkspace,
} from '@affine/env/workspace';
import { WorkspaceFlavour, WorkspaceSubPath } from '@affine/env/workspace'; import { WorkspaceFlavour, WorkspaceSubPath } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { RootWorkspaceMetadata } from '@affine/workspace/atom'; import type { WorkspaceMetadata } from '@affine/workspace';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom'; import { currentWorkspaceAtom } from '@affine/workspace/atom';
import type { DragEndEvent } from '@dnd-kit/core'; import type { DragEndEvent } from '@dnd-kit/core';
import { arrayMove } from '@dnd-kit/sortable'; import { useAtomValue, useSetAtom } from 'jotai';
import {
currentPageIdAtom,
currentWorkspaceIdAtom,
} from '@toeverything/infra/atom';
import { useAtom, useSetAtom } from 'jotai';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports // eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { useSession } from 'next-auth/react'; import { useSession } from 'next-auth/react';
import { startTransition, useCallback, useMemo, useTransition } from 'react'; import { useCallback, useMemo } from 'react';
import { import {
openCreateWorkspaceModalAtom, openCreateWorkspaceModalAtom,
openSettingModalAtom, openSettingModalAtom,
} from '../../../../../atoms'; } from '../../../../../atoms';
import type { AllWorkspace } from '../../../../../shared';
import { useIsWorkspaceOwner } from '../.././../../../hooks/affine/use-is-workspace-owner'; import { useIsWorkspaceOwner } from '../.././../../../hooks/affine/use-is-workspace-owner';
import { useNavigateHelper } from '../.././../../../hooks/use-navigate-helper'; import { useNavigateHelper } from '../.././../../../hooks/use-navigate-helper';
import * as styles from './index.css'; import * as styles from './index.css';
interface WorkspaceModalProps { interface WorkspaceModalProps {
disabled?: boolean; disabled?: boolean;
workspaces: (AffineCloudWorkspace | LocalWorkspace)[]; workspaces: WorkspaceMetadata[];
currentWorkspaceId: AllWorkspace['id'] | null; currentWorkspaceId?: string | null;
onClickWorkspace: (workspace: RootWorkspaceMetadata['id']) => void; onClickWorkspace: (workspaceMetadata: WorkspaceMetadata) => void;
onClickWorkspaceSetting: (workspace: RootWorkspaceMetadata['id']) => void; onClickWorkspaceSetting: (workspaceMetadata: WorkspaceMetadata) => void;
onNewWorkspace: () => void; onNewWorkspace: () => void;
onAddWorkspace: () => void; onAddWorkspace: () => void;
onDragEnd: (event: DragEndEvent) => void; onDragEnd: (event: DragEndEvent) => void;
@@ -102,22 +92,14 @@ export const AFFiNEWorkspaceList = ({
workspaces, workspaces,
onEventEnd, onEventEnd,
}: { }: {
workspaces: RootWorkspaceMetadata[]; workspaces: WorkspaceMetadata[];
onEventEnd?: () => void; onEventEnd?: () => void;
}) => { }) => {
const setOpenCreateWorkspaceModal = useSetAtom(openCreateWorkspaceModalAtom); const setOpenCreateWorkspaceModal = useSetAtom(openCreateWorkspaceModalAtom);
const { jumpToSubPath } = useNavigateHelper(); const { jumpToSubPath } = useNavigateHelper();
const setWorkspaces = useSetAtom(rootWorkspacesMetadataAtom); const currentWorkspace = useAtomValue(currentWorkspaceAtom);
const [currentWorkspaceId, setCurrentWorkspaceId] = useAtom(
currentWorkspaceIdAtom
);
const setCurrentPageId = useSetAtom(currentPageIdAtom);
const [, startCloseTransition] = useTransition();
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom); const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
@@ -130,7 +112,7 @@ export const AFFiNEWorkspaceList = ({
() => () =>
workspaces.filter( workspaces.filter(
({ flavour }) => flavour === WorkspaceFlavour.AFFINE_CLOUD ({ flavour }) => flavour === WorkspaceFlavour.AFFINE_CLOUD
) as (AffineCloudWorkspace | LocalWorkspace)[], ) as WorkspaceMetadata[],
[workspaces] [workspaces]
); );
@@ -138,44 +120,37 @@ export const AFFiNEWorkspaceList = ({
() => () =>
workspaces.filter( workspaces.filter(
({ flavour }) => flavour === WorkspaceFlavour.LOCAL ({ flavour }) => flavour === WorkspaceFlavour.LOCAL
) as (AffineCloudWorkspace | LocalWorkspace)[], ) as WorkspaceMetadata[],
[workspaces] [workspaces]
); );
const onClickWorkspaceSetting = useCallback( const onClickWorkspaceSetting = useCallback(
(workspaceId: string) => { (workspaceMetadata: WorkspaceMetadata) => {
setOpenSettingModalAtom({ setOpenSettingModalAtom({
open: true, open: true,
activeTab: 'workspace', activeTab: 'workspace',
workspaceId, workspaceMetadata,
}); });
onEventEnd?.(); onEventEnd?.();
}, },
[onEventEnd, setOpenSettingModalAtom] [onEventEnd, setOpenSettingModalAtom]
); );
const onMoveWorkspace = useCallback( const onMoveWorkspace = useCallback((_activeId: string, _overId: string) => {
(activeId: string, overId: string) => { // TODO: order
const oldIndex = workspaces.findIndex(w => w.id === activeId); // const oldIndex = workspaces.findIndex(w => w.id === activeId);
// const newIndex = workspaces.findIndex(w => w.id === overId);
const newIndex = workspaces.findIndex(w => w.id === overId); // startTransition(() => {
startTransition(() => { // setWorkspaces(workspaces => arrayMove(workspaces, oldIndex, newIndex));
setWorkspaces(workspaces => arrayMove(workspaces, oldIndex, newIndex)); // });
}); }, []);
},
[setWorkspaces, workspaces]
);
const onClickWorkspace = useCallback( const onClickWorkspace = useCallback(
(workspaceId: string) => { (workspaceMetadata: WorkspaceMetadata) => {
startCloseTransition(() => { jumpToSubPath(workspaceMetadata.id, WorkspaceSubPath.ALL);
setCurrentWorkspaceId(workspaceId);
setCurrentPageId(null);
jumpToSubPath(workspaceId, WorkspaceSubPath.ALL);
});
onEventEnd?.(); onEventEnd?.();
}, },
[jumpToSubPath, onEventEnd, setCurrentPageId, setCurrentWorkspaceId] [jumpToSubPath, onEventEnd]
); );
const onDragEnd = useCallback( const onDragEnd = useCallback(
@@ -211,7 +186,7 @@ export const AFFiNEWorkspaceList = ({
onClickWorkspaceSetting={onClickWorkspaceSetting} onClickWorkspaceSetting={onClickWorkspaceSetting}
onNewWorkspace={onNewWorkspace} onNewWorkspace={onNewWorkspace}
onAddWorkspace={onAddWorkspace} onAddWorkspace={onAddWorkspace}
currentWorkspaceId={currentWorkspaceId} currentWorkspaceId={currentWorkspace?.id}
onDragEnd={onDragEnd} onDragEnd={onDragEnd}
/> />
{localWorkspaces.length > 0 && cloudWorkspaces.length > 0 ? ( {localWorkspaces.length > 0 && cloudWorkspaces.length > 0 ? (
@@ -225,7 +200,7 @@ export const AFFiNEWorkspaceList = ({
onClickWorkspaceSetting={onClickWorkspaceSetting} onClickWorkspaceSetting={onClickWorkspaceSetting}
onNewWorkspace={onNewWorkspace} onNewWorkspace={onNewWorkspace}
onAddWorkspace={onAddWorkspace} onAddWorkspace={onAddWorkspace}
currentWorkspaceId={currentWorkspaceId} currentWorkspaceId={currentWorkspace?.id}
onDragEnd={onDragEnd} onDragEnd={onDragEnd}
/> />
</ScrollableContainer> </ScrollableContainer>

View File

@@ -1,12 +1,10 @@
import { Avatar } from '@affine/component/ui/avatar'; import { Avatar } from '@affine/component/ui/avatar';
import { Loading } from '@affine/component/ui/loading'; import { Loading } from '@affine/component/ui/loading';
import { Tooltip } from '@affine/component/ui/tooltip'; import { Tooltip } from '@affine/component/ui/tooltip';
import { useCurrentSyncEngine } from '@affine/core/hooks/current/use-current-sync-engine'; import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { import { type SyncEngineStatus, SyncEngineStep } from '@affine/workspace';
type SyncEngineStatus, import { waitForCurrentWorkspaceAtom } from '@affine/workspace/atom';
SyncEngineStep,
} from '@affine/workspace/providers';
import { import {
CloudWorkspaceIcon, CloudWorkspaceIcon,
InformationFillDuotoneIcon, InformationFillDuotoneIcon,
@@ -14,8 +12,9 @@ import {
NoNetworkIcon, NoNetworkIcon,
UnsyncIcon, UnsyncIcon,
} from '@blocksuite/icons'; } from '@blocksuite/icons';
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url'; import { useWorkspaceBlobObjectUrl } from '@toeverything/hooks/use-workspace-blob';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name'; import { useWorkspaceInfo } from '@toeverything/hooks/use-workspace-info';
import { useAtomValue } from 'jotai';
import { debounce } from 'lodash-es'; import { debounce } from 'lodash-es';
import { import {
forwardRef, forwardRef,
@@ -27,7 +26,6 @@ import {
} from 'react'; } from 'react';
import { useSystemOnline } from '../../../../hooks/use-system-online'; import { useSystemOnline } from '../../../../hooks/use-system-online';
import type { AllWorkspace } from '../../../../shared';
import { import {
StyledSelectorContainer, StyledSelectorContainer,
StyledSelectorWrapper, StyledSelectorWrapper,
@@ -87,21 +85,18 @@ const OfflineStatus = () => {
); );
}; };
const WorkspaceStatus = ({ const WorkspaceStatus = () => {
currentWorkspace,
}: {
currentWorkspace: AllWorkspace;
}) => {
const isOnline = useSystemOnline(); const isOnline = useSystemOnline();
const [syncEngineStatus, setSyncEngineStatus] = const [syncEngineStatus, setSyncEngineStatus] =
useState<SyncEngineStatus | null>(null); useState<SyncEngineStatus | null>(null);
const syncEngine = useCurrentSyncEngine(); const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
// debounce sync engine status
useEffect(() => { useEffect(() => {
setSyncEngineStatus(syncEngine?.status ?? null); setSyncEngineStatus(currentWorkspace.engine.sync.status);
const disposable = syncEngine?.onStatusChange.on( const disposable = currentWorkspace.engine.sync.onStatusChange.on(
debounce(status => { debounce(status => {
setSyncEngineStatus(status); setSyncEngineStatus(status);
}, 500) }, 500)
@@ -109,7 +104,7 @@ const WorkspaceStatus = ({
return () => { return () => {
disposable?.dispose(); disposable?.dispose();
}; };
}, [syncEngine]); }, [currentWorkspace]);
const content = useMemo(() => { const content = useMemo(() => {
// TODO: add i18n // TODO: add i18n
@@ -162,17 +157,18 @@ const WorkspaceStatus = ({
export const WorkspaceCard = forwardRef< export const WorkspaceCard = forwardRef<
HTMLDivElement, HTMLDivElement,
{ HTMLAttributes<HTMLDivElement>
currentWorkspace: AllWorkspace; >(({ ...props }, ref) => {
} & HTMLAttributes<HTMLDivElement> const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
>(({ currentWorkspace, ...props }, ref) => {
const [name] = useBlockSuiteWorkspaceName( const information = useWorkspaceInfo(currentWorkspace.meta);
currentWorkspace.blockSuiteWorkspace
const avatarUrl = useWorkspaceBlobObjectUrl(
currentWorkspace.meta,
information?.avatar
); );
const [workspaceAvatar] = useBlockSuiteWorkspaceAvatarUrl( const name = information?.name ?? UNTITLED_WORKSPACE_NAME;
currentWorkspace.blockSuiteWorkspace
);
return ( return (
<StyledSelectorContainer <StyledSelectorContainer
@@ -186,7 +182,7 @@ export const WorkspaceCard = forwardRef<
<Avatar <Avatar
data-testid="workspace-avatar" data-testid="workspace-avatar"
size={40} size={40}
url={workspaceAvatar} url={avatarUrl}
name={name} name={name}
colorfulFallback colorfulFallback
/> />
@@ -194,7 +190,7 @@ export const WorkspaceCard = forwardRef<
<StyledWorkspaceName data-testid="workspace-name"> <StyledWorkspaceName data-testid="workspace-name">
{name} {name}
</StyledWorkspaceName> </StyledWorkspaceName>
<WorkspaceStatus currentWorkspace={currentWorkspace} /> <WorkspaceStatus />
</StyledSelectorWrapper> </StyledSelectorWrapper>
</StyledSelectorContainer> </StyledSelectorContainer>
); );

View File

@@ -22,6 +22,7 @@ import { Menu } from '@affine/component/ui/menu';
import { collectionsCRUDAtom } from '@affine/core/atoms/collections'; import { collectionsCRUDAtom } from '@affine/core/atoms/collections';
import { WorkspaceSubPath } from '@affine/env/workspace'; import { WorkspaceSubPath } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { Workspace } from '@affine/workspace';
import { FolderIcon, SettingsIcon } from '@blocksuite/icons'; import { FolderIcon, SettingsIcon } from '@blocksuite/icons';
import { type Page } from '@blocksuite/store'; import { type Page } from '@blocksuite/store';
import { useDroppable } from '@dnd-kit/core'; import { useDroppable } from '@dnd-kit/core';
@@ -40,7 +41,6 @@ import { getDropItemId } from '../../hooks/affine/use-sidebar-drag';
import { useTrashModalHelper } from '../../hooks/affine/use-trash-modal-helper'; import { useTrashModalHelper } from '../../hooks/affine/use-trash-modal-helper';
import { useRegisterBrowserHistoryCommands } from '../../hooks/use-browser-history-commands'; import { useRegisterBrowserHistoryCommands } from '../../hooks/use-browser-history-commands';
import { useNavigateHelper } from '../../hooks/use-navigate-helper'; import { useNavigateHelper } from '../../hooks/use-navigate-helper';
import type { AllWorkspace } from '../../shared';
import { CollectionsList } from '../pure/workspace-slider-bar/collections'; import { CollectionsList } from '../pure/workspace-slider-bar/collections';
import { AddCollectionButton } from '../pure/workspace-slider-bar/collections/add-collection-button'; import { AddCollectionButton } from '../pure/workspace-slider-bar/collections/add-collection-button';
import { AddFavouriteButton } from '../pure/workspace-slider-bar/favorite/add-favourite-button'; import { AddFavouriteButton } from '../pure/workspace-slider-bar/favorite/add-favourite-button';
@@ -53,7 +53,7 @@ export type RootAppSidebarProps = {
isPublicWorkspace: boolean; isPublicWorkspace: boolean;
onOpenQuickSearchModal: () => void; onOpenQuickSearchModal: () => void;
onOpenSettingModal: () => void; onOpenSettingModal: () => void;
currentWorkspace: AllWorkspace; currentWorkspace: Workspace;
openPage: (pageId: string) => void; openPage: (pageId: string) => void;
createPage: () => Page; createPage: () => Page;
currentPath: string; currentPath: string;
@@ -185,9 +185,9 @@ export const RootAppSidebar = ({
}); });
const handleCreateCollection = useCallback(() => { const handleCreateCollection = useCallback(() => {
open('') open('')
.then(async name => { .then(name => {
const id = nanoid(); const id = nanoid();
await setting.createCollection(createEmptyCollection(id, { name })); setting.createCollection(createEmptyCollection(id, { name }));
navigateHelper.jumpToCollection(blockSuiteWorkspace.id, id); navigateHelper.jumpToCollection(blockSuiteWorkspace.id, id);
}) })
.catch(err => { .catch(err => {
@@ -230,7 +230,6 @@ export const RootAppSidebar = ({
}} }}
> >
<WorkspaceCard <WorkspaceCard
currentWorkspace={currentWorkspace}
onClick={useCallback(() => { onClick={useCallback(() => {
setOpenUserWorkspaceList(true); setOpenUserWorkspaceList(true);
}, [setOpenUserWorkspaceList])} }, [setOpenUserWorkspaceList])}

View File

@@ -1,17 +1,18 @@
import { BrowserWarning } from '@affine/component/affine-banner'; import { BrowserWarning } from '@affine/component/affine-banner';
import { LocalDemoTips } from '@affine/component/affine-banner'; import { LocalDemoTips } from '@affine/component/affine-banner';
import { import { WorkspaceFlavour } from '@affine/env/workspace';
type AffineOfficialWorkspace,
WorkspaceFlavour,
} from '@affine/env/workspace';
import { Trans } from '@affine/i18n'; import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useSetAtom } from 'jotai'; import type { Workspace } from '@affine/workspace';
import { workspaceManagerAtom } from '@affine/workspace/atom';
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
import { useAtomValue, useSetAtom } from 'jotai';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { authAtom } from '../atoms'; import { authAtom } from '../atoms';
import { useCurrentLoginStatus } from '../hooks/affine/use-current-login-status'; import { useCurrentLoginStatus } from '../hooks/affine/use-current-login-status';
import { useOnTransformWorkspace } from '../hooks/root/use-on-transform-workspace'; import { useNavigateHelper } from '../hooks/use-navigate-helper';
import { WorkspaceSubPath } from '../shared';
import { EnableAffineCloudModal } from './affine/enable-affine-cloud-modal'; import { EnableAffineCloudModal } from './affine/enable-affine-cloud-modal';
const minimumChromeVersion = 106; const minimumChromeVersion = 106;
@@ -57,9 +58,11 @@ const OSWarningMessage = () => {
}; };
export const TopTip = ({ export const TopTip = ({
pageId,
workspace, workspace,
}: { }: {
workspace: AffineOfficialWorkspace; pageId?: string;
workspace: Workspace;
}) => { }) => {
const loginStatus = useCurrentLoginStatus(); const loginStatus = useCurrentLoginStatus();
const isLoggedIn = loginStatus === 'authenticated'; const isLoggedIn = loginStatus === 'authenticated';
@@ -73,18 +76,18 @@ export const TopTip = ({
setAuthModal({ openModal: true, state: 'signIn' }); setAuthModal({ openModal: true, state: 'signIn' });
}, [setAuthModal]); }, [setAuthModal]);
const onTransformWorkspace = useOnTransformWorkspace(); const { openPage } = useNavigateHelper();
const handleConfirm = useCallback(() => { const workspaceManager = useAtomValue(workspaceManagerAtom);
const handleConfirm = useAsyncCallback(async () => {
if (workspace.flavour !== WorkspaceFlavour.LOCAL) { if (workspace.flavour !== WorkspaceFlavour.LOCAL) {
return; return;
} }
onTransformWorkspace( // TODO: we need to transform local to cloud
WorkspaceFlavour.LOCAL, const { id: newId } =
WorkspaceFlavour.AFFINE_CLOUD, await workspaceManager.transformLocalToCloud(workspace);
workspace openPage(newId, pageId || WorkspaceSubPath.ALL);
);
setOpen(false); setOpen(false);
}, [onTransformWorkspace, workspace]); }, [openPage, pageId, workspace, workspaceManager]);
if ( if (
showLocalDemoTips && showLocalDemoTips &&

View File

@@ -1,122 +0,0 @@
import { WorkspaceFlavour } from '@affine/env/workspace';
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 { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
export type UpgradeState = 'pending' | 'upgrading' | 'done' | 'error';
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 [error, setError] = useState<Error | null>(null);
const [newWorkspaceId, setNewWorkspaceId] = useState<string | null>(null);
const [workspace] = useCurrentWorkspace();
const syncEngine = useCurrentSyncEngine();
const rootStore = getCurrentStore();
const upgradeWorkspace = useAsyncCallback(async () => {
setState('upgrading');
setError(null);
try {
// Migration need to wait for root doc and all subdocs loaded.
await syncEngine?.waitForSynced();
// Clone a new doc to prevent change events.
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();
setState('done');
} catch (e: any) {
console.error(e);
setError(e);
setState('error');
}
}, [rootStore, workspace, syncEngine, migration]);
return [state, error, upgradeWorkspace, newWorkspaceId] as const;
}

View File

@@ -1,90 +1,84 @@
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 { Button } from '@affine/component/ui/button'; import { Button } from '@affine/component/ui/button';
import { WorkspaceSubPath } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { MigrationPoint } from '@toeverything/infra/blocksuite'; import {
import { useCallback, useMemo } from 'react'; waitForCurrentWorkspaceAtom,
workspaceManagerAtom,
} from '@affine/workspace/atom';
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
import { useWorkspaceStatus } from '@toeverything/hooks/use-workspace-status';
import { useAtomValue } from 'jotai';
import { useState } from 'react';
import { pathGenerator } from '../../shared'; import { useNavigateHelper } from '../../hooks/use-navigate-helper';
import * as styles from './upgrade.css'; import * as styles from './upgrade.css';
import { type UpgradeState, useUpgradeWorkspace } from './upgrade-hooks';
import { ArrowCircleIcon, HeartBreakIcon } from './upgrade-icon'; import { ArrowCircleIcon, HeartBreakIcon } from './upgrade-icon';
const UPGRADE_TIPS_KEYS = {
pending: 'com.affine.upgrade.tips.normal',
upgrading: 'com.affine.upgrade.tips.normal',
done: 'com.affine.upgrade.tips.done',
error: 'com.affine.upgrade.tips.error',
} as const;
const BUTTON_TEXT_KEYS = {
pending: 'com.affine.upgrade.button-text.pending',
upgrading: 'com.affine.upgrade.button-text.upgrading',
done: 'com.affine.upgrade.button-text.done',
error: 'com.affine.upgrade.button-text.error',
} as const;
function UpgradeIcon({ upgradeState }: { upgradeState: UpgradeState }) {
if (upgradeState === 'error') {
return <HeartBreakIcon />;
}
return (
<ArrowCircleIcon
className={upgradeState === 'upgrading' ? styles.loadingIcon : undefined}
/>
);
}
interface WorkspaceUpgradeProps {
migration: MigrationPoint;
}
/** /**
* TODO: Help info is not implemented yet. * TODO: Help info is not implemented yet.
*/ */
export const WorkspaceUpgrade = function WorkspaceUpgrade( export const WorkspaceUpgrade = function WorkspaceUpgrade() {
props: WorkspaceUpgradeProps const [error, setError] = useState<string | null>(null);
) { const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const [upgradeState, error, upgradeWorkspace, newWorkspaceId] = const workspaceManager = useAtomValue(workspaceManagerAtom);
useUpgradeWorkspace(props.migration); const upgradeStatus = useWorkspaceStatus(currentWorkspace, s => s.upgrade);
const { openPage } = useNavigateHelper();
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const refreshPage = useCallback(() => { const onButtonClick = useAsyncCallback(async () => {
window.location.reload(); if (upgradeStatus?.upgrading) {
}, []); return;
}
const onButtonClick = useMemo(() => { try {
if (upgradeState === 'done') { const newWorkspaceId =
await currentWorkspace.upgrade.upgrade(workspaceManager);
if (newWorkspaceId) { if (newWorkspaceId) {
return () => { openPage(newWorkspaceId, WorkspaceSubPath.ALL);
window.location.replace(pathGenerator.all(newWorkspaceId)); } else {
}; // blocksuite may enter an incorrect state, reload to reset it.
location.reload();
} }
} catch (error) {
return refreshPage; setError(error instanceof Error ? error.message : '' + error);
} }
}, [
if (upgradeState === 'pending') { upgradeStatus?.upgrading,
return upgradeWorkspace; currentWorkspace.upgrade,
} workspaceManager,
openPage,
return undefined; ]);
}, [upgradeState, upgradeWorkspace, refreshPage, newWorkspaceId]);
return ( return (
<div className={styles.layout}> <div className={styles.layout}>
<div className={styles.upgradeBox}> <div className={styles.upgradeBox}>
<AffineShapeIcon width={180} height={180} /> <AffineShapeIcon width={180} height={180} />
<p className={styles.upgradeTips}> <p className={styles.upgradeTips}>
{error ? error.message : t[UPGRADE_TIPS_KEYS[upgradeState]]()} {error ? error : t['com.affine.upgrade.tips.normal']()}
</p> </p>
<Button <Button
data-testid="upgrade-workspace-button" data-testid="upgrade-workspace-button"
onClick={onButtonClick} onClick={onButtonClick}
size="extraLarge" size="extraLarge"
icon={<UpgradeIcon upgradeState={upgradeState} />} icon={
type={upgradeState === 'error' ? 'error' : 'default'} error ? (
<HeartBreakIcon />
) : (
<ArrowCircleIcon
className={
upgradeStatus?.upgrading ? styles.loadingIcon : undefined
}
/>
)
}
type={error ? 'error' : 'default'}
> >
{t[BUTTON_TEXT_KEYS[upgradeState]]()} {error
? t['com.affine.upgrade.button-text.error']()
: upgradeStatus?.upgrading
? t['com.affine.upgrade.button-text.upgrading']()
: t['com.affine.upgrade.button-text.pending']()}
</Button> </Button>
</div> </div>
</div> </div>

View File

@@ -4,16 +4,17 @@ import {
FavoriteTag, FavoriteTag,
} from '@affine/component/page-list'; } from '@affine/component/page-list';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { waitForCurrentWorkspaceAtom } from '@affine/workspace/atom';
import type { PageMeta } from '@blocksuite/store'; import type { PageMeta } from '@blocksuite/store';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta'; import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useAtomValue } from 'jotai';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { usePageHelper } from '../../components/blocksuite/block-suite-page-list/utils'; import { usePageHelper } from '../../components/blocksuite/block-suite-page-list/utils';
import { useCurrentWorkspace } from '../current/use-current-workspace';
import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper'; import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper';
export const useAllPageListConfig = () => { export const useAllPageListConfig = () => {
const [currentWorkspace] = useCurrentWorkspace(); const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const workspace = currentWorkspace.blockSuiteWorkspace; const workspace = currentWorkspace.blockSuiteWorkspace;
const pageMetas = useBlockSuitePageMeta(workspace); const pageMetas = useBlockSuitePageMeta(workspace);
const { isPreferredEdgeless } = usePageHelper(workspace); const { isPreferredEdgeless } = usePageHelper(workspace);

View File

@@ -1,13 +1,23 @@
import { WorkspaceFlavour } from '@affine/env/workspace';
import { getIsOwnerQuery } from '@affine/graphql'; import { getIsOwnerQuery } from '@affine/graphql';
import { useQueryImmutable } from '@affine/workspace/affine/gql'; import { useQueryImmutable } from '@affine/workspace/affine/gql';
import type { WorkspaceMetadata } from '@affine/workspace/metadata';
export function useIsWorkspaceOwner(workspaceId: string) { export function useIsWorkspaceOwner(workspaceMetadata: WorkspaceMetadata) {
const { data } = useQueryImmutable({ const { data } = useQueryImmutable(
query: getIsOwnerQuery, workspaceMetadata.flavour !== WorkspaceFlavour.LOCAL
variables: { ? {
workspaceId, query: getIsOwnerQuery,
}, variables: {
}); workspaceId: workspaceMetadata.id,
},
}
: undefined
);
if (workspaceMetadata.flavour === WorkspaceFlavour.LOCAL) {
return true;
}
return data.isOwner; return data.isOwner;
} }

View File

@@ -1,26 +0,0 @@
import { leaveWorkspaceMutation } from '@affine/graphql';
import { useMutation } from '@affine/workspace/affine/gql';
import { useCallback } from 'react';
import { useAppHelper } from '../use-workspaces';
export function useLeaveWorkspace() {
const { deleteWorkspaceMeta } = useAppHelper();
const { trigger: leaveWorkspace } = useMutation({
mutation: leaveWorkspaceMutation,
});
return useCallback(
async (workspaceId: string, workspaceName: string) => {
deleteWorkspaceMeta(workspaceId);
await leaveWorkspace({
workspaceId,
workspaceName,
sendLeaveMail: true,
});
},
[deleteWorkspaceMeta, leaveWorkspace]
);
}

View File

@@ -1,6 +1,7 @@
import { toast } from '@affine/component'; import { toast } from '@affine/component';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { waitForCurrentWorkspaceAtom } from '@affine/workspace/atom';
import { assertExists } from '@blocksuite/global/utils'; import { assertExists } from '@blocksuite/global/utils';
import { EdgelessIcon, HistoryIcon, PageIcon } from '@blocksuite/icons'; import { EdgelessIcon, HistoryIcon, PageIcon } from '@blocksuite/icons';
import { usePageMetaHelper } from '@toeverything/hooks/use-block-suite-page-meta'; import { usePageMetaHelper } from '@toeverything/hooks/use-block-suite-page-meta';
@@ -8,11 +9,10 @@ import {
PreconditionStrategy, PreconditionStrategy,
registerAffineCommand, registerAffineCommand,
} from '@toeverything/infra/command'; } from '@toeverything/infra/command';
import { useSetAtom } from 'jotai'; import { useAtomValue, useSetAtom } from 'jotai';
import { useCallback, useEffect } from 'react'; import { useCallback, useEffect } from 'react';
import { pageHistoryModalAtom } from '../../atoms/page-history'; import { pageHistoryModalAtom } from '../../atoms/page-history';
import { useCurrentWorkspace } from '../current/use-current-workspace';
import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper'; import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper';
import { useExportPage } from './use-export-page'; import { useExportPage } from './use-export-page';
import { useTrashModalHelper } from './use-trash-modal-helper'; import { useTrashModalHelper } from './use-trash-modal-helper';
@@ -22,7 +22,7 @@ export function useRegisterBlocksuiteEditorCommands(
mode: 'page' | 'edgeless' mode: 'page' | 'edgeless'
) { ) {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const [workspace] = useCurrentWorkspace(); const workspace = useAtomValue(waitForCurrentWorkspaceAtom);
const blockSuiteWorkspace = workspace.blockSuiteWorkspace; const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
const { getPageMeta } = usePageMetaHelper(blockSuiteWorkspace); const { getPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
const currentPage = blockSuiteWorkspace.getPage(pageId); const currentPage = blockSuiteWorkspace.getPage(pageId);

View File

@@ -1,11 +1,12 @@
import { toast } from '@affine/component'; import { toast } from '@affine/component';
import type { DraggableTitleCellData } from '@affine/component/page-list'; import type { DraggableTitleCellData } from '@affine/component/page-list';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { waitForCurrentWorkspaceAtom } from '@affine/workspace/atom';
import type { DragEndEvent, UniqueIdentifier } from '@dnd-kit/core'; import type { DragEndEvent, UniqueIdentifier } from '@dnd-kit/core';
import { usePageMetaHelper } from '@toeverything/hooks/use-block-suite-page-meta'; import { usePageMetaHelper } from '@toeverything/hooks/use-block-suite-page-meta';
import { useAtomValue } from 'jotai';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useCurrentWorkspace } from '../current/use-current-workspace';
import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper'; import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper';
import { useTrashModalHelper } from './use-trash-modal-helper'; import { useTrashModalHelper } from './use-trash-modal-helper';
@@ -68,7 +69,7 @@ export function getDragItemId(
export const useSidebarDrag = () => { export const useSidebarDrag = () => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const [currentWorkspace] = useCurrentWorkspace(); const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const workspace = currentWorkspace.blockSuiteWorkspace; const workspace = currentWorkspace.blockSuiteWorkspace;
const { setTrashModal } = useTrashModalHelper(workspace); const { setTrashModal } = useTrashModalHelper(workspace);
const { addToFavorite, removeFromFavorite } = const { addToFavorite, removeFromFavorite } =

View File

@@ -1,12 +1,14 @@
import { import { waitForCurrentWorkspaceAtom } from '@affine/workspace/atom';
currentPageIdAtom, import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page';
currentWorkspaceAtom, import { currentPageIdAtom } from '@toeverything/infra/atom';
} from '@toeverything/infra/atom';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
export const useCurrentPage = () => { export const useCurrentPage = () => {
const currentPageId = useAtomValue(currentPageIdAtom); const currentPageId = useAtomValue(currentPageIdAtom);
const currentWorkspace = useAtomValue(currentWorkspaceAtom); const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
return currentPageId ? currentWorkspace.getPage(currentPageId) : null; return useBlockSuiteWorkspacePage(
currentWorkspace?.blockSuiteWorkspace,
currentPageId
);
}; };

View File

@@ -1,33 +0,0 @@
import type { SyncEngine, SyncEngineStatus } from '@affine/workspace/providers';
import { useEffect, useState } from 'react';
import { useCurrentWorkspace } from './use-current-workspace';
export function useCurrentSyncEngine(): SyncEngine | undefined {
const [workspace] = useCurrentWorkspace();
// FIXME: This is a hack to get the sync engine, we need refactor this in the future.
const syncEngine = (
workspace.blockSuiteWorkspace.providers[0] as { engine?: SyncEngine }
)?.engine;
return syncEngine;
}
export function useCurrentSyncEngineStatus(): SyncEngineStatus | undefined {
const syncEngine = useCurrentSyncEngine();
const [status, setStatus] = useState<SyncEngineStatus>();
useEffect(() => {
if (syncEngine) {
setStatus(syncEngine.status);
return syncEngine.onStatusChange.on(status => {
setStatus(status);
}).dispose;
} else {
setStatus(undefined);
}
return;
}, [syncEngine]);
return status;
}

View File

@@ -1,54 +0,0 @@
import { assertExists } from '@blocksuite/global/utils';
import {
currentPageIdAtom,
currentWorkspaceIdAtom,
} from '@toeverything/infra/atom';
import { useAtom, useSetAtom } from 'jotai';
import { useCallback, useEffect } from 'react';
import type { AllWorkspace } from '../../shared';
import { useWorkspace, useWorkspaceEffect } from '../use-workspace';
declare global {
/**
* @internal debug only
*/
// eslint-disable-next-line no-var
var currentWorkspace: AllWorkspace | undefined;
interface WindowEventMap {
'affine:workspace:change': CustomEvent<{ id: string }>;
}
}
export function useCurrentWorkspace(): [
AllWorkspace,
(id: string | null) => void,
] {
const [id, setId] = useAtom(currentWorkspaceIdAtom);
assertExists(id);
const currentWorkspace = useWorkspace(id);
// when you call current workspace, effect is always called
useWorkspaceEffect(currentWorkspace.id);
useEffect(() => {
globalThis.currentWorkspace = currentWorkspace;
globalThis.dispatchEvent(
new CustomEvent('affine:workspace:change', {
detail: { id: currentWorkspace.id },
})
);
}, [currentWorkspace]);
const setPageId = useSetAtom(currentPageIdAtom);
return [
currentWorkspace,
useCallback(
(id: string | null) => {
if (environment.isBrowser && id) {
localStorage.setItem('last_workspace_id', id);
}
setPageId(null);
setId(id);
},
[setId, setPageId]
),
];
}

View File

@@ -1,90 +0,0 @@
import { pushNotificationAtom } from '@affine/component/notification-center';
import type { WorkspaceRegistry } from '@affine/env/workspace';
import type { WorkspaceFlavour } from '@affine/env/workspace';
import { WorkspaceSubPath } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
rootWorkspacesMetadataAtom,
workspaceAdaptersAtom,
} from '@affine/workspace/atom';
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
import { currentPageIdAtom } from '@toeverything/infra/atom';
import { WorkspaceVersion } from '@toeverything/infra/blocksuite';
import { useAtomValue, useSetAtom } from 'jotai';
import { openSettingModalAtom } from '../../atoms';
import { useNavigateHelper } from '../use-navigate-helper';
export function useOnTransformWorkspace() {
const t = useAFFiNEI18N();
const setSettingModal = useSetAtom(openSettingModalAtom);
const WorkspaceAdapters = useAtomValue(workspaceAdaptersAtom);
const setMetadata = useSetAtom(rootWorkspacesMetadataAtom);
const { openPage } = useNavigateHelper();
const currentPageId = useAtomValue(currentPageIdAtom);
const pushNotification = useSetAtom(pushNotificationAtom);
return useAsyncCallback(
async <From extends WorkspaceFlavour, To extends WorkspaceFlavour>(
from: From,
to: To,
workspace: WorkspaceRegistry[From]
): Promise<void> => {
// create first, then delete, in case of failure
const newId = await WorkspaceAdapters[to].CRUD.create(
workspace.blockSuiteWorkspace
);
await WorkspaceAdapters[from].CRUD.delete(workspace.blockSuiteWorkspace);
setMetadata(workspaces => {
const idx = workspaces.findIndex(ws => ws.id === workspace.id);
workspaces.splice(idx, 1, {
id: newId,
flavour: to,
version: WorkspaceVersion.SubDoc,
});
return [...workspaces];
}, newId);
// fixme(himself65): setting modal could still open and open the non-exist workspace
setSettingModal(settings => ({
...settings,
open: false,
}));
window.dispatchEvent(
new CustomEvent('affine-workspace:transform', {
detail: {
from,
to,
oldId: workspace.id,
newId: newId,
},
})
);
openPage(newId, currentPageId ?? WorkspaceSubPath.ALL);
pushNotification({
title: t['Successfully enabled AFFiNE Cloud'](),
type: 'success',
});
},
[
WorkspaceAdapters,
setMetadata,
setSettingModal,
openPage,
currentPageId,
pushNotification,
t,
]
);
}
declare global {
// global Events
interface WindowEventMap {
'affine-workspace:transform': CustomEvent<{
from: WorkspaceFlavour;
to: WorkspaceFlavour;
oldId: string;
newId: string;
}>;
}
}

View File

@@ -1,5 +1,6 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useAtom, useStore } from 'jotai'; import { waitForCurrentWorkspaceAtom } from '@affine/workspace/atom';
import { useAtom, useAtomValue, useStore } from 'jotai';
import { useTheme } from 'next-themes'; import { useTheme } from 'next-themes';
import { useEffect } from 'react'; import { useEffect } from 'react';
@@ -14,14 +15,13 @@ import {
} from '../commands'; } from '../commands';
import { usePageHelper } from '../components/blocksuite/block-suite-page-list/utils'; import { usePageHelper } from '../components/blocksuite/block-suite-page-list/utils';
import { useLanguageHelper } from './affine/use-language-helper'; import { useLanguageHelper } from './affine/use-language-helper';
import { useCurrentWorkspace } from './current/use-current-workspace';
import { useNavigateHelper } from './use-navigate-helper'; import { useNavigateHelper } from './use-navigate-helper';
export function useRegisterWorkspaceCommands() { export function useRegisterWorkspaceCommands() {
const store = useStore(); const store = useStore();
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const theme = useTheme(); const theme = useTheme();
const [currentWorkspace] = useCurrentWorkspace(); const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const languageHelper = useLanguageHelper(); const languageHelper = useLanguageHelper();
const pageHelper = usePageHelper(currentWorkspace.blockSuiteWorkspace); const pageHelper = usePageHelper(currentWorkspace.blockSuiteWorkspace);
const navigationHelper = useNavigateHelper(); const navigationHelper = useNavigateHelper();

View File

@@ -1,58 +0,0 @@
import { DebugLogger } from '@affine/debug';
import type { BlobManager } from '@blocksuite/store';
import { useEffect, useMemo, useRef, useState } from 'react';
import type { BlockSuiteWorkspace } from '../shared';
const logger = new DebugLogger('useWorkspaceBlob');
export function useWorkspaceBlob(
blockSuiteWorkspace: BlockSuiteWorkspace
): BlobManager {
return useMemo(() => blockSuiteWorkspace.blob, [blockSuiteWorkspace.blob]);
}
export function useWorkspaceBlobImage(
key: string | null,
blockSuiteWorkspace: BlockSuiteWorkspace
) {
const blobManager = useWorkspaceBlob(blockSuiteWorkspace);
const [blob, setBlob] = useState<Blob | null>(null);
useEffect(() => {
const controller = new AbortController();
if (key === null) {
setBlob(null);
return;
}
blobManager
?.get(key)
.then(blob => {
if (controller.signal.aborted) {
return;
}
if (blob) {
setBlob(blob);
}
})
.catch(err => {
logger.error('Failed to get blob', err);
});
return () => {
controller.abort();
};
}, [blobManager, key]);
const [url, setUrl] = useState<string | null>(null);
const ref = useRef<string | null>(null);
useEffect(() => {
if (ref.current) {
URL.revokeObjectURL(ref.current);
}
if (blob) {
const url = URL.createObjectURL(blob);
setUrl(url);
ref.current = url;
}
}, [blob]);
return url;
}

View File

@@ -1,53 +0,0 @@
import {
type AffineOfficialWorkspace,
WorkspaceFlavour,
} from '@affine/env/workspace';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import type { Workspace } from '@blocksuite/store';
import { getBlockSuiteWorkspaceAtom } from '@toeverything/infra/__internal__/workspace';
import type { Atom } from 'jotai';
import { atom, useAtomValue } from 'jotai';
const workspaceWeakMap = new WeakMap<
Workspace,
Atom<Promise<AffineOfficialWorkspace>>
>();
// workspace effect is the side effect like connect to the server/indexeddb,
// this will save the workspace updates permanently.
export function useWorkspaceEffect(workspaceId: string): void {
const [, effectAtom] = getBlockSuiteWorkspaceAtom(workspaceId);
useAtomValue(effectAtom);
}
// todo(himself65): remove this hook
export function useWorkspace(workspaceId: string): AffineOfficialWorkspace {
const [workspaceAtom] = getBlockSuiteWorkspaceAtom(workspaceId);
const workspace = useAtomValue(workspaceAtom);
if (!workspaceWeakMap.has(workspace)) {
const baseAtom = atom(async get => {
const metadataList = await get(rootWorkspacesMetadataAtom);
const flavour = metadataList.find(({ id }) => id === workspaceId)
?.flavour;
if (!flavour) {
// when last workspace is removed, we may encounter this warning. it should be fine
console.warn(
'workspace not found in rootWorkspacesMetadataAtom, maybe it is removed',
workspaceId
);
}
return {
id: workspaceId,
flavour: flavour ?? WorkspaceFlavour.LOCAL,
blockSuiteWorkspace: workspace,
};
});
workspaceWeakMap.set(workspace, baseAtom);
}
return useAtomValue(
workspaceWeakMap.get(workspace) as Atom<Promise<AffineOfficialWorkspace>>
);
}

View File

@@ -1,122 +0,0 @@
import { DebugLogger } from '@affine/debug';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { saveWorkspaceToLocalStorage } from '@affine/workspace/local/crud';
import { getOrCreateWorkspace } from '@affine/workspace/manager';
import { getWorkspace } from '@toeverything/infra/__internal__/workspace';
import { getCurrentStore } from '@toeverything/infra/atom';
import {
buildShowcaseWorkspace,
WorkspaceVersion,
} from '@toeverything/infra/blocksuite';
import { useAtomValue, useSetAtom } from 'jotai';
import { nanoid } from 'nanoid';
import { useCallback } from 'react';
import { LocalAdapter } from '../adapters/local';
import { WorkspaceAdapters } from '../adapters/workspace';
import { setPageModeAtom } from '../atoms';
const logger = new DebugLogger('use-workspaces');
/**
* This hook has the permission to all workspaces. Be careful when using it.
*/
export function useAppHelper() {
const jotaiWorkspaces = useAtomValue(rootWorkspacesMetadataAtom);
const set = useSetAtom(rootWorkspacesMetadataAtom);
return {
addLocalWorkspace: useCallback(
async (workspaceId: string): Promise<string> => {
getOrCreateWorkspace(workspaceId, WorkspaceFlavour.LOCAL);
saveWorkspaceToLocalStorage(workspaceId);
set(workspaces => [
...workspaces,
{
id: workspaceId,
flavour: WorkspaceFlavour.LOCAL,
version: WorkspaceVersion.DatabaseV3,
},
]);
logger.debug('imported local workspace', workspaceId);
return workspaceId;
},
[set]
),
addCloudWorkspace: useCallback(
(workspaceId: string) => {
getOrCreateWorkspace(workspaceId, WorkspaceFlavour.AFFINE_CLOUD);
set(workspaces => [
...workspaces,
{
id: workspaceId,
flavour: WorkspaceFlavour.AFFINE_CLOUD,
version: WorkspaceVersion.DatabaseV3,
},
]);
logger.debug('imported cloud workspace', workspaceId);
},
[set]
),
createLocalWorkspace: useCallback(
async (name: string): Promise<string> => {
const blockSuiteWorkspace = getOrCreateWorkspace(
nanoid(),
WorkspaceFlavour.LOCAL
);
blockSuiteWorkspace.meta.setName(name);
const id = await LocalAdapter.CRUD.create(blockSuiteWorkspace);
{
// this is hack, because CRUD doesn't return the workspace
const blockSuiteWorkspace = getOrCreateWorkspace(
id,
WorkspaceFlavour.LOCAL
);
await buildShowcaseWorkspace(blockSuiteWorkspace, {
store: getCurrentStore(),
atoms: {
pageMode: setPageModeAtom,
},
});
}
set(workspaces => [
...workspaces,
{
id,
flavour: WorkspaceFlavour.LOCAL,
version: WorkspaceVersion.DatabaseV3,
},
]);
logger.debug('created local workspace', id);
return id;
},
[set]
),
deleteWorkspace: useCallback(
async (workspaceId: string) => {
const targetJotaiWorkspace = jotaiWorkspaces.find(
ws => ws.id === workspaceId
);
if (!targetJotaiWorkspace) {
throw new Error('page cannot be found');
}
const targetWorkspace = getWorkspace(targetJotaiWorkspace.id);
// delete workspace from plugin
await WorkspaceAdapters[targetJotaiWorkspace.flavour].CRUD.delete(
targetWorkspace
);
// delete workspace from jotai storage
set(workspaces => workspaces.filter(ws => ws.id !== workspaceId));
},
[jotaiWorkspaces, set]
),
deleteWorkspaceMeta: useCallback(
(workspaceId: string) => {
set(workspaces => workspaces.filter(ws => ws.id !== workspaceId));
},
[set]
),
};
}

View File

@@ -2,23 +2,23 @@ import './polyfill/ses-lockdown';
import './polyfill/intl-segmenter'; import './polyfill/intl-segmenter';
import './polyfill/request-idle-callback'; import './polyfill/request-idle-callback';
import { WorkspaceFallback } from '@affine/component/workspace';
import { assertExists } from '@blocksuite/global/utils'; import { assertExists } from '@blocksuite/global/utils';
import { getCurrentStore } from '@toeverything/infra/atom'; import { getCurrentStore } from '@toeverything/infra/atom';
import { StrictMode, Suspense } from 'react'; import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { App } from './app';
import { bootstrapPluginSystem } from './bootstrap/register-plugins'; import { bootstrapPluginSystem } from './bootstrap/register-plugins';
import { setup } from './bootstrap/setup';
import { performanceLogger } from './shared'; import { performanceLogger } from './shared';
const performanceMainLogger = performanceLogger.namespace('main'); const performanceMainLogger = performanceLogger.namespace('main');
async function main() { function main() {
performanceMainLogger.info('start'); performanceMainLogger.info('start');
const { setup } = await import('./bootstrap/setup');
const rootStore = getCurrentStore(); const rootStore = getCurrentStore();
performanceMainLogger.info('setup start'); performanceMainLogger.info('setup start');
await setup(rootStore); setup();
performanceMainLogger.info('setup done'); performanceMainLogger.info('setup done');
bootstrapPluginSystem(rootStore).catch(err => { bootstrapPluginSystem(rootStore).catch(err => {
@@ -26,20 +26,19 @@ async function main() {
}); });
performanceMainLogger.info('import app'); performanceMainLogger.info('import app');
const { App } = await import('./app');
const root = document.getElementById('app'); const root = document.getElementById('app');
assertExists(root); assertExists(root);
performanceMainLogger.info('render app'); performanceMainLogger.info('render app');
createRoot(root).render( createRoot(root).render(
<StrictMode> <StrictMode>
<Suspense fallback={<WorkspaceFallback key="AppLoading" />}> <App />
<App />
</Suspense>
</StrictMode> </StrictMode>
); );
} }
main().catch(err => { try {
main();
} catch (err) {
console.error('Failed to bootstrap app', err); console.error('Failed to bootstrap app', err);
}); }

View File

@@ -7,8 +7,7 @@ import {
PageListDragOverlay, PageListDragOverlay,
} from '@affine/component/page-list'; } from '@affine/component/page-list';
import { MainContainer, WorkspaceFallback } from '@affine/component/workspace'; import { MainContainer, WorkspaceFallback } from '@affine/component/workspace';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom'; import { waitForCurrentWorkspaceAtom } from '@affine/workspace/atom';
import { getBlobEngine } from '@affine/workspace/manager';
import { assertExists } from '@blocksuite/global/utils'; import { assertExists } from '@blocksuite/global/utils';
import { import {
DndContext, DndContext,
@@ -20,8 +19,7 @@ import {
useSensors, useSensors,
} 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 { useWorkspaceStatus } from '@toeverything/hooks/use-workspace-status';
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';
@@ -37,7 +35,6 @@ import { RootAppSidebar } from '../components/root-app-sidebar';
import { WorkspaceUpgrade } from '../components/workspace-upgrade'; import { WorkspaceUpgrade } from '../components/workspace-upgrade';
import { useAppSettingHelper } from '../hooks/affine/use-app-setting-helper'; import { useAppSettingHelper } from '../hooks/affine/use-app-setting-helper';
import { useSidebarDrag } from '../hooks/affine/use-sidebar-drag'; import { useSidebarDrag } from '../hooks/affine/use-sidebar-drag';
import { useCurrentWorkspace } from '../hooks/current/use-current-workspace';
import { useNavigateHelper } from '../hooks/use-navigate-helper'; import { useNavigateHelper } from '../hooks/use-navigate-helper';
import { useRegisterWorkspaceCommands } from '../hooks/use-register-workspace-commands'; import { useRegisterWorkspaceCommands } from '../hooks/use-register-workspace-commands';
import { import {
@@ -57,11 +54,11 @@ export const QuickSearch = () => {
openQuickSearchModalAtom openQuickSearchModalAtom
); );
const [currentWorkspace] = useCurrentWorkspace(); const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
const { pageId } = useParams(); const { pageId } = useParams();
const blockSuiteWorkspace = currentWorkspace?.blockSuiteWorkspace; const blockSuiteWorkspace = currentWorkspace.blockSuiteWorkspace;
const pageMeta = useBlockSuitePageMeta( const pageMeta = useBlockSuitePageMeta(
currentWorkspace?.blockSuiteWorkspace currentWorkspace.blockSuiteWorkspace
).find(meta => meta.id === pageId); ).find(meta => meta.id === pageId);
if (!blockSuiteWorkspace) { if (!blockSuiteWorkspace) {
@@ -77,89 +74,25 @@ export const QuickSearch = () => {
); );
}; };
export const CurrentWorkspaceContext = ({ export const WorkspaceLayout = function WorkspaceLayout({
children, children,
}: PropsWithChildren): ReactNode => { }: PropsWithChildren) {
const workspaceId = useAtomValue(currentWorkspaceIdAtom);
const metadata = useAtomValue(rootWorkspacesMetadataAtom);
const exist = metadata.find(m => m.id === workspaceId);
if (metadata.length === 0) {
return <WorkspaceFallback key="no-workspace" />;
}
if (!workspaceId) {
return <WorkspaceFallback key="finding-workspace-id" />;
}
if (!exist) {
return <WorkspaceFallback key="workspace-not-found" />;
}
return children;
};
type WorkspaceLayoutProps = {
migration?: MigrationPoint;
};
const useSyncWorkspaceBlob = () => {
// temporary solution for sync blob
const [currentWorkspace] = useCurrentWorkspace();
useEffect(() => {
const blobEngine = getBlobEngine(currentWorkspace.blockSuiteWorkspace);
let stopped = false;
function sync() {
if (stopped) {
return;
}
blobEngine
?.sync()
.catch(error => {
console.error('sync blob error', error);
})
.finally(() => {
// sync every 1 minute
setTimeout(sync, 60000);
});
}
// after currentWorkspace changed, wait 1 second to start sync
setTimeout(sync, 1000);
return () => {
stopped = true;
};
}, [currentWorkspace]);
};
export const WorkspaceLayout = function WorkspacesSuspense({
children,
migration,
}: PropsWithChildren<WorkspaceLayoutProps>) {
useSyncWorkspaceBlob();
return ( return (
<AdapterProviderWrapper> <AdapterProviderWrapper>
<CurrentWorkspaceContext> {/* load all workspaces is costly, do not block the whole UI */}
{/* load all workspaces is costly, do not block the whole UI */} <Suspense>
<Suspense> <AllWorkspaceModals />
<AllWorkspaceModals /> <CurrentWorkspaceModals />
<CurrentWorkspaceModals /> </Suspense>
</Suspense> <Suspense fallback={<WorkspaceFallback />}>
<Suspense fallback={<WorkspaceFallback />}> <WorkspaceLayoutInner>{children}</WorkspaceLayoutInner>
<WorkspaceLayoutInner migration={migration}> </Suspense>
{children}
</WorkspaceLayoutInner>
</Suspense>
</CurrentWorkspaceContext>
</AdapterProviderWrapper> </AdapterProviderWrapper>
); );
}; };
export const WorkspaceLayoutInner = ({ export const WorkspaceLayoutInner = ({ children }: PropsWithChildren) => {
children, const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
migration,
}: PropsWithChildren<WorkspaceLayoutProps>) => {
const [currentWorkspace] = useCurrentWorkspace();
const { openPage } = useNavigateHelper(); const { openPage } = useNavigateHelper();
const pageHelper = usePageHelper(currentWorkspace.blockSuiteWorkspace); const pageHelper = usePageHelper(currentWorkspace.blockSuiteWorkspace);
@@ -200,7 +133,6 @@ export const WorkspaceLayoutInner = ({
const handleOpenSettingModal = useCallback(() => { const handleOpenSettingModal = useCallback(() => {
setOpenSettingModalAtom({ setOpenSettingModalAtom({
activeTab: 'appearance', activeTab: 'appearance',
workspaceId: null,
open: true, open: true,
}); });
}, [setOpenSettingModalAtom]); }, [setOpenSettingModalAtom]);
@@ -224,6 +156,8 @@ export const WorkspaceLayoutInner = ({
// todo: refactor this that the root layout do not need to check route state // todo: refactor this that the root layout do not need to check route state
const isInPageDetail = !!pageId; const isInPageDetail = !!pageId;
const upgradeStatus = useWorkspaceStatus(currentWorkspace, s => s.upgrade);
return ( return (
<> <>
{/* This DndContext is used for drag page from all-pages list into a folder in sidebar */} {/* This DndContext is used for drag page from all-pages list into a folder in sidebar */}
@@ -256,8 +190,8 @@ export const WorkspaceLayoutInner = ({
padding={appSettings.clientBorder} padding={appSettings.clientBorder}
> >
<Suspense> <Suspense>
{migration ? ( {upgradeStatus?.needUpgrade || upgradeStatus?.upgrading ? (
<WorkspaceUpgrade migration={migration} /> <WorkspaceUpgrade />
) : ( ) : (
children children
)} )}

View File

@@ -9,7 +9,7 @@ import { SignOutModal } from '../components/affine/sign-out-modal';
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper'; import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
import { signOutCloud } from '../utils/cloud-utils'; import { signOutCloud } from '../utils/cloud-utils';
export const Component = (): ReactElement => { export const PageNotFound = (): ReactElement => {
const { data: session } = useSession(); const { data: session } = useSession();
const { jumpToIndex } = useNavigateHelper(); const { jumpToIndex } = useNavigateHelper();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@@ -52,3 +52,5 @@ export const Component = (): ReactElement => {
</> </>
); );
}; };
export const Component = PageNotFound;

View File

@@ -1,13 +1,12 @@
import { Menu } from '@affine/component/ui/menu'; import { Menu } from '@affine/component/ui/menu';
import { DebugLogger } from '@affine/debug'; import { workspaceListAtom } from '@affine/workspace/atom';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom'; import { useAtomValue } from 'jotai';
import { getWorkspace } from '@toeverything/infra/__internal__/workspace'; import { lazy, useEffect } from 'react';
import { getCurrentStore } from '@toeverything/infra/atom';
import { lazy } from 'react';
import type { LoaderFunction } from 'react-router-dom';
import { redirect } from 'react-router-dom';
import { createFirstAppData } from '../bootstrap/first-app-data';
import { UserWithWorkspaceList } from '../components/pure/workspace-slider-bar/user-with-workspace-list'; import { UserWithWorkspaceList } from '../components/pure/workspace-slider-bar/user-with-workspace-list';
import { useNavigateHelper } from '../hooks/use-navigate-helper';
import { WorkspaceSubPath } from '../shared';
const AllWorkspaceModals = lazy(() => const AllWorkspaceModals = lazy(() =>
import('../providers/modal-provider').then(({ AllWorkspaceModals }) => ({ import('../providers/modal-provider').then(({ AllWorkspaceModals }) => ({
@@ -15,44 +14,27 @@ const AllWorkspaceModals = lazy(() =>
})) }))
); );
const logger = new DebugLogger('index-page');
export const loader: LoaderFunction = async () => {
const rootStore = getCurrentStore();
const { createFirstAppData } = await import('../bootstrap/setup');
createFirstAppData(rootStore);
const meta = await rootStore.get(rootWorkspacesMetadataAtom);
const lastId = localStorage.getItem('last_workspace_id');
const lastPageId = localStorage.getItem('last_page_id');
const target = (lastId && meta.find(({ id }) => id === lastId)) || meta.at(0);
if (target) {
const targetWorkspace = getWorkspace(target.id);
const nonTrashPages = targetWorkspace.meta.pageMetas.filter(
({ trash }) => !trash
);
const helloWorldPage = nonTrashPages.find(({ jumpOnce }) => jumpOnce)?.id;
const pageId =
nonTrashPages.find(({ id }) => id === lastPageId)?.id ??
nonTrashPages.at(0)?.id;
if (helloWorldPage) {
logger.debug(
'Found target workspace. Jump to hello world page',
helloWorldPage
);
return redirect(`/workspace/${targetWorkspace.id}/${helloWorldPage}`);
} else if (pageId) {
logger.debug('Found target workspace. Jump to page', pageId);
return redirect(`/workspace/${targetWorkspace.id}/${pageId}`);
} else {
logger.debug('Found target workspace. Jump to all page');
return redirect(`/workspace/${targetWorkspace.id}/all`);
}
}
return null;
};
export const Component = () => { export const Component = () => {
const list = useAtomValue(workspaceListAtom);
const { openPage } = useNavigateHelper();
useEffect(() => {
if (list.length === 0) {
return;
}
// open last workspace
const lastId = localStorage.getItem('last_workspace_id');
const openWorkspace = list.find(w => w.id === lastId) ?? list[0];
openPage(openWorkspace.id, WorkspaceSubPath.ALL);
}, [list, openPage]);
useEffect(() => {
createFirstAppData().catch(err => {
console.error('Failed to create first app data', err);
});
}, []);
// TODO: We need a no workspace page // TODO: We need a no workspace page
return ( return (
<> <>

View File

@@ -14,7 +14,6 @@ import { authAtom } from '../atoms';
import { setOnceSignedInEventAtom } from '../atoms/event'; import { setOnceSignedInEventAtom } from '../atoms/event';
import { useCurrentLoginStatus } from '../hooks/affine/use-current-login-status'; import { useCurrentLoginStatus } from '../hooks/affine/use-current-login-status';
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper'; import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
import { useAppHelper } from '../hooks/use-workspaces';
export const loader: LoaderFunction = async args => { export const loader: LoaderFunction = async args => {
const inviteId = args.params.inviteId || ''; const inviteId = args.params.inviteId || '';
@@ -49,7 +48,6 @@ export const loader: LoaderFunction = async args => {
export const Component = () => { export const Component = () => {
const loginStatus = useCurrentLoginStatus(); const loginStatus = useCurrentLoginStatus();
const { jumpToSignIn } = useNavigateHelper(); const { jumpToSignIn } = useNavigateHelper();
const { addCloudWorkspace } = useAppHelper();
const { jumpToSubPath } = useNavigateHelper(); const { jumpToSubPath } = useNavigateHelper();
const setOnceSignedInEvent = useSetAtom(setOnceSignedInEventAtom); const setOnceSignedInEvent = useSetAtom(setOnceSignedInEventAtom);
@@ -61,13 +59,12 @@ export const Component = () => {
}; };
const openWorkspace = useCallback(() => { const openWorkspace = useCallback(() => {
addCloudWorkspace(inviteInfo.workspace.id);
jumpToSubPath( jumpToSubPath(
inviteInfo.workspace.id, inviteInfo.workspace.id,
WorkspaceSubPath.ALL, WorkspaceSubPath.ALL,
RouteLogic.REPLACE RouteLogic.REPLACE
); );
}, [addCloudWorkspace, inviteInfo.workspace.id, jumpToSubPath]); }, [inviteInfo.workspace.id, jumpToSubPath]);
useEffect(() => { useEffect(() => {
if (loginStatus === 'unauthenticated') { if (loginStatus === 'unauthenticated') {

View File

@@ -1,11 +1,13 @@
import { MainContainer } from '@affine/component/workspace'; import { MainContainer } from '@affine/component/workspace';
import { DebugLogger } from '@affine/debug'; import { DebugLogger } from '@affine/debug';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { fetchWithTraceReport } from '@affine/graphql';
import type { CloudDoc } from '@affine/workspace/affine/download'; import {
import { downloadBinaryFromCloud } from '@affine/workspace/affine/download'; createAffineCloudBlobStorage,
import { getOrCreateWorkspace } from '@affine/workspace/manager'; createStaticBlobStorage,
globalBlockSuiteSchema,
} from '@affine/workspace';
import { assertExists } from '@blocksuite/global/utils'; import { assertExists } from '@blocksuite/global/utils';
import type { Page } from '@blocksuite/store'; import { type Page, Workspace } from '@blocksuite/store';
import { noop } from 'foxact/noop'; import { noop } from 'foxact/noop';
import type { ReactElement } from 'react'; import type { ReactElement } from 'react';
import { useCallback } from 'react'; import { useCallback } from 'react';
@@ -24,6 +26,36 @@ import { PageDetailEditor } from '../../components/page-detail-editor';
import { SharePageNotFoundError } from '../../components/share-page-not-found-error'; import { SharePageNotFoundError } from '../../components/share-page-not-found-error';
import { ShareHeader } from './share-header'; import { ShareHeader } from './share-header';
type DocPublishMode = 'edgeless' | 'page';
export type CloudDoc = {
arrayBuffer: ArrayBuffer;
publishMode: DocPublishMode;
};
export async function downloadBinaryFromCloud(
rootGuid: string,
pageGuid: string
): Promise<CloudDoc | null> {
const response = await fetchWithTraceReport(
runtimeConfig.serverUrlPrefix +
`/api/workspaces/${rootGuid}/docs/${pageGuid}`,
{
priority: 'high',
}
);
if (response.ok) {
const publishMode = (response.headers.get('publish-mode') ||
'page') as DocPublishMode;
const arrayBuffer = await response.arrayBuffer();
// return both arrayBuffer and publish mode
return { arrayBuffer, publishMode };
}
return null;
}
type LoaderData = { type LoaderData = {
page: Page; page: Page;
publishMode: PageMode; publishMode: PageMode;
@@ -49,10 +81,18 @@ export const loader: LoaderFunction = async ({ params }) => {
if (!workspaceId || !pageId) { if (!workspaceId || !pageId) {
return redirect('/404'); return redirect('/404');
} }
const workspace = getOrCreateWorkspace( const workspace = new Workspace({
workspaceId, id: workspaceId,
WorkspaceFlavour.AFFINE_PUBLIC blobStorages: [
); () => ({
crud: createAffineCloudBlobStorage(workspaceId),
}),
() => ({
crud: createStaticBlobStorage(),
}),
],
schema: globalBlockSuiteSchema,
});
// download root workspace // download root workspace
{ {
const response = await downloadBinaryFromCloud(workspaceId, workspaceId); const response = await downloadBinaryFromCloud(workspaceId, workspaceId);
@@ -84,9 +124,9 @@ export const Component = (): ReactElement => {
<AppContainer> <AppContainer>
<MainContainer> <MainContainer>
<ShareHeader <ShareHeader
workspace={page.workspace}
pageId={page.id} pageId={page.id}
publishMode={publishMode} publishMode={publishMode}
blockSuiteWorkspace={page.workspace}
/> />
<PageDetailEditor <PageDetailEditor
isPublic isPublic

View File

@@ -1,30 +1,27 @@
import type { Workspace } from '@blocksuite/store'; import type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
import type { PageMode } from '../../atoms'; import type { PageMode } from '../../atoms';
import { BlockSuiteHeaderTitle } from '../../components/blocksuite/block-suite-header-title'; import { BlockSuiteHeaderTitle } from '../../components/blocksuite/block-suite-header-title';
import ShareHeaderLeftItem from '../../components/cloud/share-header-left-item'; import ShareHeaderLeftItem from '../../components/cloud/share-header-left-item';
import ShareHeaderRightItem from '../../components/cloud/share-header-right-item'; import ShareHeaderRightItem from '../../components/cloud/share-header-right-item';
import { Header } from '../../components/pure/header'; import { Header } from '../../components/pure/header';
import { useWorkspace } from '../../hooks/use-workspace';
export function ShareHeader({ export function ShareHeader({
workspace,
pageId, pageId,
publishMode, publishMode,
blockSuiteWorkspace,
}: { }: {
workspace: Workspace;
pageId: string; pageId: string;
publishMode: PageMode; publishMode: PageMode;
blockSuiteWorkspace: BlockSuiteWorkspace;
}) { }) {
const currentWorkspace = useWorkspace(workspace.id);
return ( return (
<Header <Header
isFloat={publishMode === 'edgeless'} isFloat={publishMode === 'edgeless'}
left={<ShareHeaderLeftItem />} left={<ShareHeaderLeftItem />}
center={ center={
<BlockSuiteHeaderTitle <BlockSuiteHeaderTitle
workspace={currentWorkspace} blockSuiteWorkspace={blockSuiteWorkspace}
pageId={pageId} pageId={pageId}
isPublic={true} isPublic={true}
publicMode={publishMode} publicMode={publishMode}
@@ -32,7 +29,7 @@ export function ShareHeader({
} }
right={ right={
<ShareHeaderRightItem <ShareHeaderRightItem
workspaceId={workspace.id} workspaceId={blockSuiteWorkspace.id}
pageId={pageId} pageId={pageId}
publishMode={publishMode} publishMode={publishMode}
/> />

Some files were not shown because too many files have changed in this diff Show More