diff --git a/CHANGELOG.md b/CHANGELOG.md index 83c2756d..8cfb8be5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to ## [Unreleased] +## Fixed + +- 🐛(frontend) share modal is shown when you don't have the abilities #557 + ## [2.0.0] - 2025-01-13 ## Added 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 91c66b32..b2d1e752 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 @@ -47,6 +47,7 @@ test.describe('Doc Header', () => { versions_list: true, versions_retrieve: true, accesses_manage: true, + accesses_view: true, update: true, partial_update: true, retrieve: true, @@ -396,6 +397,28 @@ test.describe('Doc Header', () => { const clipboardContent = await handle.jsonValue(); expect(clipboardContent.trim()).toBe(`

Hello World

`); }); + + test('it checks the copy link button', async ({ page }) => { + await mockedDocument(page, { + abilities: { + destroy: false, // Means owner + link_configuration: true, + versions_destroy: true, + versions_list: true, + versions_retrieve: true, + accesses_manage: false, + accesses_view: false, + update: true, + partial_update: true, + retrieve: true, + }, + }); + + await goToGridDoc(page); + + await page.getByRole('button', { name: 'Copy link' }).click(); + await expect(page.getByText('Link Copied !')).toBeVisible(); + }); }); test.describe('Documents Header mobile', () => { @@ -405,6 +428,45 @@ test.describe('Documents Header mobile', () => { await page.goto('/'); }); + test('it checks the copy link button', async ({ page, browserName }) => { + // eslint-disable-next-line playwright/no-skipped-test + test.skip( + browserName === 'webkit', + 'navigator.clipboard is not working with webkit and playwright', + ); + await mockedDocument(page, { + abilities: { + destroy: false, + link_configuration: true, + versions_destroy: true, + versions_list: true, + versions_retrieve: true, + accesses_manage: false, + accesses_view: false, + update: true, + partial_update: true, + retrieve: true, + }, + }); + + await goToGridDoc(page); + + await expect(page.getByRole('button', { name: 'Copy link' })).toBeHidden(); + await page.getByLabel('Open the document options').click(); + await page.getByRole('button', { name: 'Copy link' }).click(); + await expect(page.getByText('Link Copied !')).toBeVisible(); + // Test that clipboard is in HTML format + const handle = await page.evaluateHandle(() => + navigator.clipboard.readText(), + ); + const clipboardContent = await handle.jsonValue(); + + const origin = await page.evaluate(() => window.location.origin); + expect(clipboardContent.trim()).toMatch( + `${origin}/docs/mocked-document-id/`, + ); + }); + test('it checks the close button on Share modal', async ({ page }) => { await mockedDocument(page, { abilities: { @@ -414,6 +476,7 @@ test.describe('Documents Header mobile', () => { versions_list: true, versions_retrieve: true, accesses_manage: true, + accesses_view: true, update: true, partial_update: true, retrieve: true, diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-member-create.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-member-create.spec.ts index 04015e9e..f9d306a5 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-member-create.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-member-create.spec.ts @@ -79,7 +79,7 @@ test.describe('Document create member', () => { await expect(quickSearchContent.getByText(email).first()).toBeVisible(); // Check user added - await expect(page.getByText('Share with 3 users')).toBeVisible(); + await expect(page.getByText('Share with 2 users')).toBeVisible(); await expect( quickSearchContent.getByText(users[0].full_name).first(), ).toBeVisible(); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts index f493c8de..612c1a4b 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts @@ -413,14 +413,8 @@ test.describe('Doc Visibility: Authenticated', () => { await page.goto(urlDoc); await expect(page.locator('h2').getByText(docTitle)).toBeVisible(); - await page.getByRole('button', { name: 'Share' }).click(); - - await expect(selectVisibility).toBeHidden(); - - const inputSearch = page.getByRole('combobox', { - name: 'Quick search input', - }); - await expect(inputSearch).toBeHidden(); + await page.getByRole('button', { name: 'Copy link' }).click(); + await expect(page.getByText('Link Copied !')).toBeVisible(); }); test('It checks a authenticated doc in editable mode', async ({ @@ -474,13 +468,7 @@ test.describe('Doc Visibility: Authenticated', () => { await page.goto(urlDoc); await verifyDocName(page, docTitle); - await page.getByRole('button', { name: 'Share' }).click(); - - await expect(selectVisibility).toBeHidden(); - - const inputSearch = page.getByRole('combobox', { - name: 'Quick search input', - }); - await expect(inputSearch).toBeHidden(); + await page.getByRole('button', { name: 'Copy link' }).click(); + await expect(page.getByText('Link Copied !')).toBeVisible(); }); }); 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 adf3ce5c..a940e90c 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 @@ -16,10 +16,13 @@ import { Icon, IconOptions, } from '@/components'; -import { useAuthStore } from '@/core'; import { useCunninghamTheme } from '@/cunningham'; import { useEditorStore } from '@/features/docs/doc-editor/'; -import { Doc, ModalRemoveDoc } from '@/features/docs/doc-management'; +import { + Doc, + ModalRemoveDoc, + useCopyDocLink, +} from '@/features/docs/doc-management'; import { DocShareModal } from '@/features/docs/doc-share'; import { KEY_LIST_DOC_VERSIONS, @@ -37,6 +40,9 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => { const { t } = useTranslation(); const hasAccesses = doc.nb_accesses > 1; const queryClient = useQueryClient(); + + const copyDocLink = useCopyDocLink(doc.id); + const { spacingsTokens, colorsTokens } = useCunninghamTheme(); const spacings = spacingsTokens(); @@ -48,18 +54,24 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => { const modalShare = useModal(); const { isSmallMobile, isDesktop } = useResponsiveStore(); - const { authenticated } = useAuthStore(); const { editor } = useEditorStore(); + const { toast } = useToastProvider(); + const canViewAccesses = doc.abilities.accesses_view; const options: DropdownMenuOption[] = [ ...(isSmallMobile ? [ { - label: t('Share'), - icon: 'upload', + label: canViewAccesses ? t('Share') : t('Copy link'), + icon: canViewAccesses ? 'group' : 'link', + callback: () => { - modalShare.open(); + if (canViewAccesses) { + modalShare.open(); + return; + } + copyDocLink(); }, }, { @@ -153,7 +165,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => { $margin={{ left: 'auto' }} $gap={spacings['2xs']} > - {authenticated && !isSmallMobile && ( + {canViewAccesses && !isSmallMobile && ( <> {!hasAccesses && ( )} )} + {!canViewAccesses && !isSmallMobile && ( + + )} {!isSmallMobile && ( - )} - {isDesktop && !isPublic && isRestricted && isShared && ( - - )} - {isDesktop && !isPublic && isAuthenticated && ( - + {isDesktop && ( + )} + diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridItemSharedButton.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridItemSharedButton.tsx new file mode 100644 index 00000000..8742c6eb --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridItemSharedButton.tsx @@ -0,0 +1,66 @@ +import { Button } from '@openfun/cunningham-react'; +import { useMemo } from 'react'; + +import { Box, Icon } from '@/components'; + +import { Doc, LinkReach } from '../../doc-management'; + +type Props = { + doc: Doc; + handleClick: () => void; +}; +export const DocsGridItemSharedButton = ({ doc, handleClick }: Props) => { + const isPublic = doc.link_reach === LinkReach.PUBLIC; + const isAuthenticated = doc.link_reach === LinkReach.AUTHENTICATED; + const isRestricted = doc.link_reach === LinkReach.RESTRICTED; + const sharedCount = doc.nb_accesses - 1; + const isShared = sharedCount > 0; + + const icon = useMemo(() => { + if (isPublic) { + return 'public'; + } + if (isAuthenticated) { + return 'corporate_fare'; + } + if (isRestricted) { + return 'group'; + } + + return undefined; + }, [isPublic, isAuthenticated, isRestricted]); + + if (!icon) { + return null; + } + + if (!doc.abilities.accesses_view) { + return ( + + + + ); + } + + return ( + + ); +}; diff --git a/src/frontend/apps/impress/src/hook/index.ts b/src/frontend/apps/impress/src/hook/index.ts index 8b401809..b518f615 100644 --- a/src/frontend/apps/impress/src/hook/index.ts +++ b/src/frontend/apps/impress/src/hook/index.ts @@ -1 +1,2 @@ export * from './useDate'; +export * from './useClipboard'; diff --git a/src/frontend/apps/impress/src/hook/useClipboard.tsx b/src/frontend/apps/impress/src/hook/useClipboard.tsx new file mode 100644 index 00000000..f7dd59e3 --- /dev/null +++ b/src/frontend/apps/impress/src/hook/useClipboard.tsx @@ -0,0 +1,34 @@ +import { VariantType, useToastProvider } from '@openfun/cunningham-react'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +export const useClipboard = () => { + const { toast } = useToastProvider(); + const { t } = useTranslation(); + + return useCallback( + (text: string, successMessage?: string, errorMessage?: string) => { + navigator.clipboard + .writeText(text) + .then(() => { + toast( + successMessage ?? t('Copied to clipboard'), + VariantType.SUCCESS, + { + duration: 3000, + }, + ); + }) + .catch(() => { + toast( + errorMessage ?? t('Failed to copy to clipboard'), + VariantType.ERROR, + { + duration: 3000, + }, + ); + }); + }, + [t, toast], + ); +};