🛂(frontend) add access request modal on move modal
If a user tries to move a document for which they don't have the right to move, we now display a modal to request access to the owners of the document.
This commit is contained in:
@@ -8,7 +8,12 @@ import {
|
||||
toggleHeaderMenu,
|
||||
verifyDocName,
|
||||
} from './utils-common';
|
||||
import { addNewMember } from './utils-share';
|
||||
import { writeInEditor } from './utils-editor';
|
||||
import {
|
||||
addNewMember,
|
||||
connectOtherUserToDoc,
|
||||
updateShareLink,
|
||||
} from './utils-share';
|
||||
import { createRootSubPage } from './utils-sub-pages';
|
||||
|
||||
test.describe('Doc grid move', () => {
|
||||
@@ -242,6 +247,137 @@ test.describe('Doc grid move', () => {
|
||||
const docTree = page.getByTestId('doc-tree');
|
||||
await expect(docTree.getByText(titleDoc1)).toBeVisible();
|
||||
});
|
||||
|
||||
test('it proposes an access request when moving a doc without sufficient permissions', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
test.slow();
|
||||
await page.goto('/');
|
||||
|
||||
const [titleDoc1] = await createDoc(page, 'Move doc', browserName, 1);
|
||||
|
||||
const { otherPage, cleanup } = await connectOtherUserToDoc({
|
||||
docUrl: '/',
|
||||
browserName,
|
||||
});
|
||||
|
||||
// Another user creates a doc
|
||||
const [titleDoc2] = await createDoc(otherPage, 'Drop doc', browserName, 1);
|
||||
await writeInEditor({
|
||||
page: otherPage,
|
||||
text: 'Hello world',
|
||||
});
|
||||
// Make it public
|
||||
await otherPage.getByRole('button', { name: 'Share' }).click();
|
||||
await updateShareLink(otherPage, 'Public');
|
||||
await otherPage
|
||||
.getByRole('dialog')
|
||||
.getByRole('button', { name: 'close' })
|
||||
.click();
|
||||
const otherPageUrl = otherPage.url();
|
||||
|
||||
// The first user visit the doc to have it in his grid list
|
||||
await page.goto(otherPageUrl);
|
||||
await expect(page.getByText('Hello world')).toBeVisible();
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.getByRole('button', { name: 'Back to homepage' }).click();
|
||||
|
||||
const docsGrid = page.getByTestId('docs-grid');
|
||||
await expect(docsGrid.getByText(titleDoc1)).toBeVisible();
|
||||
await expect(docsGrid.getByText(titleDoc2)).toBeVisible();
|
||||
|
||||
const row = await getGridRow(page, titleDoc1);
|
||||
await row.getByText(`more_horiz`).click();
|
||||
|
||||
await page.getByRole('menuitem', { name: 'Move into a doc' }).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('dialog').getByRole('heading', { name: 'Move' }),
|
||||
).toBeVisible();
|
||||
|
||||
const input = page.getByRole('combobox', { name: 'Quick search input' });
|
||||
await input.click();
|
||||
await input.fill(titleDoc2);
|
||||
|
||||
await expect(page.getByRole('option').getByText(titleDoc2)).toBeVisible();
|
||||
|
||||
// Select the first result
|
||||
await page.keyboard.press('Enter');
|
||||
// The CTA should get the focus
|
||||
await page.keyboard.press('Tab');
|
||||
// Validate the move action
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
// Request access modal should be visible
|
||||
await expect(
|
||||
page
|
||||
.getByRole('dialog')
|
||||
.getByText(
|
||||
'You need edit access to the destination. Request access, then try again.',
|
||||
),
|
||||
).toBeVisible();
|
||||
|
||||
await page
|
||||
.getByRole('dialog')
|
||||
.getByRole('button', { name: 'Request access', exact: true })
|
||||
.first()
|
||||
.click();
|
||||
|
||||
// The other user should receive the access request and be able to approve it
|
||||
await otherPage.getByRole('button', { name: 'Share' }).click();
|
||||
await expect(otherPage.getByText('Access Requests')).toBeVisible();
|
||||
await expect(otherPage.getByText(`E2E ${browserName}`)).toBeVisible();
|
||||
|
||||
const emailRequest = `user.test@${browserName}.test`;
|
||||
await expect(otherPage.getByText(emailRequest)).toBeVisible();
|
||||
const container = otherPage.getByTestId(
|
||||
`doc-share-access-request-row-${emailRequest}`,
|
||||
);
|
||||
await container.getByTestId('doc-role-dropdown').click();
|
||||
await otherPage.getByRole('menuitem', { name: 'Administrator' }).click();
|
||||
await container.getByRole('button', { name: 'Approve' }).click();
|
||||
|
||||
await expect(otherPage.getByText('Access Requests')).toBeHidden();
|
||||
await expect(otherPage.getByText('Share with 2 users')).toBeVisible();
|
||||
await expect(otherPage.getByText(`E2E ${browserName}`)).toBeVisible();
|
||||
|
||||
// The first user should now be able to move the doc
|
||||
await page.reload();
|
||||
await row.getByText(`more_horiz`).click();
|
||||
|
||||
await page.getByRole('menuitem', { name: 'Move into a doc' }).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('dialog').getByRole('heading', { name: 'Move' }),
|
||||
).toBeVisible();
|
||||
|
||||
await input.click();
|
||||
await input.fill(titleDoc2);
|
||||
|
||||
await expect(page.getByRole('option').getByText(titleDoc2)).toBeVisible();
|
||||
|
||||
// Select the first result
|
||||
await page.keyboard.press('Enter');
|
||||
// The CTA should get the focus
|
||||
await page.keyboard.press('Tab');
|
||||
// Validate the move action
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await expect(docsGrid.getByText(titleDoc1)).toBeHidden();
|
||||
await docsGrid
|
||||
.getByRole('link', { name: `Open document ${titleDoc2}` })
|
||||
.click();
|
||||
|
||||
await verifyDocName(page, titleDoc2);
|
||||
|
||||
const docTree = page.getByTestId('doc-tree');
|
||||
await expect(docTree.getByText(titleDoc1)).toBeVisible();
|
||||
|
||||
await cleanup();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Doc grid dnd mobile', () => {
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { Button, Modal, ModalSize } from '@gouvfr-lasuite/cunningham-react';
|
||||
import {
|
||||
Button,
|
||||
ButtonProps,
|
||||
Modal,
|
||||
ModalProps,
|
||||
ModalSize,
|
||||
} from '@gouvfr-lasuite/cunningham-react';
|
||||
import { ReactNode, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -10,10 +16,11 @@ export type AlertModalProps = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
themeCTA?: ButtonProps['color'];
|
||||
title: string;
|
||||
cancelLabel?: string;
|
||||
confirmLabel?: string;
|
||||
};
|
||||
} & Partial<ModalProps>;
|
||||
|
||||
export const AlertModal = ({
|
||||
cancelLabel,
|
||||
@@ -23,6 +30,8 @@ export const AlertModal = ({
|
||||
onClose,
|
||||
onConfirm,
|
||||
title,
|
||||
themeCTA,
|
||||
...props
|
||||
}: AlertModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -71,13 +80,14 @@ export const AlertModal = ({
|
||||
</Button>
|
||||
<Button
|
||||
aria-label={confirmLabel ?? t('Confirm')}
|
||||
color="error"
|
||||
color={themeCTA ?? 'error'}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
{confirmLabel ?? t('Confirm')}
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
<Box className="--docs--alert-modal">
|
||||
<Box>
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { Button } from '@gouvfr-lasuite/cunningham-react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import { AlertModal, Box, Icon, Text } from '@/components';
|
||||
|
||||
import { useDocAccessRequests } from '../api/useDocAccessRequest';
|
||||
|
||||
import { ButtonAccessRequest } from './DocShareAccessRequest';
|
||||
|
||||
interface AlertModalRequestAccessProps {
|
||||
docId: string;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
targetDocumentTitle: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const AlertModalRequestAccess = ({
|
||||
docId,
|
||||
isOpen,
|
||||
onClose,
|
||||
onConfirm,
|
||||
targetDocumentTitle,
|
||||
title,
|
||||
}: AlertModalRequestAccessProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { data: requests } = useDocAccessRequests({
|
||||
docId,
|
||||
page: 1,
|
||||
});
|
||||
|
||||
const hasRequested = !!(
|
||||
requests && requests?.results.find((request) => request.document === docId)
|
||||
);
|
||||
|
||||
return (
|
||||
<AlertModal
|
||||
onClose={onClose}
|
||||
isOpen={isOpen}
|
||||
title={title}
|
||||
aria-label={t('Request access modal')}
|
||||
description={
|
||||
<>
|
||||
<Text $display="inline">
|
||||
<Trans
|
||||
i18nKey="You don't have permission to move this document to <strong>{{targetDocumentTitle}}</strong>. You need edit access to the destination. Request access, then try again."
|
||||
values={{
|
||||
targetDocumentTitle,
|
||||
}}
|
||||
components={{ strong: <strong /> }}
|
||||
/>
|
||||
</Text>
|
||||
{hasRequested && (
|
||||
<Text
|
||||
$weight="bold"
|
||||
$margin={{ top: 'sm' }}
|
||||
$direction="row"
|
||||
$align="center"
|
||||
>
|
||||
<Icon
|
||||
iconName="person_check"
|
||||
$margin={{ right: 'xxs' }}
|
||||
variant="symbols-outlined"
|
||||
/>
|
||||
{t('You have already requested access to this document.')}
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
confirmLabel={t('Request access')}
|
||||
onConfirm={onConfirm}
|
||||
rightActions={
|
||||
<Box $direction="row" $gap="small">
|
||||
<Button
|
||||
aria-label={t('Cancel')}
|
||||
variant="secondary"
|
||||
fullWidth
|
||||
onClick={onClose}
|
||||
autoFocus
|
||||
>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
<ButtonAccessRequest docId={docId} onClick={onConfirm} />
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
VariantType,
|
||||
useToastProvider,
|
||||
} from '@gouvfr-lasuite/cunningham-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { MouseEventHandler, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { createGlobalStyle } from 'styled-components';
|
||||
|
||||
@@ -167,10 +167,13 @@ export const QuickSearchGroupAccessRequest = ({
|
||||
|
||||
type ButtonAccessRequestProps = {
|
||||
docId: Doc['id'];
|
||||
} & ButtonProps;
|
||||
} & Omit<ButtonProps, 'onClick'> & {
|
||||
onClick?: MouseEventHandler<HTMLButtonElement | HTMLAnchorElement>;
|
||||
};
|
||||
|
||||
export const ButtonAccessRequest = ({
|
||||
docId,
|
||||
onClick,
|
||||
...buttonProps
|
||||
}: ButtonAccessRequestProps) => {
|
||||
const { authenticated } = useAuth();
|
||||
@@ -216,7 +219,10 @@ export const ButtonAccessRequest = ({
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={() => createRequest({ docId })}
|
||||
onClick={(e) => {
|
||||
createRequest({ docId });
|
||||
onClick?.(e);
|
||||
}}
|
||||
disabled={hasRequested}
|
||||
{...buttonProps}
|
||||
>
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './AlertModalRequestAccess';
|
||||
export * from './DocShareModal';
|
||||
export * from './DocShareAccessRequest';
|
||||
|
||||
@@ -16,6 +16,7 @@ import { QuickSearch } from '@/components/quick-search';
|
||||
import { Doc, useMoveDoc, useTrans } from '@/docs/doc-management';
|
||||
import { DocSearchContent, DocSearchTarget } from '@/docs/doc-search';
|
||||
import EmptySearchIcon from '@/docs/doc-search/assets/illustration-docs-empty.png';
|
||||
import { AlertModalRequestAccess } from '@/docs/doc-share';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { DocsGridItemDate, DocsGridItemTitle } from './DocsGridItem';
|
||||
@@ -75,6 +76,7 @@ export const DocMoveModal = ({
|
||||
const docTitle = doc.title || untitledDocument;
|
||||
const docTargetTitle = docSelected?.title || untitledDocument;
|
||||
const modalConfirmation = useModal();
|
||||
const modalRequest = useModal();
|
||||
const { mutate: moveDoc } = useMoveDoc(true);
|
||||
const [search, setSearch] = useState('');
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
@@ -114,6 +116,11 @@ export const DocMoveModal = ({
|
||||
variant="primary"
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
if (!docSelected?.abilities.move) {
|
||||
modalRequest.open();
|
||||
return;
|
||||
}
|
||||
|
||||
if (doc.nb_accesses_direct > 1) {
|
||||
modalConfirmation.open();
|
||||
return;
|
||||
@@ -212,9 +219,7 @@ export const DocMoveModal = ({
|
||||
<DocSearchContent
|
||||
search={search}
|
||||
filters={{ target: DocSearchTarget.ALL }}
|
||||
filterResults={(docResults) =>
|
||||
docResults.id !== doc.id && docResults.abilities.move
|
||||
}
|
||||
filterResults={(docResults) => docResults.id !== doc.id}
|
||||
onSelect={handleSelect}
|
||||
onLoadingChange={setLoading}
|
||||
renderSearchElement={(docSearch) => {
|
||||
@@ -277,6 +282,19 @@ export const DocMoveModal = ({
|
||||
targetDocumentTitle={docTargetTitle}
|
||||
/>
|
||||
)}
|
||||
{modalRequest.isOpen && docSelected?.id && (
|
||||
<AlertModalRequestAccess
|
||||
docId={docSelected.id}
|
||||
isOpen={modalRequest.isOpen}
|
||||
onClose={modalRequest.onClose}
|
||||
onConfirm={() => {
|
||||
modalRequest.onClose();
|
||||
onClose();
|
||||
}}
|
||||
targetDocumentTitle={docTargetTitle}
|
||||
title={t('Move document')}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user