refactor: workspace list (#4432)

This commit is contained in:
JimmFly 2023-09-22 15:02:31 +08:00 committed by GitHub
parent 092e2e0a3d
commit edd7d00104
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 749 additions and 728 deletions

View File

@ -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',
});

View File

@ -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 (
<div>
{runtimeConfig.enableSQLiteProvider && environment.isDesktop ? (
<MenuItem
block={true}
preFix={<ImportIcon />}
onClick={onAddWorkspace}
data-testid="add-workspace"
className={styles.ItemContainer}
>
<div className={styles.ItemText}>
{t['com.affine.workspace.local.import']()}
</div>
</MenuItem>
) : null}
<MenuItem
block={true}
preFix={<PlusIcon />}
onClick={onNewWorkspace}
data-testid="new-workspace"
className={styles.ItemContainer}
>
<div className={styles.ItemText}>
{t['com.affine.workspaceList.addWorkspace.create']()}
</div>
</MenuItem>
</div>
);
};

View File

@ -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',
});

View File

@ -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 { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom'; import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { import { Logo1Icon } from '@blocksuite/icons';
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 { Divider } from '@toeverything/components/divider'; import { Divider } from '@toeverything/components/divider';
import { Menu, MenuIcon, MenuItem } from '@toeverything/components/menu'; import { MenuItem } from '@toeverything/components/menu';
import { import { useAtomValue, useSetAtom } from 'jotai';
currentPageIdAtom,
currentWorkspaceIdAtom,
} from '@toeverything/infra/atom';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports // eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { useSession } from 'next-auth/react'; import { useSession } from 'next-auth/react';
import { startTransition, useCallback, useMemo, useTransition } from 'react'; import { useCallback, useMemo } from 'react';
import { import {
authAtom, authAtom,
openCreateWorkspaceModalAtom, openCreateWorkspaceModalAtom,
openDisableCloudAlertModalAtom, openDisableCloudAlertModalAtom,
openSettingModalAtom,
} from '../../../../atoms'; } from '../../../../atoms';
import type { AllWorkspace } from '../../../../shared'; import { AddWorkspace } from './add-workspace';
import { signOutCloud } from '../../../../utils/cloud-utils'; import * as styles from './index.css';
import { useNavigateHelper } from '../.././../../hooks/use-navigate-helper'; import { UserAccountItem } from './user-account';
import { import { AFFiNEWorkspaceList } from './workspace-list';
StyledCreateWorkspaceCardPill,
StyledCreateWorkspaceCardPillContent,
StyledCreateWorkspaceCardPillIcon,
StyledImportWorkspaceCardPill,
StyledItem,
StyledModalBody,
StyledModalContent,
StyledModalFooterContent,
StyledModalHeader,
StyledModalHeaderContent,
StyledModalHeaderLeft,
StyledModalTitle,
StyledOperationWrapper,
StyledSignInCardPill,
StyledSignInCardPillTextCotainer,
StyledSignInCardPillTextPrimary,
StyledSignInCardPillTextSecondary,
StyledWorkspaceFlavourTitle,
} from './styles';
interface WorkspaceModalProps { const SignInItem = () => {
disabled?: boolean; const setDisableCloudOpen = useSetAtom(openDisableCloudAlertModalAtom);
workspaces: RootWorkspaceMetadata[];
currentWorkspaceId: AllWorkspace['id'] | null; const setOpen = useSetAtom(authAtom);
onClickWorkspace: (workspace: RootWorkspaceMetadata['id']) => void;
onClickWorkspaceSetting: (workspace: RootWorkspaceMetadata['id']) => void;
onNewWorkspace: () => void;
onAddWorkspace: () => void;
onMoveWorkspace: (activeId: string, overId: string) => void;
}
const AccountMenu = ({
onOpenAccountSetting,
onSignOut,
}: {
onOpenAccountSetting: () => void;
onSignOut: () => void;
}) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
return ( const onClickSignIn = useCallback(async () => {
<div> if (!runtimeConfig.enableCloud) {
<MenuItem setDisableCloudOpen(true);
preFix={ } else {
<MenuIcon> setOpen(state => ({
<AccountIcon /> ...state,
</MenuIcon> openModal: true,
} }));
data-testid="editor-option-menu-import" }
onClick={onOpenAccountSetting} }, [setOpen, setDisableCloudOpen]);
>
{t['com.affine.workspace.cloud.account.settings']()}
</MenuItem>
<Divider />
<MenuItem
preFix={
<MenuIcon>
<SignOutIcon />
</MenuIcon>
}
data-testid="editor-option-menu-import"
onClick={onSignOut}
>
{t['com.affine.workspace.cloud.account.logout']()}
</MenuItem>
</div>
);
};
const CloudWorkSpaceList = ({
disabled,
workspaces,
onClickWorkspace,
onClickWorkspaceSetting,
currentWorkspaceId,
onMoveWorkspace,
}: WorkspaceModalProps) => {
const t = useAFFiNEI18N();
return ( return (
<> <MenuItem
<StyledModalHeader> className={styles.menuItem}
<StyledModalHeaderLeft> onClick={onClickSignIn}
<StyledWorkspaceFlavourTitle> data-testid="cloud-signin-button"
{t['com.affine.workspace.cloud']()} >
</StyledWorkspaceFlavourTitle> <div className={styles.signInWrapper}>
</StyledModalHeaderLeft> <div className={styles.iconContainer}>
</StyledModalHeader> <Logo1Icon />
<StyledModalContent> </div>
<WorkspaceList
disabled={disabled} <div className={styles.signInTextContainer}>
items={ <div className={styles.signInTextPrimary}>
workspaces.filter( {t['com.affine.workspace.cloud.auth']()}
({ flavour }) => flavour === WorkspaceFlavour.AFFINE_CLOUD </div>
) as (AffineCloudWorkspace | LocalWorkspace)[] <div className={styles.signInTextSecondary}>
} {t['com.affine.workspace.cloud.description']()}
currentWorkspaceId={currentWorkspaceId} </div>
onClick={onClickWorkspace} </div>
onSettingClick={onClickWorkspaceSetting} </div>
onDragEnd={useCallback( </MenuItem>
(event: DragEndEvent) => {
const { active, over } = event;
if (active.id !== over?.id) {
onMoveWorkspace(active.id as string, over?.id as string);
}
},
[onMoveWorkspace]
)}
/>
</StyledModalContent>
</>
); );
}; };
@ -158,240 +65,43 @@ export const UserWithWorkspaceList = ({
}: { }: {
onEventEnd?: () => void; onEventEnd?: () => void;
}) => { }) => {
const { data: session, status } = useSession();
const isAuthenticated = useMemo(() => status === 'authenticated', [status]);
const setOpenCreateWorkspaceModal = useSetAtom(openCreateWorkspaceModalAtom); 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(() => { const onNewWorkspace = useCallback(() => {
setOpenCreateWorkspaceModal('new'); setOpenCreateWorkspaceModal('new');
onEventEnd?.(); onEventEnd?.();
}, [onEventEnd, setOpenCreateWorkspaceModal]); }, [onEventEnd, setOpenCreateWorkspaceModal]);
const onAddWorkspace = useCallback(async () => { const onAddWorkspace = useCallback(async () => {
setOpenCreateWorkspaceModal('add'); setOpenCreateWorkspaceModal('add');
onEventEnd?.(); onEventEnd?.();
}, [onEventEnd, setOpenCreateWorkspaceModal]); }, [onEventEnd, setOpenCreateWorkspaceModal]);
const onOpenAccountSetting = useCallback(() => { const workspaces = useAtomValue(rootWorkspacesMetadataAtom, {
setSettingModalAtom(prev => ({ delay: 0,
...prev, });
open: true,
activeTab: 'account',
}));
onEventEnd?.();
}, [onEventEnd, setSettingModalAtom]);
const onSignOut = useCallback(async () => {
signOutCloud()
.then(() => {
jumpToIndex();
})
.catch(console.error);
onEventEnd?.();
}, [onEventEnd, jumpToIndex]);
return ( return (
<> <div className={styles.workspaceListWrapper}>
{!isLoggedIn ? ( {isAuthenticated ? (
<StyledModalHeaderContent> <UserAccountItem
<StyledSignInCardPill> email={session?.user.email ?? 'Unknown User'}
<StyledItem onEventEnd={onEventEnd}
onClick={async () => { />
if (!runtimeConfig.enableCloud) {
setDisableCloudOpen(true);
} else {
setOpen(state => ({
...state,
openModal: true,
}));
}
}}
data-testid="cloud-signin-button"
>
<StyledCreateWorkspaceCardPillContent>
<StyledCreateWorkspaceCardPillIcon>
<Logo1Icon />
</StyledCreateWorkspaceCardPillIcon>
<StyledSignInCardPillTextCotainer>
<StyledSignInCardPillTextPrimary>
{t['com.affine.workspace.cloud.auth']()}
</StyledSignInCardPillTextPrimary>
<StyledSignInCardPillTextSecondary>
{t['com.affine.workspace.cloud.description']()}
</StyledSignInCardPillTextSecondary>
</StyledSignInCardPillTextCotainer>
</StyledCreateWorkspaceCardPillContent>
</StyledItem>
</StyledSignInCardPill>
<Divider
style={{
margin: '12px 0px',
}}
/>
</StyledModalHeaderContent>
) : ( ) : (
<StyledModalHeaderContent> <SignInItem />
<StyledModalHeader>
<StyledModalTitle>{session?.user.email}</StyledModalTitle>
<StyledOperationWrapper>
<Menu
items={
<AccountMenu
onOpenAccountSetting={onOpenAccountSetting}
onSignOut={onSignOut}
/>
}
contentOptions={{
side: 'right',
sideOffset: 30,
}}
>
<IconButton
data-testid="more-button"
icon={<MoreHorizontalIcon />}
type="plain"
/>
</Menu>
</StyledOperationWrapper>
</StyledModalHeader>
<Divider style={{ margin: '12px 0px' }} />
</StyledModalHeaderContent>
)} )}
<StyledModalBody> <Divider size="thinner" />
{isLoggedIn && cloudWorkspaces.length !== 0 ? ( <AFFiNEWorkspaceList workspaces={workspaces} onEventEnd={onEventEnd} />
<> {workspaces.length > 0 ? <Divider size="thinner" /> : null}
<CloudWorkSpaceList <AddWorkspace
workspaces={workspaces} onAddWorkspace={onAddWorkspace}
onClickWorkspace={onClickWorkspace} onNewWorkspace={onNewWorkspace}
onClickWorkspaceSetting={onClickWorkspaceSetting} />
onNewWorkspace={onNewWorkspace} </div>
onAddWorkspace={onAddWorkspace}
currentWorkspaceId={currentWorkspaceId}
onMoveWorkspace={onMoveWorkspace}
/>
<Divider
style={{
margin: '12px 0px',
}}
/>
</>
) : null}
<StyledModalHeader>
<StyledWorkspaceFlavourTitle>
{t['com.affine.workspace.local']()}
</StyledWorkspaceFlavourTitle>
</StyledModalHeader>
<StyledModalContent>
<WorkspaceList
items={localWorkspaces}
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]
)}
/>
</StyledModalContent>
{runtimeConfig.enableSQLiteProvider && environment.isDesktop ? (
<StyledImportWorkspaceCardPill>
<StyledItem onClick={onAddWorkspace} data-testid="add-workspace">
<StyledCreateWorkspaceCardPillContent
style={{ gap: '14px', paddingLeft: '2px' }}
>
<StyledCreateWorkspaceCardPillIcon style={{ fontSize: '24px' }}>
<ImportIcon />
</StyledCreateWorkspaceCardPillIcon>
<div>
<p>{t['com.affine.workspace.local.import']()}</p>
</div>
</StyledCreateWorkspaceCardPillContent>
</StyledItem>
</StyledImportWorkspaceCardPill>
) : null}
</StyledModalBody>
<StyledModalFooterContent>
<StyledCreateWorkspaceCardPill>
<StyledItem onClick={onNewWorkspace} data-testid="new-workspace">
<StyledCreateWorkspaceCardPillContent>
<StyledCreateWorkspaceCardPillIcon>
<PlusIcon />
</StyledCreateWorkspaceCardPillIcon>
<div>
<p>{t['New Workspace']()}</p>
</div>
</StyledCreateWorkspaceCardPillContent>
</StyledItem>
</StyledCreateWorkspaceCardPill>
</StyledModalFooterContent>
</>
); );
}; };

