diff --git a/src/frontend/apps/e2e/__tests__/app-impress/pad-member-grid.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/pad-member-grid.spec.ts new file mode 100644 index 00000000..b03192ca --- /dev/null +++ b/src/frontend/apps/e2e/__tests__/app-impress/pad-member-grid.spec.ts @@ -0,0 +1,114 @@ +import { expect, test } from '@playwright/test'; + +import { createPad, keyCloakSignIn } from './common'; + +test.beforeEach(async ({ page, browserName }) => { + await page.goto('/'); + await keyCloakSignIn(page, browserName); +}); + +test.describe('Document grid members', () => { + test('it display the grid', async ({ page, browserName }) => { + await createPad(page, 'grid-display', browserName, 1); + + await page.getByLabel('Open the document options').click(); + await page.getByRole('button', { name: 'Manage members' }).click(); + + await expect(page.getByText('Members of the document')).toBeVisible(); + const table = page.getByLabel('List members card').getByRole('table'); + const thead = table.locator('thead'); + await expect(thead.getByText(/Emails/i)).toBeVisible(); + await expect(thead.getByText(/Roles/i)).toBeVisible(); + + const cells = table.getByRole('row').nth(1).getByRole('cell'); + await expect(cells.nth(0)).toHaveText(`user@${browserName}.e2e`); + await expect(cells.nth(1)).toHaveText(/Owner/i); + await expect(cells.nth(2)).toHaveAccessibleName( + 'Open the member options modal', + ); + }); + + test('it display the grid with many members', async ({ + page, + browserName, + }) => { + await page.route('**/documents/*/', async (route) => { + const request = route.request(); + if ( + request.method().includes('GET') && + !request.url().includes('page=') + ) { + await route.fulfill({ + json: { + id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg', + content: '', + title: 'Mocked document', + accesses: [], + abilities: { + destroy: true, + manage_accesses: true, + partial_update: true, + }, + is_public: false, + }, + }); + } else { + await route.continue(); + } + }); + + await page.route( + '**/documents/b0df4343-c8bd-4c20-9ff6-fbf94fc94egg/accesses/?page=*', + async (route) => { + const request = route.request(); + const url = new URL(request.url()); + const pageId = url.searchParams.get('page'); + const accesses = { + count: 100, + next: null, + previous: null, + results: Array.from({ length: 20 }, (_, i) => ({ + id: `2ff1ec07-86c1-4534-a643-f41824a6c53a-${pageId}-${i}`, + user: { + id: `fc092149-cafa-4ffa-a29d-e4b18af751-${pageId}-${i}`, + email: `impress@impress.world-page-${pageId}-${i}`, + }, + team: '', + role: 'owner', + abilities: { + destroy: false, + partial_update: true, + }, + })), + }; + + if (request.method().includes('GET')) { + await route.fulfill({ + json: accesses, + }); + } else { + await route.continue(); + } + }, + ); + + await createPad(page, 'grid-no-member', browserName, 1); + + await page.getByLabel('Open the document options').click(); + await page.getByRole('button', { name: 'Manage members' }).click(); + + await expect( + page.getByText('impress@impress.world-page-1-19'), + ).toBeVisible(); + + await page.getByLabel('Go to page 4').click(); + + await expect( + page.getByText('impress@impress.world-page-1-19'), + ).toBeHidden(); + + await expect( + page.getByText('impress@impress.world-page-4-19'), + ).toBeVisible(); + }); +}); diff --git a/src/frontend/apps/impress/src/cunningham/cunningham-style.css b/src/frontend/apps/impress/src/cunningham/cunningham-style.css index bcfa7dc3..ddf9ec49 100644 --- a/src/frontend/apps/impress/src/cunningham/cunningham-style.css +++ b/src/frontend/apps/impress/src/cunningham/cunningham-style.css @@ -233,7 +233,7 @@ input:-webkit-autofill:focus { } .c__datagrid > .c__pagination { - padding-right: 1rem; + padding-inline: 1rem; justify-content: flex-end; } @@ -464,3 +464,14 @@ input:-webkit-autofill:focus { .c__modal__close .c__button--tertiary-text:focus-visible { box-shadow: none; } + +.c__modal__close button { + padding: 1.5rem 1rem; +} + +/** + * Toast +*/ +.c__toast__container { + z-index: 10000; +} diff --git a/src/frontend/apps/impress/src/features/pads/members/members-add/api/useCreateDocAccess.tsx b/src/frontend/apps/impress/src/features/pads/members/members-add/api/useCreateDocAccess.tsx index 71590ebb..8f301754 100644 --- a/src/frontend/apps/impress/src/features/pads/members/members-add/api/useCreateDocAccess.tsx +++ b/src/frontend/apps/impress/src/features/pads/members/members-add/api/useCreateDocAccess.tsx @@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { APIError, errorCauses, fetchAPI } from '@/api'; import { User } from '@/core/auth'; +import { KEY_LIST_DOC_ACCESSES } from '@/features/pads/members/members-grid/'; import { Access, KEY_LIST_PAD, @@ -55,6 +56,9 @@ export function useCreateDocAccess() { void queryClient.resetQueries({ queryKey: [KEY_LIST_USER], }); + void queryClient.resetQueries({ + queryKey: [KEY_LIST_DOC_ACCESSES], + }); }, }); } diff --git a/src/frontend/apps/impress/src/features/pads/members/members-grid/__tests__/MemberGrid.test.tsx b/src/frontend/apps/impress/src/features/pads/members/members-grid/__tests__/MemberGrid.test.tsx new file mode 100644 index 00000000..af9e1a36 --- /dev/null +++ b/src/frontend/apps/impress/src/features/pads/members/members-grid/__tests__/MemberGrid.test.tsx @@ -0,0 +1,396 @@ +import '@testing-library/jest-dom'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import fetchMock from 'fetch-mock'; + +import { Access, Pad, Role } from '@/features/pads/pad-management'; +import { AppWrapper } from '@/tests/utils'; + +import { MemberGrid } from '../components/MemberGrid'; + +const doc: Pad = { + id: '123456', + title: 'teamName', + abilities: { + destroy: true, + manage_accesses: true, + partial_update: true, + retrieve: true, + update: true, + versions_destroy: true, + versions_list: true, + versions_retrieve: true, + }, + content: 'content', + is_public: false, + accesses: [], + created_at: '2021-09-01T12:00:00Z', + updated_at: '2021-09-01T12:00:00Z', +}; + +describe('MemberGrid', () => { + afterEach(() => { + fetchMock.restore(); + }); + + it('renders with no member to display', async () => { + fetchMock.mock(`end:/documents/123456/accesses/?page=1`, { + count: 0, + results: [], + }); + + render(, { + wrapper: AppWrapper, + }); + + expect(screen.getByRole('status')).toBeInTheDocument(); + + expect(await screen.findByRole('img')).toHaveAttribute( + 'alt', + 'Illustration of an empty table', + ); + + expect(screen.getByText('This table is empty')).toBeInTheDocument(); + }); + + it('checks the render with members', async () => { + const accesses: Access[] = [ + { + id: '1', + role: Role.OWNER, + team: '123456', + user: { + id: '11', + email: 'user1@test.com', + }, + abilities: {} as any, + }, + { + id: '2', + team: '123456', + role: Role.EDITOR, + user: { + id: '22', + email: 'user2@test.com', + }, + abilities: {} as any, + }, + { + id: '3', + team: '123456', + role: Role.READER, + user: { + id: '33', + email: 'user3@test.com', + }, + abilities: {} as any, + }, + { + id: '4', + role: Role.ADMIN, + team: '123456', + user: { + id: '44', + email: 'user4@test.com', + }, + abilities: {} as any, + }, + ]; + + fetchMock.mock(`end:/documents/123456/accesses/?page=1`, { + count: 3, + results: accesses, + }); + + render(, { + wrapper: AppWrapper, + }); + + expect(screen.getByRole('status')).toBeInTheDocument(); + + expect(await screen.findByText('user1@test.com')).toBeInTheDocument(); + expect(screen.getByText('user2@test.com')).toBeInTheDocument(); + expect(screen.getByText('user3@test.com')).toBeInTheDocument(); + expect(screen.getByText('user4@test.com')).toBeInTheDocument(); + expect(screen.getByText('Owner')).toBeInTheDocument(); + expect(screen.getByText('Administrator')).toBeInTheDocument(); + expect(screen.getByText('Reader')).toBeInTheDocument(); + expect(screen.getByText('Editor')).toBeInTheDocument(); + }); + + it('checks the pagination', async () => { + const regexp = new RegExp(/.*\/documents\/123456\/accesses\/\?page=.*/); + fetchMock.get(regexp, { + count: 40, + results: Array.from({ length: 20 }, (_, i) => ({ + id: i, + role: Role.OWNER, + user: { + id: i, + email: `user${i}@test.com`, + }, + abilities: {} as any, + })), + }); + + render(, { + wrapper: AppWrapper, + }); + + expect(screen.getByRole('status')).toBeInTheDocument(); + + expect(fetchMock.lastUrl()).toContain('/documents/123456/accesses/?page=1'); + + expect( + await screen.findByLabelText('You are currently on page 1'), + ).toBeInTheDocument(); + + await userEvent.click(screen.getByLabelText('Go to page 2')); + + expect( + await screen.findByLabelText('You are currently on page 2'), + ).toBeInTheDocument(); + + expect(fetchMock.lastUrl()).toContain('/documents/123456/accesses/?page=2'); + }); + + [ + { + role: Role.OWNER, + expected: true, + }, + { + role: Role.EDITOR, + expected: false, + }, + { + role: Role.READER, + expected: false, + }, + { + role: Role.ADMIN, + expected: true, + }, + ].forEach(({ role, expected }) => { + it(`checks action button when ${role}`, async () => { + const regexp = new RegExp(/.*\/documents\/123456\/accesses\/\?page=.*/); + fetchMock.get(regexp, { + count: 1, + results: [ + { + id: 1, + role: Role.ADMIN, + user: { + id: 1, + email: `user1@test.com`, + }, + abilities: {} as any, + }, + ], + }); + + render(, { + wrapper: AppWrapper, + }); + + expect(screen.getByRole('status')).toBeInTheDocument(); + + /* eslint-disable jest/no-conditional-expect */ + if (expected) { + expect( + await screen.findAllByRole('button', { + name: 'Open the member options modal', + }), + ).toBeDefined(); + } else { + expect( + screen.queryByRole('button', { + name: 'Open the member options modal', + }), + ).not.toBeInTheDocument(); + } + /* eslint-enable jest/no-conditional-expect */ + }); + }); + + it('controls the render when api error', async () => { + fetchMock.mock(`end:/documents/123456/accesses/?page=1`, { + status: 500, + body: { + cause: 'All broken :(', + }, + }); + + render(, { + wrapper: AppWrapper, + }); + + expect(screen.getByRole('status')).toBeInTheDocument(); + + expect(await screen.findByText('All broken :(')).toBeInTheDocument(); + }); + + [ + { + ordering: 'email', + header_name: 'Emails', + }, + { + ordering: 'role', + header_name: 'Roles', + }, + ].forEach(({ ordering, header_name }) => { + it(`checks the sorting per ${ordering}`, async () => { + const mockedData = [ + { + id: '123', + role: Role.ADMIN, + user: { + id: '123', + email: 'albert@test.com', + }, + abilities: {} as any, + }, + { + id: '789', + role: Role.OWNER, + user: { + id: '456', + email: 'philipp@test.com', + }, + abilities: {} as any, + }, + { + id: '456', + role: Role.READER, + user: { + id: '789', + email: 'fany@test.com', + }, + abilities: {} as any, + }, + { + id: '963', + role: Role.EDITOR, + user: { + id: '4548', + email: 'gege@test.com', + }, + abilities: {} as any, + }, + ]; + + const sortedMockedData = [...mockedData].sort((a, b) => { + if (ordering === 'email') { + return a.user.email > b.user.email ? 1 : -1; + } + + return a.role > b.role ? 1 : -1; + }); + const reversedMockedData = [...sortedMockedData].reverse(); + + fetchMock.get(`end:/documents/123456/accesses/?page=1`, { + count: 4, + results: mockedData, + }); + + fetchMock.get( + `end:/documents/123456/accesses/?page=1&ordering=${ordering}`, + { + count: 4, + results: sortedMockedData, + }, + ); + + fetchMock.get( + `end:/documents/123456/accesses/?page=1&ordering=-${ordering}`, + { + count: 4, + results: reversedMockedData, + }, + ); + + render(, { + wrapper: AppWrapper, + }); + + expect(screen.getByRole('status')).toBeInTheDocument(); + + expect(fetchMock.lastUrl()).toContain( + `/documents/123456/accesses/?page=1`, + ); + + await waitFor(() => { + expect(screen.queryByRole('status')).not.toBeInTheDocument(); + }); + + let rows = screen.getAllByRole('row'); + expect(rows[1]).toHaveTextContent('albert'); + expect(rows[2]).toHaveTextContent('philipp'); + expect(rows[3]).toHaveTextContent('fany'); + expect(rows[4]).toHaveTextContent('gege'); + + expect( + screen.queryByLabelText('arrow_drop_down'), + ).not.toBeInTheDocument(); + expect(screen.queryByLabelText('arrow_drop_up')).not.toBeInTheDocument(); + + await userEvent.click(screen.getByText(header_name)); + + expect(fetchMock.lastUrl()).toContain( + `/documents/123456/accesses/?page=1&ordering=${ordering}`, + ); + + await waitFor(() => { + expect(screen.queryByRole('status')).not.toBeInTheDocument(); + }); + + rows = screen.getAllByRole('row'); + + expect(rows[1]).toHaveTextContent(sortedMockedData[0].user.email); + expect(rows[2]).toHaveTextContent(sortedMockedData[1].user.email); + expect(rows[3]).toHaveTextContent(sortedMockedData[2].user.email); + expect(rows[4]).toHaveTextContent(sortedMockedData[3].user.email); + + expect(await screen.findByText('arrow_drop_up')).toBeInTheDocument(); + + await userEvent.click(screen.getByText(header_name)); + + expect(fetchMock.lastUrl()).toContain( + `/documents/123456/accesses/?page=1&ordering=-${ordering}`, + ); + await waitFor(() => { + expect(screen.queryByRole('status')).not.toBeInTheDocument(); + }); + + rows = screen.getAllByRole('row'); + expect(rows[1]).toHaveTextContent(reversedMockedData[0].user.email); + expect(rows[2]).toHaveTextContent(reversedMockedData[1].user.email); + expect(rows[3]).toHaveTextContent(reversedMockedData[2].user.email); + expect(rows[4]).toHaveTextContent(reversedMockedData[3].user.email); + + expect(await screen.findByText('arrow_drop_down')).toBeInTheDocument(); + + await userEvent.click(screen.getByText(header_name)); + + expect(fetchMock.lastUrl()).toContain( + '/documents/123456/accesses/?page=1', + ); + + await waitFor(() => { + expect(screen.queryByRole('status')).not.toBeInTheDocument(); + }); + + rows = screen.getAllByRole('row'); + expect(rows[1]).toHaveTextContent('albert'); + expect(rows[2]).toHaveTextContent('philipp'); + expect(rows[3]).toHaveTextContent('fany'); + expect(rows[4]).toHaveTextContent('gege'); + + expect( + screen.queryByLabelText('arrow_drop_down'), + ).not.toBeInTheDocument(); + expect(screen.queryByLabelText('arrow_drop_up')).not.toBeInTheDocument(); + }); + }); +}); diff --git a/src/frontend/apps/impress/src/features/pads/members/members-grid/api/index.ts b/src/frontend/apps/impress/src/features/pads/members/members-grid/api/index.ts new file mode 100644 index 00000000..385fe981 --- /dev/null +++ b/src/frontend/apps/impress/src/features/pads/members/members-grid/api/index.ts @@ -0,0 +1,3 @@ +export * from './useDeleteDocAccess'; +export * from './useDocAccesses'; +export * from './useUpdateDocAccess'; diff --git a/src/frontend/apps/impress/src/features/pads/members/members-grid/api/useDocAccesses.tsx b/src/frontend/apps/impress/src/features/pads/members/members-grid/api/useDocAccesses.tsx new file mode 100644 index 00000000..ec63c995 --- /dev/null +++ b/src/frontend/apps/impress/src/features/pads/members/members-grid/api/useDocAccesses.tsx @@ -0,0 +1,48 @@ +import { UseQueryOptions, useQuery } from '@tanstack/react-query'; + +import { APIError, APIList, errorCauses, fetchAPI } from '@/api'; +import { Access } from '@/features/pads/pad-management'; + +export type DocAccessesAPIParams = { + page: number; + docId: string; + ordering?: string; +}; + +type AccessesResponse = APIList; + +export const getDocAccesses = async ({ + page, + docId, + ordering, +}: DocAccessesAPIParams): Promise => { + let url = `documents/${docId}/accesses/?page=${page}`; + + if (ordering) { + url += '&ordering=' + ordering; + } + + const response = await fetchAPI(url); + + if (!response.ok) { + throw new APIError( + 'Failed to get the doc accesses', + await errorCauses(response), + ); + } + + return response.json() as Promise; +}; + +export const KEY_LIST_DOC_ACCESSES = 'docs-accesses'; + +export function useDocAccesses( + params: DocAccessesAPIParams, + queryConfig?: UseQueryOptions, +) { + return useQuery({ + queryKey: [KEY_LIST_DOC_ACCESSES, params], + queryFn: () => getDocAccesses(params), + ...queryConfig, + }); +} diff --git a/src/frontend/apps/impress/src/features/pads/members/members-grid/components/MemberGrid.tsx b/src/frontend/apps/impress/src/features/pads/members/members-grid/components/MemberGrid.tsx new file mode 100644 index 00000000..4b773f6b --- /dev/null +++ b/src/frontend/apps/impress/src/features/pads/members/members-grid/components/MemberGrid.tsx @@ -0,0 +1,129 @@ +import { DataGrid, SortModel, usePagination } from '@openfun/cunningham-react'; +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { Card, TextErrors } from '@/components'; +import { Pad, Role, currentDocRole } from '@/features/pads/pad-management'; + +import { useDocAccesses } from '../api'; +import { PAGE_SIZE } from '../conf'; + +import { MemberAction } from './MemberAction'; + +interface MemberGridProps { + doc: Pad; +} + +// FIXME : ask Cunningham to export this type +type SortModelItem = { + field: string; + sort: 'asc' | 'desc' | null; +}; + +const defaultOrderingMapping: Record = { + 'user.name': 'name', + 'user.email': 'email', + localizedRole: 'role', +}; + +/** + * Formats the sorting model based on a given mapping. + * @param {SortModelItem} sortModel The sorting model item containing field and sort direction. + * @param {Record} mapping The mapping object to map field names. + * @returns {string} The formatted sorting string. + */ +function formatSortModel( + sortModel: SortModelItem, + mapping = defaultOrderingMapping, +) { + const { field, sort } = sortModel; + const orderingField = mapping[field] || field; + return sort === 'desc' ? `-${orderingField}` : orderingField; +} + +export const MemberGrid = ({ doc }: MemberGridProps) => { + const { t } = useTranslation(); + const pagination = usePagination({ + pageSize: PAGE_SIZE, + }); + const [sortModel, setSortModel] = useState([]); + const { page, pageSize, setPagesCount } = pagination; + + const ordering = sortModel.length ? formatSortModel(sortModel[0]) : undefined; + + const { data, isLoading, error } = useDocAccesses({ + docId: doc.id, + page, + ordering, + }); + + const translatedRoles = { + [Role.ADMIN]: t('Administrator'), + [Role.READER]: t('Reader'), + [Role.OWNER]: t('Owner'), + [Role.EDITOR]: t('Editor'), + }; + + /* + * Bug occurs from the Cunningham Datagrid component, when applying sorting + * on null values. Sanitize empty values to ensure consistent sorting functionality. + */ + const accesses = + data?.results?.map((access) => ({ + ...access, + localizedRole: translatedRoles[access.role], + email: access.user.email, + })) || []; + + useEffect(() => { + setPagesCount(data?.count ? Math.ceil(data.count / pageSize) : 0); + }, [data?.count, pageSize, setPagesCount]); + + return ( + + {error && } + + { + return ( + + ); + }, + }, + ]} + rows={accesses} + isLoading={isLoading} + pagination={pagination} + onSortModelChange={setSortModel} + sortModel={sortModel} + /> + + ); +}; diff --git a/src/frontend/apps/impress/src/features/pads/members/members-grid/components/ModalGridMembers.tsx b/src/frontend/apps/impress/src/features/pads/members/members-grid/components/ModalGridMembers.tsx new file mode 100644 index 00000000..1da038d6 --- /dev/null +++ b/src/frontend/apps/impress/src/features/pads/members/members-grid/components/ModalGridMembers.tsx @@ -0,0 +1,47 @@ +import { Modal, ModalSize } from '@openfun/cunningham-react'; +import { useTranslation } from 'react-i18next'; +import { createGlobalStyle } from 'styled-components'; + +import { Box, Text } from '@/components'; +import { Pad } from '@/features/pads/pad-management'; + +import { MemberGrid } from './MemberGrid'; + +const GlobalStyle = createGlobalStyle` + .c__modal { + overflow: visible; + } +`; + +interface ModalGridMembersProps { + onClose: () => void; + doc: Pad; +} + +export const ModalGridMembers = ({ doc, onClose }: ModalGridMembersProps) => { + const { t } = useTranslation(); + + return ( + + + group + + + {t('Members of the document')} + + + } + > + + + + + + ); +}; diff --git a/src/frontend/apps/impress/src/features/pads/members/members-grid/components/index.ts b/src/frontend/apps/impress/src/features/pads/members/members-grid/components/index.ts new file mode 100644 index 00000000..32e4e61e --- /dev/null +++ b/src/frontend/apps/impress/src/features/pads/members/members-grid/components/index.ts @@ -0,0 +1 @@ +export * from './ModalGridMembers'; diff --git a/src/frontend/apps/impress/src/features/pads/members/members-grid/conf.ts b/src/frontend/apps/impress/src/features/pads/members/members-grid/conf.ts new file mode 100644 index 00000000..bfab9067 --- /dev/null +++ b/src/frontend/apps/impress/src/features/pads/members/members-grid/conf.ts @@ -0,0 +1 @@ +export const PAGE_SIZE = 20; diff --git a/src/frontend/apps/impress/src/features/pads/members/members-grid/index.ts b/src/frontend/apps/impress/src/features/pads/members/members-grid/index.ts new file mode 100644 index 00000000..0ef46430 --- /dev/null +++ b/src/frontend/apps/impress/src/features/pads/members/members-grid/index.ts @@ -0,0 +1,2 @@ +export * from './api'; +export * from './components'; diff --git a/src/frontend/apps/impress/src/features/pads/pad-management/api/usePad.tsx b/src/frontend/apps/impress/src/features/pads/pad-management/api/usePad.tsx index 9bf859f0..7990f891 100644 --- a/src/frontend/apps/impress/src/features/pads/pad-management/api/usePad.tsx +++ b/src/frontend/apps/impress/src/features/pads/pad-management/api/usePad.tsx @@ -9,7 +9,7 @@ export type PadParams = { }; export const getPad = async ({ id }: PadParams): Promise => { - const response = await fetchAPI(`documents/${id}`); + const response = await fetchAPI(`documents/${id}/`); if (!response.ok) { throw new APIError('Failed to get the pad', await errorCauses(response)); diff --git a/src/frontend/apps/impress/src/features/pads/pad-tools/components/PadToolBox.tsx b/src/frontend/apps/impress/src/features/pads/pad-tools/components/PadToolBox.tsx index 0b3bb6d1..ed530719 100644 --- a/src/frontend/apps/impress/src/features/pads/pad-tools/components/PadToolBox.tsx +++ b/src/frontend/apps/impress/src/features/pads/pad-tools/components/PadToolBox.tsx @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'; import { Box, DropButton, IconOptions, Text } from '@/components'; import { ModalAddMembers } from '@/features/pads/members/members-add'; +import { ModalGridMembers } from '@/features/pads/members/members-grid/'; import { ModalRemovePad, ModalUpdatePad, @@ -25,6 +26,7 @@ export const PadToolBox = ({ pad }: PadToolBoxProps) => { ordering: TemplatesOrdering.BY_CREATED_ON_DESC, }); const [isModalAddMembersOpen, setIsModalAddMembersOpen] = useState(false); + const [isModalGridMembersOpen, setIsModalGridMembersOpen] = useState(false); const [isModalUpdateOpen, setIsModalUpdateOpen] = useState(false); const [isModalRemoveOpen, setIsModalRemoveOpen] = useState(false); const [isModalPDFOpen, setIsModalPDFOpen] = useState(false); @@ -61,16 +63,30 @@ export const PadToolBox = ({ pad }: PadToolBoxProps) => { > {pad.abilities.manage_accesses && ( - + <> + + + )} {pad.abilities.partial_update && ( @@ -92,6 +109,7 @@ export const PadToolBox = ({ pad }: PadToolBoxProps) => { }} color="primary-text" icon={delete} + size="small" > {t('Delete document')} @@ -103,11 +121,18 @@ export const PadToolBox = ({ pad }: PadToolBoxProps) => { }} color="primary-text" icon={picture_as_pdf} + size="small" > {t('Generate PDF')} + {isModalGridMembersOpen && ( + setIsModalGridMembersOpen(false)} + doc={pad} + /> + )} {isModalAddMembersOpen && ( setIsModalAddMembersOpen(false)} diff --git a/src/frontend/apps/impress/src/pages/globals.css b/src/frontend/apps/impress/src/pages/globals.css index b033f2c7..20e745a5 100644 --- a/src/frontend/apps/impress/src/pages/globals.css +++ b/src/frontend/apps/impress/src/pages/globals.css @@ -9,21 +9,25 @@ body { box-sizing: border-box; } -main ::-webkit-scrollbar { +main ::-webkit-scrollbar, +.ReactModalPortal ::-webkit-scrollbar { width: 20px; } -main ::-webkit-scrollbar-track { +main ::-webkit-scrollbar-track, +.ReactModalPortal ::-webkit-scrollbar-track { background-color: transparent; } -main ::-webkit-scrollbar-thumb { +main ::-webkit-scrollbar-thumb, +.ReactModalPortal ::-webkit-scrollbar-thumb { background-color: #d6dee1; border-radius: 20px; border: 6px solid transparent; background-clip: content-box; } -main ::-webkit-scrollbar-thumb:hover { +main ::-webkit-scrollbar-thumb:hover, +.ReactModalPortal ::-webkit-scrollbar-thumb:hover { background-color: #a8bbbf; }