🛂(frontend) readers and editors can access share modal

Readers and editors of a document can access the share
modal and see the list of members and their roles.
This commit is contained in:
Anthony LC
2024-10-03 17:21:04 +02:00
committed by Anthony LC
parent eee20033ae
commit 0b15ebba71
16 changed files with 360 additions and 104 deletions

View File

@@ -21,6 +21,7 @@ and this project adheres to
- 💄(frontend) error alert closeable on editor #284 - 💄(frontend) error alert closeable on editor #284
- ♻️(backend) Change email content #283 - ♻️(backend) Change email content #283
- 🛂(frontend) viewers and editors can access share modal #302
## Fixed ## Fixed

View File

@@ -140,7 +140,13 @@ export const goToGridDoc = async (
export const mockedDocument = async (page: Page, json: object) => { export const mockedDocument = async (page: Page, json: object) => {
await page.route('**/documents/**/', async (route) => { await page.route('**/documents/**/', async (route) => {
const request = route.request(); 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({ await route.fulfill({
json: { json: {
id: 'mocked-document-id', 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();
}
});
};

View File

@@ -1,6 +1,12 @@
import { expect, test } from '@playwright/test'; import { expect, test } from '@playwright/test';
import { createDoc, goToGridDoc, mockedDocument } from './common'; import {
createDoc,
goToGridDoc,
mockedAccesses,
mockedDocument,
mockedInvitations,
} from './common';
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
await page.goto('/'); await page.goto('/');
@@ -182,20 +188,55 @@ test.describe('Doc Header', () => {
}, },
}); });
await mockedInvitations(page);
await mockedAccesses(page);
await goToGridDoc(page); await goToGridDoc(page);
await expect( await expect(
page.locator('h2').getByText('Mocked document'), page.locator('h2').getByText('Mocked document'),
).toHaveAttribute('contenteditable'); ).toHaveAttribute('contenteditable');
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
await page.getByLabel('Open the document options').click(); await page.getByLabel('Open the document options').click();
await expect(page.getByRole('button', { name: 'Export' })).toBeVisible(); await expect(page.getByRole('button', { name: 'Export' })).toBeVisible();
await expect( await expect(
page.getByRole('button', { name: 'Delete document' }), page.getByRole('button', { name: 'Delete document' }),
).toBeHidden(); ).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 }) => { 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 goToGridDoc(page);
await expect( await expect(
page.locator('h2').getByText('Mocked document'), page.locator('h2').getByText('Mocked document'),
).toHaveAttribute('contenteditable'); ).toHaveAttribute('contenteditable');
await expect(page.getByRole('button', { name: 'Share' })).toBeHidden();
await page.getByLabel('Open the document options').click(); await page.getByLabel('Open the document options').click();
await expect(page.getByRole('button', { name: 'Export' })).toBeVisible(); await expect(page.getByRole('button', { name: 'Export' })).toBeVisible();
await expect( await expect(
page.getByRole('button', { name: 'Delete document' }), page.getByRole('button', { name: 'Delete document' }),
).toBeHidden(); ).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 }) => { 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 goToGridDoc(page);
await expect( await expect(
page.locator('h2').getByText('Mocked document'), page.locator('h2').getByText('Mocked document'),
).not.toHaveAttribute('contenteditable'); ).not.toHaveAttribute('contenteditable');
await expect(page.getByRole('button', { name: 'Share' })).toBeHidden();
await page.getByLabel('Open the document options').click(); 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: 'Export' })).toBeVisible();
await expect( await expect(
page.getByRole('button', { name: 'Delete document' }), page.getByRole('button', { name: 'Delete document' }),
).toBeHidden(); ).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();
}); });
}); });

View File

