diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3594108d2e..8d16acc8a4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,8 +8,13 @@ and this project adheres to
## [Unreleased]
+### Added
+
+- ✨(frontend) add duplicate action to doc tree #1175
+
### Changed
+- ♻️(frontend) redirect to doc after duplicate #1175
- 🔧(project) change env.d system by using local files #1200
### Fixed
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 d0997ec850..ed7486cde1 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
@@ -442,9 +442,10 @@ test.describe('Doc Header', () => {
page.getByText('Document duplicated successfully!'),
).toBeVisible();
- await page.goto('/');
-
const duplicateTitle = 'Copy of ' + docTitle;
+ await verifyDocName(page, duplicateTitle);
+
+ await page.goto('/');
const row = await getGridRow(page, duplicateTitle);
@@ -470,16 +471,24 @@ test.describe('Doc Header', () => {
await editor.click();
await editor.fill('Hello Duplicated World');
- await page.getByLabel('Open the document options').click();
+ const duplicateTitle = 'Copy of ' + childTitle;
+ const docTree = page.getByTestId('doc-tree');
+
+ const child = docTree
+ .getByRole('treeitem')
+ .locator('.--docs-sub-page-item')
+ .filter({
+ hasText: childTitle,
+ });
+ await child.hover();
+ await child.getByText(`more_horiz`).click();
await page.getByRole('menuitem', { name: 'Duplicate' }).click();
- await expect(
- page.getByText('Document duplicated successfully!'),
- ).toBeVisible();
- const duplicateDuplicateTitle = 'Copy of ' + childTitle;
+ await verifyDocName(page, duplicateTitle);
+
await expect(
- page.getByTestId('doc-tree').getByText(duplicateDuplicateTitle),
+ page.getByTestId('doc-tree').getByText(duplicateTitle),
).toBeVisible();
});
});
diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocToolBox.spec.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocToolBox.spec.tsx
index 35faa9a837..11856db563 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocToolBox.spec.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocToolBox.spec.tsx
@@ -32,6 +32,13 @@ jest.mock('@/features/docs/doc-export/', () => ({
ModalExport: () => ModalExport,
}));
+jest.mock('next/router', () => ({
+ ...jest.requireActual('next/router'),
+ useRouter: () => ({
+ push: jest.fn(),
+ }),
+}));
+
const doc = {
nb_accesses: 1,
abilities: {
diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx
index cc41286389..b84321c17d 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx
@@ -1,11 +1,7 @@
import { useTreeContext } from '@gouvfr-lasuite/ui-kit';
-import {
- Button,
- VariantType,
- useModal,
- useToastProvider,
-} from '@openfun/cunningham-react';
+import { Button, useModal } from '@openfun/cunningham-react';
import { useQueryClient } from '@tanstack/react-query';
+import { useRouter } from 'next/router';
import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
@@ -62,7 +58,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
}, [doc, treeContext?.root]);
const queryClient = useQueryClient();
- const { toast } = useToastProvider();
+ const router = useRouter();
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
@@ -74,15 +70,8 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
const { isSmallMobile, isDesktop } = useResponsiveStore();
const copyDocLink = useCopyDocLink(doc.id);
const { mutate: duplicateDoc } = useDuplicateDoc({
- onSuccess: () => {
- toast(t('Document duplicated successfully!'), VariantType.SUCCESS, {
- duration: 3000,
- });
- },
- onError: () => {
- toast(t('Failed to duplicate the document...'), VariantType.ERROR, {
- duration: 3000,
- });
+ onSuccess: (data) => {
+ void router.push(`/docs/${data.id}`);
},
});
const { isFeatureFlagActivated } = useAnalytics();
diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/api/useDuplicateDoc.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/api/useDuplicateDoc.tsx
index 0c6c9193b1..f097401d7f 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-management/api/useDuplicateDoc.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-management/api/useDuplicateDoc.tsx
@@ -1,13 +1,14 @@
+import { VariantType, useToastProvider } from '@openfun/cunningham-react';
import {
UseMutationOptions,
useMutation,
useQueryClient,
} from '@tanstack/react-query';
+import { useTranslation } from 'react-i18next';
import * as Y from 'yjs';
import { APIError, errorCauses, fetchAPI } from '@/api';
import { toBase64 } from '@/docs/doc-editor';
-import { KEY_DOC_TREE } from '@/docs/doc-tree';
import { KEY_LIST_DOC_VERSIONS } from '@/docs/doc-versioning';
import { useProviderStore } from '../stores';
@@ -52,9 +53,10 @@ type DuplicateDocOptions = UseMutationOptions<
DuplicateDocParams
>;
-export function useDuplicateDoc(options: DuplicateDocOptions) {
+export function useDuplicateDoc(options?: DuplicateDocOptions) {
const queryClient = useQueryClient();
-
+ const { toast } = useToastProvider();
+ const { t } = useTranslation();
const { provider } = useProviderStore();
const { mutateAsync: updateDoc } = useUpdateDoc({
@@ -86,10 +88,19 @@ export function useDuplicateDoc(options: DuplicateDocOptions) {
void queryClient.resetQueries({
queryKey: [KEY_LIST_DOC],
});
- void queryClient.resetQueries({
- queryKey: [KEY_DOC_TREE],
+
+ toast(t('Document duplicated successfully!'), VariantType.SUCCESS, {
+ duration: 3000,
});
- void options.onSuccess?.(data, variables, context);
+
+ void options?.onSuccess?.(data, variables, context);
+ },
+ onError: (error, variables, context) => {
+ toast(t('Failed to duplicate the document...'), VariantType.ERROR, {
+ duration: 3000,
+ });
+
+ void options?.onError?.(error, variables, context);
},
});
}
diff --git a/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocTreeItemActions.tsx b/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocTreeItemActions.tsx
index 37fe819942..1d8d8a9248 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocTreeItemActions.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocTreeItemActions.tsx
@@ -10,13 +10,14 @@ import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import { Box, BoxButton, Icon } from '@/components';
-
import {
Doc,
ModalRemoveDoc,
Role,
useCopyDocLink,
-} from '../../doc-management';
+ useDuplicateDoc,
+} from '@/docs/doc-management';
+
import { useCreateChildrenDoc } from '../api/useCreateChildren';
import { useDetachDoc } from '../api/useDetach';
import MoveDocIcon from '../assets/doc-extract-bold.svg';
@@ -45,6 +46,11 @@ export const DocTreeItemActions = ({
const { isCurrentParent } = useTreeUtils(doc);
const { mutate: detachDoc } = useDetachDoc();
const treeContext = useTreeContext();
+ const { mutate: duplicateDoc } = useDuplicateDoc({
+ onSuccess: (data) => {
+ void router.push(`/docs/${data.id}`);
+ },
+ });
const handleDetachDoc = () => {
if (!treeContext?.root) {
@@ -89,6 +95,18 @@ export const DocTreeItemActions = ({
},
]
: []),
+ {
+ label: t('Duplicate'),
+ icon: ,
+ isDisabled: !doc.abilities.duplicate,
+ callback: () => {
+ duplicateDoc({
+ docId: doc.id,
+ with_accesses: false,
+ canSave: doc.abilities.partial_update,
+ });
+ },
+ },
{
label: t('Delete'),
isDisabled: !doc.abilities.destroy,
diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridActions.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridActions.tsx
index 8928a3ffc6..f2fbc04c0a 100644
--- a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridActions.tsx
+++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridActions.tsx
@@ -1,8 +1,4 @@
-import {
- VariantType,
- useModal,
- useToastProvider,
-} from '@openfun/cunningham-react';
+import { useModal } from '@openfun/cunningham-react';
import { useTranslation } from 'react-i18next';
import { DropdownMenu, DropdownMenuOption, Icon } from '@/components';
@@ -25,21 +21,9 @@ export const DocsGridActions = ({
openShareModal,
}: DocsGridActionsProps) => {
const { t } = useTranslation();
- const { toast } = useToastProvider();
const deleteModal = useModal();
- const { mutate: duplicateDoc } = useDuplicateDoc({
- onSuccess: () => {
- toast(t('Document duplicated successfully!'), VariantType.SUCCESS, {
- duration: 3000,
- });
- },
- onError: () => {
- toast(t('Failed to duplicate the document...'), VariantType.ERROR, {
- duration: 3000,
- });
- },
- });
+ const { mutate: duplicateDoc } = useDuplicateDoc();
const removeFavoriteDoc = useDeleteFavoriteDoc({
listInvalideQueries: [KEY_LIST_DOC],