diff --git a/apps/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/add-workspace/index.css.ts b/apps/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/add-workspace/index.css.ts new file mode 100644 index 0000000000..e1dc962b5c --- /dev/null +++ b/apps/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/add-workspace/index.css.ts @@ -0,0 +1,24 @@ +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: 'var(--affine-icon-secondary)', +}); + +export const ItemText = style({ + fontSize: 'var(--affine-font-sm)', + lineHeight: '22px', + color: 'var(--affine-text-secondary-color)', + fontWeight: 400, + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', +}); diff --git a/apps/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/add-workspace/index.tsx b/apps/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/add-workspace/index.tsx new file mode 100644 index 0000000000..75f04e46b8 --- /dev/null +++ b/apps/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/add-workspace/index.tsx @@ -0,0 +1,44 @@ +import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import { ImportIcon, PlusIcon } from '@blocksuite/icons'; +import { MenuItem } from '@toeverything/components/menu'; + +import * as styles from './index.css'; + +export const AddWorkspace = ({ + onAddWorkspace, + onNewWorkspace, +}: { + onAddWorkspace?: () => void; + onNewWorkspace?: () => void; +}) => { + const t = useAFFiNEI18N(); + + return ( +
+ {runtimeConfig.enableSQLiteProvider && environment.isDesktop ? ( + } + onClick={onAddWorkspace} + data-testid="add-workspace" + className={styles.ItemContainer} + > +
+ {t['com.affine.workspace.local.import']()} +
+
+ ) : null} + } + onClick={onNewWorkspace} + data-testid="new-workspace" + className={styles.ItemContainer} + > +
+ {t['com.affine.workspaceList.addWorkspace.create']()} +
+
+
+ ); +}; diff --git a/apps/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/index.css.ts b/apps/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/index.css.ts new file mode 100644 index 0000000000..5bd4cff9da --- /dev/null +++ b/apps/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/index.css.ts @@ -0,0 +1,57 @@ +import { style } from '@vanilla-extract/css'; + +export const workspaceListWrapper = style({ + display: 'flex', + width: '100%', + flexDirection: 'column', +}); + +export const signInWrapper = style({ + display: 'flex', + width: '100%', + gap: '12px', + alignItems: 'center', + justifyContent: 'flex-start', + borderRadius: '8px', +}); + +export const iconContainer = style({ + width: '28px', + padding: '2px 4px 4px', + borderRadius: '14px', + background: 'var(--affine-white)', + display: 'flex', + border: '1px solid var(--affine-icon-secondary)', + color: 'var(--affine-icon-secondary)', + alignItems: 'center', + justifyContent: 'center', + fontSize: '20px', +}); + +export const signInTextContainer = style({ + display: 'flex', + flexDirection: 'column', +}); + +export const signInTextPrimary = style({ + fontSize: 'var(--affine-font-sm)', + fontWeight: 600, + lineHeight: '22px', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', +}); + +export const signInTextSecondary = style({ + fontSize: 'var(--affine-font-xs)', + fontWeight: 400, + lineHeight: '20px', + color: 'var(--affine-text-secondary-color)', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', +}); + +export const menuItem = style({ + borderRadius: '8px', +}); diff --git a/apps/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/index.tsx b/apps/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/index.tsx index 1a38ead079..e1045d593f 100644 --- a/apps/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/index.tsx +++ b/apps/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/index.tsx @@ -1,155 +1,62 @@ -import { WorkspaceList } from '@affine/component/workspace-list'; -import type { - AffineCloudWorkspace, - LocalWorkspace, -} from '@affine/env/workspace'; -import { WorkspaceFlavour, WorkspaceSubPath } from '@affine/env/workspace'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; -import type { RootWorkspaceMetadata } from '@affine/workspace/atom'; import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom'; -import { - AccountIcon, - ImportIcon, - Logo1Icon, - MoreHorizontalIcon, - PlusIcon, - SignOutIcon, -} from '@blocksuite/icons'; -import type { DragEndEvent } from '@dnd-kit/core'; -import { arrayMove } from '@dnd-kit/sortable'; -import { IconButton } from '@toeverything/components/button'; +import { Logo1Icon } from '@blocksuite/icons'; import { Divider } from '@toeverything/components/divider'; -import { Menu, MenuIcon, MenuItem } from '@toeverything/components/menu'; -import { - currentPageIdAtom, - currentWorkspaceIdAtom, -} from '@toeverything/infra/atom'; -import { useAtom, useAtomValue, useSetAtom } from 'jotai'; +import { MenuItem } from '@toeverything/components/menu'; +import { useAtomValue, useSetAtom } from 'jotai'; // eslint-disable-next-line @typescript-eslint/no-restricted-imports import { useSession } from 'next-auth/react'; -import { startTransition, useCallback, useMemo, useTransition } from 'react'; +import { useCallback, useMemo } from 'react'; import { authAtom, openCreateWorkspaceModalAtom, openDisableCloudAlertModalAtom, - openSettingModalAtom, } from '../../../../atoms'; -import type { AllWorkspace } from '../../../../shared'; -import { signOutCloud } from '../../../../utils/cloud-utils'; -import { useNavigateHelper } from '../.././../../hooks/use-navigate-helper'; -import { - StyledCreateWorkspaceCardPill, - StyledCreateWorkspaceCardPillContent, - StyledCreateWorkspaceCardPillIcon, - StyledImportWorkspaceCardPill, - StyledItem, - StyledModalBody, - StyledModalContent, - StyledModalFooterContent, - StyledModalHeader, - StyledModalHeaderContent, - StyledModalHeaderLeft, - StyledModalTitle, - StyledOperationWrapper, - StyledSignInCardPill, - StyledSignInCardPillTextCotainer, - StyledSignInCardPillTextPrimary, - StyledSignInCardPillTextSecondary, - StyledWorkspaceFlavourTitle, -} from './styles'; +import { AddWorkspace } from './add-workspace'; +import * as styles from './index.css'; +import { UserAccountItem } from './user-account'; +import { AFFiNEWorkspaceList } from './workspace-list'; -interface WorkspaceModalProps { - disabled?: boolean; - workspaces: RootWorkspaceMetadata[]; - currentWorkspaceId: AllWorkspace['id'] | null; - onClickWorkspace: (workspace: RootWorkspaceMetadata['id']) => void; - onClickWorkspaceSetting: (workspace: RootWorkspaceMetadata['id']) => void; - onNewWorkspace: () => void; - onAddWorkspace: () => void; - onMoveWorkspace: (activeId: string, overId: string) => void; -} +const SignInItem = () => { + const setDisableCloudOpen = useSetAtom(openDisableCloudAlertModalAtom); + + const setOpen = useSetAtom(authAtom); -const AccountMenu = ({ - onOpenAccountSetting, - onSignOut, -}: { - onOpenAccountSetting: () => void; - onSignOut: () => void; -}) => { const t = useAFFiNEI18N(); - return ( -
- - - - } - data-testid="editor-option-menu-import" - onClick={onOpenAccountSetting} - > - {t['com.affine.workspace.cloud.account.settings']()} - - - - - - } - data-testid="editor-option-menu-import" - onClick={onSignOut} - > - {t['com.affine.workspace.cloud.account.logout']()} - -
- ); -}; - -const CloudWorkSpaceList = ({ - disabled, - workspaces, - onClickWorkspace, - onClickWorkspaceSetting, - currentWorkspaceId, - onMoveWorkspace, -}: WorkspaceModalProps) => { - const t = useAFFiNEI18N(); + const onClickSignIn = useCallback(async () => { + if (!runtimeConfig.enableCloud) { + setDisableCloudOpen(true); + } else { + setOpen(state => ({ + ...state, + openModal: true, + })); + } + }, [setOpen, setDisableCloudOpen]); return ( - <> - - - - {t['com.affine.workspace.cloud']()} - - - - - flavour === WorkspaceFlavour.AFFINE_CLOUD - ) as (AffineCloudWorkspace | LocalWorkspace)[] - } - currentWorkspaceId={currentWorkspaceId} - onClick={onClickWorkspace} - onSettingClick={onClickWorkspaceSetting} - onDragEnd={useCallback( - (event: DragEndEvent) => { - const { active, over } = event; - if (active.id !== over?.id) { - onMoveWorkspace(active.id as string, over?.id as string); - } - }, - [onMoveWorkspace] - )} - /> - - + +
+
+ +
+ +
+
+ {t['com.affine.workspace.cloud.auth']()} +
+
+ {t['com.affine.workspace.cloud.description']()} +
+
+
+
); }; @@ -158,240 +65,43 @@ export const UserWithWorkspaceList = ({ }: { onEventEnd?: () => void; }) => { + const { data: session, status } = useSession(); + + const isAuthenticated = useMemo(() => status === 'authenticated', [status]); + const setOpenCreateWorkspaceModal = useSetAtom(openCreateWorkspaceModalAtom); - const { jumpToSubPath, jumpToIndex } = useNavigateHelper(); - const workspaces = useAtomValue(rootWorkspacesMetadataAtom, { - delay: 0, - }); - const setWorkspaces = useSetAtom(rootWorkspacesMetadataAtom); - const [currentWorkspaceId, setCurrentWorkspaceId] = useAtom( - currentWorkspaceIdAtom - ); - const setCurrentPageId = useSetAtom(currentPageIdAtom); - const [, startCloseTransition] = useTransition(); - const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom); - const setSettingModalAtom = useSetAtom(openSettingModalAtom); - - const t = useAFFiNEI18N(); - const setOpen = useSetAtom(authAtom); - const setDisableCloudOpen = useSetAtom(openDisableCloudAlertModalAtom); - // TODO: AFFiNE Cloud support - const { data: session, status } = useSession(); - const isLoggedIn = useMemo(() => status === 'authenticated', [status]); - const cloudWorkspaces = useMemo( - () => - workspaces.filter( - ({ flavour }) => flavour === WorkspaceFlavour.AFFINE_CLOUD - ) as (AffineCloudWorkspace | LocalWorkspace)[], - [workspaces] - ); - const localWorkspaces = useMemo( - () => - workspaces.filter( - ({ flavour }) => flavour === WorkspaceFlavour.LOCAL - ) as (AffineCloudWorkspace | LocalWorkspace)[], - [workspaces] - ); - - const onClickWorkspaceSetting = useCallback( - (workspaceId: string) => { - setOpenSettingModalAtom({ - open: true, - activeTab: 'workspace', - workspaceId, - }); - onEventEnd?.(); - }, - [onEventEnd, setOpenSettingModalAtom] - ); - - const onMoveWorkspace = useCallback( - (activeId: string, overId: string) => { - const oldIndex = workspaces.findIndex(w => w.id === activeId); - const newIndex = workspaces.findIndex(w => w.id === overId); - startTransition(() => { - setWorkspaces(workspaces => arrayMove(workspaces, oldIndex, newIndex)); - }); - }, - [setWorkspaces, workspaces] - ); - const onClickWorkspace = useCallback( - (workspaceId: string) => { - startCloseTransition(() => { - setCurrentWorkspaceId(workspaceId); - setCurrentPageId(null); - jumpToSubPath(workspaceId, WorkspaceSubPath.ALL); - }); - onEventEnd?.(); - }, - [jumpToSubPath, onEventEnd, setCurrentPageId, setCurrentWorkspaceId] - ); const onNewWorkspace = useCallback(() => { setOpenCreateWorkspaceModal('new'); onEventEnd?.(); }, [onEventEnd, setOpenCreateWorkspaceModal]); + const onAddWorkspace = useCallback(async () => { setOpenCreateWorkspaceModal('add'); onEventEnd?.(); }, [onEventEnd, setOpenCreateWorkspaceModal]); - const onOpenAccountSetting = useCallback(() => { - setSettingModalAtom(prev => ({ - ...prev, - open: true, - activeTab: 'account', - })); - onEventEnd?.(); - }, [onEventEnd, setSettingModalAtom]); - const onSignOut = useCallback(async () => { - signOutCloud() - .then(() => { - jumpToIndex(); - }) - .catch(console.error); - onEventEnd?.(); - }, [onEventEnd, jumpToIndex]); + const workspaces = useAtomValue(rootWorkspacesMetadataAtom, { + delay: 0, + }); return ( - <> - {!isLoggedIn ? ( - - - { - if (!runtimeConfig.enableCloud) { - setDisableCloudOpen(true); - } else { - setOpen(state => ({ - ...state, - openModal: true, - })); - } - }} - data-testid="cloud-signin-button" - > - - - - - - - {t['com.affine.workspace.cloud.auth']()} - - - {t['com.affine.workspace.cloud.description']()} - - - - - - - +
+ {isAuthenticated ? ( + ) : ( - - - {session?.user.email} - - - } - contentOptions={{ - side: 'right', - sideOffset: 30, - }} - > - } - type="plain" - /> - - - - - + )} - - {isLoggedIn && cloudWorkspaces.length !== 0 ? ( - <> - - - - ) : null} - - - {t['com.affine.workspace.local']()} - - - - { - const { active, over } = event; - if (active.id !== over?.id) { - onMoveWorkspace(active.id as string, over?.id as string); - } - }, - [onMoveWorkspace] - )} - /> - - {runtimeConfig.enableSQLiteProvider && environment.isDesktop ? ( - - - - - - -
-

{t['com.affine.workspace.local.import']()}

-
-
-
-
- ) : null} -
- - - - - - - -
-

