From 0731872347e9d3a9b0b1a52c921869a5c2800cce Mon Sep 17 00:00:00 2001 From: CatsJuice Date: Tue, 26 Mar 2024 06:10:38 +0000 Subject: [PATCH] feat(core): refactor sidebar header (#6251) - Add user avatar - Move sign-out/user settings link from workspace-modal to user avatar modal - Modify the style of workspace list items - Modify gap of navigation buttons - Animate Syncing/Offline/... ![CleanShot 2024-03-22 at 10.22.38.gif](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/LakojjjzZNf6ogjOVwKE/7305f561-a85b-4ec6-89c2-27e2f1b63c85.gif) --- .../components/card/workspace-card/index.tsx | 102 ++++--------- .../card/workspace-card/styles.css.ts | 86 +++++++++++ .../components/card/workspace-card/styles.ts | 136 ------------------ .../component/src/ui/avatar/style.css.ts | 2 +- .../core/public/imgs/unknown-user.svg | 17 +++ .../components/app-sidebar/fallback.css.ts | 3 +- .../user-with-workspace-list/index.tsx | 2 +- .../user-account/index.tsx | 80 +---------- .../workspace-list/index.css.ts | 8 +- .../workspace-list/index.tsx | 11 ++ .../workspace-card/index.tsx | 70 +++++---- .../workspace-card/styles.css.ts | 99 +++++++++++++ .../workspace-card/styles.ts | 24 ++-- .../components/root-app-sidebar/index.css.ts | 19 +++ .../src/components/root-app-sidebar/index.tsx | 9 +- .../components/root-app-sidebar/user-info.tsx | 130 +++++++++++++++++ .../navigation/view/navigation-buttons.css.ts | 2 +- tests/affine-cloud/e2e/login.spec.ts | 10 +- tests/affine-local/e2e/image-preview.spec.ts | 86 ++++++----- tests/kit/utils/sidebar.ts | 4 + 20 files changed, 528 insertions(+), 372 deletions(-) create mode 100644 packages/frontend/component/src/components/card/workspace-card/styles.css.ts delete mode 100644 packages/frontend/component/src/components/card/workspace-card/styles.ts create mode 100644 packages/frontend/core/public/imgs/unknown-user.svg create mode 100644 packages/frontend/core/src/components/pure/workspace-slider-bar/workspace-card/styles.css.ts create mode 100644 packages/frontend/core/src/components/root-app-sidebar/index.css.ts create mode 100644 packages/frontend/core/src/components/root-app-sidebar/user-info.tsx diff --git a/packages/frontend/component/src/components/card/workspace-card/index.tsx b/packages/frontend/component/src/components/card/workspace-card/index.tsx index bd1c11216e..cce605e305 100644 --- a/packages/frontend/component/src/components/card/workspace-card/index.tsx +++ b/packages/frontend/component/src/components/card/workspace-card/index.tsx @@ -1,69 +1,18 @@ import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant'; -import { WorkspaceFlavour } from '@affine/env/workspace'; -import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import type { WorkspaceFlavour } from '@affine/env/workspace'; import { CollaborationIcon, SettingsIcon } from '@blocksuite/icons'; import type { WorkspaceMetadata } from '@toeverything/infra'; import { useCallback } from 'react'; -import { Avatar } from '../../../ui/avatar'; -import { Divider } from '../../../ui/divider'; +import { Avatar, type AvatarProps } from '../../../ui/avatar'; import { Skeleton } from '../../../ui/skeleton'; -import { Tooltip } from '../../../ui/tooltip'; -import { - StyledCard, - StyledIconContainer, - StyledSettingLink, - StyledWorkspaceInfo, - StyledWorkspaceTitle, - StyledWorkspaceTitleArea, - StyledWorkspaceType, - StyledWorkspaceTypeEllipse, - StyledWorkspaceTypeText, -} from './styles'; +import * as styles from './styles.css'; export interface WorkspaceTypeProps { flavour: WorkspaceFlavour; isOwner: boolean; } -const WorkspaceType = ({ flavour, isOwner }: WorkspaceTypeProps) => { - const t = useAFFiNEI18N(); - if (flavour === WorkspaceFlavour.LOCAL) { - return ( - - - {t['Local']()} - - ); - } - - return isOwner ? ( - - - - {t['com.affine.brand.affineCloud']()} - - - ) : ( - - - - {t['com.affine.brand.affineCloud']()} - - - - - - - - - ); -}; - export interface WorkspaceCardProps { currentWorkspaceId?: string | null; meta: WorkspaceMetadata; @@ -77,7 +26,7 @@ export interface WorkspaceCardProps { export const WorkspaceCardSkeleton = () => { return (
- +
{ width={220} style={{ marginLeft: '12px' }} /> - +
); }; +const avatarImageProps = { + style: { borderRadius: 3, overflow: 'hidden' }, +} satisfies AvatarProps['imageProps']; export const WorkspaceCard = ({ onClick, onSettingClick, @@ -101,32 +53,38 @@ export const WorkspaceCard = ({ }: WorkspaceCardProps) => { const displayName = name ?? UNTITLED_WORKSPACE_NAME; return ( - { onClick(meta); }, [onClick, meta])} - active={meta.id === currentWorkspaceId} > - - - - {displayName} + +
+
{displayName}
- + {isOwner ? null : } +
{ e.stopPropagation(); onSettingClick(meta); }} - withoutHoverStyle={true} > - - - - - - + +
+
+ + ); }; diff --git a/packages/frontend/component/src/components/card/workspace-card/styles.css.ts b/packages/frontend/component/src/components/card/workspace-card/styles.css.ts new file mode 100644 index 0000000000..20778bedbb --- /dev/null +++ b/packages/frontend/component/src/components/card/workspace-card/styles.css.ts @@ -0,0 +1,86 @@ +import { cssVar } from '@toeverything/theme'; +import { style } from '@vanilla-extract/css'; + +import { displayFlex, textEllipsis } from '../../../styles'; + +export const card = style({ + width: '100%', + cursor: 'pointer', + padding: '8px 12px', + borderRadius: 4, + // border: `1px solid ${borderColor}`, + boxShadow: 'inset 0 0 0 1px transparent', + ...displayFlex('flex-start', 'flex-start'), + transition: 'background .2s', + position: 'relative', + color: cssVar('textSecondaryColor'), + background: 'transparent', + display: 'flex', + alignItems: 'center', + gap: 12, + + selectors: { + '&:hover': { + background: cssVar('hoverColor'), + }, + '&[data-active="true"]': { + boxShadow: 'inset 0 0 0 1px ' + cssVar('brandColor'), + }, + }, +}); + +export const workspaceInfo = style({ + width: 0, + flex: 1, + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + gap: 2, +}); + +export const workspaceTitle = style({ + width: 0, + flex: 1, + fontSize: cssVar('fontSm'), + fontWeight: 500, + lineHeight: '22px', + maxWidth: '190px', + color: cssVar('textPrimaryColor'), + ...textEllipsis(1), +}); + +export const actionButtons = style({ + display: 'flex', + alignItems: 'center', +}); + +export const settingButtonWrapper = style({}); + +export const settingButton = style({ + transition: 'all 0.13s ease', + width: 0, + height: 20, + overflow: 'hidden', + marginLeft: 0, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + placeItems: 'center', + + borderRadius: 4, + boxShadow: 'none', + background: 'transparent', + cursor: 'pointer', + + selectors: { + [`.${card}:hover &`]: { + width: 20, + marginLeft: 8, + boxShadow: cssVar('shadow1'), + background: cssVar('white80'), + }, + // [`.${card}:hover &:hover`]: { + // background: cssVar('hoverColor'), + // }, + }, +}); diff --git a/packages/frontend/component/src/components/card/workspace-card/styles.ts b/packages/frontend/component/src/components/card/workspace-card/styles.ts deleted file mode 100644 index 46d48c2de2..0000000000 --- a/packages/frontend/component/src/components/card/workspace-card/styles.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { displayFlex, styled, textEllipsis } from '../../../styles'; -import { IconButton } from '../../../ui/button'; - -export const StyledWorkspaceInfo = styled('div')(() => { - return { - marginLeft: '12px', - width: '100%', - }; -}); - -export const StyledWorkspaceTitle = styled('div')(() => { - return { - fontSize: 'var(--affine-font-sm)', - fontWeight: 700, - lineHeight: '22px', - maxWidth: '190px', - color: 'var(--affine-text-primary-color)', - ...textEllipsis(1), - }; -}); - -export const StyledCard = styled('div')<{ - active?: boolean; -}>(({ active }) => { - const borderColor = active ? 'var(--affine-primary-color)' : 'transparent'; - const backgroundColor = active ? 'var(--affine-white-30)' : 'transparent'; - return { - width: '100%', - cursor: 'pointer', - padding: '12px', - borderRadius: '8px', - border: `1px solid ${borderColor}`, - ...displayFlex('flex-start', 'flex-start'), - transition: 'background .2s', - alignItems: 'center', - position: 'relative', - color: 'var(--affine-text-secondary-color)', - background: backgroundColor, - ':hover': { - background: 'var(--affine-hover-color)', - '.add-icon': { - borderColor: 'var(--affine-primary-color)', - color: 'var(--affine-primary-color)', - }, - '.setting-entry': { - opacity: 1, - pointerEvents: 'auto', - backgroundColor: 'var(--affine-white-30)', - boxShadow: 'var(--affine-shadow-1)', - ':hover': { - background: - 'linear-gradient(0deg, var(--affine-hover-color) 0%, var(--affine-hover-color) 100%), var(--affine-white-30)', - }, - }, - }, - '@media (max-width: 720px)': { - width: '100%', - }, - }; -}); - -export const StyledModalHeader = styled('div')(() => { - return { - width: '100%', - height: '72px', - position: 'absolute', - left: 0, - top: 0, - borderRadius: '24px 24px 0 0', - padding: '0 40px', - ...displayFlex('space-between', 'center'), - }; -}); - -export const StyledSettingLink = styled(IconButton)(() => { - return { - position: 'absolute', - right: '10px', - top: '10px', - opacity: 0, - borderRadius: '4px', - color: 'var(--affine-primary-color)', - pointerEvents: 'none', - transition: 'all .15s', - ':hover': { - background: 'var(--affine-hover-color)', - }, - }; -}); - -export const StyledWorkspaceType = styled('div')(() => { - return { - ...displayFlex('flex-start', 'center'), - width: '100%', - height: '20px', - }; -}); - -export const StyledWorkspaceTitleArea = styled('div')(() => { - return { - display: 'flex', - justifyContent: 'space-between', - }; -}); - -export const StyledWorkspaceTypeEllipse = styled('div')<{ - cloud?: boolean; -}>(({ cloud }) => { - return { - width: '5px', - height: '5px', - borderRadius: '50%', - background: cloud - ? 'var(--affine-palette-shape-blue)' - : 'var(--affine-palette-shape-green)', - }; -}); - -export const StyledWorkspaceTypeText = styled('div')(() => { - return { - fontSize: '12px', - fontWeight: 500, - lineHeight: '20px', - marginLeft: '4px', - color: 'var(--affine-text-secondary-color)', - }; -}); - -export const StyledIconContainer = styled('div')(() => { - return { - ...displayFlex('flex-start', 'center'), - fontSize: '14px', - gap: '8px', - color: 'var(--affine-icon-secondary)', - }; -}); diff --git a/packages/frontend/component/src/ui/avatar/style.css.ts b/packages/frontend/component/src/ui/avatar/style.css.ts index e7df958785..c34b6243e9 100644 --- a/packages/frontend/component/src/ui/avatar/style.css.ts +++ b/packages/frontend/component/src/ui/avatar/style.css.ts @@ -79,7 +79,6 @@ export const DefaultAvatarContainerStyle = style({ width: '100%', height: '100%', position: 'relative', - borderRadius: '50%', overflow: 'hidden', }); export const DefaultAvatarMiddleItemStyle = style({ @@ -155,6 +154,7 @@ export const avatarFallback = style({ width: '100%', height: '100%', borderRadius: '50%', + overflow: 'hidden', display: 'flex', alignItems: 'center', justifyContent: 'center', diff --git a/packages/frontend/core/public/imgs/unknown-user.svg b/packages/frontend/core/public/imgs/unknown-user.svg new file mode 100644 index 0000000000..25b33b446b --- /dev/null +++ b/packages/frontend/core/public/imgs/unknown-user.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/frontend/core/src/components/app-sidebar/fallback.css.ts b/packages/frontend/core/src/components/app-sidebar/fallback.css.ts index 1250b8b585..046413fa68 100644 --- a/packages/frontend/core/src/components/app-sidebar/fallback.css.ts +++ b/packages/frontend/core/src/components/app-sidebar/fallback.css.ts @@ -1,10 +1,9 @@ import { style } from '@vanilla-extract/css'; export const fallbackStyle = style({ - margin: '5px 16px', + margin: '4px 16px', height: '100%', }); export const fallbackHeaderStyle = style({ - height: '56px', width: '100%', display: 'flex', alignItems: 'center', diff --git a/packages/frontend/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/index.tsx b/packages/frontend/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/index.tsx index 4daa4c70e7..e77248ef9f 100644 --- a/packages/frontend/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/index.tsx +++ b/packages/frontend/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/index.tsx @@ -19,7 +19,7 @@ import * as styles from './index.css'; import { UserAccountItem } from './user-account'; import { AFFiNEWorkspaceList } from './workspace-list'; -const SignInItem = () => { +export const SignInItem = () => { const setDisableCloudOpen = useSetAtom(openDisableCloudAlertModalAtom); const setOpen = useSetAtom(authAtom); diff --git a/packages/frontend/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/user-account/index.tsx b/packages/frontend/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/user-account/index.tsx index bb67ce51ae..94616d703c 100644 --- a/packages/frontend/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/user-account/index.tsx +++ b/packages/frontend/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/user-account/index.tsx @@ -1,73 +1,8 @@ -import { IconButton } from '@affine/component/ui/button'; -import { Divider } from '@affine/component/ui/divider'; -import { Menu, MenuIcon, MenuItem } from '@affine/component/ui/menu'; -import { useAFFiNEI18N } from '@affine/i18n/hooks'; -import { - AccountIcon, - MoreHorizontalIcon, - SignOutIcon, -} from '@blocksuite/icons'; -import { useSetAtom } from 'jotai'; -import { useCallback } from 'react'; - -import { - openSettingModalAtom, - openSignOutModalAtom, -} from '../../../../../atoms'; import { UserPlanButton } from '../../../../affine/auth/user-plan-button'; import * as styles from './index.css'; -const AccountMenu = ({ onEventEnd }: { onEventEnd?: () => void }) => { - const setSettingModalAtom = useSetAtom(openSettingModalAtom); - const setOpenSignOutModalAtom = useSetAtom(openSignOutModalAtom); - - const onOpenAccountSetting = useCallback(() => { - setSettingModalAtom(prev => ({ - ...prev, - open: true, - activeTab: 'account', - })); - }, [setSettingModalAtom]); - - const onOpenSignOutModal = useCallback(() => { - onEventEnd?.(); - setOpenSignOutModalAtom(true); - }, [onEventEnd, setOpenSignOutModalAtom]); - - const t = useAFFiNEI18N(); - - return ( -
- - - - } - data-testid="workspace-modal-account-settings-option" - onClick={onOpenAccountSetting} - > - {t['com.affine.workspace.cloud.account.settings']()} - - - - - - } - data-testid="workspace-modal-sign-out-option" - onClick={onOpenSignOutModal} - > - {t['com.affine.workspace.cloud.account.logout']()} - -
- ); -}; - export const UserAccountItem = ({ email, - onEventEnd, }: { email: string; onEventEnd?: () => void; @@ -76,21 +11,8 @@ export const UserAccountItem = ({
{email}
-
- } - contentOptions={{ - side: 'right', - sideOffset: 12, - }} - > - } - type="plain" - /> - +
); }; diff --git a/packages/frontend/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/workspace-list/index.css.ts b/packages/frontend/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/workspace-list/index.css.ts index 13fac8256b..3bd7664102 100644 --- a/packages/frontend/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/workspace-list/index.css.ts +++ b/packages/frontend/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/workspace-list/index.css.ts @@ -10,17 +10,21 @@ export const workspaceListWrapper = style({ display: 'flex', width: '100%', flexDirection: 'column', - gap: '4px', + gap: 2, }); export const workspaceType = style({ display: 'flex', alignItems: 'center', - justifyContent: 'space-between', + gap: 4, padding: '0px 12px', + fontWeight: 500, fontSize: cssVar('fontXs'), lineHeight: '20px', color: cssVar('textSecondaryColor'), }); +export const workspaceTypeIcon = style({ + color: cssVar('iconSecondary'), +}); export const scrollbar = style({ transform: 'translateX(8px)', width: '4px', diff --git a/packages/frontend/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/workspace-list/index.tsx b/packages/frontend/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/workspace-list/index.tsx index 0b9940d435..4e72fce242 100644 --- a/packages/frontend/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/workspace-list/index.tsx +++ b/packages/frontend/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/workspace-list/index.tsx @@ -8,6 +8,7 @@ import { } from '@affine/core/hooks/use-workspace-info'; import { WorkspaceFlavour } from '@affine/env/workspace'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import { CloudWorkspaceIcon, LocalWorkspaceIcon } from '@blocksuite/icons'; import type { DragEndEvent } from '@dnd-kit/core'; import type { WorkspaceMetadata } from '@toeverything/infra'; import { useLiveData, useService, WorkspaceManager } from '@toeverything/infra'; @@ -49,6 +50,11 @@ const CloudWorkSpaceList = ({ return (
+ {t['com.affine.workspaceList.workspaceListType.cloud']()}
+ {t['com.affine.workspaceList.workspaceListType.local']()}
{ return ( <> - AFFiNE Cloud + Cloud ); }; @@ -196,21 +190,49 @@ const useSyncEngineSyncProgress = () => { ) : ( ), + active: + currentWorkspace.flavour === WorkspaceFlavour.AFFINE_CLOUD && + (syncing || retrying || isOverCapacity), }; }; -const WorkspaceStatus = () => { - const { message, icon } = useSyncEngineSyncProgress(); +const WorkspaceInfo = ({ name }: { name: string }) => { + const { message, icon, active } = useSyncEngineSyncProgress(); + const currentWorkspace = useService(Workspace); + const isCloud = currentWorkspace.flavour === WorkspaceFlavour.AFFINE_CLOUD; + + // to make sure that animation will play first time + const [delayActive, setDelayActive] = useState(false); + useEffect(() => { + setDelayActive(active); + }, [active]); return ( -
- - {icon} - +
+
+
+
+ {name} +
+
+ {isCloud ? : } +
+
+ + {/* when syncing/offline/... */} +
+
+ {icon} +
+
+
); }; +const avatarImageProps = { + style: { borderRadius: 3 }, +} satisfies AvatarProps['imageProps']; export const WorkspaceCard = forwardRef< HTMLDivElement, HTMLAttributes @@ -227,7 +249,8 @@ export const WorkspaceCard = forwardRef< const name = information?.name ?? UNTITLED_WORKSPACE_NAME; return ( - - - - {name} - - - - + +
); }); diff --git a/packages/frontend/core/src/components/pure/workspace-slider-bar/workspace-card/styles.css.ts b/packages/frontend/core/src/components/pure/workspace-slider-bar/workspace-card/styles.css.ts new file mode 100644 index 0000000000..528cabf5e7 --- /dev/null +++ b/packages/frontend/core/src/components/pure/workspace-slider-bar/workspace-card/styles.css.ts @@ -0,0 +1,99 @@ +import { cssVar } from '@toeverything/theme'; +import { globalStyle, style } from '@vanilla-extract/css'; + +const wsSlideAnim = { + ease: 'cubic-bezier(.45,.21,0,1)', + duration: '0.5s', + delay: '0.23s', +}; + +export const container = style({ + height: '50px', + display: 'flex', + alignItems: 'center', + gap: 8, + padding: '0 6px', + borderRadius: 4, + outline: 'none', + width: '100%', + maxWidth: 500, + color: cssVar('textPrimaryColor'), + ':hover': { + cursor: 'pointer', + background: cssVar('hoverColor'), + }, +}); + +export const workspaceInfoSlider = style({ + height: 42, + overflow: 'hidden', +}); +export const workspaceInfoSlide = style({ + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + transform: 'translateY(0)', + transition: `transform ${wsSlideAnim.duration} ${wsSlideAnim.ease} ${wsSlideAnim.delay}`, + selectors: { + [`.${workspaceInfoSlider}[data-active="true"] &`]: { + transform: 'translateY(-42px)', + }, + }, +}); +export const workspaceInfo = style({ + width: '100%', + flexGrow: 1, + overflow: 'hidden', + height: 42, + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + transition: `opacity ${wsSlideAnim.duration} ${wsSlideAnim.ease} ${wsSlideAnim.delay}`, + + selectors: { + [`.${workspaceInfoSlider}[data-active="true"] &[data-type="normal"]`]: { + opacity: 0, + }, + [`.${workspaceInfoSlider}[data-active="false"] &[data-type="events"]`]: { + opacity: 0, + }, + }, +}); + +export const workspaceName = style({ + fontSize: cssVar('fontSm'), + lineHeight: '22px', + fontWeight: 500, + userSelect: 'none', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', +}); + +export const workspaceStatus = style({ + display: 'flex', + gap: 2, + alignItems: 'center', + fontSize: cssVar('fontXs'), + lineHeight: '20px', + fontWeight: 400, + color: cssVar('black50'), +}); +globalStyle(`.${workspaceStatus} svg`, { + width: 16, + height: 16, + color: cssVar('iconSecondary'), +}); + +export const workspaceActiveStatus = style({ + display: 'flex', + gap: 2, + alignItems: 'center', + fontSize: cssVar('fontSm'), + lineHeight: '22px', + color: cssVar('textSecondaryColor'), +}); +globalStyle(`.${workspaceActiveStatus} svg`, { + width: 16, + height: 16, +}); diff --git a/packages/frontend/core/src/components/pure/workspace-slider-bar/workspace-card/styles.ts b/packages/frontend/core/src/components/pure/workspace-slider-bar/workspace-card/styles.ts index ea8f62c6de..55d0fb3ec6 100644 --- a/packages/frontend/core/src/components/pure/workspace-slider-bar/workspace-card/styles.ts +++ b/packages/frontend/core/src/components/pure/workspace-slider-bar/workspace-card/styles.ts @@ -1,16 +1,18 @@ import { displayFlex, styled, textEllipsis } from '@affine/component'; +import { cssVar } from '@toeverything/theme'; export const StyledSelectorContainer = styled('div')({ - height: '58px', + height: '50px', display: 'flex', alignItems: 'center', padding: '0 6px', borderRadius: '8px', outline: 'none', - width: '100%', - color: 'var(--affine-text-primary-color)', + width: 'fit-content', + maxWidth: '100%', + color: cssVar('textPrimaryColor'), ':hover': { cursor: 'pointer', - background: 'var(--affine-hover-color)', + background: cssVar('hoverColor'), }, }); @@ -23,8 +25,9 @@ export const StyledSelectorWrapper = styled('div')(() => { }); export const StyledWorkspaceName = styled('div')(() => { return { - lineHeight: '24px', - fontWeight: 600, + fontSize: cssVar('fontSm'), + lineHeight: '22px', + fontWeight: 500, userSelect: 'none', ...textEllipsis(1), marginLeft: '4px', @@ -35,17 +38,16 @@ export const StyledWorkspaceStatus = styled('div')(() => { return { height: '22px', ...displayFlex('flex-start', 'center'), - fontSize: 'var(--affine-font-sm)', - color: 'var(--affine-text-secondary-color)', + fontSize: cssVar('fontXs'), + color: cssVar('black50'), userSelect: 'none', padding: '0 4px', gap: '4px', zIndex: '1', svg: { - color: 'var(--affine-icon-color)', - fontSize: 'var(--affine-font-base)', + color: cssVar('iconSecondary'), '&[data-warning-color="true"]': { - color: 'var(--affine-error-color)', + color: cssVar('errorColor'), }, }, }; diff --git a/packages/frontend/core/src/components/root-app-sidebar/index.css.ts b/packages/frontend/core/src/components/root-app-sidebar/index.css.ts new file mode 100644 index 0000000000..1b4d57eee5 --- /dev/null +++ b/packages/frontend/core/src/components/root-app-sidebar/index.css.ts @@ -0,0 +1,19 @@ +import { style } from '@vanilla-extract/css'; + +export const workspaceAndUserWrapper = style({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + gap: 8, +}); + +export const workspaceWrapper = style({ + width: 0, + flex: 1, +}); + +export const userInfoWrapper = style({ + flexShrink: 0, + width: 28, + height: 28, +}); diff --git a/packages/frontend/core/src/components/root-app-sidebar/index.tsx b/packages/frontend/core/src/components/root-app-sidebar/index.tsx index 194ed8eeb4..e6dca11b8d 100644 --- a/packages/frontend/core/src/components/root-app-sidebar/index.tsx +++ b/packages/frontend/core/src/components/root-app-sidebar/index.tsx @@ -42,8 +42,10 @@ import { AddFavouriteButton } from '../pure/workspace-slider-bar/favorite/add-fa import FavoriteList from '../pure/workspace-slider-bar/favorite/favorite-list'; import { WorkspaceSelector } from '../workspace-selector'; import ImportPage from './import-page'; +import { workspaceAndUserWrapper, workspaceWrapper } from './index.css'; import { AppSidebarJournalButton } from './journal-button'; import { UpdaterButton } from './updater-button'; +import { UserInfo } from './user-info'; export type RootAppSidebarProps = { isPublicWorkspace: boolean; @@ -179,7 +181,12 @@ export const RootAppSidebar = ({ titles={deletePageTitles} /> - +
+
+ +
+ +
{ + const { status } = useSession(); + const isAuthenticated = status === 'authenticated'; + return isAuthenticated ? : ; +}; + +const AuthorizedUserInfo = () => { + const user = useCurrentUser(); + return ( + }> + + + ); +}; + +const UnauthorizedUserInfo = () => { + const setDisableCloudOpen = useSetAtom(openDisableCloudAlertModalAtom); + const setOpen = useSetAtom(authAtom); + + const openSignInModal = useCallback(() => { + if (!runtimeConfig.enableCloud) setDisableCloudOpen(true); + else setOpen(state => ({ ...state, openModal: true })); + }, [setDisableCloudOpen, setOpen]); + + return ( + + ); +}; + +const AccountMenu = () => { + const setSettingModalAtom = useSetAtom(openSettingModalAtom); + const setOpenSignOutModalAtom = useSetAtom(openSignOutModalAtom); + + const onOpenAccountSetting = useCallback(() => { + setSettingModalAtom(prev => ({ + ...prev, + open: true, + activeTab: 'account', + })); + }, [setSettingModalAtom]); + + const onOpenSignOutModal = useCallback(() => { + setOpenSignOutModalAtom(true); + }, [setOpenSignOutModalAtom]); + + const t = useAFFiNEI18N(); + + return ( + <> + + + + } + data-testid="workspace-modal-account-settings-option" + onClick={onOpenAccountSetting} + > + {t['com.affine.workspace.cloud.account.settings']()} + + + + + + } + data-testid="workspace-modal-sign-out-option" + onClick={onOpenSignOutModal} + > + {t['com.affine.workspace.cloud.account.logout']()} + + + ); +}; + +const OperationMenu = () => { + // TODO: display usage progress bar + const StorageUsage = null; + + return ( + <> + {StorageUsage} + + + ); +}; diff --git a/packages/frontend/core/src/modules/navigation/view/navigation-buttons.css.ts b/packages/frontend/core/src/modules/navigation/view/navigation-buttons.css.ts index 7b91edb5d6..941dd3c25d 100644 --- a/packages/frontend/core/src/modules/navigation/view/navigation-buttons.css.ts +++ b/packages/frontend/core/src/modules/navigation/view/navigation-buttons.css.ts @@ -3,7 +3,7 @@ import { style } from '@vanilla-extract/css'; export const container = style({ display: 'flex', alignItems: 'center', - columnGap: '32px', + columnGap: '8px', }); export const button = style({ diff --git a/tests/affine-cloud/e2e/login.spec.ts b/tests/affine-cloud/e2e/login.spec.ts index 6ede51db77..9ea7e65f02 100644 --- a/tests/affine-cloud/e2e/login.spec.ts +++ b/tests/affine-cloud/e2e/login.spec.ts @@ -11,6 +11,7 @@ import { clickSideBarAllPageButton, clickSideBarCurrentWorkspaceBanner, clickSideBarSettingButton, + clickSideBarUseAvatar, } from '@affine-test/kit/utils/sidebar'; import { createLocalWorkspace } from '@affine-test/kit/utils/workspace'; import { expect } from '@playwright/test'; @@ -69,14 +70,13 @@ test.describe('login first', () => { ); await clickSideBarAllPageButton(page); const currentUrl = page.url(); - await clickSideBarCurrentWorkspaceBanner(page); - await page.getByTestId('workspace-modal-account-option').click(); + await clickSideBarUseAvatar(page); await page.getByTestId('workspace-modal-sign-out-option').click(); await page.getByTestId('confirm-sign-out-button').click(); await page.reload(); - await clickSideBarCurrentWorkspaceBanner(page); - const signInButton = page.getByTestId('cloud-signin-button'); - await expect(signInButton).toBeVisible(); + await clickSideBarUseAvatar(page); + const authModal = page.getByTestId('auth-modal'); + await expect(authModal).toBeVisible(); expect(page.url()).toBe(currentUrl); }); diff --git a/tests/affine-local/e2e/image-preview.spec.ts b/tests/affine-local/e2e/image-preview.spec.ts index 4ed72ce702..5845869ba9 100644 --- a/tests/affine-local/e2e/image-preview.spec.ts +++ b/tests/affine-local/e2e/image-preview.spec.ts @@ -33,7 +33,8 @@ async function importImage(page: Page, url: string) { }, [url] ); - await page.waitForTimeout(500); + // TODO: wait for image to be loaded more reliably + await page.waitForTimeout(1000); } async function closeImagePreviewModal(page: Page) { @@ -53,7 +54,7 @@ test('image preview should be shown', async ({ page }) => { await title.click(); await page.keyboard.press('Enter'); await importImage(page, 'http://localhost:8081/large-image.png'); - await page.locator('img').first().dblclick(); + await page.locator('affine-page-image').first().dblclick(); const locator = page.getByTestId('image-preview-modal'); await expect(locator).toBeVisible(); await closeImagePreviewModal(page); @@ -70,11 +71,12 @@ test('image go left and right', async ({ page }) => { await title.click(); await page.keyboard.press('Enter'); await importImage(page, 'http://localhost:8081/large-image.png'); - await page.locator('img').first().dblclick(); + await page.locator('affine-page-image').first().dblclick(); await page.waitForTimeout(500); blobId = (await page + .getByTestId('image-preview-modal') .locator('img') - .nth(1) + .first() .getAttribute('data-blob-id')) as string; expect(blobId).toBeTruthy(); await closeImagePreviewModal(page); @@ -87,11 +89,12 @@ test('image go left and right', async ({ page }) => { } const locator = page.getByTestId('image-preview-modal'); await expect(locator).toBeHidden(); - await page.locator('img').first().dblclick(); + await page.locator('affine-page-image').first().dblclick(); await page.waitForTimeout(1000); { - const newBlobId = (await locator - .locator('img[data-blob-id]') + const newBlobId = (await page + .getByTestId('image-preview-modal') + .locator('img') .first() .getAttribute('data-blob-id')) as string; expect(newBlobId).not.toBe(blobId); @@ -99,8 +102,9 @@ test('image go left and right', async ({ page }) => { await page.keyboard.press('ArrowRight'); await page.waitForTimeout(1000); { - const newBlobId = (await locator - .locator('img[data-blob-id]') + const newBlobId = (await page + .getByTestId('image-preview-modal') + .locator('img') .first() .getAttribute('data-blob-id')) as string; expect(newBlobId).toBe(blobId); @@ -117,11 +121,12 @@ test('image able to zoom in and out with mouse scroll', async ({ page }) => { await title.click(); await page.keyboard.press('Enter'); await importImage(page, 'http://localhost:8081/large-image.png'); - await page.locator('img').first().dblclick(); + await page.locator('affine-page-image').first().dblclick(); await page.waitForTimeout(500); blobId = (await page + .getByTestId('image-preview-modal') .locator('img') - .nth(1) + .first() .getAttribute('data-blob-id')) as string; expect(blobId).toBeTruthy(); } @@ -170,11 +175,12 @@ test('image able to zoom in and out with button click', async ({ page }) => { await title.click(); await page.keyboard.press('Enter'); await importImage(page, 'http://localhost:8081/large-image.png'); - await page.locator('img').first().dblclick(); + await page.locator('affine-page-image').first().dblclick(); await page.waitForTimeout(500); blobId = (await page + .getByTestId('image-preview-modal') .locator('img') - .nth(1) + .first() .getAttribute('data-blob-id')) as string; expect(blobId).toBeTruthy(); } @@ -216,11 +222,12 @@ test('image should able to go left and right by buttons', async ({ page }) => { await title.click(); await page.keyboard.press('Enter'); await importImage(page, 'http://localhost:8081/large-image.png'); - await page.locator('img').first().dblclick(); + await page.locator('affine-page-image').first().dblclick(); await page.waitForTimeout(500); blobId = (await page + .getByTestId('image-preview-modal') .locator('img') - .nth(1) + .first() .getAttribute('data-blob-id')) as string; expect(blobId).toBeTruthy(); await closeImagePreviewModal(page); @@ -232,7 +239,7 @@ test('image should able to go left and right by buttons', async ({ page }) => { await importImage(page, 'http://localhost:8081/affine-preview.png'); } const locator = page.getByTestId('image-preview-modal'); - await page.locator('img').first().dblclick(); + await page.locator('affine-page-image').first().dblclick(); await expect(locator).toBeVisible(); { const newBlobId = (await locator @@ -268,11 +275,12 @@ test('image able to fit to screen by button', async ({ page }) => { await title.click(); await page.keyboard.press('Enter'); await importImage(page, 'http://localhost:8081/large-image.png'); - await page.locator('img').first().dblclick(); + await page.locator('affine-page-image').first().dblclick(); await page.waitForTimeout(500); blobId = (await page + .getByTestId('image-preview-modal') .locator('img') - .nth(1) + .first() .getAttribute('data-blob-id')) as string; expect(blobId).toBeTruthy(); } @@ -325,11 +333,12 @@ test('image able to reset zoom to 100%', async ({ page }) => { await title.click(); await page.keyboard.press('Enter'); await importImage(page, 'http://localhost:8081/large-image.png'); - await page.locator('img').first().dblclick(); + await page.locator('affine-page-image').first().dblclick(); await page.waitForTimeout(500); blobId = (await page + .getByTestId('image-preview-modal') .locator('img') - .nth(1) + .first() .getAttribute('data-blob-id')) as string; expect(blobId).toBeTruthy(); } @@ -378,11 +387,12 @@ test('image able to copy to clipboard', async ({ page }) => { await title.click(); await page.keyboard.press('Enter'); await importImage(page, 'http://localhost:8081/large-image.png'); - await page.locator('img').first().dblclick(); + await page.locator('affine-page-image').first().dblclick(); await page.waitForTimeout(500); blobId = (await page + .getByTestId('image-preview-modal') .locator('img') - .nth(1) + .first() .getAttribute('data-blob-id')) as string; expect(blobId).toBeTruthy(); } @@ -407,11 +417,12 @@ test('image able to download', async ({ page }) => { await title.click(); await page.keyboard.press('Enter'); await importImage(page, 'http://localhost:8081/large-image.png'); - await page.locator('img').first().dblclick(); + await page.locator('affine-page-image').first().dblclick(); await page.waitForTimeout(500); blobId = (await page + .getByTestId('image-preview-modal') .locator('img') - .nth(1) + .first() .getAttribute('data-blob-id')) as string; expect(blobId).toBeTruthy(); } @@ -437,11 +448,12 @@ test('image should only able to move when image is larger than viewport', async await title.click(); await page.keyboard.press('Enter'); await importImage(page, 'http://localhost:8081/large-image.png'); - await page.locator('img').first().dblclick(); + await page.locator('affine-page-image').first().dblclick(); await page.waitForTimeout(500); blobId = (await page + .getByTestId('image-preview-modal') .locator('img') - .nth(1) + .first() .getAttribute('data-blob-id')) as string; expect(blobId).toBeTruthy(); } @@ -494,11 +506,12 @@ test('image should able to delete and when delete, it will move to previous/next await title.click(); await page.keyboard.press('Enter'); await importImage(page, 'http://localhost:8081/large-image.png'); - await page.locator('img').first().dblclick(); + await page.locator('affine-page-image').first().dblclick(); await page.waitForTimeout(500); blobId = (await page + .getByTestId('image-preview-modal') .locator('img') - .nth(1) + .first() .getAttribute('data-blob-id')) as string; expect(blobId).toBeTruthy(); await closeImagePreviewModal(page); @@ -510,7 +523,7 @@ test('image should able to delete and when delete, it will move to previous/next await importImage(page, 'http://localhost:8081/affine-preview.png'); } const locator = page.getByTestId('image-preview-modal'); - await page.locator('img').first().dblclick(); + await page.locator('affine-page-image').first().dblclick(); await expect(locator).toBeVisible(); // ensure the new image was imported await page.waitForTimeout(1000); @@ -533,7 +546,7 @@ test('image should able to delete and when delete, it will move to previous/next await page.keyboard.press('Enter'); await importImage(page, 'http://localhost:8081/affine-preview.png'); } - await page.locator('img').first().dblclick(); + await page.locator('affine-page-image').first().dblclick(); await locator.getByTestId('next-image-button').click(); await page.waitForTimeout(1000); { @@ -569,11 +582,12 @@ test('tooltips for all buttons should be visible when hovering', async ({ await title.click(); await page.keyboard.press('Enter'); await importImage(page, 'http://localhost:8081/large-image.png'); - await page.locator('img').first().dblclick(); + await page.locator('affine-page-image').first().dblclick(); await page.waitForTimeout(500); blobId = (await page + .getByTestId('image-preview-modal') .locator('img') - .nth(1) + .first() .getAttribute('data-blob-id')) as string; expect(blobId).toBeTruthy(); } @@ -662,7 +676,7 @@ test('keypress esc should close the modal', async ({ page }) => { await title.click(); await page.keyboard.press('Enter'); await importImage(page, 'http://localhost:8081/large-image.png'); - await page.locator('img').first().dblclick(); + await page.locator('affine-page-image').first().dblclick(); const locator = page.getByTestId('image-preview-modal'); await expect(locator).toBeVisible(); await page.keyboard.press('Escape'); @@ -680,7 +694,7 @@ test('when mouse moves outside, the modal should be closed', async ({ await title.click(); await page.keyboard.press('Enter'); await importImage(page, 'http://localhost:8081/large-image.png'); - await page.locator('img').first().dblclick(); + await page.locator('affine-page-image').first().dblclick(); const locator = page.getByTestId('image-preview-modal'); await expect(locator).toBeVisible(); // animation delay @@ -701,14 +715,14 @@ test('caption should be visible and different styles were applied if image zoome await title.click(); await page.keyboard.press('Enter'); await importImage(page, 'http://localhost:8081/large-image.png'); - await page.locator('img').first().hover(); + await page.locator('affine-page-image').first().hover(); await page .locator('.embed-editing-state') .locator('icon-button') .nth(1) .click(); await page.getByPlaceholder('Write a caption').fill(sampleCaption); - await page.locator('img').first().dblclick(); + await page.locator('affine-page-image').first().dblclick(); const locator = page.getByTestId('image-preview-modal'); await expect(locator).toBeVisible(); await page.waitForTimeout(1000); diff --git a/tests/kit/utils/sidebar.ts b/tests/kit/utils/sidebar.ts index b9e685b0a5..7ac33a9a9d 100644 --- a/tests/kit/utils/sidebar.ts +++ b/tests/kit/utils/sidebar.ts @@ -12,6 +12,10 @@ export async function clickSideBarCurrentWorkspaceBanner(page: Page) { return page.getByTestId('current-workspace').click(); } +export async function clickSideBarUseAvatar(page: Page) { + return page.getByTestId('sidebar-user-avatar').click(); +} + export async function clickNewPageButton(page: Page) { return page.getByTestId('sidebar-new-page-button').click(); }