From 0b15ebba711c40ac6456ca8f78a0c8ddd9ee295b Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Thu, 3 Oct 2024 17:21:04 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=82(frontend)=20readers=20and=20editor?= =?UTF-8?q?s=20can=20access=20share=20modal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Readers and editors of a document can access the share modal and see the list of members and their roles. --- CHANGELOG.md | 1 + .../apps/e2e/__tests__/app-impress/common.ts | 87 ++++++++++- .../__tests__/app-impress/doc-header.spec.ts | 140 +++++++++++++++++- .../app-impress/doc-member-list.spec.ts | 6 +- .../apps/impress/src/components/Box.tsx | 2 + .../src/cunningham/cunningham-style.css | 14 ++ .../docs/doc-header/components/DocToolBox.tsx | 16 +- .../components/DocVisibility.tsx | 2 +- .../doc-management/components/ModalShare.tsx | 62 ++++---- .../components/TableContent.tsx | 2 + .../components/InvitationItem.tsx | 45 +++--- .../components/InvitationList.tsx | 9 +- .../members-add/components/AddMembers.tsx | 11 +- .../members-add/components/SearchUsers.tsx | 9 +- .../members-list/components/MemberItem.tsx | 49 +++--- .../members-list/components/MemberList.tsx | 9 +- 16 files changed, 360 insertions(+), 104 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c180151..58877d04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to - 💄(frontend) error alert closeable on editor #284 - ♻️(backend) Change email content #283 +- 🛂(frontend) viewers and editors can access share modal #302 ## Fixed diff --git a/src/frontend/apps/e2e/__tests__/app-impress/common.ts b/src/frontend/apps/e2e/__tests__/app-impress/common.ts index c09bc713..be29d06b 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/common.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/common.ts @@ -140,7 +140,13 @@ export const goToGridDoc = async ( export const mockedDocument = async (page: Page, json: object) => { await page.route('**/documents/**/', async (route) => { const request = route.request(); - if (request.method().includes('GET') && !request.url().includes('page=')) { + if ( + request.method().includes('GET') && + !request.url().includes('page=') && + !request.url().includes('versions') && + !request.url().includes('accesses') && + !request.url().includes('invitations') + ) { await route.fulfill({ json: { id: 'mocked-document-id', @@ -168,3 +174,82 @@ export const mockedDocument = async (page: Page, json: object) => { } }); }; + +export const mockedInvitations = async (page: Page, json?: object) => { + await page.route('**/invitations/**/', async (route) => { + const request = route.request(); + if ( + request.method().includes('GET') && + request.url().includes('invitations') && + request.url().includes('page=') + ) { + await route.fulfill({ + json: { + count: 1, + next: null, + previous: null, + results: [ + { + id: '120ec765-43af-4602-83eb-7f4e1224548a', + abilities: { + destroy: true, + update: true, + partial_update: true, + retrieve: true, + }, + created_at: '2024-10-03T12:19:26.107687Z', + email: 'test@invitation.test', + document: '4888c328-8406-4412-9b0b-c0ba5b9e5fb6', + role: 'editor', + issuer: '7380f42f-02eb-4ad5-b8f0-037a0e66066d', + is_expired: false, + ...json, + }, + ], + }, + }); + } else { + await route.continue(); + } + }); +}; + +export const mockedAccesses = async (page: Page, json?: object) => { + await page.route('**/accesses/**/', async (route) => { + const request = route.request(); + if ( + request.method().includes('GET') && + request.url().includes('accesses') && + request.url().includes('page=') + ) { + await route.fulfill({ + json: { + count: 1, + next: null, + previous: null, + results: [ + { + id: 'bc8bbbc5-a635-4f65-9817-fd1e9ec8ef87', + user: { + id: 'b4a21bb3-722e-426c-9f78-9d190eda641c', + email: 'test@accesses.test', + }, + team: '', + role: 'reader', + abilities: { + destroy: true, + update: true, + partial_update: true, + retrieve: true, + set_role_to: ['administrator', 'editor'], + }, + ...json, + }, + ], + }, + }); + } else { + await route.continue(); + } + }); +}; 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 ced6677f..e2685b61 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 @@ -1,6 +1,12 @@ import { expect, test } from '@playwright/test'; -import { createDoc, goToGridDoc, mockedDocument } from './common'; +import { + createDoc, + goToGridDoc, + mockedAccesses, + mockedDocument, + mockedInvitations, +} from './common'; test.beforeEach(async ({ page }) => { await page.goto('/'); @@ -182,20 +188,55 @@ test.describe('Doc Header', () => { }, }); + await mockedInvitations(page); + await mockedAccesses(page); + await goToGridDoc(page); await expect( page.locator('h2').getByText('Mocked document'), ).toHaveAttribute('contenteditable'); - await expect(page.getByRole('button', { name: 'Share' })).toBeVisible(); - await page.getByLabel('Open the document options').click(); await expect(page.getByRole('button', { name: 'Export' })).toBeVisible(); await expect( page.getByRole('button', { name: 'Delete document' }), ).toBeHidden(); + + // Click somewhere else to close the options + await page.click('body', { position: { x: 0, y: 0 } }); + + await page.getByRole('button', { name: 'Share' }).click(); + + const shareModal = page.getByLabel('Share modal'); + + await expect(shareModal.getByLabel('Doc private')).toBeEnabled(); + await expect(shareModal.getByText('Search by email')).toBeVisible(); + + const invitationCard = shareModal.getByLabel('List invitation card'); + await expect( + invitationCard.getByText('test@invitation.test'), + ).toBeVisible(); + await expect( + invitationCard.getByRole('combobox', { name: 'Role' }), + ).toBeEnabled(); + await expect( + invitationCard.getByRole('button', { + name: 'delete', + }), + ).toBeEnabled(); + + const memberCard = shareModal.getByLabel('List members card'); + await expect(memberCard.getByText('test@accesses.test')).toBeVisible(); + await expect( + memberCard.getByRole('combobox', { name: 'Role' }), + ).toBeEnabled(); + await expect( + memberCard.getByRole('button', { + name: 'delete', + }), + ).toBeEnabled(); }); test('it checks the options available if editor', async ({ page }) => { @@ -213,20 +254,62 @@ test.describe('Doc Header', () => { }, }); + await mockedInvitations(page, { + abilities: { + destroy: false, + update: false, + partial_update: false, + retrieve: true, + }, + }); + await mockedAccesses(page); + await goToGridDoc(page); await expect( page.locator('h2').getByText('Mocked document'), ).toHaveAttribute('contenteditable'); - await expect(page.getByRole('button', { name: 'Share' })).toBeHidden(); - await page.getByLabel('Open the document options').click(); await expect(page.getByRole('button', { name: 'Export' })).toBeVisible(); await expect( page.getByRole('button', { name: 'Delete document' }), ).toBeHidden(); + + // Click somewhere else to close the options + await page.click('body', { position: { x: 0, y: 0 } }); + + await page.getByRole('button', { name: 'Share' }).click(); + + const shareModal = page.getByLabel('Share modal'); + + await expect(shareModal.getByLabel('Doc private')).toBeDisabled(); + await expect(shareModal.getByText('Search by email')).toBeHidden(); + + const invitationCard = shareModal.getByLabel('List invitation card'); + await expect( + invitationCard.getByText('test@invitation.test'), + ).toBeVisible(); + await expect( + invitationCard.getByRole('combobox', { name: 'Role' }), + ).toHaveAttribute('disabled'); + await expect( + invitationCard.getByRole('button', { + name: 'delete', + }), + ).toBeHidden(); + + const memberCard = shareModal.getByLabel('List members card'); + await expect(memberCard.getByText('test@accesses.test')).toBeVisible(); + await expect( + memberCard.getByRole('combobox', { name: 'Role' }), + ).toHaveAttribute('disabled'); + await expect( + memberCard.getByRole('button', { + name: 'delete', + }), + ).toBeHidden(); }); test('it checks the options available if reader', async ({ page }) => { @@ -244,20 +327,61 @@ test.describe('Doc Header', () => { }, }); + await mockedInvitations(page, { + abilities: { + destroy: false, + update: false, + partial_update: false, + retrieve: true, + }, + }); + await mockedAccesses(page); + await goToGridDoc(page); await expect( page.locator('h2').getByText('Mocked document'), ).not.toHaveAttribute('contenteditable'); - await expect(page.getByRole('button', { name: 'Share' })).toBeHidden(); - await page.getByLabel('Open the document options').click(); - await expect(page.getByRole('button', { name: 'Share' })).toBeHidden(); await expect(page.getByRole('button', { name: 'Export' })).toBeVisible(); await expect( page.getByRole('button', { name: 'Delete document' }), ).toBeHidden(); + + // Click somewhere else to close the options + await page.click('body', { position: { x: 0, y: 0 } }); + + await page.getByRole('button', { name: 'Share' }).click(); + + const shareModal = page.getByLabel('Share modal'); + + await expect(shareModal.getByLabel('Doc private')).toBeDisabled(); + await expect(shareModal.getByText('Search by email')).toBeHidden(); + + const invitationCard = shareModal.getByLabel('List invitation card'); + await expect( + invitationCard.getByText('test@invitation.test'), + ).toBeVisible(); + await expect( + invitationCard.getByRole('combobox', { name: 'Role' }), + ).toHaveAttribute('disabled'); + await expect( + invitationCard.getByRole('button', { + name: 'delete', + }), + ).toBeHidden(); + + const memberCard = shareModal.getByLabel('List members card'); + await expect(memberCard.getByText('test@accesses.test')).toBeVisible(); + await expect( + memberCard.getByRole('combobox', { name: 'Role' }), + ).toHaveAttribute('disabled'); + await expect( + memberCard.getByRole('button', { + name: 'delete', + }), + ).toBeHidden(); }); }); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-member-list.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-member-list.spec.ts index 0949a106..bdad423d 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-member-list.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-member-list.spec.ts @@ -106,15 +106,17 @@ test.describe('Document list members', () => { await page.getByRole('option', { name: 'Administrator' }).click(); await expect(page.getByText('The role has been updated')).toBeVisible(); + const shareModal = page.getByLabel('Share modal'); + // Admin still have the right to share - await expect(page.locator('h3').getByText('Share')).toBeVisible(); + await expect(shareModal.getByLabel('Doc private')).toBeEnabled(); await SelectRoleCurrentUser.click(); await page.getByRole('option', { name: 'Reader' }).click(); await expect(page.getByText('The role has been updated')).toBeVisible(); // Reader does not have the right to share - await expect(page.locator('h3').getByText('Share')).toBeHidden(); + await expect(shareModal.getByLabel('Doc private')).toBeDisabled(); }); test('it checks the delete members', async ({ page, browserName }) => { diff --git a/src/frontend/apps/impress/src/components/Box.tsx b/src/frontend/apps/impress/src/components/Box.tsx index 2ff2adae..f5b50acf 100644 --- a/src/frontend/apps/impress/src/components/Box.tsx +++ b/src/frontend/apps/impress/src/components/Box.tsx @@ -33,6 +33,7 @@ export interface BoxProps { $padding?: MarginPadding; $position?: CSSProperties['position']; $radius?: CSSProperties['borderRadius']; + $shrink?: CSSProperties['flexShrink']; $transition?: CSSProperties['transition']; $width?: CSSProperties['width']; $wrap?: CSSProperties['flexWrap']; @@ -68,6 +69,7 @@ export const Box = styled('div')` ${({ $padding }) => $padding && stylesPadding($padding)} ${({ $position }) => $position && `position: ${$position};`} ${({ $radius }) => $radius && `border-radius: ${$radius};`} + ${({ $shrink }) => $shrink && `flex-shrink: ${$shrink};`} ${({ $transition }) => $transition && `transition: ${$transition};`} ${({ $width }) => $width && `width: ${$width};`} ${({ $wrap }) => $wrap && `flex-wrap: ${$wrap};`} diff --git a/src/frontend/apps/impress/src/cunningham/cunningham-style.css b/src/frontend/apps/impress/src/cunningham/cunningham-style.css index fd12415f..69f737ae 100644 --- a/src/frontend/apps/impress/src/cunningham/cunningham-style.css +++ b/src/frontend/apps/impress/src/cunningham/cunningham-style.css @@ -150,6 +150,12 @@ input:-webkit-autofill:focus { border-color: var(--c--components--forms-select--border-color-disabled-hover); } +.c__select--disabled .c__select__wrapper label, +.c__select--disabled .c__select__wrapper input, +.c__select--disabled .c__select__wrapper { + cursor: not-allowed; +} + .c__select__menu__item { transition: all var(--c--theme--transitions--duration) var(--c--theme--transitions--ease-out); @@ -313,6 +319,14 @@ input:-webkit-autofill:focus { font-size: var(--c--components--forms-checkbox--text--size); } +.c__checkbox.c__checkbox--disabled .c__field__text { + color: var(--c--theme--colors--greyscale-600); +} + +.c__switch.c__checkbox--disabled .c__switch__rail { + cursor: not-allowed; +} + /** * Button */ 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 398bf070..df3054ac 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 @@ -31,15 +31,13 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => { $align="center" $gap="1rem" > - {doc.abilities.manage_accesses && ( - - )} + { $align="center" $justify="space-between" > - + { - useEffect(() => { - if (!doc.abilities.manage_accesses) { - onClose(); - } - }, [doc.abilities.manage_accesses, onClose]); - return ( <> @@ -47,29 +40,40 @@ export const ModalShare = ({ onClose, doc }: ModalShareProps) => { width="70vw" $css="min-width: 320px;max-width: 777px;" > - - - share - - - - {t('Share')} - - - {doc.title} - + + + + + share + + + + {t('Share')} + + + {doc.title} + + + + + {doc.abilities.manage_accesses && ( + + )} - - - - - + + + + + ); diff --git a/src/frontend/apps/impress/src/features/docs/doc-table-content/components/TableContent.tsx b/src/frontend/apps/impress/src/features/docs/doc-table-content/components/TableContent.tsx index 426058b9..bbaa6560 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-table-content/components/TableContent.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-table-content/components/TableContent.tsx @@ -93,6 +93,7 @@ export const TableContent = ({ doc, headings }: TableContentProps) => { block: 'start', }); }} + $align="start" > {t('Back to top')} @@ -110,6 +111,7 @@ export const TableContent = ({ doc, headings }: TableContentProps) => { block: 'start', }); }} + $align="start" > {t('Go to bottom')} diff --git a/src/frontend/apps/impress/src/features/docs/members/invitation-list/components/InvitationItem.tsx b/src/frontend/apps/impress/src/features/docs/members/invitation-list/components/InvitationItem.tsx index 678f77a1..12b29ac6 100644 --- a/src/frontend/apps/impress/src/features/docs/members/invitation-list/components/InvitationItem.tsx +++ b/src/frontend/apps/impress/src/features/docs/members/invitation-list/components/InvitationItem.tsx @@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next'; import { Box, IconBG, Text, TextErrors } from '@/components'; import { useCunninghamTheme } from '@/cunningham'; -import { Role } from '@/features/docs/doc-management'; +import { Doc, Role } from '@/features/docs/doc-management'; import { ChooseRole } from '@/features/docs/members/members-add/'; import { useDeleteDocInvitation, useUpdateDocInvitation } from '../api'; @@ -19,11 +19,11 @@ interface InvitationItemProps { role: Role; currentRole: Role; invitation: Invitation; - docId: string; + doc: Doc; } export const InvitationItem = ({ - docId, + doc, role, invitation, currentRole, @@ -95,29 +95,34 @@ export const InvitationItem = ({ setRole={(role) => { setLocalRole(role); updateDocInvitation({ - docId, + docId: doc.id, invitationId: invitation.id, role, }); }} /> -