♻️(frontend) add user from side modal

We move the add user functionality to a side modal.
The side modal is opened from the share button.
This commit is contained in:
Anthony LC
2024-07-11 10:32:18 +02:00
committed by Anthony LC
parent ff832239d3
commit c2d6e60ae8
17 changed files with 241 additions and 152 deletions

View File

@@ -80,8 +80,7 @@ export const addNewMember = async (
response.status() === 200, response.status() === 200,
); );
await page.getByLabel('Open the document options').click(); await page.getByRole('button', { name: 'Share' }).click();
await page.getByRole('button', { name: 'Add members' }).click();
const inputSearch = page.getByLabel(/Find a member to add to the document/); const inputSearch = page.getByLabel(/Find a member to add to the document/);
@@ -98,8 +97,8 @@ export const addNewMember = async (
await page.getByRole('option', { name: users[index].email }).click(); await page.getByRole('option', { name: users[index].email }).click();
// Choose a role // Choose a role
await page.getByRole('radio', { name: role }).click(); await page.getByRole('combobox', { name: /Choose a role/ }).click();
await page.getByRole('option', { name: role }).click();
await page.getByRole('button', { name: 'Validate' }).click(); await page.getByRole('button', { name: 'Validate' }).click();
await expect( await expect(

View File

@@ -76,5 +76,6 @@ test.describe('Doc Header', () => {
card.getByText('Owners: super@owner.com / super2@owner.com'), card.getByText('Owners: super@owner.com / super2@owner.com'),
).toBeVisible(); ).toBeVisible();
await expect(card.getByText('Your role: Owner')).toBeVisible(); await expect(card.getByText('Your role: Owner')).toBeVisible();
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
}); });
}); });

View File

