diff --git a/CHANGELOG.md b/CHANGELOG.md index 4db20e21..5d70a16e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to - 📝Contributing.md #352 - 🌐(frontend) add localization to editor #268 +- ✨Public and restricted doc editable #357 ## Fixed diff --git a/src/frontend/apps/e2e/__tests__/app-impress/common.ts b/src/frontend/apps/e2e/__tests__/app-impress/common.ts index 4d5c43c4..2c52eeee 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/common.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/common.ts @@ -4,17 +4,17 @@ export const keyCloakSignIn = async (page: Page, browserName: string) => { const login = `user-e2e-${browserName}`; const password = `password-e2e-${browserName}`; + await expect( + page.locator('.login-pf-page-header').getByText('impress'), + ).toBeVisible(); + if (await page.getByLabel('Restart login').isVisible()) { - await page.getByRole('textbox', { name: 'password' }).fill(password); - - await page.click('input[type="submit"]', { force: true }); - } else { - await page.getByRole('textbox', { name: 'username' }).fill(login); - - await page.getByRole('textbox', { name: 'password' }).fill(password); - - await page.click('input[type="submit"]', { force: true }); + await page.getByLabel('Restart login').click(); } + + await page.getByRole('textbox', { name: 'username' }).fill(login); + await page.getByRole('textbox', { name: 'password' }).fill(password); + await page.click('input[type="submit"]', { force: true }); }; export const randomName = (name: string, browserName: string, length: number) => @@ -27,7 +27,6 @@ export const createDoc = async ( docName: string, browserName: string, length: number, - isPublic: boolean = false, ) => { const randomDocs = randomName(docName, browserName, length); @@ -44,22 +43,6 @@ export const createDoc = async ( await page.getByRole('heading', { name: 'Untitled document' }).click(); await page.keyboard.type(randomDocs[i]); await page.getByText('Created at ').click(); - - if (isPublic) { - await page.getByRole('button', { name: 'Share' }).click(); - await page.getByText('Doc private').click(); - - await page.locator('.c__modal__backdrop').click({ - position: { x: 0, y: 0 }, - force: true, - }); - - await expect( - page - .getByLabel('It is the card information about the document.') - .getByText('Public'), - ).toBeVisible(); - } } return randomDocs; 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 6c9b59d1..a75c4593 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 @@ -211,7 +211,11 @@ test.describe('Doc Header', () => { const shareModal = page.getByLabel('Share modal'); - await expect(shareModal.getByLabel('Doc private')).toBeEnabled(); + await expect( + shareModal.getByRole('combobox', { + name: 'Visibility', + }), + ).not.toHaveAttribute('disabled'); await expect(shareModal.getByText('Search by email')).toBeVisible(); const invitationCard = shareModal.getByLabel('List invitation card'); @@ -284,7 +288,11 @@ test.describe('Doc Header', () => { const shareModal = page.getByLabel('Share modal'); - await expect(shareModal.getByLabel('Doc private')).toBeDisabled(); + await expect( + shareModal.getByRole('combobox', { + name: 'Visibility', + }), + ).toHaveAttribute('disabled'); await expect(shareModal.getByText('Search by email')).toBeHidden(); const invitationCard = shareModal.getByLabel('List invitation card'); @@ -357,7 +365,11 @@ test.describe('Doc Header', () => { const shareModal = page.getByLabel('Share modal'); - await expect(shareModal.getByLabel('Doc private')).toBeDisabled(); + await expect( + shareModal.getByRole('combobox', { + name: 'Visibility', + }), + ).toHaveAttribute('disabled'); await expect(shareModal.getByText('Search by email')).toBeHidden(); const invitationCard = shareModal.getByLabel('List invitation card'); 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 e9bfb78f..f06c66fc 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 @@ -164,14 +164,22 @@ test.describe('Document list members', () => { const shareModal = page.getByLabel('Share modal'); // Admin still have the right to share - await expect(shareModal.getByLabel('Doc private')).toBeEnabled(); + await expect( + shareModal.getByRole('combobox', { + name: 'Visibility', + }), + ).not.toHaveAttribute('disabled'); 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(shareModal.getByLabel('Doc private')).toBeDisabled(); + await expect( + shareModal.getByRole('combobox', { + name: 'Visibility', + }), + ).toHaveAttribute('disabled'); }); test('it checks the delete members', async ({ page, browserName }) => { 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 759f5627..064d7a93 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 @@ -2,39 +2,13 @@ import { expect, test } from '@playwright/test'; import { createDoc, keyCloakSignIn } from './common'; +const browsersName = ['chromium', 'webkit', 'firefox']; + test.describe('Doc Visibility', () => { test.beforeEach(async ({ page }) => { await page.goto('/'); }); - test('Make a public doc', async ({ page, browserName }) => { - const [docTitle] = await createDoc( - page, - 'My new doc', - browserName, - 1, - true, - ); - - const header = page.locator('header').first(); - await header.locator('h2').getByText('Docs').click(); - - const datagrid = page.getByLabel('Datagrid of the documents page 1'); - const datagridTable = datagrid.getByRole('table'); - - await expect(datagrid.getByLabel('Loading data')).toBeHidden({ - timeout: 10000, - }); - - await expect(datagridTable.getByText(docTitle)).toBeVisible(); - - const row = datagridTable.getByRole('row').filter({ - hasText: docTitle, - }); - - await expect(row.getByRole('cell').nth(0)).toHaveText('Public'); - }); - test('It checks the copy link button', async ({ page, browserName }) => { // eslint-disable-next-line playwright/no-skipped-test test.skip( @@ -56,12 +30,48 @@ test.describe('Doc Visibility', () => { expect(clipboardContent).toMatch(page.url()); }); + + test('It checks the link role options', async ({ page, browserName }) => { + await createDoc(page, 'Doc role options', browserName, 1); + + await page.getByRole('button', { name: 'Share' }).click(); + + const selectVisibility = page.getByRole('combobox', { + name: 'Visibility', + }); + + await expect(selectVisibility.getByText('Authenticated')).toBeVisible(); + + await expect(page.getByLabel('Read only')).toBeVisible(); + await expect(page.getByLabel('Can read and edit')).toBeVisible(); + + await selectVisibility.click(); + await page + .getByRole('option', { + name: 'Restricted', + }) + .click(); + + await expect(page.getByLabel('Read only')).toBeHidden(); + await expect(page.getByLabel('Can read and edit')).toBeHidden(); + + await selectVisibility.click(); + + await page + .getByRole('option', { + name: 'Public', + }) + .click(); + + await expect(page.getByLabel('Read only')).toBeVisible(); + await expect(page.getByLabel('Can read and edit')).toBeVisible(); + }); }); -test.describe('Doc Visibility: Not loggued', () => { +test.describe('Doc Visibility: Restricted', () => { test.use({ storageState: { cookies: [], origins: [] } }); - test('A public doc is accessible even when not authentified.', async ({ + test('A doc is not accessible when not authentified.', async ({ page, browserName, }) => { @@ -70,14 +80,211 @@ test.describe('Doc Visibility: Not loggued', () => { const [docTitle] = await createDoc( page, - 'My new doc', + 'Restricted no auth', browserName, 1, - true, ); + await expect(page.getByRole('heading', { name: docTitle })).toBeVisible(); + + await page.getByRole('button', { name: 'Share' }).click(); + await page + .getByRole('combobox', { + name: 'Visibility', + }) + .click(); + await page + .getByRole('option', { + name: 'Restricted', + }) + .click(); + await expect( - page.getByText('The document visiblitity has been updated.'), + page.getByText('The document visibility has been updated.'), + ).toBeVisible(); + + await page.locator('.c__modal__backdrop').click({ + position: { x: 0, y: 0 }, + }); + + const urlDoc = page.url(); + + await page + .getByRole('button', { + name: 'Logout', + }) + .click(); + + await expect(page.getByRole('button', { name: 'Sign in' })).toBeVisible(); + + await page.goto(urlDoc); + + await expect(page.getByRole('textbox', { name: 'password' })).toBeVisible(); + }); + + test('A doc is not accessible when authentified but not member.', async ({ + page, + browserName, + }) => { + await page.goto('/'); + await keyCloakSignIn(page, browserName); + + const [docTitle] = await createDoc(page, 'Restricted auth', browserName, 1); + + await expect(page.getByRole('heading', { name: docTitle })).toBeVisible(); + + await page.getByRole('button', { name: 'Share' }).click(); + await page + .getByRole('combobox', { + name: 'Visibility', + }) + .click(); + await page + .getByRole('option', { + name: 'Restricted', + }) + .click(); + + await expect( + page.getByText('The document visibility has been updated.'), + ).toBeVisible(); + + await page.locator('.c__modal__backdrop').click({ + position: { x: 0, y: 0 }, + }); + + const urlDoc = page.url(); + + await page + .getByRole('button', { + name: 'Logout', + }) + .click(); + + const otherBrowser = browsersName.find((b) => b !== browserName); + + await keyCloakSignIn(page, otherBrowser!); + + await page.goto(urlDoc); + + await expect( + page.getByText('You do not have permission to perform this action.'), + ).toBeVisible(); + }); + + test('A doc is accessible when member.', async ({ page, browserName }) => { + test.slow(); + await page.goto('/'); + await keyCloakSignIn(page, browserName); + + const [docTitle] = await createDoc(page, 'Restricted auth', browserName, 1); + + await expect(page.getByRole('heading', { name: docTitle })).toBeVisible(); + + await page.getByRole('button', { name: 'Share' }).click(); + await page + .getByRole('combobox', { + name: 'Visibility', + }) + .click(); + await page + .getByRole('option', { + name: 'Restricted', + }) + .click(); + + await expect( + page.getByText('The document visibility has been updated.'), + ).toBeVisible(); + + const inputSearch = page.getByLabel(/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(); + + // Choose a role + await page.getByRole('combobox', { name: /Choose a role/ }).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.locator('.c__modal__backdrop').click({ + position: { x: 0, y: 0 }, + }); + + const urlDoc = page.url(); + + await page + .getByRole('button', { + name: 'Logout', + }) + .click(); + + await keyCloakSignIn(page, otherBrowser!); + + await page.goto(urlDoc); + + await expect(page.locator('h2').getByText(docTitle)).toBeVisible(); + await expect(page.getByRole('button', { name: 'Share' })).toBeVisible(); + }); +}); + +test.describe('Doc Visibility: Public', () => { + test.use({ storageState: { cookies: [], origins: [] } }); + + test('It checks a public doc in read only mode', async ({ + page, + browserName, + }) => { + await page.goto('/'); + await keyCloakSignIn(page, browserName); + + const [docTitle] = await createDoc( + page, + 'Public read only', + browserName, + 1, + ); + + await expect(page.getByRole('heading', { name: docTitle })).toBeVisible(); + + await page.getByRole('button', { name: 'Share' }).click(); + await page + .getByRole('combobox', { + name: 'Visibility', + }) + .click(); + + await page + .getByRole('option', { + name: 'Public', + }) + .click(); + + await expect( + page.getByText('The document visibility has been updated.'), + ).toBeVisible(); + + await page.getByLabel('Read only').click(); + + await expect( + page.getByText('The document visibility has been updated.').first(), + ).toBeVisible(); + + await page.locator('.c__modal__backdrop').click({ + position: { x: 0, y: 0 }, + }); + + await expect( + page + .getByLabel('It is the card information about the document.') + .getByText('Public', { exact: true }), ).toBeVisible(); const urlDoc = page.url(); @@ -94,19 +301,54 @@ test.describe('Doc Visibility: Not loggued', () => { await expect(page.locator('h2').getByText(docTitle)).toBeVisible(); await expect(page.getByRole('button', { name: 'Share' })).toBeHidden(); + await expect( + page.getByText('Read only, you cannot edit this document'), + ).toBeVisible(); }); - test('A private doc redirect to the OIDC when not authentified.', async ({ + test('It checks a public doc in editable mode', async ({ page, browserName, }) => { - test.slow(); await page.goto('/'); await keyCloakSignIn(page, browserName); - const [docTitle] = await createDoc(page, 'My private doc', browserName, 1); + const [docTitle] = await createDoc(page, 'Public editable', browserName, 1); - await expect(page.locator('h2').getByText(docTitle)).toBeVisible(); + await expect(page.getByRole('heading', { name: docTitle })).toBeVisible(); + + await page.getByRole('button', { name: 'Share' }).click(); + await page + .getByRole('combobox', { + name: 'Visibility', + }) + .click(); + + await page + .getByRole('option', { + name: 'Public', + }) + .click(); + + await expect( + page.getByText('The document visibility has been updated.'), + ).toBeVisible(); + + await page.getByLabel('Can read and edit').click(); + + await expect( + page.getByText('The document visibility has been updated.').first(), + ).toBeVisible(); + + await page.locator('.c__modal__backdrop').click({ + position: { x: 0, y: 0 }, + }); + + await expect( + page + .getByLabel('It is the card information about the document.') + .getByText('Public', { exact: true }), + ).toBeVisible(); const urlDoc = page.url(); @@ -116,10 +358,134 @@ test.describe('Doc Visibility: Not loggued', () => { }) .click(); - await expect(page.getByRole('textbox', { name: 'password' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Sign in' })).toBeVisible(); await page.goto(urlDoc); - await expect(page.getByRole('textbox', { name: 'password' })).toBeVisible(); + await expect(page.locator('h2').getByText(docTitle)).toBeVisible(); + await expect(page.getByRole('button', { name: 'Share' })).toBeHidden(); + await expect( + page.getByText('Read only, you cannot edit this document'), + ).toBeHidden(); + }); +}); + +test.describe('Doc Visibility: Authenticated', () => { + test.use({ storageState: { cookies: [], origins: [] } }); + + test('A doc is not accessible when unauthentified.', async ({ + page, + browserName, + }) => { + await page.goto('/'); + await keyCloakSignIn(page, browserName); + + const [docTitle] = await createDoc( + page, + 'Authenticated unauthentified', + browserName, + 1, + ); + + await expect(page.getByRole('heading', { name: docTitle })).toBeVisible(); + + const urlDoc = page.url(); + + await page + .getByRole('button', { + name: 'Logout', + }) + .click(); + + await expect(page.getByRole('button', { name: 'Sign in' })).toBeVisible(); + + await page.goto(urlDoc); + + await expect(page.locator('h2').getByText(docTitle)).toBeHidden(); + await expect(page.getByRole('textbox', { name: 'password' })).toBeVisible(); + }); + + test('It checks a authenticated doc in read only mode', async ({ + page, + browserName, + }) => { + await page.goto('/'); + await keyCloakSignIn(page, browserName); + + const [docTitle] = await createDoc( + page, + 'Authenticated read only', + browserName, + 1, + ); + + await expect(page.getByRole('heading', { name: docTitle })).toBeVisible(); + + const urlDoc = page.url(); + + await page + .getByRole('button', { + name: 'Logout', + }) + .click(); + + const otherBrowser = browsersName.find((b) => b !== browserName); + await keyCloakSignIn(page, otherBrowser!); + + await page.goto(urlDoc); + + await expect(page.locator('h2').getByText(docTitle)).toBeVisible(); + await expect(page.getByRole('button', { name: 'Share' })).toBeVisible(); + await expect( + page.getByText('Read only, you cannot edit this document'), + ).toBeVisible(); + }); + + test('It checks a authenticated doc in editable mode', async ({ + page, + browserName, + }) => { + await page.goto('/'); + await keyCloakSignIn(page, browserName); + + const [docTitle] = await createDoc( + page, + 'Authenticated editable', + browserName, + 1, + ); + + await expect(page.getByRole('heading', { name: docTitle })).toBeVisible(); + + const urlDoc = page.url(); + + await page.getByRole('button', { name: 'Share' }).click(); + + await page.getByLabel('Can read and edit').click(); + + await expect( + page.getByText('The document visibility has been updated.').first(), + ).toBeVisible(); + + await page.locator('.c__modal__backdrop').click({ + position: { x: 0, y: 0 }, + }); + + await page + .getByRole('button', { + name: 'Logout', + }) + .click(); + + const otherBrowser = browsersName.find((b) => b !== browserName); + await keyCloakSignIn(page, otherBrowser!); + + await page.goto(urlDoc); + + await expect(page.locator('h2').getByText(docTitle)).toBeVisible(); + await expect(page.getByRole('button', { name: 'Share' })).toBeVisible(); + await expect( + page.getByText('Read only, you cannot edit this document'), + ).toBeHidden(); }); }); diff --git a/src/frontend/apps/impress/cunningham.ts b/src/frontend/apps/impress/cunningham.ts index 00f3c66a..1990d492 100644 --- a/src/frontend/apps/impress/cunningham.ts +++ b/src/frontend/apps/impress/cunningham.ts @@ -358,6 +358,8 @@ const config = { }, 'forms-field': { color: 'var(--c--theme--colors--primary-text)', + 'footer-font-size': 'var(--c--theme--font--sizes--t)', + 'footer-color': 'var(--c--theme--colors--greyscale-text)', }, 'forms-input': { 'border-radius': '4px', @@ -372,6 +374,9 @@ const config = { big: 'var(--c--theme--colors--primary-text)', }, }, + 'forms-radio': { + 'accent-color': 'var(--c--theme--colors--primary-600)', + }, 'forms-select': { 'item-font-size': '14px', 'border-radius': '4px', diff --git a/src/frontend/apps/impress/src/cunningham/cunningham-style.css b/src/frontend/apps/impress/src/cunningham/cunningham-style.css index 68dca6eb..3250fca1 100644 --- a/src/frontend/apps/impress/src/cunningham/cunningham-style.css +++ b/src/frontend/apps/impress/src/cunningham/cunningham-style.css @@ -16,6 +16,12 @@ line-height: initial; } +.c__field .c__field__footer { + padding: 2px 0 0; + font-size: var(--c--components--forms-field--footer-font-size); + color: var(--c--components--forms-field--footer-color); +} + .labelled-box label { color: var(--c--theme--colors--primary-text); } @@ -328,6 +334,10 @@ input:-webkit-autofill:focus { cursor: not-allowed; } +.c__checkbox.c__checkbox--disabled .c__checkbox__label { + color: var(--c--theme--colors--greyscale-400); +} + /** * Button */ diff --git a/src/frontend/apps/impress/src/cunningham/cunningham-tokens.css b/src/frontend/apps/impress/src/cunningham/cunningham-tokens.css index b30e3507..3d87ac17 100644 --- a/src/frontend/apps/impress/src/cunningham/cunningham-tokens.css +++ b/src/frontend/apps/impress/src/cunningham/cunningham-tokens.css @@ -477,6 +477,12 @@ --c--components--forms-datepicker--border-radius: 0; --c--components--forms-fileuploader--border-radius: 0; --c--components--forms-field--color: var(--c--theme--colors--primary-text); + --c--components--forms-field--footer-font-size: var( + --c--theme--font--sizes--t + ); + --c--components--forms-field--footer-color: var( + --c--theme--colors--greyscale-text + ); --c--components--forms-input--border-radius: 4px; --c--components--forms-input--background-color: #fff; --c--components--forms-input--border-color: var( @@ -492,6 +498,9 @@ --c--components--forms-labelledbox--label-color--big: var( --c--theme--colors--primary-text ); + --c--components--forms-radio--accent-color: var( + --c--theme--colors--primary-600 + ); --c--components--forms-select--item-font-size: 14px; --c--components--forms-select--border-radius: 4px; --c--components--forms-select--border-radius-hover: 4px; diff --git a/src/frontend/apps/impress/src/cunningham/cunningham-tokens.ts b/src/frontend/apps/impress/src/cunningham/cunningham-tokens.ts index d435b859..4e6bfdda 100644 --- a/src/frontend/apps/impress/src/cunningham/cunningham-tokens.ts +++ b/src/frontend/apps/impress/src/cunningham/cunningham-tokens.ts @@ -479,7 +479,11 @@ export const tokens = { }, 'forms-datepicker': { 'border-radius': '0' }, 'forms-fileuploader': { 'border-radius': '0' }, - 'forms-field': { color: 'var(--c--theme--colors--primary-text)' }, + 'forms-field': { + color: 'var(--c--theme--colors--primary-text)', + 'footer-font-size': 'var(--c--theme--font--sizes--t)', + 'footer-color': 'var(--c--theme--colors--greyscale-text)', + }, 'forms-input': { 'border-radius': '4px', 'background-color': '#ffffff', @@ -491,6 +495,9 @@ export const tokens = { 'forms-labelledbox': { 'label-color': { big: 'var(--c--theme--colors--primary-text)' }, }, + 'forms-radio': { + 'accent-color': 'var(--c--theme--colors--primary-600)', + }, 'forms-select': { 'item-font-size': '14px', 'border-radius': '4px', diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/components/DocVisibility.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/components/DocVisibility.tsx index 8d7f9628..fb0da949 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/components/DocVisibility.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-management/components/DocVisibility.tsx @@ -1,16 +1,17 @@ import { - Button, - Switch, + Radio, + RadioGroup, + Select, VariantType, useToastProvider, } from '@openfun/cunningham-react'; -import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Box, Card, IconBG } from '@/components'; +import { useCunninghamTheme } from '@/cunningham'; import { KEY_DOC, KEY_LIST_DOC, useUpdateDocLink } from '../api'; -import { Doc, LinkReach } from '../types'; +import { Doc, LinkReach, LinkRole } from '../types'; interface DocVisibilityProps { doc: Doc; @@ -18,14 +19,12 @@ interface DocVisibilityProps { export const DocVisibility = ({ doc }: DocVisibilityProps) => { const { t } = useTranslation(); - const [docPublic, setDocPublic] = useState( - doc.link_reach === LinkReach.PUBLIC, - ); const { toast } = useToastProvider(); + const { colorsTokens } = useCunninghamTheme(); const api = useUpdateDocLink({ onSuccess: () => { toast( - t('The document visiblitity has been updated.'), + t('The document visibility has been updated.'), VariantType.SUCCESS, { duration: 4000, @@ -35,6 +34,34 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => { listInvalideQueries: [KEY_LIST_DOC, KEY_DOC], }); + const transLinkReach = { + [LinkReach.RESTRICTED]: { + label: t('Restricted'), + description: t('Only for people with access'), + }, + [LinkReach.AUTHENTICATED]: { + label: t('Authenticated'), + description: t('Only for authenticated users'), + }, + [LinkReach.PUBLIC]: { + label: t('Public'), + description: t('Anyone on the internet with the link can view'), + }, + }; + + const linkRoleList = [ + { + label: t('Read only'), + value: LinkRole.READER, + }, + { + label: t('Can read and edit'), + value: LinkRole.EDITOR, + }, + ]; + + const showLinkRoleOptions = doc.link_reach !== LinkReach.RESTRICTED; + return ( { $align="center" $justify="space-between" $gap="1rem" + $wrap="wrap" > - + - - { + +