diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-grid-move.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-grid-move.spec.ts
index ba61f4f6..822c1025 100644
--- a/src/frontend/apps/e2e/__tests__/app-impress/doc-grid-move.spec.ts
+++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-grid-move.spec.ts
@@ -3,10 +3,12 @@ import { expect, test } from '@playwright/test';
import {
createDoc,
getGridRow,
+ getOtherBrowserName,
mockedListDocs,
toggleHeaderMenu,
verifyDocName,
} from './utils-common';
+import { addNewMember } from './utils-share';
import { createRootSubPage } from './utils-sub-pages';
test.describe('Doc grid move', () => {
@@ -178,6 +180,15 @@ test.describe('Doc grid move', () => {
await page.goto('/');
const [titleDoc1] = await createDoc(page, 'Draggable doc', browserName, 1);
+
+ const otherBrowserName = getOtherBrowserName(browserName);
+ await page.getByRole('button', { name: 'Share' }).click();
+ await addNewMember(page, 0, 'Administrator', otherBrowserName);
+ await page
+ .getByRole('dialog')
+ .getByRole('button', { name: 'close' })
+ .click();
+
await page.getByRole('button', { name: 'Back to homepage' }).click();
const [titleDoc2] = await createDoc(page, 'Droppable doc', browserName, 1);
@@ -209,6 +220,18 @@ test.describe('Doc grid move', () => {
// Validate the move action
await page.keyboard.press('Enter');
+ await expect(
+ page
+ .getByRole('dialog')
+ .getByText('it will lose its current access rights'),
+ ).toBeVisible();
+
+ await page
+ .getByRole('dialog')
+ .getByRole('button', { name: 'Move', exact: true })
+ .first()
+ .click();
+
await expect(docsGrid.getByText(titleDoc1)).toBeHidden();
await docsGrid
.getByRole('link', { name: `Open document ${titleDoc2}` })
diff --git a/src/frontend/apps/impress/src/components/modal/AlertModal.tsx b/src/frontend/apps/impress/src/components/modal/AlertModal.tsx
index 778c8ff8..1b0f88c8 100644
--- a/src/frontend/apps/impress/src/components/modal/AlertModal.tsx
+++ b/src/frontend/apps/impress/src/components/modal/AlertModal.tsx
@@ -1,5 +1,5 @@
import { Button, Modal, ModalSize } from '@gouvfr-lasuite/cunningham-react';
-import { ReactNode } from 'react';
+import { ReactNode, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Box } from '../Box';
@@ -25,6 +25,22 @@ export const AlertModal = ({
title,
}: AlertModalProps) => {
const { t } = useTranslation();
+
+ /**
+ * TODO:
+ * Remove this effect when Cunningham will have this patch released:
+ * https://github.com/suitenumerique/cunningham/pull/377
+ */
+ useEffect(() => {
+ const timeout = setTimeout(() => {
+ const contents = document.querySelectorAll('.c__modal__content');
+ contents.forEach((content) => {
+ content.setAttribute('tabindex', '-1');
+ });
+ }, 100);
+ return () => clearTimeout(timeout);
+ }, []);
+
return (
}
rightActions={
- <>
+
@@ -59,7 +76,7 @@ export const AlertModal = ({
>
{confirmLabel ?? t('Confirm')}
- >
+
}
>
diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/api/index.ts b/src/frontend/apps/impress/src/features/docs/doc-management/api/index.ts
index 88c4b028..d3f041e0 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-management/api/index.ts
+++ b/src/frontend/apps/impress/src/features/docs/doc-management/api/index.ts
@@ -7,6 +7,7 @@ export * from './useDocOptions';
export * from './useDocs';
export * from './useDocsFavorite';
export * from './useDuplicateDoc';
+export * from './useMoveDoc';
export * from './useRestoreDoc';
export * from './useSubDocs';
export * from './useUpdateDoc';
diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/api/useMoveDoc.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/api/useMoveDoc.tsx
new file mode 100644
index 00000000..dce4e5ed
--- /dev/null
+++ b/src/frontend/apps/impress/src/features/docs/doc-management/api/useMoveDoc.tsx
@@ -0,0 +1,82 @@
+import { TreeViewMoveModeEnum } from '@gouvfr-lasuite/ui-kit';
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+
+import { APIError, errorCauses, fetchAPI } from '@/api';
+import {
+ getDocAccesses,
+ getDocInvitations,
+ useDeleteDocAccess,
+ useDeleteDocInvitation,
+} from '@/docs/doc-share';
+
+import { KEY_LIST_DOC } from './useDocs';
+
+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(deleteAccessOnMove = false) {
+ const queryClient = useQueryClient();
+ const { mutate: handleDeleteInvitation } = useDeleteDocInvitation();
+ const { mutate: handleDeleteAccess } = useDeleteDocAccess();
+
+ return useMutation({
+ mutationFn: moveDoc,
+ async onSuccess(_data, variables, _onMutateResult, _context) {
+ if (!deleteAccessOnMove) {
+ return;
+ }
+
+ void queryClient.invalidateQueries({
+ queryKey: [KEY_LIST_DOC],
+ });
+ const accesses = await getDocAccesses({
+ docId: variables.sourceDocumentId,
+ });
+
+ const invitationsResponse = await getDocInvitations({
+ docId: variables.sourceDocumentId,
+ page: 1,
+ });
+
+ const invitations = invitationsResponse.results;
+
+ await Promise.all([
+ ...invitations.map((invitation) =>
+ handleDeleteInvitation({
+ docId: variables.sourceDocumentId,
+ invitationId: invitation.id,
+ }),
+ ),
+ ...accesses.map((access) =>
+ handleDeleteAccess({
+ docId: variables.sourceDocumentId,
+ accessId: access.id,
+ }),
+ ),
+ ]);
+ },
+ });
+}
diff --git a/src/frontend/apps/impress/src/features/docs/doc-tree/api/index.ts b/src/frontend/apps/impress/src/features/docs/doc-tree/api/index.ts
index 44949aaa..df8c10ef 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-tree/api/index.ts
+++ b/src/frontend/apps/impress/src/features/docs/doc-tree/api/index.ts
@@ -1,3 +1,2 @@
export * from './useDocChildren';
export * from './useDocTree';
-export * from './useMove';
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
deleted file mode 100644
index 1ba87df4..00000000
--- a/src/frontend/apps/impress/src/features/docs/doc-tree/api/useMove.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-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/doc-tree/components/DocTree.tsx b/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocTree.tsx
index 08fe8005..6c8daf05 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocTree.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocTree.tsx
@@ -12,10 +12,14 @@ import { css } from 'styled-components';
import { Box, Overlayer, StyledLink } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
-import { Doc, SimpleDocItem } from '@/docs/doc-management';
+import {
+ Doc,
+ SimpleDocItem,
+ useMoveDoc,
+ useTrans,
+} from '@/docs/doc-management';
import { KEY_DOC_TREE, useDocTree } from '../api/useDocTree';
-import { useMoveDoc } from '../api/useMove';
import { findIndexInTree } from '../utils';
import { DocSubPageItem } from './DocSubPageItem';
@@ -28,6 +32,7 @@ type DocTreeProps = {
export const DocTree = ({ currentDoc }: DocTreeProps) => {
const { spacingsTokens } = useCunninghamTheme();
const { isDesktop } = useResponsive();
+ const { untitledDocument } = useTrans();
const [treeRoot, setTreeRoot] = useState(null);
const treeContext = useTreeContext();
const router = useRouter();
@@ -265,7 +270,7 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
ref={rootItemRef}
data-testid="doc-tree-root-item"
role="treeitem"
- aria-label={`${t('Root document {{title}}', { title: treeContext.root?.title || t('Untitled document') })}`}
+ aria-label={`${t('Root document {{title}}', { title: treeContext.root?.title || untitledDocument })}`}
aria-selected={rootIsSelected}
tabIndex={0}
onFocus={handleRootFocus}
@@ -325,7 +330,7 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
);
router.push(`/docs/${treeContext?.root?.id}`);
}}
- aria-label={`${t('Open root document')}: ${treeContext.root?.title || t('Untitled document')}`}
+ aria-label={`${t('Open root document')}: ${treeContext.root?.title || untitledDocument}`}
tabIndex={-1} // avoid double tabstop
>
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
index ca369147..780d0906 100644
--- 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
@@ -2,19 +2,11 @@ import { DndContext, DragOverlay, Modifier } from '@dnd-kit/core';
import { getEventCoordinates } from '@dnd-kit/utilities';
import { useModal } from '@gouvfr-lasuite/cunningham-react';
import { TreeViewMoveModeEnum } from '@gouvfr-lasuite/ui-kit';
-import { useQueryClient } from '@tanstack/react-query';
import { useEffect, useMemo, useRef, useState } from 'react';
-import { Trans, useTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
-import { AlertModal, Card, Text } from '@/components';
-import { Doc, KEY_LIST_DOC, useTrans } from '@/docs/doc-management';
-import {
- getDocAccesses,
- getDocInvitations,
- useDeleteDocAccess,
- useDeleteDocInvitation,
-} from '@/docs/doc-share';
-import { useMoveDoc } from '@/docs/doc-tree';
+import { Card, Text } from '@/components';
+import { Doc, useMoveDoc, useTrans } from '@/docs/doc-management';
import { useResponsiveStore } from '@/stores/useResponsiveStore';
import { DocDragEndData, useDragAndDrop } from '../hooks/useDragAndDrop';
@@ -22,6 +14,7 @@ import { DocDragEndData, useDragAndDrop } from '../hooks/useDragAndDrop';
import { DocsGridItem } from './DocsGridItem';
import { Draggable } from './Draggable';
import { Droppable } from './Droppable';
+import { ModalConfirmationMoveDoc } from './ModalConfimationMoveDoc';
const snapToTopLeft: Modifier = ({
activatorEvent,
@@ -55,11 +48,8 @@ type DocGridContentListProps = {
export const DraggableDocGridContentList = ({
docs,
}: DocGridContentListProps) => {
- const { mutateAsync: handleMove, isError } = useMoveDoc();
- const queryClient = useQueryClient();
+ const { mutateAsync: handleMove, isError } = useMoveDoc(true);
const modalConfirmation = useModal();
- const { mutate: handleDeleteInvitation } = useDeleteDocInvitation();
- const { mutate: handleDeleteAccess } = useDeleteDocAccess();
const onDragData = useRef(null);
const { untitledDocument } = useTrans();
@@ -82,35 +72,6 @@ export const DraggableDocGridContentList = ({
targetDocumentId,
position: TreeViewMoveModeEnum.FIRST_CHILD,
});
-
- void queryClient.invalidateQueries({
- queryKey: [KEY_LIST_DOC],
- });
- const accesses = await getDocAccesses({
- docId: sourceDocumentId,
- });
-
- const invitationsResponse = await getDocInvitations({
- docId: sourceDocumentId,
- page: 1,
- });
-
- const invitations = invitationsResponse.results;
-
- await Promise.all([
- ...invitations.map((invitation) =>
- handleDeleteInvitation({
- docId: sourceDocumentId,
- invitationId: invitation.id,
- }),
- ),
- ...accesses.map((access) =>
- handleDeleteAccess({
- docId: sourceDocumentId,
- accessId: access.id,
- }),
- ),
- ]);
} finally {
onDragData.current = null;
}
@@ -207,25 +168,13 @@ export const DraggableDocGridContentList = ({
{modalConfirmation.isOpen && (
-
- }}
- />
-
+ {
- void handleMoveDoc();
- }}
/>
)}
>
diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocMoveModal.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocMoveModal.tsx
index c05e2375..012c6880 100644
--- a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocMoveModal.tsx
+++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocMoveModal.tsx
@@ -1,4 +1,9 @@
-import { Button, Modal, ModalSize } from '@gouvfr-lasuite/cunningham-react';
+import {
+ Button,
+ Modal,
+ ModalSize,
+ useModal,
+} from '@gouvfr-lasuite/cunningham-react';
import { TreeViewMoveModeEnum } from '@gouvfr-lasuite/ui-kit';
import Image from 'next/image';
import { useState } from 'react';
@@ -14,6 +19,7 @@ import EmptySearchIcon from '@/docs/doc-search/assets/illustration-docs-empty.pn
import { useResponsiveStore } from '@/stores';
import { DocsGridItemDate, DocsGridItemTitle } from './DocsGridItem';
+import { ModalConfirmationMoveDoc } from './ModalConfimationMoveDoc';
export const DocMoveModalStyle = createGlobalStyle`
.c__modal--full .c__modal__scroller {
@@ -67,6 +73,8 @@ export const DocMoveModal = ({
const [docSelected, setDocSelected] = useState();
const { untitledDocument } = useTrans();
const docTitle = doc.title || untitledDocument;
+ const docTargetTitle = docSelected?.title || untitledDocument;
+ const modalConfirmation = useModal();
const { mutate: moveDoc } = useMoveDoc(true);
const [search, setSearch] = useState('');
const { isDesktop } = useResponsiveStore();
@@ -77,6 +85,7 @@ export const DocMoveModal = ({
};
const handleMoveDoc = () => {
+ modalConfirmation.onClose();
if (!docSelected?.id) {
return;
}
@@ -105,6 +114,11 @@ export const DocMoveModal = ({
variant="primary"
fullWidth
onClick={() => {
+ if (doc.nb_accesses_direct > 1) {
+ modalConfirmation.open();
+ return;
+ }
+
handleMoveDoc();
}}
disabled={!docSelected}
@@ -255,6 +269,14 @@ export const DocMoveModal = ({
+ {modalConfirmation.isOpen && (
+
+ )}
>
);
};
diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/ModalConfimationMoveDoc.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/ModalConfimationMoveDoc.tsx
new file mode 100644
index 00000000..aaf310c0
--- /dev/null
+++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/ModalConfimationMoveDoc.tsx
@@ -0,0 +1,41 @@
+import { Trans, useTranslation } from 'react-i18next';
+
+import { AlertModal, Text } from '@/components';
+
+interface ModalConfirmationMoveDocProps {
+ targetDocumentTitle: string;
+ onConfirm: () => void;
+ onClose: () => void;
+ isOpen: boolean;
+}
+
+export const ModalConfirmationMoveDoc = ({
+ targetDocumentTitle,
+ onClose,
+ onConfirm,
+ isOpen,
+}: ModalConfirmationMoveDocProps) => {
+ const { t } = useTranslation();
+
+ return (
+
+ }}
+ />
+
+ }
+ confirmLabel={t('Move')}
+ onConfirm={onConfirm}
+ />
+ );
+};