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 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 (
<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>
);
};
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 (
<>
<StyledModalHeader>
<StyledModalHeaderLeft>
<StyledWorkspaceFlavourTitle>
{t['com.affine.workspace.cloud']()}
</StyledWorkspaceFlavourTitle>
</StyledModalHeaderLeft>
</StyledModalHeader>
<StyledModalContent>
<WorkspaceList
disabled={disabled}
items={
workspaces.filter(
({ flavour }) => 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]
)}
/>
</StyledModalContent>
</>
<MenuItem
className={styles.menuItem}
onClick={onClickSignIn}
data-testid="cloud-signin-button"
>
<div className={styles.signInWrapper}>
<div className={styles.iconContainer}>
<Logo1Icon />
</div>
<div className={styles.signInTextContainer}>
<div className={styles.signInTextPrimary}>
{t['com.affine.workspace.cloud.auth']()}
</div>
<div className={styles.signInTextSecondary}>
{t['com.affine.workspace.cloud.description']()}
</div>
</div>
</div>
</MenuItem>
);
};
@ -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 ? (
<StyledModalHeaderContent>
<StyledSignInCardPill>
<StyledItem
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>
<div className={styles.workspaceListWrapper}>
{isAuthenticated ? (
<UserAccountItem
email={session?.user.email ?? 'Unknown User'}
onEventEnd={onEventEnd}
/>
) : (
<StyledModalHeaderContent>
<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>
<SignInItem />
)}
<StyledModalBody>
{isLoggedIn && cloudWorkspaces.length !== 0 ? (
<>
<CloudWorkSpaceList
workspaces={workspaces}
onClickWorkspace={onClickWorkspace}
onClickWorkspaceSetting={onClickWorkspaceSetting}
onNewWorkspace={onNewWorkspace}
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>
</>
<Divider size="thinner" />
<AFFiNEWorkspaceList workspaces={workspaces} onEventEnd={onEventEnd} />
{workspaces.length > 0 ? <Divider size="thinner" /> : null}
<AddWorkspace
onAddWorkspace={onAddWorkspace}
onNewWorkspace={onNewWorkspace}
/>
</div>
);
};

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';
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
);

View File

@ -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',

View File

@ -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 = ({
}
>
<SidebarContainer>
<Popover
open={openUserWorkspaceList}
content={
<Suspense>
<UserWithWorkspaceList onEventEnd={closeUserWorkspaceList} />
</Suspense>
<Menu
rootOptions={{
open: openUserWorkspaceList,
}}
items={
<UserWithWorkspaceList onEventEnd={closeUserWorkspaceList} />
}
contentOptions={{
// hide trigger
sideOffset: -58,
onInteractOutside: closeUserWorkspaceList,
onEscapeKeyDown: closeUserWorkspaceList,
style: {
width: '300px',
},
}}
>
<WorkspaceCard
@ -195,7 +191,7 @@ export const RootAppSidebar = ({
setOpenUserWorkspaceList(true);
}, [])}
/>
</Popover>
</Menu>
<QuickSearchInput
data-testid="slider-bar-quick-search-button"
onClick={onOpenQuickSearchModal}

View File

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

View File

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

View File

@ -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 (
<p
style={{ fontSize: '10px' }}
title={t['com.affine.workspaceType.local']()}
>
<span>{t['com.affine.workspaceType.local']()}</span>
</p>
<StyledWorkspaceType>
<StyledWorkspaceTypeEllipse />
<StyledWorkspaceTypeText>{t['Local']()}</StyledWorkspaceTypeText>
</StyledWorkspaceType>
);
}
return isOwner ? (
<p
style={{ fontSize: '10px' }}
title={t['com.affine.workspaceType.cloud']()}
>
<span>{t['com.affine.workspaceType.cloud']()}</span>
</p>
<StyledWorkspaceType>
<StyledWorkspaceTypeEllipse cloud={true} />
<StyledWorkspaceTypeText>
{t['com.affine.brand.affineCloud']()}
</StyledWorkspaceTypeText>
</StyledWorkspaceType>
) : (
<p
style={{ fontSize: '10px' }}
title={t['com.affine.workspaceType.joined']()}
>
<span>{t['com.affine.workspaceType.joined']()}</span>
</p>
<StyledWorkspaceType>
<StyledWorkspaceTypeEllipse cloud={true} />
<StyledWorkspaceTypeText>
{t['com.affine.brand.affineCloud']()}
</StyledWorkspaceTypeText>
<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;
onClick: (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 = ({
onClick,
onSettingClick,
currentWorkspaceId,
meta,
isOwner = true,
}: WorkspaceCardProps) => {
// const t = useAFFiNEI18N();
const workspace = useStaticBlockSuiteWorkspace(meta.id);
const [name] = useBlockSuiteWorkspaceName(workspace);
const [workspaceAvatar] = useBlockSuiteWorkspaceAvatarUrl(workspace);
return (
<StyledCard
data-testid="workspace-card"
@ -85,6 +114,7 @@ export const WorkspaceCard = ({
<StyledWorkspaceTitle>{name}</StyledWorkspaceTitle>
<StyledSettingLink
size="small"
className="setting-entry"
onClick={e => {
e.stopPropagation();
@ -92,17 +122,10 @@ export const WorkspaceCard = ({
}}
withoutHoverStyle={true}
>
<SettingsIcon style={{ margin: '0px' }} />
<SettingsIcon />
</StyledSettingLink>
</StyledWorkspaceTitleArea>
{/* {meta.flavour === WorkspaceFlavour.LOCAL && (
<p title={t['com.affine.workspaceType.offline']()}>
<LocalDataIcon />
<WorkspaceType flavour={meta.flavour} />
</p>
)} */}
<WorkspaceType flavour={meta.flavour} />
<WorkspaceType isOwner={isOwner} flavour={meta.flavour} />
</StyledWorkspaceInfo>
</StyledCard>
);

View File

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

View File

@ -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<WorkspaceListProps, 'items'> {
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 (
<div
className={workspaceItemStyle}
@ -60,10 +73,11 @@ const SortableWorkspaceItem = (props: SortableWorkspaceItemProps) => {
{...listeners}
>
<WorkspaceCard
currentWorkspaceId={props.currentWorkspaceId}
meta={props.item}
onClick={props.onClick}
onSettingClick={props.onSettingClick}
currentWorkspaceId={currentWorkspaceId}
meta={item}
onClick={onClick}
onSettingClick={onSettingClick}
isOwner={isOwner}
/>
</div>
);
@ -106,7 +120,9 @@ export const WorkspaceList = (props: WorkspaceListProps) => {
<DndContext sensors={sensors} onDragEnd={onDragEnd} modifiers={modifiers}>
<SortableContext items={optimisticList}>
{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>
</DndContext>

View File

@ -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<ScrollableContainerProps>) => {
const [hasScrollTop, ref] = useHasScrollTop();
return (
@ -39,7 +41,7 @@ export const ScrollableContainer = ({
</ScrollArea.Viewport>
<ScrollArea.Scrollbar
orientation="vertical"
className={clsx(styles.scrollbar, {
className={clsx(styles.scrollbar, scrollBarClassName, {
[styles.TableScrollbar]: inTableView,
})}
>

View File

@ -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"
}

View File

@ -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',