feat(component): support sort workspace card (#1837)

This commit is contained in:
Himself65
2023-04-06 13:21:45 -05:00
committed by GitHub
parent 773554bbac
commit b6bdf257e4
12 changed files with 366 additions and 32 deletions

View File

@@ -37,11 +37,14 @@ __metadata:
"@affine/debug": "workspace:*"
"@affine/i18n": "workspace:*"
"@affine/jotai": "workspace:*"
"@affine/workspace": "workspace:^"
"@blocksuite/blocks": 0.0.0-20230406032111-01cf598b-nightly
"@blocksuite/editor": 0.0.0-20230406032111-01cf598b-nightly
"@blocksuite/global": 0.0.0-20230406032111-01cf598b-nightly
"@blocksuite/icons": 2.1.5
"@blocksuite/store": 0.0.0-20230406032111-01cf598b-nightly
"@dnd-kit/core": ^6.0.8
"@dnd-kit/sortable": ^7.0.2
"@emotion/cache": ^11.10.7
"@emotion/react": ^11.10.6
"@emotion/server": ^11.10.0
@@ -178,7 +181,7 @@ __metadata:
languageName: unknown
linkType: soft
"@affine/workspace@workspace:../../packages/workspace":
"@affine/workspace@workspace:../../packages/workspace, @affine/workspace@workspace:^":
version: 0.0.0-use.local
resolution: "@affine/workspace@workspace:../../packages/workspace"
dependencies:
@@ -1861,6 +1864,55 @@ __metadata:
languageName: node
linkType: hard
"@dnd-kit/accessibility@npm:^3.0.0":
version: 3.0.1
resolution: "@dnd-kit/accessibility@npm:3.0.1"
dependencies:
tslib: ^2.0.0
peerDependencies:
react: ">=16.8.0"
checksum: 0afc2c0fce9a1c107453620ca0da1778f182d340e74ffbc6e369ef0ac8943cafb929d3a6c0891d9b915aa23b2b92137ff4fad958f43118466586d8129a3359d5
languageName: node
linkType: hard
"@dnd-kit/core@npm:^6.0.8":
version: 6.0.8
resolution: "@dnd-kit/core@npm:6.0.8"
dependencies:
"@dnd-kit/accessibility": ^3.0.0
"@dnd-kit/utilities": ^3.2.1
tslib: ^2.0.0
peerDependencies:
react: ">=16.8.0"
react-dom: ">=16.8.0"
checksum: abe48ff7395f84fd8c15e6c8b13da4df153dc1f1076096d783acd0c25539516c77e4854ea59be6621dde55739cb0df1d62924ad069df3267fe05ad90ef729b2f
languageName: node
linkType: hard
"@dnd-kit/sortable@npm:^7.0.2":
version: 7.0.2
resolution: "@dnd-kit/sortable@npm:7.0.2"
dependencies:
"@dnd-kit/utilities": ^3.2.0
tslib: ^2.0.0
peerDependencies:
"@dnd-kit/core": ^6.0.7
react: ">=16.8.0"
checksum: 4ce705aceb15766a0deefe25a9d95a87e9413c3fb9088ea3eb0962e57f844895000117fcec7c0944a0d4ae4e1e889cfa69e3d3778164d4d23115fb1edb218283
languageName: node
linkType: hard
"@dnd-kit/utilities@npm:^3.2.0, @dnd-kit/utilities@npm:^3.2.1":
version: 3.2.1
resolution: "@dnd-kit/utilities@npm:3.2.1"
dependencies:
tslib: ^2.0.0
peerDependencies:
react: ">=16.8.0"
checksum: 038fd5cc1328bf4c9dca17cd48046e5a687bbf9d904c7197f851aab869ab52d9dee2734b2e255256fd6158245acd00063a23deed962c7673c0fadfbf061f04ca
languageName: node
linkType: hard
"@electron-forge/cli@npm:^6.1.0":
version: 6.1.0
resolution: "@electron-forge/cli@npm:6.1.0"
@@ -16193,7 +16245,7 @@ __metadata:
languageName: node
linkType: hard
"tslib@npm:^2.0.1, tslib@npm:^2.1.0, tslib@npm:^2.4.0":
"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.1.0, tslib@npm:^2.4.0":
version: 2.5.0
resolution: "tslib@npm:2.5.0"
checksum: ae3ed5f9ce29932d049908ebfdf21b3a003a85653a9a140d614da6b767a93ef94f460e52c3d787f0e4f383546981713f165037dc2274df212ea9f8a4541004e1

View File

@@ -21,6 +21,8 @@
"@blocksuite/editor": "0.0.0-20230406032111-01cf598b-nightly",
"@blocksuite/icons": "^2.1.5",
"@blocksuite/store": "0.0.0-20230406032111-01cf598b-nightly",
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/sortable": "^7.0.2",
"@emotion/cache": "^11.10.7",
"@emotion/react": "^11.10.6",
"@emotion/server": "^11.10.0",

View File

@@ -4,10 +4,12 @@ import {
ModalWrapper,
Tooltip,
} from '@affine/component';
import { WorkspaceCard } from '@affine/component/workspace-card';
import { WorkspaceList } from '@affine/component/workspace-list';
import { useTranslation } from '@affine/i18n';
import type { AccessTokenMessage } from '@affine/workspace/affine/login';
import { HelpIcon, PlusIcon } from '@blocksuite/icons';
import type { DragEndEvent } from '@dnd-kit/core';
import { useCallback } from 'react';
import type { AllWorkspace } from '../../../shared';
import { Footer } from '../footer';
@@ -37,6 +39,7 @@ interface WorkspaceModalProps {
onClickLogin: () => void;
onClickLogout: () => void;
onCreateWorkspace: () => void;
onMoveWorkspace: (activeId: string, overId: string) => void;
}
export const WorkspaceListModal = ({
@@ -50,6 +53,7 @@ export const WorkspaceListModal = ({
onClickWorkspaceSetting,
onCreateWorkspace,
currentWorkspaceId,
onMoveWorkspace,
}: WorkspaceModalProps) => {
const { t } = useTranslation();
@@ -91,18 +95,21 @@ export const WorkspaceListModal = ({
</StyledModalHeader>
<StyledModalContent>
{workspaces.map(workspace => {
return (
<WorkspaceCard
workspace={workspace}
currentWorkspaceId={currentWorkspaceId}
onClick={onClickWorkspace}
onSettingClick={onClickWorkspaceSetting}
key={workspace.id}
/>
);
})}
<WorkspaceList
items={workspaces}
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]
)}
/>
<StyledCreateWorkspaceCard
data-testid="new-workspace"
onClick={onCreateWorkspace}

View File

@@ -2,7 +2,7 @@ import { DebugLogger } from '@affine/debug';
import { config } from '@affine/env';
import { setUpLanguage, useTranslation } from '@affine/i18n';
import { createAffineGlobalChannel } from '@affine/workspace/affine/sync';
import { jotaiWorkspacesAtom } from '@affine/workspace/atom';
import { jotaiStore, jotaiWorkspacesAtom } from '@affine/workspace/atom';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { assertExists, nanoid } from '@blocksuite/store';
import { NoSsr } from '@mui/material';
@@ -122,6 +122,8 @@ export const WorkspaceLayout: FC<PropsWithChildren> =
setUpLanguage(i18n);
}, [i18n]);
useCreateFirstWorkspace();
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom);
const jotaiWorkspaces = useAtomValue(jotaiWorkspacesAtom);
const set = useSetAtom(jotaiWorkspacesAtom);
useEffect(() => {
logger.info('mount');
@@ -131,10 +133,19 @@ export const WorkspaceLayout: FC<PropsWithChildren> =
.map(({ CRUD }) => CRUD.list);
async function fetch() {
const jotaiWorkspaces = jotaiStore.get(jotaiWorkspacesAtom);
const items = [];
for (const list of lists) {
try {
const item = await list();
if (jotaiWorkspaces.length) {
item.sort((a, b) => {
return (
jotaiWorkspaces.findIndex(x => x.id === a.id) -
jotaiWorkspaces.findIndex(x => x.id === b.id)
);
});
}
items.push(...item.map(x => ({ id: x.id, flavour: x.flavour })));
} catch (e) {
logger.error('list data error:', e);
@@ -153,8 +164,6 @@ export const WorkspaceLayout: FC<PropsWithChildren> =
logger.info('unmount');
};
}, [set]);
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom);
const jotaiWorkspaces = useAtomValue(jotaiWorkspacesAtom);
useEffect(() => {
const flavour = jotaiWorkspaces.find(

View File

@@ -1,4 +1,6 @@
import { useAtom, useAtomValue } from 'jotai';
import { jotaiWorkspacesAtom } from '@affine/workspace/atom';
import { arrayMove } from '@dnd-kit/sortable';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import dynamic from 'next/dynamic';
import { useRouter } from 'next/router';
import type React from 'react';
@@ -39,6 +41,7 @@ export function Modals() {
const { jumpToSubPath } = useRouterHelper(router);
const user = useCurrentUser();
const workspaces = useWorkspaces();
const setWorkspaces = useSetAtom(jotaiWorkspacesAtom);
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom);
const [, setCurrentWorkspace] = useCurrentWorkspace();
const { createLocalWorkspace } = useWorkspacesHelper();
@@ -53,6 +56,16 @@ export function Modals() {
onClose={useCallback(() => {
setOpenWorkspacesModal(false);
}, [setOpenWorkspacesModal])}
onMoveWorkspace={useCallback(
(activeId, overId) => {
const oldIndex = workspaces.findIndex(w => w.id === activeId);
const newIndex = workspaces.findIndex(w => w.id === overId);
setWorkspaces(workspaces =>
arrayMove(workspaces, oldIndex, newIndex)
);
},
[setWorkspaces, workspaces]
)}
onClickWorkspace={useCallback(
workspace => {
setOpenWorkspacesModal(false);

View File

@@ -17,11 +17,14 @@
"@affine/debug": "workspace:*",
"@affine/i18n": "workspace:*",
"@affine/jotai": "workspace:*",
"@affine/workspace": "workspace:^",
"@blocksuite/blocks": "0.0.0-20230406032111-01cf598b-nightly",
"@blocksuite/editor": "0.0.0-20230406032111-01cf598b-nightly",
"@blocksuite/global": "0.0.0-20230406032111-01cf598b-nightly",
"@blocksuite/icons": "2.1.5",
"@blocksuite/store": "0.0.0-20230406032111-01cf598b-nightly",
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/sortable": "^7.0.2",
"@emotion/cache": "^11.10.7",
"@emotion/react": "^11.10.6",
"@emotion/server": "^11.10.0",

View File

@@ -4,7 +4,7 @@ import type { AffineWorkspace, LocalWorkspace } from '@affine/workspace/type';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { SettingsIcon } from '@blocksuite/icons';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-blocksuite-workspace-name';
import type React from 'react';
import type { FC, MouseEvent } from 'react';
import { useCallback } from 'react';
import { WorkspaceAvatar } from '../workspace-avatar';
@@ -45,7 +45,7 @@ const PublishIcon = () => {
return <DefaultPublishIcon style={{ color: '#8699FF' }} />;
};
const WorkspaceType: React.FC<WorkspaceTypeProps> = ({ workspace }) => {
const WorkspaceType: FC<WorkspaceTypeProps> = ({ workspace }) => {
const { t } = useTranslation();
let isOwner = true;
if (workspace.flavour === WorkspaceFlavour.AFFINE) {
@@ -83,7 +83,7 @@ export type WorkspaceCardProps = {
onSettingClick: (workspace: AffineWorkspace | LocalWorkspace) => void;
};
export const WorkspaceCard: React.FC<WorkspaceCardProps> = ({
export const WorkspaceCard: FC<WorkspaceCardProps> = ({
workspace,
onClick,
onSettingClick,
@@ -95,9 +95,12 @@ export const WorkspaceCard: React.FC<WorkspaceCardProps> = ({
return (
<StyledCard
data-testid="workspace-card"
onClick={useCallback(() => {
onClick(workspace);
}, [onClick, workspace])}
onClick={useCallback(
(event: MouseEvent) => {
onClick(workspace);
},
[onClick, workspace]
)}
active={workspace.id === currentWorkspaceId}
>
<WorkspaceAvatar size={58} workspace={workspace} />

View File

@@ -0,0 +1,64 @@
import { WorkspaceFlavour } from '@affine/workspace/type';
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
import { arrayMove } from '@dnd-kit/sortable';
import type { Meta } from '@storybook/react';
import { useState } from 'react';
import type { WorkspaceListProps } from './index';
import { WorkspaceList } from './index';
export default {
title: 'AFFiNE/WorkspaceList',
component: WorkspaceList,
} satisfies Meta<WorkspaceListProps>;
export const Default = () => {
const [items, setItems] = useState(() => {
const items = [
{
id: '1',
flavour: WorkspaceFlavour.LOCAL,
blockSuiteWorkspace: createEmptyBlockSuiteWorkspace('1'),
providers: [],
},
{
id: '2',
flavour: WorkspaceFlavour.LOCAL,
blockSuiteWorkspace: createEmptyBlockSuiteWorkspace('2'),
providers: [],
},
{
id: '3',
flavour: WorkspaceFlavour.LOCAL,
blockSuiteWorkspace: createEmptyBlockSuiteWorkspace('3'),
providers: [],
},
] satisfies WorkspaceListProps['items'];
items.forEach(item => {
item.blockSuiteWorkspace.meta.setName(item.id);
});
return items;
});
return (
<WorkspaceList
currentWorkspaceId={null}
items={items}
onClick={() => {}}
onSettingClick={() => {}}
onDragEnd={event => {
const { active, over } = event;
if (active.id !== over?.id) {
setItems(items => {
const oldIndex = items.findIndex(item => item.id === active.id);
const newIndex = items.findIndex(item => item.id === over?.id);
return arrayMove(items, oldIndex, newIndex);
});
}
}}
/>
);
};

View File

@@ -0,0 +1,70 @@
import type { AffineWorkspace, LocalWorkspace } from '@affine/workspace/type';
import type { DragEndEvent } from '@dnd-kit/core';
import {
DndContext,
PointerSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import { SortableContext, useSortable } from '@dnd-kit/sortable';
import type { FC } from 'react';
import { WorkspaceCard } from '../workspace-card';
export type WorkspaceListProps = {
currentWorkspaceId: string | null;
items: (AffineWorkspace | LocalWorkspace)[];
onClick: (workspace: AffineWorkspace | LocalWorkspace) => void;
onSettingClick: (workspace: AffineWorkspace | LocalWorkspace) => void;
onDragEnd: (event: DragEndEvent) => void;
};
const SortableWorkspaceItem: FC<
Omit<WorkspaceListProps, 'items'> & {
item: AffineWorkspace | LocalWorkspace;
}
> = props => {
const { setNodeRef, attributes, listeners, transform } = useSortable({
id: props.item.id,
});
const style = transform
? {
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
}
: undefined;
return (
<div
data-testid="draggable-item"
style={style}
ref={setNodeRef}
{...attributes}
{...listeners}
>
<WorkspaceCard
currentWorkspaceId={props.currentWorkspaceId}
workspace={props.item}
onClick={props.onClick}
onSettingClick={props.onSettingClick}
/>
</div>
);
};
export const WorkspaceList: FC<WorkspaceListProps> = props => {
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 8,
},
})
);
return (
<DndContext sensors={sensors} onDragEnd={props.onDragEnd}>
<SortableContext items={props.items}>
{props.items.map(item => (
<SortableWorkspaceItem {...props} item={item} key={item.id} />
))}
</SortableContext>
</DndContext>
);
};

View File

@@ -4,13 +4,17 @@ import { expect } from '@playwright/test';
interface CreateWorkspaceParams {
name: string;
}
export async function openWorkspaceListModal(page: Page) {
const workspaceName = page.getByTestId('workspace-name');
await workspaceName.click();
}
export async function createWorkspace(
params: CreateWorkspaceParams,
page: Page
) {
// open workspace list modal
const workspaceName = page.getByTestId('workspace-name');
await workspaceName.click();
await openWorkspaceListModal(page);
// open create workspace modal
await page.locator('.add-icon').click();

View File

@@ -4,7 +4,7 @@ import { openHomePage } from '../libs/load-page';
import { waitMarkdownImported } from '../libs/page-logic';
import { test } from '../libs/playwright';
import { clickSideBarAllPageButton } from '../libs/sidebar';
import { createWorkspace } from '../libs/workspace';
import { createWorkspace, openWorkspaceListModal } from '../libs/workspace';
test.describe('Local first workspace list', () => {
test('just one item in the workspace list at first', async ({ page }) => {
@@ -59,8 +59,61 @@ test.describe('Local first workspace list', () => {
const workspaceName = page.getByTestId('workspace-name');
await workspaceName.click();
{
//check workspace list length
const workspaceCards = await page.$$('data-testid=workspace-card');
expect(workspaceCards.length).toBe(3);
}
await page.reload();
await openWorkspaceListModal(page);
await page.getByTestId('draggable-item').nth(1).click();
await page.waitForTimeout(50);
// @ts-expect-error
const currentId: string = await page.evaluate(() => currentWorkspace.id);
await openWorkspaceListModal(page);
const sourceElement = await page.getByTestId('draggable-item').nth(2);
const targetElement = await page.getByTestId('draggable-item').nth(1);
const sourceBox = await sourceElement.boundingBox();
const targetBox = await targetElement.boundingBox();
if (!sourceBox || !targetBox) {
throw new Error('sourceBox or targetBox is null');
}
await page.mouse.move(
sourceBox.x + sourceBox.width / 2,
sourceBox.y + sourceBox.height / 2,
{
steps: 5,
}
);
await page.mouse.down();
await page.mouse.move(
targetBox.x + targetBox.width / 2,
targetBox.y + targetBox.height / 2,
{
steps: 5,
}
);
await page.mouse.up();
await page.waitForTimeout(50);
await page.reload();
await openWorkspaceListModal(page);
//check workspace list length
const workspaceCards = await page.$$('data-testid=workspace-card');
expect(workspaceCards.length).toBe(3);
{
const workspaceCards1 = await page.$$('data-testid=workspace-card');
expect(workspaceCards1.length).toBe(3);
}
await page.getByTestId('draggable-item').nth(2).click();
// @ts-expect-error
const nextId: string = await page.evaluate(() => currentWorkspace.id);
expect(currentId).toBe(nextId);
});
});

View File

@@ -37,11 +37,14 @@ __metadata:
"@affine/debug": "workspace:*"
"@affine/i18n": "workspace:*"
"@affine/jotai": "workspace:*"
"@affine/workspace": "workspace:^"
"@blocksuite/blocks": 0.0.0-20230406032111-01cf598b-nightly
"@blocksuite/editor": 0.0.0-20230406032111-01cf598b-nightly
"@blocksuite/global": 0.0.0-20230406032111-01cf598b-nightly
"@blocksuite/icons": 2.1.5
"@blocksuite/store": 0.0.0-20230406032111-01cf598b-nightly
"@dnd-kit/core": ^6.0.8
"@dnd-kit/sortable": ^7.0.2
"@emotion/cache": ^11.10.7
"@emotion/react": ^11.10.6
"@emotion/server": ^11.10.0
@@ -170,6 +173,8 @@ __metadata:
"@blocksuite/editor": 0.0.0-20230406032111-01cf598b-nightly
"@blocksuite/icons": ^2.1.5
"@blocksuite/store": 0.0.0-20230406032111-01cf598b-nightly
"@dnd-kit/core": ^6.0.8
"@dnd-kit/sortable": ^7.0.2
"@emotion/cache": ^11.10.7
"@emotion/react": ^11.10.6
"@emotion/server": ^11.10.0
@@ -215,7 +220,7 @@ __metadata:
languageName: unknown
linkType: soft
"@affine/workspace@workspace:*, @affine/workspace@workspace:packages/workspace":
"@affine/workspace@workspace:*, @affine/workspace@workspace:^, @affine/workspace@workspace:packages/workspace":
version: 0.0.0-use.local
resolution: "@affine/workspace@workspace:packages/workspace"
dependencies:
@@ -2095,6 +2100,55 @@ __metadata:
languageName: node
linkType: hard
"@dnd-kit/accessibility@npm:^3.0.0":
version: 3.0.1
resolution: "@dnd-kit/accessibility@npm:3.0.1"
dependencies:
tslib: ^2.0.0
peerDependencies:
react: ">=16.8.0"
checksum: 0afc2c0fce9a1c107453620ca0da1778f182d340e74ffbc6e369ef0ac8943cafb929d3a6c0891d9b915aa23b2b92137ff4fad958f43118466586d8129a3359d5
languageName: node
linkType: hard
"@dnd-kit/core@npm:^6.0.8":
version: 6.0.8
resolution: "@dnd-kit/core@npm:6.0.8"
dependencies:
"@dnd-kit/accessibility": ^3.0.0
"@dnd-kit/utilities": ^3.2.1
tslib: ^2.0.0
peerDependencies:
react: ">=16.8.0"
react-dom: ">=16.8.0"
checksum: abe48ff7395f84fd8c15e6c8b13da4df153dc1f1076096d783acd0c25539516c77e4854ea59be6621dde55739cb0df1d62924ad069df3267fe05ad90ef729b2f
languageName: node
linkType: hard
"@dnd-kit/sortable@npm:^7.0.2":
version: 7.0.2
resolution: "@dnd-kit/sortable@npm:7.0.2"
dependencies:
"@dnd-kit/utilities": ^3.2.0
tslib: ^2.0.0
peerDependencies:
"@dnd-kit/core": ^6.0.7
react: ">=16.8.0"
checksum: 4ce705aceb15766a0deefe25a9d95a87e9413c3fb9088ea3eb0962e57f844895000117fcec7c0944a0d4ae4e1e889cfa69e3d3778164d4d23115fb1edb218283
languageName: node
linkType: hard
"@dnd-kit/utilities@npm:^3.2.0, @dnd-kit/utilities@npm:^3.2.1":
version: 3.2.1
resolution: "@dnd-kit/utilities@npm:3.2.1"
dependencies:
tslib: ^2.0.0
peerDependencies:
react: ">=16.8.0"
checksum: 038fd5cc1328bf4c9dca17cd48046e5a687bbf9d904c7197f851aab869ab52d9dee2734b2e255256fd6158245acd00063a23deed962c7673c0fadfbf061f04ca
languageName: node
linkType: hard
"@emotion/babel-plugin@npm:^11.10.6":
version: 11.10.6
resolution: "@emotion/babel-plugin@npm:11.10.6"