Skip to content

Commit b8b9b8d

Browse files
committed
🛂(frontend) add confirmation modal on move modal
If the document has more than 1 direct access,we want to display a confirmation modal before moving the document. This is to prevent users from accidentally moving a document that is shared with multiple people. The accesses and invitations will be removed from the document.
1 parent f79b18a commit b8b9b8d

File tree

10 files changed

+199
-114
lines changed

10 files changed

+199
-114
lines changed

src/frontend/apps/e2e/__tests__/app-impress/doc-grid-move.spec.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import { expect, test } from '@playwright/test';
33
import {
44
createDoc,
55
getGridRow,
6+
getOtherBrowserName,
67
mockedListDocs,
78
toggleHeaderMenu,
89
verifyDocName,
910
} from './utils-common';
11+
import { addNewMember } from './utils-share';
1012
import { createRootSubPage } from './utils-sub-pages';
1113

1214
test.describe('Doc grid move', () => {
@@ -178,6 +180,15 @@ test.describe('Doc grid move', () => {
178180
await page.goto('/');
179181

180182
const [titleDoc1] = await createDoc(page, 'Draggable doc', browserName, 1);
183+
184+
const otherBrowserName = getOtherBrowserName(browserName);
185+
await page.getByRole('button', { name: 'Share' }).click();
186+
await addNewMember(page, 0, 'Administrator', otherBrowserName);
187+
await page
188+
.getByRole('dialog')
189+
.getByRole('button', { name: 'close' })
190+
.click();
191+
181192
await page.getByRole('button', { name: 'Back to homepage' }).click();
182193

183194
const [titleDoc2] = await createDoc(page, 'Droppable doc', browserName, 1);
@@ -209,6 +220,18 @@ test.describe('Doc grid move', () => {
209220
// Validate the move action
210221
await page.keyboard.press('Enter');
211222

223+
await expect(
224+
page
225+
.getByRole('dialog')
226+
.getByText('it will lose its current access rights'),
227+
).toBeVisible();
228+
229+
await page
230+
.getByRole('dialog')
231+
.getByRole('button', { name: 'Move', exact: true })
232+
.first()
233+
.click();
234+
212235
await expect(docsGrid.getByText(titleDoc1)).toBeHidden();
213236
await docsGrid
214237
.getByRole('link', { name: `Open document ${titleDoc2}` })

src/frontend/apps/impress/src/components/modal/AlertModal.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,14 @@ export const AlertModal = ({
4343
</Text>
4444
}
4545
rightActions={
46-
<>
46+
<Box $direction="row-reverse" $gap="small">
47+
<Button
48+
aria-label={confirmLabel ?? t('Confirm')}
49+
color="error"
50+
onClick={onConfirm}
51+
>
52+
{confirmLabel ?? t('Confirm')}
53+
</Button>
4754
<Button
4855
aria-label={`${t('Cancel')} - ${title}`}
4956
variant="secondary"
@@ -52,14 +59,7 @@ export const AlertModal = ({
5259
>
5360
{cancelLabel ?? t('Cancel')}
5461
</Button>
55-
<Button
56-
aria-label={confirmLabel ?? t('Confirm')}
57-
color="error"
58-
onClick={onConfirm}
59-
>
60-
{confirmLabel ?? t('Confirm')}
61-
</Button>
62-
</>
62+
</Box>
6363
}
6464
>
6565
<Box className="--docs--alert-modal">

src/frontend/apps/impress/src/features/docs/doc-management/api/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export * from './useDocOptions';
77
export * from './useDocs';
88
export * from './useDocsFavorite';
99
export * from './useDuplicateDoc';
10+
export * from './useMoveDoc';
1011
export * from './useRestoreDoc';
1112
export * from './useSubDocs';
1213
export * from './useUpdateDoc';
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { TreeViewMoveModeEnum } from '@gouvfr-lasuite/ui-kit';
2+
import { useMutation, useQueryClient } from '@tanstack/react-query';
3+
4+
import { APIError, errorCauses, fetchAPI } from '@/api';
5+
import {
6+
getDocAccesses,
7+
getDocInvitations,
8+
useDeleteDocAccess,
9+
useDeleteDocInvitation,
10+
} from '@/docs/doc-share';
11+
12+
import { KEY_LIST_DOC } from './useDocs';
13+
14+
export type MoveDocParam = {
15+
sourceDocumentId: string;
16+
targetDocumentId: string;
17+
position: TreeViewMoveModeEnum;
18+
};
19+
20+
export const moveDoc = async ({
21+
sourceDocumentId,
22+
targetDocumentId,
23+
position,
24+
}: MoveDocParam): Promise<void> => {
25+
const response = await fetchAPI(`documents/${sourceDocumentId}/move/`, {
26+
method: 'POST',
27+
body: JSON.stringify({
28+
target_document_id: targetDocumentId,
29+
position,
30+
}),
31+
});
32+
33+
if (!response.ok) {
34+
throw new APIError('Failed to move the doc', await errorCauses(response));
35+
}
36+
37+
return response.json() as Promise<void>;
38+
};
39+
40+
export function useMoveDoc(deleteAccessOnMove = false) {
41+
const queryClient = useQueryClient();
42+
const { mutate: handleDeleteInvitation } = useDeleteDocInvitation();
43+
const { mutate: handleDeleteAccess } = useDeleteDocAccess();
44+
45+
return useMutation<void, APIError, MoveDocParam>({
46+
mutationFn: moveDoc,
47+
async onSuccess(_data, variables, _onMutateResult, _context) {
48+
if (!deleteAccessOnMove) {
49+
return;
50+
}
51+
52+
void queryClient.invalidateQueries({
53+
queryKey: [KEY_LIST_DOC],
54+
});
55+
const accesses = await getDocAccesses({
56+
docId: variables.sourceDocumentId,
57+
});
58+
59+
const invitationsResponse = await getDocInvitations({
60+
docId: variables.sourceDocumentId,
61+
page: 1,
62+
});
63+
64+
const invitations = invitationsResponse.results;
65+
66+
await Promise.all([
67+
...invitations.map((invitation) =>
68+
handleDeleteInvitation({
69+
docId: variables.sourceDocumentId,
70+
invitationId: invitation.id,
71+
}),
72+
),
73+
...accesses.map((access) =>
74+
handleDeleteAccess({
75+
docId: variables.sourceDocumentId,
76+
accessId: access.id,
77+
}),
78+
),
79+
]);
80+
},
81+
});
82+
}
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
11
export * from './useDocChildren';
22
export * from './useDocTree';
3-
export * from './useMove';

src/frontend/apps/impress/src/features/docs/doc-tree/api/useMove.tsx

Lines changed: 0 additions & 36 deletions
This file was deleted.

src/frontend/apps/impress/src/features/docs/doc-tree/components/DocTree.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,14 @@ import { css } from 'styled-components';
1212

1313
import { Box, Overlayer, StyledLink } from '@/components';
1414
import { useCunninghamTheme } from '@/cunningham';
15-
import { Doc, SimpleDocItem } from '@/docs/doc-management';
15+
import {
16+
Doc,
17+
SimpleDocItem,
18+
useMoveDoc,
19+
useTrans,
20+
} from '@/docs/doc-management';
1621

1722
import { KEY_DOC_TREE, useDocTree } from '../api/useDocTree';
18-
import { useMoveDoc } from '../api/useMove';
1923
import { findIndexInTree } from '../utils';
2024

2125
import { DocSubPageItem } from './DocSubPageItem';
@@ -28,6 +32,7 @@ type DocTreeProps = {
2832
export const DocTree = ({ currentDoc }: DocTreeProps) => {
2933
const { spacingsTokens } = useCunninghamTheme();
3034
const { isDesktop } = useResponsive();
35+
const { untitledDocument } = useTrans();
3136
const [treeRoot, setTreeRoot] = useState<HTMLElement | null>(null);
3237
const treeContext = useTreeContext<Doc | null>();
3338
const router = useRouter();
@@ -265,7 +270,7 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
265270
ref={rootItemRef}
266271
data-testid="doc-tree-root-item"
267272
role="treeitem"
268-
aria-label={`${t('Root document {{title}}', { title: treeContext.root?.title || t('Untitled document') })}`}
273+
aria-label={`${t('Root document {{title}}', { title: treeContext.root?.title || untitledDocument })}`}
269274
aria-selected={rootIsSelected}
270275
tabIndex={0}
271276
onFocus={handleRootFocus}
@@ -325,7 +330,7 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
325330
);
326331
router.push(`/docs/${treeContext?.root?.id}`);
327332
}}
328-
aria-label={`${t('Open root document')}: ${treeContext.root?.title || t('Untitled document')}`}
333+
aria-label={`${t('Open root document')}: ${treeContext.root?.title || untitledDocument}`}
329334
tabIndex={-1} // avoid double tabstop
330335
>
331336
<Box $direction="row" $align="center" $width="100%">

src/frontend/apps/impress/src/features/docs/docs-grid/components/DocGridContentList.tsx

Lines changed: 12 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,19 @@ import { DndContext, DragOverlay, Modifier } from '@dnd-kit/core';
22
import { getEventCoordinates } from '@dnd-kit/utilities';
33
import { useModal } from '@gouvfr-lasuite/cunningham-react';
44
import { TreeViewMoveModeEnum } from '@gouvfr-lasuite/ui-kit';
5-
import { useQueryClient } from '@tanstack/react-query';
65
import { useEffect, useMemo, useRef, useState } from 'react';
7-
import { Trans, useTranslation } from 'react-i18next';
8-
9-
import { AlertModal, Card, Text } from '@/components';
10-
import { Doc, KEY_LIST_DOC, useTrans } from '@/docs/doc-management';
11-
import {
12-
getDocAccesses,
13-
getDocInvitations,
14-
useDeleteDocAccess,
15-
useDeleteDocInvitation,
16-
} from '@/docs/doc-share';
17-
import { useMoveDoc } from '@/docs/doc-tree';
6+
import { useTranslation } from 'react-i18next';
7+
8+
import { Card, Text } from '@/components';
9+
import { Doc, useMoveDoc, useTrans } from '@/docs/doc-management';
1810
import { useResponsiveStore } from '@/stores/useResponsiveStore';
1911

2012
import { DocDragEndData, useDragAndDrop } from '../hooks/useDragAndDrop';
2113

2214
import { DocsGridItem } from './DocsGridItem';
2315
import { Draggable } from './Draggable';
2416
import { Droppable } from './Droppable';
17+
import { ModalConfirmationMoveDoc } from './ModalConfimationMoveDoc';
2518

2619
const snapToTopLeft: Modifier = ({
2720
activatorEvent,
@@ -55,11 +48,8 @@ type DocGridContentListProps = {
5548
export const DraggableDocGridContentList = ({
5649
docs,
5750
}: DocGridContentListProps) => {
58-
const { mutateAsync: handleMove, isError } = useMoveDoc();
59-
const queryClient = useQueryClient();
51+
const { mutateAsync: handleMove, isError } = useMoveDoc(true);
6052
const modalConfirmation = useModal();
61-
const { mutate: handleDeleteInvitation } = useDeleteDocInvitation();
62-
const { mutate: handleDeleteAccess } = useDeleteDocAccess();
6353
const onDragData = useRef<DocDragEndData | null>(null);
6454
const { untitledDocument } = useTrans();
6555

@@ -82,35 +72,6 @@ export const DraggableDocGridContentList = ({
8272
targetDocumentId,
8373
position: TreeViewMoveModeEnum.FIRST_CHILD,
8474
});
85-
86-
void queryClient.invalidateQueries({
87-
queryKey: [KEY_LIST_DOC],
88-
});
89-
const accesses = await getDocAccesses({
90-
docId: sourceDocumentId,
91-
});
92-
93-
const invitationsResponse = await getDocInvitations({
94-
docId: sourceDocumentId,
95-
page: 1,
96-
});
97-
98-
const invitations = invitationsResponse.results;
99-
100-
await Promise.all([
101-
...invitations.map((invitation) =>
102-
handleDeleteInvitation({
103-
docId: sourceDocumentId,
104-
invitationId: invitation.id,
105-
}),
106-
),
107-
...accesses.map((access) =>
108-
handleDeleteAccess({
109-
docId: sourceDocumentId,
110-
accessId: access.id,
111-
}),
112-
),
113-
]);
11475
} finally {
11576
onDragData.current = null;
11677
}
@@ -207,25 +168,13 @@ export const DraggableDocGridContentList = ({
207168
</DragOverlay>
208169
</DndContext>
209170
{modalConfirmation.isOpen && (
210-
<AlertModal
211-
{...modalConfirmation}
212-
title={t('Move document')}
213-
description={
214-
<Text $display="inline">
215-
<Trans
216-
i18nKey="By moving this document to <strong>{{targetDocumentTitle}}</strong>, it will lose its current access rights and inherit the permissions of that document. <strong>This access change cannot be undone.</strong>"
217-
values={{
218-
targetDocumentTitle:
219-
onDragData.current?.target.title ?? untitledDocument,
220-
}}
221-
components={{ strong: <strong /> }}
222-
/>
223-
</Text>
171+
<ModalConfirmationMoveDoc
172+
isOpen={modalConfirmation.isOpen}
173+
onClose={modalConfirmation.onClose}
174+
onConfirm={handleMoveDoc}
175+
targetDocumentTitle={
176+
onDragData.current?.target.title || untitledDocument
224177
}
225-
confirmLabel={t('Move')}
226-
onConfirm={() => {
227-
void handleMoveDoc();
228-
}}
229178
/>
230179
)}
231180
</>

0 commit comments

Comments
 (0)