Skip to content

Commit

Permalink
🐛(frontend) hide the sharing method when you don't have the rights
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
PanchoutNathan committed Jan 15, 2025
1 parent de8dea2 commit 601099a
Show file tree
Hide file tree
Showing 14 changed files with 268 additions and 119 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
47 changes: 47 additions & 0 deletions src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -396,6 +397,28 @@ test.describe('Doc Header', () => {
const clipboardContent = await handle.jsonValue();
expect(clipboardContent.trim()).toBe(`<h1>Hello World</h1><p></p>`);
});

test('it checks the copy link button', async ({ page }) => {
await mockedDocument(page, {
abilities: {
destroy: true, // 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('Copied to clipboard')).toBeVisible();
});
});

test.describe('Documents Header mobile', () => {
Expand All @@ -405,6 +428,29 @@ test.describe('Documents Header mobile', () => {
await page.goto('/');
});

test('it checks the copy link button', async ({ page }) => {
await mockedDocument(page, {
abilities: {
destroy: true, // 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.getByLabel('Open the document options').click();
await page.getByRole('button', { name: 'Copy link' }).click();
await expect(page.getByText('Copied to clipboard')).toBeVisible();
});

test('it checks the close button on Share modal', async ({ page }) => {
await mockedDocument(page, {
abilities: {
Expand All @@ -414,6 +460,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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
20 changes: 4 additions & 16 deletions src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ({
Expand Down Expand Up @@ -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();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ import {
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,
Expand All @@ -37,6 +41,8 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
const { t } = useTranslation();
const hasAccesses = doc.nb_accesses > 1;
const queryClient = useQueryClient();

const { copyDocLink } = useCopyDocLink(doc);
const { spacingsTokens, colorsTokens } = useCunninghamTheme();

const spacings = spacingsTokens();
Expand All @@ -50,16 +56,23 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
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();
},
},
{
Expand Down Expand Up @@ -153,7 +166,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
$margin={{ left: 'auto' }}
$gap={spacings['2xs']}
>
{authenticated && !isSmallMobile && (
{canViewAccesses && authenticated && !isSmallMobile && (
<>
{!hasAccesses && (
<Button
Expand Down Expand Up @@ -187,12 +200,23 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
}}
size={isSmallMobile ? 'small' : 'medium'}
>
{doc.nb_accesses}
{doc.nb_accesses - 1}
</Button>
</Box>
)}
</>
)}
{!canViewAccesses && !isSmallMobile && (
<Button
color="tertiary-text"
onClick={() => {
copyDocLink();
}}
size={isSmallMobile ? 'small' : 'medium'}
>
{t('Copy link')}
</Button>
)}
{!isSmallMobile && (
<Button
color="tertiary-text"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './useCollaboration';
export * from './useTrans';
export * from './useCopyDocLink';
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useTranslation } from 'react-i18next';

import { useClipboard } from '@/hook';

import { Doc } from '../types';

export const useCopyDocLink = (doc: Doc) => {
const { t } = useTranslation();
const { copyToClipboard } = useClipboard();

const copyDocLink = () => {
copyToClipboard(
`${window.location.origin}/docs/${doc.id}/`,
t('Link Copied !'),
t('Failed to copy link'),
);
};
return { copyDocLink };
};
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,9 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
const [userQuery, setUserQuery] = useState('');
const [inputValue, setInputValue] = useState('');

const [listHeight, setListHeight] = useState<string>('400px');
const [listHeight, setListHeight] = useState<string>('0px');
const canShare = doc.abilities.accesses_manage;
const canViewAccesses = doc.abilities.accesses_view;
const showMemberSection = inputValue === '' && selectedUsers.length === 0;
const showFooter = selectedUsers.length === 0 && !inputValue;

Expand Down Expand Up @@ -94,7 +95,7 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
count === 1
? t('Document owner')
: t('Share with {{count}} users', {
count,
count: count - 1,
}),
elements: members,
endActions: membersQuery.hasNextPage
Expand Down Expand Up @@ -168,6 +169,10 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
};

const handleRef = (node: HTMLDivElement) => {
if (!canViewAccesses) {
setListHeight('0px');
return;
}
const inputHeight = canShare ? 70 : 0;
const marginTop = 11;
const footerHeight = node?.clientHeight ?? 0;
Expand All @@ -191,7 +196,7 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
<ShareModalStyle />
<Box
aria-label={t('Share modal')}
$height={modalContentHeight}
$height={canViewAccesses ? modalContentHeight : 'auto'}
$overflow="hidden"
$justify="space-between"
>
Expand Down Expand Up @@ -235,39 +240,43 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
loading={searchUsersQuery.isLoading}
placeholder={t('Type a name or email')}
>
{!showMemberSection && inputValue !== '' && (
<QuickSearchGroup
group={searchUserData}
onSelect={onSelect}
renderElement={(user) => (
<DocShareModalInviteUserRow user={user} />
)}
/>
)}
{showMemberSection && (
{canViewAccesses && (
<>
{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')}>
{!showMemberSection && inputValue !== '' && (
<QuickSearchGroup
group={membersData}
renderElement={(access) => (
<DocShareMemberItem doc={doc} access={access} />
group={searchUserData}
onSelect={onSelect}
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>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import {
Button,
VariantType,
useToastProvider,
} from '@openfun/cunningham-react';
import { Button } from '@openfun/cunningham-react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';

import { Box, HorizontalSeparator } from '@/components';
import { Doc } from '@/features/docs';
import { Doc, useCopyDocLink } from '@/features/docs';

import { DocVisibility } from './DocVisibility';

Expand All @@ -18,7 +14,8 @@ type Props = {

export const DocShareModalFooter = ({ doc, onClose }: Props) => {
const canShare = doc.abilities.accesses_manage;
const { toast } = useToastProvider();

const { copyDocLink } = useCopyDocLink(doc);
const { t } = useTranslation();
return (
<Box
Expand All @@ -41,18 +38,7 @@ export const DocShareModalFooter = ({ doc, onClose }: Props) => {
<Button
fullWidth={false}
onClick={() => {
navigator.clipboard
.writeText(window.location.href)
.then(() => {
toast(t('Link Copied !'), VariantType.SUCCESS, {
duration: 3000,
});
})
.catch(() => {
toast(t('Failed to copy link'), VariantType.ERROR, {
duration: 3000,
});
});
copyDocLink();
}}
color="tertiary"
icon={<span className="material-icons">add_link</span>}
Expand Down
Loading

0 comments on commit 601099a

Please sign in to comment.