mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 12:55:00 +00:00
refactor: workspace list (#4432)
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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)',
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
})}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user