@@ -6,7 +6,7 @@ test.beforeEach(async ({ page }) => {
await page.goto('/'); await page.goto('/');
}); });
test.describe('Document add users', () => { test.describe('Document create member', () => {
test('it selects 2 users and 1 invitation', async ({ page, browserName }) => { test('it selects 2 users and 1 invitation', async ({ page, browserName }) => {
const responsePromise = page.waitForResponse( const responsePromise = page.waitForResponse(
(response) => (response) =>
@@ -14,8 +14,7 @@ test.describe('Document add users', () => {
); );
await createDoc(page, 'select-multi-users', browserName, 1); await createDoc(page, 'select-multi-users', browserName, 1);
await page.getByLabel('Open the document options').click(); await page.getByRole('button', { name: 'Share' }).click();
await page.getByRole('button', { name: 'Add members' }).click();
const inputSearch = page.getByLabel(/Find a member to add to the document/); const inputSearch = page.getByLabel(/Find a member to add to the document/);
await expect(inputSearch).toBeVisible(); await expect(inputSearch).toBeVisible();
@@ -56,12 +55,14 @@ test.describe('Document add users', () => {
await expect(page.getByLabel(`Remove ${email}`)).toBeVisible(); await expect(page.getByLabel(`Remove ${email}`)).toBeVisible();
// Check roles are displayed // Check roles are displayed
await expect(page.getByText(/Choose a role/)).toBeVisible(); await page.getByRole('combobox', { name: /Choose a role/ }).click();
await expect(page.getByRole('radio', { name: 'Reader' })).toBeChecked();
await expect(page.getByRole('radio', { name: 'Owner' })).toBeVisible(); await expect(page.getByRole('option', { name: 'Reader' })).toBeVisible();
await expect(page.getByRole('option', { name: 'Editor' })).toBeVisible();
await expect( await expect(
page.getByRole('radio', { name: 'Administrator' }), page.getByRole('option', { name: 'Administrator' }),
).toBeVisible(); ).toBeVisible();
await expect(page.getByRole('option', { name: 'Owner' })).toBeVisible();
}); });
test('it sends a new invitation and adds a new user', async ({ test('it sends a new invitation and adds a new user', async ({
@@ -75,8 +76,7 @@ test.describe('Document add users', () => {
await createDoc(page, 'user-invitation', browserName, 1); await createDoc(page, 'user-invitation', browserName, 1);
await page.getByLabel('Open the document options').click(); await page.getByRole('button', { name: 'Share' }).click();
await page.getByRole('button', { name: 'Add members' }).click();
const inputSearch = page.getByLabel(/Find a member to add to the document/); const inputSearch = page.getByLabel(/Find a member to add to the document/);
@@ -93,7 +93,8 @@ test.describe('Document add users', () => {
await page.getByRole('option', { name: user.email }).click(); await page.getByRole('option', { name: user.email }).click();
// Choose a role // Choose a role
await page.getByRole('radio', { name: 'Administrator' }).click(); await page.getByRole('combobox', { name: /Choose a role/ }).click();
await page.getByRole('option', { name: 'Administrator' }).click();
const responsePromiseCreateInvitation = page.waitForResponse( const responsePromiseCreateInvitation = page.waitForResponse(
(response) => (response) =>
@@ -127,8 +128,7 @@ test.describe('Document add users', () => {
await createDoc(page, 'user-twice', browserName, 1); await createDoc(page, 'user-twice', browserName, 1);
await page.getByLabel('Open the document options').click(); await page.getByRole('button', { name: 'Share' }).click();
await page.getByRole('button', { name: 'Add members' }).click();
const inputSearch = page.getByLabel(/Find a member to add to the document/); const inputSearch = page.getByLabel(/Find a member to add to the document/);
await inputSearch.fill('user'); await inputSearch.fill('user');
@@ -139,7 +139,8 @@ test.describe('Document add users', () => {
await page.getByRole('option', { name: user.email }).click(); await page.getByRole('option', { name: user.email }).click();
// Choose a role // Choose a role
await page.getByRole('radio', { name: 'Owner' }).click(); await page.getByRole('combobox', { name: /Choose a role/ }).click();
await page.getByRole('option', { name: 'Owner' }).click();
const responsePromiseAddMember = page.waitForResponse( const responsePromiseAddMember = page.waitForResponse(
(response) => (response) =>
@@ -154,10 +155,8 @@ test.describe('Document add users', () => {
const responseAddMember = await responsePromiseAddMember; const responseAddMember = await responsePromiseAddMember;
expect(responseAddMember.ok()).toBeTruthy(); expect(responseAddMember.ok()).toBeTruthy();
await page.getByLabel('Open the document options').click();
await page.getByRole('button', { name: 'Add members' }).click();
await inputSearch.fill('user'); await inputSearch.fill('user');
await expect(page.getByText('Loading...')).toBeHidden();
await expect(page.getByRole('option', { name: user.email })).toBeHidden(); await expect(page.getByRole('option', { name: user.email })).toBeHidden();
}); });
@@ -167,8 +166,7 @@ test.describe('Document add users', () => {
}) => { }) => {
await createDoc(page, 'invitation-twice', browserName, 1); await createDoc(page, 'invitation-twice', browserName, 1);
await page.getByLabel('Open the document options').click(); await page.getByRole('button', { name: 'Share' }).click();
await page.getByRole('button', { name: 'Add members' }).click();
const inputSearch = page.getByLabel(/Find a member to add to the document/); const inputSearch = page.getByLabel(/Find a member to add to the document/);
@@ -177,7 +175,8 @@ test.describe('Document add users', () => {
await page.getByRole('option', { name: email }).click(); await page.getByRole('option', { name: email }).click();
// Choose a role // Choose a role
await page.getByRole('radio', { name: 'Owner' }).click(); await page.getByRole('combobox', { name: /Choose a role/ }).click();
await page.getByRole('option', { name: 'Owner' }).click();
const responsePromiseCreateInvitation = page.waitForResponse( const responsePromiseCreateInvitation = page.waitForResponse(
(response) => (response) =>
@@ -191,10 +190,8 @@ test.describe('Document add users', () => {
const responseCreateInvitation = await responsePromiseCreateInvitation; const responseCreateInvitation = await responsePromiseCreateInvitation;
expect(responseCreateInvitation.ok()).toBeTruthy(); expect(responseCreateInvitation.ok()).toBeTruthy();
await page.getByLabel('Open the document options').click();
await page.getByRole('button', { name: 'Add members' }).click();
await inputSearch.fill(email); await inputSearch.fill(email);
await expect(page.getByText('Loading...')).toBeHidden();
await expect(page.getByRole('option', { name: email })).toBeHidden(); await expect(page.getByRole('option', { name: email })).toBeHidden();
}); });
}); });

View File

@@ -216,11 +216,10 @@ test.describe('Doc Tools', () => {
await expect(page.locator('h2').getByText('Mocked document')).toBeVisible(); await expect(page.locator('h2').getByText('Mocked document')).toBeVisible();
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: 'Add members' }),
).toBeVisible();
await expect( await expect(
page.getByRole('button', { name: 'Generate PDF' }), page.getByRole('button', { name: 'Generate PDF' }),
).toBeVisible(); ).toBeVisible();
@@ -267,11 +266,10 @@ test.describe('Doc Tools', () => {
await expect(page.locator('h2').getByText('Mocked document')).toBeVisible(); await expect(page.locator('h2').getByText('Mocked document')).toBeVisible();
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: 'Add members' }),
).toBeHidden();
await expect( await expect(
page.getByRole('button', { name: 'Generate PDF' }), page.getByRole('button', { name: 'Generate PDF' }),
).toBeVisible(); ).toBeVisible();
@@ -318,11 +316,11 @@ test.describe('Doc Tools', () => {
await expect(page.locator('h2').getByText('Mocked document')).toBeVisible(); await expect(page.locator('h2').getByText('Mocked document')).toBeVisible();
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( await expect(page.getByRole('button', { name: 'Share' })).toBeHidden();
page.getByRole('button', { name: 'Add members' }),
).toBeHidden();
await expect( await expect(
page.getByRole('button', { name: 'Generate PDF' }), page.getByRole('button', { name: 'Generate PDF' }),
).toBeVisible(); ).toBeVisible();

View File

@@ -5,10 +5,23 @@ import { createGlobalStyle } from 'styled-components';
interface SideModalStyleProps { interface SideModalStyleProps {
side: 'left' | 'right'; side: 'left' | 'right';
width: string; width: string;
$css?: string;
} }
const SideModalStyle = createGlobalStyle<SideModalStyleProps>` const SideModalStyle = createGlobalStyle<SideModalStyleProps>`
@keyframes slidein {
from {
transform: translateX(100%);
}
to {
transform: translateX(0%);
}
}
& .c__modal{ & .c__modal{
animation: slidein 0.7s;
width: ${({ width }) => width}; width: ${({ width }) => width};
${({ side }) => side === 'right' && 'left: auto;'}; ${({ side }) => side === 'right' && 'left: auto;'};
@@ -17,6 +30,8 @@ const SideModalStyle = createGlobalStyle<SideModalStyleProps>`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
${({ $css }) => $css}
} }
`; `;
@@ -28,11 +43,12 @@ export const SideModal = ({
children, children,
side = 'right', side = 'right',
width = '35vw', width = '35vw',
$css,
...modalProps ...modalProps
}: PropsWithChildren<SideModalProps>) => { }: PropsWithChildren<SideModalProps>) => {
return ( return (
<> <>
<SideModalStyle width={width} side={side} /> <SideModalStyle width={width} side={side} $css={$css} />
<Modal {...modalProps} size={ModalSize.FULL}> <Modal {...modalProps} size={ModalSize.FULL}>
{children} {children}
</Modal> </Modal>

View File

@@ -14,6 +14,7 @@ export interface TextProps extends BoxProps {
'p' | 'span' | 'div' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' 'p' | 'span' | 'div' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
>; >;
$elipsis?: boolean; $elipsis?: boolean;
$isMaterialIcon?: boolean;
$weight?: CSSProperties['fontWeight']; $weight?: CSSProperties['fontWeight'];
$textAlign?: CSSProperties['textAlign']; $textAlign?: CSSProperties['textAlign'];
// eslint-disable-next-line @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/ban-types
@@ -56,9 +57,17 @@ export const TextStyled = styled(Box)<TextProps>`
`; `;
export const Text = ({ export const Text = ({
className,
$isMaterialIcon,
...props ...props
}: ComponentPropsWithRef<typeof TextStyled>) => { }: ComponentPropsWithRef<typeof TextStyled>) => {
return ( return (
<TextStyled as="span" $theme="greyscale" $variation="text" {...props} /> <TextStyled
as="span"
$theme="greyscale"
$variation="text"
className={`${className || ''}${$isMaterialIcon ? ' material-icons' : ''}`}
{...props}
/>
); );
}; };

View File

@@ -477,6 +477,10 @@ input:-webkit-autofill:focus {
padding: 1.5rem 1rem; padding: 1.5rem 1rem;
} }
.c__modal--full .c__modal__content {
overflow-y: auto;
}
/** /**
* Toast * Toast
*/ */

View File

@@ -31,7 +31,7 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
<Box $padding="small" $direction="row" $align="center"> <Box $padding="small" $direction="row" $align="center">
<StyledLink href="/"> <StyledLink href="/">
<Text <Text
className="material-icons" $isMaterialIcon
$theme="primary" $theme="primary"
$size="2rem" $size="2rem"
$css={`&:hover {background-color: ${colorsTokens()['primary-100']}; };`} $css={`&:hover {background-color: ${colorsTokens()['primary-100']}; };`}
@@ -82,7 +82,9 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
{t('Owners:')}{' '} {t('Owners:')}{' '}
<strong> <strong>
{doc.accesses {doc.accesses
.filter((access) => access.role === Role.OWNER) .filter(
(access) => access.role === Role.OWNER && access.user.email,
)
.map((access, index, accesses) => ( .map((access, index, accesses) => (
<Fragment key={`access-${index}`}> <Fragment key={`access-${index}`}>
{access.user.email}{' '} {access.user.email}{' '}

View File

@@ -6,10 +6,9 @@ import { Box, DropButton, IconOptions, Text } from '@/components';
import { import {
Doc, Doc,
ModalRemoveDoc, ModalRemoveDoc,
ModalShare,
ModalUpdateDoc, ModalUpdateDoc,
currentDocRole,
} from '@/features/docs/doc-management'; } from '@/features/docs/doc-management';
import { ModalAddMembers } from '@/features/docs/members/members-add';
import { ModalGridMembers } from '@/features/docs/members/members-grid/'; import { ModalGridMembers } from '@/features/docs/members/members-grid/';
import { ModalPDF } from './ModalPDF'; import { ModalPDF } from './ModalPDF';
@@ -20,15 +19,29 @@ interface DocToolBoxProps {
export const DocToolBox = ({ doc }: DocToolBoxProps) => { export const DocToolBox = ({ doc }: DocToolBoxProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [isModalAddMembersOpen, setIsModalAddMembersOpen] = useState(false);
const [isModalGridMembersOpen, setIsModalGridMembersOpen] = useState(false); const [isModalGridMembersOpen, setIsModalGridMembersOpen] = useState(false);
const [isModalShareOpen, setIsModalShareOpen] = useState(false);
const [isModalUpdateOpen, setIsModalUpdateOpen] = useState(false); const [isModalUpdateOpen, setIsModalUpdateOpen] = useState(false);
const [isModalRemoveOpen, setIsModalRemoveOpen] = useState(false); const [isModalRemoveOpen, setIsModalRemoveOpen] = useState(false);
const [isModalPDFOpen, setIsModalPDFOpen] = useState(false); const [isModalPDFOpen, setIsModalPDFOpen] = useState(false);
const [isDropOpen, setIsDropOpen] = useState(false); const [isDropOpen, setIsDropOpen] = useState(false);
return ( return (
<Box $margin={{ left: 'auto' }}> <Box
$margin={{ left: 'auto' }}
$direction="row"
$align="center"
$gap="1rem"
>
{doc.abilities.manage_accesses && (
<Button
onClick={() => {
setIsModalShareOpen(true);
}}
>
{t('Share')}
</Button>
)}
<DropButton <DropButton
button={ button={
<IconOptions <IconOptions
@@ -42,17 +55,6 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
<Box> <Box>
{doc.abilities.manage_accesses && ( {doc.abilities.manage_accesses && (
<> <>
<Button
onClick={() => {
setIsModalAddMembersOpen(true);
setIsDropOpen(false);
}}
color="primary-text"
icon={<span className="material-icons">person_add</span>}
size="small"
>
<Text $theme="primary">{t('Add members')}</Text>
</Button>
<Button <Button
onClick={() => { onClick={() => {
setIsModalGridMembersOpen(true); setIsModalGridMembersOpen(true);
@@ -105,19 +107,15 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
</Button> </Button>
</Box> </Box>
</DropButton> </DropButton>
{isModalShareOpen && (
<ModalShare onClose={() => setIsModalShareOpen(false)} doc={doc} />
)}
{isModalGridMembersOpen && ( {isModalGridMembersOpen && (
<ModalGridMembers <ModalGridMembers
onClose={() => setIsModalGridMembersOpen(false)} onClose={() => setIsModalGridMembersOpen(false)}
doc={doc} doc={doc}
/> />
)} )}
{isModalAddMembersOpen && (
<ModalAddMembers
onClose={() => setIsModalAddMembersOpen(false)}
doc={doc}
currentRole={currentDocRole(doc.abilities)}
/>
)}
{isModalPDFOpen && ( {isModalPDFOpen && (
<ModalPDF onClose={() => setIsModalPDFOpen(false)} doc={doc} /> <ModalPDF onClose={() => setIsModalPDFOpen(false)} doc={doc} />
)} )}

View File

@@ -0,0 +1,61 @@
import { t } from 'i18next';
import { useEffect } from 'react';
import { createGlobalStyle } from 'styled-components';
import { Box, Card, Text } from '@/components';
import { SideModal } from '@/components/SideModal';
import { AddMembers } from '../../members/members-add';
import { Doc } from '../types';
import { currentDocRole } from '../utils';
const ModalShareStyle = createGlobalStyle`
& .c__modal__scroller{
background: #FAFAFA;
padding: 1.5rem .5rem;
}
`;
interface ModalShareProps {
onClose: () => void;
doc: Doc;
}
export const ModalShare = ({ onClose, doc }: ModalShareProps) => {
useEffect(() => {
if (!doc.abilities.manage_accesses) {
onClose();
}
}, [doc.abilities.manage_accesses, onClose]);
return (
<>
<ModalShareStyle />
<SideModal
isOpen
closeOnClickOutside
hideCloseButton
onClose={onClose}
width="45vw"
$css="min-width: 320px;"
title={
<Card $direction="row" $align="center" $padding="0.7rem" $gap="1rem">
<Text $isMaterialIcon $size="48px" $theme="primary">
share
</Text>
<Box $align="flex-start">
<Text as="h3" $size="26px" $margin="none">
{t('Share')}
</Text>
<Text $size="small" $weight="normal" $textAlign="left">
{doc.title}
</Text>
</Box>
</Card>
}
>
<AddMembers doc={doc} currentRole={currentDocRole(doc.abilities)} />
</SideModal>
</>
);
};

View File

@@ -1,2 +1,3 @@
export * from './ModalRemoveDoc';
export * from './ModalCreateUpdateDoc'; export * from './ModalCreateUpdateDoc';
export * from './ModalRemoveDoc';
export * from './ModalShare';

View File

@@ -27,6 +27,7 @@ const DocsGridStyle = createGlobalStyle`
position: sticky; position: sticky;
top: 0; top: 0;
background: #fff; background: #fff;
z-index: 1;
} }
& .c__pagination__goto{ & .c__pagination__goto{
display:none; display:none;

View File

@@ -1,21 +1,17 @@
import { import {
Button, Button,
Modal,
ModalSize,
VariantType, VariantType,
useToastProvider, useToastProvider,
} from '@openfun/cunningham-react'; } from '@openfun/cunningham-react';
import { useState } from 'react'; import { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { createGlobalStyle } from 'styled-components';
import { APIError } from '@/api'; import { APIError } from '@/api';
import { Box, Text } from '@/components'; import { Box, Card, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham'; import { useCunninghamTheme } from '@/cunningham';
import { Doc, Role } from '@/features/docs/doc-management'; import { Doc, Role } from '@/features/docs/doc-management';
import { useCreateDocAccess, useCreateInvitation } from '../api'; import { useCreateDocAccess, useCreateInvitation } from '../api';
import IconAddUser from '../assets/add-user.svg';
import { import {
OptionInvitation, OptionInvitation,
OptionNewMember, OptionNewMember,
@@ -27,12 +23,6 @@ import {
import { ChooseRole } from './ChooseRole'; import { ChooseRole } from './ChooseRole';
import { OptionsSelect, SearchUsers } from './SearchUsers'; import { OptionsSelect, SearchUsers } from './SearchUsers';
const GlobalStyle = createGlobalStyle`
.c__modal {
overflow: visible;
}
`;
type APIErrorUser = APIError<{ type APIErrorUser = APIError<{
value: string; value: string;
type: OptionType; type: OptionType;
@@ -40,26 +30,22 @@ type APIErrorUser = APIError<{
interface ModalAddMembersProps { interface ModalAddMembersProps {
currentRole: Role; currentRole: Role;
onClose: () => void;
doc: Doc; doc: Doc;
} }
export const ModalAddMembers = ({ export const AddMembers = ({ currentRole, doc }: ModalAddMembersProps) => {
currentRole,
onClose,
doc,
}: ModalAddMembersProps) => {
const { colorsTokens } = useCunninghamTheme();
const { t } = useTranslation(); const { t } = useTranslation();
const { colorsTokens } = useCunninghamTheme();
const [selectedUsers, setSelectedUsers] = useState<OptionsSelect>([]); const [selectedUsers, setSelectedUsers] = useState<OptionsSelect>([]);
const [selectedRole, setSelectedRole] = useState<Role>(Role.READER); const [selectedRole, setSelectedRole] = useState<Role>();
const { toast } = useToastProvider(); const { toast } = useToastProvider();
const { mutateAsync: createInvitation } = useCreateInvitation(); const { mutateAsync: createInvitation } = useCreateInvitation();
const { mutateAsync: createDocAccess } = useCreateDocAccess(); const { mutateAsync: createDocAccess } = useCreateDocAccess();
const [resetKey, setResetKey] = useState(1);
const [isPending, setIsPending] = useState<boolean>(false); const [isPending, setIsPending] = useState<boolean>(false);
const switchActions = (selectedUsers: OptionsSelect) => const switchActions = (selectedUsers: OptionsSelect, selectedRole: Role) =>
selectedUsers.map(async (selectedUser) => { selectedUsers.map(async (selectedUser) => {
switch (selectedUser.type) { switch (selectedUser.type) {
case OptionType.INVITATION: case OptionType.INVITATION:
@@ -112,12 +98,16 @@ export const ModalAddMembers = ({
const handleValidate = async () => { const handleValidate = async () => {
setIsPending(true); setIsPending(true);
if (!selectedRole) {
return;
}
const settledPromises = await Promise.allSettled< const settledPromises = await Promise.allSettled<
OptionInvitation | OptionNewMember OptionInvitation | OptionNewMember
>(switchActions(selectedUsers)); >(switchActions(selectedUsers, selectedRole));
onClose();
setIsPending(false); setIsPending(false);
setResetKey(resetKey + 1);
settledPromises.forEach((settledPromise) => { settledPromises.forEach((settledPromise) => {
switch (settledPromise.status) { switch (settledPromise.status) {
@@ -133,63 +123,57 @@ export const ModalAddMembers = ({
}; };
return ( return (
<Modal <Card
isOpen $gap="1rem"
leftActions={ $padding="1rem"
<Button $margin="1rem 0.7rem"
color="secondary" $direction="row"
fullWidth $align="center"
onClick={onClose} $wrap="wrap"
disabled={isPending}
>
{t('Cancel')}
</Button>
}
onClose={onClose}
closeOnClickOutside
hideCloseButton
rightActions={
<Button
color="primary"
fullWidth
disabled={!selectedUsers.length || isPending}
onClick={() => void handleValidate()}
>
{t('Validate')}
</Button>
}
size={ModalSize.MEDIUM}
title={
<Box $align="center" $gap="1rem">
<IconAddUser width={48} color={colorsTokens()['primary-text']} />
<Text $size="h3" $margin="none">
{t('Add members to the document')}
</Text>
</Box>
}
> >
<GlobalStyle /> <Text
<Box $margin={{ bottom: 'xl', top: 'large' }}> $isMaterialIcon
<SearchUsers $size="44px"
doc={doc} $theme="primary"
setSelectedUsers={setSelectedUsers} $background={colorsTokens()['primary-bg']}
selectedUsers={selectedUsers} $css={`border: 1px solid ${colorsTokens()['primary-200']}`}
disabled={isPending} $radius="12px"
/> $padding="4px"
{selectedUsers.length >= 0 && ( $margin="auto"
<Box $margin={{ top: 'small' }}> >
<Text as="h4" $textAlign="left" $margin={{ bottom: 'tiny' }}> group_add
{t('Choose a role')} </Text>
</Text> <Box $gap="0.7rem" $direction="row" $wrap="wrap" $css="flex: 70%;">
<Box $gap="0.7rem" $direction="row" $wrap="wrap" $css="flex: 80%;">
<Box $css="flex: auto;">
<SearchUsers
key={resetKey + 1}
doc={doc}
setSelectedUsers={setSelectedUsers}
selectedUsers={selectedUsers}
disabled={isPending}
/>
</Box>
<Box $css="flex: auto;">
<ChooseRole <ChooseRole
key={resetKey}
currentRole={currentRole} currentRole={currentRole}
disabled={isPending} disabled={isPending}
defaultRole={Role.READER}
setRole={setSelectedRole} setRole={setSelectedRole}
/> />
</Box> </Box>
)} </Box>
<Box $align="center" $justify="center" $css="flex: auto;">
<Button
color="primary"
disabled={!selectedUsers.length || isPending || !selectedRole}
onClick={() => void handleValidate()}
style={{ height: '100%', maxHeight: '55px' }}
>
{t('Validate')}
</Button>
</Box>
</Box> </Box>
</Modal> </Card>
); );
}; };

View File

@@ -1,11 +1,12 @@
import { Radio, RadioGroup } from '@openfun/cunningham-react'; import { Select } from '@openfun/cunningham-react';
import { useTranslation } from 'react-i18next';
import { Role, useTransRole } from '@/features/docs/doc-management'; import { Role, useTransRole } from '@/features/docs/doc-management';
interface ChooseRoleProps { interface ChooseRoleProps {
currentRole: Role; currentRole: Role;
disabled: boolean; disabled: boolean;
defaultRole: Role; defaultRole?: Role;
setRole: (role: Role) => void; setRole: (role: Role) => void;
} }
@@ -15,23 +16,20 @@ export const ChooseRole = ({
currentRole, currentRole,
setRole, setRole,
}: ChooseRoleProps) => { }: ChooseRoleProps) => {
const { t } = useTranslation();
const transRole = useTransRole(); const transRole = useTransRole();
return ( return (
<RadioGroup> <Select
{Object.values(Role).map((role) => ( label={t('Choose a role')}
<Radio options={Object.values(Role).map((role) => ({
key={role} label: transRole(role),
label={transRole(role)} value: role,
value={role} disabled: currentRole !== Role.OWNER && role === Role.OWNER,
name="role" }))}
onChange={(evt) => setRole(evt.target.value as Role)} onChange={(evt) => setRole(evt.target.value as Role)}
defaultChecked={defaultRole === role} disabled={disabled}
disabled={ value={defaultRole}
disabled || (currentRole !== Role.OWNER && role === Role.OWNER) />
}
/>
))}
</RadioGroup>
); );
}; };

View File

@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
import { Options } from 'react-select'; import { Options } from 'react-select';
import AsyncSelect from 'react-select/async'; import AsyncSelect from 'react-select/async';
import { useCunninghamTheme } from '@/cunningham';
import { Doc } from '@/features/docs/doc-management'; import { Doc } from '@/features/docs/doc-management';
import { isValidEmail } from '@/utils'; import { isValidEmail } from '@/utils';
@@ -24,6 +25,7 @@ export const SearchUsers = ({
setSelectedUsers, setSelectedUsers,
disabled, disabled,
}: SearchUsersProps) => { }: SearchUsersProps) => {
const { colorsTokens } = useCunninghamTheme();
const { t } = useTranslation(); const { t } = useTranslation();
const [input, setInput] = useState(''); const [input, setInput] = useState('');
const [userQuery, setUserQuery] = useState(''); const [userQuery, setUserQuery] = useState('');
@@ -102,6 +104,23 @@ export const SearchUsers = ({
return ( return (
<AsyncSelect <AsyncSelect
styles={{
placeholder: (base) => ({
...base,
fontSize: '14px',
color: colorsTokens()['primary-600'],
}),
control: (base) => ({
...base,
minHeight: '45px',
borderColor: colorsTokens()['primary-600'],
}),
input: (base) => ({
...base,
minHeight: '45px',
fontSize: '14px',
}),
}}
isDisabled={disabled} isDisabled={disabled}
aria-label={t('Find a member to add to the document')} aria-label={t('Find a member to add to the document')}
isMulti isMulti

View File

@@ -0,0 +1 @@
export * from './AddMembers';

View File

@@ -1 +1 @@
export * from './components/ModalAddMembers'; export * from './components';