Skip to content

Commit b90a091

Browse files
committed
✨(frontend) Duplicate a doc
We can duplicate a document from the tool options.
1 parent da78cc8 commit b90a091

File tree

8 files changed

+154
-6
lines changed

8 files changed

+154
-6
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to
1111
### Added
1212

1313
- ✨(frontend) add customization for translations #857
14+
- ✨(frontend) Duplicate a doc #1078
1415

1516
### Changed
1617

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

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,7 @@ test.describe('Doc Header', () => {
424424
});
425425

426426
test('it pins a document', async ({ page, browserName }) => {
427-
const [docTitle] = await createDoc(page, `Favorite doc`, browserName);
427+
const [docTitle] = await createDoc(page, `Pin doc`, browserName);
428428

429429
await page.getByLabel('Open the document options').click();
430430

@@ -456,6 +456,37 @@ test.describe('Doc Header', () => {
456456
await expect(row.getByLabel('Pin document icon')).toBeHidden();
457457
await expect(leftPanelFavorites.getByText(docTitle)).toBeHidden();
458458
});
459+
460+
test('it duplicates a document', async ({ page, browserName }) => {
461+
const [docTitle] = await createDoc(page, `Duplicate doc`, browserName);
462+
463+
const editor = page.locator('.ProseMirror');
464+
await editor.click();
465+
await editor.fill('Hello Duplicated World');
466+
467+
await page.reload();
468+
469+
await page.getByLabel('Open the document options').click();
470+
471+
await page.getByRole('menuitem', { name: 'Duplicate' }).click();
472+
await expect(
473+
page.getByText('Document duplicated successfully!'),
474+
).toBeVisible();
475+
476+
await page.goto('/');
477+
478+
const duplicateTitle = 'Copy of ' + docTitle;
479+
480+
const row = await getGridRow(page, duplicateTitle);
481+
482+
await expect(row.getByText(duplicateTitle)).toBeVisible();
483+
484+
await row.getByText(`more_horiz`).click();
485+
await page.getByRole('menuitem', { name: 'Duplicate' }).click();
486+
const duplicateDuplicateTitle = 'Copy of ' + duplicateTitle;
487+
await page.getByText(duplicateDuplicateTitle).click();
488+
await expect(page.getByText('Hello Duplicated World')).toBeVisible();
489+
});
459490
});
460491

