♻️(frontend) improve separation of concerns in DocShareModal
Improve separation of concerns in the DocShareModal component. The member and invitation list are now in a separate component. It will help us to integrate cleanly the request access list.
This commit is contained in:
committed by
Manuel Raynaud
parent
394f91387d
commit
411d52c73b
@@ -66,10 +66,5 @@ export const useDeleteDocAccess = (options?: UseDeleteDocAccessOptions) => {
|
||||
void options.onSuccess(data, variables, context);
|
||||
}
|
||||
},
|
||||
onError: (error, variables, context) => {
|
||||
if (options?.onError) {
|
||||
void options.onError(error, variables, context);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
import {
|
||||
DefinedInitialDataInfiniteOptions,
|
||||
InfiniteData,
|
||||
QueryKey,
|
||||
UseQueryOptions,
|
||||
useInfiniteQuery,
|
||||
useQuery,
|
||||
} from '@tanstack/react-query';
|
||||
import { UseQueryOptions, useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { APIError, APIList, errorCauses, fetchAPI } from '@/api';
|
||||
import {
|
||||
APIError,
|
||||
APIList,
|
||||
errorCauses,
|
||||
fetchAPI,
|
||||
useAPIInfiniteQuery,
|
||||
} from '@/api';
|
||||
import { Access } from '@/docs/doc-management';
|
||||
|
||||
export type DocAccessesParam = {
|
||||
export type DocAccessesParams = {
|
||||
docId: string;
|
||||
ordering?: string;
|
||||
};
|
||||
|
||||
export type DocAccessesAPIParams = DocAccessesParam & {
|
||||
export type DocAccessesAPIParams = DocAccessesParams & {
|
||||
page: number;
|
||||
};
|
||||
|
||||
@@ -62,33 +61,6 @@ export function useDocAccesses(
|
||||
* @param queryConfig
|
||||
* @returns
|
||||
*/
|
||||
export function useDocAccessesInfinite(
|
||||
param: DocAccessesParam,
|
||||
queryConfig?: DefinedInitialDataInfiniteOptions<
|
||||
AccessesResponse,
|
||||
APIError,
|
||||
InfiniteData<AccessesResponse>,
|
||||
QueryKey,
|
||||
number
|
||||
>,
|
||||
) {
|
||||
return useInfiniteQuery<
|
||||
AccessesResponse,
|
||||
APIError,
|
||||
InfiniteData<AccessesResponse>,
|
||||
QueryKey,
|
||||
number
|
||||
>({
|
||||
initialPageParam: 1,
|
||||
queryKey: [KEY_LIST_DOC_ACCESSES, param],
|
||||
queryFn: ({ pageParam }) =>
|
||||
getDocAccesses({
|
||||
...param,
|
||||
page: pageParam,
|
||||
}),
|
||||
getNextPageParam(lastPage, allPages) {
|
||||
return lastPage.next ? allPages.length + 1 : undefined;
|
||||
},
|
||||
...queryConfig,
|
||||
});
|
||||
export function useDocAccessesInfinite(params: DocAccessesParams) {
|
||||
return useAPIInfiniteQuery(KEY_LIST_DOC_ACCESSES, getDocAccesses, params);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import {
|
||||
DefinedInitialDataInfiniteOptions,
|
||||
InfiniteData,
|
||||
QueryKey,
|
||||
UseQueryOptions,
|
||||
useInfiniteQuery,
|
||||
useQuery,
|
||||
} from '@tanstack/react-query';
|
||||
import { UseQueryOptions, useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { APIError, APIList, errorCauses, fetchAPI } from '@/api';
|
||||
import {
|
||||
APIError,
|
||||
APIList,
|
||||
errorCauses,
|
||||
fetchAPI,
|
||||
useAPIInfiniteQuery,
|
||||
} from '@/api';
|
||||
import { Invitation } from '@/docs/doc-share/types';
|
||||
|
||||
export type DocInvitationsParams = {
|
||||
@@ -66,33 +65,10 @@ export function useDocInvitations(
|
||||
* @param queryConfig
|
||||
* @returns
|
||||
*/
|
||||
export function useDocInvitationsInfinite(
|
||||
param: DocInvitationsParams,
|
||||
queryConfig?: DefinedInitialDataInfiniteOptions<
|
||||
DocInvitationsResponse,
|
||||
APIError,
|
||||
InfiniteData<DocInvitationsResponse>,
|
||||
QueryKey,
|
||||
number
|
||||
>,
|
||||
) {
|
||||
return useInfiniteQuery<
|
||||
DocInvitationsResponse,
|
||||
APIError,
|
||||
InfiniteData<DocInvitationsResponse>,
|
||||
QueryKey,
|
||||
number
|
||||
>({
|
||||
initialPageParam: 1,
|
||||
queryKey: [KEY_LIST_DOC_INVITATIONS, param],
|
||||
queryFn: ({ pageParam }) =>
|
||||
getDocInvitations({
|
||||
...param,
|
||||
page: pageParam,
|
||||
}),
|
||||
getNextPageParam(lastPage, allPages) {
|
||||
return lastPage.next ? allPages.length + 1 : undefined;
|
||||
},
|
||||
...queryConfig,
|
||||
});
|
||||
export function useDocInvitationsInfinite(params: DocInvitationsParams) {
|
||||
return useAPIInfiniteQuery(
|
||||
KEY_LIST_DOC_INVITATIONS,
|
||||
getDocInvitations,
|
||||
params,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,10 +68,5 @@ export const useUpdateDocAccess = (options?: UseUpdateDocAccessOptions) => {
|
||||
void options.onSuccess(data, variables, context);
|
||||
}
|
||||
},
|
||||
onError: (error, variables, context) => {
|
||||
if (options?.onError) {
|
||||
void options.onError(error, variables, context);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,30 +1,44 @@
|
||||
import { VariantType, useToastProvider } from '@openfun/cunningham-react';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import {
|
||||
Box,
|
||||
DropdownMenu,
|
||||
DropdownMenuOption,
|
||||
Icon,
|
||||
IconOptions,
|
||||
LoadMoreText,
|
||||
Text,
|
||||
} from '@/components';
|
||||
import { QuickSearchData, QuickSearchGroup } from '@/components/quick-search';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { Doc, Role } from '@/docs/doc-management';
|
||||
import { User } from '@/features/auth';
|
||||
|
||||
import { useDeleteDocInvitation, useUpdateDocInvitation } from '../api';
|
||||
import {
|
||||
useDeleteDocInvitation,
|
||||
useDocInvitationsInfinite,
|
||||
useUpdateDocInvitation,
|
||||
} from '../api';
|
||||
import { Invitation } from '../types';
|
||||
|
||||
import { DocRoleDropdown } from './DocRoleDropdown';
|
||||
import { SearchUserRow } from './SearchUserRow';
|
||||
|
||||
type Props = {
|
||||
type DocShareInvitationItemProps = {
|
||||
doc: Doc;
|
||||
invitation: Invitation;
|
||||
};
|
||||
export const DocShareInvitationItem = ({ doc, invitation }: Props) => {
|
||||
|
||||
const DocShareInvitationItem = ({
|
||||
doc,
|
||||
invitation,
|
||||
}: DocShareInvitationItemProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { spacingsTokens } = useCunninghamTheme();
|
||||
const fakeUser: User = {
|
||||
const invitedUser: User = {
|
||||
id: invitation.email,
|
||||
full_name: invitation.email,
|
||||
email: invitation.email,
|
||||
@@ -79,6 +93,7 @@ export const DocShareInvitationItem = ({ doc, invitation }: Props) => {
|
||||
disabled: !canUpdate,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Box
|
||||
$width="100%"
|
||||
@@ -88,7 +103,7 @@ export const DocShareInvitationItem = ({ doc, invitation }: Props) => {
|
||||
<SearchUserRow
|
||||
isInvitation={true}
|
||||
alwaysShowRight={true}
|
||||
user={fakeUser}
|
||||
user={invitedUser}
|
||||
right={
|
||||
<Box $direction="row" $align="center" $gap={spacingsTokens['2xs']}>
|
||||
<DocRoleDropdown
|
||||
@@ -111,3 +126,85 @@ export const DocShareInvitationItem = ({ doc, invitation }: Props) => {
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
type DocShareModalInviteUserRowProps = {
|
||||
user: User;
|
||||
};
|
||||
export const DocShareModalInviteUserRow = ({
|
||||
user,
|
||||
}: DocShareModalInviteUserRowProps) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Box
|
||||
$width="100%"
|
||||
data-testid={`search-user-row-${user.email}`}
|
||||
className="--docs--doc-share-modal-invite-user-row"
|
||||
>
|
||||
<SearchUserRow
|
||||
user={user}
|
||||
right={
|
||||
<Box
|
||||
className="right-hover"
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$css={css`
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: var(--c--theme--font--sizes--sm);
|
||||
color: var(--c--theme--colors--greyscale-400);
|
||||
`}
|
||||
>
|
||||
<Text $theme="primary" $variation="800">
|
||||
{t('Add')}
|
||||
</Text>
|
||||
<Icon $theme="primary" $variation="800" iconName="add" />
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
interface QuickSearchGroupInvitationProps {
|
||||
doc: Doc;
|
||||
}
|
||||
|
||||
export const QuickSearchGroupInvitation = ({
|
||||
doc,
|
||||
}: QuickSearchGroupInvitationProps) => {
|
||||
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]);
|
||||
|
||||
if (!invitationsData.elements.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box aria-label={t('List invitation card')}>
|
||||
<QuickSearchGroup
|
||||
group={invitationsData}
|
||||
renderElement={(invitation) => (
|
||||
<DocShareInvitationItem doc={doc} invitation={invitation} />
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import { VariantType, useToastProvider } from '@openfun/cunningham-react';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
@@ -6,13 +7,19 @@ import {
|
||||
DropdownMenu,
|
||||
DropdownMenuOption,
|
||||
IconOptions,
|
||||
LoadMoreText,
|
||||
} from '@/components';
|
||||
import { QuickSearchData, QuickSearchGroup } from '@/components/quick-search';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { Access, Doc, Role } from '@/docs/doc-management/';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { useDeleteDocAccess, useUpdateDocAccess } from '../api';
|
||||
import { useWhoAmI } from '../hooks/';
|
||||
import {
|
||||
useDeleteDocAccess,
|
||||
useDocAccessesInfinite,
|
||||
useUpdateDocAccess,
|
||||
} from '../api';
|
||||
import { useWhoAmI } from '../hooks';
|
||||
|
||||
import { DocRoleDropdown } from './DocRoleDropdown';
|
||||
import { SearchUserRow } from './SearchUserRow';
|
||||
@@ -21,7 +28,8 @@ type Props = {
|
||||
doc: Doc;
|
||||
access: Access;
|
||||
};
|
||||
export const DocShareMemberItem = ({ doc, access }: Props) => {
|
||||
|
||||
const DocShareMemberItem = ({ doc, access }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { isLastOwner } = useWhoAmI(access);
|
||||
const { toast } = useToastProvider();
|
||||
@@ -36,7 +44,7 @@ export const DocShareMemberItem = ({ doc, access }: Props) => {
|
||||
|
||||
const { mutate: updateDocAccess } = useUpdateDocAccess({
|
||||
onError: () => {
|
||||
toast(t('Error during invitation update'), VariantType.ERROR, {
|
||||
toast(t('Error while updating the member role.'), VariantType.ERROR, {
|
||||
duration: 4000,
|
||||
});
|
||||
},
|
||||
@@ -44,7 +52,7 @@ export const DocShareMemberItem = ({ doc, access }: Props) => {
|
||||
|
||||
const { mutate: removeDocAccess } = useDeleteDocAccess({
|
||||
onError: () => {
|
||||
toast(t('Error while deleting invitation'), VariantType.ERROR, {
|
||||
toast(t('Error while deleting the member.'), VariantType.ERROR, {
|
||||
duration: 4000,
|
||||
});
|
||||
},
|
||||
@@ -105,3 +113,52 @@ export const DocShareMemberItem = ({ doc, access }: Props) => {
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
interface QuickSearchGroupMemberProps {
|
||||
doc: Doc;
|
||||
}
|
||||
|
||||
export const QuickSearchGroupMember = ({
|
||||
doc,
|
||||
}: QuickSearchGroupMemberProps) => {
|
||||
const { t } = useTranslation();
|
||||
const membersQuery = useDocAccessesInfinite({
|
||||
docId: doc.id,
|
||||
});
|
||||
|
||||
const membersData: QuickSearchData<Access> = useMemo(() => {
|
||||
const members =
|
||||
membersQuery.data?.pages.flatMap((page) => page.results) || [];
|
||||
|
||||
const count = membersQuery.data?.pages[0]?.count ?? 1;
|
||||
|
||||
return {
|
||||
groupName:
|
||||
count === 1
|
||||
? t('Document owner')
|
||||
: t('Share with {{count}} users', {
|
||||
count: count,
|
||||
}),
|
||||
elements: members,
|
||||
endActions: membersQuery.hasNextPage
|
||||
? [
|
||||
{
|
||||
content: <LoadMoreText data-testid="load-more-members" />,
|
||||
onSelect: () => void membersQuery.fetchNextPage(),
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
};
|
||||
}, [membersQuery, t]);
|
||||
|
||||
return (
|
||||
<Box aria-label={t('List members card')}>
|
||||
<QuickSearchGroup
|
||||
group={membersData}
|
||||
renderElement={(access) => (
|
||||
<DocShareMemberItem doc={doc} access={access} />
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -4,30 +4,26 @@ import { useTranslation } from 'react-i18next';
|
||||
import { createGlobalStyle, css } from 'styled-components';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
import { Box, HorizontalSeparator, LoadMoreText, Text } from '@/components';
|
||||
import { Box, HorizontalSeparator, Text } from '@/components';
|
||||
import {
|
||||
QuickSearch,
|
||||
QuickSearchData,
|
||||
QuickSearchGroup,
|
||||
} from '@/components/quick-search/';
|
||||
import { User } from '@/features/auth';
|
||||
import { Access, Doc } from '@/features/docs';
|
||||
import { Doc } from '@/features/docs';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
import { isValidEmail } from '@/utils';
|
||||
|
||||
import {
|
||||
KEY_LIST_USER,
|
||||
useDocAccessesInfinite,
|
||||
useDocInvitationsInfinite,
|
||||
useUsers,
|
||||
} from '../api';
|
||||
import { Invitation } from '../types';
|
||||
import { KEY_LIST_USER, useUsers } from '../api';
|
||||
|
||||
import { DocShareAddMemberList } from './DocShareAddMemberList';
|
||||
import { DocShareInvitationItem } from './DocShareInvitationItem';
|
||||
import { DocShareMemberItem } from './DocShareMemberItem';
|
||||
import {
|
||||
DocShareModalInviteUserRow,
|
||||
QuickSearchGroupInvitation,
|
||||
} from './DocShareInvitation';
|
||||
import { QuickSearchGroupMember } from './DocShareMember';
|
||||
import { DocShareModalFooter } from './DocShareModalFooter';
|
||||
import { DocShareModalInviteUserRow } from './DocShareModalInviteUserByEmail';
|
||||
|
||||
const ShareModalStyle = createGlobalStyle`
|
||||
.c__modal__title {
|
||||
@@ -66,10 +62,6 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
|
||||
setInputValue('');
|
||||
};
|
||||
|
||||
const membersQuery = useDocAccessesInfinite({
|
||||
docId: doc.id,
|
||||
});
|
||||
|
||||
const searchUsersQuery = useUsers(
|
||||
{ query: userQuery, docId: doc.id },
|
||||
{
|
||||
@@ -78,31 +70,6 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
|
||||
},
|
||||
);
|
||||
|
||||
const membersData: QuickSearchData<Access> = useMemo(() => {
|
||||
const members =
|
||||
membersQuery.data?.pages.flatMap((page) => page.results) || [];
|
||||
|
||||
const count = membersQuery.data?.pages[0]?.count ?? 1;
|
||||
|
||||
return {
|
||||
groupName:
|
||||
count === 1
|
||||
? t('Document owner')
|
||||
: t('Share with {{count}} users', {
|
||||
count: count,
|
||||
}),
|
||||
elements: members,
|
||||
endActions: membersQuery.hasNextPage
|
||||
? [
|
||||
{
|
||||
content: <LoadMoreText data-testid="load-more-members" />,
|
||||
onSelect: () => void membersQuery.fetchNextPage(),
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
};
|
||||
}, [membersQuery, t]);
|
||||
|
||||
const onFilter = useDebouncedCallback((str: string) => {
|
||||
setUserQuery(str);
|
||||
}, 300);
|
||||
@@ -205,10 +172,10 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
|
||||
placeholder={t('Type a name or email')}
|
||||
>
|
||||
{showMemberSection ? (
|
||||
<QuickSearchMemberSection
|
||||
doc={doc}
|
||||
membersData={membersData}
|
||||
/>
|
||||
<>
|
||||
<QuickSearchGroupInvitation doc={doc} />
|
||||
<QuickSearchGroupMember doc={doc} />
|
||||
</>
|
||||
) : (
|
||||
<QuickSearchInviteInputSection
|
||||
searchUsersRawData={searchUsersQuery.data}
|
||||
@@ -279,59 +246,3 @@ const QuickSearchInviteInputSection = ({
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, Icon, Text } from '@/components';
|
||||
import { User } from '@/features/auth';
|
||||
|
||||
import { SearchUserRow } from './SearchUserRow';
|
||||
|
||||
type Props = {
|
||||
user: User;
|
||||
};
|
||||
export const DocShareModalInviteUserRow = ({ user }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Box
|
||||
$width="100%"
|
||||
data-testid={`search-user-row-${user.email}`}
|
||||
className="--docs--doc-share-modal-invite-user-row"
|
||||
>
|
||||
<SearchUserRow
|
||||
user={user}
|
||||
right={
|
||||
<Box
|
||||
className="right-hover"
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$css={css`
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: var(--c--theme--font--sizes--sm);
|
||||
color: var(--c--theme--colors--greyscale-400);
|
||||
`}
|
||||
>
|
||||
<Text $theme="primary" $variation="800">
|
||||
{t('Add')}
|
||||
</Text>
|
||||
<Icon $theme="primary" $variation="800" iconName="add" />
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user