♻️(frontend) Simplify AGPL export pattern

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.
This commit is contained in:
Anthony LC
2025-06-16 12:54:07 +02:00
parent c8ae2f6549
commit 2b2e81f042
7 changed files with 214 additions and 442 deletions

View File

@@ -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: () => <span>ModalExport</span>,
}));
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');
});
});

View File

@@ -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;

View File

@@ -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: () => <span>ModalExport</span>,
}));
it('DocToolBox dynamic import: loads DocToolBox when NEXT_PUBLIC_PUBLISH_AS_MIT is false', async () => {
process.env.NEXT_PUBLIC_PUBLISH_AS_MIT = 'false';
render(<DocToolBox doc={doc as any} />, {
wrapper: AppWrapper,
});
expect(await screen.findByText('download')).toBeInTheDocument();
});

View File

@@ -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: () => <span>ModalExport</span>,
}));
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(<DocToolBox doc={doc as any} />, {
wrapper: AppWrapper,
});
await waitFor(
() => {
expect(screen.queryByText('download')).not.toBeInTheDocument();
},
{
timeout: 1000,
},
);
});

View File

@@ -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 (
<Box
@@ -99,12 +198,55 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
</>
)}
<DocToolBoxLicence
doc={doc}
modalHistory={modalHistory}
modalShare={modalShare}
/>
{!isSmallMobile && ModalExport && (
<Button
color="tertiary-text"
icon={
<Icon iconName="download" $theme="primary" $variation="800" />
}
onClick={() => {
setIsModalExportOpen(true);
}}
size={isSmallMobile ? 'small' : 'medium'}
/>
)}
<DropdownMenu options={options}>
<IconOptions
isHorizontal
$theme="primary"
$padding={{ all: 'xs' }}
$css={css`
border-radius: 4px;
&:hover {
background-color: ${colorsTokens['greyscale-100']};
}
${isSmallMobile
? css`
padding: 10px;
border: 1px solid ${colorsTokens['greyscale-300']};
`
: ''}
`}
aria-label={t('Open the document options')}
/>
</DropdownMenu>
</Box>
{modalShare.isOpen && (
<DocShareModal onClose={() => modalShare.close()} doc={doc} />
)}
{isModalExportOpen && ModalExport && (
<ModalExport onClose={() => setIsModalExportOpen(false)} doc={doc} />
)}
{isModalRemoveOpen && (
<ModalRemoveDoc onClose={() => setIsModalRemoveOpen(false)} doc={doc} />
)}
{selectHistoryModal.isOpen && (
<ModalSelectVersion
onClose={() => selectHistoryModal.close()}
doc={doc}
/>
)}
</Box>
);
};

View File

@@ -1,192 +0,0 @@
import { Button, useModal } from '@openfun/cunningham-react';
import { useQueryClient } from '@tanstack/react-query';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import {
DropdownMenu,
DropdownMenuOption,
Icon,
IconOptions,
} from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { ModalExport } from '@/docs/doc-export/';
import {
Doc,
KEY_DOC,
KEY_LIST_DOC,
ModalRemoveDoc,
useCopyDocLink,
useCreateFavoriteDoc,
useDeleteFavoriteDoc,
} from '@/docs/doc-management';
import {
KEY_LIST_DOC_VERSIONS,
ModalSelectVersion,
} from '@/docs/doc-versioning';
import { useAnalytics } from '@/libs';
import { useResponsiveStore } from '@/stores';
import { DocShareModal } from '../../doc-share';
import { useCopyCurrentEditorToClipboard } from '../hooks/useCopyCurrentEditorToClipboard';
type ModalType = ReturnType<typeof useModal>;
interface DocToolBoxLicenceProps {
doc: Doc;
modalHistory: ModalType;
modalShare: ModalType;
}
export const DocToolBoxLicenceAGPL = ({
doc,
modalHistory,
modalShare,
}: DocToolBoxLicenceProps) => {
const { t } = useTranslation();
const queryClient = useQueryClient();
const { colorsTokens } = useCunninghamTheme();
const [isModalRemoveOpen, setIsModalRemoveOpen] = useState(false);
const [isModalExportOpen, setIsModalExportOpen] = useState(false);
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],
});
const copyCurrentEditorToClipboard = useCopyCurrentEditorToClipboard();
const options: DropdownMenuOption[] = [
...(isSmallMobile
? [
{
label: t('Share'),
icon: 'group',
callback: modalShare.open,
},
{
label: t('Export'),
icon: 'download',
callback: () => {
setIsModalExportOpen(true);
},
},
{
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: () => {
modalHistory.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);
},
},
];
useEffect(() => {
if (modalHistory.isOpen) {
return;
}
void queryClient.resetQueries({
queryKey: [KEY_LIST_DOC_VERSIONS],
});
}, [modalHistory.isOpen, queryClient]);
return (
<>
{!isSmallMobile && (
<Button
color="tertiary-text"
icon={<Icon iconName="download" $theme="primary" $variation="800" />}
onClick={() => {
setIsModalExportOpen(true);
}}
size={isSmallMobile ? 'small' : 'medium'}
/>
)}
<DropdownMenu options={options}>
<IconOptions
isHorizontal
$theme="primary"
$padding={{ all: 'xs' }}
$css={css`
border-radius: 4px;
&:hover {
background-color: ${colorsTokens['greyscale-100']};
}
${isSmallMobile
? css`
padding: 10px;
border: 1px solid ${colorsTokens['greyscale-300']};
`
: ''}
`}
aria-label={t('Open the document options')}
/>
</DropdownMenu>
{modalShare.isOpen && (
<DocShareModal onClose={() => modalShare.close()} doc={doc} />
)}
{isModalExportOpen && (
<ModalExport onClose={() => setIsModalExportOpen(false)} doc={doc} />
)}
{isModalRemoveOpen && (
<ModalRemoveDoc onClose={() => setIsModalRemoveOpen(false)} doc={doc} />
)}
{modalHistory.isOpen && (
<ModalSelectVersion onClose={() => modalHistory.close()} doc={doc} />
)}
</>
);
};

