🐛(frontend) hide the sharing method when you don't have the rights
- Added a new hook `useCopyDocLink` to handle copying document links to the clipboard with success/error notifications. - Updated the `DocToolBox`, `DocsGridActions`, and `DocShareModal` components to utilize the new copy link feature. - Enhanced tests to verify the functionality of the copy link button in various scenarios. - Adjusted visibility checks for sharing options based on user access rights.
This commit is contained in:
committed by
Anthony LC
parent
de8dea20d5
commit
282200ac3d
@@ -9,6 +9,10 @@ and this project adheres to
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
|
- 🐛(frontend) share modal is shown when you don't have the abilities #557
|
||||||
|
|
||||||
## [2.0.0] - 2025-01-13
|
## [2.0.0] - 2025-01-13
|
||||||
|
|
||||||
## Added
|
## Added
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ test.describe('Doc Header', () => {
|
|||||||
versions_list: true,
|
versions_list: true,
|
||||||
versions_retrieve: true,
|
versions_retrieve: true,
|
||||||
accesses_manage: true,
|
accesses_manage: true,
|
||||||
|
accesses_view: true,
|
||||||
update: true,
|
update: true,
|
||||||
partial_update: true,
|
partial_update: true,
|
||||||
retrieve: true,
|
retrieve: true,
|
||||||
@@ -396,6 +397,28 @@ test.describe('Doc Header', () => {
|
|||||||
const clipboardContent = await handle.jsonValue();
|
const clipboardContent = await handle.jsonValue();
|
||||||
expect(clipboardContent.trim()).toBe(`<h1>Hello World</h1><p></p>`);
|
expect(clipboardContent.trim()).toBe(`<h1>Hello World</h1><p></p>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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', () => {
|
test.describe('Documents Header mobile', () => {
|
||||||
@@ -405,6 +428,45 @@ test.describe('Documents Header mobile', () => {
|
|||||||
await page.goto('/');
|
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 }) => {
|
test('it checks the close button on Share modal', async ({ page }) => {
|
||||||
await mockedDocument(page, {
|
await mockedDocument(page, {
|
||||||
abilities: {
|
abilities: {
|
||||||
@@ -414,6 +476,7 @@ test.describe('Documents Header mobile', () => {
|
|||||||
versions_list: true,
|
versions_list: true,
|
||||||
versions_retrieve: true,
|
versions_retrieve: true,
|
||||||
accesses_manage: true,
|
accesses_manage: true,
|
||||||
|
accesses_view: true,
|
||||||
update: true,
|
update: true,
|
||||||
partial_update: true,
|
partial_update: true,
|
||||||
retrieve: true,
|
retrieve: true,
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ test.describe('Document create member', () => {
|
|||||||
await expect(quickSearchContent.getByText(email).first()).toBeVisible();
|
await expect(quickSearchContent.getByText(email).first()).toBeVisible();
|
||||||
|
|
||||||
// Check user added
|
// Check user added
|
||||||
await expect(page.getByText('Share with 3 users')).toBeVisible();
|
await expect(page.getByText('Share with 2 users')).toBeVisible();
|
||||||
await expect(
|
await expect(
|
||||||
quickSearchContent.getByText(users[0].full_name).first(),
|
quickSearchContent.getByText(users[0].full_name).first(),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|||||||
@@ -413,14 +413,8 @@ test.describe('Doc Visibility: Authenticated', () => {
|
|||||||
await page.goto(urlDoc);
|
await page.goto(urlDoc);
|
||||||
|
|
||||||
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
|
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
|
||||||
await page.getByRole('button', { name: 'Share' }).click();
|
await page.getByRole('button', { name: 'Copy link' }).click();
|
||||||
|
await expect(page.getByText('Link Copied !')).toBeVisible();
|
||||||
await expect(selectVisibility).toBeHidden();
|
|
||||||
|
|
||||||
const inputSearch = page.getByRole('combobox', {
|
|
||||||
name: 'Quick search input',
|
|
||||||
});
|
|
||||||
await expect(inputSearch).toBeHidden();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('It checks a authenticated doc in editable mode', async ({
|
test('It checks a authenticated doc in editable mode', async ({
|
||||||
@@ -474,13 +468,7 @@ test.describe('Doc Visibility: Authenticated', () => {
|
|||||||
await page.goto(urlDoc);
|
await page.goto(urlDoc);
|
||||||
|
|
||||||
await verifyDocName(page, docTitle);
|
await verifyDocName(page, docTitle);
|
||||||
await page.getByRole('button', { name: 'Share' }).click();
|
await page.getByRole('button', { name: 'Copy link' }).click();
|
||||||
|
await expect(page.getByText('Link Copied !')).toBeVisible();
|
||||||
await expect(selectVisibility).toBeHidden();
|
|
||||||
|
|
||||||
const inputSearch = page.getByRole('combobox', {
|
|
||||||
name: 'Quick search input',
|
|
||||||
});
|
|
||||||
await expect(inputSearch).toBeHidden();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,10 +16,13 @@ import {
|
|||||||
Icon,
|
Icon,
|
||||||
IconOptions,
|
IconOptions,
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
import { useAuthStore } from '@/core';
|
|
||||||
import { useCunninghamTheme } from '@/cunningham';
|
import { useCunninghamTheme } from '@/cunningham';
|
||||||
import { useEditorStore } from '@/features/docs/doc-editor/';
|
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 { DocShareModal } from '@/features/docs/doc-share';
|
||||||
import {
|
import {
|
||||||
KEY_LIST_DOC_VERSIONS,
|
KEY_LIST_DOC_VERSIONS,
|
||||||
@@ -37,6 +40,9 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const hasAccesses = doc.nb_accesses > 1;
|
const hasAccesses = doc.nb_accesses > 1;
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const copyDocLink = useCopyDocLink(doc.id);
|
||||||
|
|
||||||
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
|
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
|
||||||
|
|
||||||
const spacings = spacingsTokens();
|
const spacings = spacingsTokens();
|
||||||
@@ -48,18 +54,24 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
|||||||
const modalShare = useModal();
|
const modalShare = useModal();
|
||||||
|
|
||||||
const { isSmallMobile, isDesktop } = useResponsiveStore();
|
const { isSmallMobile, isDesktop } = useResponsiveStore();
|
||||||
const { authenticated } = useAuthStore();
|
|
||||||
const { editor } = useEditorStore();
|
const { editor } = useEditorStore();
|
||||||
|
|
||||||
const { toast } = useToastProvider();
|
const { toast } = useToastProvider();
|
||||||
|
const canViewAccesses = doc.abilities.accesses_view;
|
||||||
|
|
||||||
const options: DropdownMenuOption[] = [
|
const options: DropdownMenuOption[] = [
|
||||||
...(isSmallMobile
|
...(isSmallMobile
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
label: t('Share'),
|
label: canViewAccesses ? t('Share') : t('Copy link'),
|
||||||
icon: 'upload',
|
icon: canViewAccesses ? 'group' : 'link',
|
||||||
|
|
||||||
callback: () => {
|
callback: () => {
|
||||||
modalShare.open();
|
if (canViewAccesses) {
|
||||||
|
modalShare.open();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
copyDocLink();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -153,7 +165,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
|||||||
$margin={{ left: 'auto' }}
|
$margin={{ left: 'auto' }}
|
||||||
$gap={spacings['2xs']}
|
$gap={spacings['2xs']}
|
||||||
>
|
>
|
||||||
{authenticated && !isSmallMobile && (
|
{canViewAccesses && !isSmallMobile && (
|
||||||
<>
|
<>
|
||||||
{!hasAccesses && (
|
{!hasAccesses && (
|
||||||
<Button
|
<Button
|
||||||
@@ -187,12 +199,23 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
|||||||
}}
|
}}
|
||||||
size={isSmallMobile ? 'small' : 'medium'}
|
size={isSmallMobile ? 'small' : 'medium'}
|
||||||
>
|
>
|
||||||
{doc.nb_accesses}
|
{doc.nb_accesses - 1}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{!canViewAccesses && !isSmallMobile && (
|
||||||
|
<Button
|
||||||
|
color="tertiary-text"
|
||||||
|
onClick={() => {
|
||||||
|
copyDocLink();
|
||||||
|
}}
|
||||||
|
size={isSmallMobile ? 'small' : 'medium'}
|
||||||
|
>
|
||||||
|
{t('Copy link')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{!isSmallMobile && (
|
{!isSmallMobile && (
|
||||||
<Button
|
<Button
|
||||||
color="tertiary-text"
|
color="tertiary-text"
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from './useCollaboration';
|
export * from './useCollaboration';
|
||||||
export * from './useTrans';
|
export * from './useTrans';
|
||||||
|
export * from './useCopyDocLink';
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { useClipboard } from '@/hook';
|
||||||
|
|
||||||
|
import { Doc } from '../types';
|
||||||
|
|
||||||
|
export const useCopyDocLink = (docId: Doc['id']) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const copyToClipboard = useClipboard();
|
||||||
|
|
||||||
|
return useCallback(() => {
|
||||||
|
copyToClipboard(
|
||||||
|
`${window.location.origin}/docs/${docId}/`,
|
||||||
|
t('Link Copied !'),
|
||||||
|
t('Failed to copy link'),
|
||||||
|
);
|
||||||
|
}, [copyToClipboard, docId, t]);
|
||||||
|
};
|
||||||
@@ -56,8 +56,9 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
|
|||||||
const [userQuery, setUserQuery] = useState('');
|
const [userQuery, setUserQuery] = useState('');
|
||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
|
|
||||||
const [listHeight, setListHeight] = useState<string>('400px');
|
const [listHeight, setListHeight] = useState<string>('0px');
|
||||||
const canShare = doc.abilities.accesses_manage;
|
const canShare = doc.abilities.accesses_manage;
|
||||||
|
const canViewAccesses = doc.abilities.accesses_view;
|
||||||
const showMemberSection = inputValue === '' && selectedUsers.length === 0;
|
const showMemberSection = inputValue === '' && selectedUsers.length === 0;
|
||||||
const showFooter = selectedUsers.length === 0 && !inputValue;
|
const showFooter = selectedUsers.length === 0 && !inputValue;
|
||||||
|
|
||||||
@@ -94,7 +95,7 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
|
|||||||
count === 1
|
count === 1
|
||||||
? t('Document owner')
|
? t('Document owner')
|
||||||
: t('Share with {{count}} users', {
|
: t('Share with {{count}} users', {
|
||||||
count,
|
count: count - 1,
|
||||||
}),
|
}),
|
||||||
elements: members,
|
elements: members,
|
||||||
endActions: membersQuery.hasNextPage
|
endActions: membersQuery.hasNextPage
|
||||||
@@ -168,6 +169,10 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleRef = (node: HTMLDivElement) => {
|
const handleRef = (node: HTMLDivElement) => {
|
||||||
|
if (!canViewAccesses) {
|
||||||
|
setListHeight('0px');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const inputHeight = canShare ? 70 : 0;
|
const inputHeight = canShare ? 70 : 0;
|
||||||
const marginTop = 11;
|
const marginTop = 11;
|
||||||
const footerHeight = node?.clientHeight ?? 0;
|
const footerHeight = node?.clientHeight ?? 0;
|
||||||
@@ -191,7 +196,7 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
|
|||||||
<ShareModalStyle />
|
<ShareModalStyle />
|
||||||
<Box
|
<Box
|
||||||
aria-label={t('Share modal')}
|
aria-label={t('Share modal')}
|
||||||
$height={modalContentHeight}
|
$height={canViewAccesses ? modalContentHeight : 'auto'}
|
||||||
$overflow="hidden"
|
$overflow="hidden"
|
||||||
$justify="space-between"
|
$justify="space-between"
|
||||||
>
|
>
|
||||||
@@ -235,39 +240,43 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
|
|||||||
loading={searchUsersQuery.isLoading}
|
loading={searchUsersQuery.isLoading}
|
||||||
placeholder={t('Type a name or email')}
|
placeholder={t('Type a name or email')}
|
||||||
>
|
>
|
||||||
{!showMemberSection && inputValue !== '' && (
|
{canViewAccesses && (
|
||||||
<QuickSearchGroup
|
|
||||||
group={searchUserData}
|
|
||||||
onSelect={onSelect}
|
|
||||||
renderElement={(user) => (
|
|
||||||
<DocShareModalInviteUserRow user={user} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{showMemberSection && (
|
|
||||||
<>
|
<>
|
||||||
{invitationsData.elements.length > 0 && (
|
{!showMemberSection && inputValue !== '' && (
|
||||||
<Box aria-label={t('List invitation card')}>
|
|
||||||
<QuickSearchGroup
|
|
||||||
group={invitationsData}
|
|
||||||
renderElement={(invitation) => (
|
|
||||||
<DocShareInvitationItem
|
|
||||||
doc={doc}
|
|
||||||
invitation={invitation}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Box aria-label={t('List members card')}>
|
|
||||||
<QuickSearchGroup
|
<QuickSearchGroup
|
||||||
group={membersData}
|
group={searchUserData}
|
||||||
renderElement={(access) => (
|
onSelect={onSelect}
|
||||||
<DocShareMemberItem doc={doc} access={access} />
|
renderElement={(user) => (
|
||||||
|
<DocShareModalInviteUserRow user={user} />
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Box>
|
)}
|
||||||
|
{showMemberSection && (
|
||||||
|
<>
|
||||||
|
{invitationsData.elements.length > 0 && (
|
||||||
|
<Box aria-label={t('List invitation card')}>
|
||||||
|
<QuickSearchGroup
|
||||||
|
group={invitationsData}
|
||||||
|
renderElement={(invitation) => (
|
||||||
|
<DocShareInvitationItem
|
||||||
|
doc={doc}
|
||||||
|
invitation={invitation}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box aria-label={t('List members card')}>
|
||||||
|
<QuickSearchGroup
|
||||||
|
group={membersData}
|
||||||
|
renderElement={(access) => (
|
||||||
|
<DocShareMemberItem doc={doc} access={access} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</QuickSearch>
|
</QuickSearch>
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import {
|
import { Button } from '@openfun/cunningham-react';
|
||||||
Button,
|
|
||||||
VariantType,
|
|
||||||
useToastProvider,
|
|
||||||
} from '@openfun/cunningham-react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { css } from 'styled-components';
|
import { css } from 'styled-components';
|
||||||
|
|
||||||
import { Box, HorizontalSeparator } from '@/components';
|
import { Box, HorizontalSeparator } from '@/components';
|
||||||
import { Doc } from '@/features/docs';
|
import { Doc, useCopyDocLink } from '@/features/docs';
|
||||||
|
|
||||||
import { DocVisibility } from './DocVisibility';
|
import { DocVisibility } from './DocVisibility';
|
||||||
|
|
||||||
@@ -18,7 +14,8 @@ type Props = {
|
|||||||
|
|
||||||
export const DocShareModalFooter = ({ doc, onClose }: Props) => {
|
export const DocShareModalFooter = ({ doc, onClose }: Props) => {
|
||||||
const canShare = doc.abilities.accesses_manage;
|
const canShare = doc.abilities.accesses_manage;
|
||||||
const { toast } = useToastProvider();
|
|
||||||
|
const copyDocLink = useCopyDocLink(doc.id);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -41,18 +38,7 @@ export const DocShareModalFooter = ({ doc, onClose }: Props) => {
|
|||||||
<Button
|
<Button
|
||||||
fullWidth={false}
|
fullWidth={false}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard
|
copyDocLink();
|
||||||
.writeText(window.location.href)
|
|
||||||
.then(() => {
|
|
||||||
toast(t('Link Copied !'), VariantType.SUCCESS, {
|
|
||||||
duration: 3000,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast(t('Failed to copy link'), VariantType.ERROR, {
|
|
||||||
duration: 3000,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
color="tertiary"
|
color="tertiary"
|
||||||
icon={<span className="material-icons">add_link</span>}
|
icon={<span className="material-icons">add_link</span>}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { useResponsiveStore } from '@/stores';
|
|||||||
const leftPaddingMap: { [key: number]: string } = {
|
const leftPaddingMap: { [key: number]: string } = {
|
||||||
3: '1.5rem',
|
3: '1.5rem',
|
||||||
2: '0.9rem',
|
2: '0.9rem',
|
||||||
1: '0.3',
|
1: '0.3rem',
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HeadingsHighlight = {
|
export type HeadingsHighlight = {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
Doc,
|
Doc,
|
||||||
KEY_LIST_DOC,
|
KEY_LIST_DOC,
|
||||||
ModalRemoveDoc,
|
ModalRemoveDoc,
|
||||||
|
useCopyDocLink,
|
||||||
useCreateFavoriteDoc,
|
useCreateFavoriteDoc,
|
||||||
useDeleteFavoriteDoc,
|
useDeleteFavoriteDoc,
|
||||||
} from '@/features/docs/doc-management';
|
} from '@/features/docs/doc-management';
|
||||||
@@ -20,6 +21,11 @@ export const DocsGridActions = ({
|
|||||||
openShareModal,
|
openShareModal,
|
||||||
}: DocsGridActionsProps) => {
|
}: DocsGridActionsProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const copyDocLink = useCopyDocLink(doc.id);
|
||||||
|
|
||||||
|
const canViewAccesses = doc.abilities.accesses_view;
|
||||||
|
|
||||||
const deleteModal = useModal();
|
const deleteModal = useModal();
|
||||||
|
|
||||||
const removeFavoriteDoc = useDeleteFavoriteDoc({
|
const removeFavoriteDoc = useDeleteFavoriteDoc({
|
||||||
@@ -43,9 +49,16 @@ export const DocsGridActions = ({
|
|||||||
testId: `docs-grid-actions-${doc.is_favorite ? 'unpin' : 'pin'}-${doc.id}`,
|
testId: `docs-grid-actions-${doc.is_favorite ? 'unpin' : 'pin'}-${doc.id}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('Share'),
|
label: canViewAccesses ? t('Share') : t('Copy link'),
|
||||||
icon: 'group',
|
icon: canViewAccesses ? 'group' : 'link',
|
||||||
callback: () => openShareModal?.(),
|
callback: () => {
|
||||||
|
if (canViewAccesses) {
|
||||||
|
openShareModal?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
copyDocLink();
|
||||||
|
},
|
||||||
|
|
||||||
testId: `docs-grid-actions-share-${doc.id}`,
|
testId: `docs-grid-actions-share-${doc.id}`,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { Button, useModal } from '@openfun/cunningham-react';
|
import { useModal } from '@openfun/cunningham-react';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { css } from 'styled-components';
|
import { css } from 'styled-components';
|
||||||
|
|
||||||
import { Box, Icon, StyledLink, Text } from '@/components';
|
import { Box, StyledLink, Text } from '@/components';
|
||||||
import { Doc, LinkReach } from '@/features/docs/doc-management';
|
import { Doc } from '@/features/docs/doc-management';
|
||||||
import { DocShareModal } from '@/features/docs/doc-share';
|
import { DocShareModal } from '@/features/docs/doc-share';
|
||||||
import { useResponsiveStore } from '@/stores';
|
import { useResponsiveStore } from '@/stores';
|
||||||
|
|
||||||
import { DocsGridActions } from './DocsGridActions';
|
import { DocsGridActions } from './DocsGridActions';
|
||||||
|
import { DocsGridItemSharedButton } from './DocsGridItemSharedButton';
|
||||||
import { SimpleDocItem } from './SimpleDocItem';
|
import { SimpleDocItem } from './SimpleDocItem';
|
||||||
|
|
||||||
type DocsGridItemProps = {
|
type DocsGridItemProps = {
|
||||||
@@ -17,11 +18,6 @@ export const DocsGridItem = ({ doc }: DocsGridItemProps) => {
|
|||||||
const { isDesktop } = useResponsiveStore();
|
const { isDesktop } = useResponsiveStore();
|
||||||
|
|
||||||
const shareModal = useModal();
|
const shareModal = useModal();
|
||||||
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 handleShareClick = () => {
|
const handleShareClick = () => {
|
||||||
shareModal.open();
|
shareModal.open();
|
||||||
@@ -70,49 +66,13 @@ export const DocsGridItem = ({ doc }: DocsGridItemProps) => {
|
|||||||
$justify="flex-end"
|
$justify="flex-end"
|
||||||
$gap="32px"
|
$gap="32px"
|
||||||
>
|
>
|
||||||
{isDesktop && isPublic && (
|
{isDesktop && (
|
||||||
<Button
|
<DocsGridItemSharedButton
|
||||||
onClick={(event) => {
|
doc={doc}
|
||||||
event.preventDefault();
|
handleClick={handleShareClick}
|
||||||
event.stopPropagation();
|
/>
|
||||||
handleShareClick();
|
|
||||||
}}
|
|
||||||
size="nano"
|
|
||||||
fullWidth
|
|
||||||
icon={<Icon $variation="000" iconName="public" />}
|
|
||||||
>
|
|
||||||
{isShared ? sharedCount : undefined}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{isDesktop && !isPublic && isRestricted && isShared && (
|
|
||||||
<Button
|
|
||||||
onClick={(event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
handleShareClick();
|
|
||||||
}}
|
|
||||||
fullWidth
|
|
||||||
color="tertiary"
|
|
||||||
size="nano"
|
|
||||||
icon={<Icon $variation="800" $theme="primary" iconName="group" />}
|
|
||||||
>
|
|
||||||
{sharedCount}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{isDesktop && !isPublic && isAuthenticated && (
|
|
||||||
<Button
|
|
||||||
onClick={(event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
handleShareClick();
|
|
||||||
}}
|
|
||||||
fullWidth
|
|
||||||
size="nano"
|
|
||||||
icon={<Icon $variation="000" iconName="corporate_fare" />}
|
|
||||||
>
|
|
||||||
{sharedCount}
|
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DocsGridActions doc={doc} openShareModal={handleShareClick} />
|
<DocsGridActions doc={doc} openShareModal={handleShareClick} />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<Box $align="center" $width="100%">
|
||||||
|
<Icon $variation="800" $theme="primary" iconName={icon} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
handleClick();
|
||||||
|
}}
|
||||||
|
fullWidth
|
||||||
|
color={isRestricted ? 'tertiary' : 'primary'}
|
||||||
|
size="nano"
|
||||||
|
icon={
|
||||||
|
<Icon
|
||||||
|
$variation={isRestricted ? '800' : '000'}
|
||||||
|
$theme={isRestricted ? 'primary' : 'greyscale'}
|
||||||
|
iconName={icon}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isShared ? sharedCount : undefined}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1 +1,2 @@
|
|||||||
export * from './useDate';
|
export * from './useDate';
|
||||||
|
export * from './useClipboard';
|
||||||
|
|||||||
34
src/frontend/apps/impress/src/hook/useClipboard.tsx
Normal file
34
src/frontend/apps/impress/src/hook/useClipboard.tsx
Normal file
@@ -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],
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user