mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 05:14:54 +00:00
refactor(core): new workspace selector and create dialog (#10323)
This commit is contained in:
@@ -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)',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user