diff --git a/packages/frontend/component/src/ui/menu/menu.types.ts b/packages/frontend/component/src/ui/menu/menu.types.ts index 723075028f..4db5f868dd 100644 --- a/packages/frontend/component/src/ui/menu/menu.types.ts +++ b/packages/frontend/component/src/ui/menu/menu.types.ts @@ -25,6 +25,8 @@ export interface MenuItemProps suffix?: ReactNode; prefixIcon?: ReactNode; suffixIcon?: ReactNode; + prefixIconClassName?: string; + suffixIconClassName?: string; checked?: boolean; selected?: boolean; block?: boolean; diff --git a/packages/frontend/component/src/ui/menu/use-menu-item.tsx b/packages/frontend/component/src/ui/menu/use-menu-item.tsx index 1b3d00f287..2d294aa531 100644 --- a/packages/frontend/component/src/ui/menu/use-menu-item.tsx +++ b/packages/frontend/component/src/ui/menu/use-menu-item.tsx @@ -11,8 +11,10 @@ export const useMenuItem = ({ className: propsClassName, prefix, prefixIcon, + prefixIconClassName, suffix, suffixIcon, + suffixIconClassName, checked, selected, block, @@ -38,13 +40,17 @@ export const useMenuItem = ({ {prefix} {prefixIcon ? ( -
{prefixIcon}
+
+ {prefixIcon} +
) : null} {propsChildren} {suffixIcon ? ( -
{suffixIcon}
+
+ {suffixIcon} +
) : null} {suffix} diff --git a/packages/frontend/component/src/ui/scrollbar/scrollbar.tsx b/packages/frontend/component/src/ui/scrollbar/scrollbar.tsx index 137c8bf284..bcddf8707e 100644 --- a/packages/frontend/component/src/ui/scrollbar/scrollbar.tsx +++ b/packages/frontend/component/src/ui/scrollbar/scrollbar.tsx @@ -12,6 +12,7 @@ export type ScrollableContainerProps = { viewPortClassName?: string; styles?: React.CSSProperties; scrollBarClassName?: string; + scrollThumbClassName?: string; }; export const ScrollableContainer = ({ @@ -22,6 +23,7 @@ export const ScrollableContainer = ({ styles: _styles, viewPortClassName, scrollBarClassName, + scrollThumbClassName, }: PropsWithChildren) => { const [setContainer, hasScrollTop] = useHasScrollTop(); return ( @@ -45,7 +47,9 @@ export const ScrollableContainer = ({ [styles.TableScrollbar]: inTableView, })} > - + ); diff --git a/packages/frontend/core/src/components/workspace-selector/index.tsx b/packages/frontend/core/src/components/workspace-selector/index.tsx index 3da157a68b..018f05292f 100644 --- a/packages/frontend/core/src/components/workspace-selector/index.tsx +++ b/packages/frontend/core/src/components/workspace-selector/index.tsx @@ -97,6 +97,8 @@ export const WorkspaceSelector = ({ ...menuContentOptions, style: { width: '300px', + maxHeight: 'min(800px, calc(100vh - 200px))', + padding: 0, ...menuContentOptions?.style, }, }} diff --git a/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/add-server/index.css.ts b/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/add-server/index.css.ts index dd1183dd2a..af0ef51ffb 100644 --- a/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/add-server/index.css.ts +++ b/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/add-server/index.css.ts @@ -1,23 +1,5 @@ -import { cssVar } from '@toeverything/theme'; import { style } from '@vanilla-extract/css'; -export const ItemContainer = style({ - display: 'flex', - alignItems: 'center', - justifyContent: 'flex-start', - padding: '8px 14px', - gap: '14px', - cursor: 'pointer', - borderRadius: '8px', - transition: 'background-color 0.2s', - fontSize: '24px', - color: cssVar('iconSecondary'), -}); -export const ItemText = style({ - fontSize: cssVar('fontSm'), - lineHeight: '22px', - color: cssVar('textSecondaryColor'), - fontWeight: 400, - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', + +export const addServerDividerWrapper = style({ + padding: '0px 12px', }); diff --git a/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/add-server/index.tsx b/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/add-server/index.tsx index 0753b91542..a16385cebd 100644 --- a/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/add-server/index.tsx +++ b/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/add-server/index.tsx @@ -1,34 +1,50 @@ -import { MenuItem } from '@affine/component/ui/menu'; +import { Divider, MenuItem } from '@affine/component'; +import { GlobalDialogService } from '@affine/core/modules/dialogs'; import { FeatureFlagService } from '@affine/core/modules/feature-flag'; import { useI18n } from '@affine/i18n'; import { PlusIcon } from '@blocksuite/icons/rc'; import { useLiveData, useService } from '@toeverything/infra'; +import { useCallback } from 'react'; -import * as styles from './index.css'; +import { + ItemContainer, + ItemText, + prefixIcon, +} from '../add-workspace/index.css'; +import { addServerDividerWrapper } from './index.css'; -export const AddServer = ({ onAddServer }: { onAddServer?: () => void }) => { +export const AddServer = () => { const t = useI18n(); + const globalDialogService = useService(GlobalDialogService); const featureFlagService = useService(FeatureFlagService); const enableMultipleServer = useLiveData( featureFlagService.flags.enable_multiple_cloud_servers.$ ); + const onAddServer = useCallback(() => { + globalDialogService.open('sign-in', { step: 'addSelfhosted' }); + }, [globalDialogService]); + if (!enableMultipleServer) { return null; } return ( -
+ <> +
+ +
} + prefixIconClassName={prefixIcon} onClick={onAddServer} data-testid="new-server" - className={styles.ItemContainer} + className={ItemContainer} > -
+
{t['com.affine.workspaceList.addServer']()}
-
+ ); }; diff --git a/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/add-workspace/index.css.ts b/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/add-workspace/index.css.ts index dd1183dd2a..3d14c74456 100644 --- a/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/add-workspace/index.css.ts +++ b/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/add-workspace/index.css.ts @@ -1,21 +1,28 @@ import { cssVar } from '@toeverything/theme'; +import { cssVarV2 } from '@toeverything/theme/v2'; import { style } from '@vanilla-extract/css'; + export const ItemContainer = style({ display: 'flex', alignItems: 'center', justifyContent: 'flex-start', - padding: '8px 14px', - gap: '14px', + padding: '6px 16px 6px 11px', + gap: '12px', cursor: 'pointer', borderRadius: '8px', transition: 'background-color 0.2s', fontSize: '24px', - color: cssVar('iconSecondary'), +}); +export const prefixIcon = style({ + width: 24, + height: 24, + fontSize: 24, + color: cssVarV2.icon.secondary, }); export const ItemText = style({ fontSize: cssVar('fontSm'), lineHeight: '22px', - color: cssVar('textSecondaryColor'), + color: cssVarV2.text.secondary, fontWeight: 400, whiteSpace: 'nowrap', overflow: 'hidden', diff --git a/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/add-workspace/index.tsx b/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/add-workspace/index.tsx index 564fd9a0b9..90cea7e20b 100644 --- a/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/add-workspace/index.tsx +++ b/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/add-workspace/index.tsx @@ -20,11 +20,12 @@ export const AddWorkspace = ({ ); return ( -
+ <> {BUILD_CONFIG.isElectron && ( } + prefixIconClassName={styles.prefixIcon} onClick={onAddWorkspace} data-testid="add-workspace" className={styles.ItemContainer} @@ -37,6 +38,7 @@ export const AddWorkspace = ({ } + prefixIconClassName={styles.prefixIcon} onClick={onNewWorkspace} data-testid="new-workspace" className={styles.ItemContainer} @@ -47,6 +49,6 @@ export const AddWorkspace = ({ : t['com.affine.workspaceList.addWorkspace.create-cloud']()}
-
+ ); }; diff --git a/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/index.css.ts b/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/index.css.ts index 86650bbe09..3c6c4c2088 100644 --- a/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/index.css.ts +++ b/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/index.css.ts @@ -1,10 +1,26 @@ import { cssVar } from '@toeverything/theme'; import { style } from '@vanilla-extract/css'; -export const workspaceListWrapper = style({ + +export const workspaceScrollArea = style({ display: 'flex', - width: '100%', flexDirection: 'column', }); +export const workspaceScrollAreaViewport = style({ + padding: '10px 8px 0px 8px', +}); +export const workspaceFooter = style({ + padding: '0px 8px 10px 8px', +}); +export const scrollbar = style({ + width: 9, + padding: '0px 2px', + ':hover': { + padding: 0, + }, +}); +export const scrollbarThumb = style({ + width: 5, +}); export const signInWrapper = style({ display: 'flex', width: '100%', diff --git a/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/index.tsx b/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/index.tsx index a9e38825dd..d8613128b0 100644 --- a/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/index.tsx +++ b/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/index.tsx @@ -1,3 +1,4 @@ +import { ScrollableContainer } from '@affine/component'; import { MenuItem } from '@affine/component/ui/menu'; import { AuthService } from '@affine/core/modules/cloud'; import { GlobalDialogService } from '@affine/core/modules/dialogs'; @@ -9,7 +10,6 @@ import { Logo1Icon } from '@blocksuite/icons/rc'; import { useLiveData, useService } from '@toeverything/infra'; import { useCallback } from 'react'; -import { AddServer } from './add-server'; import { AddWorkspace } from './add-workspace'; import * as styles from './index.css'; import { AFFiNEWorkspaceList } from './workspace-list'; @@ -58,7 +58,7 @@ interface UserWithWorkspaceListProps { showEnableCloudButton?: boolean; } -const UserWithWorkspaceListInner = ({ +export const UserWithWorkspaceList = ({ onEventEnd, onClickWorkspace, onCreatedWorkspace, @@ -109,26 +109,26 @@ const UserWithWorkspaceListInner = ({ onEventEnd?.(); }, [globalDialogService, onCreatedWorkspace, onEventEnd]); - const onAddServer = useCallback(() => { - globalDialogService.open('sign-in', { step: 'addSelfhosted' }); - }, [globalDialogService]); - return ( -
- - - -
+ <> + + + +
+ +
+ ); }; - -export const UserWithWorkspaceList = (props: UserWithWorkspaceListProps) => { - return ; -}; diff --git a/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/workspace-list/index.css.ts b/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/workspace-list/index.css.ts index 20a3bf56e4..6384343634 100644 --- a/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/workspace-list/index.css.ts +++ b/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/workspace-list/index.css.ts @@ -1,81 +1,73 @@ import { cssVar } from '@toeverything/theme'; import { cssVarV2 } from '@toeverything/theme/v2'; import { style } from '@vanilla-extract/css'; -export const workspaceListsWrapper = style({ - display: 'flex', - width: '100%', - flexDirection: 'column', - maxHeight: 'calc(100vh - 300px)', -}); -export const workspaceListWrapper = style({ - display: 'flex', - width: '100%', - flexDirection: 'column', - gap: 2, -}); + export const workspaceServer = style({ display: 'flex', justifyContent: 'space-between', - gap: 4, - paddingLeft: '12px', - marginBottom: '4px', + alignItems: 'center', + padding: '0px 8px', + gap: 8, + marginBottom: 6, +}); +export const workspaceServerIcon = style({ + border: `1px solid ${cssVarV2.layer.insideBorder.border}`, + borderRadius: 4, + color: cssVarV2.icon.primary, + fontSize: 18, + width: 30, + height: 30, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', }); - export const workspaceServerContent = style({ display: 'flex', flexDirection: 'column', - color: cssVarV2('text/secondary'), - gap: 4, - width: '100%', - overflow: 'hidden', }); -export const workspaceServerName = style({ - display: 'flex', - alignItems: 'center', - gap: 4, - fontWeight: 500, - fontSize: cssVar('fontXs'), - lineHeight: '20px', +const ellipsis = style({ textOverflow: 'ellipsis', whiteSpace: 'nowrap', -}); -export const account = style({ - fontSize: cssVar('fontXs'), overflow: 'hidden', - width: '100%', - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', }); -export const workspaceTypeIcon = style({ - color: cssVarV2('icon/primary'), - fontSize: '16px', -}); -export const scrollbar = style({ - width: '4px', -}); -export const workspaceCard = style({ - height: '44px', - padding: '0 12px', +export const workspaceServerAccount = style([ + ellipsis, + { + fontSize: cssVar('fontXs'), + lineHeight: '20px', + color: cssVarV2.text.secondary, + marginTop: -1.5, + }, +]); +export const workspaceServerName = style([ + ellipsis, + { + fontSize: cssVar('fontXs'), + lineHeight: '20px', + fontWeight: 500, + color: cssVarV2.text.primary, + selectors: { + [`&:has(~ ${workspaceServerAccount})`]: { + marginBottom: -1.5, + }, + }, + }, +]); + +export const workspaceServerSpacer = style({ + width: 0, + flexGrow: 1, }); -export const ItemContainer = style({ - display: 'flex', - alignItems: 'center', - justifyContent: 'flex-start', - padding: '8px 14px', - gap: '14px', - cursor: 'pointer', - borderRadius: '8px', - transition: 'background-color 0.2s', - fontSize: '24px', - color: cssVarV2('icon/secondary'), +export const workspaceCard = style({ + height: 36, + padding: '7px 12px', }); -export const ItemText = style({ - fontSize: cssVar('fontSm'), - lineHeight: '22px', - color: cssVarV2('text/secondary'), - fontWeight: 400, - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', +export const workspaceCardInfoContainer = style({ + gap: 12, +}); + +export const serverDivider = style({ + marginTop: 8, + marginBottom: 12, }); diff --git a/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/workspace-list/index.tsx b/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/workspace-list/index.tsx index f9c697acee..aefac037bc 100644 --- a/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/workspace-list/index.tsx +++ b/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/workspace-list/index.tsx @@ -1,14 +1,9 @@ -import { - IconButton, - Menu, - MenuItem, - ScrollableContainer, -} from '@affine/component'; +import { IconButton, Menu, MenuItem } from '@affine/component'; import { Divider } from '@affine/component/ui/divider'; import { useEnableCloud } from '@affine/core/components/hooks/affine/use-enable-cloud'; import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks'; import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper'; -import type { Server } from '@affine/core/modules/cloud'; +import type { AuthAccountInfo, Server } from '@affine/core/modules/cloud'; import { AuthService, ServersService } from '@affine/core/modules/cloud'; import { GlobalDialogService } from '@affine/core/modules/dialogs'; import { GlobalContextService } from '@affine/core/modules/global-context'; @@ -17,14 +12,15 @@ import { WorkspaceService, WorkspacesService, } from '@affine/core/modules/workspace'; -import { ServerDeploymentType } from '@affine/graphql'; import { useI18n } from '@affine/i18n'; import { + AccountIcon, CloudWorkspaceIcon, + DeleteIcon, LocalWorkspaceIcon, MoreHorizontalIcon, - PlusIcon, - TeamWorkspaceIcon, + SelfhostIcon, + SignOutIcon, } from '@blocksuite/icons/rc'; import { FrameworkScope, @@ -35,6 +31,7 @@ import { import { useCallback, useMemo } from 'react'; import { WorkspaceCard } from '../../workspace-card'; +import { AddServer } from '../add-server'; import * as styles from './index.css'; interface WorkspaceModalProps { @@ -46,6 +43,91 @@ interface WorkspaceModalProps { onAddWorkspace: () => void; } +const WorkspaceServerInfo = ({ + server, + name, + account, + accountStatus, + onDeleteServer, + onSignOut, + onSignIn, +}: { + server: string; + name: string; + account?: AuthAccountInfo | null; + accountStatus?: 'authenticated' | 'unauthenticated'; + onDeleteServer?: () => void; + onSignOut?: () => void; + onSignIn?: () => void; +}) => { + const t = useI18n(); + const isCloud = server !== 'local'; + const isAffineCloud = server === 'affine-cloud'; + const Icon = isAffineCloud + ? CloudWorkspaceIcon + : isCloud + ? SelfhostIcon + : LocalWorkspaceIcon; + + const menuItems = useMemo( + () => + [ + server !== 'affine-cloud' && server !== 'local' && ( + } + type="danger" + key="delete-server" + onClick={onDeleteServer} + > + {t['com.affine.server.delete']()} + + ), + accountStatus === 'authenticated' && ( + } + key="sign-out" + onClick={onSignOut} + type="danger" + > + {t['Sign out']()} + + ), + accountStatus === 'unauthenticated' && ( + } + key="sign-in" + onClick={onSignIn} + > + {t['Sign in']()} + + ), + ].filter(Boolean), + [accountStatus, onDeleteServer, onSignIn, onSignOut, server, t] + ); + + return ( +
+
+ +
+
+
{name}
+ {isCloud ? ( +
+ {account ? account.email : 'Not signed in'} +
+ ) : null} +
+
+ {menuItems.length ? ( + + } /> + + ) : null} +
+ ); +}; + const CloudWorkSpaceList = ({ server, workspaces, @@ -57,7 +139,6 @@ const CloudWorkSpaceList = ({ onClickWorkspace: (workspaceMetadata: WorkspaceMetadata) => void; onClickEnableCloud?: (meta: WorkspaceMetadata) => void; }) => { - const t = useI18n(); const globalContextService = useService(GlobalContextService); const globalDialogService = useService(GlobalDialogService); const serverName = useLiveData(server.config$.selector(c => c.serverName)); @@ -71,8 +152,6 @@ const CloudWorkSpaceList = ({ globalContextService.globalContext.workspaceFlavour.$ ); - const serverType = server.config$.value.type; - const handleDeleteServer = useCallback(() => { serversService.removeServer(server.id); @@ -100,78 +179,23 @@ const CloudWorkSpaceList = ({ }); }, [globalDialogService, server.baseUrl]); - const onNewWorkspace = useCallback(() => { - globalDialogService.open( - 'create-workspace', - { - serverId: server.id, - forcedCloud: true, - }, - payload => { - if (payload) { - navigateHelper.openPage(payload.metadata.id, 'all'); - } - } - ); - }, [globalDialogService, navigateHelper, server.id]); - return ( -
-
-
-
- {serverType === ServerDeploymentType.Affine ? ( - - ) : ( - - )} -
{serverName}
-
-
- {account ? account.email : 'Not signed in'} -
-
- - - {t['com.affine.server.delete']()} - - ), - accountStatus === 'authenticated' && ( - - {t['Sign out']()} - - ), - accountStatus === 'unauthenticated' && ( - - {t['Sign in']()} - - ), - ]} - > -
- } /> -
-
-
+ <> + - } - onClick={onNewWorkspace} - className={styles.ItemContainer} - > -
- {t['com.affine.workspaceList.addWorkspace.create']()} -
-
-
+ ); }; @@ -186,25 +210,18 @@ const LocalWorkspaces = ({ return null; } return ( -
-
-
- - {t['com.affine.workspaceList.workspaceListType.local']()} -
-
+ <> + - -
+ ); }; @@ -224,6 +241,14 @@ export const AFFiNEWorkspaceList = ({ const serversService = useService(ServersService); const servers = useLiveData(serversService.servers$); + const affineCloudServer = useMemo( + () => servers.find(s => s.id === 'affine-cloud') as Server, + [servers] + ); + const selfhostServers = useMemo( + () => servers.filter(s => s.id !== 'affine-cloud'), + [servers] + ); const cloudWorkspaces = useMemo( () => @@ -262,24 +287,26 @@ export const AFFiNEWorkspaceList = ({ ); return ( - -
- {servers.map(server => ( - - flavour === server.id - )} - onClickWorkspace={handleClickWorkspace} - /> - - + <> + {/* 1. affine-cloud */} + + flavour === affineCloudServer.id + )} + onClickWorkspace={handleClickWorkspace} + /> + + {localWorkspaces.length > 0 || + (selfhostServers.length > 0 && ( + ))} -
+ + {/* 2. local */} -
+ {selfhostServers.length > 0 && ( + + )} + + {/* 3. selfhost */} + {selfhostServers.map((server, index) => ( + + flavour === server.id + )} + onClickWorkspace={handleClickWorkspace} + /> + {index !== selfhostServers.length - 1 && ( + + )} + + ))} + + + ); }; @@ -317,9 +365,10 @@ const SortableWorkspaceItem = ({ return ( void; onClickEnableCloud?: (workspaceMetadata: WorkspaceMetadata) => void; } @@ -262,6 +263,7 @@ export const WorkspaceCard = forwardRef< onClickOpenSettings, onClickEnableCloud, className, + infoClassName, disable, hideCollaborationIcon, hideTeamWorkspaceIcon, @@ -296,7 +298,7 @@ export const WorkspaceCard = forwardRef< ref={ref} {...props} > -
+
{information ? ( ) : null} - {hideCollaborationIcon || information?.isOwner ? null : ( - - - - )} - {hideTeamWorkspaceIcon || !information?.isTeam ? null : ( - - - - )} + {onClickOpenSettings && (
)}
- {showArrowDownIcon && }
- {active && ( -
- -
- )} +
+ {hideCollaborationIcon || information?.isOwner ? null : ( + + + + )} + {hideTeamWorkspaceIcon || !information?.isTeam ? null : ( + + + + )} + {active && ( +
+ +
+ )} + {showArrowDownIcon && } +
); } diff --git a/packages/frontend/core/src/components/workspace-selector/workspace-card/styles.css.ts b/packages/frontend/core/src/components/workspace-selector/workspace-card/styles.css.ts index 3d83f50405..1817f24110 100644 --- a/packages/frontend/core/src/components/workspace-selector/workspace-card/styles.css.ts +++ b/packages/frontend/core/src/components/workspace-selector/workspace-card/styles.css.ts @@ -19,6 +19,7 @@ export const container = style({ width: '100%', maxWidth: 500, color: cssVarV2('text/primary'), + overflow: 'hidden', ':hover': { cursor: 'pointer', background: cssVar('hoverColor'), @@ -34,6 +35,7 @@ export const infoContainer = style({ }); export const activeContainer = style({ flexShrink: 0, + lineHeight: 0, }); export const disable = style({ @@ -187,6 +189,9 @@ export const showOnCardHover = style({ [`.${container}:hover &`]: { position: 'relative', }, + '&:empty': { + display: 'none', + }, }, }); @@ -194,3 +199,14 @@ export const activeIcon = style({ fontSize: 14, color: cssVarV2('icon/activated'), }); + +export const suffixIcons = style({ + display: 'flex', + gap: 8, + alignItems: 'center', + selectors: { + '&:empty': { + display: 'none', + }, + }, +}); diff --git a/packages/frontend/core/src/desktop/dialogs/create-workspace/dialog.css.ts b/packages/frontend/core/src/desktop/dialogs/create-workspace/dialog.css.ts deleted file mode 100644 index 5b0ede3fe8..0000000000 --- a/packages/frontend/core/src/desktop/dialogs/create-workspace/dialog.css.ts +++ /dev/null @@ -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', -}); diff --git a/packages/frontend/core/src/desktop/dialogs/create-workspace/index.css.ts b/packages/frontend/core/src/desktop/dialogs/create-workspace/index.css.ts new file mode 100644 index 0000000000..98e6585008 --- /dev/null +++ b/packages/frontend/core/src/desktop/dialogs/create-workspace/index.css.ts @@ -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', + }, +]); diff --git a/packages/frontend/core/src/desktop/dialogs/create-workspace/index.tsx b/packages/frontend/core/src/desktop/dialogs/create-workspace/index.tsx index 6576ae65e2..b81c065efe 100644 --- a/packages/frontend/core/src/desktop/dialogs/create-workspace/index.tsx +++ b/packages/frontend/core/src/desktop/dialogs/create-workspace/index.tsx @@ -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 ( - -
- -
- -
-
- {t['com.affine.nameWorkspace.subtitle.workspace-name']()} -
- -
- {!serverId || serverId === 'affine-cloud' ? ( -
-
{t['AFFiNE Cloud']()}
-
-
-
- - {t['com.affine.nameWorkspace.affine-cloud.title']()} - - -
-
- {t['com.affine.nameWorkspace.affine-cloud.description']()} -
-
-
- -
-
- {forcedCloud && BUILD_CONFIG.isWeb ? ( - - {t['com.affine.nameWorkspace.affine-cloud.web-tips']()} - - ) : null} -
- ) : null} -
+
+ + {input} +
); }; export const CreateWorkspaceDialog = ({ - forcedCloud, serverId, close, + ...props }: DialogComponentProps) => { - 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 ( - - { + return ( + + + close({ metadata: res.meta, defaultDocId: res.defaultDocId }) + } + /> + + ); + }} + {...props} + > + + } /> - + + + } + /> + + ); +}; + +const CustomConfirmButton = ({ + workspaceName, + server, + onCreated, +}: { + workspaceName: string; + server?: Server | null; + onCreated: (res: Awaited>) => 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 ( + ); }; diff --git a/packages/frontend/core/src/desktop/dialogs/create-workspace/server-selector.css.ts b/packages/frontend/core/src/desktop/dialogs/create-workspace/server-selector.css.ts new file mode 100644 index 0000000000..022b97b619 --- /dev/null +++ b/packages/frontend/core/src/desktop/dialogs/create-workspace/server-selector.css.ts @@ -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, +}); diff --git a/packages/frontend/core/src/desktop/dialogs/create-workspace/server-selector.tsx b/packages/frontend/core/src/desktop/dialogs/create-workspace/server-selector.tsx new file mode 100644 index 0000000000..8083671ed4 --- /dev/null +++ b/packages/frontend/core/src/desktop/dialogs/create-workspace/server-selector.tsx @@ -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, '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 ( + + + {servers.map(server => ( + + ))} + + } + > +
+ {selectedServerName ?? placeholder} + +
+
+ ); +}; + +const LocalSelectorItem = ({ + onSelect, + active, +}: { + onSelect?: (id: string) => void; + active?: boolean; +}) => { + const t = useI18n(); + const handleSelect = useCallback(() => { + onSelect?.('local'); + }, [onSelect]); + return ( + } + onClick={handleSelect} + suffixIcon={active ? : null} + > + {t['com.affine.workspaceList.workspaceListType.local']()} + + ); +}; + +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 ( + } + onClick={handleSelect} + suffixIcon={active ? : null} + > + {name} + + ); +}; diff --git a/packages/frontend/core/src/desktop/dialogs/import-template/index.tsx b/packages/frontend/core/src/desktop/dialogs/import-template/index.tsx index ec647f18e2..bc7b73b167 100644 --- a/packages/frontend/core/src/desktop/dialogs/import-template/index.tsx +++ b/packages/frontend/core/src/desktop/dialogs/import-template/index.tsx @@ -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)', + }, + }} /> )} diff --git a/packages/frontend/core/src/modules/app-sidebar/views/fallback.css.ts b/packages/frontend/core/src/modules/app-sidebar/views/fallback.css.ts index 87c48fbcc3..2992c94456 100644 --- a/packages/frontend/core/src/modules/app-sidebar/views/fallback.css.ts +++ b/packages/frontend/core/src/modules/app-sidebar/views/fallback.css.ts @@ -1,7 +1,7 @@ import { style } from '@vanilla-extract/css'; export const fallback = style({ - padding: '4px 20px', + padding: '4px 16px', height: '100%', overflow: 'clip', }); diff --git a/packages/frontend/core/src/modules/dialogs/constant.ts b/packages/frontend/core/src/modules/dialogs/constant.ts index 66eba913a5..00826d01e6 100644 --- a/packages/frontend/core/src/modules/dialogs/constant.ts +++ b/packages/frontend/core/src/modules/dialogs/constant.ts @@ -15,7 +15,7 @@ export type SettingTab = | `workspace:${'preference' | 'properties' | 'members' | 'storage' | 'billing' | 'license'}`; export type GLOBAL_DIALOG_SCHEMA = { - 'create-workspace': (props: { serverId?: string; forcedCloud?: boolean }) => { + 'create-workspace': (props: { serverId?: string }) => { metadata: WorkspaceMetadata; defaultDocId?: string; }; diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index 2038bc928e..85a1cba4ff 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -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.placeholder": "Set a 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.new.page-mode": "New page", "com.affine.new_edgeless": "New edgeless", diff --git a/tests/affine-desktop/e2e/basic.spec.ts b/tests/affine-desktop/e2e/basic.spec.ts index a38794c0fd..9dcfb97ab3 100644 --- a/tests/affine-desktop/e2e/basic.spec.ts +++ b/tests/affine-desktop/e2e/basic.spec.ts @@ -3,10 +3,8 @@ import { clickNewPageButton, getBlockSuiteEditorTitle, } from '@affine-test/kit/utils/page-logic'; -import { - clickSideBarCurrentWorkspaceBanner, - clickSideBarSettingButton, -} from '@affine-test/kit/utils/sidebar'; +import { clickSideBarSettingButton } from '@affine-test/kit/utils/sidebar'; +import { createLocalWorkspace } from '@affine-test/kit/utils/workspace'; import type { Page } from '@playwright/test'; import { expect } from '@playwright/test'; @@ -112,13 +110,7 @@ test('windows only check', async ({ page }) => { test('delete workspace', async ({ page }) => { await clickNewPageButton(page); - await clickSideBarCurrentWorkspaceBanner(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 createLocalWorkspace({ name: 'Delete Me' }, page); await page.waitForTimeout(1000); await clickSideBarSettingButton(page); await page.getByTestId('workspace-setting:preference').click(); diff --git a/tests/affine-desktop/e2e/workspace.spec.ts b/tests/affine-desktop/e2e/workspace.spec.ts index 82c383ab2f..dc313147bb 100644 --- a/tests/affine-desktop/e2e/workspace.spec.ts +++ b/tests/affine-desktop/e2e/workspace.spec.ts @@ -7,6 +7,7 @@ import { clickNewPageButton, clickSideBarCurrentWorkspaceBanner, } from '@affine-test/kit/utils/sidebar'; +import { createLocalWorkspace } from '@affine-test/kit/utils/workspace'; import { expect } from '@playwright/test'; 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 }) => { //#region 1. create a new workspace - await clickSideBarCurrentWorkspaceBanner(page); const newWorkspaceName = 'new-test-name'; - - await page.getByTestId('new-workspace').click(); - await page.getByTestId('create-workspace-input').fill(newWorkspaceName); - await page.getByTestId('create-workspace-create-button').click(); + await createLocalWorkspace({ name: newWorkspaceName }, page); //#endregion //#region 2. create a page in the new workspace (will verify later if it is successfully recovered) diff --git a/tests/affine-local/e2e/local-first-avatar.spec.ts b/tests/affine-local/e2e/local-first-avatar.spec.ts index 610785d6d3..37e4253dcb 100644 --- a/tests/affine-local/e2e/local-first-avatar.spec.ts +++ b/tests/affine-local/e2e/local-first-avatar.spec.ts @@ -4,6 +4,7 @@ import { clickNewPageButton, waitForEditorLoad, } from '@affine-test/kit/utils/page-logic'; +import { createLocalWorkspace } from '@affine-test/kit/utils/workspace'; import { expect } from '@playwright/test'; 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 waitForEditorLoad(page); await clickNewPageButton(page); - await page.getByTestId('workspace-name').click(); - 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 createLocalWorkspace({ name: 'Test Workspace 1' }, page); await page.waitForTimeout(1000); await page.getByTestId('workspace-name').click(); await page diff --git a/tests/affine-local/e2e/local-first-delete-workspace.spec.ts b/tests/affine-local/e2e/local-first-delete-workspace.spec.ts index fffc79b0b3..a38e97d4c1 100644 --- a/tests/affine-local/e2e/local-first-delete-workspace.spec.ts +++ b/tests/affine-local/e2e/local-first-delete-workspace.spec.ts @@ -5,21 +5,13 @@ import { openSettingModal, openWorkspaceSettingPanel, } 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'; test('Create new workspace, then delete it', async ({ page, workspace }) => { await openHomePage(page); await waitForEditorLoad(page); - await clickSideBarCurrentWorkspaceBanner(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 createLocalWorkspace({ name: 'Test Workspace' }, page); await page.waitForSelector('[data-testid="workspace-name"]'); 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 openHomePage(page); await expect(page.getByTestId('new-workspace')).toBeVisible(); - await page.getByTestId('new-workspace').click(); - await page - .locator('[data-testid="create-workspace-input"]') - .pressSequentially('Test Workspace'); - await page.getByTestId('create-workspace-create-button').click(); + await createLocalWorkspace({ name: 'Test Workspace' }, page, true); await page.waitForTimeout(1000); await page.waitForSelector('[data-testid="workspace-name"]'); await expect(page.getByTestId('workspace-name')).toHaveText('Test Workspace'); diff --git a/tests/kit/src/utils/workspace.ts b/tests/kit/src/utils/workspace.ts index 219335b37a..32d8c73c64 100644 --- a/tests/kit/src/utils/workspace.ts +++ b/tests/kit/src/utils/workspace.ts @@ -15,9 +15,12 @@ export async function openWorkspaceListModal(page: Page) { export async function createLocalWorkspace( params: CreateWorkspaceParams, - page: Page + page: Page, + skipOpenWorkspaceListModal = false ) { - await openWorkspaceListModal(page); + if (!skipOpenWorkspaceListModal) { + await openWorkspaceListModal(page); + } // open create workspace modal 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').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 await page.getByTestId('create-workspace-create-button').click({ delay: 500,