View File

@@ -1,165 +0,0 @@
import { useModal } from '@openfun/cunningham-react';
import { useQueryClient } from '@tanstack/react-query';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import { DropdownMenu, DropdownMenuOption, IconOptions } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import {
Doc,
KEY_DOC,
KEY_LIST_DOC,
ModalRemoveDoc,
useCopyDocLink,
useCreateFavoriteDoc,
useDeleteFavoriteDoc,
} from '@/docs/doc-management';
import {
KEY_LIST_DOC_VERSIONS,
ModalSelectVersion,
} from '@/docs/doc-versioning';
import { useAnalytics } from '@/libs';
import { useResponsiveStore } from '@/stores';
import { DocShareModal } from '../../doc-share';
import { useCopyCurrentEditorToClipboard } from '../hooks/useCopyCurrentEditorToClipboard';
type ModalType = ReturnType<typeof useModal>;
interface DocToolBoxLicenceProps {
doc: Doc;
modalHistory: ModalType;
modalShare: ModalType;
}
export const DocToolBoxLicenceMIT = ({
doc,
modalHistory,
modalShare,
}: DocToolBoxLicenceProps) => {
const { t } = useTranslation();
const queryClient = useQueryClient();
const { colorsTokens } = useCunninghamTheme();
const [isModalRemoveOpen, setIsModalRemoveOpen] = useState(false);
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],
});
const copyCurrentEditorToClipboard = useCopyCurrentEditorToClipboard();
const options: DropdownMenuOption[] = [
...(isSmallMobile
? [
{
label: t('Share'),
icon: 'group',
callback: modalShare.open,
},
{
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: () => {
modalHistory.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);
},
},
];
useEffect(() => {
if (modalHistory.isOpen) {
return;
}
void queryClient.resetQueries({
queryKey: [KEY_LIST_DOC_VERSIONS],
});
}, [modalHistory.isOpen, queryClient]);
return (
<>
<DropdownMenu options={options}>
<IconOptions
isHorizontal
$theme="primary"
$padding={{ all: 'xs' }}
$css={css`
border-radius: 4px;
&:hover {
background-color: ${colorsTokens['greyscale-100']};
}
${isSmallMobile
? css`
padding: 10px;
border: 1px solid ${colorsTokens['greyscale-300']};
`
: ''}
`}
aria-label={t('Open the document options')}
/>
</DropdownMenu>
{modalShare.isOpen && (
<DocShareModal onClose={() => modalShare.close()} doc={doc} />
)}
{isModalRemoveOpen && (
<ModalRemoveDoc onClose={() => setIsModalRemoveOpen(false)} doc={doc} />
)}
{modalHistory.isOpen && (
<ModalSelectVersion onClose={() => modalHistory.close()} doc={doc} />
)}
</>
);
};