461492
test.describe('Documents Header mobile', () => {

src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { Button, useModal } from '@openfun/cunningham-react';
1+
import {
2+
Button,
3+
VariantType,
4+
useModal,
5+
useToastProvider,
6+
} from '@openfun/cunningham-react';
27
import { useQueryClient } from '@tanstack/react-query';
38
import { useEffect, useState } from 'react';
49
import { useTranslation } from 'react-i18next';
@@ -21,6 +26,7 @@ import {
2126
useCopyDocLink,
2227
useCreateFavoriteDoc,
2328
useDeleteFavoriteDoc,
29+
useDuplicateDoc,
2430
} from '@/docs/doc-management';
2531
import { DocShareModal } from '@/docs/doc-share';
2632
import {
@@ -41,6 +47,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
4147
const hasAccesses = doc.nb_accesses_direct > 1 && doc.abilities.accesses_view;
4248
const queryClient = useQueryClient();
4349
const modulesExport = useModuleExport();
50+
const { toast } = useToastProvider();
4451

4552
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
4653

@@ -51,6 +58,18 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
5158

5259
const { isSmallMobile, isDesktop } = useResponsiveStore();
5360
const copyDocLink = useCopyDocLink(doc.id);
61+
const { mutate: duplicateDoc } = useDuplicateDoc({
62+
onSuccess: () => {
63+
toast(t('Document duplicated successfully!'), VariantType.SUCCESS, {
64+
duration: 3000,
65+
});
66+
},
67+
onError: () => {
68+
toast(t('Failed to duplicate the document...'), VariantType.ERROR, {
69+
duration: 3000,
70+
});
71+
},
72+
});
5473
const { isFeatureFlagActivated } = useAnalytics();
5574
const removeFavoriteDoc = useDeleteFavoriteDoc({
5675
listInvalideQueries: [KEY_LIST_DOC, KEY_DOC],
@@ -112,7 +131,6 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
112131
},
113132
show: isDesktop,
114133
},
115-
116134
{
117135
label: t('Copy as {{format}}', { format: 'Markdown' }),
118136
icon: 'content_copy',
@@ -128,6 +146,17 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
128146
},
129147
show: isFeatureFlagActivated('CopyAsHTML'),
130148
},
149+
{
150+
label: t('Duplicate'),
151+
icon: 'call_split',
152+
disabled: !doc.abilities.duplicate,
153+
callback: () => {
154+
duplicateDoc({
155+
docId: doc.id,
156+
with_accesses: true,
157+
});
158+
},
159+
},
131160
{
132161
label: t('Delete document'),
133162
icon: 'delete',
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
export * from './useCreateDoc';
2+
export * from './useCreateFavoriteDoc';
23
export * from './useDeleteFavoriteDoc';
34
export * from './useDoc';
45
export * from './useDocOptions';
56
export * from './useDocs';
6-
export * from './useCreateFavoriteDoc';
7+
export * from './useDuplicateDoc';
78
export * from './useUpdateDoc';
89
export * from './useUpdateDocLink';
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import {
2+
UseMutationOptions,
3+
useMutation,
4+
useQueryClient,
5+
} from '@tanstack/react-query';
6+
7+
import { APIError, errorCauses, fetchAPI } from '@/api';
8+
9+
import { Doc } from '../types';
10+
11+
import { KEY_LIST_DOC } from './useDocs';
12+
13+
interface DuplicateDocPayload {
14+
docId: string;
15+
with_accesses?: boolean;
16+
}
17+
18+
type DuplicateDocResponse = Pick<Doc, 'id'>;
19+
20+
export const duplicateDoc = async ({
21+
docId,
22+
with_accesses,
23+
}: DuplicateDocPayload): Promise<DuplicateDocResponse> => {
24+
const response = await fetchAPI(`documents/${docId}/duplicate/`, {
25+
method: 'POST',
26+
body: JSON.stringify({ with_accesses }),
27+
});
28+
29+
if (!response.ok) {
30+
throw new APIError(
31+
'Failed to duplicate the doc',
32+
await errorCauses(response),
33+
);
34+
}
35+
36+
return response.json() as Promise<DuplicateDocResponse>;
37+
};
38+
39+
type DuplicateDocOptions = UseMutationOptions<
40+
DuplicateDocResponse,
41+
APIError,
42+
DuplicateDocPayload
43+
>;
44+
45+
export function useDuplicateDoc(options: DuplicateDocOptions) {
46+
const queryClient = useQueryClient();
47+
return useMutation<DuplicateDocResponse, APIError, DuplicateDocPayload>({
48+
mutationFn: duplicateDoc,
49+
onSuccess: (data, variables, context) => {
50+
void queryClient.resetQueries({
51+
queryKey: [KEY_LIST_DOC],
52+
});
53+
void options.onSuccess?.(data, variables, context);
54+
},
55+
});
56+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export interface Doc {
5656
children_list: boolean;
5757
collaboration_auth: boolean;
5858
destroy: boolean;
59+
duplicate: boolean;
5960
favorite: boolean;
6061
invite_owner: boolean;
6162
link_configuration: boolean;

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

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { useModal } from '@openfun/cunningham-react';
1+
import {
2+
VariantType,
3+
useModal,
4+
useToastProvider,
5+
} from '@openfun/cunningham-react';
26
import { useTranslation } from 'react-i18next';
37

48
import { DropdownMenu, DropdownMenuOption, Icon } from '@/components';
@@ -8,6 +12,7 @@ import {
812
ModalRemoveDoc,
913
useCreateFavoriteDoc,
1014
useDeleteFavoriteDoc,
15+
useDuplicateDoc,
1116
} from '@/docs/doc-management';
1217

1318
interface DocsGridActionsProps {
@@ -20,8 +25,21 @@ export const DocsGridActions = ({
2025
openShareModal,
2126
}: DocsGridActionsProps) => {
2227
const { t } = useTranslation();
28+
const { toast } = useToastProvider();
2329

2430
const deleteModal = useModal();
31+
const { mutate: duplicateDoc } = useDuplicateDoc({
32+
onSuccess: () => {
33+
toast(t('Document duplicated successfully!'), VariantType.SUCCESS, {
34+
duration: 3000,
35+
});
36+
},
37+
onError: () => {
38+
toast(t('Failed to duplicate the document...'), VariantType.ERROR, {
39+
duration: 3000,
40+
});
41+
},
42+
});
2543

2644
const removeFavoriteDoc = useDeleteFavoriteDoc({
2745
listInvalideQueries: [KEY_LIST_DOC],
@@ -52,7 +70,17 @@ export const DocsGridActions = ({
5270

5371
testId: `docs-grid-actions-share-${doc.id}`,
5472
},
55-
73+
{
74+
label: t('Duplicate'),
75+
icon: 'call_split',
76+
disabled: !doc.abilities.duplicate,
77+
callback: () => {
78+
duplicateDoc({
79+
docId: doc.id,
80+
with_accesses: true,
81+
});
82+
},
83+
},
5684
{
5785
label: t('Remove'),
5886
icon: 'delete',

src/frontend/apps/impress/src/features/service-worker/plugins/ApiPlugin.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ export class ApiPlugin implements WorkboxPlugin {
186186
children_list: true,
187187
collaboration_auth: true,
188188
destroy: true,
189+
duplicate: true,
189190
favorite: true,
190191
invite_owner: true,
191192
link_configuration: true,

0 commit comments

Comments
 (0)