Skip to content

Commit cb2ecfc

Browse files
PanchoutNathanAntoLC
authored andcommitted
✨(frontend) Added drag-and-drop functionality for document management
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.
1 parent 13696ff commit cb2ecfc

File tree

14 files changed

+736
-30
lines changed

14 files changed

+736
-30
lines changed

src/frontend/apps/e2e/__tests__/app-impress/common.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ export const mockedDocument = async (page: Page, json: object) => {
239239
},
240240
link_reach: 'restricted',
241241
created_at: '2021-09-01T09:00:00Z',
242+
user_roles: ['owner'],
242243
...json,
243244
},
244245
});
@@ -248,6 +249,22 @@ export const mockedDocument = async (page: Page, json: object) => {
248249
});
249250
};
250251

252+
export const mockedListDocs = async (page: Page, data: object[] = []) => {
253+
await page.route('**/documents/**/', async (route) => {
254+
const request = route.request();
255+
if (request.method().includes('GET') && request.url().includes('page=')) {
256+
await route.fulfill({
257+
json: {
258+
count: data.length,
259+
next: null,
260+
previous: null,
261+
results: data,
262+
},
263+
});
264+
}
265+
});
266+
};
267+
251268
export const mockedInvitations = async (page: Page, json?: object) => {
252269
await page.route('**/invitations/**/', async (route) => {
253270
const request = route.request();
Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
import { expect, test } from '@playwright/test';
2+
3+
import { createDoc, mockedListDocs } from './common';
4+
5+
test.describe('Doc grid dnd', () => {
6+
test('it creates a doc', async ({ page, browserName }) => {
7+
await page.goto('/');
8+
const header = page.locator('header').first();
9+
await createDoc(page, 'Draggable doc', browserName, 1);
10+
await header.locator('h2').getByText('Docs').click();
11+
await createDoc(page, 'Droppable doc', browserName, 1);
12+
await header.locator('h2').getByText('Docs').click();
13+
14+
const response = await page.waitForResponse(
15+
(response) =>
16+
response.url().endsWith('documents/?page=1') &&
17+
response.status() === 200,
18+
);
19+
const responseJson = await response.json();
20+
21+
const items = responseJson.results;
22+
23+
const docsGrid = page.getByTestId('docs-grid');
24+
await expect(docsGrid).toBeVisible();
25+
await expect(page.getByTestId('grid-loader')).toBeHidden();
26+
const draggableElement = page.getByTestId(`draggable-doc-${items[1].id}`);
27+
const dropZone = page.getByTestId(`droppable-doc-${items[0].id}`);
28+
await expect(draggableElement).toBeVisible();
29+
await expect(dropZone).toBeVisible();
30+
31+
// Obtenir les positions des éléments
32+
const draggableBoundingBox = await draggableElement.boundingBox();
33+
const dropZoneBoundingBox = await dropZone.boundingBox();
34+
35+
expect(draggableBoundingBox).toBeDefined();
36+
expect(dropZoneBoundingBox).toBeDefined();
37+
38+
// eslint-disable-next-line playwright/no-conditional-in-test
39+
if (!draggableBoundingBox || !dropZoneBoundingBox) {
40+
throw new Error('Impossible de déterminer la position des éléments');
41+
}
42+
43+
await page.mouse.move(
44+
draggableBoundingBox.x + draggableBoundingBox.width / 2,
45+
draggableBoundingBox.y + draggableBoundingBox.height / 2,
46+
);
47+
await page.mouse.down();
48+
49+
// Déplacer vers la zone cible
50+
await page.mouse.move(
51+
dropZoneBoundingBox.x + dropZoneBoundingBox.width / 2,
52+
dropZoneBoundingBox.y + dropZoneBoundingBox.height / 2,
53+
{ steps: 10 }, // Rendre le mouvement plus fluide
54+
);
55+
56+
const dragOverlay = page.getByTestId('drag-doc-overlay');
57+
58+
await expect(dragOverlay).toBeVisible();
59+
await expect(dragOverlay).toHaveText(items[1].title as string);
60+
await page.mouse.up();
61+
62+
await expect(dragOverlay).toBeHidden();
63+
});
64+
65+
test('it checks cant drop when we have not the minimum role', async ({
66+
page,
67+
}) => {
68+
await mockedListDocs(page, data);
69+
await page.goto('/');
70+
const docsGrid = page.getByTestId('docs-grid');
71+
await expect(docsGrid).toBeVisible();
72+
await expect(page.getByTestId('grid-loader')).toBeHidden();
73+
74+
const canDropAndDrag = page.getByTestId('droppable-doc-can-drop-and-drag');
75+
76+
const noDropAndNoDrag = page.getByTestId(
77+
'droppable-doc-no-drop-and-no-drag',
78+
);
79+
80+
await expect(canDropAndDrag).toBeVisible();
81+
82+
await expect(noDropAndNoDrag).toBeVisible();
83+
84+
const canDropAndDragBoundigBox = await canDropAndDrag.boundingBox();
85+
86+
const noDropAndNoDragBoundigBox = await noDropAndNoDrag.boundingBox();
87+
88+
// eslint-disable-next-line playwright/no-conditional-in-test
89+
if (!canDropAndDragBoundigBox || !noDropAndNoDragBoundigBox) {
90+
throw new Error('Impossible de déterminer la position des éléments');
91+
}
92+
93+
await page.mouse.move(
94+
canDropAndDragBoundigBox.x + canDropAndDragBoundigBox.width / 2,
95+
canDropAndDragBoundigBox.y + canDropAndDragBoundigBox.height / 2,
96+
);
97+
98+
await page.mouse.down();
99+
100+
await page.mouse.move(
101+
noDropAndNoDragBoundigBox.x + noDropAndNoDragBoundigBox.width / 2,
102+
noDropAndNoDragBoundigBox.y + noDropAndNoDragBoundigBox.height / 2,
103+
{ steps: 10 },
104+
);
105+
106+
const dragOverlay = page.getByTestId('drag-doc-overlay');
107+
108+
await expect(dragOverlay).toBeVisible();
109+
await expect(dragOverlay).toHaveText(
110+
'You must be at least the editor of the target document',
111+
);
112+
113+
await page.mouse.up();
114+
});
115+
116+
test('it checks cant drag when we have not the minimum role', async ({
117+
page,
118+
}) => {
119+
await mockedListDocs(page, data);
120+
await page.goto('/');
121+
const docsGrid = page.getByTestId('docs-grid');
122+
await expect(docsGrid).toBeVisible();
123+
await expect(page.getByTestId('grid-loader')).toBeHidden();
124+
125+
const canDropAndDrag = page.getByTestId('droppable-doc-can-drop-and-drag');
126+
127+
const noDropAndNoDrag = page.getByTestId(
128+
'droppable-doc-no-drop-and-no-drag',
129+
);
130+
131+
await expect(canDropAndDrag).toBeVisible();
132+
133+
await expect(noDropAndNoDrag).toBeVisible();
134+
135+
const canDropAndDragBoundigBox = await canDropAndDrag.boundingBox();
136+
137+
const noDropAndNoDragBoundigBox = await noDropAndNoDrag.boundingBox();
138+
139+
// eslint-disable-next-line playwright/no-conditional-in-test
140+
if (!canDropAndDragBoundigBox || !noDropAndNoDragBoundigBox) {
141+
throw new Error('Impossible de déterminer la position des éléments');
142+
}
143+
144+
await page.mouse.move(
145+
noDropAndNoDragBoundigBox.x + noDropAndNoDragBoundigBox.width / 2,
146+
noDropAndNoDragBoundigBox.y + noDropAndNoDragBoundigBox.height / 2,
147+
);
148+
149+
await page.mouse.down();
150+
151+
await page.mouse.move(
152+
canDropAndDragBoundigBox.x + canDropAndDragBoundigBox.width / 2,
153+
canDropAndDragBoundigBox.y + canDropAndDragBoundigBox.height / 2,
154+
{ steps: 10 },
155+
);
156+
157+
const dragOverlay = page.getByTestId('drag-doc-overlay');
158+
159+
await expect(dragOverlay).toBeVisible();
160+
await expect(dragOverlay).toHaveText(
161+
'You must have admin rights to move the document',
162+
);
163+
164+
await page.mouse.up();
165+
});
166+
});
167+
168+
const data = [
169+
{
170+
id: 'can-drop-and-drag',
171+
abilities: {
172+
accesses_manage: true,
173+
accesses_view: true,
174+
ai_transform: true,
175+
ai_translate: true,
176+
attachment_upload: true,
177+
children_list: true,
178+
children_create: true,
179+
collaboration_auth: true,
180+
descendants: true,
181+
destroy: true,
182+
favorite: true,
183+
link_configuration: true,
184+
invite_owner: true,
185+
move: true,
186+
partial_update: true,
187+
restore: true,
188+
retrieve: true,
189+
media_auth: true,
190+
link_select_options: {
191+
restricted: ['reader', 'editor'],
192+
authenticated: ['reader', 'editor'],
193+
public: ['reader', 'editor'],
194+
},
195+
tree: true,
196+
update: true,
197+
versions_destroy: true,
198+
versions_list: true,
199+
versions_retrieve: true,
200+
},
201+
created_at: '2025-03-14T14:45:22.527221Z',
202+
creator: 'bc6895e0-8f6d-4b00-827d-c143aa6b2ecb',
203+
depth: 1,
204+
excerpt: null,
205+
is_favorite: false,
206+
link_role: 'reader',
207+
link_reach: 'restricted',
208+
nb_accesses_ancestors: 1,
209+
nb_accesses_direct: 1,
210+
numchild: 5,
211+
path: '000000o',
212+
title: 'Can drop and drag',
213+
updated_at: '2025-03-14T14:45:27.699542Z',
214+
user_roles: ['owner'],
215+
},
216+
{
217+
id: 'can-only-drop',
218+
title: 'Can only drop',
219+
abilities: {
220+
accesses_manage: true,
221+
accesses_view: true,
222+
ai_transform: true,
223+
ai_translate: true,
224+
attachment_upload: true,
225+
children_list: true,
226+
children_create: true,
227+
collaboration_auth: true,
228+
descendants: true,
229+
destroy: true,
230+
favorite: true,
231+
link_configuration: true,
232+
invite_owner: true,
233+
move: true,
234+
partial_update: true,
235+
restore: true,
236+
retrieve: true,
237+
media_auth: true,
238+
link_select_options: {
239+
restricted: ['reader', 'editor'],
240+
authenticated: ['reader', 'editor'],
241+
public: ['reader', 'editor'],
242+
},
243+
tree: true,
244+
update: true,
245+
versions_destroy: true,
246+
versions_list: true,
247+
versions_retrieve: true,
248+
},
249+
created_at: '2025-03-14T14:45:22.527221Z',
250+
creator: 'bc6895e0-8f6d-4b00-827d-c143aa6b2ecb',
251+
depth: 1,
252+
excerpt: null,
253+
is_favorite: false,
254+
link_role: 'reader',
255+
link_reach: 'restricted',
256+
nb_accesses_ancestors: 1,
257+
nb_accesses_direct: 1,
258+
numchild: 5,
259+
path: '000000o',
260+
261+
updated_at: '2025-03-14T14:45:27.699542Z',
262+
user_roles: ['editor'],
263+
},
264+
{
265+
id: 'no-drop-and-no-drag',
266+
abilities: {
267+
accesses_manage: false,
268+
accesses_view: true,
269+
ai_transform: false,
270+
ai_translate: false,
271+
attachment_upload: false,
272+
children_list: true,
273+
children_create: false,
274+
collaboration_auth: true,
275+
descendants: true,
276+
destroy: false,
277+
favorite: true,
278+
link_configuration: false,
279+
invite_owner: false,
280+
move: false,
281+
partial_update: false,
282+
restore: false,
283+
retrieve: true,
284+
media_auth: true,
285+
link_select_options: {
286+
restricted: ['reader', 'editor'],
287+
authenticated: ['reader', 'editor'],
288+
public: ['reader', 'editor'],
289+
},
290+
tree: true,
291+
update: false,
292+
versions_destroy: false,
293+
versions_list: true,
294+
versions_retrieve: true,
295+
},
296+
created_at: '2025-03-14T14:44:16.032773Z',
297+
creator: '9264f420-f018-4bd6-96ae-4788f41af56d',
298+
depth: 1,
299+
excerpt: null,
300+
is_favorite: false,
301+
link_role: 'reader',
302+
link_reach: 'restricted',
303+
nb_accesses_ancestors: 14,
304+
nb_accesses_direct: 14,
305+
numchild: 0,
306+
path: '000000l',
307+
title: 'No drop and no drag',
308+
updated_at: '2025-03-14T14:44:16.032774Z',
309+
user_roles: ['reader'],
310+
},
311+
];

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ test.describe('Documents Grid mobile', () => {
5959
link_reach: 'public',
6060
created_at: '2024-10-07T13:02:41.085298Z',
6161
updated_at: '2024-10-07T13:30:21.829690Z',
62+
user_roles: ['owner'],
6263
},
6364
],
6465
},
@@ -168,6 +169,7 @@ test.describe('Document grid item options', () => {
168169
},
169170
link_reach: 'restricted',
170171
created_at: '2021-09-01T09:00:00Z',
172+
user_roles: ['editor'],
171173
},
172174
],
173175
},

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@ test.describe('Doc Header', () => {
7070
).toBeVisible();
7171

7272
await expect(
73-
page.getByText(`Are you sure you want to delete this document ?`),
73+
page.getByText(
74+
`Are you sure you want to delete the document "${randomDoc}"?`,
75+
),
7476
).toBeVisible();
7577

7678
await page

src/frontend/apps/impress/src/features/docs/doc-management/types.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,14 @@ export interface Doc {
4242
is_favorite: boolean;
4343
link_reach: LinkReach;
4444
link_role: LinkRole;
45-
nb_accesses_ancestors: number;
46-
nb_accesses_direct: number;
45+
user_roles: Role[];
4746
created_at: string;
4847
updated_at: string;
48+
nb_accesses_direct: number;
49+
nb_accesses_ancestors: number;
50+
children?: Doc[];
51+
childrenCount?: number;
52+
numchild: number;
4953
abilities: {
5054
accesses_manage: boolean;
5155
accesses_view: boolean;

0 commit comments

Comments
 (0)