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],