{t['New Workspace']()}

-
-
-
-
-
- + + + {workspaces.length > 0 ? : null} + +
); }; diff --git a/apps/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/styles.ts b/apps/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/styles.ts deleted file mode 100644 index 60c5b96e85..0000000000 --- a/apps/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/styles.ts +++ /dev/null @@ -1,273 +0,0 @@ -import { displayFlex, styled, textEllipsis } from '@affine/component'; - -export const StyledSplitLine = styled('div')(() => { - return { - width: '1px', - height: '20px', - background: 'var(--affine-border-color)', - marginRight: '12px', - }; -}); - -export const StyleWorkspaceInfo = styled('div')(() => { - return { - marginLeft: '15px', - width: '202px', - p: { - height: '20px', - fontSize: 'var(--affine-font-sm)', - ...displayFlex('flex-start', 'center'), - }, - svg: { - marginRight: '10px', - fontSize: '16px', - flexShrink: 0, - }, - span: { - flexGrow: 1, - ...textEllipsis(1), - }, - }; -}); - -export const StyleWorkspaceTitle = styled('div')(() => { - return { - fontSize: 'var(--affine-font-base)', - fontWeight: 600, - lineHeight: '24px', - marginBottom: '10px', - maxWidth: '200px', - ...textEllipsis(1), - }; -}); - -export const StyledCreateWorkspaceCard = styled('div')(() => { - return { - width: '310px', - height: '124px', - marginBottom: '24px', - cursor: 'pointer', - padding: '16px', - boxShadow: 'var(--affine-shadow-1)', - borderRadius: '12px', - transition: 'all .1s', - background: 'var(--affine-white-80)', - ...displayFlex('flex-start', 'flex-start'), - color: 'var(--affine-text-secondary-color)', - - ':hover': { - background: 'var(--affine-hover-color)', - color: 'var(--affine-text-primary-color)', - '.add-icon': { - borderColor: 'var(--affine-white)', - color: 'var(--affine-primary-color)', - }, - }, - '@media (max-width: 720px)': { - width: '100%', - }, - }; -}); -export const StyledCreateWorkspaceCardPillContainer = styled('div')(() => { - return { - borderRadius: '10px', - display: 'flex', - margin: '-8px -4px', - flexFlow: 'column', - gap: '12px', - background: 'var(--affine-background-overlay-panel-color)', - }; -}); - -export const StyledCreateWorkspaceCardPill = styled('div')(() => { - return { - borderRadius: '8px', - display: 'flex', - width: '100%', - height: '58px', - border: `1px solid var(--affine-border-color)`, - }; -}); - -export const StyledSignInCardPill = styled('div')(() => { - return { - borderRadius: '8px', - display: 'flex', - width: '100%', - height: '58px', - }; -}); - -export const StyledImportWorkspaceCardPill = styled('div')(() => { - return { - borderRadius: '5px', - display: 'flex', - width: '100%', - }; -}); - -export const StyledCreateWorkspaceCardPillContent = styled('div')(() => { - return { - display: 'flex', - gap: '12px', - alignItems: 'center', - }; -}); - -export const StyledCreateWorkspaceCardPillIcon = styled('div')(() => { - return { - fontSize: '28px', - width: '1em', - height: '1em', - }; -}); - -export const StyledSignInCardPillTextCotainer = styled('div')(() => { - return { - display: 'flex', - flexDirection: 'column', - }; -}); - -export const StyledSignInCardPillTextSecondary = styled('div')(() => { - return { - fontSize: '12px', - color: 'var(--affine-text-secondary-color)', - }; -}); - -export const StyledSignInCardPillTextPrimary = styled('div')(() => { - return { - fontSize: 'var(--affine-font-base)', - fontWeight: 600, - lineHeight: '24px', - maxWidth: '200px', - textAlign: 'left', - ...textEllipsis(1), - }; -}); - -export const StyledModalHeaderLeft = styled('div')(() => { - return { ...displayFlex('flex-start', 'center') }; -}); -export const StyledModalTitle = styled('div')(() => { - return { - fontWeight: 600, - fontSize: 'var(--affine-font-h6)', - color: 'var(--affine-text-primary-color)', - }; -}); - -export const StyledHelperContainer = styled('div')(() => { - return { - color: 'var(--affine-icon-color)', - marginLeft: '15px', - fontWeight: 400, - fontSize: 'var(--affine-font-h6)', - ...displayFlex('center', 'center'), - }; -}); - -export const StyledModalContent = styled('div')({ - ...displayFlex('space-between', 'flex-start', 'flex-start'), - flexWrap: 'wrap', - flexDirection: 'column', - width: '100%', - gap: '4px', -}); - -export const StyledModalFooterContent = styled('div')({ - ...displayFlex('space-between', 'flex-start', 'flex-start'), - flexWrap: 'wrap', - flexDirection: 'column', - width: '100%', - marginTop: '12px', - backgroundColor: 'var(--affine-background-overlay-panel-color)', -}); - -export const StyledModalHeaderContent = styled('div')({ - ...displayFlex('space-between', 'flex-start', 'flex-start'), - flexWrap: 'wrap', - flexDirection: 'column', - width: '100%', - backgroundColor: 'var(--affine-background-overlay-panel-color)', -}); - -export const StyledOperationWrapper = styled('div')(() => { - return { - ...displayFlex('flex-end', 'center'), - }; -}); - -export const StyleWorkspaceAdd = styled('div')(() => { - return { - width: '58px', - height: '58px', - borderRadius: '100%', - background: 'var(--affine-background-overlay-panel-color)', - border: '1.5px dashed #f4f5fa', - transition: 'background .2s', - fontSize: '24px', - ...displayFlex('center', 'center'), - borderColor: 'var(--affine-white)', - color: 'var(--affine-background-overlay-panel-color)', - }; -}); -export const StyledModalHeader = styled('div')(() => { - return { - width: '100%', - left: 0, - top: 0, - borderRadius: '24px 24px 0 0', - padding: '0px 14px', - ...displayFlex('space-between', 'center'), - }; -}); - -export const StyledModalBody = styled('div')(() => { - return { - display: 'inline-flex', - flexDirection: 'column', - alignItems: 'flex-start', - gap: '4px', - flex: 1, - overflowY: 'auto', - }; -}); - -export const StyledWorkspaceFlavourTitle = styled('div')(() => { - return { - fontSize: 'var(--affine-font-xs)', - color: 'var(--affine-text-secondary-color)', - marginBottom: '4px', - }; -}); -export const StyledItem = styled('button')<{ - active?: boolean; -}>(({ active = false }) => { - return { - height: 'auto', - padding: '8px 12px', - width: '100%', - borderRadius: '5px', - fontSize: 'var(--affine-font-sm)', - ...displayFlex('flex-start', 'center'), - cursor: 'pointer', - position: 'relative', - backgroundColor: 'transparent', - color: 'var(--affine-text-primary-color)', - svg: { - color: 'var(--affine-icon-color)', - }, - - ':hover': { - backgroundColor: 'var(--affine-hover-color)', - }, - - ...(active - ? { - backgroundColor: 'var(--affine-hover-color)', - } - : {}), - }; -}); diff --git a/apps/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/user-account/index.css.ts b/apps/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/user-account/index.css.ts new file mode 100644 index 0000000000..43275e4318 --- /dev/null +++ b/apps/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/user-account/index.css.ts @@ -0,0 +1,18 @@ +import { style } from '@vanilla-extract/css'; + +export const userAccountContainer = style({ + display: 'flex', + padding: '4px 0px 4px 12px', + gap: '12px', + alignItems: 'center', + justifyContent: 'space-between', +}); +export const userEmail = style({ + fontSize: 'var(--affine-font-sm)', + fontWeight: 400, + lineHeight: '22px', + textOverflow: 'ellipsis', + overflow: 'hidden', + whiteSpace: 'nowrap', + maxWidth: 'calc(100% - 36px)', +}); diff --git a/apps/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/user-account/index.tsx b/apps/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/user-account/index.tsx new file mode 100644 index 0000000000..827ad56587 --- /dev/null +++ b/apps/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/user-account/index.tsx @@ -0,0 +1,96 @@ +import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import { + AccountIcon, + MoreHorizontalIcon, + SignOutIcon, +} from '@blocksuite/icons'; +import { IconButton } from '@toeverything/components/button'; +import { Divider } from '@toeverything/components/divider'; +import { Menu, MenuIcon, MenuItem } from '@toeverything/components/menu'; +import { useSetAtom } from 'jotai'; +import { useCallback } from 'react'; + +import { openSettingModalAtom } from '../../../../../atoms'; +import { signOutCloud } from '../../../../../utils/cloud-utils'; +import { useNavigateHelper } from '../.././../../../hooks/use-navigate-helper'; +import * as styles from './index.css'; + +const AccountMenu = ({ onEventEnd }: { onEventEnd?: () => void }) => { + const setSettingModalAtom = useSetAtom(openSettingModalAtom); + + const { jumpToIndex } = useNavigateHelper(); + + const onOpenAccountSetting = useCallback(() => { + setSettingModalAtom(prev => ({ + ...prev, + open: true, + activeTab: 'account', + })); + }, [setSettingModalAtom]); + + const onSignOut = useCallback(async () => { + signOutCloud() + .then(() => { + jumpToIndex(); + }) + .catch(console.error); + onEventEnd?.(); + }, [onEventEnd, jumpToIndex]); + + const t = useAFFiNEI18N(); + + return ( +
+ + + + } + data-testid="editor-option-menu-import" + onClick={onOpenAccountSetting} + > + {t['com.affine.workspace.cloud.account.settings']()} + + + + + + } + data-testid="editor-option-menu-import" + onClick={onSignOut} + > + {t['com.affine.workspace.cloud.account.logout']()} + +
+ ); +}; + +export const UserAccountItem = ({ + email, + onEventEnd, +}: { + email: string; + onEventEnd?: () => void; +}) => { + return ( +
+
{email}
+ } + contentOptions={{ + side: 'right', + sideOffset: 12, + }} + > + } + type="plain" + /> + +
+ ); +}; diff --git a/apps/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/workspace-list/index.css.ts b/apps/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/workspace-list/index.css.ts new file mode 100644 index 0000000000..0ace06dbcc --- /dev/null +++ b/apps/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/workspace-list/index.css.ts @@ -0,0 +1,29 @@ +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: '4px', +}); + +export const workspaceType = style({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + padding: '0px 12px', + fontSize: 'var(--affine-font-xs)', + lineHeight: '20px', + color: 'var(--affine-text-secondary-color)', +}); + +export const scrollbar = style({ + transform: 'translateX(10px)', + width: '4px', +}); diff --git a/apps/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/workspace-list/index.tsx b/apps/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/workspace-list/index.tsx new file mode 100644 index 0000000000..be17d2dcf0 --- /dev/null +++ b/apps/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/workspace-list/index.tsx @@ -0,0 +1,233 @@ +import { ScrollableContainer } from '@affine/component'; +import { WorkspaceList } from '@affine/component/workspace-list'; +import type { + AffineCloudWorkspace, + LocalWorkspace, +} from '@affine/env/workspace'; +import { WorkspaceFlavour, WorkspaceSubPath } from '@affine/env/workspace'; +import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import type { RootWorkspaceMetadata } from '@affine/workspace/atom'; +import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom'; +import type { DragEndEvent } from '@dnd-kit/core'; +import { arrayMove } from '@dnd-kit/sortable'; +import { Divider } from '@toeverything/components/divider'; +import { + currentPageIdAtom, + currentWorkspaceIdAtom, +} from '@toeverything/infra/atom'; +import { useAtom, useSetAtom } from 'jotai'; +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { useSession } from 'next-auth/react'; +import { startTransition, useCallback, useMemo, useTransition } from 'react'; + +import { + openCreateWorkspaceModalAtom, + openSettingModalAtom, +} from '../../../../../atoms'; +import type { AllWorkspace } from '../../../../../shared'; +import { useIsWorkspaceOwner } from '../.././../../../hooks/affine/use-is-workspace-owner'; +import { useNavigateHelper } from '../.././../../../hooks/use-navigate-helper'; +import * as styles from './index.css'; +interface WorkspaceModalProps { + disabled?: boolean; + workspaces: (AffineCloudWorkspace | LocalWorkspace)[]; + currentWorkspaceId: AllWorkspace['id'] | null; + onClickWorkspace: (workspace: RootWorkspaceMetadata['id']) => void; + onClickWorkspaceSetting: (workspace: RootWorkspaceMetadata['id']) => void; + onNewWorkspace: () => void; + onAddWorkspace: () => void; + onDragEnd: (event: DragEndEvent) => void; +} + +const CloudWorkSpaceList = ({ + disabled, + workspaces, + onClickWorkspace, + onClickWorkspaceSetting, + currentWorkspaceId, + onDragEnd, +}: WorkspaceModalProps) => { + const t = useAFFiNEI18N(); + if (workspaces.length === 0) { + return null; + } + return ( +
+
+ {t['com.affine.workspaceList.workspaceListType.cloud']()} +
+ +
+ ); +}; + +const LocalWorkspaces = ({ + disabled, + workspaces, + onClickWorkspace, + onClickWorkspaceSetting, + currentWorkspaceId, + onDragEnd, +}: WorkspaceModalProps) => { + const t = useAFFiNEI18N(); + if (workspaces.length === 0) { + return null; + } + return ( +
+
+ {t['com.affine.workspaceList.workspaceListType.local']()} +
+ +
+ ); +}; + +export const AFFiNEWorkspaceList = ({ + workspaces, + onEventEnd, +}: { + workspaces: RootWorkspaceMetadata[]; + onEventEnd?: () => void; +}) => { + const setOpenCreateWorkspaceModal = useSetAtom(openCreateWorkspaceModalAtom); + + const { jumpToSubPath } = useNavigateHelper(); + + const setWorkspaces = useSetAtom(rootWorkspacesMetadataAtom); + + const [currentWorkspaceId, setCurrentWorkspaceId] = useAtom( + currentWorkspaceIdAtom + ); + + const setCurrentPageId = useSetAtom(currentPageIdAtom); + + const [, startCloseTransition] = useTransition(); + + const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom); + + // TODO: AFFiNE Cloud support + const { status } = useSession(); + + const isAuthenticated = useMemo(() => status === 'authenticated', [status]); + + const cloudWorkspaces = useMemo( + () => + workspaces.filter( + ({ flavour }) => flavour === WorkspaceFlavour.AFFINE_CLOUD + ) as (AffineCloudWorkspace | LocalWorkspace)[], + [workspaces] + ); + + const localWorkspaces = useMemo( + () => + workspaces.filter( + ({ flavour }) => flavour === WorkspaceFlavour.LOCAL + ) as (AffineCloudWorkspace | LocalWorkspace)[], + [workspaces] + ); + + const onClickWorkspaceSetting = useCallback( + (workspaceId: string) => { + setOpenSettingModalAtom({ + open: true, + activeTab: 'workspace', + workspaceId, + }); + onEventEnd?.(); + }, + [onEventEnd, setOpenSettingModalAtom] + ); + + const onMoveWorkspace = useCallback( + (activeId: string, overId: string) => { + const oldIndex = workspaces.findIndex(w => w.id === activeId); + + const newIndex = workspaces.findIndex(w => w.id === overId); + startTransition(() => { + setWorkspaces(workspaces => arrayMove(workspaces, oldIndex, newIndex)); + }); + }, + [setWorkspaces, workspaces] + ); + + const onClickWorkspace = useCallback( + (workspaceId: string) => { + startCloseTransition(() => { + setCurrentWorkspaceId(workspaceId); + setCurrentPageId(null); + jumpToSubPath(workspaceId, WorkspaceSubPath.ALL); + }); + onEventEnd?.(); + }, + [jumpToSubPath, onEventEnd, setCurrentPageId, setCurrentWorkspaceId] + ); + + const onDragEnd = useCallback( + (event: DragEndEvent) => { + const { active, over } = event; + if (active.id !== over?.id) { + onMoveWorkspace(active.id as string, over?.id as string); + } + }, + [onMoveWorkspace] + ); + + const onNewWorkspace = useCallback(() => { + setOpenCreateWorkspaceModal('new'); + onEventEnd?.(); + }, [onEventEnd, setOpenCreateWorkspaceModal]); + + const onAddWorkspace = useCallback(async () => { + setOpenCreateWorkspaceModal('add'); + onEventEnd?.(); + }, [onEventEnd, setOpenCreateWorkspaceModal]); + + return ( + + {isAuthenticated ? ( +
+ + {localWorkspaces.length > 0 && cloudWorkspaces.length > 0 ? ( + + ) : null} +
+ ) : null} + +
+ ); +}; diff --git a/apps/core/src/components/pure/workspace-slider-bar/workspace-card/index.tsx b/apps/core/src/components/pure/workspace-slider-bar/workspace-card/index.tsx index 5937c32650..d7829bea30 100644 --- a/apps/core/src/components/pure/workspace-slider-bar/workspace-card/index.tsx +++ b/apps/core/src/components/pure/workspace-slider-bar/workspace-card/index.tsx @@ -30,6 +30,7 @@ import { } from './styles'; const hoverAtom = atom(false); + // FIXME: // 1. Remove mui style // 2. Refactor the code to improve readability @@ -41,6 +42,7 @@ const CloudWorkspaceStatus = () => { ); }; + const SyncingWorkspaceStatus = () => { return ( <> @@ -49,6 +51,7 @@ const SyncingWorkspaceStatus = () => { ); }; + const UnSyncWorkspaceStatus = () => { return ( <> @@ -82,11 +85,14 @@ const WorkspaceStatus = ({ currentWorkspace: AllWorkspace; }) => { const isOnline = useSystemOnline(); + // todo: finish display sync status const [forceSyncStatus, startForceSync] = useDatasourceSync( currentWorkspace.blockSuiteWorkspace ); + const setIsHovered = useSetAtom(hoverAtom); + const content = useMemo(() => { if (currentWorkspace.flavour === WorkspaceFlavour.LOCAL) { return 'Saved locally'; @@ -103,6 +109,7 @@ const WorkspaceStatus = ({ return 'Sync with AFFiNE Cloud'; } }, [currentWorkspace.flavour, forceSyncStatus.type, isOnline]); + const CloudWorkspaceSyncStatus = useCallback(() => { if (forceSyncStatus.type === 'syncing') { return SyncingWorkspaceStatus(); @@ -160,6 +167,7 @@ export const WorkspaceCard = forwardRef< const [name] = useBlockSuiteWorkspaceName( currentWorkspace.blockSuiteWorkspace ); + const [workspaceAvatar] = useBlockSuiteWorkspaceAvatarUrl( currentWorkspace.blockSuiteWorkspace ); diff --git a/apps/core/src/components/pure/workspace-slider-bar/workspace-card/styles.ts b/apps/core/src/components/pure/workspace-slider-bar/workspace-card/styles.ts index c59ff8e731..a510341dfb 100644 --- a/apps/core/src/components/pure/workspace-slider-bar/workspace-card/styles.ts +++ b/apps/core/src/components/pure/workspace-slider-bar/workspace-card/styles.ts @@ -6,6 +6,7 @@ export const StyledSelectorContainer = styled('div')({ alignItems: 'center', padding: '0 6px', borderRadius: '8px', + outline: 'none', color: 'var(--affine-text-primary-color)', ':hover': { cursor: 'pointer', diff --git a/apps/core/src/components/root-app-sidebar/index.tsx b/apps/core/src/components/root-app-sidebar/index.tsx index 0eaf6d2b69..9cc1cd6de9 100644 --- a/apps/core/src/components/root-app-sidebar/index.tsx +++ b/apps/core/src/components/root-app-sidebar/index.tsx @@ -19,17 +19,10 @@ import { } from '@blocksuite/icons'; import type { Page } from '@blocksuite/store'; import { useDroppable } from '@dnd-kit/core'; -import { Popover } from '@toeverything/components/popover'; +import { Menu } from '@toeverything/components/menu'; import { useAtom } from 'jotai'; import type { HTMLAttributes, ReactElement } from 'react'; -import { - forwardRef, - Suspense, - useCallback, - useEffect, - useMemo, - useState, -} from 'react'; +import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react'; import { useHistoryAtom } from '../../atoms/history'; import { useAppSetting } from '../../atoms/settings'; @@ -175,18 +168,21 @@ export const RootAppSidebar = ({ } > - - - + } contentOptions={{ // hide trigger sideOffset: -58, onInteractOutside: closeUserWorkspaceList, onEscapeKeyDown: closeUserWorkspaceList, + style: { + width: '300px', + }, }} > - + { <>
- + } + contentOptions={{ + style: { + width: 300, + transform: 'translate(-50%, -50%)', + borderRadius: '8px', + boxShadow: 'var(--affine-shadow-2)', + backgroundColor: 'var(--affine-background-overlay-panel-color)', + padding: '16px 12px', + }, + }} + > +
+
diff --git a/apps/storybook/src/stories/card.stories.tsx b/apps/storybook/src/stories/card.stories.tsx index 8602e61c48..305554f2a3 100644 --- a/apps/storybook/src/stories/card.stories.tsx +++ b/apps/storybook/src/stories/card.stories.tsx @@ -37,6 +37,7 @@ export const AffineWorkspaceCard = () => { onClick={() => {}} onSettingClick={() => {}} currentWorkspaceId={null} + isOwner={true} /> ); }; diff --git a/packages/component/src/components/card/workspace-card/index.tsx b/packages/component/src/components/card/workspace-card/index.tsx index 433f244bd0..779355b2c8 100644 --- a/packages/component/src/components/card/workspace-card/index.tsx +++ b/packages/component/src/components/card/workspace-card/index.tsx @@ -1,8 +1,11 @@ import { WorkspaceFlavour } from '@affine/env/workspace'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import type { RootWorkspaceMetadata } from '@affine/workspace/atom'; -import { SettingsIcon } from '@blocksuite/icons'; +import { CollaborationIcon, SettingsIcon } from '@blocksuite/icons'; +import { Skeleton } from '@mui/material'; import { Avatar } from '@toeverything/components/avatar'; +import { Divider } from '@toeverything/components/divider'; +import { Tooltip } from '@toeverything/components/tooltip'; import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url'; import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name'; import { useStaticBlockSuiteWorkspace } from '@toeverything/infra/__internal__/react'; @@ -10,46 +13,56 @@ import { useCallback } from 'react'; import { StyledCard, + StyledIconContainer, StyledSettingLink, StyledWorkspaceInfo, StyledWorkspaceTitle, StyledWorkspaceTitleArea, + StyledWorkspaceType, + StyledWorkspaceTypeEllipse, + StyledWorkspaceTypeText, } from './styles'; export interface WorkspaceTypeProps { flavour: WorkspaceFlavour; + isOwner: boolean; } -const WorkspaceType = ({ flavour }: WorkspaceTypeProps) => { +const WorkspaceType = ({ flavour, isOwner }: WorkspaceTypeProps) => { const t = useAFFiNEI18N(); - // fixme: cloud regression - const isOwner = true; - if (flavour === WorkspaceFlavour.LOCAL) { return ( -

- {t['com.affine.workspaceType.local']()} -

+ + + {t['Local']()} + ); } return isOwner ? ( -

- {t['com.affine.workspaceType.cloud']()} -

+ + + + {t['com.affine.brand.affineCloud']()} + + ) : ( -

- {t['com.affine.workspaceType.joined']()} -

+ + + + {t['com.affine.brand.affineCloud']()} + + + + + + + + ); }; @@ -58,19 +71,35 @@ export interface WorkspaceCardProps { meta: RootWorkspaceMetadata; onClick: (workspaceId: string) => void; onSettingClick: (workspaceId: string) => void; + isOwner?: boolean; } +export const WorkspaceCardSkeleton = () => { + return ( +
+ + + + +
+ ); +}; + export const WorkspaceCard = ({ onClick, onSettingClick, currentWorkspaceId, meta, + isOwner = true, }: WorkspaceCardProps) => { - // const t = useAFFiNEI18N(); const workspace = useStaticBlockSuiteWorkspace(meta.id); const [name] = useBlockSuiteWorkspaceName(workspace); const [workspaceAvatar] = useBlockSuiteWorkspaceAvatarUrl(workspace); - return ( {name} { e.stopPropagation(); @@ -92,17 +122,10 @@ export const WorkspaceCard = ({ }} withoutHoverStyle={true} > - + - {/* {meta.flavour === WorkspaceFlavour.LOCAL && ( -

- - -

- - )} */} - +
); diff --git a/packages/component/src/components/card/workspace-card/styles.ts b/packages/component/src/components/card/workspace-card/styles.ts index 6ca289f4f5..bd1d645003 100644 --- a/packages/component/src/components/card/workspace-card/styles.ts +++ b/packages/component/src/components/card/workspace-card/styles.ts @@ -5,30 +5,16 @@ import { displayFlex, styled, textEllipsis } from '../../../styles'; export const StyledWorkspaceInfo = styled('div')(() => { return { marginLeft: '12px', - width: '202px', - p: { - height: '20px', - fontSize: 'var(--affine-font-sm)', - ...displayFlex('flex-start', 'center'), - }, - svg: { - marginRight: '10px', - fontSize: '16px', - flexShrink: 0, - }, - span: { - flexGrow: 1, - ...textEllipsis(1), - }, + width: '100%', }; }); export const StyledWorkspaceTitle = styled('div')(() => { return { - fontSize: 'var(--affine-font-base)', - fontWeight: 600, - lineHeight: '24px', - maxWidth: '200px', + fontSize: 'var(--affine-font-sm)', + fontWeight: 700, + lineHeight: '22px', + maxWidth: '190px', color: 'var(--affine-text-primary-color)', ...textEllipsis(1), }; @@ -38,13 +24,12 @@ export const StyledCard = styled('div')<{ active?: boolean; }>(({ active }) => { const borderColor = active ? 'var(--affine-primary-color)' : 'transparent'; - const backgroundColor = active ? 'var(--affine-white)' : 'transparent'; + const backgroundColor = active ? 'var(--affine-white-30)' : 'transparent'; return { - width: '280px', - height: '58px', + width: '100%', cursor: 'pointer', padding: '12px', - borderRadius: '12px', + borderRadius: '8px', border: `1px solid ${borderColor}`, ...displayFlex('flex-start', 'flex-start'), transition: 'background .2s', @@ -91,8 +76,8 @@ export const StyledModalHeader = styled('div')(() => { export const StyledSettingLink = styled(IconButton)(() => { return { position: 'absolute', - right: '6px', - bottom: '6px', + right: '10px', + top: '10px', opacity: 0, borderRadius: '4px', color: 'var(--affine-primary-color)', @@ -104,9 +89,11 @@ export const StyledSettingLink = styled(IconButton)(() => { }; }); -export const StyledWorkspaceType = styled('p')(() => { +export const StyledWorkspaceType = styled('div')(() => { return { - fontSize: 10, + ...displayFlex('flex-start', 'center'), + width: '100%', + height: '20px', }; }); @@ -116,3 +103,35 @@ export const StyledWorkspaceTitleArea = styled('div')(() => { 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/component/src/components/workspace-list/index.tsx b/packages/component/src/components/workspace-list/index.tsx index 24004ee1a5..3ec35c8674 100644 --- a/packages/component/src/components/workspace-list/index.tsx +++ b/packages/component/src/components/workspace-list/index.tsx @@ -16,9 +16,12 @@ import { } from '@dnd-kit/modifiers'; import { arrayMove, SortableContext, useSortable } from '@dnd-kit/sortable'; import type { CSSProperties } from 'react'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { Suspense, useCallback, useEffect, useMemo, useState } from 'react'; -import { WorkspaceCard } from '../../components/card/workspace-card'; +import { + WorkspaceCard, + WorkspaceCardSkeleton, +} from '../../components/card/workspace-card'; import { workspaceItemStyle } from './index.css'; export interface WorkspaceListProps { @@ -28,16 +31,25 @@ export interface WorkspaceListProps { onClick: (workspaceId: string) => void; onSettingClick: (workspaceId: string) => void; onDragEnd: (event: DragEndEvent) => void; + useIsWorkspaceOwner?: (workspaceId: string) => boolean; } interface SortableWorkspaceItemProps extends Omit { item: RootWorkspaceMetadata; + useIsWorkspaceOwner?: (workspaceId: string) => boolean; } -const SortableWorkspaceItem = (props: SortableWorkspaceItemProps) => { +const SortableWorkspaceItem = ({ + disabled, + item, + useIsWorkspaceOwner, + currentWorkspaceId, + onClick, + onSettingClick, +}: SortableWorkspaceItemProps) => { const { setNodeRef, attributes, listeners, transform, transition } = useSortable({ - id: props.item.id, + id: item.id, }); const style: CSSProperties = useMemo( () => ({ @@ -45,11 +57,12 @@ const SortableWorkspaceItem = (props: SortableWorkspaceItemProps) => { ? `translate3d(${transform.x}px, ${transform.y}px, 0)` : undefined, transition, - pointerEvents: props.disabled ? 'none' : undefined, - opacity: props.disabled ? 0.6 : undefined, + pointerEvents: disabled ? 'none' : undefined, + opacity: disabled ? 0.6 : undefined, }), - [props.disabled, transform, transition] + [disabled, transform, transition] ); + const isOwner = useIsWorkspaceOwner?.(item.id); return (
{ {...listeners} >
); @@ -106,7 +120,9 @@ export const WorkspaceList = (props: WorkspaceListProps) => { {optimisticList.map(item => ( - + } key={item.id}> + + ))} diff --git a/packages/component/src/ui/scrollbar/scrollbar.tsx b/packages/component/src/ui/scrollbar/scrollbar.tsx index 6d06d2ca25..e9beb6ce2f 100644 --- a/packages/component/src/ui/scrollbar/scrollbar.tsx +++ b/packages/component/src/ui/scrollbar/scrollbar.tsx @@ -11,6 +11,7 @@ export type ScrollableContainerProps = { className?: string; viewPortClassName?: string; styles?: React.CSSProperties; + scrollBarClassName?: string; }; export const ScrollableContainer = ({ @@ -20,6 +21,7 @@ export const ScrollableContainer = ({ className, styles: _styles, viewPortClassName, + scrollBarClassName, }: PropsWithChildren) => { const [hasScrollTop, ref] = useHasScrollTop(); return ( @@ -39,7 +41,7 @@ export const ScrollableContainer = ({ diff --git a/packages/i18n/src/resources/en.json b/packages/i18n/src/resources/en.json index 786cf633ca..825673ab43 100644 --- a/packages/i18n/src/resources/en.json +++ b/packages/i18n/src/resources/en.json @@ -580,5 +580,9 @@ "Successfully enabled AFFiNE Cloud": "Successfully enabled AFFiNE Cloud", "404.hint": "Sorry, you do not have access or this content does not exist...", "404.back": "Back to My Content", - "404.signOut": "Sign in to another account" + "404.signOut": "Sign in to another account", + "com.affine.workspaceList.addWorkspace.create": "Create Workspace", + "com.affine.workspaceList.workspaceListType.local": "Local Storage", + "com.affine.workspaceList.workspaceListType.cloud": "Cloud Sync", + "Local": "Local" } diff --git a/tests/affine-local/e2e/local-first-collections-items.spec.ts b/tests/affine-local/e2e/local-first-collections-items.spec.ts index 301563d75e..c9c51ec106 100644 --- a/tests/affine-local/e2e/local-first-collections-items.spec.ts +++ b/tests/affine-local/e2e/local-first-collections-items.spec.ts @@ -74,8 +74,6 @@ test('Show collections items in sidebar', async ({ page }) => { skipInitialPage: true, }); expect(await items.count()).toBe(1); - - await clickSideBarCurrentWorkspaceBanner(page); await createLocalWorkspace( { name: 'Test 1',