From b6bdf257e40d3af0c053ede864d287ba3c1d9c42 Mon Sep 17 00:00:00 2001 From: Himself65 Date: Thu, 6 Apr 2023 13:21:45 -0500 Subject: [PATCH] feat(component): support sort workspace card (#1837) --- apps/electron/yarn.lock | 56 ++++++++++++++- apps/web/package.json | 2 + .../pure/workspace-list-modal/index.tsx | 33 +++++---- apps/web/src/layouts/index.tsx | 15 +++- apps/web/src/providers/ModalProvider.tsx | 15 +++- packages/component/package.json | 3 + .../src/components/workspace-card/index.tsx | 15 ++-- .../workspace-list/index.stories.tsx | 64 +++++++++++++++++ .../src/components/workspace-list/index.tsx | 70 +++++++++++++++++++ tests/libs/workspace.ts | 10 ++- .../local-first-workspace-list.spec.ts | 59 +++++++++++++++- yarn.lock | 56 ++++++++++++++- 12 files changed, 366 insertions(+), 32 deletions(-) create mode 100644 packages/component/src/components/workspace-list/index.stories.tsx create mode 100644 packages/component/src/components/workspace-list/index.tsx diff --git a/apps/electron/yarn.lock b/apps/electron/yarn.lock index 31fd9e2c0..5db72388b 100644 --- a/apps/electron/yarn.lock +++ b/apps/electron/yarn.lock @@ -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 diff --git a/apps/web/package.json b/apps/web/package.json index e1eb99a07..70f567093 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -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", diff --git a/apps/web/src/components/pure/workspace-list-modal/index.tsx b/apps/web/src/components/pure/workspace-list-modal/index.tsx index 8a8765d1d..fd720c49c 100644 --- a/apps/web/src/components/pure/workspace-list-modal/index.tsx +++ b/apps/web/src/components/pure/workspace-list-modal/index.tsx @@ -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 = ({ - {workspaces.map(workspace => { - return ( - - ); - })} - + { + const { active, over } = event; + if (active.id !== over?.id) { + onMoveWorkspace(active.id as string, over?.id as string); + } + }, + [onMoveWorkspace] + )} + /> = 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 = .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 = logger.info('unmount'); }; }, [set]); - const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom); - const jotaiWorkspaces = useAtomValue(jotaiWorkspacesAtom); useEffect(() => { const flavour = jotaiWorkspaces.find( diff --git a/apps/web/src/providers/ModalProvider.tsx b/apps/web/src/providers/ModalProvider.tsx index bf90e7be8..2a48d4b49 100644 --- a/apps/web/src/providers/ModalProvider.tsx +++ b/apps/web/src/providers/ModalProvider.tsx @@ -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); diff --git a/packages/component/package.json b/packages/component/package.json index 3414e89bb..675f4b1d5 100644 --- a/packages/component/package.json +++ b/packages/component/package.json @@ -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", diff --git a/packages/component/src/components/workspace-card/index.tsx b/packages/component/src/components/workspace-card/index.tsx index 627a4b13e..d2ecba795 100644 --- a/packages/component/src/components/workspace-card/index.tsx +++ b/packages/component/src/components/workspace-card/index.tsx @@ -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 ; }; -const WorkspaceType: React.FC = ({ workspace }) => { +const WorkspaceType: FC = ({ 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 = ({ +export const WorkspaceCard: FC = ({ workspace, onClick, onSettingClick, @@ -95,9 +95,12 @@ export const WorkspaceCard: React.FC = ({ return ( { - onClick(workspace); - }, [onClick, workspace])} + onClick={useCallback( + (event: MouseEvent) => { + onClick(workspace); + }, + [onClick, workspace] + )} active={workspace.id === currentWorkspaceId} > diff --git a/packages/component/src/components/workspace-list/index.stories.tsx b/packages/component/src/components/workspace-list/index.stories.tsx new file mode 100644 index 000000000..0d1a4593e --- /dev/null +++ b/packages/component/src/components/workspace-list/index.stories.tsx @@ -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; + +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 ( + {}} + 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); + }); + } + }} + /> + ); +}; diff --git a/packages/component/src/components/workspace-list/index.tsx b/packages/component/src/components/workspace-list/index.tsx new file mode 100644 index 000000000..6fe966160 --- /dev/null +++ b/packages/component/src/components/workspace-list/index.tsx @@ -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 & { + 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 ( +
+ +
+ ); +}; + +export const WorkspaceList: FC = props => { + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { + distance: 8, + }, + }) + ); + return ( + + + {props.items.map(item => ( + + ))} + + + ); +}; diff --git a/tests/libs/workspace.ts b/tests/libs/workspace.ts index 6ab8b1b78..360b9b2f7 100644 --- a/tests/libs/workspace.ts +++ b/tests/libs/workspace.ts @@ -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(); diff --git a/tests/parallels/local-first-workspace-list.spec.ts b/tests/parallels/local-first-workspace-list.spec.ts index 82972c539..7550e82a1 100644 --- a/tests/parallels/local-first-workspace-list.spec.ts +++ b/tests/parallels/local-first-workspace-list.spec.ts @@ -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); }); }); diff --git a/yarn.lock b/yarn.lock index 0186727ad..8d374eb31 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"