From cb2ecfcea3a4eb93416b60d54c30ec4c9beae419 Mon Sep 17 00:00:00 2001 From: Nathan Panchout Date: Mon, 17 Mar 2025 15:12:12 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(frontend)=20Added=20drag-and-drop=20f?= =?UTF-8?q?unctionality=20for=20document=20management?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added a new feature for moving documents within the user interface via drag-and-drop. This includes the creation of Draggable and Droppable components, as well as tests to verify document creation and movement behavior. Changes have also been made to document types to include user roles and child management capabilities. --- .../apps/e2e/__tests__/app-impress/common.ts | 17 + .../app-impress/doc-grid-dnd.spec.ts | 311 ++++++++++++++++++ .../__tests__/app-impress/doc-grid.spec.ts | 2 + .../__tests__/app-impress/doc-header.spec.ts | 4 +- .../features/docs/doc-management/types.tsx | 8 +- .../features/docs/doc-tree/api/useMove.tsx | 36 ++ .../components/DocGridContentList.tsx | 170 ++++++++++ .../docs/docs-grid/components/DocsGrid.tsx | 11 +- .../docs-grid/components/DocsGridItem.tsx | 55 ++-- .../docs/docs-grid/components/Draggable.tsx | 26 ++ .../docs/docs-grid/components/Droppable.tsx | 53 +++ .../docs-grid/components/SimpleDocItem.tsx | 1 + .../docs/docs-grid/hooks/useDragAndDrop.tsx | 70 ++++ .../service-worker/plugins/ApiPlugin.ts | 2 + 14 files changed, 736 insertions(+), 30 deletions(-) create mode 100644 src/frontend/apps/e2e/__tests__/app-impress/doc-grid-dnd.spec.ts create mode 100644 src/frontend/apps/impress/src/features/docs/doc-tree/api/useMove.tsx create mode 100644 src/frontend/apps/impress/src/features/docs/docs-grid/components/DocGridContentList.tsx create mode 100644 src/frontend/apps/impress/src/features/docs/docs-grid/components/Draggable.tsx create mode 100644 src/frontend/apps/impress/src/features/docs/docs-grid/components/Droppable.tsx create mode 100644 src/frontend/apps/impress/src/features/docs/docs-grid/hooks/useDragAndDrop.tsx diff --git a/src/frontend/apps/e2e/__tests__/app-impress/common.ts b/src/frontend/apps/e2e/__tests__/app-impress/common.ts index fede1a9b..36ee1448 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/common.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/common.ts @@ -239,6 +239,7 @@ export const mockedDocument = async (page: Page, json: object) => { }, link_reach: 'restricted', created_at: '2021-09-01T09:00:00Z', + user_roles: ['owner'], ...json, }, }); @@ -248,6 +249,22 @@ export const mockedDocument = async (page: Page, json: object) => { }); }; +export const mockedListDocs = async (page: Page, data: object[] = []) => { + await page.route('**/documents/**/', async (route) => { + const request = route.request(); + if (request.method().includes('GET') && request.url().includes('page=')) { + await route.fulfill({ + json: { + count: data.length, + next: null, + previous: null, + results: data, + }, + }); + } + }); +}; + export const mockedInvitations = async (page: Page, json?: object) => { await page.route('**/invitations/**/', async (route) => { const request = route.request(); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-grid-dnd.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-grid-dnd.spec.ts new file mode 100644 index 00000000..e3e41bf9 --- /dev/null +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-grid-dnd.spec.ts @@ -0,0 +1,311 @@ +import { expect, test } from '@playwright/test'; + +import { createDoc, mockedListDocs } from './common'; + +test.describe('Doc grid dnd', () => { + test('it creates a doc', async ({ page, browserName }) => { + await page.goto('/'); + const header = page.locator('header').first(); + await createDoc(page, 'Draggable doc', browserName, 1); + await header.locator('h2').getByText('Docs').click(); + await createDoc(page, 'Droppable doc', browserName, 1); + await header.locator('h2').getByText('Docs').click(); + + const response = await page.waitForResponse( + (response) => + response.url().endsWith('documents/?page=1') && + response.status() === 200, + ); + const responseJson = await response.json(); + + const items = responseJson.results; + + const docsGrid = page.getByTestId('docs-grid'); + await expect(docsGrid).toBeVisible(); + await expect(page.getByTestId('grid-loader')).toBeHidden(); + const draggableElement = page.getByTestId(`draggable-doc-${items[1].id}`); + const dropZone = page.getByTestId(`droppable-doc-${items[0].id}`); + await expect(draggableElement).toBeVisible(); + await expect(dropZone).toBeVisible(); + + // Obtenir les positions des éléments + const draggableBoundingBox = await draggableElement.boundingBox(); + const dropZoneBoundingBox = await dropZone.boundingBox(); + + expect(draggableBoundingBox).toBeDefined(); + expect(dropZoneBoundingBox).toBeDefined(); + + // eslint-disable-next-line playwright/no-conditional-in-test + if (!draggableBoundingBox || !dropZoneBoundingBox) { + throw new Error('Impossible de déterminer la position des éléments'); + } + + await page.mouse.move( + draggableBoundingBox.x + draggableBoundingBox.width / 2, + draggableBoundingBox.y + draggableBoundingBox.height / 2, + ); + await page.mouse.down(); + + // Déplacer vers la zone cible + await page.mouse.move( + dropZoneBoundingBox.x + dropZoneBoundingBox.width / 2, + dropZoneBoundingBox.y + dropZoneBoundingBox.height / 2, + { steps: 10 }, // Rendre le mouvement plus fluide + ); + + const dragOverlay = page.getByTestId('drag-doc-overlay'); + + await expect(dragOverlay).toBeVisible(); + await expect(dragOverlay).toHaveText(items[1].title as string); + await page.mouse.up(); + + await expect(dragOverlay).toBeHidden(); + }); + + test('it checks cant drop when we have not the minimum role', async ({ + page, + }) => { + await mockedListDocs(page, data); + await page.goto('/'); + const docsGrid = page.getByTestId('docs-grid'); + await expect(docsGrid).toBeVisible(); + await expect(page.getByTestId('grid-loader')).toBeHidden(); + + const canDropAndDrag = page.getByTestId('droppable-doc-can-drop-and-drag'); + + const noDropAndNoDrag = page.getByTestId( + 'droppable-doc-no-drop-and-no-drag', + ); + + await expect(canDropAndDrag).toBeVisible(); + + await expect(noDropAndNoDrag).toBeVisible(); + + const canDropAndDragBoundigBox = await canDropAndDrag.boundingBox(); + + const noDropAndNoDragBoundigBox = await noDropAndNoDrag.boundingBox(); + + // eslint-disable-next-line playwright/no-conditional-in-test + if (!canDropAndDragBoundigBox || !noDropAndNoDragBoundigBox) { + throw new Error('Impossible de déterminer la position des éléments'); + } + + await page.mouse.move( + canDropAndDragBoundigBox.x + canDropAndDragBoundigBox.width / 2, + canDropAndDragBoundigBox.y + canDropAndDragBoundigBox.height / 2, + ); + + await page.mouse.down(); + + await page.mouse.move( + noDropAndNoDragBoundigBox.x + noDropAndNoDragBoundigBox.width / 2, + noDropAndNoDragBoundigBox.y + noDropAndNoDragBoundigBox.height / 2, + { steps: 10 }, + ); + + const dragOverlay = page.getByTestId('drag-doc-overlay'); + + await expect(dragOverlay).toBeVisible(); + await expect(dragOverlay).toHaveText( + 'You must be at least the editor of the target document', + ); + + await page.mouse.up(); + }); + + test('it checks cant drag when we have not the minimum role', async ({ + page, + }) => { + await mockedListDocs(page, data); + await page.goto('/'); + const docsGrid = page.getByTestId('docs-grid'); + await expect(docsGrid).toBeVisible(); + await expect(page.getByTestId('grid-loader')).toBeHidden(); + + const canDropAndDrag = page.getByTestId('droppable-doc-can-drop-and-drag'); + + const noDropAndNoDrag = page.getByTestId( + 'droppable-doc-no-drop-and-no-drag', + ); + + await expect(canDropAndDrag).toBeVisible(); + + await expect(noDropAndNoDrag).toBeVisible(); + + const canDropAndDragBoundigBox = await canDropAndDrag.boundingBox(); + + const noDropAndNoDragBoundigBox = await noDropAndNoDrag.boundingBox(); + + // eslint-disable-next-line playwright/no-conditional-in-test + if (!canDropAndDragBoundigBox || !noDropAndNoDragBoundigBox) { + throw new Error('Impossible de déterminer la position des éléments'); + } + + await page.mouse.move( + noDropAndNoDragBoundigBox.x + noDropAndNoDragBoundigBox.width / 2, + noDropAndNoDragBoundigBox.y + noDropAndNoDragBoundigBox.height / 2, + ); + + await page.mouse.down(); + + await page.mouse.move( + canDropAndDragBoundigBox.x + canDropAndDragBoundigBox.width / 2, + canDropAndDragBoundigBox.y + canDropAndDragBoundigBox.height / 2, + { steps: 10 }, + ); + + const dragOverlay = page.getByTestId('drag-doc-overlay'); + + await expect(dragOverlay).toBeVisible(); + await expect(dragOverlay).toHaveText( + 'You must have admin rights to move the document', + ); + + await page.mouse.up(); + }); +}); + +const data = [ + { + id: 'can-drop-and-drag', + abilities: { + accesses_manage: true, + accesses_view: true, + ai_transform: true, + ai_translate: true, + attachment_upload: true, + children_list: true, + children_create: true, + collaboration_auth: true, + descendants: true, + destroy: true, + favorite: true, + link_configuration: true, + invite_owner: true, + move: true, + partial_update: true, + restore: true, + retrieve: true, + media_auth: true, + link_select_options: { + restricted: ['reader', 'editor'], + authenticated: ['reader', 'editor'], + public: ['reader', 'editor'], + }, + tree: true, + update: true, + versions_destroy: true, + versions_list: true, + versions_retrieve: true, + }, + created_at: '2025-03-14T14:45:22.527221Z', + creator: 'bc6895e0-8f6d-4b00-827d-c143aa6b2ecb', + depth: 1, + excerpt: null, + is_favorite: false, + link_role: 'reader', + link_reach: 'restricted', + nb_accesses_ancestors: 1, + nb_accesses_direct: 1, + numchild: 5, + path: '000000o', + title: 'Can drop and drag', + updated_at: '2025-03-14T14:45:27.699542Z', + user_roles: ['owner'], + }, + { + id: 'can-only-drop', + title: 'Can only drop', + abilities: { + accesses_manage: true, + accesses_view: true, + ai_transform: true, + ai_translate: true, + attachment_upload: true, + children_list: true, + children_create: true, + collaboration_auth: true, + descendants: true, + destroy: true, + favorite: true, + link_configuration: true, + invite_owner: true, + move: true, + partial_update: true, + restore: true, + retrieve: true, + media_auth: true, + link_select_options: { + restricted: ['reader', 'editor'], + authenticated: ['reader', 'editor'], + public: ['reader', 'editor'], + }, + tree: true, + update: true, + versions_destroy: true, + versions_list: true, + versions_retrieve: true, + }, + created_at: '2025-03-14T14:45:22.527221Z', + creator: 'bc6895e0-8f6d-4b00-827d-c143aa6b2ecb', + depth: 1, + excerpt: null, + is_favorite: false, + link_role: 'reader', + link_reach: 'restricted', + nb_accesses_ancestors: 1, + nb_accesses_direct: 1, + numchild: 5, + path: '000000o', + + updated_at: '2025-03-14T14:45:27.699542Z', + user_roles: ['editor'], + }, + { + id: 'no-drop-and-no-drag', + abilities: { + accesses_manage: false, + accesses_view: true, + ai_transform: false, + ai_translate: false, + attachment_upload: false, + children_list: true, + children_create: false, + collaboration_auth: true, + descendants: true, + destroy: false, + favorite: true, + link_configuration: false, + invite_owner: false, + move: false, + partial_update: false, + restore: false, + retrieve: true, + media_auth: true, + link_select_options: { + restricted: ['reader', 'editor'], + authenticated: ['reader', 'editor'], + public: ['reader', 'editor'], + }, + tree: true, + update: false, + versions_destroy: false, + versions_list: true, + versions_retrieve: true, + }, + created_at: '2025-03-14T14:44:16.032773Z', + creator: '9264f420-f018-4bd6-96ae-4788f41af56d', + depth: 1, + excerpt: null, + is_favorite: false, + link_role: 'reader', + link_reach: 'restricted', + nb_accesses_ancestors: 14, + nb_accesses_direct: 14, + numchild: 0, + path: '000000l', + title: 'No drop and no drag', + updated_at: '2025-03-14T14:44:16.032774Z', + user_roles: ['reader'], + }, +]; diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-grid.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-grid.spec.ts index 12be84ce..4c5eac3c 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-grid.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-grid.spec.ts @@ -59,6 +59,7 @@ test.describe('Documents Grid mobile', () => { link_reach: 'public', created_at: '2024-10-07T13:02:41.085298Z', updated_at: '2024-10-07T13:30:21.829690Z', + user_roles: ['owner'], }, ], }, @@ -168,6 +169,7 @@ test.describe('Document grid item options', () => { }, link_reach: 'restricted', created_at: '2021-09-01T09:00:00Z', + user_roles: ['editor'], }, ], }, diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts index feaf18a6..b33923fa 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts @@ -70,7 +70,9 @@ test.describe('Doc Header', () => { ).toBeVisible(); await expect( - page.getByText(`Are you sure you want to delete this document ?`), + page.getByText( + `Are you sure you want to delete the document "${randomDoc}"?`, + ), ).toBeVisible(); await page diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/types.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/types.tsx index 29680401..bd8c4849 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/types.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-management/types.tsx @@ -42,10 +42,14 @@ export interface Doc { is_favorite: boolean; link_reach: LinkReach; link_role: LinkRole; - nb_accesses_ancestors: number; - nb_accesses_direct: number; + user_roles: Role[]; created_at: string; updated_at: string; + nb_accesses_direct: number; + nb_accesses_ancestors: number; + children?: Doc[]; + childrenCount?: number; + numchild: number; abilities: { accesses_manage: boolean; accesses_view: boolean; diff --git a/src/frontend/apps/impress/src/features/docs/doc-tree/api/useMove.tsx b/src/frontend/apps/impress/src/features/docs/doc-tree/api/useMove.tsx new file mode 100644 index 00000000..1ba87df4 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-tree/api/useMove.tsx @@ -0,0 +1,36 @@ +import { TreeViewMoveModeEnum } from '@gouvfr-lasuite/ui-kit'; +import { useMutation } from '@tanstack/react-query'; + +import { APIError, errorCauses, fetchAPI } from '@/api'; + +export type MoveDocParam = { + sourceDocumentId: string; + targetDocumentId: string; + position: TreeViewMoveModeEnum; +}; + +export const moveDoc = async ({ + sourceDocumentId, + targetDocumentId, + position, +}: MoveDocParam): Promise => { + const response = await fetchAPI(`documents/${sourceDocumentId}/move/`, { + method: 'POST', + body: JSON.stringify({ + target_document_id: targetDocumentId, + position, + }), + }); + + if (!response.ok) { + throw new APIError('Failed to move the doc', await errorCauses(response)); + } + + return response.json() as Promise; +}; + +export function useMoveDoc() { + return useMutation({ + mutationFn: moveDoc, + }); +} diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocGridContentList.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocGridContentList.tsx new file mode 100644 index 00000000..fb62a421 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocGridContentList.tsx @@ -0,0 +1,170 @@ +import { DndContext, DragOverlay, Modifier } from '@dnd-kit/core'; +import { getEventCoordinates } from '@dnd-kit/utilities'; +import { TreeViewMoveModeEnum } from '@gouvfr-lasuite/ui-kit'; +import { useQueryClient } from '@tanstack/react-query'; +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { Box, Text } from '@/components'; +import { Doc, KEY_LIST_DOC } from '@/docs/doc-management'; +import { useMoveDoc } from '@/docs/doc-tree/api/useMove'; + +import { useDragAndDrop } from '../hooks/useDragAndDrop'; + +import { DocsGridItem } from './DocsGridItem'; +import { Draggable } from './Draggable'; +import { Droppable } from './Droppable'; + +const snapToTopLeft: Modifier = ({ + activatorEvent, + draggingNodeRect, + transform, +}) => { + if (draggingNodeRect && activatorEvent) { + const activatorCoordinates = getEventCoordinates(activatorEvent); + + if (!activatorCoordinates) { + return transform; + } + + const offsetX = activatorCoordinates.x - draggingNodeRect.left; + const offsetY = activatorCoordinates.y - draggingNodeRect.top; + + return { + ...transform, + x: transform.x + offsetX - 3, + y: transform.y + offsetY - 3, + }; + } + + return transform; +}; + +type DocGridContentListProps = { + docs: Doc[]; +}; + +export const DocGridContentList = ({ docs }: DocGridContentListProps) => { + const { mutate: handleMove, isError } = useMoveDoc(); + const queryClient = useQueryClient(); + const onDrag = (sourceDocumentId: string, targetDocumentId: string) => + handleMove( + { + sourceDocumentId, + targetDocumentId, + position: TreeViewMoveModeEnum.FIRST_CHILD, + }, + { + onSuccess: () => { + void queryClient.invalidateQueries({ + queryKey: [KEY_LIST_DOC], + }); + }, + }, + ); + + const { + selectedDoc, + canDrag, + canDrop, + sensors, + handleDragStart, + handleDragEnd, + updateCanDrop, + } = useDragAndDrop(onDrag); + + const { t } = useTranslation(); + + const overlayText = useMemo(() => { + if (!canDrag) { + return t('You must have admin rights to move the document'); + } + if (!canDrop) { + return t('You must be at least the editor of the target document'); + } + + return selectedDoc?.title || t('Unnamed document'); + }, [canDrag, canDrop, selectedDoc, t]); + + const overlayBgColor = useMemo(() => { + if (!canDrag) { + return 'var(--c--theme--colors--danger-600)'; + } + if (canDrop !== undefined && !canDrop) { + return 'var(--c--theme--colors--danger-600)'; + } + if (isError) { + return 'var(--c--theme--colors--danger-600)'; + } + + return '#5858D3'; + }, [canDrag, canDrop, isError]); + + if (docs.length === 0) { + return null; + } + + return ( + + {docs.map((doc) => ( + + ))} + + + + {overlayText} + + + + + ); +}; + +interface DocGridItemProps { + doc: Doc; + dragMode: boolean; + canDrag: boolean; + updateCanDrop: (canDrop: boolean, isOver: boolean) => void; +} + +export const DraggableDocGridItem = ({ + doc, + dragMode, + canDrag, + updateCanDrop, +}: DocGridItemProps) => { + const canDrop = doc.abilities.move; + + return ( + updateCanDrop(canDrop, isOver)} + id={doc.id} + data={doc} + > + + + + + ); +}; diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGrid.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGrid.tsx index 53425210..fc0dba2f 100644 --- a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGrid.tsx +++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGrid.tsx @@ -9,7 +9,7 @@ import { useResponsiveStore } from '@/stores'; import { useResponsiveDocGrid } from '../hooks/useResponsiveDocGrid'; -import { DocsGridItem } from './DocsGridItem'; +import { DocGridContentList } from './DocGridContentList'; import { DocsGridLoader } from './DocsGridLoader'; type DocsGridProps = { @@ -37,6 +37,9 @@ export const DocsGrid = ({ is_creator_me: target === DocDefaultFilter.MY_DOCS, }), }); + + const docs = data?.pages.flatMap((page) => page.results) ?? []; + const loading = isFetching || isLoading; const hasDocs = data?.pages.some((page) => page.results.length > 0); const loadMore = (inView: boolean) => { @@ -115,11 +118,7 @@ export const DocsGrid = ({ )} - {data?.pages.map((currentPage) => { - return currentPage.results.map((doc) => ( - - )); - })} + {hasNextPage && !loading && ( { +export const DocsGridItem = ({ doc, dragMode = false }: DocsGridItemProps) => { const { t } = useTranslation(); const { isDesktop } = useResponsiveStore(); const { flexLeft, flexRight } = useResponsiveDocGrid(); @@ -45,7 +46,9 @@ export const DocsGridItem = ({ doc }: DocsGridItemProps) => { cursor: pointer; border-radius: 4px; &:hover { - background-color: var(--c--theme--colors--greyscale-100); + background-color: ${dragMode + ? 'none' + : 'var(--c--theme--colors--greyscale-100)'}; } `} className="--docs--doc-grid-item" @@ -79,25 +82,35 @@ export const DocsGridItem = ({ doc }: DocsGridItemProps) => { : undefined } > - - {isPublic - ? t('Accessible to anyone') - : t('Accessible to authenticated users')} - - } - placement="top" - > -
- -
-
+ {dragMode && ( + + )} + {!dragMode && ( + + {isPublic + ? t('Accessible to anyone') + : t('Accessible to authenticated users')} + + } + placement="top" + > +
+ +
+
+ )} )} diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/Draggable.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/Draggable.tsx new file mode 100644 index 00000000..bafacf0b --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/Draggable.tsx @@ -0,0 +1,26 @@ +import { Data, useDraggable } from '@dnd-kit/core'; + +type DraggableProps = { + id: string; + data?: Data; + children: React.ReactNode; +}; + +export const Draggable = (props: DraggableProps) => { + const { attributes, listeners, setNodeRef } = useDraggable({ + id: props.id, + data: props.data, + }); + + return ( +
+ {props.children} +
+ ); +}; diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/Droppable.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/Droppable.tsx new file mode 100644 index 00000000..851bf6f6 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/Droppable.tsx @@ -0,0 +1,53 @@ +import { Data, useDroppable } from '@dnd-kit/core'; +import { PropsWithChildren, useEffect } from 'react'; +import { css } from 'styled-components'; + +import { Box } from '@/components'; +import { Doc } from '@/docs/doc-management'; + +type DroppableProps = { + id: string; + onOver?: (isOver: boolean, data?: Data) => void; + data?: Data; + enabledDrop?: boolean; + canDrop?: boolean; +}; + +export const Droppable = ({ + onOver, + canDrop, + data, + children, + id, +}: PropsWithChildren) => { + const { isOver, setNodeRef } = useDroppable({ + id, + data, + }); + + const enableHover = canDrop && isOver; + + useEffect(() => { + onOver?.(isOver, data); + }, [isOver, data, onOver]); + + return ( + + {children} + + ); +}; diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/SimpleDocItem.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/SimpleDocItem.tsx index e0b17350..5fa63bc6 100644 --- a/src/frontend/apps/impress/src/features/docs/docs-grid/components/SimpleDocItem.tsx +++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/SimpleDocItem.tsx @@ -41,6 +41,7 @@ export const SimpleDocItem = ({ $direction="row" $gap={spacingsTokens.sm} $overflow="auto" + $width="100%" className="--docs--simple-doc-item" > void, +) { + const [selectedDoc, setSelectedDoc] = useState(); + const [canDrop, setCanDrop] = useState(); + + const canDrag = selectedDoc?.abilities.move; + + const mouseSensor = useSensor(MouseSensor, { activationConstraint }); + const touchSensor = useSensor(TouchSensor, { activationConstraint }); + const keyboardSensor = useSensor(KeyboardSensor, {}); + const sensors = useSensors(mouseSensor, touchSensor, keyboardSensor); + + const handleDragStart = (e: DragStartEvent) => { + document.body.style.cursor = 'grabbing'; + if (e.active.data.current) { + setSelectedDoc(e.active.data.current as Doc); + } + }; + + const handleDragEnd = (e: DragEndEvent) => { + setSelectedDoc(undefined); + setCanDrop(undefined); + document.body.style.cursor = 'default'; + if (!canDrag || !canDrop) { + return; + } + + const { active, over } = e; + + if (!over?.id || active.id === over?.id) { + return; + } + + onDrag(active.id as string, over.id as string); + }; + + const updateCanDrop = (docCanDrop: boolean, isOver: boolean) => { + if (isOver) { + setCanDrop(docCanDrop); + } + }; + + return { + selectedDoc, + canDrag, + canDrop, + sensors, + handleDragStart, + handleDragEnd, + updateCanDrop, + }; +} diff --git a/src/frontend/apps/impress/src/features/service-worker/plugins/ApiPlugin.ts b/src/frontend/apps/impress/src/features/service-worker/plugins/ApiPlugin.ts index 661f01a1..856585b1 100644 --- a/src/frontend/apps/impress/src/features/service-worker/plugins/ApiPlugin.ts +++ b/src/frontend/apps/impress/src/features/service-worker/plugins/ApiPlugin.ts @@ -175,6 +175,7 @@ export class ApiPlugin implements WorkboxPlugin { is_favorite: false, nb_accesses_direct: 1, nb_accesses_ancestors: 1, + numchild: 0, updated_at: new Date().toISOString(), abilities: { accesses_manage: true, @@ -202,6 +203,7 @@ export class ApiPlugin implements WorkboxPlugin { }, link_reach: LinkReach.RESTRICTED, link_role: LinkRole.READER, + user_roles: [], }; await DocsDB.cacheResponse(