View File

@ -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)',
}
: {}),
};
});

View File

@ -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)',
});

View File

@ -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 (
<div>
<MenuItem
preFix={
<MenuIcon>
<AccountIcon />
</MenuIcon>
}
data-testid="editor-option-menu-import"
onClick={onOpenAccountSetting}
>
{t['com.affine.workspace.cloud.account.settings']()}
</MenuItem>
<Divider />
<MenuItem
preFix={
<MenuIcon>
<SignOutIcon />
</MenuIcon>
}
data-testid="editor-option-menu-import"
onClick={onSignOut}
>
{t['com.affine.workspace.cloud.account.logout']()}
</MenuItem>
</div>
);
};
export const UserAccountItem = ({
email,
onEventEnd,
}: {
email: string;
onEventEnd?: () => void;
}) => {
return (
<div className={styles.userAccountContainer}>
<div className={styles.userEmail}>{email}</div>
<Menu
items={<AccountMenu onEventEnd={onEventEnd} />}
contentOptions={{
side: 'right',
sideOffset: 12,
}}
>
<IconButton
data-testid="more-button"
icon={<MoreHorizontalIcon />}
type="plain"
/>
</Menu>
</div>
);
};

View File

@ -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',
});

View File

@ -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 (
<div className={styles.workspaceListWrapper}>
<div className={styles.workspaceType}>
{t['com.affine.workspaceList.workspaceListType.cloud']()}
</div>
<WorkspaceList
disabled={disabled}
items={workspaces}
currentWorkspaceId={currentWorkspaceId}
onClick={onClickWorkspace}
onSettingClick={onClickWorkspaceSetting}
onDragEnd={onDragEnd}
useIsWorkspaceOwner={useIsWorkspaceOwner}
/>
</div>
);
};
const LocalWorkspaces = ({
disabled,
workspaces,
onClickWorkspace,
onClickWorkspaceSetting,
currentWorkspaceId,
onDragEnd,
}: WorkspaceModalProps) => {
const t = useAFFiNEI18N();
if (workspaces.length === 0) {
return null;
}
return (
<div className={styles.workspaceListWrapper}>
<div className={styles.workspaceType}>
{t['com.affine.workspaceList.workspaceListType.local']()}
</div>
<WorkspaceList
disabled={disabled}
items={workspaces}
currentWorkspaceId={currentWorkspaceId}
onClick={onClickWorkspace}
onSettingClick={onClickWorkspaceSetting}
onDragEnd={onDragEnd}
/>
</div>
);
};
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 (
<ScrollableContainer
className={styles.workspaceListsWrapper}
scrollBarClassName={styles.scrollbar}
>
{isAuthenticated ? (
<div>
<CloudWorkSpaceList
workspaces={cloudWorkspaces}
onClickWorkspace={onClickWorkspace}
onClickWorkspaceSetting={onClickWorkspaceSetting}
onNewWorkspace={onNewWorkspace}
onAddWorkspace={onAddWorkspace}
currentWorkspaceId={currentWorkspaceId}
onDragEnd={onDragEnd}
/>
{localWorkspaces.length > 0 && cloudWorkspaces.length > 0 ? (
<Divider size="thinner" />
) : null}
</div>
) : null}
<LocalWorkspaces
workspaces={localWorkspaces}
onClickWorkspace={onClickWorkspace}
onClickWorkspaceSetting={onClickWorkspaceSetting}
onNewWorkspace={onNewWorkspace}
onAddWorkspace={onAddWorkspace}
currentWorkspaceId={currentWorkspaceId}
onDragEnd={onDragEnd}
/>
</ScrollableContainer>
);
};

