From 411d52c73b2a5953b41f606438a870d8c6547fea Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Fri, 20 Jun 2025 17:34:04 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F(frontend)=20improve=20separa?= =?UTF-8?q?tion=20of=20concerns=20in=20DocShareModal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../docs/doc-share/api/useDeleteDocAccess.ts | 5 - .../docs/doc-share/api/useDocAccesses.tsx | 52 ++------ .../docs/doc-share/api/useDocInvitations.tsx | 52 +++----- .../docs/doc-share/api/useUpdateDocAccess.ts | 5 - ...itationItem.tsx => DocShareInvitation.tsx} | 107 ++++++++++++++++- ...ShareMemberItem.tsx => DocShareMember.tsx} | 67 ++++++++++- .../doc-share/components/DocShareModal.tsx | 113 ++---------------- .../DocShareModalInviteUserByEmail.tsx | 42 ------- 8 files changed, 202 insertions(+), 241 deletions(-) rename src/frontend/apps/impress/src/features/docs/doc-share/components/{DocShareInvitationItem.tsx => DocShareInvitation.tsx} (50%) rename src/frontend/apps/impress/src/features/docs/doc-share/components/{DocShareMemberItem.tsx => DocShareMember.tsx} (61%) delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModalInviteUserByEmail.tsx diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/api/useDeleteDocAccess.ts b/src/frontend/apps/impress/src/features/docs/doc-share/api/useDeleteDocAccess.ts index 9a52189e..a563ea7e 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-share/api/useDeleteDocAccess.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-share/api/useDeleteDocAccess.ts @@ -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); - } - }, }); }; diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/api/useDocAccesses.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/api/useDocAccesses.tsx index aa65e3f7..f072f657 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-share/api/useDocAccesses.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-share/api/useDocAccesses.tsx @@ -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, - QueryKey, - number - >, -) { - return useInfiniteQuery< - AccessesResponse, - APIError, - InfiniteData, - 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); } diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/api/useDocInvitations.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/api/useDocInvitations.tsx index 785d0482..150f99ad 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-share/api/useDocInvitations.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-share/api/useDocInvitations.tsx @@ -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, - QueryKey, - number - >, -) { - return useInfiniteQuery< - DocInvitationsResponse, - APIError, - InfiniteData, - 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, + ); } diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/api/useUpdateDocAccess.ts b/src/frontend/apps/impress/src/features/docs/doc-share/api/useUpdateDocAccess.ts index 5119c0ae..58183dcb 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-share/api/useUpdateDocAccess.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-share/api/useUpdateDocAccess.ts @@ -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); - } - }, }); }; diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareInvitationItem.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareInvitation.tsx similarity index 50% rename from src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareInvitationItem.tsx rename to src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareInvitation.tsx index 76a04fbd..91507915 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareInvitationItem.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareInvitation.tsx @@ -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 ( { { ); }; + +type DocShareModalInviteUserRowProps = { + user: User; +}; +export const DocShareModalInviteUserRow = ({ + user, +}: DocShareModalInviteUserRowProps) => { + const { t } = useTranslation(); + return ( + + + + {t('Add')} + + + + } + /> + + ); +}; + +interface QuickSearchGroupInvitationProps { + doc: Doc; +} + +export const QuickSearchGroupInvitation = ({ + doc, +}: QuickSearchGroupInvitationProps) => { + 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]); + + if (!invitationsData.elements.length) { + return null; + } + + return ( + + ( + + )} + /> + + ); +}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareMemberItem.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareMember.tsx similarity index 61% rename from src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareMemberItem.tsx rename to src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareMember.tsx index 4da05ec7..8109325d 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareMemberItem.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareMember.tsx @@ -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) => { ); }; + +interface QuickSearchGroupMemberProps { + doc: Doc; +} + +export const QuickSearchGroupMember = ({ + doc, +}: QuickSearchGroupMemberProps) => { + const { t } = useTranslation(); + const membersQuery = useDocAccessesInfinite({ + docId: doc.id, + }); + + const membersData: QuickSearchData = 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: , + onSelect: () => void membersQuery.fetchNextPage(), + }, + ] + : undefined, + }; + }, [membersQuery, t]); + + return ( + + ( + + )} + /> + + ); +}; 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 54c36d24..ebd3f3f2 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 @@ -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 = 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: , - 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 ? ( - + <> + + + ) : ( ); }; - -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 && ( - - ( - - )} - /> - - )} - - - ( - - )} - /> - - - ); -}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModalInviteUserByEmail.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModalInviteUserByEmail.tsx deleted file mode 100644 index 35f741fa..00000000 --- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModalInviteUserByEmail.tsx +++ /dev/null @@ -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 ( - - - - {t('Add')} - - - - } - /> - - ); -};