♻️(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:
@@ -80,8 +80,7 @@ export const addNewMember = async (
|
||||
response.status() === 200,
|
||||
);
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
await page.getByRole('button', { name: 'Add members' }).click();
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
|
||||
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();
|
||||
|
||||
// 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 expect(
|
||||
|
||||
@@ -76,5 +76,6 @@ test.describe('Doc Header', () => {
|
||||
card.getByText('Owners: super@owner.com / super2@owner.com'),
|
||||
).toBeVisible();
|
||||
await expect(card.getByText('Your role: Owner')).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test.describe('Document add users', () => {
|
||||
test.describe('Document create member', () => {
|
||||
test('it selects 2 users and 1 invitation', async ({ page, browserName }) => {
|
||||
const responsePromise = page.waitForResponse(
|
||||
(response) =>
|
||||
@@ -14,8 +14,7 @@ test.describe('Document add users', () => {
|
||||
);
|
||||
await createDoc(page, 'select-multi-users', browserName, 1);
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
await page.getByRole('button', { name: 'Add members' }).click();
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
|
||||
const inputSearch = page.getByLabel(/Find a member to add to the document/);
|
||||
await expect(inputSearch).toBeVisible();
|
||||
@@ -56,12 +55,14 @@ test.describe('Document add users', () => {
|
||||
await expect(page.getByLabel(`Remove ${email}`)).toBeVisible();
|
||||
|
||||
// Check roles are displayed
|
||||
await expect(page.getByText(/Choose a role/)).toBeVisible();
|
||||
await expect(page.getByRole('radio', { name: 'Reader' })).toBeChecked();
|
||||
await expect(page.getByRole('radio', { name: 'Owner' })).toBeVisible();
|
||||
await page.getByRole('combobox', { name: /Choose a role/ }).click();
|
||||
|
||||
await expect(page.getByRole('option', { name: 'Reader' })).toBeVisible();
|
||||
await expect(page.getByRole('option', { name: 'Editor' })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('radio', { name: 'Administrator' }),
|
||||
page.getByRole('option', { name: 'Administrator' }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole('option', { name: 'Owner' })).toBeVisible();
|
||||
});
|
||||
|
||||
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 page.getByLabel('Open the document options').click();
|
||||
await page.getByRole('button', { name: 'Add members' }).click();
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
|
||||
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();
|
||||
|
||||
// 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(
|
||||
(response) =>
|
||||
@@ -127,8 +128,7 @@ test.describe('Document add users', () => {
|
||||
|
||||
await createDoc(page, 'user-twice', browserName, 1);
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
await page.getByRole('button', { name: 'Add members' }).click();
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
|
||||
const inputSearch = page.getByLabel(/Find a member to add to the document/);
|
||||
await inputSearch.fill('user');
|
||||
@@ -139,7 +139,8 @@ test.describe('Document add users', () => {
|
||||
await page.getByRole('option', { name: user.email }).click();
|
||||
|
||||
// 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(
|
||||
(response) =>
|
||||
@@ -154,10 +155,8 @@ test.describe('Document add users', () => {
|
||||
const responseAddMember = await responsePromiseAddMember;
|
||||
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 expect(page.getByText('Loading...')).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 page.getByLabel('Open the document options').click();
|
||||
await page.getByRole('button', { name: 'Add members' }).click();
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
|
||||
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();
|
||||
|
||||
// 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(
|
||||
(response) =>
|
||||
@@ -191,10 +190,8 @@ test.describe('Document add users', () => {
|
||||
const responseCreateInvitation = await responsePromiseCreateInvitation;
|
||||
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 expect(page.getByText('Loading...')).toBeHidden();
|
||||
await expect(page.getByRole('option', { name: email })).toBeHidden();
|
||||
});
|
||||
});
|
||||
@@ -216,11 +216,10 @@ test.describe('Doc Tools', () => {
|
||||
|
||||
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 expect(
|
||||
page.getByRole('button', { name: 'Add members' }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Generate PDF' }),
|
||||
).toBeVisible();
|
||||
@@ -267,11 +266,10 @@ test.describe('Doc Tools', () => {
|
||||
|
||||
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 expect(
|
||||
page.getByRole('button', { name: 'Add members' }),
|
||||
).toBeHidden();
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Generate PDF' }),
|
||||
).toBeVisible();
|
||||
@@ -318,11 +316,11 @@ test.describe('Doc Tools', () => {
|
||||
|
||||
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 expect(
|
||||
page.getByRole('button', { name: 'Add members' }),
|
||||
).toBeHidden();
|
||||
await expect(page.getByRole('button', { name: 'Share' })).toBeHidden();
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Generate PDF' }),
|
||||
).toBeVisible();
|
||||
|
||||
@@ -5,10 +5,23 @@ import { createGlobalStyle } from 'styled-components';
|
||||
interface SideModalStyleProps {
|
||||
side: 'left' | 'right';
|
||||
width: string;
|
||||
$css?: string;
|
||||
}
|
||||
|
||||
const SideModalStyle = createGlobalStyle<SideModalStyleProps>`
|
||||
@keyframes slidein {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(0%);
|
||||
}
|
||||
}
|
||||
|
||||
& .c__modal{
|
||||
animation: slidein 0.7s;
|
||||
|
||||
width: ${({ width }) => width};
|
||||
${({ side }) => side === 'right' && 'left: auto;'};
|
||||
|
||||
@@ -17,6 +30,8 @@ const SideModalStyle = createGlobalStyle<SideModalStyleProps>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
${({ $css }) => $css}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -28,11 +43,12 @@ export const SideModal = ({
|
||||
children,
|
||||
side = 'right',
|
||||
width = '35vw',
|
||||
$css,
|
||||
...modalProps
|
||||
}: PropsWithChildren<SideModalProps>) => {
|
||||
return (
|
||||
<>
|
||||
<SideModalStyle width={width} side={side} />
|
||||
<SideModalStyle width={width} side={side} $css={$css} />
|
||||
<Modal {...modalProps} size={ModalSize.FULL}>
|
||||
{children}
|
||||
</Modal>
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface TextProps extends BoxProps {
|
||||
'p' | 'span' | 'div' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
|
||||
>;
|
||||
$elipsis?: boolean;
|
||||
$isMaterialIcon?: boolean;
|
||||
$weight?: CSSProperties['fontWeight'];
|
||||
$textAlign?: CSSProperties['textAlign'];
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
@@ -56,9 +57,17 @@ export const TextStyled = styled(Box)<TextProps>`
|
||||
`;
|
||||
|
||||
export const Text = ({
|
||||
className,
|
||||
$isMaterialIcon,
|
||||
...props
|
||||
}: ComponentPropsWithRef<typeof TextStyled>) => {
|
||||
return (
|
||||
<TextStyled as="span" $theme="greyscale" $variation="text" {...props} />
|
||||
<TextStyled
|
||||
as="span"
|
||||
$theme="greyscale"
|
||||
$variation="text"
|
||||
className={`${className || ''}${$isMaterialIcon ? ' material-icons' : ''}`}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -477,6 +477,10 @@ input:-webkit-autofill:focus {
|
||||
padding: 1.5rem 1rem;
|
||||
}
|
||||
|
||||
.c__modal--full .c__modal__content {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toast
|
||||
*/
|
||||
|
||||
@@ -31,7 +31,7 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
|
||||
<Box $padding="small" $direction="row" $align="center">
|
||||
<StyledLink href="/">
|
||||
<Text
|
||||
className="material-icons"
|
||||
$isMaterialIcon
|
||||
$theme="primary"
|
||||
$size="2rem"
|
||||
$css={`&:hover {background-color: ${colorsTokens()['primary-100']}; };`}
|
||||
@@ -82,7 +82,9 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
|
||||
{t('Owners:')}{' '}
|
||||
<strong>
|
||||
{doc.accesses
|
||||
.filter((access) => access.role === Role.OWNER)
|
||||
.filter(
|
||||
(access) => access.role === Role.OWNER && access.user.email,
|
||||
)
|
||||
.map((access, index, accesses) => (
|
||||
<Fragment key={`access-${index}`}>
|
||||
{access.user.email}{' '}
|
||||
|
||||
@@ -6,10 +6,9 @@ import { Box, DropButton, IconOptions, Text } from '@/components';
|
||||
import {
|
||||
Doc,
|
||||
ModalRemoveDoc,
|
||||
ModalShare,
|
||||
ModalUpdateDoc,
|
||||
currentDocRole,
|
||||
} from '@/features/docs/doc-management';
|
||||
import { ModalAddMembers } from '@/features/docs/members/members-add';
|
||||
import { ModalGridMembers } from '@/features/docs/members/members-grid/';
|
||||
|
||||
import { ModalPDF } from './ModalPDF';
|
||||
@@ -20,15 +19,29 @@ interface DocToolBoxProps {
|
||||
|
||||
export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [isModalAddMembersOpen, setIsModalAddMembersOpen] = useState(false);
|
||||
const [isModalGridMembersOpen, setIsModalGridMembersOpen] = useState(false);
|
||||
const [isModalShareOpen, setIsModalShareOpen] = useState(false);
|
||||
const [isModalUpdateOpen, setIsModalUpdateOpen] = useState(false);
|
||||
const [isModalRemoveOpen, setIsModalRemoveOpen] = useState(false);
|
||||
const [isModalPDFOpen, setIsModalPDFOpen] = useState(false);
|
||||
const [isDropOpen, setIsDropOpen] = useState(false);
|
||||
|
||||
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
|
||||
button={
|
||||
<IconOptions
|
||||
@@ -42,17 +55,6 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
<Box>
|
||||
{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
|
||||
onClick={() => {
|
||||
setIsModalGridMembersOpen(true);
|
||||
@@ -105,19 +107,15 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
</Button>
|
||||
</Box>
|
||||
</DropButton>
|
||||
{isModalShareOpen && (
|
||||
<ModalShare onClose={() => setIsModalShareOpen(false)} doc={doc} />
|
||||
)}
|
||||
{isModalGridMembersOpen && (
|
||||
<ModalGridMembers
|
||||
onClose={() => setIsModalGridMembersOpen(false)}
|
||||
doc={doc}
|
||||
/>
|
||||
)}
|
||||
{isModalAddMembersOpen && (
|
||||
<ModalAddMembers
|
||||
onClose={() => setIsModalAddMembersOpen(false)}
|
||||
doc={doc}
|
||||
currentRole={currentDocRole(doc.abilities)}
|
||||
/>
|
||||
)}
|
||||
{isModalPDFOpen && (
|
||||
<ModalPDF onClose={() => setIsModalPDFOpen(false)} doc={doc} />
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './ModalRemoveDoc';
|
||||
export * from './ModalCreateUpdateDoc';
|
||||
export * from './ModalRemoveDoc';
|
||||
export * from './ModalShare';
|
||||
|
||||
@@ -27,6 +27,7 @@ const DocsGridStyle = createGlobalStyle`
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: #fff;
|
||||
z-index: 1;
|
||||
}
|
||||
& .c__pagination__goto{
|
||||
display:none;
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
ModalSize,
|
||||
VariantType,
|
||||
useToastProvider,
|
||||
} from '@openfun/cunningham-react';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { createGlobalStyle } from 'styled-components';
|
||||
|
||||
import { APIError } from '@/api';
|
||||
import { Box, Text } from '@/components';
|
||||
import { Box, Card, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { Doc, Role } from '@/features/docs/doc-management';
|
||||
|
||||
import { useCreateDocAccess, useCreateInvitation } from '../api';
|
||||
import IconAddUser from '../assets/add-user.svg';
|
||||
import {
|
||||
OptionInvitation,
|
||||
OptionNewMember,
|
||||
@@ -27,12 +23,6 @@ import {
|
||||
import { ChooseRole } from './ChooseRole';
|
||||
import { OptionsSelect, SearchUsers } from './SearchUsers';
|
||||
|
||||
const GlobalStyle = createGlobalStyle`
|
||||
.c__modal {
|
||||
overflow: visible;
|
||||
}
|
||||
`;
|
||||
|
||||
type APIErrorUser = APIError<{
|
||||
value: string;
|
||||
type: OptionType;
|
||||
@@ -40,26 +30,22 @@ type APIErrorUser = APIError<{
|
||||
|
||||
interface ModalAddMembersProps {
|
||||
currentRole: Role;
|
||||
onClose: () => void;
|
||||
doc: Doc;
|
||||
}
|
||||
|
||||
export const ModalAddMembers = ({
|
||||
currentRole,
|
||||
onClose,
|
||||
doc,
|
||||
}: ModalAddMembersProps) => {
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
export const AddMembers = ({ currentRole, doc }: ModalAddMembersProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
const [selectedUsers, setSelectedUsers] = useState<OptionsSelect>([]);
|
||||
const [selectedRole, setSelectedRole] = useState<Role>(Role.READER);
|
||||
const [selectedRole, setSelectedRole] = useState<Role>();
|
||||
const { toast } = useToastProvider();
|
||||
const { mutateAsync: createInvitation } = useCreateInvitation();
|
||||
const { mutateAsync: createDocAccess } = useCreateDocAccess();
|
||||
const [resetKey, setResetKey] = useState(1);
|
||||
|
||||
const [isPending, setIsPending] = useState<boolean>(false);
|
||||
|
||||
const switchActions = (selectedUsers: OptionsSelect) =>
|
||||
const switchActions = (selectedUsers: OptionsSelect, selectedRole: Role) =>
|
||||
selectedUsers.map(async (selectedUser) => {
|
||||
switch (selectedUser.type) {
|
||||
case OptionType.INVITATION:
|
||||
@@ -112,12 +98,16 @@ export const ModalAddMembers = ({
|
||||
const handleValidate = async () => {
|
||||
setIsPending(true);
|
||||
|
||||
if (!selectedRole) {
|
||||
return;
|
||||
}
|
||||
|
||||
const settledPromises = await Promise.allSettled<
|
||||
OptionInvitation | OptionNewMember
|
||||
>(switchActions(selectedUsers));
|
||||
>(switchActions(selectedUsers, selectedRole));
|
||||
|
||||
onClose();
|
||||
setIsPending(false);
|
||||
setResetKey(resetKey + 1);
|
||||
|
||||
settledPromises.forEach((settledPromise) => {
|
||||
switch (settledPromise.status) {
|
||||
@@ -133,63 +123,57 @@ export const ModalAddMembers = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen
|
||||
leftActions={
|
||||
<Button
|
||||
color="secondary"
|
||||
fullWidth
|
||||
onClick={onClose}
|
||||
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>
|
||||
}
|
||||
<Card
|
||||
$gap="1rem"
|
||||
$padding="1rem"
|
||||
$margin="1rem 0.7rem"
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$wrap="wrap"
|
||||
>
|
||||
<GlobalStyle />
|
||||
<Box $margin={{ bottom: 'xl', top: 'large' }}>
|
||||
<SearchUsers
|
||||
doc={doc}
|
||||
setSelectedUsers={setSelectedUsers}
|
||||
selectedUsers={selectedUsers}
|
||||
disabled={isPending}
|
||||
/>
|
||||
{selectedUsers.length >= 0 && (
|
||||
<Box $margin={{ top: 'small' }}>
|
||||
<Text as="h4" $textAlign="left" $margin={{ bottom: 'tiny' }}>
|
||||
{t('Choose a role')}
|
||||
</Text>
|
||||
<Text
|
||||
$isMaterialIcon
|
||||
$size="44px"
|
||||
$theme="primary"
|
||||
$background={colorsTokens()['primary-bg']}
|
||||
$css={`border: 1px solid ${colorsTokens()['primary-200']}`}
|
||||
$radius="12px"
|
||||
$padding="4px"
|
||||
$margin="auto"
|
||||
>
|
||||
group_add
|
||||
</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
|
||||
key={resetKey}
|
||||
currentRole={currentRole}
|
||||
disabled={isPending}
|
||||
defaultRole={Role.READER}
|
||||
setRole={setSelectedRole}
|
||||
/>
|
||||
</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>
|
||||
</Modal>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
interface ChooseRoleProps {
|
||||
currentRole: Role;
|
||||
disabled: boolean;
|
||||
defaultRole: Role;
|
||||
defaultRole?: Role;
|
||||
setRole: (role: Role) => void;
|
||||
}
|
||||
|
||||
@@ -15,23 +16,20 @@ export const ChooseRole = ({
|
||||
currentRole,
|
||||
setRole,
|
||||
}: ChooseRoleProps) => {
|
||||
const { t } = useTranslation();
|
||||
const transRole = useTransRole();
|
||||
|
||||
return (
|
||||
<RadioGroup>
|
||||
{Object.values(Role).map((role) => (
|
||||
<Radio
|
||||
key={role}
|
||||
label={transRole(role)}
|
||||
value={role}
|
||||
name="role"
|
||||
onChange={(evt) => setRole(evt.target.value as Role)}
|
||||
defaultChecked={defaultRole === role}
|
||||
disabled={
|
||||
disabled || (currentRole !== Role.OWNER && role === Role.OWNER)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</RadioGroup>
|
||||
<Select
|
||||
label={t('Choose a role')}
|
||||
options={Object.values(Role).map((role) => ({
|
||||
label: transRole(role),
|
||||
value: role,
|
||||
disabled: currentRole !== Role.OWNER && role === Role.OWNER,
|
||||
}))}
|
||||
onChange={(evt) => setRole(evt.target.value as Role)}
|
||||
disabled={disabled}
|
||||
value={defaultRole}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Options } from 'react-select';
|
||||
import AsyncSelect from 'react-select/async';
|
||||
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { Doc } from '@/features/docs/doc-management';
|
||||
import { isValidEmail } from '@/utils';
|
||||
|
||||
@@ -24,6 +25,7 @@ export const SearchUsers = ({
|
||||
setSelectedUsers,
|
||||
disabled,
|
||||
}: SearchUsersProps) => {
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
const { t } = useTranslation();
|
||||
const [input, setInput] = useState('');
|
||||
const [userQuery, setUserQuery] = useState('');
|
||||
@@ -102,6 +104,23 @@ export const SearchUsers = ({
|
||||
|
||||
return (
|
||||
<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}
|
||||
aria-label={t('Find a member to add to the document')}
|
||||
isMulti
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './AddMembers';
|
||||
@@ -1 +1 @@
|
||||
export * from './components/ModalAddMembers';
|
||||
export * from './components';
|
||||
|
||||
Reference in New Issue
Block a user