refactor: remove legacy cloud (#2987)

This commit is contained in:
Alex Yang
2023-07-03 22:29:37 +08:00
parent 1887a36ca5
commit dedb4ba833
87 changed files with 148 additions and 6383 deletions

View File

@@ -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;

View File

@@ -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}
/>
);
},
},
};

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,
},
},

View File

@@ -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);
}
);

View File

@@ -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
)
);

View File

@@ -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 (
<>

View File

@@ -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']()}

View File

@@ -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);

View File

@@ -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']()}>

View File

@@ -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();
};

View File

@@ -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',
};
});

View File

@@ -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',
});

View File

@@ -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

View File

@@ -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} />;

View File

@@ -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}

View File

@@ -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;

View File

@@ -1,6 +1,6 @@
import type {
AffineLegacyCloudWorkspace,
AffineCloudWorkspace,
LocalWorkspace,
} from '@affine/env/workspace';
export type Workspace = AffineLegacyCloudWorkspace | LocalWorkspace;
export type Workspace = AffineCloudWorkspace | LocalWorkspace;

View File

@@ -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"

View File

@@ -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)',
});

View File

@@ -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>
);
};

View File

@@ -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();
};

View File

@@ -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',
};
});

View File

@@ -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)``;

View File

@@ -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>
</>
);
};

View File

@@ -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>
);
};

View File

@@ -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',
// };
// });

View File

@@ -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>
);
};

View File

@@ -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>
);
}

View File

@@ -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>
);
};

View File

@@ -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',
};
});

View File

@@ -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',
};
});

View File

@@ -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();
};

View File

@@ -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>
&nbsp;
<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>
</>
);
};

View File

@@ -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} />;

View File

@@ -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;

View File

@@ -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}

View File

@@ -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) => {

View File

@@ -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>
);
};

View File

@@ -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;
});

View File

@@ -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>
);

View File

@@ -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} />

View File

@@ -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]);
}

View File

@@ -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]);
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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,
};
}

View File

@@ -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]
);
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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]
);
}

View File

@@ -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 (

View File

@@ -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>

View File

@@ -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;

View File

@@ -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',
},
},
};
});

View File

@@ -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>;
};

View File

@@ -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>;
};

View File

@@ -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>;
};

View File

@@ -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>;
}
);

View File

@@ -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])}

View File

@@ -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;