From c2d6e60ae84d2a4d585b4dd0e5dcf5456685e43f Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Thu, 11 Jul 2024 10:32:18 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F(frontend)=20add=20user=20fro?= =?UTF-8?q?m=20side=20modal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We move the add user functionality to a side modal. The side modal is opened from the share button. --- .../apps/e2e/__tests__/app-impress/common.ts | 7 +- .../__tests__/app-impress/doc-header.spec.ts | 1 + ...bers.spec.ts => doc-member-create.spec.ts} | 41 +++--- .../__tests__/app-impress/doc-tools.spec.ts | 16 +-- .../apps/impress/src/components/SideModal.tsx | 18 ++- .../apps/impress/src/components/Text.tsx | 11 +- .../src/cunningham/cunningham-style.css | 4 + .../docs/doc-header/components/DocHeader.tsx | 6 +- .../docs/doc-header/components/DocToolBox.tsx | 42 +++--- .../doc-management/components/ModalShare.tsx | 61 +++++++++ .../docs/doc-management/components/index.ts | 3 +- .../docs/docs-grid/components/DocsGrid.tsx | 1 + .../{ModalAddMembers.tsx => AddMembers.tsx} | 128 ++++++++---------- .../members-add/components/ChooseRole.tsx | 32 ++--- .../members-add/components/SearchUsers.tsx | 19 +++ .../members/members-add/components/index.ts | 1 + .../docs/members/members-add/index.ts | 2 +- 17 files changed, 241 insertions(+), 152 deletions(-) rename src/frontend/apps/e2e/__tests__/app-impress/{doc-add-members.spec.ts => doc-member-create.spec.ts} (81%) create mode 100644 src/frontend/apps/impress/src/features/docs/doc-management/components/ModalShare.tsx rename src/frontend/apps/impress/src/features/docs/members/members-add/components/{ModalAddMembers.tsx => AddMembers.tsx} (62%) create mode 100644 src/frontend/apps/impress/src/features/docs/members/members-add/components/index.ts diff --git a/src/frontend/apps/e2e/__tests__/app-impress/common.ts b/src/frontend/apps/e2e/__tests__/app-impress/common.ts index 3429ce2c..53a88da5 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/common.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/common.ts @@ -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( 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 47d204f5..0da98bd5 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 @@ -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(); }); }); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-add-members.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-member-create.spec.ts similarity index 81% rename from src/frontend/apps/e2e/__tests__/app-impress/doc-add-members.spec.ts rename to src/frontend/apps/e2e/__tests__/app-impress/doc-member-create.spec.ts index b5b80ffb..be8c8253 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-add-members.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-member-create.spec.ts @@ -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(); }); }); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-tools.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-tools.spec.ts index a903da96..3c6d1c20 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-tools.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-tools.spec.ts @@ -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(); diff --git a/src/frontend/apps/impress/src/components/SideModal.tsx b/src/frontend/apps/impress/src/components/SideModal.tsx index 249dd66a..d8443040 100644 --- a/src/frontend/apps/impress/src/components/SideModal.tsx +++ b/src/frontend/apps/impress/src/components/SideModal.tsx @@ -5,10 +5,23 @@ import { createGlobalStyle } from 'styled-components'; interface SideModalStyleProps { side: 'left' | 'right'; width: string; + $css?: string; } const SideModalStyle = createGlobalStyle` + @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` display: flex; flex-direction: column; } + + ${({ $css }) => $css} } `; @@ -28,11 +43,12 @@ export const SideModal = ({ children, side = 'right', width = '35vw', + $css, ...modalProps }: PropsWithChildren) => { return ( <> - + {children} diff --git a/src/frontend/apps/impress/src/components/Text.tsx b/src/frontend/apps/impress/src/components/Text.tsx index a0c0300e..b5ebd150 100644 --- a/src/frontend/apps/impress/src/components/Text.tsx +++ b/src/frontend/apps/impress/src/components/Text.tsx @@ -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)` `; export const Text = ({ + className, + $isMaterialIcon, ...props }: ComponentPropsWithRef) => { return ( - + ); }; diff --git a/src/frontend/apps/impress/src/cunningham/cunningham-style.css b/src/frontend/apps/impress/src/cunningham/cunningham-style.css index 84c54bde..426de341 100644 --- a/src/frontend/apps/impress/src/cunningham/cunningham-style.css +++ b/src/frontend/apps/impress/src/cunningham/cunningham-style.css @@ -477,6 +477,10 @@ input:-webkit-autofill:focus { padding: 1.5rem 1rem; } +.c__modal--full .c__modal__content { + overflow-y: auto; +} + /** * Toast */ diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx index 51b6f342..4a842548 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx @@ -31,7 +31,7 @@ export const DocHeader = ({ doc }: DocHeaderProps) => { { {t('Owners:')}{' '} {doc.accesses - .filter((access) => access.role === Role.OWNER) + .filter( + (access) => access.role === Role.OWNER && access.user.email, + ) .map((access, index, accesses) => ( {access.user.email}{' '} diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx index 4f2f43ab..fb4ebb65 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx @@ -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 ( - + + {doc.abilities.manage_accesses && ( + + )} { {doc.abilities.manage_accesses && ( <> - + {isModalShareOpen && ( + setIsModalShareOpen(false)} doc={doc} /> + )} {isModalGridMembersOpen && ( setIsModalGridMembersOpen(false)} doc={doc} /> )} - {isModalAddMembersOpen && ( - setIsModalAddMembersOpen(false)} - doc={doc} - currentRole={currentDocRole(doc.abilities)} - /> - )} {isModalPDFOpen && ( setIsModalPDFOpen(false)} doc={doc} /> )} diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/components/ModalShare.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/components/ModalShare.tsx new file mode 100644 index 00000000..3ec550e3 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-management/components/ModalShare.tsx @@ -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 ( + <> + + + + share + + + + {t('Share')} + + + {doc.title} + + + + } + > + + + + ); +}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/components/index.ts b/src/frontend/apps/impress/src/features/docs/doc-management/components/index.ts index 5c19308f..04d5b0c9 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/components/index.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-management/components/index.ts @@ -1,2 +1,3 @@ -export * from './ModalRemoveDoc'; export * from './ModalCreateUpdateDoc'; +export * from './ModalRemoveDoc'; +export * from './ModalShare'; diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGrid.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGrid.tsx index 6eca072b..ea5ab68b 100644 --- a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGrid.tsx +++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGrid.tsx @@ -27,6 +27,7 @@ const DocsGridStyle = createGlobalStyle` position: sticky; top: 0; background: #fff; + z-index: 1; } & .c__pagination__goto{ display:none; diff --git a/src/frontend/apps/impress/src/features/docs/members/members-add/components/ModalAddMembers.tsx b/src/frontend/apps/impress/src/features/docs/members/members-add/components/AddMembers.tsx similarity index 62% rename from src/frontend/apps/impress/src/features/docs/members/members-add/components/ModalAddMembers.tsx rename to src/frontend/apps/impress/src/features/docs/members/members-add/components/AddMembers.tsx index b3fd0266..b30f1298 100644 --- a/src/frontend/apps/impress/src/features/docs/members/members-add/components/ModalAddMembers.tsx +++ b/src/frontend/apps/impress/src/features/docs/members/members-add/components/AddMembers.tsx @@ -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([]); - const [selectedRole, setSelectedRole] = useState(Role.READER); + const [selectedRole, setSelectedRole] = useState(); const { toast } = useToastProvider(); const { mutateAsync: createInvitation } = useCreateInvitation(); const { mutateAsync: createDocAccess } = useCreateDocAccess(); + const [resetKey, setResetKey] = useState(1); const [isPending, setIsPending] = useState(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 ( - - {t('Cancel')} - - } - onClose={onClose} - closeOnClickOutside - hideCloseButton - rightActions={ - - } - size={ModalSize.MEDIUM} - title={ - - - - {t('Add members to the document')} - - - } + - - - - {selectedUsers.length >= 0 && ( - - - {t('Choose a role')} - + + group_add + + + + + + + - )} + + + + - + ); }; diff --git a/src/frontend/apps/impress/src/features/docs/members/members-add/components/ChooseRole.tsx b/src/frontend/apps/impress/src/features/docs/members/members-add/components/ChooseRole.tsx index 86094e51..17044d6e 100644 --- a/src/frontend/apps/impress/src/features/docs/members/members-add/components/ChooseRole.tsx +++ b/src/frontend/apps/impress/src/features/docs/members/members-add/components/ChooseRole.tsx @@ -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 ( - - {Object.values(Role).map((role) => ( - setRole(evt.target.value as Role)} - defaultChecked={defaultRole === role} - disabled={ - disabled || (currentRole !== Role.OWNER && role === Role.OWNER) - } - /> - ))} - +