@@ -106,15 +106,17 @@ test.describe('Document list members', () => {
await page.getByRole('option', { name: 'Administrator' }).click(); await page.getByRole('option', { name: 'Administrator' }).click();
await expect(page.getByText('The role has been updated')).toBeVisible(); await expect(page.getByText('The role has been updated')).toBeVisible();
const shareModal = page.getByLabel('Share modal');
// Admin still have the right to share // 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 SelectRoleCurrentUser.click();
await page.getByRole('option', { name: 'Reader' }).click(); await page.getByRole('option', { name: 'Reader' }).click();
await expect(page.getByText('The role has been updated')).toBeVisible(); await expect(page.getByText('The role has been updated')).toBeVisible();
// Reader does not have the right to share // 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 }) => { test('it checks the delete members', async ({ page, browserName }) => {

View File

@@ -33,6 +33,7 @@ export interface BoxProps {
$padding?: MarginPadding; $padding?: MarginPadding;
$position?: CSSProperties['position']; $position?: CSSProperties['position'];
$radius?: CSSProperties['borderRadius']; $radius?: CSSProperties['borderRadius'];
$shrink?: CSSProperties['flexShrink'];
$transition?: CSSProperties['transition']; $transition?: CSSProperties['transition'];
$width?: CSSProperties['width']; $width?: CSSProperties['width'];
$wrap?: CSSProperties['flexWrap']; $wrap?: CSSProperties['flexWrap'];
@@ -68,6 +69,7 @@ export const Box = styled('div')<BoxProps>`
${({ $padding }) => $padding && stylesPadding($padding)} ${({ $padding }) => $padding && stylesPadding($padding)}
${({ $position }) => $position && `position: ${$position};`} ${({ $position }) => $position && `position: ${$position};`}
${({ $radius }) => $radius && `border-radius: ${$radius};`} ${({ $radius }) => $radius && `border-radius: ${$radius};`}
${({ $shrink }) => $shrink && `flex-shrink: ${$shrink};`}
${({ $transition }) => $transition && `transition: ${$transition};`} ${({ $transition }) => $transition && `transition: ${$transition};`}
${({ $width }) => $width && `width: ${$width};`} ${({ $width }) => $width && `width: ${$width};`}
${({ $wrap }) => $wrap && `flex-wrap: ${$wrap};`} ${({ $wrap }) => $wrap && `flex-wrap: ${$wrap};`}

View File

@@ -150,6 +150,12 @@ input:-webkit-autofill:focus {
border-color: var(--c--components--forms-select--border-color-disabled-hover); 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 { .c__select__menu__item {
transition: all var(--c--theme--transitions--duration) transition: all var(--c--theme--transitions--duration)
var(--c--theme--transitions--ease-out); var(--c--theme--transitions--ease-out);
@@ -313,6 +319,14 @@ input:-webkit-autofill:focus {
font-size: var(--c--components--forms-checkbox--text--size); 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 * Button
*/ */

View File

@@ -31,15 +31,13 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
$align="center" $align="center"
$gap="1rem" $gap="1rem"
> >
{doc.abilities.manage_accesses && ( <Button
<Button onClick={() => {
onClick={() => { setIsModalShareOpen(true);
setIsModalShareOpen(true); }}
}} >
> {t('Share')}
{t('Share')} </Button>
</Button>
)}
<DropButton <DropButton
button={ button={
<IconOptions <IconOptions

View File

@@ -44,7 +44,7 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => {
$align="center" $align="center"
$justify="space-between" $justify="space-between"
> >
<Box $direction="row" $gap="1rem"> <Box $direction="row" $gap="1rem" $align="center">
<IconBG iconName="public" $margin="none" /> <IconBG iconName="public" $margin="none" />
<Switch <Switch
label={t(docPublic ? 'Doc public' : 'Doc private')} label={t(docPublic ? 'Doc public' : 'Doc private')}

View File

@@ -1,5 +1,4 @@
import { t } from 'i18next'; import { t } from 'i18next';
import { useEffect } from 'react';
import { createGlobalStyle } from 'styled-components'; import { createGlobalStyle } from 'styled-components';
import { Box, Card, SideModal, Text } from '@/components'; import { Box, Card, SideModal, Text } from '@/components';
@@ -30,12 +29,6 @@ interface ModalShareProps {
} }
export const ModalShare = ({ onClose, doc }: ModalShareProps) => { export const ModalShare = ({ onClose, doc }: ModalShareProps) => {
useEffect(() => {
if (!doc.abilities.manage_accesses) {
onClose();
}
}, [doc.abilities.manage_accesses, onClose]);
return ( return (
<> <>
<ModalShareStyle /> <ModalShareStyle />
@@ -47,29 +40,40 @@ export const ModalShare = ({ onClose, doc }: ModalShareProps) => {
width="70vw" width="70vw"
$css="min-width: 320px;max-width: 777px;" $css="min-width: 320px;max-width: 777px;"
> >
<Card <Box aria-label={t('Share modal')}>
$direction="row" <Box $shrink="0">
$align="center" <Card
$margin={{ horizontal: 'tiny', top: 'none', bottom: 'big' }} $direction="row"
$padding="tiny" $align="center"
$gap="1rem" $margin={{ horizontal: 'tiny', top: 'none', bottom: 'big' }}
> $padding="tiny"
<Text $isMaterialIcon $size="48px" $theme="primary"> $gap="1rem"
share >
</Text> <Text $isMaterialIcon $size="48px" $theme="primary">
<Box $align="flex-start"> share
<Text as="h3" $size="26px" $margin="none"> </Text>
{t('Share')} <Box $align="flex-start">
</Text> <Text as="h3" $size="26px" $margin="none">
<Text $size="small" $weight="normal" $textAlign="left"> {t('Share')}
{doc.title} </Text>
</Text> <Text $size="small" $weight="normal" $textAlign="left">
{doc.title}
</Text>
</Box>
</Card>
<DocVisibility doc={doc} />
{doc.abilities.manage_accesses && (
<AddMembers
doc={doc}
currentRole={currentDocRole(doc.abilities)}
/>
)}
</Box> </Box>
</Card> <Box $minHeight="0">
<DocVisibility doc={doc} /> <InvitationList doc={doc} />
<AddMembers doc={doc} currentRole={currentDocRole(doc.abilities)} /> <MemberList doc={doc} />
<InvitationList doc={doc} /> </Box>
<MemberList doc={doc} /> </Box>
</SideModal> </SideModal>
</> </>
); );

View File

@@ -93,6 +93,7 @@ export const TableContent = ({ doc, headings }: TableContentProps) => {
block: 'start', block: 'start',
}); });
}} }}
$align="start"
> >
<Text $theme="primary" $padding={{ vertical: 'xtiny' }}> <Text $theme="primary" $padding={{ vertical: 'xtiny' }}>
{t('Back to top')} {t('Back to top')}
@@ -110,6 +111,7 @@ export const TableContent = ({ doc, headings }: TableContentProps) => {
block: 'start', block: 'start',
}); });
}} }}
$align="start"
> >
<Text $theme="primary" $padding={{ vertical: 'xtiny' }}> <Text $theme="primary" $padding={{ vertical: 'xtiny' }}>
{t('Go to bottom')} {t('Go to bottom')}

View File

@@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next';
import { Box, IconBG, Text, TextErrors } from '@/components'; import { Box, IconBG, Text, TextErrors } from '@/components';
import { useCunninghamTheme } from '@/cunningham'; 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 { ChooseRole } from '@/features/docs/members/members-add/';
import { useDeleteDocInvitation, useUpdateDocInvitation } from '../api'; import { useDeleteDocInvitation, useUpdateDocInvitation } from '../api';
@@ -19,11 +19,11 @@ interface InvitationItemProps {
role: Role; role: Role;
currentRole: Role; currentRole: Role;
invitation: Invitation; invitation: Invitation;
docId: string; doc: Doc;
} }
export const InvitationItem = ({ export const InvitationItem = ({
docId, doc,
role, role,
invitation, invitation,
currentRole, currentRole,
@@ -95,29 +95,34 @@ export const InvitationItem = ({
setRole={(role) => { setRole={(role) => {
setLocalRole(role); setLocalRole(role);
updateDocInvitation({ updateDocInvitation({
docId, docId: doc.id,
invitationId: invitation.id, invitationId: invitation.id,
role, role,
}); });
}} }}
/> />
</Box> </Box>
<Button {doc.abilities.manage_accesses && (
color="tertiary-text" <Button
icon={ color="tertiary-text"
<Text icon={
$isMaterialIcon <Text
$theme={!canDelete ? 'greyscale' : 'primary'} $isMaterialIcon
$variation={!canDelete ? '500' : 'text'} $theme={!canDelete ? 'greyscale' : 'primary'}
> $variation={!canDelete ? '500' : 'text'}
delete >
</Text> delete
} </Text>
disabled={!canDelete} }
onClick={() => disabled={!canDelete}
removeDocInvitation({ docId, invitationId: invitation.id }) onClick={() =>
} removeDocInvitation({
/> docId: doc.id,
invitationId: invitation.id,
})
}
/>
)}
</Box> </Box>
</Box> </Box>
</Box> </Box>

View File

@@ -58,7 +58,7 @@ const InvitationListState = ({
<InvitationItem <InvitationItem
invitation={invitation} invitation={invitation}
role={invitation.role} role={invitation.role}
docId={doc.id} doc={doc}
currentRole={currentDocRole(doc.abilities)} currentRole={currentDocRole(doc.abilities)}
/> />
</Box> </Box>
@@ -97,9 +97,9 @@ export const InvitationList = ({ doc }: InvitationListProps) => {
return ( return (
<Card <Card
$margin="tiny" $margin="tiny"
$padding="tiny"
$maxHeight="40%"
$overflow="auto" $overflow="auto"
$maxHeight="50vh"
$padding="tiny"
aria-label={t('List invitation card')} aria-label={t('List invitation card')}
> >
<Box ref={containerRef} $overflow="auto"> <Box ref={containerRef} $overflow="auto">
@@ -111,8 +111,9 @@ export const InvitationList = ({ doc }: InvitationListProps) => {
}} }}
scrollContainer={containerRef.current} scrollContainer={containerRef.current}
as="ul" as="ul"
className="p-0 mt-0"
role="listbox" role="listbox"
$padding="none"
$margin="none"
> >
<InvitationListState <InvitationListState
isLoading={isLoading} isLoading={isLoading}

View File

@@ -153,14 +153,14 @@ export const AddMembers = ({ currentRole, doc }: ModalAddMembersProps) => {
doc={doc} doc={doc}
setSelectedUsers={setSelectedUsers} setSelectedUsers={setSelectedUsers}
selectedUsers={selectedUsers} selectedUsers={selectedUsers}
disabled={isPending} disabled={isPending || !doc.abilities.manage_accesses}
/> />
</Box> </Box>
<Box $css="flex: auto;"> <Box $css="flex: auto;">
<ChooseRole <ChooseRole
key={resetKey} key={resetKey}
currentRole={currentRole} currentRole={currentRole}
disabled={isPending} disabled={isPending || !doc.abilities.manage_accesses}
setRole={setSelectedRole} setRole={setSelectedRole}
/> />
</Box> </Box>
@@ -168,7 +168,12 @@ export const AddMembers = ({ currentRole, doc }: ModalAddMembersProps) => {
<Box $align="center" $justify="center" $css="flex: auto;"> <Box $align="center" $justify="center" $css="flex: auto;">
<Button <Button
color="primary" color="primary"
disabled={!selectedUsers.length || isPending || !selectedRole} disabled={
!selectedUsers.length ||
isPending ||
!selectedRole ||
!doc.abilities.manage_accesses
}
onClick={() => void handleValidate()} onClick={() => void handleValidate()}
style={{ height: '100%', maxHeight: '55px' }} style={{ height: '100%', maxHeight: '55px' }}
> >

View File

@@ -120,12 +120,17 @@ export const SearchUsers = ({
placeholder: (base) => ({ placeholder: (base) => ({
...base, ...base,
fontSize: '14px', fontSize: '14px',
color: colorsTokens()['primary-600'], color: disabled
? colorsTokens()['greyscale-300']
: colorsTokens()['primary-600'],
}), }),
control: (base) => ({ control: (base) => ({
...base, ...base,
minHeight: '45px', minHeight: '45px',
borderColor: colorsTokens()['primary-600'], borderColor: disabled
? colorsTokens()['greyscale-300']
: colorsTokens()['primary-600'],
backgroundColor: 'white',
}), }),
input: (base) => ({ input: (base) => ({
...base, ...base,

View File

@@ -10,7 +10,7 @@ import React, { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Box, IconBG, Text, TextErrors } from '@/components'; import { Box, IconBG, Text, TextErrors } from '@/components';
import { Access, Role } from '@/features/docs/doc-management'; import { Access, Doc, Role } from '@/features/docs/doc-management';
import { ChooseRole } from '@/features/docs/members/members-add/'; import { ChooseRole } from '@/features/docs/members/members-add/';
import { useDeleteDocAccess, useUpdateDocAccess } from '../api'; import { useDeleteDocAccess, useUpdateDocAccess } from '../api';
@@ -20,11 +20,11 @@ interface MemberItemProps {
role: Role; role: Role;
currentRole: Role; currentRole: Role;
access: Access; access: Access;
docId: string; doc: Doc;
} }
export const MemberItem = ({ export const MemberItem = ({
docId, doc,
role, role,
access, access,
currentRole, currentRole,
@@ -58,7 +58,8 @@ export const MemberItem = ({
}, },
}); });
const isNotAllowed = isOtherOwner || isLastOwner; const isNotAllowed =
isOtherOwner || isLastOwner || !doc.abilities.manage_accesses;
if (!access.user) { if (!access.user) {
return ( return (
@@ -91,34 +92,40 @@ export const MemberItem = ({
setRole={(role) => { setRole={(role) => {
setLocalRole(role); setLocalRole(role);
updateDocAccess({ updateDocAccess({
docId, docId: doc.id,
accessId: access.id, accessId: access.id,
role, role,
}); });
}} }}
/> />
</Box> </Box>
<Button {doc.abilities.manage_accesses && (
color="tertiary-text" <Button
icon={ color="tertiary-text"
<Text icon={
$isMaterialIcon <Text
$theme={isNotAllowed ? 'greyscale' : 'primary'} $isMaterialIcon
$variation={isNotAllowed ? '500' : 'text'} $theme={isNotAllowed ? 'greyscale' : 'primary'}
> $variation={isNotAllowed ? '500' : 'text'}
delete >
</Text> delete
} </Text>
disabled={isNotAllowed} }
onClick={() => removeDocAccess({ docId, accessId: access.id })} disabled={isNotAllowed}
/> onClick={() =>
removeDocAccess({ docId: doc.id, accessId: access.id })
}
/>
)}
</Box> </Box>
</Box> </Box>
</Box> </Box>
{(errorUpdate || errorDelete) && ( {(errorUpdate || errorDelete) && (
<TextErrors causes={errorUpdate?.cause || errorDelete?.cause} /> <Box $margin={{ top: 'tiny' }}>
<TextErrors causes={errorUpdate?.cause || errorDelete?.cause} />
</Box>
)} )}
{(isLastOwner || isOtherOwner) && ( {(isLastOwner || isOtherOwner) && doc.abilities.manage_accesses && (
<Box $margin={{ top: 'tiny' }}> <Box $margin={{ top: 'tiny' }}>
<Alert <Alert
canClose={false} canClose={false}

View File

@@ -57,7 +57,7 @@ const MemberListState = ({
<MemberItem <MemberItem
access={access} access={access}
role={access.role} role={access.role}
docId={doc.id} doc={doc}
currentRole={currentDocRole(doc.abilities)} currentRole={currentDocRole(doc.abilities)}
/> />
</Box> </Box>
@@ -92,9 +92,9 @@ export const MemberList = ({ doc }: MemberListProps) => {
return ( return (
<Card <Card
$margin="tiny" $margin="tiny"
$padding="tiny"
$maxHeight="69%"
$overflow="auto" $overflow="auto"
$maxHeight="80vh"
$padding="tiny"
aria-label={t('List members card')} aria-label={t('List members card')}
> >
<Box ref={containerRef} $overflow="auto"> <Box ref={containerRef} $overflow="auto">
@@ -106,7 +106,8 @@ export const MemberList = ({ doc }: MemberListProps) => {
}} }}
scrollContainer={containerRef.current} scrollContainer={containerRef.current}
as="ul" as="ul"
className="p-0 mt-0" $padding="none"
$margin="none"
role="listbox" role="listbox"
> >
<MemberListState <MemberListState