diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b97afbe..6256b7d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,8 @@ and this project adheres to ## Changed -⚡️(frontend) reduce unblocking time for config #867 +- ⚡️(frontend) reduce unblocking time for config #867 +- ♻️(frontend) bind UI with ability access #900 ## Fixed diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts index 9d452b34..e512cb92 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts @@ -79,7 +79,7 @@ test.describe('Doc Export', () => { }) .click(); - await page + void page .getByRole('button', { name: 'Download', }) @@ -129,7 +129,7 @@ test.describe('Doc Export', () => { await page.getByRole('combobox', { name: 'Format' }).click(); await page.getByRole('option', { name: 'Docx' }).click(); - await page + void page .getByRole('button', { name: 'Download', }) @@ -206,7 +206,7 @@ test.describe('Doc Export', () => { await new Promise((resolve) => setTimeout(resolve, 1000)); - await page + void page .getByRole('button', { name: 'Download', }) @@ -254,7 +254,7 @@ test.describe('Doc Export', () => { }) .click(); - await page + void page .getByRole('button', { name: 'Download', }) @@ -298,7 +298,7 @@ test.describe('Doc Export', () => { }) .click(); - await page + void page .getByRole('button', { name: 'Download', }) diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-member-list.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-member-list.spec.ts index 649dbc74..45f68b78 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-member-list.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-member-list.spec.ts @@ -152,12 +152,7 @@ test.describe('Document list members', () => { await expect(soloOwner).toBeHidden(); await list.click(); - const otherOwner = page.getByText( - `You cannot update the role or remove other owner.`, - ); - await newUserRoles.click(); - await expect(otherOwner).toBeVisible(); await list.click(); await currentUserRole.click(); diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/hooks/useTrans.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/hooks/useTrans.tsx index eb9a7016..d97bd85e 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/hooks/useTrans.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-management/hooks/useTrans.tsx @@ -12,34 +12,11 @@ export const useTrans = () => { [Role.OWNER]: t('Owner'), }; - const getNotAllowedMessage = ( - canUpdate: boolean, - isLastOwner: boolean, - isOtherOwner: boolean, - ) => { - if (!canUpdate) { - return undefined; - } - - if (isLastOwner) { - return t( - 'You are the sole owner of this group, make another member the group owner before you can change your own role or be removed from your document.', - ); - } - - if (isOtherOwner) { - return t('You cannot update the role or remove other owner.'); - } - - return undefined; - }; - return { transRole: (role: Role) => { return translatedRoles[role]; }, untitledDocument: t('Untitled document'), translatedRoles, - getNotAllowedMessage, }; }; diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocRoleDropdown.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocRoleDropdown.tsx index 95569369..261eb5f6 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocRoleDropdown.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocRoleDropdown.tsx @@ -3,32 +3,22 @@ import { css } from 'styled-components'; import { DropdownMenu, DropdownMenuOption, Text } from '@/components'; import { Role, useTrans } from '@/docs/doc-management/'; -type Props = { - currentRole: Role; - onSelectRole?: (role: Role) => void; +type DocRoleDropdownProps = { canUpdate?: boolean; - isLastOwner?: boolean; - isOtherOwner?: boolean; + currentRole: Role; + message?: string; + onSelectRole: (role: Role) => void; + rolesAllowed?: Role[]; }; + export const DocRoleDropdown = ({ canUpdate = true, currentRole, + message, onSelectRole, - isLastOwner, - isOtherOwner, -}: Props) => { - const { transRole, translatedRoles, getNotAllowedMessage } = useTrans(); - - const roles: DropdownMenuOption[] = Object.keys(translatedRoles).map( - (key) => { - return { - label: transRole(key as Role), - callback: () => onSelectRole?.(key as Role), - disabled: isLastOwner || isOtherOwner, - isSelected: currentRole === (key as Role), - }; - }, - ); + rolesAllowed, +}: DocRoleDropdownProps) => { + const { transRole, translatedRoles } = useTrans(); if (!canUpdate) { return ( @@ -38,13 +28,20 @@ export const DocRoleDropdown = ({ ); } + const roles: DropdownMenuOption[] = Object.keys(translatedRoles).map( + (key) => { + return { + label: transRole(key as Role), + callback: () => onSelectRole?.(key as Role), + disabled: rolesAllowed && !rolesAllowed.includes(key as Role), + isSelected: currentRole === (key as Role), + }; + }, + ); + return ( { const { t } = useTranslation(); - const { isLastOwner, isOtherOwner } = useWhoAmI(access); + const { isLastOwner } = useWhoAmI(access); const { toast } = useToastProvider(); const { isDesktop } = useResponsiveStore(); const { spacingsTokens } = useCunninghamTheme(); - const isNotAllowed = - isOtherOwner || !!isLastOwner || !doc.abilities.accesses_manage; + + const message = isLastOwner + ? t( + 'You are the sole owner of this group, make another member the group owner before you can change your own role or be removed from your document.', + ) + : undefined; const { mutate: updateDocAccess } = useUpdateDocAccess({ onError: () => { @@ -63,7 +67,7 @@ export const DocShareMemberItem = ({ doc, access }: Props) => { label: t('Delete'), icon: 'delete', callback: onRemove, - disabled: isNotAllowed, + disabled: !access.abilities.destroy, }, ]; @@ -82,8 +86,8 @@ export const DocShareMemberItem = ({ doc, access }: Props) => { currentRole={access.role} onSelectRole={onUpdate} canUpdate={doc.abilities.accesses_manage} - isLastOwner={isLastOwner} - isOtherOwner={!!isOtherOwner} + message={message} + rolesAllowed={access.abilities.set_role_to} /> {isDesktop && doc.abilities.accesses_manage && ( diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModal.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModal.tsx index bc5a805d..54c36d24 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModal.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModal.tsx @@ -70,10 +70,6 @@ export const DocShareModal = ({ doc, onClose }: Props) => { docId: doc.id, }); - const invitationQuery = useDocInvitationsInfinite({ - docId: doc.id, - }); - const searchUsersQuery = useUsers( { query: userQuery, docId: doc.id }, { @@ -107,52 +103,6 @@ export const DocShareModal = ({ doc, onClose }: Props) => { }; }, [membersQuery, t]); - const invitationsData: QuickSearchData = useMemo(() => { - const invitations = - invitationQuery.data?.pages.flatMap((page) => page.results) || []; - - return { - groupName: t('Pending invitations'), - elements: invitations, - endActions: invitationQuery.hasNextPage - ? [ - { - content: , - onSelect: () => void invitationQuery.fetchNextPage(), - }, - ] - : undefined, - }; - }, [invitationQuery, t]); - - const searchUserData: QuickSearchData = useMemo(() => { - const users = searchUsersQuery.data || []; - const isEmail = isValidEmail(userQuery); - const newUser: User = { - id: userQuery, - full_name: '', - email: userQuery, - short_name: '', - language: '', - }; - - const hasEmailInUsers = users.some((user) => user.email === userQuery); - - return { - groupName: t('Search user result'), - elements: users, - endActions: - isEmail && !hasEmailInUsers - ? [ - { - content: , - onSelect: () => void onSelect(newUser), - }, - ] - : undefined, - }; - }, [searchUsersQuery.data, t, userQuery]); - const onFilter = useDebouncedCallback((str: string) => { setUserQuery(str); }, 300); @@ -254,44 +204,17 @@ export const DocShareModal = ({ doc, onClose }: Props) => { loading={searchUsersQuery.isLoading} placeholder={t('Type a name or email')} > - {canViewAccesses && ( - <> - {!showMemberSection && inputValue !== '' && ( - ( - - )} - /> - )} - {showMemberSection && ( - <> - {invitationsData.elements.length > 0 && ( - - ( - - )} - /> - - )} - - - ( - - )} - /> - - - )} - + {showMemberSection ? ( + + ) : ( + )} )} @@ -306,3 +229,109 @@ export const DocShareModal = ({ doc, onClose }: Props) => { ); }; + +interface QuickSearchInviteInputSectionProps { + onSelect: (usr: User) => void; + searchUsersRawData: User[] | undefined; + userQuery: string; +} + +const QuickSearchInviteInputSection = ({ + onSelect, + searchUsersRawData, + userQuery, +}: QuickSearchInviteInputSectionProps) => { + const { t } = useTranslation(); + + const searchUserData: QuickSearchData = useMemo(() => { + const users = searchUsersRawData || []; + const isEmail = isValidEmail(userQuery); + const newUser: User = { + id: userQuery, + full_name: '', + email: userQuery, + short_name: '', + language: '', + }; + + const hasEmailInUsers = users.some((user) => user.email === userQuery); + + return { + groupName: t('Search user result'), + elements: users, + endActions: + isEmail && !hasEmailInUsers + ? [ + { + content: , + onSelect: () => void onSelect(newUser), + }, + ] + : undefined, + }; + }, [onSelect, searchUsersRawData, t, userQuery]); + + return ( + } + /> + ); +}; + +interface QuickSearchMemberSectionProps { + doc: Doc; + membersData: QuickSearchData; +} + +const QuickSearchMemberSection = ({ + doc, + membersData, +}: QuickSearchMemberSectionProps) => { + const { t } = useTranslation(); + const { data, hasNextPage, fetchNextPage } = useDocInvitationsInfinite({ + docId: doc.id, + }); + + const invitationsData: QuickSearchData = useMemo(() => { + const invitations = data?.pages.flatMap((page) => page.results) || []; + + return { + groupName: t('Pending invitations'), + elements: invitations, + endActions: hasNextPage + ? [ + { + content: , + onSelect: () => void fetchNextPage(), + }, + ] + : undefined, + }; + }, [data?.pages, fetchNextPage, hasNextPage, t]); + + return ( + <> + {invitationsData.elements.length > 0 && ( + + ( + + )} + /> + + )} + + + ( + + )} + /> + + + ); +};