diff --git a/src/frontend/apps/e2e/__tests__/app-impress/common.ts b/src/frontend/apps/e2e/__tests__/app-impress/common.ts index 017001c8f..d2529fcc5 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/common.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/common.ts @@ -67,7 +67,9 @@ export const addNewMember = async ( response.status() === 200, ); - const inputSearch = page.getByLabel(/Find a member to add to the document/); + const inputSearch = page.getByRole('combobox', { + name: 'Find a member to add to the document', + }); // Select a new user await inputSearch.fill(fillText); @@ -79,17 +81,19 @@ export const addNewMember = async ( }[]; // Choose user - await page.getByRole('option', { name: users[index].email }).click(); + await page.getByTestId(`search-user-row-${users[index].email}`).click(); // Choose a role - await page.getByRole('combobox', { name: /Choose a role/ }).click(); + const container = page.getByTestId('doc-share-add-member-list'); + await container.getByLabel('doc-role-dropdown').click(); await page.getByRole('option', { name: role }).click(); - await page.getByRole('button', { name: 'Validate' }).click(); - - await expect( - page.getByText(`User ${users[index].email} added to the document.`), - ).toBeVisible(); + await page.getByRole('button', { name: 'Invite' }).click(); + const list = page.getByTestId('doc-share-quick-search'); + const newUser = list.getByTestId( + `doc-share-member-row-${users[index].email}`, + ); + await expect(newUser).toBeVisible(); return users[index].email; }; diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-member-create2.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-member-create2.spec.ts new file mode 100644 index 000000000..daa9c7a6c --- /dev/null +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-member-create2.spec.ts @@ -0,0 +1,238 @@ +import { expect, test } from '@playwright/test'; + +import { createDoc, randomName } from './common'; + +test.beforeEach(async ({ page }) => { + await page.goto('/'); +}); + +test.describe('Document create member', () => { + test('it selects 2 users and 1 invitation', async ({ page, browserName }) => { + const responsePromise = page.waitForResponse( + (response) => + response.url().includes('/users/?q=user') && response.status() === 200, + ); + await createDoc(page, 'select-multi-users', browserName, 1); + + await page.getByRole('button', { name: 'Share' }).click(); + + const inputSearch = page.getByRole('combobox', { + name: 'Find a member to add to the document', + }); + await expect(inputSearch).toBeVisible(); + + // Select user 1 and verify tag + await inputSearch.fill('user'); + const response = await responsePromise; + const users = (await response.json()).results as { + email: string; + full_name: string; + }[]; + + const list = page.getByTestId('doc-share-add-member-list'); + await expect(list).toBeHidden(); + const quickSearchContent = page.getByTestId('doc-share-quick-search'); + await quickSearchContent + .getByTestId(`search-user-row-${users[0].email}`) + .click(); + + await expect(list).toBeVisible(); + await expect( + list.getByTestId(`doc-share-add-member-${users[0].email}`), + ).toBeVisible(); + await expect(list.getByText(`${users[0].full_name}`)).toBeVisible(); + + // Select user 2 and verify tag + await inputSearch.fill('user'); + await quickSearchContent + .getByTestId(`search-user-row-${users[1].email}`) + .click(); + + await expect( + list.getByTestId(`doc-share-add-member-${users[1].email}`), + ).toBeVisible(); + await expect(list.getByText(`${users[1].full_name}`)).toBeVisible(); + + // Select email and verify tag + const email = randomName('test@test.fr', browserName, 1)[0]; + await inputSearch.fill(email); + await quickSearchContent.getByText(email).click(); + await expect(list.getByText(email)).toBeVisible(); + + // Check roles are displayed + await list.getByLabel('doc-role-dropdown').click(); + await expect(page.getByRole('option', { name: 'Reader' })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Editor' })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Owner' })).toBeVisible(); + await expect( + page.getByRole('option', { name: 'Administrator' }), + ).toBeVisible(); + + // Validate + await page.getByRole('option', { name: 'Administrator' }).click(); + await page.getByRole('button', { name: 'Invite' }).click(); + + // Check invitation added + await expect( + quickSearchContent.getByText('Pending invitations'), + ).toBeVisible(); + await expect(quickSearchContent.getByText(email).first()).toBeVisible(); + + // Check user added + await expect(page.getByText('Share with 3 users')).toBeVisible(); + await expect( + quickSearchContent.getByText(users[0].full_name).first(), + ).toBeVisible(); + await expect( + quickSearchContent.getByText(users[0].email).first(), + ).toBeVisible(); + await expect( + quickSearchContent.getByText(users[1].email).first(), + ).toBeVisible(); + await expect( + quickSearchContent.getByText(users[1].full_name).first(), + ).toBeVisible(); + }); + + test('it try to add twice the same invitation', async ({ + page, + browserName, + }) => { + await createDoc(page, 'invitation-twice', browserName, 1); + + await page.getByRole('button', { name: 'Share' }).click(); + + const inputSearch = page.getByRole('combobox', { + name: 'Find a member to add to the document', + }); + + const [email] = randomName('test@test.fr', browserName, 1); + await inputSearch.fill(email); + await page.getByTestId(`search-user-row-${email}`).click(); + + // Choose a role + const container = page.getByTestId('doc-share-add-member-list'); + await container.getByLabel('doc-role-dropdown').click(); + await page.getByRole('option', { name: 'Owner' }).click(); + + const responsePromiseCreateInvitation = page.waitForResponse( + (response) => + response.url().includes('/invitations/') && response.status() === 201, + ); + await page.getByRole('button', { name: 'Invite' }).click(); + + // Check invitation sent + + const responseCreateInvitation = await responsePromiseCreateInvitation; + expect(responseCreateInvitation.ok()).toBeTruthy(); + + await inputSearch.fill(email); + await page.getByTestId(`search-user-row-${email}`).click(); + + // Choose a role + await container.getByLabel('doc-role-dropdown').click(); + await page.getByRole('option', { name: 'Owner' }).click(); + + const responsePromiseCreateInvitationFail = page.waitForResponse( + (response) => + response.url().includes('/invitations/') && response.status() === 400, + ); + + await page.getByRole('button', { name: 'Invite' }).click(); + await expect( + page.getByText(`"${email}" is already invited to the document.`), + ).toBeVisible(); + const responseCreateInvitationFail = + await responsePromiseCreateInvitationFail; + expect(responseCreateInvitationFail.ok()).toBeFalsy(); + }); + + test('The invitation endpoint get the language of the website', async ({ + page, + browserName, + }) => { + await createDoc(page, 'user-invitation', browserName, 1); + + const header = page.locator('header').first(); + await header.getByRole('combobox').getByText('EN').click(); + await header.getByRole('option', { name: 'FR' }).click(); + + await page.getByRole('button', { name: 'Partager' }).click(); + + const inputSearch = page.getByRole('combobox', { + name: 'Trouver un membre à ajouter au document', + }); + + const email = randomName('test@test.fr', browserName, 1)[0]; + await inputSearch.fill(email); + await page.getByTestId(`search-user-row-${email}`).click(); + + // Choose a role + const container = page.getByTestId('doc-share-add-member-list'); + await container.getByLabel('doc-role-dropdown').click(); + await page.getByRole('option', { name: 'Administrateur' }).click(); + + const responsePromiseCreateInvitation = page.waitForResponse( + (response) => + response.url().includes('/invitations/') && response.status() === 201, + ); + + await page.getByRole('button', { name: 'Invite' }).click(); + + // Check invitation sent + + const responseCreateInvitation = await responsePromiseCreateInvitation; + expect(responseCreateInvitation.ok()).toBeTruthy(); + expect( + responseCreateInvitation.request().headers()['content-language'], + ).toBe('fr-fr'); + }); + + test('it manages invitation', async ({ page, browserName }) => { + await createDoc(page, 'user-invitation', browserName, 1); + + await page.getByRole('button', { name: 'Share' }).click(); + + const inputSearch = page.getByRole('combobox', { + name: 'Find a member to add to the document', + }); + + const email = randomName('test@test.fr', browserName, 1)[0]; + await inputSearch.fill(email); + await page.getByTestId(`search-user-row-${email}`).click(); + + // Choose a role + const container = page.getByTestId('doc-share-add-member-list'); + await container.getByLabel('doc-role-dropdown').click(); + await page.getByRole('option', { name: 'Administrator' }).click(); + + const responsePromiseCreateInvitation = page.waitForResponse( + (response) => + response.url().includes('/invitations/') && response.status() === 201, + ); + + await page.getByRole('button', { name: 'Invite' }).click(); + + // Check invitation sent + const responseCreateInvitation = await responsePromiseCreateInvitation; + expect(responseCreateInvitation.ok()).toBeTruthy(); + + const listInvitation = page.getByTestId('doc-share-quick-search'); + const userInvitation = listInvitation.getByTestId( + `doc-share-invitation-row-${email}`, + ); + await expect(userInvitation).toBeVisible(); + + await userInvitation.getByLabel('doc-role-dropdown').click(); + await page.getByRole('option', { name: 'Reader' }).click(); + + const moreActions = userInvitation.getByRole('button', { + name: 'more_vert', + }); + await moreActions.click(); + + await page.getByRole('option', { name: 'delete Delete' }).click(); + + await expect(userInvitation).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 3d9553a1a..a76f47c02 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 @@ -1,7 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForElementCount } from '../helpers'; - import { addNewMember, createDoc, goToGridDoc, verifyDocName } from './common'; test.beforeEach(async ({ page }) => { @@ -15,10 +13,11 @@ test.describe('Document list members', () => { async (route) => { const request = route.request(); const url = new URL(request.url()); - const pageId = url.searchParams.get('page'); + const pageId = url.searchParams.get('page') ?? '1'; + const accesses = { - count: 100, - next: 'http://anything/?page=2', + count: 40, + next: +pageId < 2 ? 'http://anything/?page=2' : undefined, previous: null, results: Array.from({ length: 20 }, (_, i) => ({ id: `2ff1ec07-86c1-4534-a643-f41824a6c53a-${pageId}-${i}`, @@ -48,25 +47,20 @@ test.describe('Document list members', () => { ); await goToGridDoc(page); - await page.getByRole('button', { name: 'Share' }).click(); - const list = page.getByLabel('List members card').locator('ul'); - await expect(list.locator('li')).toHaveCount(20); - await list.getByText(`impress@impress.world-page-${1}-18`).hover(); - await page.mouse.wheel(0, 10); + const prefix = 'doc-share-member-row'; + const elements = page.locator(`[data-testid^="${prefix}"]`); + const loadMore = page.getByTestId('load-more-members'); - await waitForElementCount(list.locator('li'), 21, 10000); + await expect(elements).toHaveCount(20); + await expect(page.getByText(`Impress World Page 1-16`)).toBeVisible(); - expect(await list.locator('li').count()).toBeGreaterThan(20); - await expect(list.getByText(`Impress World Page 1-16`)).toBeVisible(); - await expect( - list.getByText(`impress@impress.world-page-1-16`), - ).toBeVisible(); - await expect(list.getByText(`Impress World Page 2-15`)).toBeVisible(); - await expect( - list.getByText(`impress@impress.world-page-2-15`), - ).toBeVisible(); + await loadMore.click(); + await expect(elements).toHaveCount(40); + await expect(page.getByText(`Impress World Page 2-15`)).toBeVisible(); + + await expect(loadMore).toBeHidden(); }); test('it checks a big list of invitations', async ({ page }) => { @@ -75,10 +69,10 @@ test.describe('Document list members', () => { async (route) => { const request = route.request(); const url = new URL(request.url()); - const pageId = url.searchParams.get('page'); + const pageId = url.searchParams.get('page') ?? '1'; const accesses = { - count: 100, - next: 'http://anything/?page=2', + count: 40, + next: +pageId < 2 ? 'http://anything/?page=2' : null, previous: null, results: Array.from({ length: 20 }, (_, i) => ({ id: `2ff1ec07-86c1-4534-a643-f41824a6c53a-${pageId}-${i}`, @@ -105,23 +99,24 @@ test.describe('Document list members', () => { ); await goToGridDoc(page); - await page.getByRole('button', { name: 'Share' }).click(); - const list = page.getByLabel('List invitation card').locator('ul'); - await expect(list.locator('li')).toHaveCount(20); - await list.getByText(`impress@impress.world-page-${1}-18`).hover(); - await page.mouse.wheel(0, 10); + const prefix = 'doc-share-invitation'; + const elements = page.locator(`[data-testid^="${prefix}"]`); + const loadMore = page.getByTestId('load-more-invitations'); - await waitForElementCount(list.locator('li'), 21, 10000); + await expect(elements).toHaveCount(20); + await expect( + page.getByText(`impress@impress.world-page-1-16`).first(), + ).toBeVisible(); - expect(await list.locator('li').count()).toBeGreaterThan(20); + await loadMore.click(); + await expect(elements).toHaveCount(40); await expect( - list.getByText(`impress@impress.world-page-1-16`), - ).toBeVisible(); - await expect( - list.getByText(`impress@impress.world-page-2-15`), + page.getByText(`impress@impress.world-page-2-16`).first(), ).toBeVisible(); + + await expect(loadMore).toBeHidden(); }); test('it checks the role rules', async ({ page, browserName }) => { @@ -130,59 +125,47 @@ test.describe('Document list members', () => { await verifyDocName(page, docTitle); await page.getByRole('button', { name: 'Share' }).click(); - - const list = page.getByLabel('List members card').locator('ul'); - - await expect(list.getByText(`user@${browserName}.e2e`)).toBeVisible(); - - const soleOwner = list.getByText( + const list = page.getByTestId('doc-share-quick-search'); + await expect(list).toBeVisible(); + const currentUser = list.getByTestId( + `doc-share-member-row-user@chromium.e2e`, + ); + const currentUserRole = currentUser.getByLabel('doc-role-dropdown'); + await expect(currentUser).toBeVisible(); + await expect(currentUserRole).toBeVisible(); + await currentUserRole.click(); + const soloOwner = page.getByText( `You are the sole owner of this group, make another member the group owner before you can change your own role or be removed from your document.`, ); + await expect(soloOwner).toBeVisible(); + await list.click(); + const newUserEmail = await addNewMember(page, 0, 'Owner'); + const newUser = list.getByTestId(`doc-share-member-row-${newUserEmail}`); + const newUserRoles = newUser.getByLabel('doc-role-dropdown'); - await expect(soleOwner).toBeVisible(); + await expect(newUser).toBeVisible(); - const username = await addNewMember(page, 0, 'Owner'); + await currentUserRole.click(); + await expect(soloOwner).toBeHidden(); + await list.click(); - await expect(list.getByText(username)).toBeVisible(); - - await expect(soleOwner).toBeHidden(); - - const otherOwner = list.getByText( + const otherOwner = page.getByText( `You cannot update the role or remove other owner.`, ); + await newUserRoles.click(); await expect(otherOwner).toBeVisible(); + await list.click(); - const SelectRoleCurrentUser = list - .locator('li') - .filter({ - hasText: `user@${browserName}.e2e`, - }) - .getByRole('combobox', { name: 'Role' }); - - await SelectRoleCurrentUser.click(); + await currentUserRole.click(); await page.getByRole('option', { name: 'Administrator' }).click(); - await expect(page.getByText('The role has been updated')).toBeVisible(); + await list.click(); + await expect(currentUserRole).toBeVisible(); - const shareModal = page.getByLabel('Share modal'); - - // Admin still have the right to share - await expect( - shareModal.getByRole('combobox', { - name: 'Visibility', - }), - ).not.toHaveAttribute('disabled'); - - await SelectRoleCurrentUser.click(); + await currentUserRole.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( - shareModal.getByRole('combobox', { - name: 'Visibility', - }), - ).toHaveAttribute('disabled'); + await list.click(); + await expect(currentUserRole).toBeHidden(); }); test('it checks the delete members', async ({ page, browserName }) => { @@ -192,43 +175,44 @@ test.describe('Document list members', () => { await page.getByRole('button', { name: 'Share' }).click(); - const list = page.getByLabel('List members card').locator('ul'); + const list = page.getByTestId('doc-share-quick-search'); - const nameMyself = `user@${browserName}.e2e`; - await expect(list.getByText(nameMyself)).toBeVisible(); + const emailMyself = `user@${browserName}.e2e`; + const mySelf = list.getByTestId(`doc-share-member-row-${emailMyself}`); + const mySelfMoreActions = mySelf.getByRole('button', { name: 'more_vert' }); - const userOwner = await addNewMember(page, 0, 'Owner'); - await expect(list.getByText(userOwner)).toBeVisible(); + const userOwnerEmail = await addNewMember(page, 0, 'Owner'); + const userOwner = list.getByTestId( + `doc-share-member-row-${userOwnerEmail}`, + ); + const userOwnerMoreActions = userOwner.getByRole('button', { + name: 'more_vert', + }); - const userReader = await addNewMember(page, 0, 'Reader'); - await expect(list.getByText(userReader)).toBeVisible(); + const userReaderEmail = await addNewMember(page, 0, 'Reader'); + const userReader = list.getByTestId( + `doc-share-member-row-${userReaderEmail}`, + ); + const userReaderMoreActions = userReader.getByRole('button', { + name: 'more_vert', + }); - await list - .locator('li') - .filter({ - hasText: userReader, - }) - .getByText('delete') - .click(); + await expect(mySelf).toBeVisible(); + await expect(userOwner).toBeVisible(); + await expect(userReader).toBeVisible(); - await expect(list.getByText(userReader)).toBeHidden(); + await expect(userOwnerMoreActions).toBeVisible(); + await expect(userReaderMoreActions).toBeVisible(); + await expect(mySelfMoreActions).toBeVisible(); - await list - .locator('li') - .filter({ - hasText: nameMyself, - }) - .getByText('delete') - .click(); - - await expect(list.getByText(nameMyself)).toBeHidden(); + await userReaderMoreActions.click(); + await page.getByRole('option', { name: 'Delete' }).click(); + await expect(userReader).toBeHidden(); + await mySelfMoreActions.click(); + await page.getByRole('option', { name: 'Delete' }).click(); await expect( - page.getByText('The member has been removed from the document').first(), + page.getByText('You do not have permission to perform this action.'), ).toBeVisible(); - - await expect( - page.getByRole('heading', { name: 'Share', level: 3 }), - ).toBeHidden(); }); }); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts index c5a0cc779..38ca8346a 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts @@ -143,22 +143,22 @@ test.describe('Doc Visibility: Restricted', () => { await page.getByRole('button', { name: 'Share' }).click(); - const inputSearch = page.getByLabel(/Find a member to add to the document/); + const inputSearch = page.getByRole('combobox', { + name: 'Find a member to add to the document', + }); const otherBrowser = browsersName.find((b) => b !== browserName); const username = `user@${otherBrowser}.e2e`; await inputSearch.fill(username); - await page.getByRole('option', { name: username }).click(); + await page.getByTestId(`search-user-row-${username}`).click(); // Choose a role - await page.getByRole('combobox', { name: /Choose a role/ }).click(); + + const container = page.getByTestId('doc-share-add-member-list'); + await container.getByLabel('doc-role-dropdown').click(); await page.getByRole('option', { name: 'Administrator' }).click(); - await page.getByRole('button', { name: 'Validate' }).click(); - - await expect( - page.getByText(`User ${username} added to the document.`), - ).toBeVisible(); + await page.getByRole('button', { name: 'Invite' }).click(); await page.locator('.c__modal__backdrop').click({ position: { x: 0, y: 0 }, @@ -434,15 +434,17 @@ test.describe('Doc Visibility: Authenticated', () => { page.getByText('Read only, you cannot edit this document'), ).toBeVisible(); - const shareModal = page.getByLabel('Share modal'); - await expect( - shareModal.getByRole('combobox', { + page.getByRole('combobox', { name: 'Visibility', }), ).toHaveAttribute('disabled'); - await expect(shareModal.getByText('Search by email')).toBeHidden(); - await expect(shareModal.getByLabel('List members card')).toBeHidden(); + + const inputSearch = page.getByRole('combobox', { + name: 'Find a member to add to the document', + }); + + await expect(inputSearch).toBeHidden(); }); test('It checks a authenticated doc in editable mode', async ({ @@ -512,14 +514,15 @@ test.describe('Doc Visibility: Authenticated', () => { page.getByText('Read only, you cannot edit this document'), ).toBeHidden(); - const shareModal = page.getByLabel('Share modal'); - await expect( - shareModal.getByRole('combobox', { + page.getByRole('combobox', { name: 'Visibility', }), ).toHaveAttribute('disabled'); - await expect(shareModal.getByText('Search by email')).toBeHidden(); - await expect(shareModal.getByLabel('List members card')).toBeHidden(); + const inputSearch = page.getByRole('combobox', { + name: 'Find a member to add to the document', + }); + + await expect(inputSearch).toBeHidden(); }); }); diff --git a/src/frontend/apps/impress/src/components/DropButton.tsx b/src/frontend/apps/impress/src/components/DropButton.tsx index 90fee617d..622e5c6df 100644 --- a/src/frontend/apps/impress/src/components/DropButton.tsx +++ b/src/frontend/apps/impress/src/components/DropButton.tsx @@ -27,6 +27,7 @@ const StyledButton = styled(Button)` export interface DropButtonProps { button: ReactNode; isOpen?: boolean; + label?: string; onOpenChange?: (isOpen: boolean) => void; } @@ -34,6 +35,7 @@ export const DropButton = ({ button, isOpen = false, onOpenChange, + label, children, }: PropsWithChildren) => { const [opacity, setOpacity] = useState(false); @@ -53,7 +55,7 @@ export const DropButton = ({ return ( - {button} + {button} void | Promise; danger?: boolean; + isSelected?: boolean; disabled?: boolean; }; export type DropdownMenuProps = { options: DropdownMenuOption[]; showArrow?: boolean; + label?: string; arrowCss?: BoxProps['$css']; + topMessage?: string; }; export const DropdownMenu = ({ @@ -24,6 +27,8 @@ export const DropdownMenu = ({ children, showArrow = false, arrowCss, + label, + topMessage, }: PropsWithChildren) => { const theme = useCunninghamTheme(); const spacings = theme.spacingsTokens(); @@ -34,10 +39,12 @@ export const DropdownMenu = ({ setIsOpen(isOpen); }; + console.log('topMessage', topMessage); return ( @@ -57,11 +64,23 @@ export const DropdownMenu = ({ ) } > - - {options.map((option, index) => { + + {topMessage && ( + + {topMessage} + + )} + {options.map((option) => { const isDisabled = option.disabled !== undefined && option.disabled; return ( - {option.icon && ( - + + {option.icon && ( + + )} + + {option.label} + + + {option.isSelected && ( + )} - {option.label} ); })} diff --git a/src/frontend/apps/impress/src/components/LoadMoreText.tsx b/src/frontend/apps/impress/src/components/LoadMoreText.tsx index c081ac012..2b715b5b3 100644 --- a/src/frontend/apps/impress/src/components/LoadMoreText.tsx +++ b/src/frontend/apps/impress/src/components/LoadMoreText.tsx @@ -4,11 +4,18 @@ import { Box } from './Box'; import { Icon } from './Icon'; import { Text } from './Text'; -export const LoadMoreText = () => { +type LoadMoreTextProps = { + ['data-testid']?: string; +}; + +export const LoadMoreText = ({ + 'data-testid': dataTestId, +}: LoadMoreTextProps) => { const { t } = useTranslation(); return ( diff --git a/src/frontend/apps/impress/src/components/quick-search/QuickSearchStyle.tsx b/src/frontend/apps/impress/src/components/quick-search/QuickSearchStyle.tsx index c3dd72b55..5e0ed47ea 100644 --- a/src/frontend/apps/impress/src/components/quick-search/QuickSearchStyle.tsx +++ b/src/frontend/apps/impress/src/components/quick-search/QuickSearchStyle.tsx @@ -7,13 +7,8 @@ export const QuickSearchStyle = createGlobalStyle` background: #ffffff; border-radius: 12px; overflow: hidden; - transition: transform 100ms ease; outline: none; - - .dark & { - background: rgba(22, 22, 22, 0.7); - } } [cmdk-input] { @@ -23,9 +18,7 @@ export const QuickSearchStyle = createGlobalStyle` padding: 8px; background: white; outline: none; - color: var(--c--theme--colors--greyscale-1000); - border-radius: 0; &::placeholder { @@ -50,17 +43,12 @@ export const QuickSearchStyle = createGlobalStyle` [cmdk-item] { content-visibility: auto; - - cursor: pointer; - - + cursor: pointer; border-radius: var(--c--theme--spacings--xs); font-size: 14px; display: flex; align-items: center; gap: 8px; - /* padding: var(--c--theme--spacings--2xs) ; */ - user-select: none; will-change: background, color; transition: all 150ms ease; @@ -89,11 +77,11 @@ export const QuickSearchStyle = createGlobalStyle` } [cmdk-list] { - height: 500px; + padding: 0 var(--c--theme--spacings--sm) var(--c--theme--spacings--sm) var(--c--theme--spacings--sm); - max-height: 700px; - + + flex:1; overflow-y: auto; overscroll-behavior: contain; transition: 100ms ease; @@ -161,7 +149,7 @@ export const QuickSearchStyle = createGlobalStyle` .c__modal__title { font-size: var(--c--theme--font--sizes--xs); - padding: var(--c--theme--spacings--200W); + padding: var(--c--theme--spacings--base); margin-bottom: 0; } } 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 9ef1e628d..1651786ac 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 @@ -21,11 +21,7 @@ import { useEditorStore, usePanelEditorStore, } from '@/features/docs/doc-editor/'; -import { - Doc, - ModalRemoveDoc, - ModalShare, -} from '@/features/docs/doc-management'; +import { Doc, ModalRemoveDoc } from '@/features/docs/doc-management'; import { ModalVersion, Versions } from '@/features/docs/doc-versioning'; import { useResponsiveStore } from '@/stores'; @@ -204,9 +200,9 @@ export const DocToolBox = ({ doc, versionId }: DocToolBoxProps) => { /> - {isModalShareOpen && ( + {/* {isModalShareOpen && ( setIsModalShareOpen(false)} doc={doc} /> - )} + )} */} {modalShare.isOpen && ( )} diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/components/DocRoleDropdown.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/components/DocRoleDropdown.tsx index 5f80d9e70..ed7080de0 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/components/DocRoleDropdown.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-management/components/DocRoleDropdown.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from 'react-i18next'; import { css } from 'styled-components'; import { DropdownMenu, DropdownMenuOption, Text } from '@/components'; @@ -9,19 +10,26 @@ type Props = { currentRole: Role; onSelectRole?: (role: Role) => void; canUpdate?: boolean; + isLastOwner?: boolean; + isOtherOwner?: boolean; }; export const DocRoleDropdown = ({ canUpdate = true, currentRole, onSelectRole, + isLastOwner, + isOtherOwner, }: Props) => { - const { transRole, translatedRoles } = useTrans(); + const { t } = useTranslation(); + const { transRole, translatedRoles, getNotAllowedMessage } = useTrans(); const roles: DropdownMenuOption[] = Object.keys(translatedRoles).map( (key) => { return { label: transRole(key as Role), callback: () => onSelectRole?.(key as Role), + disabled: isLastOwner || isOtherOwner, + isSelected: currentRole === (key as Role), }; }, ); @@ -40,7 +48,16 @@ export const DocRoleDropdown = ({ } return ( - + { const fontSize = fontSizesTokens(); return ( { }; const { toast } = useToastProvider(); - const canUpdate = invitation.abilities.partial_update; + const canUpdate = doc.abilities.accesses_manage; + const { mutate: updateDocInvitation } = useUpdateDocInvitation({ onError: (error) => { toast( @@ -78,22 +79,30 @@ export const DocShareInvitationItem = ({ doc, invitation }: Props) => { }, ]; return ( - - + + + - - - - - } - /> + + + + + } + /> + ); }; diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/components/share/DocShareMemberItem.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/components/share/DocShareMemberItem.tsx index 75f3102f3..08ba421b1 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/components/share/DocShareMemberItem.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-management/components/share/DocShareMemberItem.tsx @@ -28,7 +28,7 @@ export const DocShareMemberItem = ({ doc, access }: Props) => { const { toast } = useToastProvider(); const { isDesktop } = useResponsiveStore(); const isNotAllowed = - isOtherOwner || isLastOwner || !doc.abilities.accesses_manage; + isOtherOwner || !!isLastOwner || !doc.abilities.accesses_manage; const { mutate: updateDocAccess } = useUpdateDocAccess({ onError: () => { @@ -68,24 +68,34 @@ export const DocShareMemberItem = ({ doc, access }: Props) => { ]; return ( - - + + + - {isDesktop && ( - - - - )} - - } - /> + {isDesktop && doc.abilities.accesses_manage && ( + + + + )} + + } + /> + ); }; diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/components/share/DocShareModal.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/components/share/DocShareModal.tsx index 8501aa47b..656c06e8d 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/components/share/DocShareModal.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-management/components/share/DocShareModal.tsx @@ -7,6 +7,7 @@ import { } from '@openfun/cunningham-react'; import { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { css } from 'styled-components'; import { useDebouncedCallback } from 'use-debounce'; import { Box } from '@/components'; @@ -84,7 +85,7 @@ export const DocShareModal = ({ doc, onClose }: Props) => { endActions: membersQuery.hasNextPage ? [ { - content: , + content: , onSelect: () => void membersQuery.fetchNextPage(), }, ] @@ -102,7 +103,7 @@ export const DocShareModal = ({ doc, onClose }: Props) => { endActions: invitationQuery.hasNextPage ? [ { - content: , + content: , onSelect: () => void invitationQuery.fetchNextPage(), }, ] @@ -121,7 +122,7 @@ export const DocShareModal = ({ doc, onClose }: Props) => { }; return { - groupName: t('Share with {{count}} users', { count: users.length }), + groupName: t('Search user result', { count: users.length }), elements: users, endActions: isEmail && users.length === 0 @@ -154,6 +155,7 @@ export const DocShareModal = ({ doc, onClose }: Props) => { return ( { } > - {canShare && selectedUsers.length > 0 && ( - - { - setUserQuery(''); - setInputValue(''); - setSelectedUsers([]); - }} - /> - - )} - - { - setInputValue(str); - onFilter(str); - }} - inputValue={inputValue} - showInput={canShare} - loading={searchUsersQuery.isLoading} - renderElement={(user) => } - onSelect={onSelect} - placeholder={t('Type a name or email')} + - {!showMemberSection && inputValue !== '' && ( - } - /> - )} - {showMemberSection && ( - <> - {invitationsData.elements.length > 0 && ( - ( - - )} - /> - )} - - ( - - )} - /> - - )} - - - - - - - + {canShare && selectedUsers.length > 0 && ( + + { + setUserQuery(''); + setInputValue(''); + setSelectedUsers([]); + }} + /> + + )} + + + { + setInputValue(str); + onFilter(str); + }} + inputValue={inputValue} + showInput={canShare} + loading={searchUsersQuery.isLoading} + renderElement={(user) => ( + + )} + onSelect={onSelect} + placeholder={t('Type a name or email')} + > + {!showMemberSection && inputValue !== '' && ( + ( + + )} + /> + )} + {showMemberSection && ( + <> + {invitationsData.elements.length > 0 && ( + ( + + )} + /> + )} + + ( + + )} + /> + + )} + + + + + + + + + + + + ); diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/components/share/DocShareModalInviteUserByEmail.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/components/share/DocShareModalInviteUserByEmail.tsx index 632ac4a33..8ea54cc6e 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/components/share/DocShareModalInviteUserByEmail.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-management/components/share/DocShareModalInviteUserByEmail.tsx @@ -11,25 +11,27 @@ type Props = { export const DocShareModalInviteUserRow = ({ user }: Props) => { const { t } = useTranslation(); return ( - - - {t('Add')} - - - - } - /> + + + + {t('Add')} + + + + } + /> + ); }; diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/hooks/useTrans.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/hooks/useTrans.tsx index 807c6c8b7..dce32687d 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/hooks/useTrans.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-management/hooks/useTrans.tsx @@ -12,11 +12,34 @@ export const useTrans = () => { [Role.EDITOR]: t('Editor'), }; + const getNotAllowedMessage = ( + canUpdate: boolean, + isLastOwner: boolean, + isOtherOwner: boolean, + ) => { + if (!canUpdate) { + return undefined; + } + + if (isLastOwner) { + return t( + 'You are the sole owner of this group, make another member the group owner before you can change your own role or be removed from your document.', + ); + } + + if (isOtherOwner) { + return t('You cannot update the role or remove other owner.'); + } + + return undefined; + }; + return { transRole: (role: Role) => { return translatedRoles[role]; }, untitledDocument: t('Untitled document'), translatedRoles, + getNotAllowedMessage, }; };