From 2b2e81f04217196b3df549ee4bc8387fff3089c9 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Mon, 16 Jun 2025 12:54:07 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F(frontend)=20Simplify=20AGPL?= =?UTF-8?q?=20export=20pattern?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We were maintaining two separate components for AGPL and MIT license exports. This commit consolidates the functionality into a single component that handles both licenses, simplifying the codebase and reducing duplication. --- .../doc-export/__tests__/ExportMIT.test.tsx | 33 +++ .../src/features/docs/doc-export/index.ts | 19 +- .../__tests__/DocToolBoxAGPL.spec.tsx | 28 --- .../__tests__/DocToolBoxMIT.spec.tsx | 35 ---- .../docs/doc-header/components/DocToolBox.tsx | 184 +++++++++++++++-- .../components/DocToolBoxLicenceAGPL.tsx | 192 ------------------ .../components/DocToolBoxLicenceMIT.tsx | 165 --------------- 7 files changed, 214 insertions(+), 442 deletions(-) create mode 100644 src/frontend/apps/impress/src/features/docs/doc-export/__tests__/ExportMIT.test.tsx delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocToolBoxAGPL.spec.tsx delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocToolBoxMIT.spec.tsx delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBoxLicenceAGPL.tsx delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBoxLicenceMIT.tsx diff --git a/src/frontend/apps/impress/src/features/docs/doc-export/__tests__/ExportMIT.test.tsx b/src/frontend/apps/impress/src/features/docs/doc-export/__tests__/ExportMIT.test.tsx new file mode 100644 index 00000000..a4c7e740 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-export/__tests__/ExportMIT.test.tsx @@ -0,0 +1,33 @@ +const originalEnv = process.env.NEXT_PUBLIC_PUBLISH_AS_MIT; + +jest.mock('@/features/docs/doc-export/utils', () => ({ + anything: true, +})); +jest.mock('@/features/docs/doc-export/components/ModalExport', () => ({ + ModalExport: () => ModalExport, +})); + +describe('useModuleExport', () => { + afterAll(() => { + process.env.NEXT_PUBLIC_PUBLISH_AS_MIT = originalEnv; + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.resetModules(); + }); + + it('should return undefined when NEXT_PUBLIC_PUBLISH_AS_MIT is true', async () => { + process.env.NEXT_PUBLIC_PUBLISH_AS_MIT = 'true'; + const Export = await import('@/features/docs/doc-export/'); + + expect(Export.default).toBeUndefined(); + }); + + it('should load modules when NEXT_PUBLIC_PUBLISH_AS_MIT is false', async () => { + process.env.NEXT_PUBLIC_PUBLISH_AS_MIT = 'false'; + const Export = await import('@/features/docs/doc-export/'); + + expect(Export.default).toHaveProperty('ModalExport'); + }); +}); diff --git a/src/frontend/apps/impress/src/features/docs/doc-export/index.ts b/src/frontend/apps/impress/src/features/docs/doc-export/index.ts index 527c58f0..cb1ab543 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-export/index.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-export/index.ts @@ -1,3 +1,20 @@ +/** + * To import Export modules you must import from the index file. + * This is to ensure that the Export modules are only loaded when + * the application is not published as MIT. + */ export * from './api'; -export * from './components'; export * from './utils'; + +import * as ModalExport from './components/ModalExport'; + +let modulesExport = undefined; +if (process.env.NEXT_PUBLIC_PUBLISH_AS_MIT === 'false') { + modulesExport = { + ...ModalExport, + }; +} + +type ModulesExport = typeof modulesExport; + +export default modulesExport as ModulesExport; diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocToolBoxAGPL.spec.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocToolBoxAGPL.spec.tsx deleted file mode 100644 index 2624889b..00000000 --- a/src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocToolBoxAGPL.spec.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import React from 'react'; - -import { AppWrapper } from '@/tests/utils'; - -import { DocToolBox } from '../components/DocToolBox'; - -const doc = { - nb_accesses: 1, - abilities: { - versions_list: true, - destroy: true, - }, -}; - -jest.mock('@/features/docs/doc-export/', () => ({ - ModalExport: () => ModalExport, -})); - -it('DocToolBox dynamic import: loads DocToolBox when NEXT_PUBLIC_PUBLISH_AS_MIT is false', async () => { - process.env.NEXT_PUBLIC_PUBLISH_AS_MIT = 'false'; - - render(, { - wrapper: AppWrapper, - }); - - expect(await screen.findByText('download')).toBeInTheDocument(); -}); diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocToolBoxMIT.spec.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocToolBoxMIT.spec.tsx deleted file mode 100644 index b7901f2d..00000000 --- a/src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocToolBoxMIT.spec.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { render, screen, waitFor } from '@testing-library/react'; -import React from 'react'; - -import { AppWrapper } from '@/tests/utils'; - -const doc = { - nb_accesses: 1, - abilities: { - versions_list: true, - destroy: true, - }, -}; - -jest.mock('@/features/docs/doc-export/', () => ({ - ModalExport: () => ModalExport, -})); - -it('DocToolBox dynamic import: loads DocToolBox when NEXT_PUBLIC_PUBLISH_AS_MIT is true', async () => { - process.env.NEXT_PUBLIC_PUBLISH_AS_MIT = 'true'; - - const { DocToolBox } = await import('../components/DocToolBox'); - - render(, { - wrapper: AppWrapper, - }); - - await waitFor( - () => { - expect(screen.queryByText('download')).not.toBeInTheDocument(); - }, - { - timeout: 1000, - }, - ); -}); 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 3cdadfab..49c257ee 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,47 +1,146 @@ import { Button, useModal } from '@openfun/cunningham-react'; import { useQueryClient } from '@tanstack/react-query'; -import dynamic from 'next/dynamic'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { css } from 'styled-components'; -import { Box, Icon } from '@/components'; +import { + Box, + DropdownMenu, + DropdownMenuOption, + Icon, + IconOptions, +} from '@/components'; import { useCunninghamTheme } from '@/cunningham'; -import { Doc } from '@/docs/doc-management'; -import { KEY_LIST_DOC_VERSIONS } from '@/docs/doc-versioning'; +import Export from '@/docs/doc-export/'; +import { + Doc, + KEY_DOC, + KEY_LIST_DOC, + ModalRemoveDoc, + useCopyDocLink, + useCreateFavoriteDoc, + useDeleteFavoriteDoc, +} from '@/docs/doc-management'; +import { DocShareModal } from '@/docs/doc-share'; +import { + KEY_LIST_DOC_VERSIONS, + ModalSelectVersion, +} from '@/docs/doc-versioning'; +import { useAnalytics } from '@/libs'; import { useResponsiveStore } from '@/stores'; +import { useCopyCurrentEditorToClipboard } from '../hooks/useCopyCurrentEditorToClipboard'; + +const ModalExport = Export?.ModalExport; + interface DocToolBoxProps { doc: Doc; } -const DocToolBoxLicence = dynamic(() => - process.env.NEXT_PUBLIC_PUBLISH_AS_MIT === 'false' - ? import('./DocToolBoxLicenceAGPL').then((mod) => mod.DocToolBoxLicenceAGPL) - : import('./DocToolBoxLicenceMIT').then((mod) => mod.DocToolBoxLicenceMIT), -); - export const DocToolBox = ({ doc }: DocToolBoxProps) => { const { t } = useTranslation(); const hasAccesses = doc.nb_accesses_direct > 1 && doc.abilities.accesses_view; const queryClient = useQueryClient(); - const { spacingsTokens } = useCunninghamTheme(); + const { spacingsTokens, colorsTokens } = useCunninghamTheme(); - const modalHistory = useModal(); + const [isModalRemoveOpen, setIsModalRemoveOpen] = useState(false); + const [isModalExportOpen, setIsModalExportOpen] = useState(false); + const selectHistoryModal = useModal(); const modalShare = useModal(); - const { isSmallMobile } = useResponsiveStore(); + const { isSmallMobile, isDesktop } = useResponsiveStore(); + const copyDocLink = useCopyDocLink(doc.id); + const { isFeatureFlagActivated } = useAnalytics(); + const removeFavoriteDoc = useDeleteFavoriteDoc({ + listInvalideQueries: [KEY_LIST_DOC, KEY_DOC], + }); + const makeFavoriteDoc = useCreateFavoriteDoc({ + listInvalideQueries: [KEY_LIST_DOC, KEY_DOC], + }); useEffect(() => { - if (modalHistory.isOpen) { + if (selectHistoryModal.isOpen) { return; } void queryClient.resetQueries({ queryKey: [KEY_LIST_DOC_VERSIONS], }); - }, [modalHistory.isOpen, queryClient]); + }, [selectHistoryModal.isOpen, queryClient]); + + const options: DropdownMenuOption[] = [ + ...(isSmallMobile + ? [ + { + label: t('Share'), + icon: 'group', + callback: modalShare.open, + }, + { + label: t('Export'), + icon: 'download', + callback: () => { + setIsModalExportOpen(true); + }, + show: !!ModalExport, + }, + { + label: t('Copy link'), + icon: 'add_link', + callback: copyDocLink, + }, + ] + : []), + { + label: doc.is_favorite ? t('Unpin') : t('Pin'), + icon: 'push_pin', + callback: () => { + if (doc.is_favorite) { + removeFavoriteDoc.mutate({ id: doc.id }); + } else { + makeFavoriteDoc.mutate({ id: doc.id }); + } + }, + testId: `docs-actions-${doc.is_favorite ? 'unpin' : 'pin'}-${doc.id}`, + }, + { + label: t('Version history'), + icon: 'history', + disabled: !doc.abilities.versions_list, + callback: () => { + selectHistoryModal.open(); + }, + show: isDesktop, + }, + + { + label: t('Copy as {{format}}', { format: 'Markdown' }), + icon: 'content_copy', + callback: () => { + void copyCurrentEditorToClipboard('markdown'); + }, + }, + { + label: t('Copy as {{format}}', { format: 'HTML' }), + icon: 'content_copy', + callback: () => { + void copyCurrentEditorToClipboard('html'); + }, + show: isFeatureFlagActivated('CopyAsHTML'), + }, + { + label: t('Delete document'), + icon: 'delete', + disabled: !doc.abilities.destroy, + callback: () => { + setIsModalRemoveOpen(true); + }, + }, + ]; + + const copyCurrentEditorToClipboard = useCopyCurrentEditorToClipboard(); return ( { )} - + {!isSmallMobile && ModalExport && ( +