mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
refactor: remove legacy cloud (#2987)
This commit is contained in:
@@ -5,14 +5,10 @@ import {
|
||||
} from '@affine/component/share-menu';
|
||||
import { ShareMenu } from '@affine/component/share-menu';
|
||||
import type {
|
||||
AffineLegacyCloudWorkspace,
|
||||
AffineCloudWorkspace,
|
||||
LocalWorkspace,
|
||||
} from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import {
|
||||
PermissionType,
|
||||
WorkspaceType,
|
||||
} from '@affine/env/workspace/legacy-cloud';
|
||||
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { expect } from '@storybook/jest';
|
||||
@@ -59,13 +55,10 @@ const localWorkspace: LocalWorkspace = {
|
||||
blockSuiteWorkspace,
|
||||
};
|
||||
|
||||
const affineWorkspace: AffineLegacyCloudWorkspace = {
|
||||
const affineWorkspace: AffineCloudWorkspace = {
|
||||
id: 'test-workspace',
|
||||
flavour: WorkspaceFlavour.AFFINE,
|
||||
flavour: WorkspaceFlavour.AFFINE_CLOUD,
|
||||
blockSuiteWorkspace,
|
||||
public: false,
|
||||
type: WorkspaceType.Normal,
|
||||
permission: PermissionType.Owner,
|
||||
};
|
||||
|
||||
async function unimplemented() {
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import { Unreachable } from '@affine/env/constant';
|
||||
import type { AffineLegacyCloudWorkspace } from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { affineApis } from '@affine/workspace/affine/shared';
|
||||
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
||||
import { assertExists } from '@blocksuite/store';
|
||||
import { rootStore } from '@toeverything/plugin-infra/manager';
|
||||
|
||||
import { workspacesAtom } from '../../atoms';
|
||||
|
||||
type Query = (typeof QueryKey)[keyof typeof QueryKey];
|
||||
|
||||
export const fetcher = async (
|
||||
query:
|
||||
| Query
|
||||
| [Query, string, boolean]
|
||||
| [Query, string]
|
||||
| [Query, string, string]
|
||||
) => {
|
||||
if (Array.isArray(query)) {
|
||||
if (query[0] === QueryKey.downloadWorkspace) {
|
||||
if (typeof query[2] !== 'boolean') {
|
||||
throw new Unreachable();
|
||||
}
|
||||
return affineApis.downloadWorkspace(query[1], query[2]);
|
||||
} else if (query[0] === QueryKey.getMembers) {
|
||||
return affineApis.getWorkspaceMembers({
|
||||
id: query[1],
|
||||
});
|
||||
} else if (query[0] === QueryKey.getUserByEmail) {
|
||||
if (typeof query[2] !== 'string') {
|
||||
throw new Unreachable();
|
||||
}
|
||||
return affineApis.getUserByEmail({
|
||||
workspace_id: query[1],
|
||||
email: query[2],
|
||||
});
|
||||
} else if (query[0] === QueryKey.getImage) {
|
||||
const workspaceId = query[1];
|
||||
const key = query[2];
|
||||
if (typeof key !== 'string') {
|
||||
throw new TypeError('key must be a string');
|
||||
}
|
||||
const workspaces = await rootStore.get(workspacesAtom);
|
||||
const workspace = workspaces.find(({ id }) => id === workspaceId);
|
||||
assertExists(workspace);
|
||||
const storage = await workspace.blockSuiteWorkspace.blobs;
|
||||
if (!storage) {
|
||||
return null;
|
||||
}
|
||||
return storage.get(key);
|
||||
} else if (query[0] === QueryKey.acceptInvite) {
|
||||
const invitingCode = query[1];
|
||||
if (typeof invitingCode !== 'string') {
|
||||
throw new TypeError('invitingCode must be a string');
|
||||
}
|
||||
return affineApis.acceptInviting({
|
||||
invitingCode,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (query === QueryKey.getWorkspaces) {
|
||||
return affineApis.getWorkspaces().then(workspaces => {
|
||||
return workspaces.map(workspace => {
|
||||
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
|
||||
workspace.id,
|
||||
WorkspaceFlavour.AFFINE,
|
||||
{
|
||||
workspaceApis: affineApis,
|
||||
}
|
||||
);
|
||||
const remWorkspace: AffineLegacyCloudWorkspace = {
|
||||
...workspace,
|
||||
flavour: WorkspaceFlavour.AFFINE,
|
||||
blockSuiteWorkspace,
|
||||
};
|
||||
return remWorkspace;
|
||||
});
|
||||
});
|
||||
}
|
||||
return (affineApis as any)[query]();
|
||||
}
|
||||
};
|
||||
|
||||
export const QueryKey = {
|
||||
acceptInvite: 'acceptInvite',
|
||||
getImage: 'getImage',
|
||||
getWorkspaces: 'getWorkspaces',
|
||||
downloadWorkspace: 'downloadWorkspace',
|
||||
getMembers: 'getMembers',
|
||||
getUserByEmail: 'getUserByEmail',
|
||||
} as const;
|
||||
@@ -1,380 +0,0 @@
|
||||
/**
|
||||
* This file has deprecated because we do not maintain legacy affine cloud,
|
||||
* please use new affine cloud instead.
|
||||
*/
|
||||
import { initEmptyPage } from '@affine/env/blocksuite';
|
||||
import { AFFINE_STORAGE_KEY, PageNotFoundError } from '@affine/env/constant';
|
||||
import type {
|
||||
AffineDownloadProvider,
|
||||
AffineLegacyCloudWorkspace,
|
||||
LocalIndexedDBDownloadProvider,
|
||||
} from '@affine/env/workspace';
|
||||
import type { WorkspaceAdapter } from '@affine/env/workspace';
|
||||
import {
|
||||
LoadPriority,
|
||||
ReleaseType,
|
||||
WorkspaceFlavour,
|
||||
} from '@affine/env/workspace';
|
||||
import { currentAffineUserAtom } from '@affine/workspace/affine/atom';
|
||||
import {
|
||||
clearLoginStorage,
|
||||
getLoginStorage,
|
||||
isExpired,
|
||||
parseIdToken,
|
||||
setLoginStorage,
|
||||
SignMethod,
|
||||
} from '@affine/workspace/affine/login';
|
||||
import { affineApis, affineAuth } from '@affine/workspace/affine/shared';
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import { createIndexedDBDownloadProvider } from '@affine/workspace/providers';
|
||||
import { createAffineDownloadProvider } from '@affine/workspace/providers';
|
||||
import {
|
||||
cleanupWorkspace,
|
||||
createEmptyBlockSuiteWorkspace,
|
||||
} from '@affine/workspace/utils';
|
||||
import { rootStore } from '@toeverything/plugin-infra/manager';
|
||||
import { createJSONStorage } from 'jotai/utils';
|
||||
import type { PropsWithChildren, ReactElement } from 'react';
|
||||
import { Suspense, useEffect } from 'react';
|
||||
import { mutate } from 'swr';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { PageLoading } from '../../components/pure/loading';
|
||||
import { useAffineRefreshAuthToken } from '../../hooks/affine/use-affine-refresh-auth-token';
|
||||
import { BlockSuiteWorkspace } from '../../shared';
|
||||
import { toast } from '../../utils';
|
||||
import {
|
||||
BlockSuitePageList,
|
||||
NewWorkspaceSettingDetail,
|
||||
PageDetailEditor,
|
||||
WorkspaceHeader,
|
||||
WorkspaceSettingDetail,
|
||||
} from '../shared';
|
||||
import { QueryKey } from './fetcher';
|
||||
|
||||
const storage = createJSONStorage(() => localStorage);
|
||||
const schema = z.object({
|
||||
id: z.string(),
|
||||
type: z.number(),
|
||||
public: z.boolean(),
|
||||
permission: z.number(),
|
||||
});
|
||||
|
||||
const getPersistenceAllWorkspace = () => {
|
||||
const items = storage.getItem(AFFINE_STORAGE_KEY, []);
|
||||
const allWorkspaces: AffineLegacyCloudWorkspace[] = [];
|
||||
if (
|
||||
Array.isArray(items) &&
|
||||
items.every(item => schema.safeParse(item).success)
|
||||
) {
|
||||
allWorkspaces.push(
|
||||
...items.map((item: z.infer<typeof schema>) => {
|
||||
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
|
||||
item.id,
|
||||
WorkspaceFlavour.AFFINE,
|
||||
{
|
||||
workspaceApis: affineApis,
|
||||
}
|
||||
);
|
||||
const affineWorkspace: AffineLegacyCloudWorkspace = {
|
||||
...item,
|
||||
flavour: WorkspaceFlavour.AFFINE,
|
||||
blockSuiteWorkspace,
|
||||
};
|
||||
return affineWorkspace;
|
||||
})
|
||||
);
|
||||
}
|
||||
return allWorkspaces;
|
||||
};
|
||||
|
||||
function AuthContext({ children }: PropsWithChildren): ReactElement {
|
||||
const login = useAffineRefreshAuthToken();
|
||||
|
||||
useEffect(() => {
|
||||
if (!login) {
|
||||
console.warn('No login, redirecting to local workspace page...');
|
||||
}
|
||||
}, [login]);
|
||||
if (!login) {
|
||||
return <PageLoading />;
|
||||
}
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
export const AffineAdapter: WorkspaceAdapter<WorkspaceFlavour.AFFINE> = {
|
||||
releaseType: ReleaseType.STABLE,
|
||||
flavour: WorkspaceFlavour.AFFINE,
|
||||
loadPriority: LoadPriority.HIGH,
|
||||
Events: {
|
||||
'workspace:access': async () => {
|
||||
if (!runtimeConfig.enableLegacyCloud) {
|
||||
console.warn('Legacy cloud is disabled');
|
||||
return;
|
||||
}
|
||||
const response = await affineAuth.generateToken(SignMethod.Google);
|
||||
if (response) {
|
||||
setLoginStorage(response);
|
||||
const user = parseIdToken(response.token);
|
||||
rootStore.set(currentAffineUserAtom, user);
|
||||
} else {
|
||||
toast('Login failed');
|
||||
}
|
||||
},
|
||||
'workspace:revoke': async () => {
|
||||
if (!runtimeConfig.enableLegacyCloud) {
|
||||
console.warn('Legacy cloud is disabled');
|
||||
return;
|
||||
}
|
||||
await rootStore.set(rootWorkspacesMetadataAtom, workspaces =>
|
||||
workspaces.filter(
|
||||
workspace => workspace.flavour !== WorkspaceFlavour.AFFINE
|
||||
)
|
||||
);
|
||||
storage.removeItem(AFFINE_STORAGE_KEY);
|
||||
clearLoginStorage();
|
||||
rootStore.set(currentAffineUserAtom, null);
|
||||
},
|
||||
},
|
||||
CRUD: {
|
||||
create: async blockSuiteWorkspace => {
|
||||
const binary = BlockSuiteWorkspace.Y.encodeStateAsUpdate(
|
||||
blockSuiteWorkspace.doc
|
||||
);
|
||||
const { id } = await affineApis.createWorkspace(binary);
|
||||
// fixme: syncing images
|
||||
const newWorkspaceId = id;
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
const blobManager = blockSuiteWorkspace.blobs;
|
||||
for (const id of await blobManager.list()) {
|
||||
const blob = await blobManager.get(id);
|
||||
if (blob) {
|
||||
await affineApis.uploadBlob(
|
||||
newWorkspaceId,
|
||||
await blob.arrayBuffer(),
|
||||
blob.type
|
||||
);
|
||||
}
|
||||
}
|
||||
{
|
||||
const bs = createEmptyBlockSuiteWorkspace(id, WorkspaceFlavour.AFFINE, {
|
||||
workspaceApis: affineApis,
|
||||
});
|
||||
// fixme:
|
||||
// force to download workspace binary
|
||||
// to make sure the workspace is synced
|
||||
const provider = createAffineDownloadProvider(bs.id, bs.doc, {
|
||||
awareness: bs.awarenessStore.awareness,
|
||||
}) as AffineDownloadProvider;
|
||||
const indexedDBProvider = createIndexedDBDownloadProvider(
|
||||
bs.id,
|
||||
bs.doc,
|
||||
{
|
||||
awareness: bs.awarenessStore.awareness,
|
||||
}
|
||||
) as LocalIndexedDBDownloadProvider;
|
||||
indexedDBProvider.sync();
|
||||
await indexedDBProvider.whenReady;
|
||||
provider.disconnect();
|
||||
}
|
||||
|
||||
await mutate(matcher => matcher === QueryKey.getWorkspaces);
|
||||
// refresh the local storage
|
||||
await AffineAdapter.CRUD.list();
|
||||
return id;
|
||||
},
|
||||
delete: async workspace => {
|
||||
const items = storage.getItem(AFFINE_STORAGE_KEY, []);
|
||||
if (
|
||||
Array.isArray(items) &&
|
||||
items.every(item => schema.safeParse(item).success)
|
||||
) {
|
||||
storage.setItem(
|
||||
AFFINE_STORAGE_KEY,
|
||||
items.filter(item => item.id !== workspace.id)
|
||||
);
|
||||
}
|
||||
await affineApis.deleteWorkspace({
|
||||
id: workspace.id,
|
||||
});
|
||||
await mutate(matcher => matcher === QueryKey.getWorkspaces);
|
||||
},
|
||||
get: async workspaceId => {
|
||||
// fixme(himself65): rewrite the auth logic
|
||||
try {
|
||||
const loginStorage = getLoginStorage();
|
||||
if (
|
||||
loginStorage == null ||
|
||||
isExpired(parseIdToken(loginStorage.token))
|
||||
) {
|
||||
rootStore.set(currentAffineUserAtom, null);
|
||||
storage.removeItem(AFFINE_STORAGE_KEY);
|
||||
cleanupWorkspace(WorkspaceFlavour.AFFINE);
|
||||
return null;
|
||||
}
|
||||
const workspaces: AffineLegacyCloudWorkspace[] =
|
||||
await AffineAdapter.CRUD.list();
|
||||
return (
|
||||
workspaces.find(workspace => workspace.id === workspaceId) ?? null
|
||||
);
|
||||
} catch (e) {
|
||||
const workspaces = getPersistenceAllWorkspace();
|
||||
return (
|
||||
workspaces.find(workspace => workspace.id === workspaceId) ?? null
|
||||
);
|
||||
}
|
||||
},
|
||||
list: async () => {
|
||||
const allWorkspaces = getPersistenceAllWorkspace();
|
||||
const loginStorage = getLoginStorage();
|
||||
// fixme(himself65): rewrite the auth logic
|
||||
try {
|
||||
if (
|
||||
loginStorage == null ||
|
||||
isExpired(parseIdToken(loginStorage.token))
|
||||
) {
|
||||
rootStore.set(currentAffineUserAtom, null);
|
||||
storage.removeItem(AFFINE_STORAGE_KEY);
|
||||
return [];
|
||||
}
|
||||
} catch (e) {
|
||||
storage.removeItem(AFFINE_STORAGE_KEY);
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const workspaces = await affineApis.getWorkspaces().then(workspaces => {
|
||||
return workspaces.map(workspace => {
|
||||
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
|
||||
workspace.id,
|
||||
WorkspaceFlavour.AFFINE,
|
||||
{
|
||||
workspaceApis: affineApis,
|
||||
}
|
||||
);
|
||||
const dump = workspaces.map(workspace => {
|
||||
return {
|
||||
id: workspace.id,
|
||||
type: workspace.type,
|
||||
public: workspace.public,
|
||||
permission: workspace.permission,
|
||||
} satisfies z.infer<typeof schema>;
|
||||
});
|
||||
const old = storage.getItem(AFFINE_STORAGE_KEY, []);
|
||||
if (
|
||||
Array.isArray(old) &&
|
||||
old.every(item => schema.safeParse(item).success)
|
||||
) {
|
||||
const data = [...dump];
|
||||
old.forEach((item: z.infer<typeof schema>) => {
|
||||
const has = dump.find(dump => dump.id === item.id);
|
||||
if (!has) {
|
||||
data.push(item);
|
||||
}
|
||||
});
|
||||
storage.setItem(AFFINE_STORAGE_KEY, [...data]);
|
||||
}
|
||||
|
||||
const affineWorkspace: AffineLegacyCloudWorkspace = {
|
||||
...workspace,
|
||||
flavour: WorkspaceFlavour.AFFINE,
|
||||
blockSuiteWorkspace,
|
||||
};
|
||||
return affineWorkspace;
|
||||
});
|
||||
});
|
||||
workspaces.forEach(workspace => {
|
||||
const idx = allWorkspaces.findIndex(({ id }) => id === workspace.id);
|
||||
if (idx !== -1) {
|
||||
allWorkspaces.splice(idx, 1, workspace);
|
||||
} else {
|
||||
allWorkspaces.push(workspace);
|
||||
}
|
||||
});
|
||||
|
||||
// only save data when login in
|
||||
const dump = allWorkspaces.map(workspace => {
|
||||
return {
|
||||
id: workspace.id,
|
||||
type: workspace.type,
|
||||
public: workspace.public,
|
||||
permission: workspace.permission,
|
||||
} satisfies z.infer<typeof schema>;
|
||||
});
|
||||
storage.setItem(AFFINE_STORAGE_KEY, [...dump]);
|
||||
} catch (e) {
|
||||
console.error('fetch affine workspaces failed', e);
|
||||
}
|
||||
return [...allWorkspaces];
|
||||
},
|
||||
},
|
||||
UI: {
|
||||
Provider: ({ children }) => {
|
||||
return (
|
||||
<Suspense fallback={<PageLoading />}>
|
||||
<AuthContext>{children}</AuthContext>
|
||||
</Suspense>
|
||||
);
|
||||
},
|
||||
Header: WorkspaceHeader,
|
||||
PageDetail: ({ currentWorkspace, currentPageId, onLoadEditor }) => {
|
||||
const page = currentWorkspace.blockSuiteWorkspace.getPage(currentPageId);
|
||||
if (!page) {
|
||||
throw new PageNotFoundError(
|
||||
currentWorkspace.blockSuiteWorkspace,
|
||||
currentPageId
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<PageDetailEditor
|
||||
pageId={currentPageId}
|
||||
workspace={currentWorkspace}
|
||||
onInit={initEmptyPage}
|
||||
onLoad={onLoadEditor}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
},
|
||||
PageList: ({ blockSuiteWorkspace, onOpenPage, collection }) => {
|
||||
return (
|
||||
<BlockSuitePageList
|
||||
collection={collection}
|
||||
listType="all"
|
||||
onOpenPage={onOpenPage}
|
||||
blockSuiteWorkspace={blockSuiteWorkspace}
|
||||
/>
|
||||
);
|
||||
},
|
||||
SettingsDetail: ({
|
||||
currentWorkspace,
|
||||
onChangeTab,
|
||||
currentTab,
|
||||
onDeleteWorkspace,
|
||||
onTransformWorkspace,
|
||||
}) => {
|
||||
return (
|
||||
<WorkspaceSettingDetail
|
||||
onDeleteWorkspace={onDeleteWorkspace}
|
||||
onChangeTab={onChangeTab}
|
||||
currentTab={currentTab}
|
||||
workspace={currentWorkspace}
|
||||
onTransferWorkspace={onTransformWorkspace}
|
||||
/>
|
||||
);
|
||||
},
|
||||
NewSettingsDetail: ({
|
||||
currentWorkspace,
|
||||
onDeleteWorkspace,
|
||||
onTransformWorkspace,
|
||||
}) => {
|
||||
return (
|
||||
<NewWorkspaceSettingDetail
|
||||
onDeleteWorkspace={onDeleteWorkspace}
|
||||
workspace={currentWorkspace}
|
||||
onTransferWorkspace={onTransformWorkspace}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
NewWorkspaceSettingDetail,
|
||||
PageDetailEditor,
|
||||
WorkspaceHeader,
|
||||
WorkspaceSettingDetail,
|
||||
} from '../shared';
|
||||
|
||||
const logger = new DebugLogger('use-create-first-workspace');
|
||||
@@ -105,23 +104,6 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
|
||||
/>
|
||||
);
|
||||
},
|
||||
SettingsDetail: ({
|
||||
currentWorkspace,
|
||||
onChangeTab,
|
||||
currentTab,
|
||||
onDeleteWorkspace,
|
||||
onTransformWorkspace,
|
||||
}) => {
|
||||
return (
|
||||
<WorkspaceSettingDetail
|
||||
onDeleteWorkspace={onDeleteWorkspace}
|
||||
onChangeTab={onChangeTab}
|
||||
currentTab={currentTab}
|
||||
workspace={currentWorkspace}
|
||||
onTransferWorkspace={onTransformWorkspace}
|
||||
/>
|
||||
);
|
||||
},
|
||||
NewSettingsDetail: ({
|
||||
currentWorkspace,
|
||||
onDeleteWorkspace,
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import { lazy } from 'react';
|
||||
// export { WorkspaceSettingDetail as NewWorkspaceSettingDetail } from '../components/affine/new-workspace-setting-detail';
|
||||
export const WorkspaceSettingDetail = lazy(() =>
|
||||
import('../components/affine/workspace-setting-detail').then(
|
||||
({ WorkspaceSettingDetail }) => ({
|
||||
default: WorkspaceSettingDetail,
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
export const NewWorkspaceSettingDetail = lazy(() =>
|
||||
import('../components/affine/new-workspace-setting-detail').then(
|
||||
({ WorkspaceSettingDetail }) => ({
|
||||
@@ -14,6 +7,7 @@ export const NewWorkspaceSettingDetail = lazy(() =>
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
export const BlockSuitePageList = lazy(() =>
|
||||
import('../components/blocksuite/block-suite-page-list').then(
|
||||
({ BlockSuitePageList }) => ({
|
||||
@@ -21,6 +15,7 @@ export const BlockSuitePageList = lazy(() =>
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
export const PageDetailEditor = lazy(() =>
|
||||
import('../components/page-detail-editor').then(({ PageDetailEditor }) => ({
|
||||
default: PageDetailEditor,
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
WorkspaceFlavour,
|
||||
} from '@affine/env/workspace';
|
||||
|
||||
import { AffineAdapter } from './affine';
|
||||
import { LocalAdapter } from './local';
|
||||
|
||||
const unimplemented = () => {
|
||||
@@ -22,7 +21,6 @@ const bypassList = async () => {
|
||||
};
|
||||
|
||||
export const WorkspaceAdapters = {
|
||||
[WorkspaceFlavour.AFFINE]: AffineAdapter,
|
||||
[WorkspaceFlavour.LOCAL]: LocalAdapter,
|
||||
[WorkspaceFlavour.AFFINE_CLOUD]: {
|
||||
releaseType: ReleaseType.UNRELEASED,
|
||||
@@ -42,7 +40,6 @@ export const WorkspaceAdapters = {
|
||||
Header: unimplemented,
|
||||
PageDetail: unimplemented,
|
||||
PageList: unimplemented,
|
||||
SettingsDetail: unimplemented,
|
||||
NewSettingsDetail: unimplemented,
|
||||
},
|
||||
},
|
||||
@@ -64,7 +61,6 @@ export const WorkspaceAdapters = {
|
||||
Header: unimplemented,
|
||||
PageDetail: unimplemented,
|
||||
PageList: unimplemented,
|
||||
SettingsDetail: unimplemented,
|
||||
NewSettingsDetail: unimplemented,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import type { BlockSuiteFeatureFlags } from '@affine/env/global';
|
||||
import type { AffinePublicWorkspace } from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { affineApis } from '@affine/workspace/affine/shared';
|
||||
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
||||
import { atom } from 'jotai';
|
||||
|
||||
import { BlockSuiteWorkspace } from '../../shared';
|
||||
|
||||
function createPublicWorkspace(
|
||||
workspaceId: string,
|
||||
binary: ArrayBuffer,
|
||||
singlePage = false
|
||||
): AffinePublicWorkspace {
|
||||
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
|
||||
workspaceId,
|
||||
WorkspaceFlavour.AFFINE,
|
||||
{
|
||||
workspaceApis: affineApis,
|
||||
cachePrefix: WorkspaceFlavour.PUBLIC + (singlePage ? '-single-page' : ''),
|
||||
}
|
||||
);
|
||||
BlockSuiteWorkspace.Y.applyUpdate(
|
||||
blockSuiteWorkspace.doc,
|
||||
new Uint8Array(binary)
|
||||
);
|
||||
Object.entries(runtimeConfig.editorFlags).forEach(([key, value]) => {
|
||||
blockSuiteWorkspace.awarenessStore.setFlag(
|
||||
key as keyof BlockSuiteFeatureFlags,
|
||||
value
|
||||
);
|
||||
});
|
||||
// force disable some features
|
||||
blockSuiteWorkspace.awarenessStore.setFlag('enable_block_hub', false);
|
||||
blockSuiteWorkspace.awarenessStore.setFlag('enable_drag_handle', false);
|
||||
return {
|
||||
flavour: WorkspaceFlavour.PUBLIC,
|
||||
id: workspaceId,
|
||||
blockSuiteWorkspace,
|
||||
};
|
||||
}
|
||||
|
||||
export const publicWorkspaceIdAtom = atom<string | null>(null);
|
||||
export const publicWorkspacePageIdAtom = atom<string | null>(null);
|
||||
export const publicPageBlockSuiteAtom = atom<Promise<AffinePublicWorkspace>>(
|
||||
async get => {
|
||||
const workspaceId = get(publicWorkspaceIdAtom);
|
||||
const pageId = get(publicWorkspacePageIdAtom);
|
||||
if (!workspaceId || !pageId) {
|
||||
throw new Error('No workspace id or page id');
|
||||
}
|
||||
const binary = await affineApis.downloadPublicWorkspacePage(
|
||||
workspaceId,
|
||||
pageId
|
||||
);
|
||||
return createPublicWorkspace(workspaceId, binary, true);
|
||||
}
|
||||
);
|
||||
export const publicWorkspaceAtom = atom<Promise<AffinePublicWorkspace>>(
|
||||
async get => {
|
||||
const workspaceId = get(publicWorkspaceIdAtom);
|
||||
if (!workspaceId) {
|
||||
throw new Error('No workspace id');
|
||||
}
|
||||
const binary = await affineApis.downloadWorkspace(workspaceId, true);
|
||||
return createPublicWorkspace(workspaceId, binary, false);
|
||||
}
|
||||
);
|
||||
@@ -4,7 +4,7 @@ import type {
|
||||
WorkspaceAdapter,
|
||||
WorkspaceRegistry,
|
||||
} from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import type { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import {
|
||||
rootCurrentWorkspaceIdAtom,
|
||||
rootWorkspacesMetadataAtom,
|
||||
@@ -26,16 +26,9 @@ export const workspacesAtom = atom<Promise<AllWorkspace[]>>(
|
||||
const flavours: string[] = Object.values(WorkspaceAdapters).map(
|
||||
plugin => plugin.flavour
|
||||
);
|
||||
const jotaiWorkspaces = (await get(rootWorkspacesMetadataAtom))
|
||||
.filter(
|
||||
workspace => flavours.includes(workspace.flavour)
|
||||
// TODO: remove this when we remove the legacy cloud
|
||||
)
|
||||
.filter(workspace =>
|
||||
!runtimeConfig.enableLegacyCloud
|
||||
? workspace.flavour !== WorkspaceFlavour.AFFINE
|
||||
: true
|
||||
);
|
||||
const jotaiWorkspaces = (await get(rootWorkspacesMetadataAtom)).filter(
|
||||
workspace => flavours.includes(workspace.flavour)
|
||||
);
|
||||
if (jotaiWorkspaces.some(meta => !('version' in meta))) {
|
||||
// wait until all workspaces have migrated to v2
|
||||
await new Promise((resolve, reject) => {
|
||||
@@ -63,7 +56,7 @@ export const workspacesAtom = atom<Promise<AllWorkspace[]>>(
|
||||
})
|
||||
).then(workspaces =>
|
||||
workspaces.filter(
|
||||
(workspace): workspace is WorkspaceRegistry['affine' | 'local'] =>
|
||||
(workspace): workspace is WorkspaceRegistry['affine-cloud' | 'local'] =>
|
||||
workspace !== null
|
||||
)
|
||||
);
|
||||
|
||||
@@ -4,7 +4,6 @@ import type {
|
||||
WorkspaceNotFoundError,
|
||||
} from '@affine/env/constant';
|
||||
import { PageNotFoundError } from '@affine/env/constant';
|
||||
import { RequestError } from '@affine/workspace/affine/api';
|
||||
import type { NextRouter } from 'next/router';
|
||||
import type { ErrorInfo, ReactNode } from 'react';
|
||||
import type React from 'react';
|
||||
@@ -19,7 +18,6 @@ type AffineError =
|
||||
| Unreachable
|
||||
| WorkspaceNotFoundError
|
||||
| PageNotFoundError
|
||||
| RequestError
|
||||
| Error;
|
||||
|
||||
interface AffineErrorBoundaryState {
|
||||
@@ -78,13 +76,6 @@ export class AffineErrorBoundary extends Component<
|
||||
</>
|
||||
</>
|
||||
);
|
||||
} else if (error instanceof RequestError) {
|
||||
return (
|
||||
<>
|
||||
<h1>Sorry.. there was an error</h1>
|
||||
{error.message}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { CloseIcon } from '@blocksuite/icons';
|
||||
import type React from 'react';
|
||||
|
||||
import { useCurrentUser } from '../../../hooks/current/use-current-user';
|
||||
import { Content, ContentTitle, Header, StyleButton, StyleTips } from './style';
|
||||
|
||||
interface EnableAffineCloudModalProps {
|
||||
@@ -18,7 +17,6 @@ export const EnableAffineCloudModal: React.FC<EnableAffineCloudModalProps> = ({
|
||||
onClose,
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const user = useCurrentUser();
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose} data-testid="logout-modal">
|
||||
@@ -39,7 +37,7 @@ export const EnableAffineCloudModal: React.FC<EnableAffineCloudModalProps> = ({
|
||||
type="primary"
|
||||
onClick={onConfirm}
|
||||
>
|
||||
{user ? t.Enable() : t['Sign in and Enable']()}
|
||||
{t['Sign in and Enable']()}
|
||||
</StyleButton>
|
||||
<StyleButton shape="round" onClick={onClose}>
|
||||
{t['Not now']()}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { ArrowRightSmallIcon } from '@blocksuite/icons';
|
||||
import { type FC, useState } from 'react';
|
||||
|
||||
import { useIsWorkspaceOwner } from '../../../../hooks/affine/use-is-workspace-owner';
|
||||
import type { AffineOfficialWorkspace } from '../../../../shared';
|
||||
import type { WorkspaceSettingDetailProps } from '../index';
|
||||
import { WorkspaceDeleteModal } from './delete';
|
||||
@@ -14,7 +13,8 @@ export const DeleteLeaveWorkspace: FC<{
|
||||
onDeleteWorkspace: WorkspaceSettingDetailProps['onDeleteWorkspace'];
|
||||
}> = ({ workspace, onDeleteWorkspace }) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const isOwner = useIsWorkspaceOwner(workspace);
|
||||
// fixme: cloud regression
|
||||
const isOwner = true;
|
||||
|
||||
const [showDelete, setShowDelete] = useState(false);
|
||||
const [showLeave, setShowLeave] = useState(false);
|
||||
|
||||
@@ -14,7 +14,6 @@ import type { FC } from 'react';
|
||||
import type { AffineOfficialWorkspace } from '../../../shared';
|
||||
import { DeleteLeaveWorkspace } from './delete-leave-workspace';
|
||||
import { ExportPanel } from './export';
|
||||
import { MembersPanel } from './members';
|
||||
import { ProfilePanel } from './profile';
|
||||
import { PublishPanel } from './publish';
|
||||
import { StoragePanel } from './storage';
|
||||
@@ -63,11 +62,6 @@ export const WorkspaceSettingDetail: FC<WorkspaceSettingDetailProps> = ({
|
||||
onDeleteWorkspace={onDeleteWorkspace}
|
||||
{...props}
|
||||
/>
|
||||
<MembersPanel
|
||||
workspace={workspace}
|
||||
onDeleteWorkspace={onDeleteWorkspace}
|
||||
{...props}
|
||||
/>
|
||||
</SettingWrapper>
|
||||
{environment.isDesktop ? (
|
||||
<SettingWrapper title={t['Storage and Export']()}>
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
import { Button, IconButton, Menu, MenuItem } from '@affine/component';
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import { UserAvatar } from '@affine/component/user-avatar';
|
||||
import { Unreachable } from '@affine/env/constant';
|
||||
import type { AffineLegacyCloudWorkspace } from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { PermissionType } from '@affine/env/workspace/legacy-cloud';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { DeleteTemporarilyIcon, MoreVerticalIcon } from '@blocksuite/icons';
|
||||
import type { FC } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import { useMembers } from '../../../../hooks/affine/use-members';
|
||||
import type { AffineOfficialWorkspace } from '../../../../shared';
|
||||
import { toast } from '../../../../utils';
|
||||
import type { WorkspaceSettingDetailProps } from '../index';
|
||||
import { fakeWrapper } from '../style.css';
|
||||
import { InviteMemberModal } from './invite-member-modal';
|
||||
import * as style from './style.css';
|
||||
|
||||
export type AffineRemoteMembersProps = WorkspaceSettingDetailProps & {
|
||||
workspace: AffineLegacyCloudWorkspace;
|
||||
};
|
||||
export type MemberPanelProps = WorkspaceSettingDetailProps & {
|
||||
workspace: AffineOfficialWorkspace;
|
||||
};
|
||||
|
||||
const MemberList: FC<{
|
||||
workspace: AffineLegacyCloudWorkspace;
|
||||
}> = ({ workspace }) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const { members, removeMember } = useMembers(workspace.id);
|
||||
|
||||
if (members.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className={style.memberList}>
|
||||
{members
|
||||
.sort((b, a) => a.type - b.type)
|
||||
.map(member => {
|
||||
const { id, name, email, avatar_url } = {
|
||||
name: '',
|
||||
email: '',
|
||||
avatar_url: '',
|
||||
...member,
|
||||
};
|
||||
return (
|
||||
<li className="member-list-item" key={id}>
|
||||
<div className="left-col">
|
||||
<UserAvatar size={36} name={name} url={avatar_url} />
|
||||
<div className="user-info-wrapper">
|
||||
<p className="user-name">{name}</p>
|
||||
<p className="email">{email}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="right-col">
|
||||
<div className="user-identity">
|
||||
{member.accepted
|
||||
? member.type !== PermissionType.Owner
|
||||
? t['Member']()
|
||||
: t['Owner']()
|
||||
: t['Pending']()}
|
||||
</div>
|
||||
<Menu
|
||||
content={
|
||||
<>
|
||||
<MenuItem
|
||||
onClick={async () => {
|
||||
await removeMember(Number(id));
|
||||
toast(
|
||||
t['Member has been removed']({
|
||||
name,
|
||||
})
|
||||
);
|
||||
}}
|
||||
icon={<DeleteTemporarilyIcon />}
|
||||
>
|
||||
{t['Remove from workspace']()}
|
||||
</MenuItem>
|
||||
</>
|
||||
}
|
||||
placement="bottom"
|
||||
disablePortal={true}
|
||||
trigger="click"
|
||||
>
|
||||
<IconButton>
|
||||
<MoreVerticalIcon />
|
||||
</IconButton>
|
||||
</Menu>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
export const AffineRemoteMembers: FC<AffineRemoteMembersProps> = ({
|
||||
workspace,
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const { members } = useMembers(workspace.id);
|
||||
|
||||
const [isInviteModalShow, setIsInviteModalShow] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingRow
|
||||
name={`${t['Members']()} (${members.length})`}
|
||||
desc={t['Members hint']()}
|
||||
style={{ marginTop: '25px' }}
|
||||
>
|
||||
<Button
|
||||
size="middle"
|
||||
onClick={() => {
|
||||
setIsInviteModalShow(true);
|
||||
}}
|
||||
>
|
||||
{t['Invite']()}
|
||||
</Button>
|
||||
</SettingRow>
|
||||
<MemberList workspace={workspace} />
|
||||
<InviteMemberModal
|
||||
onClose={useCallback(() => {
|
||||
setIsInviteModalShow(false);
|
||||
}, [])}
|
||||
onInviteSuccess={useCallback(() => {
|
||||
setIsInviteModalShow(false);
|
||||
}, [])}
|
||||
workspaceId={workspace.id}
|
||||
open={isInviteModalShow}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export const FakeMembers: FC = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
return (
|
||||
<div className={fakeWrapper} style={{ marginTop: '25px' }}>
|
||||
<SettingRow name={`${t['Members']()} (0)`} desc={t['Members hint']()}>
|
||||
<Button size="middle">{t['Invite']()}</Button>
|
||||
</SettingRow>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const MembersPanel: FC<MemberPanelProps> = props => {
|
||||
switch (props.workspace.flavour) {
|
||||
case WorkspaceFlavour.AFFINE: {
|
||||
const workspace = props.workspace as AffineLegacyCloudWorkspace;
|
||||
return <AffineRemoteMembers {...props} workspace={workspace} />;
|
||||
}
|
||||
case WorkspaceFlavour.LOCAL: {
|
||||
return <FakeMembers />;
|
||||
}
|
||||
}
|
||||
throw new Unreachable();
|
||||
};
|
||||
@@ -1,222 +0,0 @@
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Modal,
|
||||
ModalCloseButton,
|
||||
ModalWrapper,
|
||||
MuiAvatar,
|
||||
styled,
|
||||
} from '@affine/component';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { EmailIcon } from '@blocksuite/icons';
|
||||
import type React from 'react';
|
||||
import { Suspense, useCallback, useState } from 'react';
|
||||
|
||||
import { useMembers } from '../../../../../hooks/affine/use-members';
|
||||
import { useUsersByEmail } from '../../../../../hooks/affine/use-users-by-email';
|
||||
|
||||
interface LoginModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
workspaceId: string;
|
||||
onInviteSuccess: () => void;
|
||||
}
|
||||
|
||||
const gmailReg =
|
||||
/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@(gmail|example)\.(com|org)$/;
|
||||
|
||||
const Result: React.FC<{
|
||||
workspaceId: string;
|
||||
queryEmail: string;
|
||||
}> = ({ workspaceId, queryEmail }) => {
|
||||
const users = useUsersByEmail(workspaceId, queryEmail);
|
||||
const firstUser = users?.at(0) ?? null;
|
||||
if (!firstUser || !firstUser.email) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Members>
|
||||
<Member>
|
||||
{firstUser.avatar_url ? (
|
||||
<MuiAvatar src={firstUser.avatar_url}></MuiAvatar>
|
||||
) : (
|
||||
<MemberIcon>
|
||||
<EmailIcon></EmailIcon>
|
||||
</MemberIcon>
|
||||
)}
|
||||
<Email>{firstUser.email}</Email>
|
||||
{/* <div>invited</div> */}
|
||||
</Member>
|
||||
</Members>
|
||||
);
|
||||
};
|
||||
|
||||
export const InviteMemberModal = ({
|
||||
open,
|
||||
onClose,
|
||||
onInviteSuccess,
|
||||
workspaceId,
|
||||
}: LoginModalProps) => {
|
||||
const { inviteMember } = useMembers(workspaceId);
|
||||
const [email, setEmail] = useState<string>('');
|
||||
const [showMemberPreview, setShowMemberPreview] = useState(false);
|
||||
const t = useAFFiNEI18N();
|
||||
const inputChange = useCallback((value: string) => {
|
||||
setEmail(value);
|
||||
}, []);
|
||||
return (
|
||||
<div>
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<ModalWrapper width={460} height={236}>
|
||||
<Header>
|
||||
<ModalCloseButton
|
||||
onClick={() => {
|
||||
onClose();
|
||||
setEmail('');
|
||||
}}
|
||||
/>
|
||||
</Header>
|
||||
<Content>
|
||||
<ContentTitle>{t['Invite Members']()}</ContentTitle>
|
||||
<InviteBox>
|
||||
<Input
|
||||
data-testid="invite-member-input"
|
||||
width={360}
|
||||
value={email}
|
||||
onChange={inputChange}
|
||||
onFocus={useCallback(() => {
|
||||
setShowMemberPreview(true);
|
||||
}, [])}
|
||||
onBlur={useCallback(() => {
|
||||
setShowMemberPreview(false);
|
||||
}, [])}
|
||||
placeholder={t['Invite placeholder']()}
|
||||
/>
|
||||
{showMemberPreview && gmailReg.test(email) && (
|
||||
<Suspense fallback="loading...">
|
||||
<Result workspaceId={workspaceId} queryEmail={email} />
|
||||
</Suspense>
|
||||
)}
|
||||
</InviteBox>
|
||||
</Content>
|
||||
<Footer>
|
||||
<Button
|
||||
data-testid="invite-member-button"
|
||||
disabled={!gmailReg.test(email)}
|
||||
shape="circle"
|
||||
type="primary"
|
||||
style={{
|
||||
width: '364px',
|
||||
height: '38px',
|
||||
borderRadius: '40px',
|
||||
}}
|
||||
onClick={async () => {
|
||||
await inviteMember(email);
|
||||
setEmail('');
|
||||
onInviteSuccess();
|
||||
}}
|
||||
>
|
||||
{t['Invite']()}
|
||||
</Button>
|
||||
</Footer>
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Header = styled('div')({
|
||||
position: 'relative',
|
||||
height: '44px',
|
||||
});
|
||||
|
||||
const Content = styled('div')({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
const ContentTitle = styled('h1')({
|
||||
fontSize: '20px',
|
||||
lineHeight: '28px',
|
||||
fontWeight: 600,
|
||||
textAlign: 'center',
|
||||
paddingBottom: '16px',
|
||||
});
|
||||
|
||||
const Footer = styled('div')({
|
||||
height: '102px',
|
||||
margin: '32px 0',
|
||||
textAlign: 'center',
|
||||
});
|
||||
|
||||
const InviteBox = styled('div')({
|
||||
position: 'relative',
|
||||
});
|
||||
|
||||
const Members = styled('div')(() => {
|
||||
return {
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
background: 'var(--affine-background-primary-color)',
|
||||
textAlign: 'left',
|
||||
zIndex: 1,
|
||||
borderRadius: '0px 10px 10px 10px',
|
||||
height: '56px',
|
||||
padding: '8px 12px',
|
||||
input: {
|
||||
'&::placeholder': {
|
||||
color: 'var(--affine-placeholder-color)',
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// const NoFind = styled('div')(({ theme }) => {
|
||||
// return {
|
||||
// color: 'var(--affine-icon-color)',
|
||||
// fontSize: 'var(--affine-font-sm)',
|
||||
// lineHeight: '40px',
|
||||
// userSelect: 'none',
|
||||
// width: '100%',
|
||||
// };
|
||||
// });
|
||||
|
||||
const Member = styled('div')(() => {
|
||||
return {
|
||||
color: 'var(--affine-icon-color)',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
lineHeight: '40px',
|
||||
userSelect: 'none',
|
||||
display: 'flex',
|
||||
};
|
||||
});
|
||||
|
||||
const MemberIcon = styled('div')(() => {
|
||||
return {
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
borderRadius: '50%',
|
||||
color: 'var(--affine-primary-color)',
|
||||
background: '#F5F5F5',
|
||||
textAlign: 'center',
|
||||
lineHeight: '45px',
|
||||
// icon size
|
||||
fontSize: '20px',
|
||||
overflow: 'hidden',
|
||||
img: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const Email = styled('div')(() => {
|
||||
return {
|
||||
flex: '1',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
marginLeft: '8px',
|
||||
};
|
||||
});
|
||||
@@ -1,54 +0,0 @@
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const memberList = style({
|
||||
marginTop: '12px',
|
||||
});
|
||||
|
||||
globalStyle(`${memberList} .member-list-item`, {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
});
|
||||
globalStyle(`${memberList} .member-list-item:not(:last-of-type)`, {
|
||||
marginBottom: '8px',
|
||||
});
|
||||
|
||||
globalStyle(`${memberList} .left-col`, {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: '60%',
|
||||
});
|
||||
globalStyle(`${memberList} .right-col`, {
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
width: '35%',
|
||||
});
|
||||
globalStyle(`${memberList} .user-info-wrapper`, {
|
||||
flexGrow: 1,
|
||||
marginLeft: '12px',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
globalStyle(`${memberList} .user-info-wrapper p`, {
|
||||
width: '100%',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
});
|
||||
|
||||
globalStyle(`${memberList} .user-name`, {
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
});
|
||||
globalStyle(`${memberList} .email`, {
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
});
|
||||
globalStyle(`${memberList} .user-identity`, {
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
marginRight: '15px',
|
||||
flexGrow: '1',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
});
|
||||
@@ -6,17 +6,33 @@ import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-s
|
||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||
import { type FC, useCallback, useState } from 'react';
|
||||
|
||||
import { useIsWorkspaceOwner } from '../../../hooks/affine/use-is-workspace-owner';
|
||||
import type { AffineOfficialWorkspace } from '../../../shared';
|
||||
import { Upload } from '../../pure/file-upload';
|
||||
import { CameraIcon } from '../workspace-setting-detail/panel/general/icons';
|
||||
import * as style from './style.css';
|
||||
|
||||
const CameraIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10.6236 4.25001C10.635 4.25001 10.6467 4.25002 10.6584 4.25002H13.3416C13.3533 4.25002 13.365 4.25001 13.3764 4.25001C13.5609 4.24995 13.7105 4.2499 13.8543 4.26611C14.5981 4.34997 15.2693 4.75627 15.6826 5.38026C15.7624 5.50084 15.83 5.63398 15.9121 5.79586C15.9173 5.80613 15.9226 5.81652 15.9279 5.82703C15.9538 5.87792 15.9679 5.90562 15.9789 5.9261C15.9832 5.9341 15.9857 5.93861 15.9869 5.94065C16.0076 5.97069 16.0435 5.99406 16.0878 5.99905L16.0849 5.99877C16.0849 5.99877 16.0907 5.99918 16.1047 5.99947C16.1286 5.99998 16.1604 6.00002 16.2181 6.00002L17.185 6.00001C17.6577 6 18.0566 5.99999 18.3833 6.02627C18.7252 6.05377 19.0531 6.11364 19.3656 6.27035C19.8402 6.50842 20.2283 6.88944 20.4723 7.36077C20.6336 7.67233 20.6951 7.99944 20.7232 8.33858C20.75 8.66166 20.75 9.05554 20.75 9.51992V16.2301C20.75 16.6945 20.75 17.0884 20.7232 17.4114C20.6951 17.7506 20.6336 18.0777 20.4723 18.3893C20.2283 18.8606 19.8402 19.2416 19.3656 19.4797C19.0531 19.6364 18.7252 19.6963 18.3833 19.7238C18.0566 19.75 17.6578 19.75 17.185 19.75H6.81497C6.34225 19.75 5.9434 19.75 5.61668 19.7238C5.27477 19.6963 4.94688 19.6364 4.63444 19.4797C4.15978 19.2416 3.77167 18.8606 3.52771 18.3893C3.36644 18.0777 3.30494 17.7506 3.27679 17.4114C3.24998 17.0884 3.24999 16.6945 3.25 16.2302V9.51987C3.24999 9.05551 3.24998 8.66164 3.27679 8.33858C3.30494 7.99944 3.36644 7.67233 3.52771 7.36077C3.77167 6.88944 4.15978 6.50842 4.63444 6.27035C4.94688 6.11364 5.27477 6.05377 5.61668 6.02627C5.9434 5.99999 6.34225 6 6.81498 6.00001L7.78191 6.00002C7.83959 6.00002 7.87142 5.99998 7.8953 5.99947C7.90607 5.99924 7.91176 5.99897 7.91398 5.99884C7.95747 5.99343 7.99267 5.9703 8.01312 5.94066C8.01429 5.93863 8.01684 5.93412 8.02113 5.9261C8.0321 5.90561 8.04622 5.87791 8.07206 5.82703C8.07739 5.81653 8.08266 5.80615 8.08787 5.79588C8.17004 5.63397 8.23759 5.50086 8.31745 5.38026C8.73067 4.75627 9.40192 4.34997 10.1457 4.26611C10.2895 4.2499 10.4391 4.24995 10.6236 4.25001ZM10.6584 5.75002C10.422 5.75002 10.3627 5.75114 10.3138 5.75666C10.0055 5.79142 9.73316 5.95919 9.56809 6.20845C9.54218 6.24758 9.51544 6.29761 9.40943 6.50633C9.40611 6.51287 9.40274 6.5195 9.39934 6.52622C9.36115 6.60161 9.31758 6.68761 9.26505 6.76694C8.9964 7.17261 8.56105 7.4354 8.08026 7.48961C7.98625 7.50021 7.89021 7.50011 7.80434 7.50003C7.79678 7.50002 7.7893 7.50002 7.78191 7.50002H6.84445C6.33444 7.50002 5.99634 7.50058 5.73693 7.52144C5.48594 7.54163 5.37478 7.57713 5.30693 7.61115C5.11257 7.70864 4.95675 7.86306 4.85983 8.05029C4.82733 8.11308 4.79194 8.21816 4.77165 8.46266C4.7506 8.71626 4.75 9.0474 4.75 9.55001V16.2C4.75 16.7026 4.7506 17.0338 4.77165 17.2874C4.79194 17.5319 4.82733 17.6369 4.85983 17.6997C4.95675 17.887 5.11257 18.0414 5.30693 18.1389C5.37478 18.1729 5.48594 18.2084 5.73693 18.2286C5.99634 18.2494 6.33444 18.25 6.84445 18.25H17.1556C17.6656 18.25 18.0037 18.2494 18.2631 18.2286C18.5141 18.2084 18.6252 18.1729 18.6931 18.1389C18.8874 18.0414 19.0433 17.887 19.1402 17.6997C19.1727 17.6369 19.2081 17.5319 19.2283 17.2874C19.2494 17.0338 19.25 16.7026 19.25 16.2V9.55001C19.25 9.0474 19.2494 8.71626 19.2283 8.46266C19.2081 8.21816 19.1727 8.11308 19.1402 8.05029C19.0433 7.86306 18.8874 7.70864 18.6931 7.61115C18.6252 7.57713 18.5141 7.54163 18.2631 7.52144C18.0037 7.50058 17.6656 7.50002 17.1556 7.50002H16.2181C16.2107 7.50002 16.2032 7.50002 16.1957 7.50003C16.1098 7.50011 16.0138 7.50021 15.9197 7.48961C15.4389 7.4354 15.0036 7.17261 14.735 6.76694C14.6824 6.68761 14.6389 6.60163 14.6007 6.52622C14.5973 6.5195 14.5939 6.51287 14.5906 6.50633C14.4846 6.29763 14.4578 6.24758 14.4319 6.20846C14.2668 5.95919 13.9945 5.79142 13.6862 5.75666C13.6373 5.75114 13.578 5.75002 13.3416 5.75002H10.6584ZM12 11C10.9303 11 10.0833 11.8506 10.0833 12.875C10.0833 13.8995 10.9303 14.75 12 14.75C13.0697 14.75 13.9167 13.8995 13.9167 12.875C13.9167 11.8506 13.0697 11 12 11ZM8.58333 12.875C8.58333 11 10.1242 9.50002 12 9.50002C13.8758 9.50002 15.4167 11 15.4167 12.875C15.4167 14.7501 13.8758 16.25 12 16.25C10.1242 16.25 8.58333 14.7501 8.58333 12.875Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const ProfilePanel: FC<{
|
||||
workspace: AffineOfficialWorkspace;
|
||||
}> = ({ workspace }) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const isOwner = useIsWorkspaceOwner(workspace);
|
||||
|
||||
const [, update] = useBlockSuiteWorkspaceAvatarUrl(
|
||||
workspace.blockSuiteWorkspace
|
||||
@@ -38,22 +54,18 @@ export const ProfilePanel: FC<{
|
||||
return (
|
||||
<div className={style.profileWrapper}>
|
||||
<div className={style.avatarWrapper}>
|
||||
{isOwner ? (
|
||||
<Upload
|
||||
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
|
||||
fileChange={update}
|
||||
data-testid="upload-avatar"
|
||||
>
|
||||
<>
|
||||
<div className="camera-icon-wrapper">
|
||||
<CameraIcon />
|
||||
</div>
|
||||
<WorkspaceAvatar size={56} workspace={workspace} />
|
||||
</>
|
||||
</Upload>
|
||||
) : (
|
||||
<WorkspaceAvatar size={56} workspace={workspace} />
|
||||
)}
|
||||
<Upload
|
||||
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
|
||||
fileChange={update}
|
||||
data-testid="upload-avatar"
|
||||
>
|
||||
<>
|
||||
<div className="camera-icon-wrapper">
|
||||
<CameraIcon />
|
||||
</div>
|
||||
<WorkspaceAvatar size={56} workspace={workspace} />
|
||||
</>
|
||||
</Upload>
|
||||
</div>
|
||||
<div className={style.profileHandlerWrapper}>
|
||||
<Input
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Button, FlexWrapper, Switch } from '@affine/component';
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import { Unreachable } from '@affine/env/constant';
|
||||
import type {
|
||||
AffineLegacyCloudWorkspace,
|
||||
AffineCloudWorkspace,
|
||||
LocalWorkspace,
|
||||
} from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
@@ -11,7 +11,6 @@ import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-
|
||||
import type { FC } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { useToggleWorkspacePublish } from '../../../hooks/affine/use-toggle-workspace-publish';
|
||||
import type { AffineOfficialWorkspace } from '../../../shared';
|
||||
import { toast } from '../../../utils';
|
||||
import { EnableAffineCloudModal } from '../enable-affine-cloud-modal';
|
||||
@@ -26,13 +25,13 @@ export type PublishPanelLocalProps = WorkspaceSettingDetailProps & {
|
||||
workspace: LocalWorkspace;
|
||||
};
|
||||
export type PublishPanelAffineProps = WorkspaceSettingDetailProps & {
|
||||
workspace: AffineLegacyCloudWorkspace;
|
||||
workspace: AffineCloudWorkspace;
|
||||
};
|
||||
|
||||
const PublishPanelAffine: FC<PublishPanelAffineProps> = props => {
|
||||
const { workspace } = props;
|
||||
const t = useAFFiNEI18N();
|
||||
const toggleWorkspacePublish = useToggleWorkspacePublish(workspace);
|
||||
// const toggleWorkspacePublish = useToggleWorkspacePublish(workspace);
|
||||
|
||||
const [origin, setOrigin] = useState('');
|
||||
const shareUrl = origin + '/public-workspace/' + workspace.id;
|
||||
@@ -54,13 +53,14 @@ const PublishPanelAffine: FC<PublishPanelAffineProps> = props => {
|
||||
<SettingRow
|
||||
name={t['Publish']()}
|
||||
desc={
|
||||
workspace.public ? t['Unpublished hint']() : t['Published hint']()
|
||||
// workspace.public ? t['Unpublished hint']() : t['Published hint']()
|
||||
'UNFINISHED'
|
||||
}
|
||||
>
|
||||
<Switch
|
||||
{/* <Switch
|
||||
checked={workspace.public}
|
||||
onChange={checked => toggleWorkspacePublish(checked)}
|
||||
/>
|
||||
/> */}
|
||||
</SettingRow>
|
||||
<FlexWrapper justifyContent="space-between">
|
||||
<Button
|
||||
@@ -150,7 +150,7 @@ const PublishPanelLocal: FC<PublishPanelLocalProps> = ({
|
||||
onConfirm={() => {
|
||||
onTransferWorkspace(
|
||||
WorkspaceFlavour.LOCAL,
|
||||
WorkspaceFlavour.AFFINE,
|
||||
WorkspaceFlavour.AFFINE_CLOUD,
|
||||
workspace
|
||||
);
|
||||
setOpen(false);
|
||||
@@ -169,7 +169,7 @@ const PublishPanelLocal: FC<PublishPanelLocalProps> = ({
|
||||
};
|
||||
|
||||
export const PublishPanel: FC<PublishPanelProps> = props => {
|
||||
if (props.workspace.flavour === WorkspaceFlavour.AFFINE) {
|
||||
if (props.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD) {
|
||||
return <PublishPanelAffine {...props} workspace={props.workspace} />;
|
||||
} else if (props.workspace.flavour === WorkspaceFlavour.LOCAL) {
|
||||
return <PublishPanelLocal {...props} workspace={props.workspace} />;
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
type SettingModalProps,
|
||||
} from '@affine/component/setting-components';
|
||||
import type {
|
||||
AffineLegacyCloudWorkspace,
|
||||
AffineCloudWorkspace,
|
||||
LocalWorkspace,
|
||||
} from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
@@ -77,7 +77,7 @@ export const SettingModal: React.FC<SettingModalProps> = ({
|
||||
generalSettingList={generalSettingList}
|
||||
onGeneralSettingClick={onGeneralSettingClick}
|
||||
currentWorkspace={
|
||||
currentWorkspace as AffineLegacyCloudWorkspace | LocalWorkspace
|
||||
currentWorkspace as AffineCloudWorkspace | LocalWorkspace
|
||||
}
|
||||
workspaceList={workspaceList}
|
||||
onWorkspaceSettingClick={onWorkspaceSettingClick}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { UserAvatar } from '@affine/component/user-avatar';
|
||||
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
|
||||
import type {
|
||||
AffineLegacyCloudWorkspace,
|
||||
AffineCloudWorkspace,
|
||||
LocalWorkspace,
|
||||
} from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
@@ -37,7 +37,7 @@ export const SettingSidebar = ({
|
||||
currentWorkspace: Workspace;
|
||||
workspaceList: Workspace[];
|
||||
onWorkspaceSettingClick: (
|
||||
workspace: AffineLegacyCloudWorkspace | LocalWorkspace
|
||||
workspace: AffineCloudWorkspace | LocalWorkspace
|
||||
) => void;
|
||||
|
||||
selectedWorkspace: Workspace | null;
|
||||
@@ -118,7 +118,7 @@ const WorkspaceListItem = ({
|
||||
isCurrent,
|
||||
isActive,
|
||||
}: {
|
||||
workspace: AffineLegacyCloudWorkspace | LocalWorkspace;
|
||||
workspace: AffineCloudWorkspace | LocalWorkspace;
|
||||
onClick: () => void;
|
||||
isCurrent: boolean;
|
||||
isActive: boolean;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type {
|
||||
AffineLegacyCloudWorkspace,
|
||||
AffineCloudWorkspace,
|
||||
LocalWorkspace,
|
||||
} from '@affine/env/workspace';
|
||||
|
||||
export type Workspace = AffineLegacyCloudWorkspace | LocalWorkspace;
|
||||
export type Workspace = AffineCloudWorkspace | LocalWorkspace;
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { CloseIcon } from '@blocksuite/icons';
|
||||
import type React from 'react';
|
||||
|
||||
import { useCurrentUser } from '../../../hooks/current/use-current-user';
|
||||
import { Content, ContentTitle, Header, StyleButton, StyleTips } from './style';
|
||||
|
||||
export type TransformWorkspaceToAffineModalProps = {
|
||||
@@ -16,7 +15,6 @@ export const TransformWorkspaceToAffineModal: React.FC<
|
||||
TransformWorkspaceToAffineModalProps
|
||||
> = ({ open, onClose, onConform }) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const user = useCurrentUser();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -41,7 +39,7 @@ export const TransformWorkspaceToAffineModal: React.FC<
|
||||
type="primary"
|
||||
onClick={onConform}
|
||||
>
|
||||
{user ? t['Enable']() : t['Sign in and Enable']()}
|
||||
{t['Sign in and Enable']()}
|
||||
</StyleButton>
|
||||
<StyleButton
|
||||
shape="round"
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
// import { styled } from '@affine/component';
|
||||
// import { FlexWrapper } from '@affine/component';
|
||||
|
||||
import { globalStyle, style, styleVariants } from '@vanilla-extract/css';
|
||||
|
||||
export const container = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
marginTop: '52px',
|
||||
padding: '0 52px 52px 52px',
|
||||
height: 'calc(100vh - 52px)',
|
||||
overflow: 'auto',
|
||||
});
|
||||
|
||||
export const sidebar = style({
|
||||
marginTop: '52px',
|
||||
});
|
||||
|
||||
export const content = style({
|
||||
flex: 1,
|
||||
marginTop: '40px',
|
||||
});
|
||||
|
||||
const baseAvatar = style({
|
||||
position: 'relative',
|
||||
marginRight: '20px',
|
||||
cursor: 'pointer',
|
||||
});
|
||||
|
||||
globalStyle(`${baseAvatar} .camera-icon`, {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
display: 'none',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'rgba(60, 61, 63, 0.5)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
zIndex: 10,
|
||||
});
|
||||
|
||||
globalStyle(`${baseAvatar}:hover .camera-icon`, {
|
||||
display: 'flex',
|
||||
});
|
||||
|
||||
export const avatar = styleVariants({
|
||||
disabled: [
|
||||
baseAvatar,
|
||||
{
|
||||
cursor: 'default',
|
||||
},
|
||||
],
|
||||
enabled: [
|
||||
baseAvatar,
|
||||
{
|
||||
cursor: 'pointer',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const baseTagItem = style({
|
||||
display: 'flex',
|
||||
margin: '0 48px 0 0',
|
||||
height: '34px',
|
||||
fontWeight: '500',
|
||||
fontSize: 'var(--affine-font-h6)',
|
||||
lineHeight: 'var(--affine-line-height)',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.15s ease',
|
||||
});
|
||||
|
||||
export const tagItem = styleVariants({
|
||||
active: [
|
||||
baseTagItem,
|
||||
{
|
||||
color: 'var(--affine-primary-color)',
|
||||
},
|
||||
],
|
||||
inactive: [
|
||||
baseTagItem,
|
||||
{
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export const settingKey = style({
|
||||
width: '140px',
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
fontWeight: 500,
|
||||
marginRight: '56px',
|
||||
flexShrink: 0,
|
||||
});
|
||||
|
||||
export const settingItemLabel = style({
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
fontWeight: 600,
|
||||
flexShrink: 0,
|
||||
});
|
||||
|
||||
export const settingItemLabelHint = style({
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
fontWeight: 400,
|
||||
flexShrink: 0,
|
||||
marginTop: '4px',
|
||||
});
|
||||
|
||||
export const settingsCannotDelete = style([
|
||||
settingItemLabelHint,
|
||||
{
|
||||
color: 'var(--affine-warning-color)',
|
||||
},
|
||||
]);
|
||||
|
||||
export const row = style({
|
||||
padding: '40px 0',
|
||||
display: 'flex',
|
||||
columnGap: '60px',
|
||||
rowGap: '12px',
|
||||
selectors: {
|
||||
'&': {
|
||||
borderBottom: '1px solid var(--affine-border-color)',
|
||||
},
|
||||
'&:first-child': {
|
||||
paddingTop: 0,
|
||||
},
|
||||
},
|
||||
flexWrap: 'wrap',
|
||||
});
|
||||
|
||||
export const col = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
selectors: {
|
||||
[`${row} &:nth-child(1)`]: {
|
||||
flex: '3 0 200px',
|
||||
},
|
||||
[`${row} &:nth-child(2)`]: {
|
||||
flex: '5 0 240px',
|
||||
},
|
||||
[`${row} &:nth-child(3)`]: {
|
||||
flex: '2 0 200px',
|
||||
alignItems: 'flex-end',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const workspaceName = style({
|
||||
fontWeight: '400',
|
||||
fontSize: 'var(--affine-font-h6)',
|
||||
});
|
||||
|
||||
export const indicator = style({
|
||||
height: '2px',
|
||||
background: 'var(--affine-primary-color)',
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
bottom: '0',
|
||||
transition: 'left .3s, width .3s',
|
||||
});
|
||||
|
||||
export const tabButtonWrapper = style({
|
||||
display: 'flex',
|
||||
position: 'sticky',
|
||||
top: '0',
|
||||
background: 'var(--affine-background-primary-color)',
|
||||
zIndex: 1,
|
||||
});
|
||||
|
||||
export const storageTypeWrapper = style({
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
padding: '12px',
|
||||
borderRadius: '10px',
|
||||
gap: '12px',
|
||||
boxShadow: 'var(--affine-shadow-1)',
|
||||
cursor: 'pointer',
|
||||
selectors: {
|
||||
'&:hover': {
|
||||
boxShadow: 'var(--affine-shadow-2)',
|
||||
},
|
||||
'&:not(:last-child)': {
|
||||
marginBottom: '12px',
|
||||
},
|
||||
'&[data-disabled="true"]': {
|
||||
cursor: 'default',
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const storageTypeLabelWrapper = style({
|
||||
flex: 1,
|
||||
});
|
||||
|
||||
export const storageTypeLabel = style({
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
});
|
||||
|
||||
export const storageTypeLabelHint = style({
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
});
|
||||
@@ -1,169 +0,0 @@
|
||||
import type { SettingPanel, WorkspaceRegistry } from '@affine/env/workspace';
|
||||
import { settingPanel, WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { MouseEvent } from 'react';
|
||||
import type React from 'react';
|
||||
import { Suspense, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { preload } from 'swr';
|
||||
|
||||
import { fetcher, QueryKey } from '../../../adapters/affine/fetcher';
|
||||
import { useIsWorkspaceOwner } from '../../../hooks/affine/use-is-workspace-owner';
|
||||
import type { AffineOfficialWorkspace } from '../../../shared';
|
||||
import * as style from './index.css';
|
||||
import { CollaborationPanel } from './panel/collaboration';
|
||||
import { ExportPanel } from './panel/export';
|
||||
import { GeneralPanel } from './panel/general';
|
||||
import { PublishPanel } from './panel/publish';
|
||||
import { SyncPanel } from './panel/sync';
|
||||
|
||||
export type WorkspaceSettingDetailProps = {
|
||||
workspace: AffineOfficialWorkspace;
|
||||
currentTab: SettingPanel;
|
||||
onChangeTab: (tab: SettingPanel) => void;
|
||||
onDeleteWorkspace: () => Promise<void>;
|
||||
onTransferWorkspace: <
|
||||
From extends WorkspaceFlavour,
|
||||
To extends WorkspaceFlavour
|
||||
>(
|
||||
from: From,
|
||||
to: To,
|
||||
workspace: WorkspaceRegistry[From]
|
||||
) => void;
|
||||
};
|
||||
|
||||
export type PanelProps = WorkspaceSettingDetailProps;
|
||||
|
||||
type Name = 'General' | 'Sync' | 'Collaboration' | 'Publish' | 'Export';
|
||||
const panelMap = {
|
||||
[settingPanel.General]: {
|
||||
name: 'General',
|
||||
ui: GeneralPanel,
|
||||
},
|
||||
[settingPanel.Sync]: {
|
||||
name: 'Sync',
|
||||
enable: (flavour: WorkspaceFlavour) => flavour === WorkspaceFlavour.AFFINE,
|
||||
ui: SyncPanel,
|
||||
},
|
||||
[settingPanel.Collaboration]: {
|
||||
name: 'Collaboration',
|
||||
ui: CollaborationPanel,
|
||||
},
|
||||
[settingPanel.Publish]: {
|
||||
name: 'Publish',
|
||||
ui: PublishPanel,
|
||||
},
|
||||
[settingPanel.Export]: {
|
||||
name: 'Export',
|
||||
ui: ExportPanel,
|
||||
},
|
||||
} satisfies {
|
||||
[Key in SettingPanel]: {
|
||||
name: Name;
|
||||
enable?: (flavour: WorkspaceFlavour) => boolean;
|
||||
ui: React.FC<PanelProps>;
|
||||
};
|
||||
};
|
||||
|
||||
function assertInstanceOf<T, U extends T>(
|
||||
obj: T,
|
||||
type: new (...args: any[]) => U
|
||||
): asserts obj is U {
|
||||
if (!(obj instanceof type)) {
|
||||
throw new Error('Object is not instance of type');
|
||||
}
|
||||
}
|
||||
|
||||
export const WorkspaceSettingDetail: React.FC<
|
||||
WorkspaceSettingDetailProps
|
||||
> = props => {
|
||||
const {
|
||||
workspace,
|
||||
currentTab,
|
||||
onChangeTab,
|
||||
// onDeleteWorkspace,
|
||||
// onTransferWorkspace,
|
||||
} = props;
|
||||
const isAffine = workspace.flavour === 'affine';
|
||||
const isOwner = useIsWorkspaceOwner(workspace);
|
||||
if (!(workspace.flavour === 'affine' || workspace.flavour === 'local')) {
|
||||
throw new Error('Unsupported workspace flavour');
|
||||
}
|
||||
if (!(currentTab in panelMap)) {
|
||||
throw new Error('Invalid activeTab: ' + currentTab);
|
||||
}
|
||||
const t = useAFFiNEI18N();
|
||||
const workspaceId = workspace.id;
|
||||
useEffect(() => {
|
||||
if (isAffine && isOwner) {
|
||||
preload([QueryKey.getMembers, workspaceId], fetcher).catch(console.error);
|
||||
}
|
||||
}, [isAffine, isOwner, workspaceId]);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const indicatorRef = useRef<HTMLDivElement | null>(null);
|
||||
const startTransaction = useCallback(() => {
|
||||
if (indicatorRef.current && containerRef.current) {
|
||||
const indicator = indicatorRef.current;
|
||||
const activeTabElement = containerRef.current.querySelector(
|
||||
`[data-tab-key="${currentTab}"]`
|
||||
);
|
||||
assertInstanceOf(activeTabElement, HTMLElement);
|
||||
requestAnimationFrame(() => {
|
||||
indicator.style.left = `${activeTabElement.offsetLeft}px`;
|
||||
indicator.style.width = `${activeTabElement.offsetWidth}px`;
|
||||
});
|
||||
}
|
||||
}, [currentTab]);
|
||||
const handleTabClick = useCallback(
|
||||
(event: MouseEvent<HTMLElement>) => {
|
||||
assertInstanceOf(event.target, HTMLElement);
|
||||
const key = event.target.getAttribute('data-tab-key');
|
||||
if (!key || !(key in panelMap)) {
|
||||
throw new Error('data-tab-key is invalid: ' + key);
|
||||
}
|
||||
onChangeTab(key as SettingPanel);
|
||||
startTransaction();
|
||||
},
|
||||
[onChangeTab, startTransaction]
|
||||
);
|
||||
const Component = useMemo(() => panelMap[currentTab].ui, [currentTab]);
|
||||
return (
|
||||
<div
|
||||
className={style.container}
|
||||
aria-label="workspace-setting-detail"
|
||||
ref={containerRef}
|
||||
>
|
||||
<div className={style.tabButtonWrapper}>
|
||||
{Object.entries(panelMap).map(([key, value]) => {
|
||||
if ('enable' in value && !value.enable(workspace.flavour)) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
style.tagItem[currentTab === key ? 'active' : 'inactive']
|
||||
}
|
||||
key={key}
|
||||
data-tab-key={key}
|
||||
onClick={handleTabClick}
|
||||
>
|
||||
{t[value.name]()}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div
|
||||
className={style.indicator}
|
||||
ref={ref => {
|
||||
indicatorRef.current = ref;
|
||||
startTransaction();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={style.content}>
|
||||
{/* todo: add skeleton */}
|
||||
<Suspense fallback="loading panel...">
|
||||
<Component {...props} key={currentTab} data-tab-ui={currentTab} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,229 +0,0 @@
|
||||
import { Button, IconButton, Menu, MenuItem, Wrapper } from '@affine/component';
|
||||
import { Unreachable } from '@affine/env/constant';
|
||||
import type {
|
||||
AffineLegacyCloudWorkspace,
|
||||
LocalWorkspace,
|
||||
} from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { PermissionType } from '@affine/env/workspace/legacy-cloud';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import {
|
||||
DeleteTemporarilyIcon,
|
||||
EmailIcon,
|
||||
MoreVerticalIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import type React from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { useMembers } from '../../../../../hooks/affine/use-members';
|
||||
import { toast } from '../../../../../utils';
|
||||
import { TmpDisableAffineCloudModal } from '../../../tmp-disable-affine-cloud-modal';
|
||||
import { TransformWorkspaceToAffineModal } from '../../../transform-workspace-to-affine-modal';
|
||||
import type { PanelProps } from '../../index';
|
||||
import { InviteMemberModal } from './invite-member-modal';
|
||||
import {
|
||||
StyledMemberAvatar,
|
||||
StyledMemberButtonContainer,
|
||||
StyledMemberContainer,
|
||||
StyledMemberEmail,
|
||||
StyledMemberInfo,
|
||||
StyledMemberListContainer,
|
||||
StyledMemberListItem,
|
||||
StyledMemberName,
|
||||
StyledMemberNameContainer,
|
||||
StyledMemberRoleContainer,
|
||||
StyledMemberTitleContainer,
|
||||
StyledMoreVerticalButton,
|
||||
StyledMoreVerticalDiv,
|
||||
} from './style';
|
||||
|
||||
const AffineRemoteCollaborationPanel: React.FC<
|
||||
Omit<PanelProps, 'workspace'> & {
|
||||
workspace: AffineLegacyCloudWorkspace;
|
||||
}
|
||||
> = ({ workspace }) => {
|
||||
const [isInviteModalShow, setIsInviteModalShow] = useState(false);
|
||||
const t = useAFFiNEI18N();
|
||||
const { members, removeMember } = useMembers(workspace.id);
|
||||
return (
|
||||
<>
|
||||
<StyledMemberContainer>
|
||||
<ul>
|
||||
<StyledMemberTitleContainer>
|
||||
<StyledMemberNameContainer>
|
||||
{t['Users']()} (
|
||||
<span data-testid="member-length">{members.length}</span>)
|
||||
</StyledMemberNameContainer>
|
||||
<StyledMemberRoleContainer>
|
||||
{t['Access level']()}
|
||||
</StyledMemberRoleContainer>
|
||||
<div style={{ width: '24px', paddingRight: '48px' }}></div>
|
||||
</StyledMemberTitleContainer>
|
||||
</ul>
|
||||
|
||||
<StyledMemberListContainer>
|
||||
{members.length > 0 && (
|
||||
<>
|
||||
{members
|
||||
.sort((b, a) => a.type - b.type)
|
||||
.map((member, index) => {
|
||||
const user = {
|
||||
avatar_url: '',
|
||||
id: '',
|
||||
name: '',
|
||||
...member.user,
|
||||
};
|
||||
return (
|
||||
<StyledMemberListItem key={index}>
|
||||
<StyledMemberNameContainer>
|
||||
<StyledMemberAvatar
|
||||
alt="member avatar"
|
||||
src={user.avatar_url}
|
||||
>
|
||||
<EmailIcon />
|
||||
</StyledMemberAvatar>
|
||||
|
||||
<StyledMemberInfo>
|
||||
<StyledMemberName>{user.name}</StyledMemberName>
|
||||
<StyledMemberEmail>
|
||||
{member.user.email}
|
||||
</StyledMemberEmail>
|
||||
</StyledMemberInfo>
|
||||
</StyledMemberNameContainer>
|
||||
<StyledMemberRoleContainer>
|
||||
{member.accepted
|
||||
? member.type !== PermissionType.Owner
|
||||
? t['Member']()
|
||||
: t['Owner']()
|
||||
: t['Pending']()}
|
||||
</StyledMemberRoleContainer>
|
||||
{member.type === PermissionType.Owner ? (
|
||||
<StyledMoreVerticalDiv />
|
||||
) : (
|
||||
<StyledMoreVerticalButton>
|
||||
<Menu
|
||||
content={
|
||||
<>
|
||||
<MenuItem
|
||||
onClick={async () => {
|
||||
// FIXME: remove ignore
|
||||
|
||||
await removeMember(Number(member.id));
|
||||
toast(
|
||||
t['Member has been removed']({
|
||||
name: user.name,
|
||||
})
|
||||
);
|
||||
}}
|
||||
icon={<DeleteTemporarilyIcon />}
|
||||
>
|
||||
{t['Remove from workspace']()}
|
||||
</MenuItem>
|
||||
</>
|
||||
}
|
||||
placement="bottom"
|
||||
disablePortal={true}
|
||||
trigger="click"
|
||||
>
|
||||
<IconButton>
|
||||
<MoreVerticalIcon />
|
||||
</IconButton>
|
||||
</Menu>
|
||||
</StyledMoreVerticalButton>
|
||||
)}
|
||||
</StyledMemberListItem>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</StyledMemberListContainer>
|
||||
<StyledMemberButtonContainer>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsInviteModalShow(true);
|
||||
}}
|
||||
type="primary"
|
||||
data-testid="invite-members"
|
||||
shape="circle"
|
||||
>
|
||||
{t['Invite Members']()}
|
||||
</Button>
|
||||
</StyledMemberButtonContainer>
|
||||
</StyledMemberContainer>
|
||||
<InviteMemberModal
|
||||
onClose={useCallback(() => {
|
||||
setIsInviteModalShow(false);
|
||||
}, [])}
|
||||
onInviteSuccess={useCallback(() => {
|
||||
setIsInviteModalShow(false);
|
||||
}, [])}
|
||||
workspaceId={workspace.id}
|
||||
open={isInviteModalShow}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const LocalCollaborationPanel: React.FC<
|
||||
Omit<PanelProps, 'workspace'> & {
|
||||
workspace: LocalWorkspace;
|
||||
}
|
||||
> = ({ workspace, onTransferWorkspace }) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<Wrapper marginBottom="42px">{t['Collaboration Description']()}</Wrapper>
|
||||
|
||||
<Button
|
||||
data-testid="local-workspace-enable-cloud-button"
|
||||
type="light"
|
||||
shape="circle"
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
{t['Enable AFFiNE Cloud']()}
|
||||
</Button>
|
||||
{runtimeConfig.enableLegacyCloud ? (
|
||||
<TransformWorkspaceToAffineModal
|
||||
open={open}
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
onConform={() => {
|
||||
onTransferWorkspace(
|
||||
WorkspaceFlavour.LOCAL,
|
||||
WorkspaceFlavour.AFFINE,
|
||||
workspace
|
||||
);
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<TmpDisableAffineCloudModal
|
||||
open={open}
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const CollaborationPanel: React.FC<PanelProps> = props => {
|
||||
switch (props.workspace.flavour) {
|
||||
case WorkspaceFlavour.AFFINE: {
|
||||
const workspace = props.workspace as AffineLegacyCloudWorkspace;
|
||||
return (
|
||||
<AffineRemoteCollaborationPanel {...props} workspace={workspace} />
|
||||
);
|
||||
}
|
||||
case WorkspaceFlavour.LOCAL: {
|
||||
const workspace = props.workspace as LocalWorkspace;
|
||||
return <LocalCollaborationPanel {...props} workspace={workspace} />;
|
||||
}
|
||||
}
|
||||
throw new Unreachable();
|
||||
};
|
||||
@@ -1,218 +0,0 @@
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Modal,
|
||||
ModalCloseButton,
|
||||
ModalWrapper,
|
||||
MuiAvatar,
|
||||
styled,
|
||||
} from '@affine/component';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { EmailIcon } from '@blocksuite/icons';
|
||||
import type React from 'react';
|
||||
import { Suspense, useCallback, useState } from 'react';
|
||||
|
||||
import { useMembers } from '../../../../../../hooks/affine/use-members';
|
||||
import { useUsersByEmail } from '../../../../../../hooks/affine/use-users-by-email';
|
||||
|
||||
interface LoginModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
workspaceId: string;
|
||||
onInviteSuccess: () => void;
|
||||
}
|
||||
|
||||
const gmailReg =
|
||||
/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@(gmail|example)\.(com|org)$/;
|
||||
|
||||
const Result: React.FC<{
|
||||
workspaceId: string;
|
||||
queryEmail: string;
|
||||
}> = ({ workspaceId, queryEmail }) => {
|
||||
const users = useUsersByEmail(workspaceId, queryEmail);
|
||||
const firstUser = users?.at(0) ?? null;
|
||||
if (!firstUser || !firstUser.email) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Members>
|
||||
<Member>
|
||||
{firstUser.avatar_url ? (
|
||||
<MuiAvatar src={firstUser.avatar_url}></MuiAvatar>
|
||||
) : (
|
||||
<MemberIcon>
|
||||
<EmailIcon></EmailIcon>
|
||||
</MemberIcon>
|
||||
)}
|
||||
<Email>{firstUser.email}</Email>
|
||||
{/* <div>invited</div> */}
|
||||
</Member>
|
||||
</Members>
|
||||
);
|
||||
};
|
||||
|
||||
export const InviteMemberModal = ({
|
||||
open,
|
||||
onClose,
|
||||
onInviteSuccess,
|
||||
workspaceId,
|
||||
}: LoginModalProps) => {
|
||||
const { inviteMember } = useMembers(workspaceId);
|
||||
const [email, setEmail] = useState<string>('');
|
||||
const [showMemberPreview, setShowMemberPreview] = useState(false);
|
||||
const t = useAFFiNEI18N();
|
||||
const inputChange = useCallback((value: string) => {
|
||||
setEmail(value);
|
||||
}, []);
|
||||
return (
|
||||
<div>
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<ModalWrapper width={460} height={236}>
|
||||
<Header>
|
||||
<ModalCloseButton
|
||||
onClick={() => {
|
||||
onClose();
|
||||
setEmail('');
|
||||
}}
|
||||
/>
|
||||
</Header>
|
||||
<Content>
|
||||
<ContentTitle>{t['Invite Members']()}</ContentTitle>
|
||||
<InviteBox>
|
||||
<Input
|
||||
data-testid="invite-member-input"
|
||||
width={360}
|
||||
value={email}
|
||||
onChange={inputChange}
|
||||
onFocus={useCallback(() => {
|
||||
setShowMemberPreview(true);
|
||||
}, [])}
|
||||
onBlur={useCallback(() => {
|
||||
setShowMemberPreview(false);
|
||||
}, [])}
|
||||
placeholder={t['Invite placeholder']()}
|
||||
/>
|
||||
{showMemberPreview && gmailReg.test(email) && (
|
||||
<Suspense fallback="loading...">
|
||||
<Result workspaceId={workspaceId} queryEmail={email} />
|
||||
</Suspense>
|
||||
)}
|
||||
</InviteBox>
|
||||
</Content>
|
||||
<Footer>
|
||||
<Button
|
||||
data-testid="invite-member-button"
|
||||
disabled={!gmailReg.test(email)}
|
||||
shape="circle"
|
||||
type="primary"
|
||||
style={{ width: '364px', height: '38px', borderRadius: '40px' }}
|
||||
onClick={async () => {
|
||||
await inviteMember(email);
|
||||
setEmail('');
|
||||
onInviteSuccess();
|
||||
}}
|
||||
>
|
||||
{t['Invite']()}
|
||||
</Button>
|
||||
</Footer>
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Header = styled('div')({
|
||||
position: 'relative',
|
||||
height: '44px',
|
||||
});
|
||||
|
||||
const Content = styled('div')({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
const ContentTitle = styled('h1')({
|
||||
fontSize: '20px',
|
||||
lineHeight: '28px',
|
||||
fontWeight: 600,
|
||||
textAlign: 'center',
|
||||
paddingBottom: '16px',
|
||||
});
|
||||
|
||||
const Footer = styled('div')({
|
||||
height: '102px',
|
||||
margin: '32px 0',
|
||||
textAlign: 'center',
|
||||
});
|
||||
|
||||
const InviteBox = styled('div')({
|
||||
position: 'relative',
|
||||
});
|
||||
|
||||
const Members = styled('div')(() => {
|
||||
return {
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
background: 'var(--affine-background-primary-color)',
|
||||
textAlign: 'left',
|
||||
zIndex: 1,
|
||||
borderRadius: '0px 10px 10px 10px',
|
||||
height: '56px',
|
||||
padding: '8px 12px',
|
||||
input: {
|
||||
'&::placeholder': {
|
||||
color: 'var(--affine-placeholder-color)',
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// const NoFind = styled('div')(({ theme }) => {
|
||||
// return {
|
||||
// color: 'var(--affine-icon-color)',
|
||||
// fontSize: 'var(--affine-font-sm)',
|
||||
// lineHeight: '40px',
|
||||
// userSelect: 'none',
|
||||
// width: '100%',
|
||||
// };
|
||||
// });
|
||||
|
||||
const Member = styled('div')(() => {
|
||||
return {
|
||||
color: 'var(--affine-icon-color)',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
lineHeight: '40px',
|
||||
userSelect: 'none',
|
||||
display: 'flex',
|
||||
};
|
||||
});
|
||||
|
||||
const MemberIcon = styled('div')(() => {
|
||||
return {
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
borderRadius: '50%',
|
||||
color: 'var(--affine-primary-color)',
|
||||
background: '#F5F5F5',
|
||||
textAlign: 'center',
|
||||
lineHeight: '45px',
|
||||
// icon size
|
||||
fontSize: '20px',
|
||||
overflow: 'hidden',
|
||||
img: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const Email = styled('div')(() => {
|
||||
return {
|
||||
flex: '1',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
marginLeft: '8px',
|
||||
};
|
||||
});
|
||||
@@ -1,101 +0,0 @@
|
||||
import { styled } from '@affine/component';
|
||||
import { MuiAvatar } from '@affine/component';
|
||||
|
||||
export const StyledMemberTitleContainer = styled('li')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
fontWeight: '500',
|
||||
marginBottom: '42px',
|
||||
flex: 1,
|
||||
};
|
||||
});
|
||||
export const StyledMemberContainer = styled('div')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
height: '100%',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberAvatar = styled(MuiAvatar)(() => {
|
||||
return { height: '40px', width: '40px' };
|
||||
});
|
||||
|
||||
export const StyledMemberNameContainer = styled('div')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flex: '2 0 402px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberRoleContainer = styled('div')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flex: '1 0 222px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberListContainer = styled('ul')(() => {
|
||||
return {
|
||||
overflowY: 'scroll',
|
||||
flexGrow: 1,
|
||||
paddingBottom: '58px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberListItem = styled('li')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
height: '72px',
|
||||
width: '100%',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberInfo = styled('div')(() => {
|
||||
return {
|
||||
paddingLeft: '12px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberName = styled('div')(() => {
|
||||
return {
|
||||
fontWeight: '400',
|
||||
fontSize: '18px',
|
||||
lineHeight: '26px',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberEmail = styled('div')(() => {
|
||||
return {
|
||||
fontWeight: '400',
|
||||
fontSize: '16px',
|
||||
lineHeight: '22px',
|
||||
color: 'var(--affine-icon-color)',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberButtonContainer = styled('div')(() => {
|
||||
return {
|
||||
position: 'fixed',
|
||||
bottom: '20px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMoreVerticalDiv = styled('div')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
cursor: 'pointer',
|
||||
paddingRight: '48px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMoreVerticalButton = styled(StyledMoreVerticalDiv)``;
|
||||
@@ -1,35 +0,0 @@
|
||||
import { Button, toast, Wrapper } from '@affine/component';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { rootCurrentWorkspaceIdAtom } from '@affine/workspace/atom';
|
||||
import { useAtomValue } from 'jotai';
|
||||
|
||||
export const ExportPanel = () => {
|
||||
const id = useAtomValue(rootCurrentWorkspaceIdAtom);
|
||||
const t = useAFFiNEI18N();
|
||||
return (
|
||||
<>
|
||||
<Wrapper marginBottom="42px"> {t['Export Description']()}</Wrapper>
|
||||
<Button
|
||||
type="light"
|
||||
shape="circle"
|
||||
disabled={
|
||||
!environment.isDesktop || !id || !runtimeConfig.enableSQLiteProvider
|
||||
}
|
||||
data-testid="export-affine-backup"
|
||||
onClick={async () => {
|
||||
if (id) {
|
||||
const result = await window.apis?.dialog.saveDBFileAs(id);
|
||||
if (result?.error) {
|
||||
// @ts-expect-error: result.error is dynamic
|
||||
toast(t[result.error]());
|
||||
} else if (!result?.canceled) {
|
||||
toast(t['Export success']());
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t['Export AFFiNE backup file']()}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,112 +0,0 @@
|
||||
import { Button, Input, Modal, ModalCloseButton } from '@affine/component';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import type { AffineOfficialWorkspace } from '../../../../../../shared';
|
||||
import { toast } from '../../../../../../utils';
|
||||
import {
|
||||
StyledButtonContent,
|
||||
StyledInputContent,
|
||||
StyledModalHeader,
|
||||
StyledModalWrapper,
|
||||
StyledTextContent,
|
||||
StyledWorkspaceName,
|
||||
} from './style';
|
||||
|
||||
interface WorkspaceDeleteProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
workspace: AffineOfficialWorkspace;
|
||||
onDeleteWorkspace: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const WorkspaceDeleteModal = ({
|
||||
open,
|
||||
onClose,
|
||||
workspace,
|
||||
onDeleteWorkspace,
|
||||
}: WorkspaceDeleteProps) => {
|
||||
const [workspaceName] = useBlockSuiteWorkspaceName(
|
||||
workspace.blockSuiteWorkspace ?? null
|
||||
);
|
||||
const [deleteStr, setDeleteStr] = useState<string>('');
|
||||
const allowDelete = deleteStr === workspaceName;
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
onDeleteWorkspace()
|
||||
.then(() => {
|
||||
toast(t['Successfully deleted'](), {
|
||||
portal: document.body,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// ignore error
|
||||
});
|
||||
}, [onDeleteWorkspace, t]);
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<StyledModalWrapper>
|
||||
<ModalCloseButton onClick={onClose} />
|
||||
<StyledModalHeader>{t['Delete Workspace']()}?</StyledModalHeader>
|
||||
{workspace.flavour === WorkspaceFlavour.LOCAL ? (
|
||||
<StyledTextContent>
|
||||
<Trans i18nKey="Delete Workspace Description">
|
||||
Deleting (
|
||||
<StyledWorkspaceName>
|
||||
{{ workspace: workspaceName } as any}
|
||||
</StyledWorkspaceName>
|
||||
) cannot be undone, please proceed with caution. All contents will
|
||||
be lost.
|
||||
</Trans>
|
||||
</StyledTextContent>
|
||||
) : (
|
||||
<StyledTextContent>
|
||||
<Trans i18nKey="Delete Workspace Description2">
|
||||
Deleting (
|
||||
<StyledWorkspaceName>
|
||||
{{ workspace: workspaceName } as any}
|
||||
</StyledWorkspaceName>
|
||||
) will delete both local and cloud data, this operation cannot be
|
||||
undone, please proceed with caution.
|
||||
</Trans>
|
||||
</StyledTextContent>
|
||||
)}
|
||||
<StyledInputContent>
|
||||
<Input
|
||||
ref={ref => {
|
||||
if (ref) {
|
||||
setTimeout(() => ref.focus(), 0);
|
||||
}
|
||||
}}
|
||||
onChange={setDeleteStr}
|
||||
data-testid="delete-workspace-input"
|
||||
placeholder={t['Placeholder of delete workspace']()}
|
||||
value={deleteStr}
|
||||
width={315}
|
||||
height={42}
|
||||
/>
|
||||
</StyledInputContent>
|
||||
<StyledButtonContent>
|
||||
<Button shape="circle" onClick={onClose}>
|
||||
{t['Cancel']()}
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="delete-workspace-confirm-button"
|
||||
disabled={!allowDelete}
|
||||
onClick={handleDelete}
|
||||
type="danger"
|
||||
shape="circle"
|
||||
style={{ marginLeft: '24px' }}
|
||||
>
|
||||
{t['Delete']()}
|
||||
</Button>
|
||||
</StyledButtonContent>
|
||||
</StyledModalWrapper>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -1,76 +0,0 @@
|
||||
import { styled } from '@affine/component';
|
||||
|
||||
export const StyledModalWrapper = styled('div')(() => {
|
||||
return {
|
||||
position: 'relative',
|
||||
padding: '0px',
|
||||
width: '560px',
|
||||
background: 'var(--affine-white)',
|
||||
borderRadius: '12px',
|
||||
// height: '312px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledModalHeader = styled('div')(() => {
|
||||
return {
|
||||
margin: '44px 0px 12px 0px',
|
||||
width: '560px',
|
||||
fontWeight: '600',
|
||||
fontSize: '20px;',
|
||||
textAlign: 'center',
|
||||
};
|
||||
});
|
||||
|
||||
// export const StyledModalContent = styled('div')(({ theme }) => {});
|
||||
|
||||
export const StyledTextContent = styled('div')(() => {
|
||||
return {
|
||||
margin: 'auto',
|
||||
width: '425px',
|
||||
fontFamily: 'Avenir Next',
|
||||
fontStyle: 'normal',
|
||||
fontWeight: '400',
|
||||
fontSize: '18px',
|
||||
lineHeight: '26px',
|
||||
textAlign: 'left',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledInputContent = styled('div')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
margin: '24px 0',
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledButtonContent = styled('div')(() => {
|
||||
return {
|
||||
marginBottom: '42px',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledWorkspaceName = styled('span')(() => {
|
||||
return {
|
||||
fontWeight: '600',
|
||||
};
|
||||
});
|
||||
|
||||
// export const StyledCancelButton = styled(Button)(({ theme }) => {
|
||||
// return {
|
||||
// width: '100px',
|
||||
// justifyContent: 'center',
|
||||
// };
|
||||
// });
|
||||
|
||||
// export const StyledDeleteButton = styled(Button)(({ theme }) => {
|
||||
// return {
|
||||
// width: '100px',
|
||||
// justifyContent: 'center',
|
||||
// };
|
||||
// });
|
||||
@@ -1,18 +0,0 @@
|
||||
export const CameraIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10.6236 4.25001C10.635 4.25001 10.6467 4.25002 10.6584 4.25002H13.3416C13.3533 4.25002 13.365 4.25001 13.3764 4.25001C13.5609 4.24995 13.7105 4.2499 13.8543 4.26611C14.5981 4.34997 15.2693 4.75627 15.6826 5.38026C15.7624 5.50084 15.83 5.63398 15.9121 5.79586C15.9173 5.80613 15.9226 5.81652 15.9279 5.82703C15.9538 5.87792 15.9679 5.90562 15.9789 5.9261C15.9832 5.9341 15.9857 5.93861 15.9869 5.94065C16.0076 5.97069 16.0435 5.99406 16.0878 5.99905L16.0849 5.99877C16.0849 5.99877 16.0907 5.99918 16.1047 5.99947C16.1286 5.99998 16.1604 6.00002 16.2181 6.00002L17.185 6.00001C17.6577 6 18.0566 5.99999 18.3833 6.02627C18.7252 6.05377 19.0531 6.11364 19.3656 6.27035C19.8402 6.50842 20.2283 6.88944 20.4723 7.36077C20.6336 7.67233 20.6951 7.99944 20.7232 8.33858C20.75 8.66166 20.75 9.05554 20.75 9.51992V16.2301C20.75 16.6945 20.75 17.0884 20.7232 17.4114C20.6951 17.7506 20.6336 18.0777 20.4723 18.3893C20.2283 18.8606 19.8402 19.2416 19.3656 19.4797C19.0531 19.6364 18.7252 19.6963 18.3833 19.7238C18.0566 19.75 17.6578 19.75 17.185 19.75H6.81497C6.34225 19.75 5.9434 19.75 5.61668 19.7238C5.27477 19.6963 4.94688 19.6364 4.63444 19.4797C4.15978 19.2416 3.77167 18.8606 3.52771 18.3893C3.36644 18.0777 3.30494 17.7506 3.27679 17.4114C3.24998 17.0884 3.24999 16.6945 3.25 16.2302V9.51987C3.24999 9.05551 3.24998 8.66164 3.27679 8.33858C3.30494 7.99944 3.36644 7.67233 3.52771 7.36077C3.77167 6.88944 4.15978 6.50842 4.63444 6.27035C4.94688 6.11364 5.27477 6.05377 5.61668 6.02627C5.9434 5.99999 6.34225 6 6.81498 6.00001L7.78191 6.00002C7.83959 6.00002 7.87142 5.99998 7.8953 5.99947C7.90607 5.99924 7.91176 5.99897 7.91398 5.99884C7.95747 5.99343 7.99267 5.9703 8.01312 5.94066C8.01429 5.93863 8.01684 5.93412 8.02113 5.9261C8.0321 5.90561 8.04622 5.87791 8.07206 5.82703C8.07739 5.81653 8.08266 5.80615 8.08787 5.79588C8.17004 5.63397 8.23759 5.50086 8.31745 5.38026C8.73067 4.75627 9.40192 4.34997 10.1457 4.26611C10.2895 4.2499 10.4391 4.24995 10.6236 4.25001ZM10.6584 5.75002C10.422 5.75002 10.3627 5.75114 10.3138 5.75666C10.0055 5.79142 9.73316 5.95919 9.56809 6.20845C9.54218 6.24758 9.51544 6.29761 9.40943 6.50633C9.40611 6.51287 9.40274 6.5195 9.39934 6.52622C9.36115 6.60161 9.31758 6.68761 9.26505 6.76694C8.9964 7.17261 8.56105 7.4354 8.08026 7.48961C7.98625 7.50021 7.89021 7.50011 7.80434 7.50003C7.79678 7.50002 7.7893 7.50002 7.78191 7.50002H6.84445C6.33444 7.50002 5.99634 7.50058 5.73693 7.52144C5.48594 7.54163 5.37478 7.57713 5.30693 7.61115C5.11257 7.70864 4.95675 7.86306 4.85983 8.05029C4.82733 8.11308 4.79194 8.21816 4.77165 8.46266C4.7506 8.71626 4.75 9.0474 4.75 9.55001V16.2C4.75 16.7026 4.7506 17.0338 4.77165 17.2874C4.79194 17.5319 4.82733 17.6369 4.85983 17.6997C4.95675 17.887 5.11257 18.0414 5.30693 18.1389C5.37478 18.1729 5.48594 18.2084 5.73693 18.2286C5.99634 18.2494 6.33444 18.25 6.84445 18.25H17.1556C17.6656 18.25 18.0037 18.2494 18.2631 18.2286C18.5141 18.2084 18.6252 18.1729 18.6931 18.1389C18.8874 18.0414 19.0433 17.887 19.1402 17.6997C19.1727 17.6369 19.2081 17.5319 19.2283 17.2874C19.2494 17.0338 19.25 16.7026 19.25 16.2V9.55001C19.25 9.0474 19.2494 8.71626 19.2283 8.46266C19.2081 8.21816 19.1727 8.11308 19.1402 8.05029C19.0433 7.86306 18.8874 7.70864 18.6931 7.61115C18.6252 7.57713 18.5141 7.54163 18.2631 7.52144C18.0037 7.50058 17.6656 7.50002 17.1556 7.50002H16.2181C16.2107 7.50002 16.2032 7.50002 16.1957 7.50003C16.1098 7.50011 16.0138 7.50021 15.9197 7.48961C15.4389 7.4354 15.0036 7.17261 14.735 6.76694C14.6824 6.68761 14.6389 6.60163 14.6007 6.52622C14.5973 6.5195 14.5939 6.51287 14.5906 6.50633C14.4846 6.29763 14.4578 6.24758 14.4319 6.20846C14.2668 5.95919 13.9945 5.79142 13.6862 5.75666C13.6373 5.75114 13.578 5.75002 13.3416 5.75002H10.6584ZM12 11C10.9303 11 10.0833 11.8506 10.0833 12.875C10.0833 13.8995 10.9303 14.75 12 14.75C13.0697 14.75 13.9167 13.8995 13.9167 12.875C13.9167 11.8506 13.0697 11 12 11ZM8.58333 12.875C8.58333 11 10.1242 9.50002 12 9.50002C13.8758 9.50002 15.4167 11 15.4167 12.875C15.4167 14.7501 13.8758 16.25 12 16.25C10.1242 16.25 8.58333 14.7501 8.58333 12.875Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,283 +0,0 @@
|
||||
import { Button, toast } from '@affine/component';
|
||||
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import {
|
||||
ArrowRightSmallIcon,
|
||||
DeleteIcon,
|
||||
FolderIcon,
|
||||
MoveToIcon,
|
||||
SaveIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
|
||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||
import clsx from 'clsx';
|
||||
import type React from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { useIsWorkspaceOwner } from '../../../../../hooks/affine/use-is-workspace-owner';
|
||||
import { Upload } from '../../../../pure/file-upload';
|
||||
import type { PanelProps } from '../../index';
|
||||
import * as style from '../../index.css';
|
||||
import { WorkspaceDeleteModal } from './delete';
|
||||
import { CameraIcon } from './icons';
|
||||
import { WorkspaceLeave } from './leave';
|
||||
import { StyledInput } from './style';
|
||||
|
||||
const useShowOpenDBFile = (workspaceId: string) => {
|
||||
const [show, setShow] = useState(false);
|
||||
useEffect(() => {
|
||||
if (
|
||||
window.apis &&
|
||||
window.events &&
|
||||
environment.isDesktop &&
|
||||
runtimeConfig.enableSQLiteProvider
|
||||
) {
|
||||
window.apis.workspace
|
||||
.getMeta(workspaceId)
|
||||
.then(meta => {
|
||||
setShow(!!meta.secondaryDBPath);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
return window.events.workspace.onMetaChange((newMeta: any) => {
|
||||
if (newMeta.workspaceId === workspaceId) {
|
||||
const meta = newMeta.meta;
|
||||
setShow(!!meta.secondaryDBPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [workspaceId]);
|
||||
return show;
|
||||
};
|
||||
|
||||
export const GeneralPanel: React.FC<PanelProps> = ({
|
||||
workspace,
|
||||
onDeleteWorkspace,
|
||||
}) => {
|
||||
const [showDelete, setShowDelete] = useState<boolean>(false);
|
||||
const [showLeave, setShowLeave] = useState<boolean>(false);
|
||||
const [name, setName] = useBlockSuiteWorkspaceName(
|
||||
workspace.blockSuiteWorkspace
|
||||
);
|
||||
const [input, setInput] = useState<string>(name);
|
||||
const isOwner = useIsWorkspaceOwner(workspace);
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const handleUpdateWorkspaceName = (name: string) => {
|
||||
setName(name);
|
||||
toast(t['Update workspace name success']());
|
||||
};
|
||||
|
||||
const [, update] = useBlockSuiteWorkspaceAvatarUrl(
|
||||
workspace.blockSuiteWorkspace
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div data-testid="avatar-row" className={style.row}>
|
||||
<div className={style.col}>
|
||||
<div className={style.settingItemLabel}>
|
||||
{t['Workspace Avatar']()}
|
||||
</div>
|
||||
<div className={style.settingItemLabelHint}>
|
||||
{t['Change avatar hint']()}
|
||||
</div>
|
||||
</div>
|
||||
<div className={clsx(style.col)}>
|
||||
<div className={style.avatar[isOwner ? 'enabled' : 'disabled']}>
|
||||
{isOwner ? (
|
||||
<Upload
|
||||
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
|
||||
fileChange={update}
|
||||
data-testid="upload-avatar"
|
||||
>
|
||||
<>
|
||||
<div className="camera-icon">
|
||||
<CameraIcon></CameraIcon>
|
||||
</div>
|
||||
<WorkspaceAvatar size={72} workspace={workspace} />
|
||||
</>
|
||||
</Upload>
|
||||
) : (
|
||||
<WorkspaceAvatar size={72} workspace={workspace} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={clsx(style.col)}></div>
|
||||
</div>
|
||||
|
||||
<div data-testid="workspace-name-row" className={style.row}>
|
||||
<div className={style.col}>
|
||||
<div className={style.settingItemLabel}>{t['Workspace Name']()}</div>
|
||||
<div className={style.settingItemLabelHint}>
|
||||
{t['Change workspace name hint']()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={style.col}>
|
||||
<StyledInput
|
||||
height={38}
|
||||
value={input}
|
||||
data-testid="workspace-name-input"
|
||||
placeholder={t['Workspace Name']()}
|
||||
maxLength={64}
|
||||
minLength={0}
|
||||
onChange={setInput}
|
||||
></StyledInput>
|
||||
</div>
|
||||
|
||||
<div className={style.col}>
|
||||
<Button
|
||||
type="light"
|
||||
size="middle"
|
||||
data-testid="save-workspace-name"
|
||||
icon={<SaveIcon />}
|
||||
disabled={input === workspace.blockSuiteWorkspace.meta.name}
|
||||
onClick={() => {
|
||||
handleUpdateWorkspaceName(input);
|
||||
}}
|
||||
>
|
||||
{t['Save']()}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{environment.isDesktop && runtimeConfig.enableSQLiteProvider ? (
|
||||
<DesktopClientOnly workspaceId={workspace.id} />
|
||||
) : null}
|
||||
<div className={style.row}>
|
||||
<div className={style.col}>
|
||||
<div className={style.settingItemLabel}>
|
||||
{t['Delete Workspace']()}
|
||||
</div>
|
||||
<div className={style.settingItemLabelHint}>
|
||||
{t['Delete Workspace Label Hint']()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={style.col}></div>
|
||||
<div className={style.col}>
|
||||
{isOwner ? (
|
||||
<>
|
||||
<Button
|
||||
type="warning"
|
||||
data-testid="delete-workspace-button"
|
||||
size="middle"
|
||||
icon={<DeleteIcon />}
|
||||
onClick={() => {
|
||||
setShowDelete(true);
|
||||
}}
|
||||
>
|
||||
{t['Delete']()}
|
||||
</Button>
|
||||
<WorkspaceDeleteModal
|
||||
onDeleteWorkspace={onDeleteWorkspace}
|
||||
open={showDelete}
|
||||
onClose={() => {
|
||||
setShowDelete(false);
|
||||
}}
|
||||
workspace={workspace}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
type="warning"
|
||||
size="middle"
|
||||
onClick={() => {
|
||||
setShowLeave(true);
|
||||
}}
|
||||
>
|
||||
{t['Leave']()}
|
||||
</Button>
|
||||
<WorkspaceLeave
|
||||
open={showLeave}
|
||||
onClose={() => {
|
||||
setShowLeave(false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function DesktopClientOnly({ workspaceId }: { workspaceId: string }) {
|
||||
const t = useAFFiNEI18N();
|
||||
const showOpenFolder = useShowOpenDBFile(workspaceId);
|
||||
const onRevealDBFile = useCallback(() => {
|
||||
if (environment.isDesktop && runtimeConfig.enableSQLiteProvider) {
|
||||
window.apis?.dialog.revealDBFile(workspaceId).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
}, [workspaceId]);
|
||||
const [moveToInProgress, setMoveToInProgress] = useState<boolean>(false);
|
||||
const handleMoveTo = useCallback(() => {
|
||||
if (moveToInProgress) {
|
||||
return;
|
||||
}
|
||||
setMoveToInProgress(true);
|
||||
window.apis?.dialog
|
||||
.moveDBFile(workspaceId)
|
||||
.then(result => {
|
||||
if (!result?.error && !result?.canceled) {
|
||||
toast(t['Move folder success']());
|
||||
} else if (result?.error) {
|
||||
// @ts-expect-error: result.error is dynamic
|
||||
toast(t[result.error]());
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
toast(t['UNKNOWN_ERROR']());
|
||||
})
|
||||
.finally(() => {
|
||||
setMoveToInProgress(false);
|
||||
});
|
||||
}, [moveToInProgress, t, workspaceId]);
|
||||
const openFolderNode = showOpenFolder ? (
|
||||
<div className={style.storageTypeWrapper} onClick={onRevealDBFile}>
|
||||
<FolderIcon color="var(--affine-primary-color)" />
|
||||
<div className={style.storageTypeLabelWrapper}>
|
||||
<div className={style.storageTypeLabel}>{t['Open folder']()}</div>
|
||||
<div className={style.storageTypeLabelHint}>
|
||||
{t['Open folder hint']()}
|
||||
</div>
|
||||
</div>
|
||||
<ArrowRightSmallIcon color="var(--affine-primary-color)" />
|
||||
</div>
|
||||
) : null;
|
||||
return (
|
||||
<div className={style.row}>
|
||||
<div className={style.col}>
|
||||
<div className={style.settingItemLabel}>{t['Storage Folder']()}</div>
|
||||
<div className={style.settingItemLabelHint}>
|
||||
{t['Storage Folder Hint']()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={style.col}>
|
||||
{openFolderNode}
|
||||
|
||||
<div
|
||||
data-testid="move-folder"
|
||||
data-disabled={moveToInProgress}
|
||||
className={style.storageTypeWrapper}
|
||||
onClick={handleMoveTo}
|
||||
>
|
||||
<MoveToIcon color="var(--affine-primary-color)" />
|
||||
<div className={style.storageTypeLabelWrapper}>
|
||||
<div className={style.storageTypeLabel}>{t['Move folder']()}</div>
|
||||
<div className={style.storageTypeLabelHint}>
|
||||
{t['Move folder hint']()}
|
||||
</div>
|
||||
</div>
|
||||
<ArrowRightSmallIcon color="var(--affine-primary-color)" />
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.col}></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { Modal } from '@affine/component';
|
||||
import { ModalCloseButton } from '@affine/component';
|
||||
import { Button } from '@affine/component';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
|
||||
import {
|
||||
StyledButtonContent,
|
||||
StyledModalHeader,
|
||||
StyledModalWrapper,
|
||||
StyledTextContent,
|
||||
} from './style';
|
||||
|
||||
interface WorkspaceDeleteProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const WorkspaceLeave = ({ open, onClose }: WorkspaceDeleteProps) => {
|
||||
// const { leaveWorkSpace } = useWorkspaceHelper();
|
||||
const t = useAFFiNEI18N();
|
||||
const handleLeave = async () => {
|
||||
// await leaveWorkSpace();
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<StyledModalWrapper>
|
||||
<ModalCloseButton onClick={onClose} />
|
||||
<StyledModalHeader>{t['Leave Workspace']()}</StyledModalHeader>
|
||||
<StyledTextContent>
|
||||
{t['Leave Workspace Description']()}
|
||||
</StyledTextContent>
|
||||
<StyledButtonContent>
|
||||
<Button shape="circle" onClick={onClose}>
|
||||
{t['Cancel']()}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleLeave}
|
||||
type="danger"
|
||||
shape="circle"
|
||||
style={{ marginLeft: '24px' }}
|
||||
>
|
||||
{t['Leave']()}
|
||||
</Button>
|
||||
</StyledButtonContent>
|
||||
</StyledModalWrapper>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
import { styled } from '@affine/component';
|
||||
|
||||
export const StyledModalWrapper = styled('div')(() => {
|
||||
return {
|
||||
position: 'relative',
|
||||
padding: '0px',
|
||||
width: '460px',
|
||||
background: 'var(--affine-white)',
|
||||
borderRadius: '12px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledModalHeader = styled('div')(() => {
|
||||
return {
|
||||
margin: '44px 0px 12px 0px',
|
||||
width: '460px',
|
||||
fontWeight: '600',
|
||||
fontSize: '20px;',
|
||||
textAlign: 'center',
|
||||
};
|
||||
});
|
||||
|
||||
// export const StyledModalContent = styled('div')(({ theme }) => {});
|
||||
|
||||
export const StyledTextContent = styled('div')(() => {
|
||||
return {
|
||||
margin: 'auto',
|
||||
width: '425px',
|
||||
fontFamily: 'Avenir Next',
|
||||
fontStyle: 'normal',
|
||||
fontWeight: '400',
|
||||
fontSize: '18px',
|
||||
lineHeight: '26px',
|
||||
textAlign: 'center',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledButtonContent = styled('div')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
margin: '0px 0 32px 0',
|
||||
};
|
||||
});
|
||||
@@ -1,51 +0,0 @@
|
||||
import { displayFlex, styled } from '@affine/component';
|
||||
import { Input } from '@affine/component';
|
||||
|
||||
export const StyledInput = Input;
|
||||
|
||||
export const StyledWorkspaceInfo = styled('div')(() => {
|
||||
return {
|
||||
...displayFlex('flex-start', 'center'),
|
||||
fontSize: '20px',
|
||||
span: {
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
marginLeft: '15px',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledAvatar = styled('div')(
|
||||
({ disabled }: { disabled: boolean }) => {
|
||||
return {
|
||||
position: 'relative',
|
||||
marginRight: '20px',
|
||||
cursor: disabled ? 'default' : 'pointer',
|
||||
':hover': {
|
||||
'.camera-icon': {
|
||||
display: 'flex',
|
||||
},
|
||||
},
|
||||
'.camera-icon': {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
display: 'none',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'rgba(60, 61, 63, 0.5)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
zIndex: 10,
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export const StyledEditButton = styled('div')(() => {
|
||||
return {
|
||||
color: 'var(--affine-primary-color)',
|
||||
cursor: 'pointer',
|
||||
marginLeft: '36px',
|
||||
};
|
||||
});
|
||||
@@ -1,171 +0,0 @@
|
||||
import {
|
||||
Button,
|
||||
Content,
|
||||
FlexWrapper,
|
||||
Input,
|
||||
Wrapper,
|
||||
} from '@affine/component';
|
||||
import { isBrowser, Unreachable } from '@affine/env/constant';
|
||||
import type {
|
||||
AffineLegacyCloudWorkspace,
|
||||
LocalWorkspace,
|
||||
} from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Box } from '@mui/material';
|
||||
import type React from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { useToggleWorkspacePublish } from '../../../../../hooks/affine/use-toggle-workspace-publish';
|
||||
import type { AffineOfficialWorkspace } from '../../../../../shared';
|
||||
import { toast } from '../../../../../utils';
|
||||
import { EnableAffineCloudModal } from '../../../enable-affine-cloud-modal';
|
||||
import { TmpDisableAffineCloudModal } from '../../../tmp-disable-affine-cloud-modal';
|
||||
import type { WorkspaceSettingDetailProps } from '../../index';
|
||||
|
||||
export type PublishPanelProps = WorkspaceSettingDetailProps & {
|
||||
workspace: AffineOfficialWorkspace;
|
||||
};
|
||||
|
||||
export type PublishPanelAffineProps = WorkspaceSettingDetailProps & {
|
||||
workspace: AffineLegacyCloudWorkspace;
|
||||
};
|
||||
|
||||
const PublishPanelAffine: React.FC<PublishPanelAffineProps> = ({
|
||||
workspace,
|
||||
}) => {
|
||||
const [origin, setOrigin] = useState('');
|
||||
useEffect(() => {
|
||||
setOrigin(
|
||||
isBrowser && window.location.origin ? window.location.origin : ''
|
||||
);
|
||||
}, []);
|
||||
const shareUrl = origin + '/public-workspace/' + workspace.id;
|
||||
const t = useAFFiNEI18N();
|
||||
const publishWorkspace = useToggleWorkspacePublish(workspace);
|
||||
const copyUrl = useCallback(async () => {
|
||||
await navigator.clipboard.writeText(shareUrl);
|
||||
toast(t['Copied link to clipboard']());
|
||||
}, [shareUrl, t]);
|
||||
|
||||
if (workspace.public) {
|
||||
return (
|
||||
<>
|
||||
<Wrapper marginBottom="42px">{t['Published Description']()}</Wrapper>
|
||||
|
||||
<Wrapper marginBottom="12px">
|
||||
<Content weight="500">{t['Share with link']()}</Content>
|
||||
</Wrapper>
|
||||
<FlexWrapper>
|
||||
<Input
|
||||
data-testid="share-url"
|
||||
width={582}
|
||||
value={shareUrl}
|
||||
disabled={true}
|
||||
></Input>
|
||||
<Button
|
||||
onClick={copyUrl}
|
||||
type="light"
|
||||
shape="circle"
|
||||
style={{ marginLeft: '24px' }}
|
||||
>
|
||||
{t['Copy Link']()}
|
||||
</Button>
|
||||
</FlexWrapper>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
await publishWorkspace(false);
|
||||
}}
|
||||
loading={false}
|
||||
type="danger"
|
||||
shape="circle"
|
||||
style={{ marginTop: '38px' }}
|
||||
>
|
||||
{t['Stop publishing']()}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Wrapper marginBottom="42px">{t['Publishing Description']()}</Wrapper>
|
||||
<Button
|
||||
data-testid="publish-to-web-button"
|
||||
onClick={async () => {
|
||||
await publishWorkspace(true);
|
||||
}}
|
||||
type="light"
|
||||
shape="circle"
|
||||
>
|
||||
{t['Publish to web']()}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export type PublishPanelLocalProps = WorkspaceSettingDetailProps & {
|
||||
workspace: LocalWorkspace;
|
||||
};
|
||||
|
||||
const PublishPanelLocal: React.FC<PublishPanelLocalProps> = ({
|
||||
workspace,
|
||||
onTransferWorkspace,
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
marginBottom: '42px',
|
||||
}}
|
||||
>
|
||||
{t['Publishing']()}
|
||||
</Box>
|
||||
{/* TmpDisableAffineCloudModal */}
|
||||
|
||||
<Button
|
||||
data-testid="publish-enable-affine-cloud-button"
|
||||
type="light"
|
||||
shape="circle"
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
{t['Enable AFFiNE Cloud']()}
|
||||
</Button>
|
||||
{runtimeConfig.enableLegacyCloud ? (
|
||||
<EnableAffineCloudModal
|
||||
open={open}
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
onConfirm={() => {
|
||||
onTransferWorkspace(
|
||||
WorkspaceFlavour.LOCAL,
|
||||
WorkspaceFlavour.AFFINE,
|
||||
workspace
|
||||
);
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<TmpDisableAffineCloudModal
|
||||
open={open}
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const PublishPanel: React.FC<PublishPanelProps> = props => {
|
||||
if (props.workspace.flavour === WorkspaceFlavour.AFFINE) {
|
||||
return <PublishPanelAffine {...props} workspace={props.workspace} />;
|
||||
} else if (props.workspace.flavour === WorkspaceFlavour.LOCAL) {
|
||||
return <PublishPanelLocal {...props} workspace={props.workspace} />;
|
||||
}
|
||||
throw new Unreachable();
|
||||
};
|
||||
@@ -1,51 +0,0 @@
|
||||
import { Content, FlexWrapper, styled } from '@affine/component';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
|
||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||
import type React from 'react';
|
||||
|
||||
import { useCurrentUser } from '../../../../../hooks/current/use-current-user';
|
||||
import { WorkspaceAvatar } from '../../../../pure/footer';
|
||||
import type { PanelProps } from '../../index';
|
||||
|
||||
export const StyledWorkspaceName = styled('span')(() => {
|
||||
return {
|
||||
fontWeight: '400',
|
||||
fontSize: 'var(--affine-font-h6)',
|
||||
};
|
||||
});
|
||||
|
||||
export const SyncPanel: React.FC<PanelProps> = ({ workspace }) => {
|
||||
if (workspace.flavour !== WorkspaceFlavour.AFFINE) {
|
||||
throw new TypeError('SyncPanel can only be used with Affine workspace');
|
||||
}
|
||||
const [name] = useBlockSuiteWorkspaceName(workspace.blockSuiteWorkspace);
|
||||
const [avatar] = useBlockSuiteWorkspaceAvatarUrl(
|
||||
workspace.blockSuiteWorkspace
|
||||
);
|
||||
const user = useCurrentUser();
|
||||
const t = useAFFiNEI18N();
|
||||
return (
|
||||
<>
|
||||
<FlexWrapper alignItems="center" style={{ marginBottom: '12px' }}>
|
||||
<WorkspaceAvatar
|
||||
size={32}
|
||||
name={name}
|
||||
avatar={avatar}
|
||||
style={{ marginRight: '12px' }}
|
||||
/>
|
||||
<StyledWorkspaceName>{name}</StyledWorkspaceName>
|
||||
|
||||
<Content weight={500}>{t['is a Cloud Workspace']()}</Content>
|
||||
</FlexWrapper>
|
||||
<Trans i18nKey="Cloud Workspace Description">
|
||||
All data will be synchronised and saved to the AFFiNE account
|
||||
{{
|
||||
email: user?.email,
|
||||
}}
|
||||
</Trans>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ShareMenu } from '@affine/component/share-menu';
|
||||
import { Unreachable } from '@affine/env/constant';
|
||||
import type {
|
||||
AffineLegacyCloudWorkspace,
|
||||
AffineCloudWorkspace,
|
||||
LocalWorkspace,
|
||||
} from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour, WorkspaceSubPath } from '@affine/env/workspace';
|
||||
@@ -11,21 +11,20 @@ import { useRouter } from 'next/router';
|
||||
import type React from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { useToggleWorkspacePublish } from '../../../../hooks/affine/use-toggle-workspace-publish';
|
||||
import { useOnTransformWorkspace } from '../../../../hooks/root/use-on-transform-workspace';
|
||||
import { useRouterHelper } from '../../../../hooks/use-router-helper';
|
||||
import { TransformWorkspaceToAffineModal } from '../../../affine/transform-workspace-to-affine-modal';
|
||||
import type { BaseHeaderProps } from '../header';
|
||||
|
||||
const AffineHeaderShareMenu: React.FC<BaseHeaderProps> = props => {
|
||||
// todo: these hooks should be moved to the top level
|
||||
const togglePublish = useToggleWorkspacePublish(
|
||||
props.workspace as AffineLegacyCloudWorkspace
|
||||
);
|
||||
// fixme: cloud regression
|
||||
// const togglePublish = useToggleWorkspacePublish(
|
||||
// props.workspace as AffineCloudWorkspace
|
||||
// );
|
||||
const helper = useRouterHelper(useRouter());
|
||||
return (
|
||||
<ShareMenu
|
||||
workspace={props.workspace as AffineLegacyCloudWorkspace}
|
||||
workspace={props.workspace as AffineCloudWorkspace}
|
||||
currentPage={props.currentPage as Page}
|
||||
onEnableAffineCloud={useCallback(async () => {
|
||||
throw new Unreachable(
|
||||
@@ -42,12 +41,12 @@ const AffineHeaderShareMenu: React.FC<BaseHeaderProps> = props => {
|
||||
page.workspace.setPageMeta(page.id, { isPublic });
|
||||
}, [])}
|
||||
toggleWorkspacePublish={useCallback(
|
||||
async (workspace, publish) => {
|
||||
assertEquals(workspace.flavour, WorkspaceFlavour.AFFINE);
|
||||
async workspace => {
|
||||
assertEquals(workspace.flavour, WorkspaceFlavour.AFFINE_CLOUD);
|
||||
assertEquals(workspace.id, props.workspace.id);
|
||||
await togglePublish(publish);
|
||||
throw new Error('unreachable');
|
||||
},
|
||||
[props.workspace.id, togglePublish]
|
||||
[props.workspace.id]
|
||||
)}
|
||||
/>
|
||||
);
|
||||
@@ -98,7 +97,7 @@ const LocalHeaderShareMenu: React.FC<BaseHeaderProps> = props => {
|
||||
onConform={async () => {
|
||||
await onTransformWorkspace(
|
||||
WorkspaceFlavour.LOCAL,
|
||||
WorkspaceFlavour.AFFINE,
|
||||
WorkspaceFlavour.AFFINE_CLOUD,
|
||||
props.workspace as LocalWorkspace
|
||||
);
|
||||
setOpen(false);
|
||||
@@ -112,7 +111,7 @@ export const HeaderShareMenu: React.FC<BaseHeaderProps> = props => {
|
||||
if (!runtimeConfig.enableLegacyCloud) {
|
||||
return null;
|
||||
}
|
||||
if (props.workspace.flavour === WorkspaceFlavour.AFFINE) {
|
||||
if (props.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD) {
|
||||
return <AffineHeaderShareMenu {...props} />;
|
||||
} else if (props.workspace.flavour === WorkspaceFlavour.LOCAL) {
|
||||
return <LocalHeaderShareMenu {...props} />;
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
import { displayFlex, IconButton, styled, Tooltip } from '@affine/component';
|
||||
import type { LocalWorkspace } from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import {
|
||||
getLoginStorage,
|
||||
setLoginStorage,
|
||||
SignMethod,
|
||||
} from '@affine/workspace/affine/login';
|
||||
import { affineAuth } from '@affine/workspace/affine/shared';
|
||||
import {
|
||||
CloudWorkspaceIcon,
|
||||
LocalWorkspaceIcon,
|
||||
NoNetworkIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import { assertEquals, assertExists } from '@blocksuite/store';
|
||||
import { useRouter } from 'next/router';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
|
||||
import { useTransformWorkspace } from '../../../../hooks/use-transform-workspace';
|
||||
import type { AffineOfficialWorkspace } from '../../../../shared';
|
||||
import { TransformWorkspaceToAffineModal } from '../../../affine/transform-workspace-to-affine-modal';
|
||||
|
||||
const IconWrapper = styled('div')(() => {
|
||||
return {
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
marginRight: '12px',
|
||||
fontSize: '24px',
|
||||
color: 'var(--affine-icon-color)',
|
||||
WebkitAppRegion: 'no-drag',
|
||||
...displayFlex('center', 'center'),
|
||||
};
|
||||
});
|
||||
|
||||
const getStatus = (workspace: AffineOfficialWorkspace) => {
|
||||
if (!navigator.onLine) {
|
||||
return 'offline';
|
||||
}
|
||||
if (workspace.flavour === 'local') {
|
||||
return 'local';
|
||||
}
|
||||
return 'cloud';
|
||||
};
|
||||
|
||||
export const SyncUser = () => {
|
||||
//#region fixme(himself65): remove these hooks ASAP
|
||||
const [workspace] = useCurrentWorkspace();
|
||||
assertExists(workspace);
|
||||
const router = useRouter();
|
||||
|
||||
const [status, setStatus] = useState<'offline' | 'local' | 'cloud'>(
|
||||
getStatus(workspace)
|
||||
);
|
||||
const [prevWorkspace, setPrevWorkspace] = useState(workspace);
|
||||
if (prevWorkspace !== workspace) {
|
||||
setPrevWorkspace(workspace);
|
||||
setStatus(getStatus(workspace));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const online = () => {
|
||||
setStatus(getStatus(workspace));
|
||||
};
|
||||
|
||||
const offline = () => {
|
||||
setStatus('offline');
|
||||
};
|
||||
window.addEventListener('online', online);
|
||||
window.addEventListener('offline', offline);
|
||||
return () => {
|
||||
window.removeEventListener('online', online);
|
||||
window.removeEventListener('offline', offline);
|
||||
};
|
||||
}, [workspace]);
|
||||
//#endregion
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const t = useAFFiNEI18N();
|
||||
const transformWorkspace = useTransformWorkspace();
|
||||
|
||||
if (!runtimeConfig.enableLegacyCloud) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (status === 'offline') {
|
||||
return (
|
||||
<Tooltip
|
||||
content={t['Please make sure you are online']()}
|
||||
placement="bottom-end"
|
||||
>
|
||||
<IconWrapper>
|
||||
<NoNetworkIcon />
|
||||
</IconWrapper>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === 'local') {
|
||||
return (
|
||||
<>
|
||||
<Tooltip
|
||||
content={t['Saved then enable AFFiNE Cloud']()}
|
||||
placement="bottom-end"
|
||||
>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
style={{ marginRight: '12px' }}
|
||||
>
|
||||
<LocalWorkspaceIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<TransformWorkspaceToAffineModal
|
||||
open={open}
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
onConform={async () => {
|
||||
if (!getLoginStorage()) {
|
||||
const response = await affineAuth.generateToken(
|
||||
SignMethod.Google
|
||||
);
|
||||
if (response) {
|
||||
setLoginStorage(response);
|
||||
}
|
||||
router.reload();
|
||||
return;
|
||||
}
|
||||
assertEquals(workspace.flavour, WorkspaceFlavour.LOCAL);
|
||||
const id = await transformWorkspace(
|
||||
WorkspaceFlavour.LOCAL,
|
||||
WorkspaceFlavour.AFFINE,
|
||||
workspace as LocalWorkspace
|
||||
);
|
||||
// fixme(himself65): refactor this
|
||||
await router.replace({
|
||||
pathname: `/workspace/[workspaceId]/all`,
|
||||
query: {
|
||||
workspaceId: id,
|
||||
},
|
||||
});
|
||||
setOpen(false);
|
||||
router.reload();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip content={t['Synced with AFFiNE Cloud']()} placement="bottom-end">
|
||||
<IconWrapper>
|
||||
<CloudWorkspaceIcon />
|
||||
</IconWrapper>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default SyncUser;
|
||||
@@ -3,7 +3,6 @@ import { AffineLogoSBlue2_1Icon, SignOutIcon } from '@blocksuite/icons';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import { useCurrentUser } from '../../../../hooks/current/use-current-user';
|
||||
const EditMenu = (
|
||||
<MenuItem data-testid="editor-option-menu-favorite" icon={<SignOutIcon />}>
|
||||
Sign Out
|
||||
@@ -11,7 +10,8 @@ const EditMenu = (
|
||||
);
|
||||
|
||||
export const UserAvatar = () => {
|
||||
const user = useCurrentUser();
|
||||
// fixme: cloud regression
|
||||
const user: any = null;
|
||||
return (
|
||||
<Menu
|
||||
width={276}
|
||||
|
||||
@@ -29,7 +29,6 @@ import { DownloadClientTip } from './download-tips';
|
||||
import EditPage from './header-right-items/edit-page';
|
||||
import { EditorOptionMenu } from './header-right-items/editor-option-menu';
|
||||
import { HeaderShareMenu } from './header-right-items/share-menu';
|
||||
import SyncUser from './header-right-items/sync-user';
|
||||
import TrashButtonGroup from './header-right-items/trash-button-group';
|
||||
import UserAvatar from './header-right-items/user-avatar';
|
||||
import * as styles from './styles.css';
|
||||
@@ -47,7 +46,6 @@ export type BaseHeaderProps<
|
||||
export enum HeaderRightItemName {
|
||||
EditorOptionMenu = 'editorOptionMenu',
|
||||
TrashButtonGroup = 'trashButtonGroup',
|
||||
SyncUser = 'syncUser',
|
||||
ShareMenu = 'shareMenu',
|
||||
EditPage = 'editPage',
|
||||
UserAvatar = 'userAvatar',
|
||||
@@ -75,12 +73,6 @@ const HeaderRightItems: Record<HeaderRightItemName, HeaderItem> = {
|
||||
return currentPage?.meta.trash === true;
|
||||
},
|
||||
},
|
||||
[HeaderRightItemName.SyncUser]: {
|
||||
Component: SyncUser,
|
||||
availableWhen: (_, currentPage, { isPublic }) => {
|
||||
return !isPublic;
|
||||
},
|
||||
},
|
||||
[HeaderRightItemName.ShareMenu]: {
|
||||
Component: HeaderShareMenu,
|
||||
availableWhen: (workspace, currentPage) => {
|
||||
|
||||
@@ -1,76 +1,34 @@
|
||||
import { FlexWrapper } from '@affine/component';
|
||||
import { IconButton } from '@affine/component';
|
||||
import { Tooltip } from '@affine/component';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { AccessTokenMessage } from '@affine/workspace/affine/login';
|
||||
import { CloudWorkspaceIcon, SignOutIcon } from '@blocksuite/icons';
|
||||
import { CloudWorkspaceIcon } from '@blocksuite/icons';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import type { CSSProperties } from 'react';
|
||||
import type React from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import { type CSSProperties, type FC, forwardRef } from 'react';
|
||||
|
||||
import { openDisableCloudAlertModalAtom } from '../../../atoms';
|
||||
import { stringToColour } from '../../../utils';
|
||||
import { StyledFooter, StyledSignInButton, StyleUserInfo } from './styles';
|
||||
import { StyledFooter, StyledSignInButton } from './styles';
|
||||
|
||||
export type FooterProps = {
|
||||
user: AccessTokenMessage | null;
|
||||
onLogin: () => void;
|
||||
onLogout: () => void;
|
||||
};
|
||||
|
||||
export const Footer: React.FC<FooterProps> = ({ user, onLogin, onLogout }) => {
|
||||
export const Footer: FC = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const setOpen = useSetAtom(openDisableCloudAlertModalAtom);
|
||||
return (
|
||||
<StyledFooter data-testid="workspace-list-modal-footer">
|
||||
{user && (
|
||||
<>
|
||||
<FlexWrapper>
|
||||
<WorkspaceAvatar
|
||||
size={40}
|
||||
name={user.name}
|
||||
avatar={user.avatar_url}
|
||||
></WorkspaceAvatar>
|
||||
<StyleUserInfo>
|
||||
<p>{user.name}</p>
|
||||
<p>{user.email}</p>
|
||||
</StyleUserInfo>
|
||||
</FlexWrapper>
|
||||
<Tooltip content={t['Sign out']()} disablePortal={true}>
|
||||
<IconButton
|
||||
data-testid="workspace-list-modal-sign-out"
|
||||
onClick={() => {
|
||||
onLogout();
|
||||
}}
|
||||
>
|
||||
<SignOutIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!user && (
|
||||
<StyledSignInButton
|
||||
data-testid="sign-in-button"
|
||||
noBorder
|
||||
bold
|
||||
icon={
|
||||
<div className="circle">
|
||||
<CloudWorkspaceIcon />
|
||||
</div>
|
||||
<StyledSignInButton
|
||||
data-testid="sign-in-button"
|
||||
noBorder
|
||||
bold
|
||||
icon={
|
||||
<div className="circle">
|
||||
<CloudWorkspaceIcon />
|
||||
</div>
|
||||
}
|
||||
onClick={async () => {
|
||||
if (!runtimeConfig.enableLegacyCloud) {
|
||||
setOpen(true);
|
||||
}
|
||||
onClick={async () => {
|
||||
if (!runtimeConfig.enableLegacyCloud) {
|
||||
setOpen(true);
|
||||
} else {
|
||||
onLogin();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t['Sign in']()}
|
||||
</StyledSignInButton>
|
||||
)}
|
||||
}}
|
||||
>
|
||||
{t['Sign in']()}
|
||||
</StyledSignInButton>
|
||||
</StyledFooter>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import { MessageCode, Messages } from '@affine/env/constant';
|
||||
import { setLoginStorage, SignMethod } from '@affine/workspace/affine/login';
|
||||
import { affineAuth } from '@affine/workspace/affine/shared';
|
||||
import type { FC } from 'react';
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
|
||||
import { useAffineLogOut } from '../../../hooks/affine/use-affine-log-out';
|
||||
import { toast } from '../../../utils';
|
||||
|
||||
declare global {
|
||||
interface DocumentEventMap {
|
||||
'affine-error': CustomEvent<{
|
||||
code: keyof typeof Messages;
|
||||
}>;
|
||||
}
|
||||
}
|
||||
|
||||
export const MessageCenter: FC = memo(function MessageCenter() {
|
||||
const [popup, setPopup] = useState(false);
|
||||
const onLogout = useAffineLogOut();
|
||||
useEffect(() => {
|
||||
const listener = (
|
||||
event: CustomEvent<{
|
||||
code: keyof typeof Messages;
|
||||
}>
|
||||
) => {
|
||||
// fixme: need refactor
|
||||
// - login and refresh refresh logic should be refactored
|
||||
// - error message should be refactored
|
||||
if (
|
||||
!popup &&
|
||||
(event.detail.code === MessageCode.refreshTokenError ||
|
||||
event.detail.code === MessageCode.loginError)
|
||||
) {
|
||||
setPopup(true);
|
||||
affineAuth
|
||||
.generateToken(SignMethod.Google)
|
||||
.then(response => {
|
||||
if (response) {
|
||||
setLoginStorage(response);
|
||||
}
|
||||
setPopup(false);
|
||||
})
|
||||
.catch(() => {
|
||||
setPopup(false);
|
||||
return onLogout();
|
||||
});
|
||||
} else {
|
||||
toast(Messages[event.detail.code].message);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('affine-error', listener);
|
||||
return () => {
|
||||
document.removeEventListener('affine-error', listener);
|
||||
};
|
||||
}, [onLogout, popup]);
|
||||
return null;
|
||||
});
|
||||
@@ -9,12 +9,11 @@ import {
|
||||
import { ScrollableContainer } from '@affine/component';
|
||||
import { WorkspaceList } from '@affine/component/workspace-list';
|
||||
import type {
|
||||
AffineLegacyCloudWorkspace,
|
||||
AffineCloudWorkspace,
|
||||
LocalWorkspace,
|
||||
} from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { AccessTokenMessage } from '@affine/workspace/affine/login';
|
||||
import { HelpIcon, ImportIcon, PlusIcon } from '@blocksuite/icons';
|
||||
import type { DragEndEvent } from '@dnd-kit/core';
|
||||
import { useCallback, useRef } from 'react';
|
||||
@@ -41,15 +40,12 @@ import {
|
||||
|
||||
interface WorkspaceModalProps {
|
||||
disabled?: boolean;
|
||||
user: AccessTokenMessage | null;
|
||||
workspaces: AllWorkspace[];
|
||||
currentWorkspaceId: AllWorkspace['id'] | null;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onClickWorkspace: (workspace: AllWorkspace) => void;
|
||||
onClickWorkspaceSetting: (workspace: AllWorkspace) => void;
|
||||
onClickLogin: () => void;
|
||||
onClickLogout: () => void;
|
||||
onNewWorkspace: () => void;
|
||||
onAddWorkspace: () => void;
|
||||
onMoveWorkspace: (activeId: string, overId: string) => void;
|
||||
@@ -161,9 +157,6 @@ export const WorkspaceListModal = ({
|
||||
open,
|
||||
onClose,
|
||||
workspaces,
|
||||
user,
|
||||
onClickLogin,
|
||||
onClickLogout,
|
||||
onClickWorkspace,
|
||||
onClickWorkspaceSetting,
|
||||
onNewWorkspace,
|
||||
@@ -213,7 +206,7 @@ export const WorkspaceListModal = ({
|
||||
items={
|
||||
workspaces.filter(
|
||||
({ flavour }) => flavour !== WorkspaceFlavour.PUBLIC
|
||||
) as (AffineLegacyCloudWorkspace | LocalWorkspace)[]
|
||||
) as (AffineCloudWorkspace | LocalWorkspace)[]
|
||||
}
|
||||
currentWorkspaceId={currentWorkspaceId}
|
||||
onClick={onClickWorkspace}
|
||||
@@ -234,7 +227,7 @@ export const WorkspaceListModal = ({
|
||||
/>
|
||||
</StyledModalContent>
|
||||
</ScrollableContainer>
|
||||
<Footer user={user} onLogin={onClickLogin} onLogout={onClickLogout} />
|
||||
<Footer />
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -11,13 +11,11 @@ import {
|
||||
SidebarScrollableContainer,
|
||||
} from '@affine/component/app-sidebar';
|
||||
import { isDesktop } from '@affine/env/constant';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import {
|
||||
DeleteTemporarilyIcon,
|
||||
FolderIcon,
|
||||
SettingsIcon,
|
||||
ShareIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { useDroppable } from '@dnd-kit/core';
|
||||
@@ -199,25 +197,6 @@ export const RootAppSidebar = ({
|
||||
{blockSuiteWorkspace && (
|
||||
<FavoriteList currentWorkspace={currentWorkspace} />
|
||||
)}
|
||||
{runtimeConfig.enableLegacyCloud &&
|
||||
(currentWorkspace?.flavour === WorkspaceFlavour.AFFINE &&
|
||||
currentWorkspace.public ? (
|
||||
<RouteMenuLinkItem
|
||||
icon={<ShareIcon />}
|
||||
currentPath={currentPath}
|
||||
path={currentWorkspaceId && paths.setting(currentWorkspaceId)}
|
||||
>
|
||||
<span data-testid="Published-to-web">Published to web</span>
|
||||
</RouteMenuLinkItem>
|
||||
) : (
|
||||
<RouteMenuLinkItem
|
||||
icon={<ShareIcon />}
|
||||
currentPath={currentPath}
|
||||
path={currentWorkspaceId && paths.shared(currentWorkspaceId)}
|
||||
>
|
||||
<span data-testid="shared-pages">{t['Shared Pages']()}</span>
|
||||
</RouteMenuLinkItem>
|
||||
))}
|
||||
<CategoryDivider label={t['Collections']()} />
|
||||
{blockSuiteWorkspace && (
|
||||
<CollectionsList currentWorkspace={currentWorkspace} />
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { WorkspaceAdapters } from '../../adapters/workspace';
|
||||
|
||||
export function useAffineLogIn() {
|
||||
const router = useRouter();
|
||||
return useCallback(async () => {
|
||||
await WorkspaceAdapters[WorkspaceFlavour.AFFINE].Events[
|
||||
'workspace:access'
|
||||
]?.();
|
||||
// todo: remove reload page requirement
|
||||
router.reload();
|
||||
}, [router]);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { WorkspaceAdapters } from '../../adapters/workspace';
|
||||
|
||||
export function useAffineLogOut() {
|
||||
const router = useRouter();
|
||||
return useCallback(async () => {
|
||||
await WorkspaceAdapters[WorkspaceFlavour.AFFINE].Events[
|
||||
'workspace:revoke'
|
||||
]?.();
|
||||
router.reload();
|
||||
}, [router]);
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import {
|
||||
getLoginStorage,
|
||||
isExpired,
|
||||
parseIdToken,
|
||||
setLoginStorage,
|
||||
storageChangeSlot,
|
||||
} from '@affine/workspace/affine/login';
|
||||
import { affineAuth } from '@affine/workspace/affine/shared';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const logger = new DebugLogger('auth-token');
|
||||
|
||||
const revalidate = async () => {
|
||||
const storage = getLoginStorage();
|
||||
if (storage) {
|
||||
try {
|
||||
const tokenMessage = parseIdToken(storage.token);
|
||||
logger.debug('revalidate affine user');
|
||||
if (isExpired(tokenMessage)) {
|
||||
logger.debug('need to refresh token');
|
||||
const response = await affineAuth.refreshToken(storage);
|
||||
if (response) {
|
||||
setLoginStorage(response);
|
||||
storageChangeSlot.emit();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export function useAffineRefreshAuthToken(
|
||||
// every 30 seconds, check if the token is expired
|
||||
refreshInterval = 30 * 1000
|
||||
) {
|
||||
const { data } = useSWR<boolean>('autoRefreshToken', {
|
||||
suspense: true,
|
||||
fetcher: revalidate,
|
||||
refreshInterval,
|
||||
revalidateOnFocus: true,
|
||||
revalidateOnReconnect: true,
|
||||
revalidateOnMount: true,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { PermissionType } from '@affine/env/workspace/legacy-cloud';
|
||||
|
||||
import type { AffineOfficialWorkspace } from '../../shared';
|
||||
|
||||
export function useIsWorkspaceOwner(workspace: AffineOfficialWorkspace) {
|
||||
if (workspace.flavour === WorkspaceFlavour.LOCAL) return true;
|
||||
if (workspace.flavour === WorkspaceFlavour.PUBLIC) return false;
|
||||
return workspace.permission === PermissionType.Owner;
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import type { Member } from '@affine/env/workspace/legacy-cloud';
|
||||
import { affineApis } from '@affine/workspace/affine/shared';
|
||||
import { useCallback } from 'react';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import { QueryKey } from '../../adapters/affine/fetcher';
|
||||
|
||||
export function useMembers(workspaceId: string) {
|
||||
const { data, mutate } = useSWR<Member[]>(
|
||||
[QueryKey.getMembers, workspaceId],
|
||||
{
|
||||
fallbackData: [],
|
||||
}
|
||||
);
|
||||
|
||||
const inviteMember = useCallback(
|
||||
async (email: string) => {
|
||||
await affineApis.inviteMember({
|
||||
id: workspaceId,
|
||||
email,
|
||||
});
|
||||
return mutate();
|
||||
},
|
||||
[mutate, workspaceId]
|
||||
);
|
||||
|
||||
const removeMember = useCallback(
|
||||
async (permissionId: number) => {
|
||||
await affineApis.removeMember({
|
||||
permissionId,
|
||||
});
|
||||
return mutate();
|
||||
},
|
||||
[mutate]
|
||||
);
|
||||
|
||||
return {
|
||||
members: data ?? [],
|
||||
inviteMember,
|
||||
removeMember,
|
||||
};
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import type { AffineLegacyCloudWorkspace } from '@affine/env/workspace';
|
||||
import { affineApis } from '@affine/workspace/affine/shared';
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import { rootStore } from '@toeverything/plugin-infra/manager';
|
||||
import { useCallback } from 'react';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import { QueryKey } from '../../adapters/affine/fetcher';
|
||||
|
||||
export function useToggleWorkspacePublish(
|
||||
workspace: AffineLegacyCloudWorkspace
|
||||
) {
|
||||
const { mutate } = useSWR(QueryKey.getWorkspaces);
|
||||
return useCallback(
|
||||
async (isPublish: boolean) => {
|
||||
await affineApis.updateWorkspace({
|
||||
id: workspace.id,
|
||||
public: isPublish,
|
||||
});
|
||||
await mutate(QueryKey.getWorkspaces);
|
||||
// fixme: remove force update
|
||||
await rootStore.set(rootWorkspacesMetadataAtom, ws => [...ws]);
|
||||
},
|
||||
[mutate, workspace.id]
|
||||
);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import useSWR from 'swr';
|
||||
|
||||
import { QueryKey } from '../../adapters/affine/fetcher';
|
||||
|
||||
export interface QueryEmailMember {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
avatar_url: string;
|
||||
create_at: string;
|
||||
}
|
||||
|
||||
export function useUsersByEmail(
|
||||
workspaceId: string,
|
||||
email: string
|
||||
): QueryEmailMember[] | null {
|
||||
const { data } = useSWR<QueryEmailMember[] | null>(
|
||||
[QueryKey.getUserByEmail, workspaceId, email],
|
||||
{
|
||||
fallbackData: null,
|
||||
}
|
||||
);
|
||||
return data ?? null;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { currentAffineUserAtom } from '@affine/workspace/affine/atom';
|
||||
import type { AccessTokenMessage } from '@affine/workspace/affine/login';
|
||||
import { useAtomValue } from 'jotai';
|
||||
|
||||
export function useCurrentUser(): AccessTokenMessage | null {
|
||||
return useAtomValue(currentAffineUserAtom);
|
||||
}
|
||||
@@ -1,14 +1,5 @@
|
||||
import type { WorkspaceRegistry } from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { currentAffineUserAtom } from '@affine/workspace/affine/atom';
|
||||
import {
|
||||
getLoginStorage,
|
||||
parseIdToken,
|
||||
setLoginStorage,
|
||||
SignMethod,
|
||||
storageChangeSlot,
|
||||
} from '@affine/workspace/affine/login';
|
||||
import { affineAuth } from '@affine/workspace/affine/shared';
|
||||
import type { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { rootCurrentWorkspaceIdAtom } from '@affine/workspace/atom';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
@@ -17,7 +8,6 @@ import { useTransformWorkspace } from '../use-transform-workspace';
|
||||
|
||||
export function useOnTransformWorkspace() {
|
||||
const transformWorkspace = useTransformWorkspace();
|
||||
const setUser = useSetAtom(currentAffineUserAtom);
|
||||
const setWorkspaceId = useSetAtom(rootCurrentWorkspaceIdAtom);
|
||||
return useCallback(
|
||||
async <From extends WorkspaceFlavour, To extends WorkspaceFlavour>(
|
||||
@@ -25,15 +15,6 @@ export function useOnTransformWorkspace() {
|
||||
to: To,
|
||||
workspace: WorkspaceRegistry[From]
|
||||
): Promise<void> => {
|
||||
const needRefresh = to === WorkspaceFlavour.AFFINE && !getLoginStorage();
|
||||
if (needRefresh) {
|
||||
const response = await affineAuth.generateToken(SignMethod.Google);
|
||||
if (response) {
|
||||
setLoginStorage(response);
|
||||
setUser(parseIdToken(response.token));
|
||||
storageChangeSlot.emit();
|
||||
}
|
||||
}
|
||||
const workspaceId = await transformWorkspace(from, to, workspace);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('affine-workspace:transform', {
|
||||
@@ -47,7 +28,7 @@ export function useOnTransformWorkspace() {
|
||||
);
|
||||
setWorkspaceId(workspaceId);
|
||||
},
|
||||
[setUser, setWorkspaceId, transformWorkspace]
|
||||
[setWorkspaceId, transformWorkspace]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -43,10 +43,6 @@ import {
|
||||
openWorkspacesModalAtom,
|
||||
} from '../atoms';
|
||||
import { useTrackRouterHistoryEffect } from '../atoms/history';
|
||||
import {
|
||||
publicWorkspaceAtom,
|
||||
publicWorkspaceIdAtom,
|
||||
} from '../atoms/public-workspace';
|
||||
import { AppContainer } from '../components/affine/app-container';
|
||||
import type { IslandItemNames } from '../components/pure/help-island';
|
||||
import { HelpIsland } from '../components/pure/help-island';
|
||||
@@ -78,24 +74,6 @@ const SettingModal = lazy(() =>
|
||||
}))
|
||||
);
|
||||
|
||||
export const PublicQuickSearch: FC = () => {
|
||||
const publicWorkspace = useAtomValue(publicWorkspaceAtom);
|
||||
const router = useRouter();
|
||||
const [openQuickSearchModal, setOpenQuickSearchModalAtom] = useAtom(
|
||||
openQuickSearchModalAtom
|
||||
);
|
||||
return (
|
||||
<Suspense>
|
||||
<QuickSearchModal
|
||||
blockSuiteWorkspace={publicWorkspace.blockSuiteWorkspace}
|
||||
open={openQuickSearchModal}
|
||||
setOpen={setOpenQuickSearchModalAtom}
|
||||
router={router}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
function DefaultProvider({ children }: PropsWithChildren) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -107,13 +85,7 @@ export const QuickSearch: FC = () => {
|
||||
openQuickSearchModalAtom
|
||||
);
|
||||
const blockSuiteWorkspace = currentWorkspace?.blockSuiteWorkspace;
|
||||
const isPublicWorkspace =
|
||||
router.pathname.split('/')[1] === 'public-workspace';
|
||||
const publicWorkspaceId = useAtomValue(publicWorkspaceIdAtom);
|
||||
if (!blockSuiteWorkspace) {
|
||||
if (isPublicWorkspace && publicWorkspaceId) {
|
||||
return <PublicQuickSearch />;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
@@ -127,13 +99,10 @@ export const QuickSearch: FC = () => {
|
||||
};
|
||||
export const Setting: FC = () => {
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
const router = useRouter();
|
||||
const [openSettingModal, setOpenSettingModalAtom] =
|
||||
useAtom(openSettingModalAtom);
|
||||
const blockSuiteWorkspace = currentWorkspace?.blockSuiteWorkspace;
|
||||
const isPublicWorkspace =
|
||||
router.pathname.split('/')[1] === 'public-workspace';
|
||||
if (!blockSuiteWorkspace || isPublicWorkspace) {
|
||||
if (!blockSuiteWorkspace) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
|
||||
@@ -15,7 +15,6 @@ import type { PropsWithChildren, ReactElement } from 'react';
|
||||
import React, { lazy, Suspense, useEffect } from 'react';
|
||||
|
||||
import { AffineErrorBoundary } from '../components/affine/affine-error-eoundary';
|
||||
import { MessageCenter } from '../components/pure/message-center';
|
||||
import type { NextPageWithLayout } from '../shared';
|
||||
import createEmotionCache from '../utils/create-emotion-cache';
|
||||
|
||||
@@ -61,7 +60,6 @@ const App = function App({
|
||||
return (
|
||||
<CacheProvider value={emotionCache}>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<MessageCenter />
|
||||
<AffineErrorBoundary router={useRouter()}>
|
||||
<AffineContext>
|
||||
<Head>
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
import { Button } from '@affine/component';
|
||||
import { MainContainer } from '@affine/component/workspace';
|
||||
import { currentAffineUserAtom } from '@affine/workspace/affine/atom';
|
||||
import {
|
||||
clearLoginStorage,
|
||||
createAffineAuth,
|
||||
getLoginStorage,
|
||||
isExpired,
|
||||
parseIdToken,
|
||||
setLoginStorage,
|
||||
SignMethod,
|
||||
} from '@affine/workspace/affine/login';
|
||||
import { useAtom } from 'jotai';
|
||||
import type { NextPage } from 'next';
|
||||
import { lazy, Suspense, useMemo } from 'react';
|
||||
|
||||
import { AppContainer } from '../../components/affine/app-container';
|
||||
import { toast } from '../../utils';
|
||||
|
||||
const Viewer = lazy(() =>
|
||||
import('@rich-data/viewer').then(m => ({ default: m.JsonViewer }))
|
||||
);
|
||||
|
||||
import { useTheme } from 'next-themes';
|
||||
|
||||
const LoginDevPage: NextPage = () => {
|
||||
const [user, setUser] = useAtom(currentAffineUserAtom);
|
||||
const auth = useMemo(() => createAffineAuth(), []);
|
||||
return (
|
||||
<AppContainer>
|
||||
<MainContainer>
|
||||
<h1>LoginDevPage</h1>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
const storage = getLoginStorage();
|
||||
if (storage) {
|
||||
const user = parseIdToken(storage.token);
|
||||
if (isExpired(user)) {
|
||||
await auth.refreshToken(storage);
|
||||
}
|
||||
}
|
||||
const response = await auth.generateToken(SignMethod.Google);
|
||||
if (response) {
|
||||
setLoginStorage(response);
|
||||
const user = parseIdToken(response.token);
|
||||
setUser(user);
|
||||
} else {
|
||||
toast('Login failed');
|
||||
}
|
||||
}}
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
const storage = getLoginStorage();
|
||||
if (!storage) {
|
||||
throw new Error('No storage');
|
||||
}
|
||||
const response = await auth.refreshToken(storage);
|
||||
if (response) {
|
||||
setLoginStorage(response);
|
||||
const user = parseIdToken(response.token);
|
||||
setUser(user);
|
||||
} else {
|
||||
toast('Login failed');
|
||||
}
|
||||
}}
|
||||
>
|
||||
Refresh Token
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
clearLoginStorage();
|
||||
setUser(null);
|
||||
}}
|
||||
>
|
||||
Reset Storage
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
const status = await fetch('/api/workspace', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
Authorization: getLoginStorage()?.token ?? '',
|
||||
},
|
||||
}).then(r => r.status);
|
||||
toast(`Response Status: ${status}`);
|
||||
}}
|
||||
>
|
||||
Check Permission
|
||||
</Button>
|
||||
<Suspense>
|
||||
<Viewer
|
||||
theme={useTheme().resolvedTheme === 'light' ? 'light' : 'dark'}
|
||||
value={user}
|
||||
/>
|
||||
</Suspense>
|
||||
</MainContainer>
|
||||
</AppContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginDevPage;
|
||||
@@ -1,112 +0,0 @@
|
||||
import { displayFlex, styled } from '@affine/component';
|
||||
import { Button } from '@affine/component';
|
||||
import { WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import type { Permission } from '@affine/env/workspace/legacy-cloud';
|
||||
import {
|
||||
SucessfulDuotoneIcon,
|
||||
UnsucessfulDuotoneIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import { NoSsr } from '@mui/material';
|
||||
import Image from 'next/legacy/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import { Suspense } from 'react';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import { QueryKey } from '../../adapters/affine/fetcher';
|
||||
import { PageLoading } from '../../components/pure/loading';
|
||||
import { RouteLogic, useRouterHelper } from '../../hooks/use-router-helper';
|
||||
import type { NextPageWithLayout } from '../../shared';
|
||||
|
||||
const InvitePage: NextPageWithLayout = () => {
|
||||
const router = useRouter();
|
||||
const { jumpToSubPath } = useRouterHelper(router);
|
||||
const { data: inviteData } = useSWR<Permission>(
|
||||
typeof router.query.invite_code === 'string'
|
||||
? [QueryKey.acceptInvite, router.query.invite_code]
|
||||
: null
|
||||
);
|
||||
|
||||
if (inviteData?.accepted) {
|
||||
return (
|
||||
<StyledContainer>
|
||||
<Image
|
||||
src="/imgs/invite-success.svg"
|
||||
alt=""
|
||||
layout="fill"
|
||||
width={300}
|
||||
height={300}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
shape="round"
|
||||
onClick={() => {
|
||||
jumpToSubPath(
|
||||
inviteData.workspace_id,
|
||||
WorkspaceSubPath.ALL,
|
||||
RouteLogic.REPLACE
|
||||
).catch(err => console.error(err));
|
||||
}}
|
||||
>
|
||||
Go to Workspace
|
||||
</Button>
|
||||
<p>
|
||||
<SucessfulDuotoneIcon />
|
||||
Successfully joined
|
||||
</p>
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
|
||||
if (inviteData?.accepted === false) {
|
||||
return (
|
||||
<StyledContainer>
|
||||
<Image src="/imgs/invite-error.svg" alt="" />
|
||||
<Button
|
||||
shape="round"
|
||||
onClick={() => {
|
||||
router.replace(`/`).catch(err => console.error(err));
|
||||
}}
|
||||
>
|
||||
Back to Home
|
||||
</Button>
|
||||
<p>
|
||||
<UnsucessfulDuotoneIcon />
|
||||
The link has expired
|
||||
</p>
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
throw new Error('Invalid invite code');
|
||||
};
|
||||
|
||||
export default InvitePage;
|
||||
|
||||
InvitePage.getLayout = page => {
|
||||
return (
|
||||
<Suspense fallback={<PageLoading />}>
|
||||
<NoSsr>{page}</NoSsr>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledContainer = styled('div')(() => {
|
||||
return {
|
||||
height: '100vh',
|
||||
...displayFlex('center', 'center'),
|
||||
flexDirection: 'column',
|
||||
backgroundColor: 'var(--affine-background-primary-color)',
|
||||
img: {
|
||||
width: '300px',
|
||||
height: '300px',
|
||||
},
|
||||
p: {
|
||||
...displayFlex('center', 'center'),
|
||||
marginTop: '24px',
|
||||
svg: {
|
||||
color: 'var(--affine-primary-color)',
|
||||
fontSize: '24px',
|
||||
marginRight: '12px',
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -1,134 +0,0 @@
|
||||
import { Breadcrumbs, IconButton, ListSkeleton } from '@affine/component';
|
||||
import { StyledTableContainer } from '@affine/component/page-list';
|
||||
import { QueryParamError } from '@affine/env/constant';
|
||||
import { rootCurrentWorkspaceIdAtom } from '@affine/workspace/atom';
|
||||
import { SearchIcon } from '@blocksuite/icons';
|
||||
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
|
||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import { useRouter } from 'next/router';
|
||||
import type React from 'react';
|
||||
import { lazy, Suspense, useCallback, useEffect } from 'react';
|
||||
|
||||
import { openQuickSearchModalAtom } from '../../atoms';
|
||||
import {
|
||||
publicWorkspaceAtom,
|
||||
publicWorkspaceIdAtom,
|
||||
} from '../../atoms/public-workspace';
|
||||
import { WorkspaceAvatar } from '../../components/pure/footer';
|
||||
import { PageLoading } from '../../components/pure/loading';
|
||||
import {
|
||||
PublicQuickSearch,
|
||||
PublicWorkspaceLayout,
|
||||
} from '../../layouts/public-workspace-layout';
|
||||
import type { NextPageWithLayout } from '../../shared';
|
||||
import { NavContainer, StyledBreadcrumbs } from './[workspaceId]/[pageId]';
|
||||
|
||||
const BlockSuitePageList = lazy(() =>
|
||||
import('../../components/blocksuite/block-suite-page-list').then(module => ({
|
||||
default: module.BlockSuitePageList,
|
||||
}))
|
||||
);
|
||||
|
||||
const ListPageInner: React.FC<{
|
||||
workspaceId: string;
|
||||
}> = ({ workspaceId }) => {
|
||||
const router = useRouter();
|
||||
const publicWorkspace = useAtomValue(publicWorkspaceAtom);
|
||||
const blockSuiteWorkspace = publicWorkspace.blockSuiteWorkspace;
|
||||
const handleClickPage = useCallback(
|
||||
(pageId: string) => {
|
||||
return router.push({
|
||||
pathname: `/public-workspace/[workspaceId]/[pageId]`,
|
||||
query: {
|
||||
workspaceId,
|
||||
pageId,
|
||||
},
|
||||
});
|
||||
},
|
||||
[router, workspaceId]
|
||||
);
|
||||
const [name] = useBlockSuiteWorkspaceName(blockSuiteWorkspace);
|
||||
const [avatar] = useBlockSuiteWorkspaceAvatarUrl(blockSuiteWorkspace);
|
||||
const setSearchModalOpen = useSetAtom(openQuickSearchModalAtom);
|
||||
const handleOpen = useCallback(() => {
|
||||
setSearchModalOpen(true);
|
||||
}, [setSearchModalOpen]);
|
||||
if (!blockSuiteWorkspace) {
|
||||
return <PageLoading />;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<PublicQuickSearch workspace={publicWorkspace} />
|
||||
<NavContainer sx={{ px: '20px' }}>
|
||||
<Breadcrumbs>
|
||||
<StyledBreadcrumbs
|
||||
href={`/public-workspace/${blockSuiteWorkspace.id}`}
|
||||
>
|
||||
<WorkspaceAvatar size={24} name={name} avatar={avatar} />
|
||||
<span>{name}</span>
|
||||
</StyledBreadcrumbs>
|
||||
</Breadcrumbs>
|
||||
<IconButton onClick={handleOpen}>
|
||||
<SearchIcon />
|
||||
</IconButton>
|
||||
</NavContainer>
|
||||
<Suspense
|
||||
fallback={
|
||||
<StyledTableContainer>
|
||||
<ListSkeleton />
|
||||
</StyledTableContainer>
|
||||
}
|
||||
>
|
||||
<BlockSuitePageList
|
||||
listType="public"
|
||||
isPublic={true}
|
||||
onOpenPage={handleClickPage}
|
||||
blockSuiteWorkspace={blockSuiteWorkspace}
|
||||
/>
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// This is affine only page, so we don't need to dynamic use WorkspacePlugin
|
||||
const ListPage: NextPageWithLayout = () => {
|
||||
const router = useRouter();
|
||||
const workspaceId = router.query.workspaceId;
|
||||
const setWorkspaceId = useSetAtom(publicWorkspaceIdAtom);
|
||||
// todo: remove this atom usage here
|
||||
const setCurrentWorkspaceId = useSetAtom(rootCurrentWorkspaceIdAtom);
|
||||
useEffect(() => {
|
||||
if (!router.isReady) {
|
||||
return;
|
||||
}
|
||||
if (typeof workspaceId === 'string') {
|
||||
setWorkspaceId(workspaceId);
|
||||
setCurrentWorkspaceId(workspaceId);
|
||||
}
|
||||
}, [router.isReady, setCurrentWorkspaceId, setWorkspaceId, workspaceId]);
|
||||
const value = useAtomValue(publicWorkspaceIdAtom);
|
||||
if (!router.isReady || !value) {
|
||||
return <PageLoading />;
|
||||
}
|
||||
if (typeof workspaceId !== 'string') {
|
||||
throw new QueryParamError('workspaceId', workspaceId);
|
||||
}
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<StyledTableContainer>
|
||||
<ListSkeleton />
|
||||
</StyledTableContainer>
|
||||
}
|
||||
>
|
||||
<ListPageInner workspaceId={workspaceId} />
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListPage;
|
||||
|
||||
ListPage.getLayout = page => {
|
||||
return <PublicWorkspaceLayout>{page}</PublicWorkspaceLayout>;
|
||||
};
|
||||
@@ -1,158 +0,0 @@
|
||||
import { Breadcrumbs, displayFlex, styled } from '@affine/component';
|
||||
import { initEmptyPage } from '@affine/env/blocksuite';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { PageIcon } from '@blocksuite/icons';
|
||||
import { assertExists } from '@blocksuite/store';
|
||||
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
|
||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import type { ReactElement } from 'react';
|
||||
import { Suspense, useCallback, useEffect } from 'react';
|
||||
|
||||
import {
|
||||
publicPageBlockSuiteAtom,
|
||||
publicWorkspaceIdAtom,
|
||||
publicWorkspacePageIdAtom,
|
||||
} from '../../../atoms/public-workspace';
|
||||
import { BlockSuiteEditorHeader } from '../../../components/blocksuite/workspace-header';
|
||||
import {
|
||||
PageDetailEditor,
|
||||
type PageDetailEditorProps,
|
||||
} from '../../../components/page-detail-editor';
|
||||
import { WorkspaceAvatar } from '../../../components/pure/footer';
|
||||
import { PageLoading } from '../../../components/pure/loading';
|
||||
import { useRouterHelper } from '../../../hooks/use-router-helper';
|
||||
import {
|
||||
PublicQuickSearch,
|
||||
PublicWorkspaceLayout,
|
||||
} from '../../../layouts/public-workspace-layout';
|
||||
import type { NextPageWithLayout } from '../../../shared';
|
||||
|
||||
export const NavContainer = styled('div')(() => {
|
||||
return {
|
||||
width: '100vw',
|
||||
height: '52px',
|
||||
...displayFlex('space-between', 'center'),
|
||||
backgroundColor: 'var(--affine-background-primary-color)',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledBreadcrumbs = styled(Link)(() => {
|
||||
return {
|
||||
flex: 1,
|
||||
...displayFlex('center', 'center'),
|
||||
paddingLeft: '12px',
|
||||
span: {
|
||||
padding: '0 12px',
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
lineHeight: 'var(--affine-line-height)',
|
||||
},
|
||||
':hover': { color: 'var(--affine-primary-color)' },
|
||||
transition: 'all .15s',
|
||||
':visited': {
|
||||
':hover': { color: 'var(--affine-primary-color)' },
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const PublicWorkspaceDetailPageInner = (): ReactElement => {
|
||||
const pageId = useAtomValue(publicWorkspacePageIdAtom);
|
||||
assertExists(pageId, 'pageId is null');
|
||||
const publicWorkspace = useAtomValue(publicPageBlockSuiteAtom);
|
||||
const blockSuiteWorkspace = publicWorkspace.blockSuiteWorkspace;
|
||||
if (!blockSuiteWorkspace) {
|
||||
throw new Error('cannot find workspace');
|
||||
}
|
||||
const router = useRouter();
|
||||
const { openPage } = useRouterHelper(router);
|
||||
const t = useAFFiNEI18N();
|
||||
const [name] = useBlockSuiteWorkspaceName(blockSuiteWorkspace);
|
||||
const [avatar] = useBlockSuiteWorkspaceAvatarUrl(blockSuiteWorkspace);
|
||||
const pageTitle = blockSuiteWorkspace.meta.getPageMeta(pageId)?.title;
|
||||
const onLoad = useCallback<NonNullable<PageDetailEditorProps['onLoad']>>(
|
||||
(_, editor) => {
|
||||
const { page } = editor;
|
||||
page.awarenessStore.setReadonly(page, true);
|
||||
const dispose = editor.slots.pageLinkClicked.on(({ pageId }) => {
|
||||
return openPage(blockSuiteWorkspace.id, pageId);
|
||||
});
|
||||
return () => {
|
||||
dispose.dispose();
|
||||
};
|
||||
},
|
||||
[blockSuiteWorkspace.id, openPage]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<PublicQuickSearch workspace={publicWorkspace} />
|
||||
<BlockSuiteEditorHeader
|
||||
isPublic={true}
|
||||
workspace={publicWorkspace}
|
||||
currentPage={blockSuiteWorkspace.getPage(pageId)}
|
||||
>
|
||||
<NavContainer>
|
||||
<Breadcrumbs>
|
||||
<StyledBreadcrumbs
|
||||
href={`/public-workspace/${blockSuiteWorkspace.id}`}
|
||||
>
|
||||
<WorkspaceAvatar size={24} name={name} avatar={avatar} />
|
||||
<span>{name}</span>
|
||||
</StyledBreadcrumbs>
|
||||
<StyledBreadcrumbs
|
||||
href={`/public-workspace/${blockSuiteWorkspace.id}/${pageId}`}
|
||||
>
|
||||
<PageIcon fontSize={24} />
|
||||
<span>{pageTitle ? pageTitle : t['Untitled']()}</span>
|
||||
</StyledBreadcrumbs>
|
||||
</Breadcrumbs>
|
||||
</NavContainer>
|
||||
</BlockSuiteEditorHeader>
|
||||
<PageDetailEditor
|
||||
isPublic={true}
|
||||
pageId={pageId}
|
||||
workspace={publicWorkspace}
|
||||
onLoad={onLoad}
|
||||
onInit={initEmptyPage}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const PublicWorkspaceDetailPage: NextPageWithLayout = () => {
|
||||
const router = useRouter();
|
||||
const [workspaceId, setWorkspaceId] = useAtom(publicWorkspaceIdAtom);
|
||||
const [pageId, setPageId] = useAtom(publicWorkspacePageIdAtom);
|
||||
useEffect(() => {
|
||||
if (!router.isReady) {
|
||||
return;
|
||||
}
|
||||
if (typeof router.query.workspaceId === 'string') {
|
||||
setWorkspaceId(router.query.workspaceId);
|
||||
}
|
||||
if (typeof router.query.pageId === 'string') {
|
||||
setPageId(router.query.pageId);
|
||||
}
|
||||
}, [
|
||||
router.isReady,
|
||||
router.query.pageId,
|
||||
router.query.workspaceId,
|
||||
setPageId,
|
||||
setWorkspaceId,
|
||||
]);
|
||||
if (!router.isReady || !workspaceId || !pageId) {
|
||||
return <PageLoading />;
|
||||
}
|
||||
return (
|
||||
<Suspense fallback={<PageLoading />}>
|
||||
<PublicWorkspaceDetailPageInner />
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
export default PublicWorkspaceDetailPage;
|
||||
|
||||
PublicWorkspaceDetailPage.getLayout = page => {
|
||||
return <PublicWorkspaceLayout>{page}</PublicWorkspaceLayout>;
|
||||
};
|
||||
@@ -1,136 +0,0 @@
|
||||
import type { SettingPanel } from '@affine/env/workspace';
|
||||
import {
|
||||
settingPanel,
|
||||
settingPanelValues,
|
||||
WorkspaceSubPath,
|
||||
} from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { assertExists } from '@blocksuite/store';
|
||||
import { useAtom } from 'jotai';
|
||||
import { atomWithStorage } from 'jotai/utils';
|
||||
import Head from 'next/head';
|
||||
import type { NextRouter } from 'next/router';
|
||||
import { useRouter } from 'next/router';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { getUIAdapter } from '../../../adapters/workspace';
|
||||
import { PageLoading } from '../../../components/pure/loading';
|
||||
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
|
||||
import { useOnTransformWorkspace } from '../../../hooks/root/use-on-transform-workspace';
|
||||
import { useAppHelper } from '../../../hooks/use-workspaces';
|
||||
import { WorkspaceLayout } from '../../../layouts/workspace-layout';
|
||||
import type { NextPageWithLayout } from '../../../shared';
|
||||
|
||||
const settingPanelAtom = atomWithStorage<SettingPanel>(
|
||||
'workspaceId',
|
||||
settingPanel.General
|
||||
);
|
||||
|
||||
function useTabRouterSync(
|
||||
router: NextRouter,
|
||||
currentTab: SettingPanel,
|
||||
setCurrentTab: (tab: SettingPanel) => void
|
||||
): void {
|
||||
if (!router.isReady) {
|
||||
return;
|
||||
}
|
||||
const queryCurrentTab =
|
||||
typeof router.query.currentTab === 'string'
|
||||
? router.query.currentTab
|
||||
: null;
|
||||
if (
|
||||
(queryCurrentTab !== null &&
|
||||
settingPanelValues.indexOf(queryCurrentTab as SettingPanel) === -1) ||
|
||||
settingPanelValues.indexOf(currentTab as SettingPanel) === -1
|
||||
) {
|
||||
setCurrentTab(settingPanel.General);
|
||||
router
|
||||
.replace({
|
||||
pathname: router.pathname,
|
||||
query: {
|
||||
...router.query,
|
||||
currentTab: settingPanel.General,
|
||||
},
|
||||
})
|
||||
.catch(console.error);
|
||||
} else if (queryCurrentTab !== currentTab) {
|
||||
router
|
||||
.replace({
|
||||
pathname: router.pathname,
|
||||
query: {
|
||||
...router.query,
|
||||
currentTab: currentTab,
|
||||
},
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
}
|
||||
|
||||
const SettingPage: NextPageWithLayout = () => {
|
||||
const router = useRouter();
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
const t = useAFFiNEI18N();
|
||||
const [currentTab, setCurrentTab] = useAtom(settingPanelAtom);
|
||||
const onChangeTab = useCallback(
|
||||
(tab: SettingPanel) => {
|
||||
setCurrentTab(tab as SettingPanel);
|
||||
router
|
||||
.push({
|
||||
pathname: router.pathname,
|
||||
query: {
|
||||
...router.query,
|
||||
currentTab: tab,
|
||||
},
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
},
|
||||
[router, setCurrentTab]
|
||||
);
|
||||
|
||||
useTabRouterSync(router, currentTab, setCurrentTab);
|
||||
|
||||
const helper = useAppHelper();
|
||||
|
||||
const onDeleteWorkspace = useCallback(async () => {
|
||||
assertExists(currentWorkspace);
|
||||
const workspaceId = currentWorkspace.id;
|
||||
return helper.deleteWorkspace(workspaceId);
|
||||
}, [currentWorkspace, helper]);
|
||||
const onTransformWorkspace = useOnTransformWorkspace();
|
||||
if (
|
||||
!router.isReady ||
|
||||
currentWorkspace === null ||
|
||||
settingPanelValues.indexOf(currentTab as SettingPanel) === -1
|
||||
) {
|
||||
return <PageLoading />;
|
||||
}
|
||||
const { SettingsDetail, Header } = getUIAdapter(currentWorkspace.flavour);
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{t['Settings']()} - AFFiNE</title>
|
||||
</Head>
|
||||
<Header
|
||||
currentWorkspace={currentWorkspace}
|
||||
currentEntry={{
|
||||
subPath: WorkspaceSubPath.SETTING,
|
||||
}}
|
||||
/>
|
||||
<SettingsDetail
|
||||
onTransformWorkspace={onTransformWorkspace}
|
||||
onDeleteWorkspace={onDeleteWorkspace}
|
||||
currentWorkspace={currentWorkspace}
|
||||
currentTab={currentTab as SettingPanel}
|
||||
onChangeTab={onChangeTab}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingPage;
|
||||
|
||||
SettingPage.getLayout = page => {
|
||||
return <WorkspaceLayout>{page}</WorkspaceLayout>;
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
import type React from 'react';
|
||||
import { memo } from 'react';
|
||||
import type { SWRConfiguration } from 'swr';
|
||||
import { SWRConfig } from 'swr';
|
||||
|
||||
import { fetcher } from '../adapters/affine/fetcher';
|
||||
|
||||
const config: SWRConfiguration = {
|
||||
suspense: true,
|
||||
fetcher,
|
||||
};
|
||||
|
||||
export const AffineSwrConfigProvider = memo<React.PropsWithChildren>(
|
||||
function AffineSWRConfigProvider({ children }) {
|
||||
return <SWRConfig value={config}>{children}</SWRConfig>;
|
||||
}
|
||||
);
|
||||
@@ -15,9 +15,6 @@ import {
|
||||
openOnboardingModalAtom,
|
||||
openWorkspacesModalAtom,
|
||||
} from '../atoms';
|
||||
import { useAffineLogIn } from '../hooks/affine/use-affine-log-in';
|
||||
import { useAffineLogOut } from '../hooks/affine/use-affine-log-out';
|
||||
import { useCurrentUser } from '../hooks/current/use-current-user';
|
||||
import { useRouterHelper } from '../hooks/use-router-helper';
|
||||
import { useWorkspaces } from '../hooks/use-workspaces';
|
||||
|
||||
@@ -90,7 +87,6 @@ export const AllWorkspaceModals = (): ReactElement => {
|
||||
|
||||
const router = useRouter();
|
||||
const { jumpToSubPath } = useRouterHelper(router);
|
||||
const user = useCurrentUser();
|
||||
const workspaces = useWorkspaces();
|
||||
const setWorkspaces = useSetAtom(rootWorkspacesMetadataAtom);
|
||||
const [currentWorkspaceId, setCurrentWorkspaceId] = useAtom(
|
||||
@@ -102,7 +98,6 @@ export const AllWorkspaceModals = (): ReactElement => {
|
||||
<Suspense>
|
||||
<WorkspaceListModal
|
||||
disabled={transitioning}
|
||||
user={user}
|
||||
workspaces={workspaces}
|
||||
currentWorkspaceId={currentWorkspaceId}
|
||||
open={
|
||||
@@ -146,8 +141,6 @@ export const AllWorkspaceModals = (): ReactElement => {
|
||||
},
|
||||
[jumpToSubPath, setCurrentWorkspaceId, setOpenWorkspacesModal]
|
||||
)}
|
||||
onClickLogin={useAffineLogIn()}
|
||||
onClickLogout={useAffineLogOut()}
|
||||
onNewWorkspace={useCallback(() => {
|
||||
setOpenCreateWorkspaceModal('new');
|
||||
}, [setOpenCreateWorkspaceModal])}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type {
|
||||
AffineLegacyCloudWorkspace,
|
||||
AffineCloudWorkspace,
|
||||
LocalWorkspace,
|
||||
} from '@affine/env/workspace';
|
||||
import type { AffinePublicWorkspace } from '@affine/env/workspace';
|
||||
@@ -11,7 +11,7 @@ import type { ReactElement, ReactNode } from 'react';
|
||||
export { BlockSuiteWorkspace };
|
||||
|
||||
export type AffineOfficialWorkspace =
|
||||
| AffineLegacyCloudWorkspace
|
||||
| AffineCloudWorkspace
|
||||
| LocalWorkspace
|
||||
| AffinePublicWorkspace;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user