refactor(core): new workspace selector and create dialog (#10323)
This commit is contained in:
@@ -25,6 +25,8 @@ export interface MenuItemProps
|
|||||||
suffix?: ReactNode;
|
suffix?: ReactNode;
|
||||||
prefixIcon?: ReactNode;
|
prefixIcon?: ReactNode;
|
||||||
suffixIcon?: ReactNode;
|
suffixIcon?: ReactNode;
|
||||||
|
prefixIconClassName?: string;
|
||||||
|
suffixIconClassName?: string;
|
||||||
checked?: boolean;
|
checked?: boolean;
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
block?: boolean;
|
block?: boolean;
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ export const useMenuItem = <T extends MenuItemProps>({
|
|||||||
className: propsClassName,
|
className: propsClassName,
|
||||||
prefix,
|
prefix,
|
||||||
prefixIcon,
|
prefixIcon,
|
||||||
|
prefixIconClassName,
|
||||||
suffix,
|
suffix,
|
||||||
suffixIcon,
|
suffixIcon,
|
||||||
|
suffixIconClassName,
|
||||||
checked,
|
checked,
|
||||||
selected,
|
selected,
|
||||||
block,
|
block,
|
||||||
@@ -38,13 +40,17 @@ export const useMenuItem = <T extends MenuItemProps>({
|
|||||||
{prefix}
|
{prefix}
|
||||||
|
|
||||||
{prefixIcon ? (
|
{prefixIcon ? (
|
||||||
<div className={styles.menuItemIcon}>{prefixIcon}</div>
|
<div className={clsx(styles.menuItemIcon, prefixIconClassName)}>
|
||||||
|
{prefixIcon}
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<span className={styles.menuSpan}>{propsChildren}</span>
|
<span className={styles.menuSpan}>{propsChildren}</span>
|
||||||
|
|
||||||
{suffixIcon ? (
|
{suffixIcon ? (
|
||||||
<div className={styles.menuItemIcon}>{suffixIcon}</div>
|
<div className={clsx(styles.menuItemIcon, suffixIconClassName)}>
|
||||||
|
{suffixIcon}
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{suffix}
|
{suffix}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export type ScrollableContainerProps = {
|
|||||||
viewPortClassName?: string;
|
viewPortClassName?: string;
|
||||||
styles?: React.CSSProperties;
|
styles?: React.CSSProperties;
|
||||||
scrollBarClassName?: string;
|
scrollBarClassName?: string;
|
||||||
|
scrollThumbClassName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ScrollableContainer = ({
|
export const ScrollableContainer = ({
|
||||||
@@ -22,6 +23,7 @@ export const ScrollableContainer = ({
|
|||||||
styles: _styles,
|
styles: _styles,
|
||||||
viewPortClassName,
|
viewPortClassName,
|
||||||
scrollBarClassName,
|
scrollBarClassName,
|
||||||
|
scrollThumbClassName,
|
||||||
}: PropsWithChildren<ScrollableContainerProps>) => {
|
}: PropsWithChildren<ScrollableContainerProps>) => {
|
||||||
const [setContainer, hasScrollTop] = useHasScrollTop();
|
const [setContainer, hasScrollTop] = useHasScrollTop();
|
||||||
return (
|
return (
|
||||||
@@ -45,7 +47,9 @@ export const ScrollableContainer = ({
|
|||||||
[styles.TableScrollbar]: inTableView,
|
[styles.TableScrollbar]: inTableView,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<ScrollArea.Thumb className={styles.scrollbarThumb} />
|
<ScrollArea.Thumb
|
||||||
|
className={clsx(styles.scrollbarThumb, scrollThumbClassName)}
|
||||||
|
/>
|
||||||
</ScrollArea.Scrollbar>
|
</ScrollArea.Scrollbar>
|
||||||
</ScrollArea.Root>
|
</ScrollArea.Root>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -97,6 +97,8 @@ export const WorkspaceSelector = ({
|
|||||||
...menuContentOptions,
|
...menuContentOptions,
|
||||||
style: {
|
style: {
|
||||||
width: '300px',
|
width: '300px',
|
||||||
|
maxHeight: 'min(800px, calc(100vh - 200px))',
|
||||||
|
padding: 0,
|
||||||
...menuContentOptions?.style,
|
...menuContentOptions?.style,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,23 +1,5 @@
|
|||||||
import { cssVar } from '@toeverything/theme';
|
|
||||||
import { style } from '@vanilla-extract/css';
|
import { style } from '@vanilla-extract/css';
|
||||||
export const ItemContainer = style({
|
|
||||||
display: 'flex',
|
export const addServerDividerWrapper = style({
|
||||||
alignItems: 'center',
|
padding: '0px 12px',
|
||||||
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',
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { PlusIcon } from '@blocksuite/icons/rc';
|
import { PlusIcon } from '@blocksuite/icons/rc';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
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 t = useI18n();
|
||||||
|
const globalDialogService = useService(GlobalDialogService);
|
||||||
const featureFlagService = useService(FeatureFlagService);
|
const featureFlagService = useService(FeatureFlagService);
|
||||||
const enableMultipleServer = useLiveData(
|
const enableMultipleServer = useLiveData(
|
||||||
featureFlagService.flags.enable_multiple_cloud_servers.$
|
featureFlagService.flags.enable_multiple_cloud_servers.$
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onAddServer = useCallback(() => {
|
||||||
|
globalDialogService.open('sign-in', { step: 'addSelfhosted' });
|
||||||
|
}, [globalDialogService]);
|
||||||
|
|
||||||
if (!enableMultipleServer) {
|
if (!enableMultipleServer) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
|
<div className={addServerDividerWrapper}>
|
||||||
|
<Divider size="thinner" />
|
||||||
|
</div>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
block={true}
|
block={true}
|
||||||
prefixIcon={<PlusIcon />}
|
prefixIcon={<PlusIcon />}
|
||||||
|
prefixIconClassName={prefixIcon}
|
||||||
onClick={onAddServer}
|
onClick={onAddServer}
|
||||||
data-testid="new-server"
|
data-testid="new-server"
|
||||||
className={styles.ItemContainer}
|
className={ItemContainer}
|
||||||
>
|
>
|
||||||
<div className={styles.ItemText}>
|
<div className={ItemText}>
|
||||||
{t['com.affine.workspaceList.addServer']()}
|
{t['com.affine.workspaceList.addServer']()}
|
||||||
</div>
|
</div>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,21 +1,28 @@
|
|||||||
import { cssVar } from '@toeverything/theme';
|
import { cssVar } from '@toeverything/theme';
|
||||||
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
import { style } from '@vanilla-extract/css';
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
export const ItemContainer = style({
|
export const ItemContainer = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'flex-start',
|
justifyContent: 'flex-start',
|
||||||
padding: '8px 14px',
|
padding: '6px 16px 6px 11px',
|
||||||
gap: '14px',
|
gap: '12px',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
transition: 'background-color 0.2s',
|
transition: 'background-color 0.2s',
|
||||||
fontSize: '24px',
|
fontSize: '24px',
|
||||||
color: cssVar('iconSecondary'),
|
});
|
||||||
|
export const prefixIcon = style({
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
fontSize: 24,
|
||||||
|
color: cssVarV2.icon.secondary,
|
||||||
});
|
});
|
||||||
export const ItemText = style({
|
export const ItemText = style({
|
||||||
fontSize: cssVar('fontSm'),
|
fontSize: cssVar('fontSm'),
|
||||||
lineHeight: '22px',
|
lineHeight: '22px',
|
||||||
color: cssVar('textSecondaryColor'),
|
color: cssVarV2.text.secondary,
|
||||||
fontWeight: 400,
|
fontWeight: 400,
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
|
|||||||
@@ -20,11 +20,12 @@ export const AddWorkspace = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
{BUILD_CONFIG.isElectron && (
|
{BUILD_CONFIG.isElectron && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
block={true}
|
block={true}
|
||||||
prefixIcon={<ImportIcon />}
|
prefixIcon={<ImportIcon />}
|
||||||
|
prefixIconClassName={styles.prefixIcon}
|
||||||
onClick={onAddWorkspace}
|
onClick={onAddWorkspace}
|
||||||
data-testid="add-workspace"
|
data-testid="add-workspace"
|
||||||
className={styles.ItemContainer}
|
className={styles.ItemContainer}
|
||||||
@@ -37,6 +38,7 @@ export const AddWorkspace = ({
|
|||||||
<MenuItem
|
<MenuItem
|
||||||
block={true}
|
block={true}
|
||||||
prefixIcon={<PlusIcon />}
|
prefixIcon={<PlusIcon />}
|
||||||
|
prefixIconClassName={styles.prefixIcon}
|
||||||
onClick={onNewWorkspace}
|
onClick={onNewWorkspace}
|
||||||
data-testid="new-workspace"
|
data-testid="new-workspace"
|
||||||
className={styles.ItemContainer}
|
className={styles.ItemContainer}
|
||||||
@@ -47,6 +49,6 @@ export const AddWorkspace = ({
|
|||||||
: t['com.affine.workspaceList.addWorkspace.create-cloud']()}
|
: t['com.affine.workspaceList.addWorkspace.create-cloud']()}
|
||||||
</div>
|
</div>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,26 @@
|
|||||||
import { cssVar } from '@toeverything/theme';
|
import { cssVar } from '@toeverything/theme';
|
||||||
import { style } from '@vanilla-extract/css';
|
import { style } from '@vanilla-extract/css';
|
||||||
export const workspaceListWrapper = style({
|
|
||||||
|
export const workspaceScrollArea = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
width: '100%',
|
|
||||||
flexDirection: 'column',
|
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({
|
export const signInWrapper = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ScrollableContainer } from '@affine/component';
|
||||||
import { MenuItem } from '@affine/component/ui/menu';
|
import { MenuItem } from '@affine/component/ui/menu';
|
||||||
import { AuthService } from '@affine/core/modules/cloud';
|
import { AuthService } from '@affine/core/modules/cloud';
|
||||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||||
@@ -9,7 +10,6 @@ import { Logo1Icon } from '@blocksuite/icons/rc';
|
|||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { AddServer } from './add-server';
|
|
||||||
import { AddWorkspace } from './add-workspace';
|
import { AddWorkspace } from './add-workspace';
|
||||||
import * as styles from './index.css';
|
import * as styles from './index.css';
|
||||||
import { AFFiNEWorkspaceList } from './workspace-list';
|
import { AFFiNEWorkspaceList } from './workspace-list';
|
||||||
@@ -58,7 +58,7 @@ interface UserWithWorkspaceListProps {
|
|||||||
showEnableCloudButton?: boolean;
|
showEnableCloudButton?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserWithWorkspaceListInner = ({
|
export const UserWithWorkspaceList = ({
|
||||||
onEventEnd,
|
onEventEnd,
|
||||||
onClickWorkspace,
|
onClickWorkspace,
|
||||||
onCreatedWorkspace,
|
onCreatedWorkspace,
|
||||||
@@ -109,26 +109,26 @@ const UserWithWorkspaceListInner = ({
|
|||||||
onEventEnd?.();
|
onEventEnd?.();
|
||||||
}, [globalDialogService, onCreatedWorkspace, onEventEnd]);
|
}, [globalDialogService, onCreatedWorkspace, onEventEnd]);
|
||||||
|
|
||||||
const onAddServer = useCallback(() => {
|
|
||||||
globalDialogService.open('sign-in', { step: 'addSelfhosted' });
|
|
||||||
}, [globalDialogService]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.workspaceListWrapper}>
|
<>
|
||||||
|
<ScrollableContainer
|
||||||
|
className={styles.workspaceScrollArea}
|
||||||
|
viewPortClassName={styles.workspaceScrollAreaViewport}
|
||||||
|
scrollBarClassName={styles.scrollbar}
|
||||||
|
scrollThumbClassName={styles.scrollbarThumb}
|
||||||
|
>
|
||||||
<AFFiNEWorkspaceList
|
<AFFiNEWorkspaceList
|
||||||
onEventEnd={onEventEnd}
|
onEventEnd={onEventEnd}
|
||||||
onClickWorkspace={onClickWorkspace}
|
onClickWorkspace={onClickWorkspace}
|
||||||
showEnableCloudButton={showEnableCloudButton}
|
showEnableCloudButton={showEnableCloudButton}
|
||||||
/>
|
/>
|
||||||
|
</ScrollableContainer>
|
||||||
|
<div className={styles.workspaceFooter}>
|
||||||
<AddWorkspace
|
<AddWorkspace
|
||||||
onAddWorkspace={onAddWorkspace}
|
onAddWorkspace={onAddWorkspace}
|
||||||
onNewWorkspace={onNewWorkspace}
|
onNewWorkspace={onNewWorkspace}
|
||||||
/>
|
/>
|
||||||
<AddServer onAddServer={onAddServer} />
|
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UserWithWorkspaceList = (props: UserWithWorkspaceListProps) => {
|
|
||||||
return <UserWithWorkspaceListInner {...props} />;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,81 +1,73 @@
|
|||||||
import { cssVar } from '@toeverything/theme';
|
import { cssVar } from '@toeverything/theme';
|
||||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
import { style } from '@vanilla-extract/css';
|
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({
|
export const workspaceServer = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
gap: 4,
|
alignItems: 'center',
|
||||||
paddingLeft: '12px',
|
padding: '0px 8px',
|
||||||
marginBottom: '4px',
|
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({
|
export const workspaceServerContent = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
color: cssVarV2('text/secondary'),
|
});
|
||||||
gap: 4,
|
const ellipsis = style({
|
||||||
width: '100%',
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
});
|
});
|
||||||
export const workspaceServerName = style({
|
export const workspaceServerAccount = style([
|
||||||
display: 'flex',
|
ellipsis,
|
||||||
alignItems: 'center',
|
{
|
||||||
gap: 4,
|
|
||||||
fontWeight: 500,
|
|
||||||
fontSize: cssVar('fontXs'),
|
fontSize: cssVar('fontXs'),
|
||||||
lineHeight: '20px',
|
lineHeight: '20px',
|
||||||
textOverflow: 'ellipsis',
|
color: cssVarV2.text.secondary,
|
||||||
whiteSpace: 'nowrap',
|
marginTop: -1.5,
|
||||||
});
|
},
|
||||||
export const account = style({
|
]);
|
||||||
|
export const workspaceServerName = style([
|
||||||
|
ellipsis,
|
||||||
|
{
|
||||||
fontSize: cssVar('fontXs'),
|
fontSize: cssVar('fontXs'),
|
||||||
overflow: 'hidden',
|
lineHeight: '20px',
|
||||||
width: '100%',
|
fontWeight: 500,
|
||||||
textOverflow: 'ellipsis',
|
color: cssVarV2.text.primary,
|
||||||
whiteSpace: 'nowrap',
|
selectors: {
|
||||||
});
|
[`&:has(~ ${workspaceServerAccount})`]: {
|
||||||
export const workspaceTypeIcon = style({
|
marginBottom: -1.5,
|
||||||
color: cssVarV2('icon/primary'),
|
},
|
||||||
fontSize: '16px',
|
},
|
||||||
});
|
},
|
||||||
export const scrollbar = style({
|
]);
|
||||||
width: '4px',
|
|
||||||
});
|
export const workspaceServerSpacer = style({
|
||||||
export const workspaceCard = style({
|
width: 0,
|
||||||
height: '44px',
|
flexGrow: 1,
|
||||||
padding: '0 12px',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ItemContainer = style({
|
export const workspaceCard = style({
|
||||||
display: 'flex',
|
height: 36,
|
||||||
alignItems: 'center',
|
padding: '7px 12px',
|
||||||
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 ItemText = style({
|
export const workspaceCardInfoContainer = style({
|
||||||
fontSize: cssVar('fontSm'),
|
gap: 12,
|
||||||
lineHeight: '22px',
|
});
|
||||||
color: cssVarV2('text/secondary'),
|
|
||||||
fontWeight: 400,
|
export const serverDivider = style({
|
||||||
whiteSpace: 'nowrap',
|
marginTop: 8,
|
||||||
overflow: 'hidden',
|
marginBottom: 12,
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
import {
|
import { IconButton, Menu, MenuItem } from '@affine/component';
|
||||||
IconButton,
|
|
||||||
Menu,
|
|
||||||
MenuItem,
|
|
||||||
ScrollableContainer,
|
|
||||||
} from '@affine/component';
|
|
||||||
import { Divider } from '@affine/component/ui/divider';
|
import { Divider } from '@affine/component/ui/divider';
|
||||||
import { useEnableCloud } from '@affine/core/components/hooks/affine/use-enable-cloud';
|
import { useEnableCloud } from '@affine/core/components/hooks/affine/use-enable-cloud';
|
||||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||||
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
|
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 { AuthService, ServersService } from '@affine/core/modules/cloud';
|
||||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||||
@@ -17,14 +12,15 @@ import {
|
|||||||
WorkspaceService,
|
WorkspaceService,
|
||||||
WorkspacesService,
|
WorkspacesService,
|
||||||
} from '@affine/core/modules/workspace';
|
} from '@affine/core/modules/workspace';
|
||||||
import { ServerDeploymentType } from '@affine/graphql';
|
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import {
|
import {
|
||||||
|
AccountIcon,
|
||||||
CloudWorkspaceIcon,
|
CloudWorkspaceIcon,
|
||||||
|
DeleteIcon,
|
||||||
LocalWorkspaceIcon,
|
LocalWorkspaceIcon,
|
||||||
MoreHorizontalIcon,
|
MoreHorizontalIcon,
|
||||||
PlusIcon,
|
SelfhostIcon,
|
||||||
TeamWorkspaceIcon,
|
SignOutIcon,
|
||||||
} from '@blocksuite/icons/rc';
|
} from '@blocksuite/icons/rc';
|
||||||
import {
|
import {
|
||||||
FrameworkScope,
|
FrameworkScope,
|
||||||
@@ -35,6 +31,7 @@ import {
|
|||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import { WorkspaceCard } from '../../workspace-card';
|
import { WorkspaceCard } from '../../workspace-card';
|
||||||
|
import { AddServer } from '../add-server';
|
||||||
import * as styles from './index.css';
|
import * as styles from './index.css';
|
||||||
|
|
||||||
interface WorkspaceModalProps {
|
interface WorkspaceModalProps {
|
||||||
@@ -46,6 +43,91 @@ interface WorkspaceModalProps {
|
|||||||
onAddWorkspace: () => void;
|
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 = ({
|
const CloudWorkSpaceList = ({
|
||||||
server,
|
server,
|
||||||
workspaces,
|
workspaces,
|
||||||
@@ -57,7 +139,6 @@ const CloudWorkSpaceList = ({
|
|||||||
onClickWorkspace: (workspaceMetadata: WorkspaceMetadata) => void;
|
onClickWorkspace: (workspaceMetadata: WorkspaceMetadata) => void;
|
||||||
onClickEnableCloud?: (meta: WorkspaceMetadata) => void;
|
onClickEnableCloud?: (meta: WorkspaceMetadata) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const t = useI18n();
|
|
||||||
const globalContextService = useService(GlobalContextService);
|
const globalContextService = useService(GlobalContextService);
|
||||||
const globalDialogService = useService(GlobalDialogService);
|
const globalDialogService = useService(GlobalDialogService);
|
||||||
const serverName = useLiveData(server.config$.selector(c => c.serverName));
|
const serverName = useLiveData(server.config$.selector(c => c.serverName));
|
||||||
@@ -71,8 +152,6 @@ const CloudWorkSpaceList = ({
|
|||||||
globalContextService.globalContext.workspaceFlavour.$
|
globalContextService.globalContext.workspaceFlavour.$
|
||||||
);
|
);
|
||||||
|
|
||||||
const serverType = server.config$.value.type;
|
|
||||||
|
|
||||||
const handleDeleteServer = useCallback(() => {
|
const handleDeleteServer = useCallback(() => {
|
||||||
serversService.removeServer(server.id);
|
serversService.removeServer(server.id);
|
||||||
|
|
||||||
@@ -100,78 +179,23 @@ const CloudWorkSpaceList = ({
|
|||||||
});
|
});
|
||||||
}, [globalDialogService, server.baseUrl]);
|
}, [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 (
|
return (
|
||||||
<div className={styles.workspaceListWrapper}>
|
<>
|
||||||
<div className={styles.workspaceServer}>
|
<WorkspaceServerInfo
|
||||||
<div className={styles.workspaceServerContent}>
|
server={server.id}
|
||||||
<div className={styles.workspaceServerName}>
|
name={serverName}
|
||||||
{serverType === ServerDeploymentType.Affine ? (
|
account={account}
|
||||||
<CloudWorkspaceIcon className={styles.workspaceTypeIcon} />
|
accountStatus={accountStatus}
|
||||||
) : (
|
onDeleteServer={handleDeleteServer}
|
||||||
<TeamWorkspaceIcon className={styles.workspaceTypeIcon} />
|
onSignOut={handleSignOut}
|
||||||
)}
|
onSignIn={handleSignIn}
|
||||||
<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>
|
|
||||||
<WorkspaceList
|
<WorkspaceList
|
||||||
items={workspaces}
|
items={workspaces}
|
||||||
onClick={onClickWorkspace}
|
onClick={onClickWorkspace}
|
||||||
onEnableCloudClick={onClickEnableCloud}
|
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 null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className={styles.workspaceListWrapper}>
|
<>
|
||||||
<div className={styles.workspaceServer}>
|
<WorkspaceServerInfo
|
||||||
<div className={styles.workspaceServerName}>
|
server="local"
|
||||||
<LocalWorkspaceIcon
|
name={t['com.affine.workspaceList.workspaceListType.local']()}
|
||||||
width={14}
|
|
||||||
height={14}
|
|
||||||
className={styles.workspaceTypeIcon}
|
|
||||||
/>
|
/>
|
||||||
{t['com.affine.workspaceList.workspaceListType.local']()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<WorkspaceList
|
<WorkspaceList
|
||||||
items={workspaces}
|
items={workspaces}
|
||||||
onClick={onClickWorkspace}
|
onClick={onClickWorkspace}
|
||||||
onSettingClick={onClickWorkspaceSetting}
|
onSettingClick={onClickWorkspaceSetting}
|
||||||
onEnableCloudClick={onClickEnableCloud}
|
onEnableCloudClick={onClickEnableCloud}
|
||||||
/>
|
/>
|
||||||
<Divider size="thinner" />
|
</>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -224,6 +241,14 @@ export const AFFiNEWorkspaceList = ({
|
|||||||
|
|
||||||
const serversService = useService(ServersService);
|
const serversService = useService(ServersService);
|
||||||
const servers = useLiveData(serversService.servers$);
|
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(
|
const cloudWorkspaces = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -262,12 +287,39 @@ export const AFFiNEWorkspaceList = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollableContainer
|
<>
|
||||||
className={styles.workspaceListsWrapper}
|
{/* 1. affine-cloud */}
|
||||||
scrollBarClassName={styles.scrollbar}
|
<FrameworkScope
|
||||||
|
key={affineCloudServer.id}
|
||||||
|
scope={affineCloudServer.scope}
|
||||||
>
|
>
|
||||||
<div>
|
<CloudWorkSpaceList
|
||||||
{servers.map(server => (
|
server={affineCloudServer}
|
||||||
|
workspaces={cloudWorkspaces.filter(
|
||||||
|
({ flavour }) => flavour === affineCloudServer.id
|
||||||
|
)}
|
||||||
|
onClickWorkspace={handleClickWorkspace}
|
||||||
|
/>
|
||||||
|
</FrameworkScope>
|
||||||
|
{localWorkspaces.length > 0 ||
|
||||||
|
(selfhostServers.length > 0 && (
|
||||||
|
<Divider size="thinner" className={styles.serverDivider} />
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* 2. local */}
|
||||||
|
<LocalWorkspaces
|
||||||
|
workspaces={localWorkspaces}
|
||||||
|
onClickWorkspace={handleClickWorkspace}
|
||||||
|
onClickEnableCloud={
|
||||||
|
showEnableCloudButton ? onClickEnableCloud : undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{selfhostServers.length > 0 && (
|
||||||
|
<Divider size="thinner" className={styles.serverDivider} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 3. selfhost */}
|
||||||
|
{selfhostServers.map((server, index) => (
|
||||||
<FrameworkScope key={server.id} scope={server.scope}>
|
<FrameworkScope key={server.id} scope={server.scope}>
|
||||||
<CloudWorkSpaceList
|
<CloudWorkSpaceList
|
||||||
server={server}
|
server={server}
|
||||||
@@ -276,18 +328,14 @@ export const AFFiNEWorkspaceList = ({
|
|||||||
)}
|
)}
|
||||||
onClickWorkspace={handleClickWorkspace}
|
onClickWorkspace={handleClickWorkspace}
|
||||||
/>
|
/>
|
||||||
<Divider size="thinner" />
|
{index !== selfhostServers.length - 1 && (
|
||||||
|
<Divider size="thinner" className={styles.serverDivider} />
|
||||||
|
)}
|
||||||
</FrameworkScope>
|
</FrameworkScope>
|
||||||
))}
|
))}
|
||||||
</div>
|
<AddServer />
|
||||||
<LocalWorkspaces
|
<Divider size="thinner" />
|
||||||
workspaces={localWorkspaces}
|
</>
|
||||||
onClickWorkspace={handleClickWorkspace}
|
|
||||||
onClickEnableCloud={
|
|
||||||
showEnableCloudButton ? onClickEnableCloud : undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</ScrollableContainer>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -317,9 +365,10 @@ const SortableWorkspaceItem = ({
|
|||||||
return (
|
return (
|
||||||
<WorkspaceCard
|
<WorkspaceCard
|
||||||
className={styles.workspaceCard}
|
className={styles.workspaceCard}
|
||||||
|
infoClassName={styles.workspaceCardInfoContainer}
|
||||||
workspaceMetadata={workspaceMetadata}
|
workspaceMetadata={workspaceMetadata}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
avatarSize={28}
|
avatarSize={22}
|
||||||
active={currentWorkspace?.id === workspaceMetadata.id}
|
active={currentWorkspace?.id === workspaceMetadata.id}
|
||||||
onClickOpenSettings={onSettingClick}
|
onClickOpenSettings={onSettingClick}
|
||||||
onClickEnableCloud={onEnableCloudClick}
|
onClickEnableCloud={onEnableCloudClick}
|
||||||
|
|||||||
@@ -249,6 +249,7 @@ export const WorkspaceCard = forwardRef<
|
|||||||
hideCollaborationIcon?: boolean;
|
hideCollaborationIcon?: boolean;
|
||||||
hideTeamWorkspaceIcon?: boolean;
|
hideTeamWorkspaceIcon?: boolean;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
|
infoClassName?: string;
|
||||||
onClickOpenSettings?: (workspaceMetadata: WorkspaceMetadata) => void;
|
onClickOpenSettings?: (workspaceMetadata: WorkspaceMetadata) => void;
|
||||||
onClickEnableCloud?: (workspaceMetadata: WorkspaceMetadata) => void;
|
onClickEnableCloud?: (workspaceMetadata: WorkspaceMetadata) => void;
|
||||||
}
|
}
|
||||||
@@ -262,6 +263,7 @@ export const WorkspaceCard = forwardRef<
|
|||||||
onClickOpenSettings,
|
onClickOpenSettings,
|
||||||
onClickEnableCloud,
|
onClickEnableCloud,
|
||||||
className,
|
className,
|
||||||
|
infoClassName,
|
||||||
disable,
|
disable,
|
||||||
hideCollaborationIcon,
|
hideCollaborationIcon,
|
||||||
hideTeamWorkspaceIcon,
|
hideTeamWorkspaceIcon,
|
||||||
@@ -296,7 +298,7 @@ export const WorkspaceCard = forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className={styles.infoContainer}>
|
<div className={clsx(styles.infoContainer, infoClassName)}>
|
||||||
{information ? (
|
{information ? (
|
||||||
<WorkspaceAvatar
|
<WorkspaceAvatar
|
||||||
meta={workspaceMetadata}
|
meta={workspaceMetadata}
|
||||||
@@ -332,6 +334,16 @@ export const WorkspaceCard = forwardRef<
|
|||||||
Enable Cloud
|
Enable Cloud
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{onClickOpenSettings && (
|
||||||
|
<div className={styles.settingButton} onClick={onOpenSettings}>
|
||||||
|
<SettingsIcon width={16} height={16} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.suffixIcons}>
|
||||||
{hideCollaborationIcon || information?.isOwner ? null : (
|
{hideCollaborationIcon || information?.isOwner ? null : (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={t['com.affine.settings.workspace.state.joined']()}
|
content={t['com.affine.settings.workspace.state.joined']()}
|
||||||
@@ -340,26 +352,17 @@ export const WorkspaceCard = forwardRef<
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{hideTeamWorkspaceIcon || !information?.isTeam ? null : (
|
{hideTeamWorkspaceIcon || !information?.isTeam ? null : (
|
||||||
<Tooltip
|
<Tooltip content={t['com.affine.settings.workspace.state.team']()}>
|
||||||
content={t['com.affine.settings.workspace.state.team']()}
|
|
||||||
>
|
|
||||||
<TeamWorkspaceIcon className={styles.collaborationIcon} />
|
<TeamWorkspaceIcon className={styles.collaborationIcon} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{onClickOpenSettings && (
|
|
||||||
<div className={styles.settingButton} onClick={onOpenSettings}>
|
|
||||||
<SettingsIcon width={16} height={16} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{showArrowDownIcon && <ArrowDownSmallIcon />}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{active && (
|
{active && (
|
||||||
<div className={styles.activeContainer}>
|
<div className={styles.activeContainer}>
|
||||||
<DoneIcon className={styles.activeIcon} />
|
<DoneIcon className={styles.activeIcon} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{showArrowDownIcon && <ArrowDownSmallIcon />}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export const container = style({
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
maxWidth: 500,
|
maxWidth: 500,
|
||||||
color: cssVarV2('text/primary'),
|
color: cssVarV2('text/primary'),
|
||||||
|
overflow: 'hidden',
|
||||||
':hover': {
|
':hover': {
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
background: cssVar('hoverColor'),
|
background: cssVar('hoverColor'),
|
||||||
@@ -34,6 +35,7 @@ export const infoContainer = style({
|
|||||||
});
|
});
|
||||||
export const activeContainer = style({
|
export const activeContainer = style({
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
|
lineHeight: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const disable = style({
|
export const disable = style({
|
||||||
@@ -187,6 +189,9 @@ export const showOnCardHover = style({
|
|||||||
[`.${container}:hover &`]: {
|
[`.${container}:hover &`]: {
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
},
|
},
|
||||||
|
'&:empty': {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -194,3 +199,14 @@ export const activeIcon = style({
|
|||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: cssVarV2('icon/activated'),
|
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,194 +1,157 @@
|
|||||||
import { Avatar, ConfirmModal, Input, notify, Switch } from '@affine/component';
|
import { Button, ConfirmModal, notify, RowInput } from '@affine/component';
|
||||||
import type { ConfirmModalProps } from '@affine/component/ui/modal';
|
|
||||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
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 {
|
import {
|
||||||
type DialogComponentProps,
|
type DialogComponentProps,
|
||||||
type GLOBAL_DIALOG_SCHEMA,
|
type GLOBAL_DIALOG_SCHEMA,
|
||||||
GlobalDialogService,
|
GlobalDialogService,
|
||||||
} from '@affine/core/modules/dialogs';
|
} 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 { WorkspacesService } from '@affine/core/modules/workspace';
|
||||||
|
import { buildShowcaseWorkspace } from '@affine/core/utils/first-app-data';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { track } from '@affine/track';
|
import track from '@affine/track';
|
||||||
import { FrameworkScope, useLiveData, useService } from '@toeverything/infra';
|
import { FrameworkScope, useLiveData, useService } from '@toeverything/infra';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
import { buildShowcaseWorkspace } from '../../../utils/first-app-data';
|
import * as styles from './index.css';
|
||||||
import * as styles from './dialog.css';
|
import { ServerSelector } from './server-selector';
|
||||||
|
|
||||||
interface NameWorkspaceContentProps extends ConfirmModalProps {
|
const FormSection = ({
|
||||||
loading: boolean;
|
label,
|
||||||
forcedCloud?: boolean;
|
input,
|
||||||
serverId?: string;
|
}: {
|
||||||
onConfirmName: (
|
label: string;
|
||||||
name: string,
|
input: React.ReactNode;
|
||||||
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
|
|
||||||
return (
|
return (
|
||||||
<ConfirmModal
|
<section className={styles.section}>
|
||||||
defaultOpen={true}
|
<label className={styles.label}>{label}</label>
|
||||||
title={t['com.affine.nameWorkspace.title']()}
|
{input}
|
||||||
description={t['com.affine.nameWorkspace.description']()}
|
</section>
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CreateWorkspaceDialog = ({
|
export const CreateWorkspaceDialog = ({
|
||||||
forcedCloud,
|
|
||||||
serverId,
|
serverId,
|
||||||
close,
|
close,
|
||||||
|
...props
|
||||||
}: DialogComponentProps<GLOBAL_DIALOG_SCHEMA['create-workspace']>) => {
|
}: 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 serversService = useService(ServersService);
|
||||||
const featureFlagService = useService(FeatureFlagService);
|
|
||||||
const enableLocalWorkspace = useLiveData(
|
|
||||||
featureFlagService.flags.enable_local_workspace.$
|
|
||||||
);
|
|
||||||
const server = useLiveData(
|
const server = useLiveData(
|
||||||
serverId ? serversService.server$(serverId) : null
|
inputServerId ? serversService.server$(inputServerId) : null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onOpenChange = useCallback(
|
||||||
|
(open: boolean) => {
|
||||||
|
if (!open) close();
|
||||||
|
},
|
||||||
|
[close]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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 [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const onConfirmName = useAsyncCallback(
|
const session = useService(AuthService).session;
|
||||||
async (name: string, workspaceFlavour: string) => {
|
const loginStatus = useLiveData(session.status$);
|
||||||
track.$.$.$.createWorkspace({ flavour: workspaceFlavour });
|
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;
|
if (loading) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
track.$.$.$.createWorkspace({
|
||||||
|
flavour: !server ? 'local' : 'affine-cloud',
|
||||||
|
});
|
||||||
|
|
||||||
// this will be the last step for web for now
|
// this will be the last step for web for now
|
||||||
// fix me later
|
// fix me later
|
||||||
try {
|
try {
|
||||||
const { meta, defaultDocId } = await buildShowcaseWorkspace(
|
const res = await buildShowcaseWorkspace(
|
||||||
workspacesService,
|
workspacesService,
|
||||||
workspaceFlavour,
|
server?.id ?? 'local',
|
||||||
name
|
workspaceName
|
||||||
);
|
);
|
||||||
close({ metadata: meta, defaultDocId });
|
onCreated(res);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
notify.error({
|
notify.error({
|
||||||
@@ -198,29 +161,24 @@ export const CreateWorkspaceDialog = ({
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
},
|
}, [loading, onCreated, server, workspaceName, workspacesService]);
|
||||||
[loading, workspacesService, close]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onOpenChange = useCallback(
|
const handleCheckSessionAndConfirm = useCallback(() => {
|
||||||
(open: boolean) => {
|
if (server && loginStatus !== 'authenticated') {
|
||||||
if (!open) {
|
return openSignInModal();
|
||||||
close();
|
|
||||||
}
|
}
|
||||||
},
|
handleConfirm();
|
||||||
[close]
|
}, [handleConfirm, loginStatus, openSignInModal, server]);
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FrameworkScope scope={server?.scope}>
|
<Button
|
||||||
<NameWorkspaceContent
|
disabled={!workspaceName}
|
||||||
|
data-testid="create-workspace-create-button"
|
||||||
|
variant="primary"
|
||||||
|
onClick={handleCheckSessionAndConfirm}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
serverId={serverId}
|
>
|
||||||
open
|
{t['com.affine.nameWorkspace.button.create']()}
|
||||||
forcedCloud={forcedCloud || !enableLocalWorkspace}
|
</Button>
|
||||||
onOpenChange={onOpenChange}
|
|
||||||
onConfirmName={onConfirmName}
|
|
||||||
/>
|
|
||||||
</FrameworkScope>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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}
|
className={styles.workspaceSelector}
|
||||||
showArrowDownIcon
|
showArrowDownIcon
|
||||||
disable={disabled}
|
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';
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
export const fallback = style({
|
export const fallback = style({
|
||||||
padding: '4px 20px',
|
padding: '4px 16px',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
overflow: 'clip',
|
overflow: 'clip',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export type SettingTab =
|
|||||||
| `workspace:${'preference' | 'properties' | 'members' | 'storage' | 'billing' | 'license'}`;
|
| `workspace:${'preference' | 'properties' | 'members' | 'storage' | 'billing' | 'license'}`;
|
||||||
|
|
||||||
export type GLOBAL_DIALOG_SCHEMA = {
|
export type GLOBAL_DIALOG_SCHEMA = {
|
||||||
'create-workspace': (props: { serverId?: string; forcedCloud?: boolean }) => {
|
'create-workspace': (props: { serverId?: string }) => {
|
||||||
metadata: WorkspaceMetadata;
|
metadata: WorkspaceMetadata;
|
||||||
defaultDocId?: string;
|
defaultDocId?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -666,6 +666,7 @@
|
|||||||
"com.affine.nameWorkspace.description": "A workspace is your virtual space to capture, create and plan as just one person or together as a team.",
|
"com.affine.nameWorkspace.description": "A workspace is your virtual space to capture, create and plan as just one person or together as a team.",
|
||||||
"com.affine.nameWorkspace.placeholder": "Set a workspace name",
|
"com.affine.nameWorkspace.placeholder": "Set a workspace name",
|
||||||
"com.affine.nameWorkspace.subtitle.workspace-name": "Workspace name",
|
"com.affine.nameWorkspace.subtitle.workspace-name": "Workspace name",
|
||||||
|
"com.affine.nameWorkspace.subtitle.workspace-type": "Workspace type",
|
||||||
"com.affine.nameWorkspace.title": "Name your workspace",
|
"com.affine.nameWorkspace.title": "Name your workspace",
|
||||||
"com.affine.new.page-mode": "New page",
|
"com.affine.new.page-mode": "New page",
|
||||||
"com.affine.new_edgeless": "New edgeless",
|
"com.affine.new_edgeless": "New edgeless",
|
||||||
|
|||||||
@@ -3,10 +3,8 @@ import {
|
|||||||
clickNewPageButton,
|
clickNewPageButton,
|
||||||
getBlockSuiteEditorTitle,
|
getBlockSuiteEditorTitle,
|
||||||
} from '@affine-test/kit/utils/page-logic';
|
} from '@affine-test/kit/utils/page-logic';
|
||||||
import {
|
import { clickSideBarSettingButton } from '@affine-test/kit/utils/sidebar';
|
||||||
clickSideBarCurrentWorkspaceBanner,
|
import { createLocalWorkspace } from '@affine-test/kit/utils/workspace';
|
||||||
clickSideBarSettingButton,
|
|
||||||
} from '@affine-test/kit/utils/sidebar';
|
|
||||||
import type { Page } from '@playwright/test';
|
import type { Page } from '@playwright/test';
|
||||||
import { expect } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
|
|
||||||
@@ -112,13 +110,7 @@ test('windows only check', async ({ page }) => {
|
|||||||
test('delete workspace', async ({ page }) => {
|
test('delete workspace', async ({ page }) => {
|
||||||
await clickNewPageButton(page);
|
await clickNewPageButton(page);
|
||||||
|
|
||||||
await clickSideBarCurrentWorkspaceBanner(page);
|
await createLocalWorkspace({ name: 'Delete Me' }, page);
|
||||||
await page.getByTestId('new-workspace').click();
|
|
||||||
await page.getByTestId('create-workspace-input').fill('Delete Me');
|
|
||||||
await page.getByTestId('create-workspace-create-button').click();
|
|
||||||
// await page.getByTestId('create-workspace-continue-button').click({
|
|
||||||
// delay: 100,
|
|
||||||
// });
|
|
||||||
await page.waitForTimeout(1000);
|
await page.waitForTimeout(1000);
|
||||||
await clickSideBarSettingButton(page);
|
await clickSideBarSettingButton(page);
|
||||||
await page.getByTestId('workspace-setting:preference').click();
|
await page.getByTestId('workspace-setting:preference').click();
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
clickNewPageButton,
|
clickNewPageButton,
|
||||||
clickSideBarCurrentWorkspaceBanner,
|
clickSideBarCurrentWorkspaceBanner,
|
||||||
} from '@affine-test/kit/utils/sidebar';
|
} from '@affine-test/kit/utils/sidebar';
|
||||||
|
import { createLocalWorkspace } from '@affine-test/kit/utils/workspace';
|
||||||
import { expect } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
|
|
||||||
@@ -106,12 +107,8 @@ test('export then add', async ({ page, appInfo, workspace }) => {
|
|||||||
|
|
||||||
test('delete workspace and then restore it from backup', async ({ page }) => {
|
test('delete workspace and then restore it from backup', async ({ page }) => {
|
||||||
//#region 1. create a new workspace
|
//#region 1. create a new workspace
|
||||||
await clickSideBarCurrentWorkspaceBanner(page);
|
|
||||||
const newWorkspaceName = 'new-test-name';
|
const newWorkspaceName = 'new-test-name';
|
||||||
|
await createLocalWorkspace({ name: newWorkspaceName }, page);
|
||||||
await page.getByTestId('new-workspace').click();
|
|
||||||
await page.getByTestId('create-workspace-input').fill(newWorkspaceName);
|
|
||||||
await page.getByTestId('create-workspace-create-button').click();
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region 2. create a page in the new workspace (will verify later if it is successfully recovered)
|
//#region 2. create a page in the new workspace (will verify later if it is successfully recovered)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
clickNewPageButton,
|
clickNewPageButton,
|
||||||
waitForEditorLoad,
|
waitForEditorLoad,
|
||||||
} from '@affine-test/kit/utils/page-logic';
|
} from '@affine-test/kit/utils/page-logic';
|
||||||
|
import { createLocalWorkspace } from '@affine-test/kit/utils/workspace';
|
||||||
import { expect } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
|
|
||||||
test('should create a page with a local first avatar and remove it', async ({
|
test('should create a page with a local first avatar and remove it', async ({
|
||||||
@@ -13,10 +14,7 @@ test('should create a page with a local first avatar and remove it', async ({
|
|||||||
await openHomePage(page);
|
await openHomePage(page);
|
||||||
await waitForEditorLoad(page);
|
await waitForEditorLoad(page);
|
||||||
await clickNewPageButton(page);
|
await clickNewPageButton(page);
|
||||||
await page.getByTestId('workspace-name').click();
|
await createLocalWorkspace({ name: 'Test Workspace 1' }, page);
|
||||||
await page.getByTestId('new-workspace').click();
|
|
||||||
await page.getByTestId('create-workspace-input').fill('Test Workspace 1');
|
|
||||||
await page.getByTestId('create-workspace-create-button').click();
|
|
||||||
await page.waitForTimeout(1000);
|
await page.waitForTimeout(1000);
|
||||||
await page.getByTestId('workspace-name').click();
|
await page.getByTestId('workspace-name').click();
|
||||||
await page
|
await page
|
||||||
|
|||||||
@@ -5,21 +5,13 @@ import {
|
|||||||
openSettingModal,
|
openSettingModal,
|
||||||
openWorkspaceSettingPanel,
|
openWorkspaceSettingPanel,
|
||||||
} from '@affine-test/kit/utils/setting';
|
} from '@affine-test/kit/utils/setting';
|
||||||
import { clickSideBarCurrentWorkspaceBanner } from '@affine-test/kit/utils/sidebar';
|
import { createLocalWorkspace } from '@affine-test/kit/utils/workspace';
|
||||||
import { expect } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
|
|
||||||
test('Create new workspace, then delete it', async ({ page, workspace }) => {
|
test('Create new workspace, then delete it', async ({ page, workspace }) => {
|
||||||
await openHomePage(page);
|
await openHomePage(page);
|
||||||
await waitForEditorLoad(page);
|
await waitForEditorLoad(page);
|
||||||
await clickSideBarCurrentWorkspaceBanner(page);
|
await createLocalWorkspace({ name: 'Test Workspace' }, page);
|
||||||
await page.getByTestId('new-workspace').click();
|
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
await page
|
|
||||||
.getByTestId('create-workspace-input')
|
|
||||||
.pressSequentially('Test Workspace', { delay: 50 });
|
|
||||||
const createButton = page.getByTestId('create-workspace-create-button');
|
|
||||||
await createButton.click();
|
|
||||||
await createButton.waitFor({ state: 'hidden' });
|
|
||||||
|
|
||||||
await page.waitForSelector('[data-testid="workspace-name"]');
|
await page.waitForSelector('[data-testid="workspace-name"]');
|
||||||
expect(await page.getByTestId('workspace-name').textContent()).toBe(
|
expect(await page.getByTestId('workspace-name').textContent()).toBe(
|
||||||
@@ -72,11 +64,7 @@ test('Delete last workspace', async ({ page }) => {
|
|||||||
await page.getByTestId('delete-workspace-confirm-button').click();
|
await page.getByTestId('delete-workspace-confirm-button').click();
|
||||||
await openHomePage(page);
|
await openHomePage(page);
|
||||||
await expect(page.getByTestId('new-workspace')).toBeVisible();
|
await expect(page.getByTestId('new-workspace')).toBeVisible();
|
||||||
await page.getByTestId('new-workspace').click();
|
await createLocalWorkspace({ name: 'Test Workspace' }, page, true);
|
||||||
await page
|
|
||||||
.locator('[data-testid="create-workspace-input"]')
|
|
||||||
.pressSequentially('Test Workspace');
|
|
||||||
await page.getByTestId('create-workspace-create-button').click();
|
|
||||||
await page.waitForTimeout(1000);
|
await page.waitForTimeout(1000);
|
||||||
await page.waitForSelector('[data-testid="workspace-name"]');
|
await page.waitForSelector('[data-testid="workspace-name"]');
|
||||||
await expect(page.getByTestId('workspace-name')).toHaveText('Test Workspace');
|
await expect(page.getByTestId('workspace-name')).toHaveText('Test Workspace');
|
||||||
|
|||||||
@@ -15,9 +15,12 @@ export async function openWorkspaceListModal(page: Page) {
|
|||||||
|
|
||||||
export async function createLocalWorkspace(
|
export async function createLocalWorkspace(
|
||||||
params: CreateWorkspaceParams,
|
params: CreateWorkspaceParams,
|
||||||
page: Page
|
page: Page,
|
||||||
|
skipOpenWorkspaceListModal = false
|
||||||
) {
|
) {
|
||||||
|
if (!skipOpenWorkspaceListModal) {
|
||||||
await openWorkspaceListModal(page);
|
await openWorkspaceListModal(page);
|
||||||
|
}
|
||||||
|
|
||||||
// open create workspace modal
|
// open create workspace modal
|
||||||
await page.getByTestId('new-workspace').click();
|
await page.getByTestId('new-workspace').click();
|
||||||
@@ -30,6 +33,11 @@ export async function createLocalWorkspace(
|
|||||||
await page.getByPlaceholder('Set a Workspace name').click();
|
await page.getByPlaceholder('Set a Workspace name').click();
|
||||||
await page.getByPlaceholder('Set a Workspace name').fill(params.name);
|
await page.getByPlaceholder('Set a Workspace name').fill(params.name);
|
||||||
|
|
||||||
|
// select local server
|
||||||
|
await page.getByTestId('server-selector-trigger').click();
|
||||||
|
const serverSelectorList = page.getByTestId('server-selector-list');
|
||||||
|
await serverSelectorList.getByTestId('local').click();
|
||||||
|
|
||||||
// click create button
|
// click create button
|
||||||
await page.getByTestId('create-workspace-create-button').click({
|
await page.getByTestId('create-workspace-create-button').click({
|
||||||
delay: 500,
|
delay: 500,
|
||||||
|
|||||||
Reference in New Issue
Block a user