feat!: affine cloud support (#3813)

Co-authored-by: Hongtao Lye <codert.sn@gmail.com>
Co-authored-by: liuyi <forehalo@gmail.com>
Co-authored-by: LongYinan <lynweklm@gmail.com>
Co-authored-by: X1a0t <405028157@qq.com>
Co-authored-by: JimmFly <yangjinfei001@gmail.com>
Co-authored-by: Peng Xiao <pengxiao@outlook.com>
Co-authored-by: xiaodong zuo <53252747+zuoxiaodong0815@users.noreply.github.com>
Co-authored-by: DarkSky <25152247+darkskygit@users.noreply.github.com>
Co-authored-by: Qi <474021214@qq.com>
Co-authored-by: danielchim <kahungchim@gmail.com>
This commit is contained in:
Alex Yang
2023-08-29 05:07:05 -05:00
committed by GitHub
parent d0145c6f38
commit 2f6c4e3696
414 changed files with 19469 additions and 7591 deletions

View File

@@ -0,0 +1,164 @@
import type {
AffineCloudWorkspace,
WorkspaceCRUD,
} from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace';
import {
createWorkspaceMutation,
deleteWorkspaceMutation,
getWorkspaceQuery,
getWorkspacesQuery,
} from '@affine/graphql';
import { fetcher } from '@affine/workspace/affine/gql';
import { getOrCreateWorkspace } from '@affine/workspace/manager';
import { createIndexeddbStorage, Workspace } from '@blocksuite/store';
import { migrateLocalBlobStorage } from '@toeverything/infra/blocksuite';
import {
createIndexedDBProvider,
DEFAULT_DB_NAME,
} from '@toeverything/y-indexeddb';
import { getSession } from 'next-auth/react';
import { proxy } from 'valtio/vanilla';
const Y = Workspace.Y;
async function deleteLocalBlobStorage(id: string) {
const storage = createIndexeddbStorage(id);
const keys = await storage.crud.list();
for (const key of keys) {
await storage.crud.delete(key);
}
}
// we don't need to persistence the state into local storage
// because if a user clicks create multiple time and nothing happened
// because of the server delay or something, he/she will wait.
// and also the user journey of creating workspace is long.
const createdWorkspaces = proxy<string[]>([]);
export const CRUD: WorkspaceCRUD<WorkspaceFlavour.AFFINE_CLOUD> = {
create: async blockSuiteWorkspace => {
if (createdWorkspaces.some(id => id === blockSuiteWorkspace.id)) {
throw new Error('workspace already created');
}
const { createWorkspace } = await fetcher({
query: createWorkspaceMutation,
variables: {
init: new File(
[Y.encodeStateAsUpdate(blockSuiteWorkspace.doc)],
'initBinary.yDoc'
),
},
});
createdWorkspaces.push(blockSuiteWorkspace.id);
const newBLockSuiteWorkspace = getOrCreateWorkspace(
createWorkspace.id,
WorkspaceFlavour.AFFINE_CLOUD
);
Y.applyUpdate(
newBLockSuiteWorkspace.doc,
Y.encodeStateAsUpdate(blockSuiteWorkspace.doc)
);
await Promise.all(
[...blockSuiteWorkspace.doc.subdocs].map(async subdoc => {
subdoc.load();
return subdoc.whenLoaded.then(() => {
newBLockSuiteWorkspace.doc.subdocs.forEach(newSubdoc => {
if (newSubdoc.guid === subdoc.guid) {
Y.applyUpdate(newSubdoc, Y.encodeStateAsUpdate(subdoc));
}
});
});
})
);
const provider = createIndexedDBProvider(
newBLockSuiteWorkspace.doc,
DEFAULT_DB_NAME
);
provider.connect();
migrateLocalBlobStorage(blockSuiteWorkspace.id, createWorkspace.id)
.then(() => deleteLocalBlobStorage(blockSuiteWorkspace.id))
.catch(e => {
console.error('error when moving blob storage:', e);
});
// todo(himself65): delete old workspace in the future
return createWorkspace.id;
},
delete: async workspace => {
await fetcher({
query: deleteWorkspaceMutation,
variables: {
id: workspace.id,
},
});
},
get: async id => {
if (!environment.isServer && !navigator.onLine) {
// no network
return null;
}
if (
!(await getSession()
.then(() => true)
.catch(() => false))
) {
return null;
}
try {
await fetcher({
query: getWorkspaceQuery,
variables: {
id,
},
});
return {
id,
flavour: WorkspaceFlavour.AFFINE_CLOUD,
blockSuiteWorkspace: getOrCreateWorkspace(
id,
WorkspaceFlavour.AFFINE_CLOUD
),
} satisfies AffineCloudWorkspace;
} catch (e) {
console.error('error when fetching cloud workspace:', e);
return null;
}
},
list: async () => {
if (!environment.isServer && !navigator.onLine) {
// no network
return [];
}
if (
!(await getSession()
.then(() => true)
.catch(() => false))
) {
return [];
}
try {
const { workspaces } = await fetcher({
query: getWorkspacesQuery,
});
const ids = workspaces.map(({ id }) => id);
return ids.map(
id =>
({
id,
flavour: WorkspaceFlavour.AFFINE_CLOUD,
blockSuiteWorkspace: getOrCreateWorkspace(
id,
WorkspaceFlavour.AFFINE_CLOUD
),
}) satisfies AffineCloudWorkspace
);
} catch (e) {
console.error('error when fetching cloud workspaces:', e);
return [];
}
},
};

View File

@@ -0,0 +1,75 @@
import { initEmptyPage } from '@affine/env/blocksuite';
import { PageNotFoundError } from '@affine/env/constant';
import type {
WorkspaceFlavour,
WorkspaceUISchema,
} from '@affine/env/workspace';
import { lazy, useCallback } from 'react';
import { useIsWorkspaceOwner } from '../../hooks/affine/use-is-workspace-owner';
import { useWorkspace } from '../../hooks/use-workspace';
import {
BlockSuitePageList,
NewWorkspaceSettingDetail,
PageDetailEditor,
Provider,
WorkspaceHeader,
} from '../shared';
const LoginCard = lazy(() =>
import('../../components/cloud/login-card').then(({ LoginCard }) => ({
default: LoginCard,
}))
);
export const UI = {
Provider,
LoginCard,
Header: WorkspaceHeader,
PageDetail: ({ currentWorkspaceId, currentPageId, onLoadEditor }) => {
const workspace = useWorkspace(currentWorkspaceId);
const page = workspace.blockSuiteWorkspace.getPage(currentPageId);
if (!page) {
throw new PageNotFoundError(workspace.blockSuiteWorkspace, currentPageId);
}
return (
<>
<PageDetailEditor
pageId={currentPageId}
onInit={useCallback(async page => initEmptyPage(page), [])}
onLoad={onLoadEditor}
workspace={workspace.blockSuiteWorkspace}
/>
</>
);
},
PageList: ({ blockSuiteWorkspace, onOpenPage, collection }) => {
return (
<BlockSuitePageList
listType="all"
collection={collection}
onOpenPage={onOpenPage}
blockSuiteWorkspace={blockSuiteWorkspace}
/>
);
},
NewSettingsDetail: ({
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

@@ -29,6 +29,7 @@ import {
BlockSuitePageList,
NewWorkspaceSettingDetail,
PageDetailEditor,
Provider,
WorkspaceHeader,
} from '../shared';
@@ -39,6 +40,7 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
flavour: WorkspaceFlavour.LOCAL,
loadPriority: LoadPriority.LOW,
Events: {
'app:access': async () => true,
'app:init': () => {
const blockSuiteWorkspace = getOrCreateWorkspace(
nanoid(),
@@ -79,9 +81,7 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
CRUD,
UI: {
Header: WorkspaceHeader,
Provider: ({ children }) => {
return <>{children}</>;
},
Provider,
PageDetail: ({ currentWorkspaceId, currentPageId, onLoadEditor }) => {
const workspace = useStaticBlockSuiteWorkspace(currentWorkspaceId);
const page = workspace.getPage(currentPageId);
@@ -111,14 +111,19 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
},
NewSettingsDetail: ({
currentWorkspaceId,
onDeleteWorkspace,
onTransformWorkspace,
onDeleteLocalWorkspace,
onDeleteCloudWorkspace,
onLeaveWorkspace,
}) => {
return (
<NewWorkspaceSettingDetail
onDeleteWorkspace={onDeleteWorkspace}
onDeleteLocalWorkspace={onDeleteLocalWorkspace}
onDeleteCloudWorkspace={onDeleteCloudWorkspace}
onLeaveWorkspace={onLeaveWorkspace}
workspaceId={currentWorkspaceId}
onTransferWorkspace={onTransformWorkspace}
isOwner={true}
/>
);
},

View File

@@ -0,0 +1,45 @@
import { initEmptyPage } from '@affine/env/blocksuite';
import { PageNotFoundError } from '@affine/env/constant';
import type { WorkspaceFlavour } from '@affine/env/workspace';
import { type WorkspaceUISchema } from '@affine/env/workspace';
import { useCallback } from 'react';
import { useWorkspace } from '../../hooks/use-workspace';
import { BlockSuitePageList, PageDetailEditor, Provider } from '../shared';
export const UI = {
Provider,
Header: () => {
return null;
},
PageDetail: ({ currentWorkspaceId, currentPageId, onLoadEditor }) => {
const workspace = useWorkspace(currentWorkspaceId);
const page = workspace.blockSuiteWorkspace.getPage(currentPageId);
if (!page) {
throw new PageNotFoundError(workspace.blockSuiteWorkspace, currentPageId);
}
return (
<>
<PageDetailEditor
pageId={currentPageId}
onInit={useCallback(async page => initEmptyPage(page), [])}
onLoad={onLoadEditor}
workspace={workspace.blockSuiteWorkspace}
/>
</>
);
},
PageList: ({ blockSuiteWorkspace, onOpenPage, collection }) => {
return (
<BlockSuitePageList
listType="all"
collection={collection}
onOpenPage={onOpenPage}
blockSuiteWorkspace={blockSuiteWorkspace}
/>
);
},
NewSettingsDetail: () => {
throw new Error('Not implemented');
},
} satisfies WorkspaceUISchema<WorkspaceFlavour.AFFINE_PUBLIC>;

View File

@@ -1,5 +1,11 @@
import { lazy } from 'react';
export const Provider = lazy(() =>
import('../components/cloud/provider').then(({ Provider }) => ({
default: Provider,
}))
);
export const NewWorkspaceSettingDetail = lazy(() =>
import('../components/affine/new-workspace-setting-detail').then(
({ WorkspaceSettingDetail }) => ({

View File

@@ -10,7 +10,10 @@ import {
WorkspaceFlavour,
} from '@affine/env/workspace';
import { CRUD as CloudCRUD } from './cloud/crud';
import { UI as CloudUI } from './cloud/ui';
import { LocalAdapter } from './local';
import { UI as PublicCloudUI } from './public-cloud/ui';
const unimplemented = () => {
throw new Error('Not implemented');
@@ -26,26 +29,24 @@ export const WorkspaceAdapters = {
releaseType: ReleaseType.UNRELEASED,
flavour: WorkspaceFlavour.AFFINE_CLOUD,
loadPriority: LoadPriority.HIGH,
Events: {} as Partial<AppEvents>,
// todo: implement this
CRUD: {
get: unimplemented,
list: bypassList,
delete: unimplemented,
create: unimplemented,
},
// todo: implement this
UI: {
Provider: unimplemented,
Header: unimplemented,
PageDetail: unimplemented,
PageList: unimplemented,
NewSettingsDetail: unimplemented,
},
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,
},
[WorkspaceFlavour.PUBLIC]: {
[WorkspaceFlavour.AFFINE_PUBLIC]: {
releaseType: ReleaseType.UNRELEASED,
flavour: WorkspaceFlavour.PUBLIC,
flavour: WorkspaceFlavour.AFFINE_PUBLIC,
loadPriority: LoadPriority.LOW,
Events: {} as Partial<AppEvents>,
// todo: implement this
@@ -55,14 +56,7 @@ export const WorkspaceAdapters = {
delete: unimplemented,
create: unimplemented,
},
// todo: implement this
UI: {
Provider: unimplemented,
Header: unimplemented,
PageDetail: unimplemented,
PageList: unimplemented,
NewSettingsDetail: unimplemented,
},
UI: PublicCloudUI,
},
} satisfies {
[Key in WorkspaceFlavour]: WorkspaceAdapter<Key>;