♻️(frontend) bind ui with ability access
Some actions were not available in the frontend but allowed in the backend, this commit binds the frontend ui with the ability access coming from the backend.
This commit is contained in:
@@ -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',
|
||||
})
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<DropdownMenu
|
||||
topMessage={getNotAllowedMessage(
|
||||
canUpdate,
|
||||
!!isLastOwner,
|
||||
!!isOtherOwner,
|
||||
)}
|
||||
topMessage={message}
|
||||
label="doc-role-dropdown"
|
||||
showArrow={true}
|
||||
options={roles}
|
||||
|
||||
@@ -23,12 +23,16 @@ type Props = {
|
||||
};
|
||||
export const DocShareMemberItem = ({ doc, access }: Props) => {
|
||||
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 && (
|
||||
|
||||
@@ -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<Invitation> = useMemo(() => {
|
||||
const invitations =
|
||||
invitationQuery.data?.pages.flatMap((page) => page.results) || [];
|
||||
|
||||
return {
|
||||
groupName: t('Pending invitations'),
|
||||
elements: invitations,
|
||||
endActions: invitationQuery.hasNextPage
|
||||
? [
|
||||
{
|
||||
content: <LoadMoreText data-testid="load-more-invitations" />,
|
||||
onSelect: () => void invitationQuery.fetchNextPage(),
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
};
|
||||
}, [invitationQuery, t]);
|
||||
|
||||
const searchUserData: QuickSearchData<User> = 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: <DocShareModalInviteUserRow user={newUser} />,
|
||||
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 !== '' && (
|
||||
<QuickSearchGroup
|
||||
group={searchUserData}
|
||||
onSelect={onSelect}
|
||||
renderElement={(user) => (
|
||||
<DocShareModalInviteUserRow user={user} />
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{showMemberSection && (
|
||||
<>
|
||||
{invitationsData.elements.length > 0 && (
|
||||
<Box aria-label={t('List invitation card')}>
|
||||
<QuickSearchGroup
|
||||
group={invitationsData}
|
||||
renderElement={(invitation) => (
|
||||
<DocShareInvitationItem
|
||||
doc={doc}
|
||||
invitation={invitation}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box aria-label={t('List members card')}>
|
||||
<QuickSearchGroup
|
||||
group={membersData}
|
||||
renderElement={(access) => (
|
||||
<DocShareMemberItem doc={doc} access={access} />
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
{showMemberSection ? (
|
||||
<QuickSearchMemberSection
|
||||
doc={doc}
|
||||
membersData={membersData}
|
||||
/>
|
||||
) : (
|
||||
<QuickSearchInviteInputSection
|
||||
searchUsersRawData={searchUsersQuery.data}
|
||||
onSelect={onSelect}
|
||||
userQuery={userQuery}
|
||||
/>
|
||||
)}
|
||||
</QuickSearch>
|
||||
)}
|
||||
@@ -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<User> = 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: <DocShareModalInviteUserRow user={newUser} />,
|
||||
onSelect: () => void onSelect(newUser),
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
};
|
||||
}, [onSelect, searchUsersRawData, t, userQuery]);
|
||||
|
||||
return (
|
||||
<QuickSearchGroup
|
||||
group={searchUserData}
|
||||
onSelect={onSelect}
|
||||
renderElement={(user) => <DocShareModalInviteUserRow user={user} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface QuickSearchMemberSectionProps {
|
||||
doc: Doc;
|
||||
membersData: QuickSearchData<Access>;
|
||||
}
|
||||
|
||||
const QuickSearchMemberSection = ({
|
||||
doc,
|
||||
membersData,
|
||||
}: QuickSearchMemberSectionProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { data, hasNextPage, fetchNextPage } = useDocInvitationsInfinite({
|
||||
docId: doc.id,
|
||||
});
|
||||
|
||||
const invitationsData: QuickSearchData<Invitation> = useMemo(() => {
|
||||
const invitations = data?.pages.flatMap((page) => page.results) || [];
|
||||
|
||||
return {
|
||||
groupName: t('Pending invitations'),
|
||||
elements: invitations,
|
||||
endActions: hasNextPage
|
||||
? [
|
||||
{
|
||||
content: <LoadMoreText data-testid="load-more-invitations" />,
|
||||
onSelect: () => void fetchNextPage(),
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
};
|
||||
}, [data?.pages, fetchNextPage, hasNextPage, t]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{invitationsData.elements.length > 0 && (
|
||||
<Box aria-label={t('List invitation card')}>
|
||||
<QuickSearchGroup
|
||||
group={invitationsData}
|
||||
renderElement={(invitation) => (
|
||||
<DocShareInvitationItem doc={doc} invitation={invitation} />
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box aria-label={t('List members card')}>
|
||||
<QuickSearchGroup
|
||||
group={membersData}
|
||||
renderElement={(access) => (
|
||||
<DocShareMemberItem doc={doc} access={access} />
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user