mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00:00
refactor(core): new workspace selector and create dialog (#10323)
This commit is contained in:
@@ -97,6 +97,8 @@ export const WorkspaceSelector = ({
|
||||
...menuContentOptions,
|
||||
style: {
|
||||
width: '300px',
|
||||
maxHeight: 'min(800px, calc(100vh - 200px))',
|
||||
padding: 0,
|
||||
...menuContentOptions?.style,
|
||||
},
|
||||
}}
|
||||
|
||||
@@ -1,23 +1,5 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
export const ItemContainer = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
padding: '8px 14px',
|
||||
gap: '14px',
|
||||
cursor: 'pointer',
|
||||
borderRadius: '8px',
|
||||
transition: 'background-color 0.2s',
|
||||
fontSize: '24px',
|
||||
color: cssVar('iconSecondary'),
|
||||
});
|
||||
export const ItemText = style({
|
||||
fontSize: cssVar('fontSm'),
|
||||
lineHeight: '22px',
|
||||
color: cssVar('textSecondaryColor'),
|
||||
fontWeight: 400,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
|
||||
export const addServerDividerWrapper = style({
|
||||
padding: '0px 12px',
|
||||
});
|
||||
|
||||
@@ -1,34 +1,50 @@
|
||||
import { MenuItem } from '@affine/component/ui/menu';
|
||||
import { Divider, MenuItem } from '@affine/component';
|
||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { PlusIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import * as styles from './index.css';
|
||||
import {
|
||||
ItemContainer,
|
||||
ItemText,
|
||||
prefixIcon,
|
||||
} from '../add-workspace/index.css';
|
||||
import { addServerDividerWrapper } from './index.css';
|
||||
|
||||
export const AddServer = ({ onAddServer }: { onAddServer?: () => void }) => {
|
||||
export const AddServer = () => {
|
||||
const t = useI18n();
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
const featureFlagService = useService(FeatureFlagService);
|
||||
const enableMultipleServer = useLiveData(
|
||||
featureFlagService.flags.enable_multiple_cloud_servers.$
|
||||
);
|
||||
|
||||
const onAddServer = useCallback(() => {
|
||||
globalDialogService.open('sign-in', { step: 'addSelfhosted' });
|
||||
}, [globalDialogService]);
|
||||
|
||||
if (!enableMultipleServer) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<>
|
||||
<div className={addServerDividerWrapper}>
|
||||
<Divider size="thinner" />
|
||||
</div>
|
||||
<MenuItem
|
||||
block={true}
|
||||
prefixIcon={<PlusIcon />}
|
||||
prefixIconClassName={prefixIcon}
|
||||
onClick={onAddServer}
|
||||
data-testid="new-server"
|
||||
className={styles.ItemContainer}
|
||||
className={ItemContainer}
|
||||
>
|
||||
<div className={styles.ItemText}>
|
||||
<div className={ItemText}>
|
||||
{t['com.affine.workspaceList.addServer']()}
|
||||
</div>
|
||||
</MenuItem>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const ItemContainer = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
padding: '8px 14px',
|
||||
gap: '14px',
|
||||
padding: '6px 16px 6px 11px',
|
||||
gap: '12px',
|
||||
cursor: 'pointer',
|
||||
borderRadius: '8px',
|
||||
transition: 'background-color 0.2s',
|
||||
fontSize: '24px',
|
||||
color: cssVar('iconSecondary'),
|
||||
});
|
||||
export const prefixIcon = style({
|
||||
width: 24,
|
||||
height: 24,
|
||||
fontSize: 24,
|
||||
color: cssVarV2.icon.secondary,
|
||||
});
|
||||
export const ItemText = style({
|
||||
fontSize: cssVar('fontSm'),
|
||||
lineHeight: '22px',
|
||||
color: cssVar('textSecondaryColor'),
|
||||
color: cssVarV2.text.secondary,
|
||||
fontWeight: 400,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
|
||||
@@ -20,11 +20,12 @@ export const AddWorkspace = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<>
|
||||
{BUILD_CONFIG.isElectron && (
|
||||
<MenuItem
|
||||
block={true}
|
||||
prefixIcon={<ImportIcon />}
|
||||
prefixIconClassName={styles.prefixIcon}
|
||||
onClick={onAddWorkspace}
|
||||
data-testid="add-workspace"
|
||||
className={styles.ItemContainer}
|
||||
@@ -37,6 +38,7 @@ export const AddWorkspace = ({
|
||||
<MenuItem
|
||||
block={true}
|
||||
prefixIcon={<PlusIcon />}
|
||||
prefixIconClassName={styles.prefixIcon}
|
||||
onClick={onNewWorkspace}
|
||||
data-testid="new-workspace"
|
||||
className={styles.ItemContainer}
|
||||
@@ -47,6 +49,6 @@ export const AddWorkspace = ({
|
||||
: t['com.affine.workspaceList.addWorkspace.create-cloud']()}
|
||||
</div>
|
||||
</MenuItem>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,26 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
export const workspaceListWrapper = style({
|
||||
|
||||
export const workspaceScrollArea = style({
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
flexDirection: 'column',
|
||||
});
|
||||
export const workspaceScrollAreaViewport = style({
|
||||
padding: '10px 8px 0px 8px',
|
||||
});
|
||||
export const workspaceFooter = style({
|
||||
padding: '0px 8px 10px 8px',
|
||||
});
|
||||
export const scrollbar = style({
|
||||
width: 9,
|
||||
padding: '0px 2px',
|
||||
':hover': {
|
||||
padding: 0,
|
||||
},
|
||||
});
|
||||
export const scrollbarThumb = style({
|
||||
width: 5,
|
||||
});
|
||||
export const signInWrapper = style({
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ScrollableContainer } from '@affine/component';
|
||||
import { MenuItem } from '@affine/component/ui/menu';
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||
@@ -9,7 +10,6 @@ import { Logo1Icon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { AddServer } from './add-server';
|
||||
import { AddWorkspace } from './add-workspace';
|
||||
import * as styles from './index.css';
|
||||
import { AFFiNEWorkspaceList } from './workspace-list';
|
||||
@@ -58,7 +58,7 @@ interface UserWithWorkspaceListProps {
|
||||
showEnableCloudButton?: boolean;
|
||||
}
|
||||
|
||||
const UserWithWorkspaceListInner = ({
|
||||
export const UserWithWorkspaceList = ({
|
||||
onEventEnd,
|
||||
onClickWorkspace,
|
||||
onCreatedWorkspace,
|
||||
@@ -109,26 +109,26 @@ const UserWithWorkspaceListInner = ({
|
||||
onEventEnd?.();
|
||||
}, [globalDialogService, onCreatedWorkspace, onEventEnd]);
|
||||
|
||||
const onAddServer = useCallback(() => {
|
||||
globalDialogService.open('sign-in', { step: 'addSelfhosted' });
|
||||
}, [globalDialogService]);
|
||||
|
||||
return (
|
||||
<div className={styles.workspaceListWrapper}>
|
||||
<AFFiNEWorkspaceList
|
||||
onEventEnd={onEventEnd}
|
||||
onClickWorkspace={onClickWorkspace}
|
||||
showEnableCloudButton={showEnableCloudButton}
|
||||
/>
|
||||
<AddWorkspace
|
||||
onAddWorkspace={onAddWorkspace}
|
||||
onNewWorkspace={onNewWorkspace}
|
||||
/>
|
||||
<AddServer onAddServer={onAddServer} />
|
||||
</div>
|
||||
<>
|
||||
<ScrollableContainer
|
||||
className={styles.workspaceScrollArea}
|
||||
viewPortClassName={styles.workspaceScrollAreaViewport}
|
||||
scrollBarClassName={styles.scrollbar}
|
||||
scrollThumbClassName={styles.scrollbarThumb}
|
||||
>
|
||||
<AFFiNEWorkspaceList
|
||||
onEventEnd={onEventEnd}
|
||||
onClickWorkspace={onClickWorkspace}
|
||||
showEnableCloudButton={showEnableCloudButton}
|
||||
/>
|
||||
</ScrollableContainer>
|
||||
<div className={styles.workspaceFooter}>
|
||||
<AddWorkspace
|
||||
onAddWorkspace={onAddWorkspace}
|
||||
onNewWorkspace={onNewWorkspace}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const UserWithWorkspaceList = (props: UserWithWorkspaceListProps) => {
|
||||
return <UserWithWorkspaceListInner {...props} />;
|
||||
};
|
||||
|
||||
@@ -1,81 +1,73 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
export const workspaceListsWrapper = style({
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
flexDirection: 'column',
|
||||
maxHeight: 'calc(100vh - 300px)',
|
||||
});
|
||||
export const workspaceListWrapper = style({
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
});
|
||||
|
||||
export const workspaceServer = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
gap: 4,
|
||||
paddingLeft: '12px',
|
||||
marginBottom: '4px',
|
||||
alignItems: 'center',
|
||||
padding: '0px 8px',
|
||||
gap: 8,
|
||||
marginBottom: 6,
|
||||
});
|
||||
export const workspaceServerIcon = style({
|
||||
border: `1px solid ${cssVarV2.layer.insideBorder.border}`,
|
||||
borderRadius: 4,
|
||||
color: cssVarV2.icon.primary,
|
||||
fontSize: 18,
|
||||
width: 30,
|
||||
height: 30,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
});
|
||||
|
||||
export const workspaceServerContent = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
color: cssVarV2('text/secondary'),
|
||||
gap: 4,
|
||||
width: '100%',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
export const workspaceServerName = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
fontWeight: 500,
|
||||
fontSize: cssVar('fontXs'),
|
||||
lineHeight: '20px',
|
||||
const ellipsis = style({
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
});
|
||||
export const account = style({
|
||||
fontSize: cssVar('fontXs'),
|
||||
overflow: 'hidden',
|
||||
width: '100%',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
});
|
||||
export const workspaceTypeIcon = style({
|
||||
color: cssVarV2('icon/primary'),
|
||||
fontSize: '16px',
|
||||
});
|
||||
export const scrollbar = style({
|
||||
width: '4px',
|
||||
});
|
||||
export const workspaceCard = style({
|
||||
height: '44px',
|
||||
padding: '0 12px',
|
||||
export const workspaceServerAccount = style([
|
||||
ellipsis,
|
||||
{
|
||||
fontSize: cssVar('fontXs'),
|
||||
lineHeight: '20px',
|
||||
color: cssVarV2.text.secondary,
|
||||
marginTop: -1.5,
|
||||
},
|
||||
]);
|
||||
export const workspaceServerName = style([
|
||||
ellipsis,
|
||||
{
|
||||
fontSize: cssVar('fontXs'),
|
||||
lineHeight: '20px',
|
||||
fontWeight: 500,
|
||||
color: cssVarV2.text.primary,
|
||||
selectors: {
|
||||
[`&:has(~ ${workspaceServerAccount})`]: {
|
||||
marginBottom: -1.5,
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
export const workspaceServerSpacer = style({
|
||||
width: 0,
|
||||
flexGrow: 1,
|
||||
});
|
||||
|
||||
export const ItemContainer = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
padding: '8px 14px',
|
||||
gap: '14px',
|
||||
cursor: 'pointer',
|
||||
borderRadius: '8px',
|
||||
transition: 'background-color 0.2s',
|
||||
fontSize: '24px',
|
||||
color: cssVarV2('icon/secondary'),
|
||||
export const workspaceCard = style({
|
||||
height: 36,
|
||||
padding: '7px 12px',
|
||||
});
|
||||
export const ItemText = style({
|
||||
fontSize: cssVar('fontSm'),
|
||||
lineHeight: '22px',
|
||||
color: cssVarV2('text/secondary'),
|
||||
fontWeight: 400,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
export const workspaceCardInfoContainer = style({
|
||||
gap: 12,
|
||||
});
|
||||
|
||||
export const serverDivider = style({
|
||||
marginTop: 8,
|
||||
marginBottom: 12,
|
||||
});
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import {
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
ScrollableContainer,
|
||||
} from '@affine/component';
|
||||
import { IconButton, Menu, MenuItem } from '@affine/component';
|
||||
import { Divider } from '@affine/component/ui/divider';
|
||||
import { useEnableCloud } from '@affine/core/components/hooks/affine/use-enable-cloud';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
|
||||
import type { Server } from '@affine/core/modules/cloud';
|
||||
import type { AuthAccountInfo, Server } from '@affine/core/modules/cloud';
|
||||
import { AuthService, ServersService } from '@affine/core/modules/cloud';
|
||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||
@@ -17,14 +12,15 @@ import {
|
||||
WorkspaceService,
|
||||
WorkspacesService,
|
||||
} from '@affine/core/modules/workspace';
|
||||
import { ServerDeploymentType } from '@affine/graphql';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import {
|
||||
AccountIcon,
|
||||
CloudWorkspaceIcon,
|
||||
DeleteIcon,
|
||||
LocalWorkspaceIcon,
|
||||
MoreHorizontalIcon,
|
||||
PlusIcon,
|
||||
TeamWorkspaceIcon,
|
||||
SelfhostIcon,
|
||||
SignOutIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import {
|
||||
FrameworkScope,
|
||||
@@ -35,6 +31,7 @@ import {
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { WorkspaceCard } from '../../workspace-card';
|
||||
import { AddServer } from '../add-server';
|
||||
import * as styles from './index.css';
|
||||
|
||||
interface WorkspaceModalProps {
|
||||
@@ -46,6 +43,91 @@ interface WorkspaceModalProps {
|
||||
onAddWorkspace: () => void;
|
||||
}
|
||||
|
||||
const WorkspaceServerInfo = ({
|
||||
server,
|
||||
name,
|
||||
account,
|
||||
accountStatus,
|
||||
onDeleteServer,
|
||||
onSignOut,
|
||||
onSignIn,
|
||||
}: {
|
||||
server: string;
|
||||
name: string;
|
||||
account?: AuthAccountInfo | null;
|
||||
accountStatus?: 'authenticated' | 'unauthenticated';
|
||||
onDeleteServer?: () => void;
|
||||
onSignOut?: () => void;
|
||||
onSignIn?: () => void;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const isCloud = server !== 'local';
|
||||
const isAffineCloud = server === 'affine-cloud';
|
||||
const Icon = isAffineCloud
|
||||
? CloudWorkspaceIcon
|
||||
: isCloud
|
||||
? SelfhostIcon
|
||||
: LocalWorkspaceIcon;
|
||||
|
||||
const menuItems = useMemo(
|
||||
() =>
|
||||
[
|
||||
server !== 'affine-cloud' && server !== 'local' && (
|
||||
<MenuItem
|
||||
prefixIcon={<DeleteIcon />}
|
||||
type="danger"
|
||||
key="delete-server"
|
||||
onClick={onDeleteServer}
|
||||
>
|
||||
{t['com.affine.server.delete']()}
|
||||
</MenuItem>
|
||||
),
|
||||
accountStatus === 'authenticated' && (
|
||||
<MenuItem
|
||||
prefixIcon={<SignOutIcon />}
|
||||
key="sign-out"
|
||||
onClick={onSignOut}
|
||||
type="danger"
|
||||
>
|
||||
{t['Sign out']()}
|
||||
</MenuItem>
|
||||
),
|
||||
accountStatus === 'unauthenticated' && (
|
||||
<MenuItem
|
||||
prefixIcon={<AccountIcon />}
|
||||
key="sign-in"
|
||||
onClick={onSignIn}
|
||||
>
|
||||
{t['Sign in']()}
|
||||
</MenuItem>
|
||||
),
|
||||
].filter(Boolean),
|
||||
[accountStatus, onDeleteServer, onSignIn, onSignOut, server, t]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.workspaceServer}>
|
||||
<div className={styles.workspaceServerIcon}>
|
||||
<Icon />
|
||||
</div>
|
||||
<div className={styles.workspaceServerContent}>
|
||||
<div className={styles.workspaceServerName}>{name}</div>
|
||||
{isCloud ? (
|
||||
<div className={styles.workspaceServerAccount}>
|
||||
{account ? account.email : 'Not signed in'}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className={styles.workspaceServerSpacer} />
|
||||
{menuItems.length ? (
|
||||
<Menu items={menuItems}>
|
||||
<IconButton icon={<MoreHorizontalIcon />} />
|
||||
</Menu>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CloudWorkSpaceList = ({
|
||||
server,
|
||||
workspaces,
|
||||
@@ -57,7 +139,6 @@ const CloudWorkSpaceList = ({
|
||||
onClickWorkspace: (workspaceMetadata: WorkspaceMetadata) => void;
|
||||
onClickEnableCloud?: (meta: WorkspaceMetadata) => void;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const globalContextService = useService(GlobalContextService);
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
const serverName = useLiveData(server.config$.selector(c => c.serverName));
|
||||
@@ -71,8 +152,6 @@ const CloudWorkSpaceList = ({
|
||||
globalContextService.globalContext.workspaceFlavour.$
|
||||
);
|
||||
|
||||
const serverType = server.config$.value.type;
|
||||
|
||||
const handleDeleteServer = useCallback(() => {
|
||||
serversService.removeServer(server.id);
|
||||
|
||||
@@ -100,78 +179,23 @@ const CloudWorkSpaceList = ({
|
||||
});
|
||||
}, [globalDialogService, server.baseUrl]);
|
||||
|
||||
const onNewWorkspace = useCallback(() => {
|
||||
globalDialogService.open(
|
||||
'create-workspace',
|
||||
{
|
||||
serverId: server.id,
|
||||
forcedCloud: true,
|
||||
},
|
||||
payload => {
|
||||
if (payload) {
|
||||
navigateHelper.openPage(payload.metadata.id, 'all');
|
||||
}
|
||||
}
|
||||
);
|
||||
}, [globalDialogService, navigateHelper, server.id]);
|
||||
|
||||
return (
|
||||
<div className={styles.workspaceListWrapper}>
|
||||
<div className={styles.workspaceServer}>
|
||||
<div className={styles.workspaceServerContent}>
|
||||
<div className={styles.workspaceServerName}>
|
||||
{serverType === ServerDeploymentType.Affine ? (
|
||||
<CloudWorkspaceIcon className={styles.workspaceTypeIcon} />
|
||||
) : (
|
||||
<TeamWorkspaceIcon className={styles.workspaceTypeIcon} />
|
||||
)}
|
||||
<div className={styles.account}>{serverName}</div>
|
||||
</div>
|
||||
<div className={styles.account}>
|
||||
{account ? account.email : 'Not signed in'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Menu
|
||||
items={[
|
||||
server.id !== 'affine-cloud' && (
|
||||
<MenuItem key="delete-server" onClick={handleDeleteServer}>
|
||||
{t['com.affine.server.delete']()}
|
||||
</MenuItem>
|
||||
),
|
||||
accountStatus === 'authenticated' && (
|
||||
<MenuItem key="sign-out" onClick={handleSignOut}>
|
||||
{t['Sign out']()}
|
||||
</MenuItem>
|
||||
),
|
||||
accountStatus === 'unauthenticated' && (
|
||||
<MenuItem key="sign-in" onClick={handleSignIn}>
|
||||
{t['Sign in']()}
|
||||
</MenuItem>
|
||||
),
|
||||
]}
|
||||
>
|
||||
<div>
|
||||
<IconButton icon={<MoreHorizontalIcon />} />
|
||||
</div>
|
||||
</Menu>
|
||||
</div>
|
||||
<>
|
||||
<WorkspaceServerInfo
|
||||
server={server.id}
|
||||
name={serverName}
|
||||
account={account}
|
||||
accountStatus={accountStatus}
|
||||
onDeleteServer={handleDeleteServer}
|
||||
onSignOut={handleSignOut}
|
||||
onSignIn={handleSignIn}
|
||||
/>
|
||||
<WorkspaceList
|
||||
items={workspaces}
|
||||
onClick={onClickWorkspace}
|
||||
onEnableCloudClick={onClickEnableCloud}
|
||||
/>
|
||||
<MenuItem
|
||||
block={true}
|
||||
prefixIcon={<PlusIcon />}
|
||||
onClick={onNewWorkspace}
|
||||
className={styles.ItemContainer}
|
||||
>
|
||||
<div className={styles.ItemText}>
|
||||
{t['com.affine.workspaceList.addWorkspace.create']()}
|
||||
</div>
|
||||
</MenuItem>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -186,25 +210,18 @@ const LocalWorkspaces = ({
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className={styles.workspaceListWrapper}>
|
||||
<div className={styles.workspaceServer}>
|
||||
<div className={styles.workspaceServerName}>
|
||||
<LocalWorkspaceIcon
|
||||
width={14}
|
||||
height={14}
|
||||
className={styles.workspaceTypeIcon}
|
||||
/>
|
||||
{t['com.affine.workspaceList.workspaceListType.local']()}
|
||||
</div>
|
||||
</div>
|
||||
<>
|
||||
<WorkspaceServerInfo
|
||||
server="local"
|
||||
name={t['com.affine.workspaceList.workspaceListType.local']()}
|
||||
/>
|
||||
<WorkspaceList
|
||||
items={workspaces}
|
||||
onClick={onClickWorkspace}
|
||||
onSettingClick={onClickWorkspaceSetting}
|
||||
onEnableCloudClick={onClickEnableCloud}
|
||||
/>
|
||||
<Divider size="thinner" />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -224,6 +241,14 @@ export const AFFiNEWorkspaceList = ({
|
||||
|
||||
const serversService = useService(ServersService);
|
||||
const servers = useLiveData(serversService.servers$);
|
||||
const affineCloudServer = useMemo(
|
||||
() => servers.find(s => s.id === 'affine-cloud') as Server,
|
||||
[servers]
|
||||
);
|
||||
const selfhostServers = useMemo(
|
||||
() => servers.filter(s => s.id !== 'affine-cloud'),
|
||||
[servers]
|
||||
);
|
||||
|
||||
const cloudWorkspaces = useMemo(
|
||||
() =>
|
||||
@@ -262,24 +287,26 @@ export const AFFiNEWorkspaceList = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<ScrollableContainer
|
||||
className={styles.workspaceListsWrapper}
|
||||
scrollBarClassName={styles.scrollbar}
|
||||
>
|
||||
<div>
|
||||
{servers.map(server => (
|
||||
<FrameworkScope key={server.id} scope={server.scope}>
|
||||
<CloudWorkSpaceList
|
||||
server={server}
|
||||
workspaces={cloudWorkspaces.filter(
|
||||
({ flavour }) => flavour === server.id
|
||||
)}
|
||||
onClickWorkspace={handleClickWorkspace}
|
||||
/>
|
||||
<Divider size="thinner" />
|
||||
</FrameworkScope>
|
||||
<>
|
||||
{/* 1. affine-cloud */}
|
||||
<FrameworkScope
|
||||
key={affineCloudServer.id}
|
||||
scope={affineCloudServer.scope}
|
||||
>
|
||||
<CloudWorkSpaceList
|
||||
server={affineCloudServer}
|
||||
workspaces={cloudWorkspaces.filter(
|
||||
({ flavour }) => flavour === affineCloudServer.id
|
||||
)}
|
||||
onClickWorkspace={handleClickWorkspace}
|
||||
/>
|
||||
</FrameworkScope>
|
||||
{localWorkspaces.length > 0 ||
|
||||
(selfhostServers.length > 0 && (
|
||||
<Divider size="thinner" className={styles.serverDivider} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 2. local */}
|
||||
<LocalWorkspaces
|
||||
workspaces={localWorkspaces}
|
||||
onClickWorkspace={handleClickWorkspace}
|
||||
@@ -287,7 +314,28 @@ export const AFFiNEWorkspaceList = ({
|
||||
showEnableCloudButton ? onClickEnableCloud : undefined
|
||||
}
|
||||
/>
|
||||
</ScrollableContainer>
|
||||
{selfhostServers.length > 0 && (
|
||||
<Divider size="thinner" className={styles.serverDivider} />
|
||||
)}
|
||||
|
||||
{/* 3. selfhost */}
|
||||
{selfhostServers.map((server, index) => (
|
||||
<FrameworkScope key={server.id} scope={server.scope}>
|
||||
<CloudWorkSpaceList
|
||||
server={server}
|
||||
workspaces={cloudWorkspaces.filter(
|
||||
({ flavour }) => flavour === server.id
|
||||
)}
|
||||
onClickWorkspace={handleClickWorkspace}
|
||||
/>
|
||||
{index !== selfhostServers.length - 1 && (
|
||||
<Divider size="thinner" className={styles.serverDivider} />
|
||||
)}
|
||||
</FrameworkScope>
|
||||
))}
|
||||
<AddServer />
|
||||
<Divider size="thinner" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -317,9 +365,10 @@ const SortableWorkspaceItem = ({
|
||||
return (
|
||||
<WorkspaceCard
|
||||
className={styles.workspaceCard}
|
||||
infoClassName={styles.workspaceCardInfoContainer}
|
||||
workspaceMetadata={workspaceMetadata}
|
||||
onClick={handleClick}
|
||||
avatarSize={28}
|
||||
avatarSize={22}
|
||||
active={currentWorkspace?.id === workspaceMetadata.id}
|
||||
onClickOpenSettings={onSettingClick}
|
||||
onClickEnableCloud={onEnableCloudClick}
|
||||
|
||||
@@ -249,6 +249,7 @@ export const WorkspaceCard = forwardRef<
|
||||
hideCollaborationIcon?: boolean;
|
||||
hideTeamWorkspaceIcon?: boolean;
|
||||
active?: boolean;
|
||||
infoClassName?: string;
|
||||
onClickOpenSettings?: (workspaceMetadata: WorkspaceMetadata) => void;
|
||||
onClickEnableCloud?: (workspaceMetadata: WorkspaceMetadata) => void;
|
||||
}
|
||||
@@ -262,6 +263,7 @@ export const WorkspaceCard = forwardRef<
|
||||
onClickOpenSettings,
|
||||
onClickEnableCloud,
|
||||
className,
|
||||
infoClassName,
|
||||
disable,
|
||||
hideCollaborationIcon,
|
||||
hideTeamWorkspaceIcon,
|
||||
@@ -296,7 +298,7 @@ export const WorkspaceCard = forwardRef<
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
<div className={styles.infoContainer}>
|
||||
<div className={clsx(styles.infoContainer, infoClassName)}>
|
||||
{information ? (
|
||||
<WorkspaceAvatar
|
||||
meta={workspaceMetadata}
|
||||
@@ -332,34 +334,35 @@ export const WorkspaceCard = forwardRef<
|
||||
Enable Cloud
|
||||
</Button>
|
||||
) : null}
|
||||
{hideCollaborationIcon || information?.isOwner ? null : (
|
||||
<Tooltip
|
||||
content={t['com.affine.settings.workspace.state.joined']()}
|
||||
>
|
||||
<CollaborationIcon className={styles.collaborationIcon} />
|
||||
</Tooltip>
|
||||
)}
|
||||
{hideTeamWorkspaceIcon || !information?.isTeam ? null : (
|
||||
<Tooltip
|
||||
content={t['com.affine.settings.workspace.state.team']()}
|
||||
>
|
||||
<TeamWorkspaceIcon className={styles.collaborationIcon} />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{onClickOpenSettings && (
|
||||
<div className={styles.settingButton} onClick={onOpenSettings}>
|
||||
<SettingsIcon width={16} height={16} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{showArrowDownIcon && <ArrowDownSmallIcon />}
|
||||
</div>
|
||||
|
||||
{active && (
|
||||
<div className={styles.activeContainer}>
|
||||
<DoneIcon className={styles.activeIcon} />
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.suffixIcons}>
|
||||
{hideCollaborationIcon || information?.isOwner ? null : (
|
||||
<Tooltip
|
||||
content={t['com.affine.settings.workspace.state.joined']()}
|
||||
>
|
||||
<CollaborationIcon className={styles.collaborationIcon} />
|
||||
</Tooltip>
|
||||
)}
|
||||
{hideTeamWorkspaceIcon || !information?.isTeam ? null : (
|
||||
<Tooltip content={t['com.affine.settings.workspace.state.team']()}>
|
||||
<TeamWorkspaceIcon className={styles.collaborationIcon} />
|
||||
</Tooltip>
|
||||
)}
|
||||
{active && (
|
||||
<div className={styles.activeContainer}>
|
||||
<DoneIcon className={styles.activeIcon} />
|
||||
</div>
|
||||
)}
|
||||
{showArrowDownIcon && <ArrowDownSmallIcon />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ export const container = style({
|
||||
width: '100%',
|
||||
maxWidth: 500,
|
||||
color: cssVarV2('text/primary'),
|
||||
overflow: 'hidden',
|
||||
':hover': {
|
||||
cursor: 'pointer',
|
||||
background: cssVar('hoverColor'),
|
||||
@@ -34,6 +35,7 @@ export const infoContainer = style({
|
||||
});
|
||||
export const activeContainer = style({
|
||||
flexShrink: 0,
|
||||
lineHeight: 0,
|
||||
});
|
||||
|
||||
export const disable = style({
|
||||
@@ -187,6 +189,9 @@ export const showOnCardHover = style({
|
||||
[`.${container}:hover &`]: {
|
||||
position: 'relative',
|
||||
},
|
||||
'&:empty': {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -194,3 +199,14 @@ export const activeIcon = style({
|
||||
fontSize: 14,
|
||||
color: cssVarV2('icon/activated'),
|
||||
});
|
||||
|
||||
export const suffixIcons = style({
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
alignItems: 'center',
|
||||
selectors: {
|
||||
'&:empty': {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const header = style({
|
||||
position: 'relative',
|
||||
marginTop: '44px',
|
||||
});
|
||||
|
||||
export const subTitle = style({
|
||||
fontSize: cssVar('fontSm'),
|
||||
color: cssVar('textPrimaryColor'),
|
||||
fontWeight: 600,
|
||||
});
|
||||
|
||||
export const avatarWrapper = style({
|
||||
display: 'flex',
|
||||
margin: '10px 0',
|
||||
});
|
||||
|
||||
export const workspaceNameWrapper = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '8px',
|
||||
padding: '12px 0',
|
||||
});
|
||||
export const affineCloudWrapper = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '6px',
|
||||
paddingTop: '10px',
|
||||
});
|
||||
|
||||
export const card = style({
|
||||
padding: '12px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: cssVar('backgroundSecondaryColor'),
|
||||
minHeight: '114px',
|
||||
position: 'relative',
|
||||
});
|
||||
|
||||
export const cardText = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
gap: '12px',
|
||||
});
|
||||
|
||||
export const cardTitle = style({
|
||||
fontSize: cssVar('fontBase'),
|
||||
color: cssVar('textPrimaryColor'),
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
});
|
||||
export const cardDescription = style({
|
||||
fontSize: cssVar('fontXs'),
|
||||
color: cssVar('textSecondaryColor'),
|
||||
maxWidth: '288px',
|
||||
});
|
||||
|
||||
export const cloudTips = style({
|
||||
fontSize: cssVar('fontXs'),
|
||||
color: cssVar('textSecondaryColor'),
|
||||
});
|
||||
|
||||
export const cloudSvgContainer = style({
|
||||
width: '146px',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
position: 'absolute',
|
||||
bottom: '0',
|
||||
right: '0',
|
||||
pointerEvents: 'none',
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const content = style({
|
||||
// to avoid content clipped
|
||||
width: `calc(100% + 20px)`,
|
||||
padding: '10px 10px 20px 10px',
|
||||
marginLeft: '-10px',
|
||||
});
|
||||
|
||||
export const section = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 8,
|
||||
padding: '12px 0px',
|
||||
});
|
||||
export const label = style({
|
||||
fontSize: cssVar('fontSm'),
|
||||
fontWeight: 600,
|
||||
lineHeight: '22px',
|
||||
color: cssVarV2.text.primary,
|
||||
});
|
||||
const baseFormInput = style({
|
||||
fontSize: 15,
|
||||
fontWeight: 500,
|
||||
lineHeight: '24px',
|
||||
color: cssVarV2.text.primary,
|
||||
border: `1px solid ${cssVarV2.layer.insideBorder.blackBorder}`,
|
||||
});
|
||||
export const input = style([
|
||||
baseFormInput,
|
||||
{
|
||||
borderRadius: 4,
|
||||
padding: '8px 10px',
|
||||
},
|
||||
]);
|
||||
export const select = style([
|
||||
baseFormInput,
|
||||
{
|
||||
borderRadius: 8,
|
||||
padding: '10px',
|
||||
},
|
||||
]);
|
||||
@@ -1,226 +1,184 @@
|
||||
import { Avatar, ConfirmModal, Input, notify, Switch } from '@affine/component';
|
||||
import type { ConfirmModalProps } from '@affine/component/ui/modal';
|
||||
import { Button, ConfirmModal, notify, RowInput } from '@affine/component';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { AuthService, ServersService } from '@affine/core/modules/cloud';
|
||||
import {
|
||||
AuthService,
|
||||
type Server,
|
||||
ServersService,
|
||||
} from '@affine/core/modules/cloud';
|
||||
import {
|
||||
type DialogComponentProps,
|
||||
type GLOBAL_DIALOG_SCHEMA,
|
||||
GlobalDialogService,
|
||||
} from '@affine/core/modules/dialogs';
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import { CloudSvg } from '@affine/core/modules/share-menu';
|
||||
import { WorkspacesService } from '@affine/core/modules/workspace';
|
||||
import { buildShowcaseWorkspace } from '@affine/core/utils/first-app-data';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import track from '@affine/track';
|
||||
import { FrameworkScope, useLiveData, useService } from '@toeverything/infra';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { buildShowcaseWorkspace } from '../../../utils/first-app-data';
|
||||
import * as styles from './dialog.css';
|
||||
import * as styles from './index.css';
|
||||
import { ServerSelector } from './server-selector';
|
||||
|
||||
interface NameWorkspaceContentProps extends ConfirmModalProps {
|
||||
loading: boolean;
|
||||
forcedCloud?: boolean;
|
||||
serverId?: string;
|
||||
onConfirmName: (
|
||||
name: string,
|
||||
workspaceFlavour: string,
|
||||
avatar?: File
|
||||
) => void;
|
||||
}
|
||||
|
||||
const NameWorkspaceContent = ({
|
||||
loading,
|
||||
onConfirmName,
|
||||
forcedCloud,
|
||||
serverId,
|
||||
...props
|
||||
}: NameWorkspaceContentProps) => {
|
||||
const t = useI18n();
|
||||
const [workspaceName, setWorkspaceName] = useState('');
|
||||
|
||||
const [enable, setEnable] = useState(!!forcedCloud);
|
||||
const session = useService(AuthService).session;
|
||||
const loginStatus = useLiveData(session.status$);
|
||||
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
|
||||
const openSignInModal = useCallback(() => {
|
||||
globalDialogService.open('sign-in', {});
|
||||
}, [globalDialogService]);
|
||||
|
||||
const onSwitchChange = useCallback(
|
||||
(checked: boolean) => {
|
||||
if (loginStatus !== 'authenticated') {
|
||||
return openSignInModal();
|
||||
}
|
||||
return setEnable(checked);
|
||||
},
|
||||
[loginStatus, openSignInModal]
|
||||
);
|
||||
|
||||
const handleCreateWorkspace = useCallback(() => {
|
||||
if (loginStatus !== 'authenticated' && enable) {
|
||||
return openSignInModal();
|
||||
}
|
||||
onConfirmName(workspaceName, enable ? serverId || 'affine-cloud' : 'local');
|
||||
}, [
|
||||
enable,
|
||||
loginStatus,
|
||||
onConfirmName,
|
||||
openSignInModal,
|
||||
serverId,
|
||||
workspaceName,
|
||||
]);
|
||||
|
||||
const onEnter = useCallback(() => {
|
||||
if (workspaceName) {
|
||||
handleCreateWorkspace();
|
||||
}
|
||||
}, [handleCreateWorkspace, workspaceName]);
|
||||
|
||||
// Currently, when we create a new workspace and upload an avatar at the same time,
|
||||
// an error occurs after the creation is successful: get blob 404 not found
|
||||
const FormSection = ({
|
||||
label,
|
||||
input,
|
||||
}: {
|
||||
label: string;
|
||||
input: React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<ConfirmModal
|
||||
defaultOpen={true}
|
||||
title={t['com.affine.nameWorkspace.title']()}
|
||||
description={t['com.affine.nameWorkspace.description']()}
|
||||
cancelText={t['com.affine.nameWorkspace.button.cancel']()}
|
||||
confirmText={t['com.affine.nameWorkspace.button.create']()}
|
||||
confirmButtonOptions={{
|
||||
variant: 'primary',
|
||||
loading,
|
||||
disabled: !workspaceName,
|
||||
'data-testid': 'create-workspace-create-button',
|
||||
}}
|
||||
closeButtonOptions={{
|
||||
['data-testid' as string]: 'create-workspace-close-button',
|
||||
}}
|
||||
onConfirm={handleCreateWorkspace}
|
||||
{...props}
|
||||
>
|
||||
<div className={styles.avatarWrapper}>
|
||||
<Avatar size={56} name={workspaceName} colorfulFallback />
|
||||
</div>
|
||||
|
||||
<div className={styles.workspaceNameWrapper}>
|
||||
<div className={styles.subTitle}>
|
||||
{t['com.affine.nameWorkspace.subtitle.workspace-name']()}
|
||||
</div>
|
||||
<Input
|
||||
autoFocus
|
||||
data-testid="create-workspace-input"
|
||||
onEnter={onEnter}
|
||||
placeholder={t['com.affine.nameWorkspace.placeholder']()}
|
||||
maxLength={64}
|
||||
minLength={0}
|
||||
onChange={setWorkspaceName}
|
||||
size="large"
|
||||
/>
|
||||
</div>
|
||||
{!serverId || serverId === 'affine-cloud' ? (
|
||||
<div className={styles.affineCloudWrapper}>
|
||||
<div className={styles.subTitle}>{t['AFFiNE Cloud']()}</div>
|
||||
<div className={styles.card}>
|
||||
<div className={styles.cardText}>
|
||||
<div className={styles.cardTitle}>
|
||||
<span>
|
||||
{t['com.affine.nameWorkspace.affine-cloud.title']()}
|
||||
</span>
|
||||
<Switch
|
||||
checked={enable}
|
||||
onChange={onSwitchChange}
|
||||
disabled={forcedCloud}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.cardDescription}>
|
||||
{t['com.affine.nameWorkspace.affine-cloud.description']()}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.cloudSvgContainer}>
|
||||
<CloudSvg />
|
||||
</div>
|
||||
</div>
|
||||
{forcedCloud && BUILD_CONFIG.isWeb ? (
|
||||
<a
|
||||
className={styles.cloudTips}
|
||||
href={BUILD_CONFIG.downloadUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t['com.affine.nameWorkspace.affine-cloud.web-tips']()}
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</ConfirmModal>
|
||||
<section className={styles.section}>
|
||||
<label className={styles.label}>{label}</label>
|
||||
{input}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export const CreateWorkspaceDialog = ({
|
||||
forcedCloud,
|
||||
serverId,
|
||||
close,
|
||||
...props
|
||||
}: DialogComponentProps<GLOBAL_DIALOG_SCHEMA['create-workspace']>) => {
|
||||
const workspacesService = useService(WorkspacesService);
|
||||
const t = useI18n();
|
||||
|
||||
const [workspaceName, setWorkspaceName] = useState('');
|
||||
const [inputServerId, setInputServerId] = useState(
|
||||
serverId ?? 'affine-cloud'
|
||||
);
|
||||
|
||||
const serversService = useService(ServersService);
|
||||
const featureFlagService = useService(FeatureFlagService);
|
||||
const enableLocalWorkspace = useLiveData(
|
||||
featureFlagService.flags.enable_local_workspace.$
|
||||
);
|
||||
const server = useLiveData(
|
||||
serverId ? serversService.server$(serverId) : null
|
||||
);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const onConfirmName = useAsyncCallback(
|
||||
async (name: string, workspaceFlavour: string) => {
|
||||
track.$.$.$.createWorkspace({ flavour: workspaceFlavour });
|
||||
if (loading) return;
|
||||
setLoading(true);
|
||||
|
||||
// this will be the last step for web for now
|
||||
// fix me later
|
||||
try {
|
||||
const { meta, defaultDocId } = await buildShowcaseWorkspace(
|
||||
workspacesService,
|
||||
workspaceFlavour,
|
||||
name
|
||||
);
|
||||
close({ metadata: meta, defaultDocId });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
notify.error({
|
||||
title: 'Failed to create workspace',
|
||||
message: 'please try again later.',
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[loading, workspacesService, close]
|
||||
inputServerId ? serversService.server$(inputServerId) : null
|
||||
);
|
||||
|
||||
const onOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
if (!open) {
|
||||
close();
|
||||
}
|
||||
if (!open) close();
|
||||
},
|
||||
[close]
|
||||
);
|
||||
|
||||
return (
|
||||
<FrameworkScope scope={server?.scope}>
|
||||
<NameWorkspaceContent
|
||||
loading={loading}
|
||||
serverId={serverId}
|
||||
open
|
||||
forcedCloud={forcedCloud || !enableLocalWorkspace}
|
||||
onOpenChange={onOpenChange}
|
||||
onConfirmName={onConfirmName}
|
||||
<ConfirmModal
|
||||
open
|
||||
onOpenChange={onOpenChange}
|
||||
title={t['com.affine.nameWorkspace.title']()}
|
||||
description={t['com.affine.nameWorkspace.description']()}
|
||||
cancelText={t['com.affine.nameWorkspace.button.cancel']()}
|
||||
closeButtonOptions={{
|
||||
['data-testid' as string]: 'create-workspace-close-button',
|
||||
}}
|
||||
contentOptions={{}}
|
||||
childrenContentClassName={styles.content}
|
||||
customConfirmButton={() => {
|
||||
return (
|
||||
<FrameworkScope scope={server?.scope}>
|
||||
<CustomConfirmButton
|
||||
workspaceName={workspaceName}
|
||||
server={server}
|
||||
onCreated={res =>
|
||||
close({ metadata: res.meta, defaultDocId: res.defaultDocId })
|
||||
}
|
||||
/>
|
||||
</FrameworkScope>
|
||||
);
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<FormSection
|
||||
label={t['com.affine.nameWorkspace.subtitle.workspace-name']()}
|
||||
input={
|
||||
<RowInput
|
||||
autoFocus
|
||||
className={styles.input}
|
||||
data-testid="create-workspace-input"
|
||||
placeholder={t['com.affine.nameWorkspace.placeholder']()}
|
||||
maxLength={64}
|
||||
minLength={0}
|
||||
onChange={setWorkspaceName}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</FrameworkScope>
|
||||
|
||||
<FormSection
|
||||
label={t['com.affine.nameWorkspace.subtitle.workspace-type']()}
|
||||
input={
|
||||
<ServerSelector
|
||||
className={styles.select}
|
||||
selectedId={inputServerId}
|
||||
onChange={setInputServerId}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</ConfirmModal>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomConfirmButton = ({
|
||||
workspaceName,
|
||||
server,
|
||||
onCreated,
|
||||
}: {
|
||||
workspaceName: string;
|
||||
server?: Server | null;
|
||||
onCreated: (res: Awaited<ReturnType<typeof buildShowcaseWorkspace>>) => void;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const session = useService(AuthService).session;
|
||||
const loginStatus = useLiveData(session.status$);
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
const workspacesService = useService(WorkspacesService);
|
||||
|
||||
const openSignInModal = useCallback(() => {
|
||||
globalDialogService.open('sign-in', { server: server?.baseUrl });
|
||||
}, [globalDialogService, server?.baseUrl]);
|
||||
|
||||
const handleConfirm = useAsyncCallback(async () => {
|
||||
if (loading) return;
|
||||
setLoading(true);
|
||||
track.$.$.$.createWorkspace({
|
||||
flavour: !server ? 'local' : 'affine-cloud',
|
||||
});
|
||||
|
||||
// this will be the last step for web for now
|
||||
// fix me later
|
||||
try {
|
||||
const res = await buildShowcaseWorkspace(
|
||||
workspacesService,
|
||||
server?.id ?? 'local',
|
||||
workspaceName
|
||||
);
|
||||
onCreated(res);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
notify.error({
|
||||
title: 'Failed to create workspace',
|
||||
message: 'please try again later.',
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [loading, onCreated, server, workspaceName, workspacesService]);
|
||||
|
||||
const handleCheckSessionAndConfirm = useCallback(() => {
|
||||
if (server && loginStatus !== 'authenticated') {
|
||||
return openSignInModal();
|
||||
}
|
||||
handleConfirm();
|
||||
}, [handleConfirm, loginStatus, openSignInModal, server]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
disabled={!workspaceName}
|
||||
data-testid="create-workspace-create-button"
|
||||
variant="primary"
|
||||
onClick={handleCheckSessionAndConfirm}
|
||||
loading={loading}
|
||||
>
|
||||
{t['com.affine.nameWorkspace.button.create']()}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const trigger = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
});
|
||||
|
||||
export const arrow = style({
|
||||
transition: 'transform 0.2s ease',
|
||||
transform: 'rotate(0deg)',
|
||||
fontSize: 16,
|
||||
color: cssVarV2.icon.primary,
|
||||
selectors: {
|
||||
'&.open': {
|
||||
transform: 'rotate(180deg)',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const list = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 4,
|
||||
});
|
||||
|
||||
export const item = style({
|
||||
padding: 4,
|
||||
gap: 8,
|
||||
});
|
||||
|
||||
export const done = style({
|
||||
color: cssVarV2.icon.primary,
|
||||
fontSize: 20,
|
||||
});
|
||||
@@ -0,0 +1,147 @@
|
||||
import { Menu, MenuItem } from '@affine/component';
|
||||
import { type Server, ServersService } from '@affine/core/modules/cloud';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import {
|
||||
ArrowDownSmallIcon,
|
||||
CloudWorkspaceIcon,
|
||||
DoneIcon,
|
||||
LocalWorkspaceIcon,
|
||||
SelfhostIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
type HTMLAttributes,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import * as styles from './server-selector.css';
|
||||
|
||||
export interface ServerSelectorProps
|
||||
extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {
|
||||
selectedId: Server['id'];
|
||||
onChange: (id: Server['id']) => void;
|
||||
placeholder?: ReactNode;
|
||||
}
|
||||
export const ServerSelector = ({
|
||||
selectedId,
|
||||
onChange,
|
||||
placeholder,
|
||||
className,
|
||||
...props
|
||||
}: ServerSelectorProps) => {
|
||||
const t = useI18n();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const serversService = useService(ServersService);
|
||||
const servers = useLiveData(serversService.servers$);
|
||||
|
||||
const selectedServer = useMemo(() => {
|
||||
return servers.find(s => s.id === selectedId);
|
||||
}, [selectedId, servers]);
|
||||
|
||||
const serverName = useLiveData(
|
||||
selectedServer?.config$.selector(c => c.serverName)
|
||||
);
|
||||
const selectedServerName =
|
||||
selectedId === 'local'
|
||||
? t['com.affine.workspaceList.workspaceListType.local']()
|
||||
: serverName;
|
||||
|
||||
return (
|
||||
<Menu
|
||||
rootOptions={{
|
||||
open,
|
||||
onOpenChange: setOpen,
|
||||
}}
|
||||
contentOptions={{
|
||||
style: {
|
||||
maxWidth: 432,
|
||||
width: 'calc(100dvw - 68px)',
|
||||
},
|
||||
}}
|
||||
items={
|
||||
<ul className={styles.list} data-testid="server-selector-list">
|
||||
<LocalSelectorItem
|
||||
onSelect={onChange}
|
||||
active={selectedId === 'local'}
|
||||
/>
|
||||
{servers.map(server => (
|
||||
<ServerSelectorItem
|
||||
key={server.id}
|
||||
server={server}
|
||||
onSelect={onChange}
|
||||
active={selectedId === server.id}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
}
|
||||
>
|
||||
<div
|
||||
data-testid="server-selector-trigger"
|
||||
className={clsx(styles.trigger, className)}
|
||||
{...props}
|
||||
>
|
||||
{selectedServerName ?? placeholder}
|
||||
<ArrowDownSmallIcon className={clsx(styles.arrow, { open })} />
|
||||
</div>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
const LocalSelectorItem = ({
|
||||
onSelect,
|
||||
active,
|
||||
}: {
|
||||
onSelect?: (id: string) => void;
|
||||
active?: boolean;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const handleSelect = useCallback(() => {
|
||||
onSelect?.('local');
|
||||
}, [onSelect]);
|
||||
return (
|
||||
<MenuItem
|
||||
data-testid="local"
|
||||
className={styles.item}
|
||||
prefixIcon={<LocalWorkspaceIcon />}
|
||||
onClick={handleSelect}
|
||||
suffixIcon={active ? <DoneIcon className={styles.done} /> : null}
|
||||
>
|
||||
{t['com.affine.workspaceList.workspaceListType.local']()}
|
||||
</MenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
const ServerSelectorItem = ({
|
||||
server,
|
||||
onSelect,
|
||||
active,
|
||||
}: {
|
||||
server: Server;
|
||||
onSelect?: (id: string) => void;
|
||||
active?: boolean;
|
||||
}) => {
|
||||
const name = useLiveData(server.config$.selector(c => c.serverName));
|
||||
|
||||
const Icon = server.id === 'affine-cloud' ? CloudWorkspaceIcon : SelfhostIcon;
|
||||
|
||||
const handleSelect = useCallback(() => {
|
||||
onSelect?.(server.id);
|
||||
}, [onSelect, server.id]);
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
data-testid={server.id}
|
||||
className={styles.item}
|
||||
prefixIcon={<Icon />}
|
||||
onClick={handleSelect}
|
||||
suffixIcon={active ? <DoneIcon className={styles.done} /> : null}
|
||||
>
|
||||
{name}
|
||||
</MenuItem>
|
||||
);
|
||||
};
|
||||
@@ -186,6 +186,14 @@ const Dialog = ({
|
||||
className={styles.workspaceSelector}
|
||||
showArrowDownIcon
|
||||
disable={disabled}
|
||||
menuContentOptions={{
|
||||
side: 'top',
|
||||
style: {
|
||||
maxHeight: 'min(600px, calc(50vh + 50px))',
|
||||
width: 352,
|
||||
maxWidth: 'calc(100vw - 20px)',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const fallback = style({
|
||||
padding: '4px 20px',
|
||||
padding: '4px 16px',
|
||||
height: '100%',
|
||||
overflow: 'clip',
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ export type SettingTab =
|
||||
| `workspace:${'preference' | 'properties' | 'members' | 'storage' | 'billing' | 'license'}`;
|
||||
|
||||
export type GLOBAL_DIALOG_SCHEMA = {
|
||||
'create-workspace': (props: { serverId?: string; forcedCloud?: boolean }) => {
|
||||
'create-workspace': (props: { serverId?: string }) => {
|
||||
metadata: WorkspaceMetadata;
|
||||
defaultDocId?: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user