View File

@ -30,6 +30,7 @@ import {
} from './styles'; } from './styles';
const hoverAtom = atom(false); const hoverAtom = atom(false);
// FIXME: // FIXME:
// 1. Remove mui style // 1. Remove mui style
// 2. Refactor the code to improve readability // 2. Refactor the code to improve readability
@ -41,6 +42,7 @@ const CloudWorkspaceStatus = () => {
</> </>
); );
}; };
const SyncingWorkspaceStatus = () => { const SyncingWorkspaceStatus = () => {
return ( return (
<> <>
@ -49,6 +51,7 @@ const SyncingWorkspaceStatus = () => {
</> </>
); );
}; };
const UnSyncWorkspaceStatus = () => { const UnSyncWorkspaceStatus = () => {
return ( return (
<> <>
@ -82,11 +85,14 @@ const WorkspaceStatus = ({
currentWorkspace: AllWorkspace; currentWorkspace: AllWorkspace;
}) => { }) => {
const isOnline = useSystemOnline(); const isOnline = useSystemOnline();
// todo: finish display sync status // todo: finish display sync status
const [forceSyncStatus, startForceSync] = useDatasourceSync( const [forceSyncStatus, startForceSync] = useDatasourceSync(
currentWorkspace.blockSuiteWorkspace currentWorkspace.blockSuiteWorkspace
); );
const setIsHovered = useSetAtom(hoverAtom); const setIsHovered = useSetAtom(hoverAtom);
const content = useMemo(() => { const content = useMemo(() => {
if (currentWorkspace.flavour === WorkspaceFlavour.LOCAL) { if (currentWorkspace.flavour === WorkspaceFlavour.LOCAL) {
return 'Saved locally'; return 'Saved locally';
@ -103,6 +109,7 @@ const WorkspaceStatus = ({
return 'Sync with AFFiNE Cloud'; return 'Sync with AFFiNE Cloud';
} }
}, [currentWorkspace.flavour, forceSyncStatus.type, isOnline]); }, [currentWorkspace.flavour, forceSyncStatus.type, isOnline]);
const CloudWorkspaceSyncStatus = useCallback(() => { const CloudWorkspaceSyncStatus = useCallback(() => {
if (forceSyncStatus.type === 'syncing') { if (forceSyncStatus.type === 'syncing') {
return SyncingWorkspaceStatus(); return SyncingWorkspaceStatus();
@ -160,6 +167,7 @@ export const WorkspaceCard = forwardRef<
const [name] = useBlockSuiteWorkspaceName( const [name] = useBlockSuiteWorkspaceName(
currentWorkspace.blockSuiteWorkspace currentWorkspace.blockSuiteWorkspace
); );
const [workspaceAvatar] = useBlockSuiteWorkspaceAvatarUrl( const [workspaceAvatar] = useBlockSuiteWorkspaceAvatarUrl(
currentWorkspace.blockSuiteWorkspace currentWorkspace.blockSuiteWorkspace
); );

View File

@ -6,6 +6,7 @@ export const StyledSelectorContainer = styled('div')({
alignItems: 'center', alignItems: 'center',
padding: '0 6px', padding: '0 6px',
borderRadius: '8px', borderRadius: '8px',
outline: 'none',
color: 'var(--affine-text-primary-color)', color: 'var(--affine-text-primary-color)',
':hover': { ':hover': {
cursor: 'pointer', cursor: 'pointer',

View File

@ -19,17 +19,10 @@ import {
} from '@blocksuite/icons'; } from '@blocksuite/icons';
import type { Page } from '@blocksuite/store'; import type { Page } from '@blocksuite/store';
import { useDroppable } from '@dnd-kit/core'; import { useDroppable } from '@dnd-kit/core';
import { Popover } from '@toeverything/components/popover'; import { Menu } from '@toeverything/components/menu';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
import type { HTMLAttributes, ReactElement } from 'react'; import type { HTMLAttributes, ReactElement } from 'react';
import { import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react';
forwardRef,
Suspense,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { useHistoryAtom } from '../../atoms/history'; import { useHistoryAtom } from '../../atoms/history';
import { useAppSetting } from '../../atoms/settings'; import { useAppSetting } from '../../atoms/settings';
@ -175,18 +168,21 @@ export const RootAppSidebar = ({
} }
> >
<SidebarContainer> <SidebarContainer>
<Popover <Menu
open={openUserWorkspaceList} rootOptions={{
content={ open: openUserWorkspaceList,
<Suspense> }}
<UserWithWorkspaceList onEventEnd={closeUserWorkspaceList} /> items={
</Suspense> <UserWithWorkspaceList onEventEnd={closeUserWorkspaceList} />
} }
contentOptions={{ contentOptions={{
// hide trigger // hide trigger
sideOffset: -58, sideOffset: -58,
onInteractOutside: closeUserWorkspaceList, onInteractOutside: closeUserWorkspaceList,
onEscapeKeyDown: closeUserWorkspaceList, onEscapeKeyDown: closeUserWorkspaceList,
style: {
width: '300px',
},
}} }}
> >
<WorkspaceCard <WorkspaceCard
@ -195,7 +191,7 @@ export const RootAppSidebar = ({
setOpenUserWorkspaceList(true); setOpenUserWorkspaceList(true);
}, [])} }, [])}
/> />
</Popover> </Menu>
<QuickSearchInput <QuickSearchInput
data-testid="slider-bar-quick-search-button" data-testid="slider-bar-quick-search-button"
onClick={onOpenQuickSearchModal} onClick={onOpenQuickSearchModal}

View File

@ -1,6 +1,7 @@
import { DebugLogger } from '@affine/debug'; import { DebugLogger } from '@affine/debug';
import { DEFAULT_HELLO_WORLD_PAGE_ID_SUFFIX } from '@affine/env/constant'; import { DEFAULT_HELLO_WORLD_PAGE_ID_SUFFIX } from '@affine/env/constant';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom'; import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { Menu } from '@toeverything/components/menu';
import { getWorkspace } from '@toeverything/infra/__internal__/workspace'; import { getWorkspace } from '@toeverything/infra/__internal__/workspace';
import { getCurrentStore } from '@toeverything/infra/atom'; import { getCurrentStore } from '@toeverything/infra/atom';
import { lazy } from 'react'; import { lazy } from 'react';
@ -61,15 +62,29 @@ export const Component = () => {
<> <>
<div <div
style={{ style={{
width: 300, position: 'fixed',
margin: '80px auto', left: '50%',
borderRadius: '8px', top: '50%',
boxShadow: 'var(--affine-shadow-2)',
backgroundColor: 'var(--affine-background-overlay-panel-color)',
padding: '16px 12px',
}} }}
> >
<UserWithWorkspaceList /> <Menu
rootOptions={{
open: true,
}}
items={<UserWithWorkspaceList />}
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',
},
}}
>
<div></div>
</Menu>
</div> </div>
<AllWorkspaceModals /> <AllWorkspaceModals />
</> </>

View File

@ -37,6 +37,7 @@ export const AffineWorkspaceCard = () => {
onClick={() => {}} onClick={() => {}}
onSettingClick={() => {}} onSettingClick={() => {}}
currentWorkspaceId={null} currentWorkspaceId={null}
isOwner={true}
/> />
); );
}; };

View File

@ -1,8 +1,11 @@
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { RootWorkspaceMetadata } from '@affine/workspace/atom'; 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 { 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 { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name'; import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import { useStaticBlockSuiteWorkspace } from '@toeverything/infra/__internal__/react'; import { useStaticBlockSuiteWorkspace } from '@toeverything/infra/__internal__/react';
@ -10,46 +13,56 @@ import { useCallback } from 'react';
import { import {
StyledCard, StyledCard,
StyledIconContainer,
StyledSettingLink, StyledSettingLink,
StyledWorkspaceInfo, StyledWorkspaceInfo,
StyledWorkspaceTitle, StyledWorkspaceTitle,
StyledWorkspaceTitleArea, StyledWorkspaceTitleArea,
StyledWorkspaceType,
StyledWorkspaceTypeEllipse,
StyledWorkspaceTypeText,
} from './styles'; } from './styles';
export interface WorkspaceTypeProps { export interface WorkspaceTypeProps {
flavour: WorkspaceFlavour; flavour: WorkspaceFlavour;
isOwner: boolean;
} }
const WorkspaceType = ({ flavour }: WorkspaceTypeProps) => { const WorkspaceType = ({ flavour, isOwner }: WorkspaceTypeProps) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
// fixme: cloud regression
const isOwner = true;
if (flavour === WorkspaceFlavour.LOCAL) { if (flavour === WorkspaceFlavour.LOCAL) {
return ( return (
<p <StyledWorkspaceType>
style={{ fontSize: '10px' }} <StyledWorkspaceTypeEllipse />
title={t['com.affine.workspaceType.local']()} <StyledWorkspaceTypeText>{t['Local']()}</StyledWorkspaceTypeText>
> </StyledWorkspaceType>
<span>{t['com.affine.workspaceType.local']()}</span>
</p>
); );
} }
return isOwner ? ( return isOwner ? (
<p <StyledWorkspaceType>
style={{ fontSize: '10px' }} <StyledWorkspaceTypeEllipse cloud={true} />
title={t['com.affine.workspaceType.cloud']()} <StyledWorkspaceTypeText>
> {t['com.affine.brand.affineCloud']()}
<span>{t['com.affine.workspaceType.cloud']()}</span> </StyledWorkspaceTypeText>
</p> </StyledWorkspaceType>
) : ( ) : (
<p <StyledWorkspaceType>
style={{ fontSize: '10px' }} <StyledWorkspaceTypeEllipse cloud={true} />
title={t['com.affine.workspaceType.joined']()} <StyledWorkspaceTypeText>
> {t['com.affine.brand.affineCloud']()}
<span>{t['com.affine.workspaceType.joined']()}</span> </StyledWorkspaceTypeText>
</p> <Divider
orientation="vertical"
size="thinner"
style={{ margin: '0px 8px', height: '7px' }}
/>
<Tooltip content={t['com.affine.workspaceType.joined']()}>
<StyledIconContainer>
<CollaborationIcon />
</StyledIconContainer>
</Tooltip>
</StyledWorkspaceType>
); );
}; };
@ -58,19 +71,35 @@ export interface WorkspaceCardProps {
meta: RootWorkspaceMetadata; meta: RootWorkspaceMetadata;
onClick: (workspaceId: string) => void; onClick: (workspaceId: string) => void;
onSettingClick: (workspaceId: string) => void; onSettingClick: (workspaceId: string) => void;
isOwner?: boolean;
} }
export const WorkspaceCardSkeleton = () => {
return (
<div>
<StyledCard data-testid="workspace-card">
<Skeleton variant="circular" width={28} height={28} />
<Skeleton
variant="rectangular"
height={43}
width={220}
style={{ marginLeft: '12px' }}
/>
</StyledCard>
</div>
);
};
export const WorkspaceCard = ({ export const WorkspaceCard = ({
onClick, onClick,
onSettingClick, onSettingClick,
currentWorkspaceId, currentWorkspaceId,
meta, meta,
isOwner = true,
}: WorkspaceCardProps) => { }: WorkspaceCardProps) => {
// const t = useAFFiNEI18N();
const workspace = useStaticBlockSuiteWorkspace(meta.id); const workspace = useStaticBlockSuiteWorkspace(meta.id);
const [name] = useBlockSuiteWorkspaceName(workspace); const [name] = useBlockSuiteWorkspaceName(workspace);
const [workspaceAvatar] = useBlockSuiteWorkspaceAvatarUrl(workspace); const [workspaceAvatar] = useBlockSuiteWorkspaceAvatarUrl(workspace);
return ( return (
<StyledCard <StyledCard
data-testid="workspace-card" data-testid="workspace-card"
@ -85,6 +114,7 @@ export const WorkspaceCard = ({
<StyledWorkspaceTitle>{name}</StyledWorkspaceTitle> <StyledWorkspaceTitle>{name}</StyledWorkspaceTitle>
<StyledSettingLink <StyledSettingLink
size="small"
className="setting-entry" className="setting-entry"
onClick={e => { onClick={e => {
e.stopPropagation(); e.stopPropagation();
@ -92,17 +122,10 @@ export const WorkspaceCard = ({
}} }}
withoutHoverStyle={true} withoutHoverStyle={true}
> >
<SettingsIcon style={{ margin: '0px' }} /> <SettingsIcon />
</StyledSettingLink> </StyledSettingLink>
</StyledWorkspaceTitleArea> </StyledWorkspaceTitleArea>
{/* {meta.flavour === WorkspaceFlavour.LOCAL && ( <WorkspaceType isOwner={isOwner} flavour={meta.flavour} />
<p title={t['com.affine.workspaceType.offline']()}>
<LocalDataIcon />
<WorkspaceType flavour={meta.flavour} />
</p>
)} */}
<WorkspaceType flavour={meta.flavour} />
</StyledWorkspaceInfo> </StyledWorkspaceInfo>
</StyledCard> </StyledCard>
); );

View File

@ -5,30 +5,16 @@ import { displayFlex, styled, textEllipsis } from '../../../styles';
export const StyledWorkspaceInfo = styled('div')(() => { export const StyledWorkspaceInfo = styled('div')(() => {
return { return {
marginLeft: '12px', marginLeft: '12px',
width: '202px', width: '100%',
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 StyledWorkspaceTitle = styled('div')(() => { export const StyledWorkspaceTitle = styled('div')(() => {
return { return {
fontSize: 'var(--affine-font-base)', fontSize: 'var(--affine-font-sm)',
fontWeight: 600, fontWeight: 700,
lineHeight: '24px', lineHeight: '22px',
maxWidth: '200px', maxWidth: '190px',
color: 'var(--affine-text-primary-color)', color: 'var(--affine-text-primary-color)',
...textEllipsis(1), ...textEllipsis(1),
}; };
@ -38,13 +24,12 @@ export const StyledCard = styled('div')<{
active?: boolean; active?: boolean;
}>(({ active }) => { }>(({ active }) => {
const borderColor = active ? 'var(--affine-primary-color)' : 'transparent'; const borderColor = active ? 'var(--affine-primary-color)' : 'transparent';
const backgroundColor = active ? 'var(--affine-white)' : 'transparent'; const backgroundColor = active ? 'var(--affine-white-30)' : 'transparent';
return { return {
width: '280px', width: '100%',
height: '58px',
cursor: 'pointer', cursor: 'pointer',
padding: '12px', padding: '12px',
borderRadius: '12px', borderRadius: '8px',
border: `1px solid ${borderColor}`, border: `1px solid ${borderColor}`,
...displayFlex('flex-start', 'flex-start'), ...displayFlex('flex-start', 'flex-start'),
transition: 'background .2s', transition: 'background .2s',
@ -91,8 +76,8 @@ export const StyledModalHeader = styled('div')(() => {
export const StyledSettingLink = styled(IconButton)(() => { export const StyledSettingLink = styled(IconButton)(() => {
return { return {
position: 'absolute', position: 'absolute',
right: '6px', right: '10px',
bottom: '6px', top: '10px',
opacity: 0, opacity: 0,
borderRadius: '4px', borderRadius: '4px',
color: 'var(--affine-primary-color)', 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 { return {
fontSize: 10, ...displayFlex('flex-start', 'center'),
width: '100%',
height: '20px',
}; };
}); });
@ -116,3 +103,35 @@ export const StyledWorkspaceTitleArea = styled('div')(() => {
justifyContent: 'space-between', 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)',
};
});

View File

@ -16,9 +16,12 @@ import {
} from '@dnd-kit/modifiers'; } from '@dnd-kit/modifiers';
import { arrayMove, SortableContext, useSortable } from '@dnd-kit/sortable'; import { arrayMove, SortableContext, useSortable } from '@dnd-kit/sortable';
import type { CSSProperties } from 'react'; 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'; import { workspaceItemStyle } from './index.css';
export interface WorkspaceListProps { export interface WorkspaceListProps {
@ -28,16 +31,25 @@ export interface WorkspaceListProps {
onClick: (workspaceId: string) => void; onClick: (workspaceId: string) => void;
onSettingClick: (workspaceId: string) => void; onSettingClick: (workspaceId: string) => void;
onDragEnd: (event: DragEndEvent) => void; onDragEnd: (event: DragEndEvent) => void;
useIsWorkspaceOwner?: (workspaceId: string) => boolean;
} }
interface SortableWorkspaceItemProps extends Omit<WorkspaceListProps, 'items'> { interface SortableWorkspaceItemProps extends Omit<WorkspaceListProps, 'items'> {
item: RootWorkspaceMetadata; 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 } = const { setNodeRef, attributes, listeners, transform, transition } =
useSortable({ useSortable({
id: props.item.id, id: item.id,
}); });
const style: CSSProperties = useMemo( const style: CSSProperties = useMemo(
() => ({ () => ({
@ -45,11 +57,12 @@ const SortableWorkspaceItem = (props: SortableWorkspaceItemProps) => {
? `translate3d(${transform.x}px, ${transform.y}px, 0)` ? `translate3d(${transform.x}px, ${transform.y}px, 0)`
: undefined, : undefined,
transition, transition,
pointerEvents: props.disabled ? 'none' : undefined, pointerEvents: disabled ? 'none' : undefined,
opacity: props.disabled ? 0.6 : undefined, opacity: disabled ? 0.6 : undefined,
}), }),
[props.disabled, transform, transition] [disabled, transform, transition]
); );
const isOwner = useIsWorkspaceOwner?.(item.id);
return ( return (
<div <div
className={workspaceItemStyle} className={workspaceItemStyle}
@ -60,10 +73,11 @@ const SortableWorkspaceItem = (props: SortableWorkspaceItemProps) => {
{...listeners} {...listeners}
> >
<WorkspaceCard <WorkspaceCard
currentWorkspaceId={props.currentWorkspaceId} currentWorkspaceId={currentWorkspaceId}
meta={props.item} meta={item}
onClick={props.onClick} onClick={onClick}
onSettingClick={props.onSettingClick} onSettingClick={onSettingClick}
isOwner={isOwner}
/> />
</div> </div>
); );
@ -106,7 +120,9 @@ export const WorkspaceList = (props: WorkspaceListProps) => {
<DndContext sensors={sensors} onDragEnd={onDragEnd} modifiers={modifiers}> <DndContext sensors={sensors} onDragEnd={onDragEnd} modifiers={modifiers}>
<SortableContext items={optimisticList}> <SortableContext items={optimisticList}>
{optimisticList.map(item => ( {optimisticList.map(item => (
<SortableWorkspaceItem {...props} item={item} key={item.id} /> <Suspense fallback={<WorkspaceCardSkeleton />} key={item.id}>
<SortableWorkspaceItem {...props} item={item} key={item.id} />
</Suspense>
))} ))}
</SortableContext> </SortableContext>
</DndContext> </DndContext>

View File

@ -11,6 +11,7 @@ export type ScrollableContainerProps = {
className?: string; className?: string;
viewPortClassName?: string; viewPortClassName?: string;
styles?: React.CSSProperties; styles?: React.CSSProperties;
scrollBarClassName?: string;
}; };
export const ScrollableContainer = ({ export const ScrollableContainer = ({
@ -20,6 +21,7 @@ export const ScrollableContainer = ({
className, className,
styles: _styles, styles: _styles,
viewPortClassName, viewPortClassName,
scrollBarClassName,
}: PropsWithChildren<ScrollableContainerProps>) => { }: PropsWithChildren<ScrollableContainerProps>) => {
const [hasScrollTop, ref] = useHasScrollTop(); const [hasScrollTop, ref] = useHasScrollTop();
return ( return (
@ -39,7 +41,7 @@ export const ScrollableContainer = ({
</ScrollArea.Viewport> </ScrollArea.Viewport>
<ScrollArea.Scrollbar <ScrollArea.Scrollbar
orientation="vertical" orientation="vertical"
className={clsx(styles.scrollbar, { className={clsx(styles.scrollbar, scrollBarClassName, {
[styles.TableScrollbar]: inTableView, [styles.TableScrollbar]: inTableView,
})} })}
> >

View File

@ -580,5 +580,9 @@
"Successfully enabled AFFiNE Cloud": "Successfully enabled AFFiNE Cloud", "Successfully enabled AFFiNE Cloud": "Successfully enabled AFFiNE Cloud",
"404.hint": "Sorry, you do not have access or this content does not exist...", "404.hint": "Sorry, you do not have access or this content does not exist...",
"404.back": "Back to My Content", "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"
} }

View File

@ -74,8 +74,6 @@ test('Show collections items in sidebar', async ({ page }) => {
skipInitialPage: true, skipInitialPage: true,
}); });
expect(await items.count()).toBe(1); expect(await items.count()).toBe(1);
await clickSideBarCurrentWorkspaceBanner(page);
await createLocalWorkspace( await createLocalWorkspace(
{ {
name: 'Test 1', name: 'Test 1',