From c011a09910d079f0a0d9aff92b8856e35872847f Mon Sep 17 00:00:00 2001 From: Thibault Reidy Date: Thu, 22 Aug 2024 17:28:38 +0200 Subject: [PATCH] feat: finalise the migration to v5 --- src/.d.ts | 13 + src/hooks/invitation.ts | 8 +- src/hooks/itemGeolocation.ts | 23 +- src/hooks/itemPublish.ts | 52 +-- src/hooks/itemTag.ts | 51 +-- src/hooks/membership.ts | 55 ++-- src/item/accessible/hooks.ts | 91 +++--- src/item/create/mutations.ts | 131 ++++---- src/item/descendants/hooks.ts | 43 +-- src/item/h5p/mutations.ts | 38 ++- src/item/hooks.ts | 223 +++++++------ src/item/import-zip/mutations.ts | 32 +- src/item/mutations.ts | 448 ++++++++++++-------------- src/item/reorder/mutations.ts | 39 +-- src/item/thumbnail/mutations.ts | 149 +++++---- src/member/hooks.ts | 51 +-- src/member/mutations.ts | 284 ++++++++-------- src/member/publicProfile/mutations.ts | 72 ++--- src/member/subscription/mutations.ts | 60 ++-- src/queryClient.ts | 21 +- src/utils/useOnDataChanged.ts | 39 +++ src/ws/hooks/item.ts | 32 +- tsconfig.json | 2 +- 23 files changed, 1031 insertions(+), 926 deletions(-) create mode 100644 src/.d.ts create mode 100644 src/utils/useOnDataChanged.ts diff --git a/src/.d.ts b/src/.d.ts new file mode 100644 index 000000000..5c1d812cc --- /dev/null +++ b/src/.d.ts @@ -0,0 +1,13 @@ +import '@tanstack/react-query'; + +interface MyMeta extends Record { + // Your meta type definition. + typeError?: string; +} + +declare module '@tanstack/react-query' { + interface Register { + queryMeta: MyMeta; + mutationMeta: MyMeta; + } +} diff --git a/src/hooks/invitation.ts b/src/hooks/invitation.ts index d37d19443..bbfce5189 100644 --- a/src/hooks/invitation.ts +++ b/src/hooks/invitation.ts @@ -9,7 +9,7 @@ import { getInvitationRoutine } from '../routines/invitation.js'; import { QueryClientConfig } from '../types.js'; export default (queryConfig: QueryClientConfig) => { - const { notifier, defaultQueryOptions } = queryConfig; + const { defaultQueryOptions } = queryConfig; const useInvitation = (id?: UUID) => useQuery({ @@ -20,11 +20,11 @@ export default (queryConfig: QueryClientConfig) => { } return Api.getInvitation(queryConfig, id); }, + meta: { + typeError: getInvitationRoutine.FAILURE, + }, ...defaultQueryOptions, enabled: Boolean(id), - onError: (error) => { - notifier?.({ type: getInvitationRoutine.FAILURE, payload: { error } }); - }, }); const useItemInvitations = (itemId?: UUID) => diff --git a/src/hooks/itemGeolocation.ts b/src/hooks/itemGeolocation.ts index ab36b7937..0ea74c290 100644 --- a/src/hooks/itemGeolocation.ts +++ b/src/hooks/itemGeolocation.ts @@ -20,7 +20,7 @@ import { QueryClientConfig } from '../types.js'; import useDebounce from './useDebounce.js'; export default (queryConfig: QueryClientConfig) => { - const { notifier, defaultQueryOptions } = queryConfig; + const { defaultQueryOptions } = queryConfig; const useItemGeolocation = (id?: DiscriminatedItem['id']) => useQuery({ @@ -33,11 +33,8 @@ export default (queryConfig: QueryClientConfig) => { }, ...defaultQueryOptions, enabled: Boolean(id), - onError: (error) => { - notifier?.({ - type: getItemGeolocationRoutine.FAILURE, - payload: { error }, - }); + meta: { + typeError: getItemGeolocationRoutine.FAILURE, }, }); @@ -115,11 +112,8 @@ export default (queryConfig: QueryClientConfig) => { }, ...defaultQueryOptions, enabled: Boolean((lat || lat === 0) && (lng || lng === 0)) && enabled, - onError: (error) => { - notifier?.({ - type: getAddressFromCoordinatesRoutine.FAILURE, - payload: { error }, - }); + meta: { + typeError: getAddressFromCoordinatesRoutine.FAILURE, }, }); }; @@ -155,11 +149,8 @@ export default (queryConfig: QueryClientConfig) => { }, ...defaultQueryOptions, enabled: Boolean(debouncedAddress) && enabled, - onError: (error) => { - notifier?.({ - type: getSuggestionsForAddressRoutine.FAILURE, - payload: { error }, - }); + meta: { + typeError: getSuggestionsForAddressRoutine.FAILURE, }, }); }; diff --git a/src/hooks/itemPublish.ts b/src/hooks/itemPublish.ts index 2cdabe50c..bd4b01380 100644 --- a/src/hooks/itemPublish.ts +++ b/src/hooks/itemPublish.ts @@ -7,6 +7,7 @@ import * as Api from '../api/itemPublish.js'; import { UndefinedArgument } from '../config/errors.js'; import { itemKeys } from '../keys.js'; import { QueryClientConfig } from '../types.js'; +import { useOnDataChanged } from '../utils/useOnDataChanged.js'; export default (queryConfig: QueryClientConfig) => { const { defaultQueryOptions } = queryConfig; @@ -98,30 +99,35 @@ export default (queryConfig: QueryClientConfig) => { const enabled = (options?.enabled ?? true) && Boolean(args.itemIds.length); const queryClient = useQueryClient(); - return useQuery({ - queryKey: itemKeys.many(args.itemIds).publishedInformation, - queryFn: () => - splitRequestByIdsAndReturn( - args.itemIds, - MAX_TARGETS_FOR_READ_REQUEST, - (chunk) => Api.getManyItemPublishedInformations(chunk, queryConfig), - true, - ), - onSuccess: async (publishedData) => { - // save items in their own key - if (publishedData?.data) { - Object.values(publishedData?.data)?.forEach(async (p) => { - const { id } = p.item; - queryClient.setQueryData( - itemKeys.single(id).publishedInformation, - p, - ); - }); - } + return useOnDataChanged( + useQuery({ + queryKey: itemKeys.many(args.itemIds).publishedInformation, + queryFn: () => + splitRequestByIdsAndReturn( + args.itemIds, + MAX_TARGETS_FOR_READ_REQUEST, + (chunk) => + Api.getManyItemPublishedInformations(chunk, queryConfig), + true, + ), + ...defaultQueryOptions, + enabled, + }), + { + onSuccess: (publishedData) => { + // save items in their own key + if (publishedData?.data) { + Object.values(publishedData?.data)?.forEach((p) => { + const { id } = p.item; + queryClient.setQueryData( + itemKeys.single(id).publishedInformation, + p, + ); + }); + } + }, }, - ...defaultQueryOptions, - enabled, - }); + ); }, }; }; diff --git a/src/hooks/itemTag.ts b/src/hooks/itemTag.ts index d59bde369..f7b4f2c40 100644 --- a/src/hooks/itemTag.ts +++ b/src/hooks/itemTag.ts @@ -7,6 +7,7 @@ import * as Api from '../api/itemTag.js'; import { UndefinedArgument } from '../config/errors.js'; import { itemKeys } from '../keys.js'; import { QueryClientConfig } from '../types.js'; +import { useOnDataChanged } from '../utils/useOnDataChanged.js'; export default (queryConfig: QueryClientConfig) => { const { defaultQueryOptions } = queryConfig; @@ -26,31 +27,35 @@ export default (queryConfig: QueryClientConfig) => { const useItemsTags = (ids?: UUID[]) => { const queryClient = useQueryClient(); - return useQuery({ - queryKey: itemKeys.many(ids).tags, - queryFn: () => { - if (!ids || ids?.length === 0) { - throw new UndefinedArgument(); - } - return splitRequestByIdsAndReturn( - ids, - MAX_TARGETS_FOR_READ_REQUEST, - (chunk) => Api.getItemsTags(chunk, queryConfig), - true, - ); - }, - onSuccess: async (tags) => { - // save tags in their own key - ids?.forEach(async (id) => { - const itemTags = tags?.data?.[id]; - if (itemTags?.length) { - queryClient.setQueryData(itemKeys.single(id).tags, itemTags); + return useOnDataChanged( + useQuery({ + queryKey: itemKeys.many(ids).tags, + queryFn: () => { + if (!ids || ids?.length === 0) { + throw new UndefinedArgument(); } - }); + return splitRequestByIdsAndReturn( + ids, + MAX_TARGETS_FOR_READ_REQUEST, + (chunk) => Api.getItemsTags(chunk, queryConfig), + true, + ); + }, + enabled: Boolean(ids && ids.length), + ...defaultQueryOptions, + }), + { + onSuccess: (tags) => { + // save tags in their own key + ids?.forEach((id) => { + const itemTags = tags?.data?.[id]; + if (itemTags?.length) { + queryClient.setQueryData(itemKeys.single(id).tags, itemTags); + } + }); + }, }, - enabled: Boolean(ids && ids.length), - ...defaultQueryOptions, - }); + ); }; return { useItemTags, useItemsTags }; diff --git a/src/hooks/membership.ts b/src/hooks/membership.ts index 12cde2c2c..27cb6f733 100644 --- a/src/hooks/membership.ts +++ b/src/hooks/membership.ts @@ -11,6 +11,7 @@ import * as Api from '../api/membership.js'; import { UndefinedArgument } from '../config/errors.js'; import { buildManyItemMembershipsKey, itemKeys } from '../keys.js'; import { QueryClientConfig } from '../types.js'; +import { useOnDataChanged } from '../utils/useOnDataChanged.js'; import { configureWsMembershipHooks } from '../ws/index.js'; export default ( @@ -57,33 +58,37 @@ export default ( membershipWsHooks?.useItemsMembershipsUpdates(getUpdates ? ids : null); - return useQuery({ - queryKey: buildManyItemMembershipsKey(ids), - queryFn: () => { - if (!ids) { - throw new UndefinedArgument(); - } + return useOnDataChanged( + useQuery({ + queryKey: buildManyItemMembershipsKey(ids), + queryFn: () => { + if (!ids) { + throw new UndefinedArgument(); + } - return splitRequestByIdsAndReturn( - ids, - MAX_TARGETS_FOR_READ_REQUEST, - (chunk) => Api.getMembershipsForItems(chunk, queryConfig), - ); + return splitRequestByIdsAndReturn( + ids, + MAX_TARGETS_FOR_READ_REQUEST, + (chunk) => Api.getMembershipsForItems(chunk, queryConfig), + ); + }, + enabled: Boolean(ids?.length) && ids?.every((id) => Boolean(id)), + ...defaultQueryOptions, + }), + { + onSuccess: (memberships) => { + // save memberships in their own key + if (memberships) { + ids?.forEach((id) => { + queryClient.setQueryData( + itemKeys.single(id).memberships, + memberships.data[id], + ); + }); + } + }, }, - onSuccess: async (memberships) => { - // save memberships in their own key - if (memberships) { - ids?.forEach(async (id) => { - queryClient.setQueryData( - itemKeys.single(id).memberships, - memberships.data[id], - ); - }); - } - }, - enabled: Boolean(ids?.length) && ids?.every((id) => Boolean(id)), - ...defaultQueryOptions, - }); + ); }, }; }; diff --git a/src/item/accessible/hooks.ts b/src/item/accessible/hooks.ts index 0de7ca2fb..68325ba81 100644 --- a/src/item/accessible/hooks.ts +++ b/src/item/accessible/hooks.ts @@ -9,6 +9,7 @@ import { import useDebounce from '../../hooks/useDebounce.js'; import { itemKeys } from '../../keys.js'; import { QueryClientConfig } from '../../types.js'; +import { useOnDataChanged } from '../../utils/useOnDataChanged.js'; import { getAccessibleItemsRoutine } from '../routines.js'; import { ItemSearchParams } from '../types.js'; import { getAccessibleItems } from './api.js'; @@ -24,33 +25,35 @@ import { getAccessibleItems } from './api.js'; export const useAccessibleItems = (queryConfig: QueryClientConfig) => (params?: ItemSearchParams, pagination?: Partial) => { - const { notifier, defaultQueryOptions } = queryConfig; + const { defaultQueryOptions } = queryConfig; const queryClient = useQueryClient(); const debouncedKeywords = useDebounce(params?.keywords, 500); const finalParams = { ...params, keywords: debouncedKeywords }; const paginationParams = { ...(pagination ?? {}) }; - return useQuery({ - queryKey: itemKeys.accessiblePage(finalParams, paginationParams), - queryFn: () => - getAccessibleItems(finalParams, paginationParams, queryConfig), - onSuccess: async ({ data: items }) => { - // save items in their own key - // eslint-disable-next-line no-unused-expressions - items?.forEach(async (item) => { - const { id } = item; - queryClient.setQueryData(itemKeys.single(id).content, item); - }); - }, - onError: (error) => { - notifier?.({ - type: getAccessibleItemsRoutine.FAILURE, - payload: { error }, - }); + return useOnDataChanged( + useQuery({ + queryKey: itemKeys.accessiblePage(finalParams, paginationParams), + queryFn: () => + getAccessibleItems(finalParams, paginationParams, queryConfig), + meta: { + typeError: getAccessibleItemsRoutine.FAILURE, + }, + ...defaultQueryOptions, + }), + { + onSuccess: (paginated) => { + const { data: items } = paginated ?? {}; + // save items in their own key + // eslint-disable-next-line no-unused-expressions + items?.forEach((item) => { + const { id } = item; + queryClient.setQueryData(itemKeys.single(id).content, item); + }); + }, }, - ...defaultQueryOptions, - }); + ); }; /** @@ -67,25 +70,33 @@ export const useInfiniteAccessibleItems = const debouncedKeywords = useDebounce(params?.keywords, 500); const finalParams = { ...params, keywords: debouncedKeywords }; - return useInfiniteQuery({ - queryKey: itemKeys.infiniteAccessible(finalParams), - queryFn: ({ pageParam }) => - getAccessibleItems( - finalParams, - { page: pageParam ?? 1, ...pagination }, - queryConfig, - ), - onSuccess: async ({ pages }) => { - // save items in their own key - // eslint-disable-next-line no-unused-expressions - for (const p of pages) { - p?.data?.forEach(async (item) => { - const { id } = item; - queryClient.setQueryData(itemKeys.single(id).content, item); - }); - } + return useOnDataChanged( + useInfiniteQuery({ + queryKey: itemKeys.infiniteAccessible(finalParams), + queryFn: ({ pageParam }) => + getAccessibleItems( + finalParams, + { page: pageParam ?? 1, ...pagination }, + queryConfig, + ), + getNextPageParam: (_lastPage, pages) => pages.length + 1, + refetchOnWindowFocus: () => false, + initialPageParam: 1, + }), + { + onSuccess: (data) => { + const { pages } = data ?? {}; + if (pages) { + // save items in their own key + // eslint-disable-next-line no-unused-expressions + for (const p of pages) { + p?.data?.forEach(async (item) => { + const { id } = item; + queryClient.setQueryData(itemKeys.single(id).content, item); + }); + } + } + }, }, - getNextPageParam: (_lastPage, pages) => pages.length + 1, - refetchOnWindowFocus: () => false, - }); + ); }; diff --git a/src/item/create/mutations.ts b/src/item/create/mutations.ts index 407ee3c8a..74572fe9c 100644 --- a/src/item/create/mutations.ts +++ b/src/item/create/mutations.ts @@ -22,8 +22,10 @@ import { postItem, postItemWithThumbnail, uploadFiles } from './api.js'; export const usePostItem = (queryConfig: QueryClientConfig) => () => { const queryClient = useQueryClient(); const { notifier } = queryConfig; - return useMutation( - async (item: PostItemPayloadType | PostItemWithThumbnailPayloadType) => { + return useMutation({ + mutationFn: async ( + item: PostItemPayloadType | PostItemWithThumbnailPayloadType, + ) => { // check if thumbnail was provided and if it is defined if ('thumbnail' in item && item.thumbnail) { return postItemWithThumbnail(item, queryConfig); @@ -31,27 +33,27 @@ export const usePostItem = (queryConfig: QueryClientConfig) => () => { return postItem(item, queryConfig); }, // we cannot optimistically add an item because we need its id - { - onSuccess: () => { - notifier?.({ - type: createItemRoutine.SUCCESS, - payload: { message: SUCCESS_MESSAGES.CREATE_ITEM }, - }); - }, - onError: (error: Error) => { - notifier?.({ type: createItemRoutine.FAILURE, payload: { error } }); - }, - onSettled: (_data, _error, { geolocation, parentId }) => { - const key = getKeyForParentId(parentId); - queryClient.invalidateQueries(key); + onSuccess: () => { + notifier?.({ + type: createItemRoutine.SUCCESS, + payload: { message: SUCCESS_MESSAGES.CREATE_ITEM }, + }); + }, + onError: (error: Error) => { + notifier?.({ type: createItemRoutine.FAILURE, payload: { error } }); + }, + onSettled: (_data, _error, { geolocation, parentId }) => { + const key = getKeyForParentId(parentId); + queryClient.invalidateQueries({ queryKey: key }); - // if item has geolocation, invalidate map related keys - if (geolocation) { - queryClient.invalidateQueries(itemsWithGeolocationKeys.allBounds); - } - }, + // if item has geolocation, invalidate map related keys + if (geolocation) { + queryClient.invalidateQueries({ + queryKey: itemsWithGeolocationKeys.allBounds, + }); + } }, - ); + }); }; /** @@ -64,33 +66,32 @@ export const useUploadFilesFeedback = (queryConfig: QueryClientConfig) => () => { const queryClient = useQueryClient(); const { notifier } = queryConfig; - return useMutation( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async ({ error, data }: { error?: Error; data?: any; id?: string }) => { - throwIfArrayContainsErrorOrReturn(data); - if (error) { - throw new Error(JSON.stringify(error)); - } - }, - { - onSuccess: () => { - notifier?.({ - type: uploadFilesRoutine.SUCCESS, - payload: { message: SUCCESS_MESSAGES.UPLOAD_FILES }, - }); - }, - onError: (axiosError: Error, { error }) => { - notifier?.({ - type: uploadFilesRoutine.FAILURE, - payload: { error: error ?? axiosError }, - }); - }, - onSettled: (_data, _error, { id }) => { - const parentKey = getKeyForParentId(id); - queryClient.invalidateQueries(parentKey); + return useMutation({ + mutationFn: + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async ({ error, data }: { error?: Error; data?: any; id?: string }) => { + throwIfArrayContainsErrorOrReturn(data); + if (error) { + throw new Error(JSON.stringify(error)); + } }, + onSuccess: () => { + notifier?.({ + type: uploadFilesRoutine.SUCCESS, + payload: { message: SUCCESS_MESSAGES.UPLOAD_FILES }, + }); + }, + onError: (axiosError: Error, { error }) => { + notifier?.({ + type: uploadFilesRoutine.FAILURE, + payload: { error: error ?? axiosError }, + }); + }, + onSettled: (_data, _error, { id }) => { + const parentKey = getKeyForParentId(id); + queryClient.invalidateQueries({ queryKey: parentKey }); }, - ); + }); }; /** @@ -101,8 +102,8 @@ export const useUploadFilesFeedback = export const useUploadFiles = (queryConfig: QueryClientConfig) => () => { const queryClient = useQueryClient(); const { notifier } = queryConfig; - return useMutation( - async (args: { + return useMutation({ + mutationFn: async (args: { id?: DiscriminatedItem['id']; files: File[]; previousItemId?: DiscriminatedItem['id']; @@ -135,23 +136,21 @@ export const useUploadFiles = (queryConfig: QueryClientConfig) => () => { return uploadFiles({ ...args, files: validFiles }, queryConfig); }, - { - onSuccess: () => { - notifier?.({ - type: uploadFilesRoutine.SUCCESS, - payload: { message: SUCCESS_MESSAGES.UPLOAD_FILES }, - }); - }, - onError: (error: Error) => { - notifier?.({ - type: uploadFilesRoutine.FAILURE, - payload: { error }, - }); - }, - onSettled: (_data, _error, { id }) => { - const parentKey = getKeyForParentId(id); - queryClient.invalidateQueries(parentKey); - }, + onSuccess: () => { + notifier?.({ + type: uploadFilesRoutine.SUCCESS, + payload: { message: SUCCESS_MESSAGES.UPLOAD_FILES }, + }); + }, + onError: (error: Error) => { + notifier?.({ + type: uploadFilesRoutine.FAILURE, + payload: { error }, + }); + }, + onSettled: (_data, _error, { id }) => { + const parentKey = getKeyForParentId(id); + queryClient.invalidateQueries({ queryKey: parentKey }); }, - ); + }); }; diff --git a/src/item/descendants/hooks.ts b/src/item/descendants/hooks.ts index b2132ba27..952cefb60 100644 --- a/src/item/descendants/hooks.ts +++ b/src/item/descendants/hooks.ts @@ -5,6 +5,7 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'; import { UndefinedArgument } from '../../config/errors.js'; import { itemKeys } from '../../keys.js'; import { QueryClientConfig } from '../../types.js'; +import { useOnDataChanged } from '../../utils/useOnDataChanged.js'; import { getDescendants } from './api.js'; export const useDescendants = @@ -23,24 +24,28 @@ export const useDescendants = const { defaultQueryOptions } = queryConfig; const queryClient = useQueryClient(); - return useQuery({ - queryKey: itemKeys.single(id).descendants({ types, showHidden }), - queryFn: () => { - if (!id) { - throw new UndefinedArgument(); - } - return getDescendants({ id, types, showHidden }, queryConfig); + return useOnDataChanged( + useQuery({ + queryKey: itemKeys.single(id).descendants({ types, showHidden }), + queryFn: () => { + if (!id) { + throw new UndefinedArgument(); + } + return getDescendants({ id, types, showHidden }, queryConfig); + }, + ...defaultQueryOptions, + enabled: enabled && Boolean(id), + }), + { + onSuccess: (items) => { + if (items?.length) { + // save items in their own key + items.forEach((item) => { + const { id: itemId } = item; + queryClient.setQueryData(itemKeys.single(itemId).content, item); + }); + } + }, }, - onSuccess: async (items) => { - if (items?.length) { - // save items in their own key - items.forEach(async (item) => { - const { id: itemId } = item; - queryClient.setQueryData(itemKeys.single(itemId).content, item); - }); - } - }, - ...defaultQueryOptions, - enabled: enabled && Boolean(id), - }); + ); }; diff --git a/src/item/h5p/mutations.ts b/src/item/h5p/mutations.ts index ee56872d9..65deef085 100644 --- a/src/item/h5p/mutations.ts +++ b/src/item/h5p/mutations.ts @@ -12,30 +12,28 @@ import { importH5P } from './api.js'; export const useImportH5P = (queryConfig: QueryClientConfig) => () => { const queryClient = useQueryClient(); const { notifier } = queryConfig; - return useMutation( - async (args: { + return useMutation({ + mutationFn: async (args: { id?: DiscriminatedItem['id']; file: Blob; previousItemId?: DiscriminatedItem['id']; onUploadProgress?: (progressEvent: AxiosProgressEvent) => void; }) => importH5P(args, queryConfig), - { - onSuccess: () => { - notifier?.({ - type: importH5PRoutine.SUCCESS, - payload: { message: SUCCESS_MESSAGES.IMPORT_H5P }, - }); - }, - onError: (error: Error) => { - notifier?.({ - type: importH5PRoutine.FAILURE, - payload: { error }, - }); - }, - onSettled: (_data, _error, { id }) => { - const parentKey = getKeyForParentId(id); - queryClient.invalidateQueries(parentKey); - }, + onSuccess: () => { + notifier?.({ + type: importH5PRoutine.SUCCESS, + payload: { message: SUCCESS_MESSAGES.IMPORT_H5P }, + }); }, - ); + onError: (error: Error) => { + notifier?.({ + type: importH5PRoutine.FAILURE, + payload: { error }, + }); + }, + onSettled: (_data, _error, { id }) => { + const parentKey = getKeyForParentId(id); + queryClient.invalidateQueries({ queryKey: parentKey }); + }, + }); }; diff --git a/src/item/hooks.ts b/src/item/hooks.ts index 76ca4a6fc..83b4e56a0 100644 --- a/src/item/hooks.ts +++ b/src/item/hooks.ts @@ -20,6 +20,7 @@ import { UndefinedArgument } from '../config/errors.js'; import useDebounce from '../hooks/useDebounce.js'; import { OWN_ITEMS_KEY, itemKeys, memberKeys } from '../keys.js'; import { QueryClientConfig } from '../types.js'; +import { useOnDataChanged } from '../utils/useOnDataChanged.js'; import { paginate } from '../utils/util.js'; import { configureWsItemHooks } from '../ws/index.js'; import { @@ -52,8 +53,8 @@ const config = ( useQuery({ queryKey: OWN_ITEMS_KEY, queryFn: () => Api.getOwnItems(queryConfig), - onError: (error) => { - notifier?.({ type: getOwnItemsRoutine.FAILURE, payload: { error } }); + meta: { + typeError: getOwnItemsRoutine.FAILURE, }, ...defaultQueryOptions, }), @@ -75,35 +76,39 @@ const config = ( // cannot debounce on array directly const debouncedKeywords = useDebounce(params?.keywords, 500); - return useQuery({ - queryKey: itemKeys.single(id).children({ - ordered, - types: params?.types, - keywords: debouncedKeywords, + return useOnDataChanged( + useQuery({ + queryKey: itemKeys.single(id).children({ + ordered, + types: params?.types, + keywords: debouncedKeywords, + }), + queryFn: () => { + if (!id) { + throw new UndefinedArgument(); + } + return Api.getChildren( + id, + { ...params, ordered, keywords: debouncedKeywords }, + queryConfig, + ); + }, + ...defaultQueryOptions, + enabled: Boolean(id) && enabled, + placeholderData: options?.placeholderData, }), - queryFn: () => { - if (!id) { - throw new UndefinedArgument(); - } - return Api.getChildren( - id, - { ...params, ordered, keywords: debouncedKeywords }, - queryConfig, - ); - }, - onSuccess: async (items) => { - if (items?.length) { - // save items in their own key - items.forEach(async (item) => { - const { id: itemId } = item; - queryClient.setQueryData(itemKeys.single(itemId).content, item); - }); - } + { + onSuccess: (items) => { + if (items?.length) { + // save items in their own key + items.forEach((item) => { + const { id: itemId } = item; + queryClient.setQueryData(itemKeys.single(itemId).content, item); + }); + } + }, }, - ...defaultQueryOptions, - enabled: Boolean(id) && enabled, - placeholderData: options?.placeholderData, - }); + ); }, useChildrenPaginated: ( @@ -121,27 +126,26 @@ const config = ( ...defaultQueryOptions, }; - return useInfiniteQuery( - itemKeys.single(id).paginatedChildren, - ({ pageParam = 1 }) => + return useInfiniteQuery({ + queryKey: itemKeys.single(id).paginatedChildren, + queryFn: ({ pageParam = 1 }) => paginate( children, options?.itemsPerPage || PAGINATED_ITEMS_PER_PAGE, pageParam, options?.filterFunction, ), - { - enabled, - getNextPageParam: (lastPage) => { - const { pageNumber } = lastPage; - if (pageNumber !== -1) { - return pageNumber + 1; - } - return undefined; - }, - ...childrenPaginatedOptions, + getNextPageParam: (lastPage) => { + const { pageNumber } = lastPage; + if (pageNumber !== -1) { + return pageNumber + 1; + } + return undefined; }, - ); + initialPageParam: 1, + enabled, + ...childrenPaginatedOptions, + }); }, /** @@ -160,29 +164,33 @@ const config = ( enabled?: boolean; }) => { const queryClient = useQueryClient(); - return useQuery({ - queryKey: itemKeys.single(id).parents, - queryFn: () => { - if (!id) { - throw new UndefinedArgument(); - } + return useOnDataChanged( + useQuery({ + queryKey: itemKeys.single(id).parents, + queryFn: () => { + if (!id) { + throw new UndefinedArgument(); + } - return Api.getParents({ id, path }, queryConfig).then( - (items) => items, - ); - }, - onSuccess: async (items) => { - if (items?.length) { - // save items in their own key - items.forEach(async (item) => { - const { id: itemId } = item; - queryClient.setQueryData(itemKeys.single(itemId).content, item); - }); - } + return Api.getParents({ id, path }, queryConfig).then( + (items) => items, + ); + }, + ...defaultQueryOptions, + enabled: enabled && Boolean(id), + }), + { + onSuccess: (items) => { + if (items?.length) { + // save items in their own key + items.forEach((item) => { + const { id: itemId } = item; + queryClient.setQueryData(itemKeys.single(itemId).content, item); + }); + } + }, }, - ...defaultQueryOptions, - enabled: enabled && Boolean(id), - }); + ); }, useDescendants: useDescendants(queryConfig), @@ -212,31 +220,35 @@ const config = ( // todo: add optimisation to avoid fetching items already in cache useItems: (ids: UUID[]) => { const queryClient = useQueryClient(); - return useQuery({ - queryKey: itemKeys.many(ids).content, - queryFn: () => { - if (!ids) { - throw new UndefinedArgument(); - } - return splitRequestByIdsAndReturn( - ids, - MAX_TARGETS_FOR_READ_REQUEST, - (chunk) => Api.getItems(chunk, queryConfig), - true, - ); - }, - onSuccess: async (items) => { - // save items in their own key - if (items?.data) { - Object.values(items?.data)?.forEach(async (item) => { - const { id } = item; - queryClient.setQueryData(itemKeys.single(id).content, item); - }); - } + return useOnDataChanged( + useQuery({ + queryKey: itemKeys.many(ids).content, + queryFn: () => { + if (!ids) { + throw new UndefinedArgument(); + } + return splitRequestByIdsAndReturn( + ids, + MAX_TARGETS_FOR_READ_REQUEST, + (chunk) => Api.getItems(chunk, queryConfig), + true, + ); + }, + enabled: ids && Boolean(ids.length) && ids.every((id) => Boolean(id)), + ...defaultQueryOptions, + }), + { + onSuccess: (items) => { + // save items in their own key + if (items?.data) { + Object.values(items.data).forEach((item) => { + const { id } = item; + queryClient.setQueryData(itemKeys.single(id).content, item); + }); + } + }, }, - enabled: ids && Boolean(ids.length) && ids.every((id) => Boolean(id)), - ...defaultQueryOptions, - }); + ); }, /** @@ -290,22 +302,27 @@ const config = ( useRecycledItemsData: () => { const queryClient = useQueryClient(); - return useQuery({ - queryKey: memberKeys.current().recycled, - queryFn: () => Api.getRecycledItemsData(queryConfig), - onSuccess: async (items) => { - // save items in their own key - // eslint-disable-next-line no-unused-expressions - items?.forEach(async (item) => { - const { item: recycledItem } = item; - queryClient.setQueryData( - itemKeys.single(recycledItem.id).content, - recycledItem, - ); - }); + + return useOnDataChanged( + useQuery({ + queryKey: memberKeys.current().recycled, + queryFn: () => Api.getRecycledItemsData(queryConfig), + ...defaultQueryOptions, + }), + { + onSuccess: (items) => { + // save items in their own key + // eslint-disable-next-line no-unused-expressions + items?.forEach((item) => { + const { item: recycledItem } = item; + queryClient.setQueryData( + itemKeys.single(recycledItem.id).content, + recycledItem, + ); + }); + }, }, - ...defaultQueryOptions, - }); + ); }, useItemFeedbackUpdates: itemWsHooks?.useItemFeedbackUpdates, diff --git a/src/item/import-zip/mutations.ts b/src/item/import-zip/mutations.ts index afd902fd0..04ac7ae3e 100644 --- a/src/item/import-zip/mutations.ts +++ b/src/item/import-zip/mutations.ts @@ -10,26 +10,24 @@ import { importZip } from './api.js'; export const useImportZip = (queryConfig: QueryClientConfig) => () => { const { notifier } = queryConfig; - return useMutation( - async (args: { + return useMutation({ + mutationFn: async (args: { id?: DiscriminatedItem['id']; file: Blob; onUploadProgress?: (progressEvent: AxiosProgressEvent) => void; }) => importZip(args, queryConfig), - { - onSuccess: () => { - // send request notification, async endpoint - notifier?.({ - type: importZipRoutine.SUCCESS, - payload: { message: REQUEST_MESSAGES.IMPORT_ZIP }, - }); - }, - onError: (error: Error) => { - notifier?.({ - type: importZipRoutine.FAILURE, - payload: { error }, - }); - }, + onSuccess: () => { + // send request notification, async endpoint + notifier?.({ + type: importZipRoutine.SUCCESS, + payload: { message: REQUEST_MESSAGES.IMPORT_ZIP }, + }); }, - ); + onError: (error: Error) => { + notifier?.({ + type: importZipRoutine.FAILURE, + payload: { error }, + }); + }, + }); }; diff --git a/src/item/mutations.ts b/src/item/mutations.ts index 9fa0bd14e..9e29cc49c 100644 --- a/src/item/mutations.ts +++ b/src/item/mutations.ts @@ -65,7 +65,7 @@ export default (queryConfig: QueryClientConfig) => { }): Promise => { const itemKey = itemKeys.single(id).content; - await queryClient.cancelQueries(itemKey); + await queryClient.cancelQueries({ queryKey: itemKey }); // Snapshot the previous value const prevValue = queryClient.getQueryData(itemKey); @@ -91,7 +91,7 @@ export default (queryConfig: QueryClientConfig) => { ? OWN_ITEMS_KEY : itemKeys.single(parentId).allChildren; // Cancel any outgoing re-fetches (so they don't overwrite our optimistic update) - await queryClient.cancelQueries(childrenKey); + await queryClient.cancelQueries({ queryKey: childrenKey }); // Snapshot the previous value if exists // do not create entry if it doesn't exist @@ -111,8 +111,8 @@ export default (queryConfig: QueryClientConfig) => { enableNotifications, }: EnableNotificationsParam = DEFAULT_ENABLE_NOTIFICATIONS) => { const queryClient = useQueryClient(); - return useMutation( - ( + return useMutation({ + mutationFn: ( item: Pick & Partial< Pick< @@ -127,288 +127,270 @@ export default (queryConfig: QueryClientConfig) => { >, ) => Api.editItem(item.id, item, queryConfig), // newItem contains all updatable properties - { - onMutate: async ( - newItem: Partial & - Pick & { - extra?: DiscriminatedItem['extra']; - }, - ) => { - const itemKey = itemKeys.single(newItem.id).content; - - // invalidate key - await queryClient.cancelQueries(itemKey); - - // build full item with new values - const prevItem = queryClient.getQueryData(itemKey); - - // if the item is not in the cache, we don't need to continue with optimistic mutation - if (!prevItem) { - return {}; - } - - // trim manually names because it does it in the backend - const newFullItem = { - ...prevItem, - name: prevItem.name.trim(), - displayName: prevItem.displayName.trim(), - }; - queryClient.setQueryData(itemKey, newFullItem); - - const previousItems = { - ...(Boolean(prevItem) && { - parent: await mutateParentChildren( - { - childPath: prevItem?.path, - value: (old: DiscriminatedItem[]) => { - if (!old?.length) { - return old; - } - const idx = old.findIndex(({ id }) => id === newItem.id); - if (newFullItem && idx >= 0) { - // eslint-disable-next-line no-param-reassign - old[idx] = newFullItem; - } + onMutate: async ( + newItem: Partial & + Pick & { + extra?: DiscriminatedItem['extra']; + }, + ) => { + const itemKey = itemKeys.single(newItem.id).content; + + // invalidate key + await queryClient.cancelQueries({ queryKey: itemKey }); + + // build full item with new values + const prevItem = queryClient.getQueryData(itemKey); + + // if the item is not in the cache, we don't need to continue with optimistic mutation + if (!prevItem) { + return {}; + } + + // trim manually names because it does it in the backend + const newFullItem = { + ...prevItem, + name: prevItem.name.trim(), + displayName: prevItem.displayName.trim(), + }; + queryClient.setQueryData(itemKey, newFullItem); + + const previousItems = { + ...(Boolean(prevItem) && { + parent: await mutateParentChildren( + { + childPath: prevItem?.path, + value: (old: DiscriminatedItem[]) => { + if (!old?.length) { return old; - }, + } + const idx = old.findIndex(({ id }) => id === newItem.id); + if (newFullItem && idx >= 0) { + // eslint-disable-next-line no-param-reassign + old[idx] = newFullItem; + } + return old; }, - queryClient, - ), - item: prevItem, - }), - }; - return previousItems; - }, - onSuccess: () => { - notifier?.( - { - type: editItemRoutine.SUCCESS, - payload: { message: SUCCESS_MESSAGES.EDIT_ITEM }, - }, - { enableNotifications }, - ); - }, - onError: (error: Error, newItem, context) => { - if (context?.parent && context?.item) { - const prevItem = context?.item; - const parentKey = getKeyForParentId( - getParentFromPath(prevItem?.path), - ); - queryClient.setQueryData(parentKey, context.parent); - } - - const itemKey = itemKeys.single(newItem.id).content; - queryClient.setQueryData(itemKey, context?.item); - - notifier?.( - { type: editItemRoutine.FAILURE, payload: { error } }, - { enableNotifications }, - ); - }, - onSettled: (_newItem, _error, { id }, context) => { + }, + queryClient, + ), + item: prevItem, + }), + }; + return previousItems; + }, + onSuccess: () => { + notifier?.( + { + type: editItemRoutine.SUCCESS, + payload: { message: SUCCESS_MESSAGES.EDIT_ITEM }, + }, + { enableNotifications }, + ); + }, + onError: (error: Error, newItem, context) => { + if (context?.parent && context?.item) { const prevItem = context?.item; - if (prevItem) { - const parentKey = getKeyForParentId( - getParentFromPath(prevItem.path), - ); - queryClient.invalidateQueries(parentKey); - } - - const itemKey = itemKeys.single(id).content; - queryClient.invalidateQueries(itemKey); - }, + const parentKey = getKeyForParentId( + getParentFromPath(prevItem?.path), + ); + queryClient.setQueryData(parentKey, context.parent); + } + + const itemKey = itemKeys.single(newItem.id).content; + queryClient.setQueryData(itemKey, context?.item); + + notifier?.( + { type: editItemRoutine.FAILURE, payload: { error } }, + { enableNotifications }, + ); + }, + onSettled: (_newItem, _error, { id }, context) => { + const prevItem = context?.item; + if (prevItem) { + const parentKey = getKeyForParentId(getParentFromPath(prevItem.path)); + queryClient.invalidateQueries({ queryKey: parentKey }); + } + + const itemKey = itemKeys.single(id).content; + queryClient.invalidateQueries({ queryKey: itemKey }); }, - ); + }); }; const useRecycleItems = () => { const queryClient = useQueryClient(); - return useMutation( - (itemIds: UUID[]) => + return useMutation({ + mutationFn: (itemIds: UUID[]) => splitRequestByIds(itemIds, MAX_TARGETS_FOR_MODIFY_REQUEST, (chunk) => Api.recycleItems(chunk, queryConfig), ), - { - onMutate: async (itemIds: UUID[]) => { - // get path from first item and invalidate parent's children - const itemKey = itemKeys.single(itemIds[0]).content; - const itemData = queryClient.getQueryData(itemKey); - const itemPath = itemData?.path; - const newParent = itemPath - ? { - parent: await mutateParentChildren( - { - childPath: itemPath, - value: (old: DiscriminatedItem[]) => - old.filter(({ id }) => !itemIds.includes(id)), - }, - queryClient, - ), - } - : {}; - const previousItems = { - ...newParent, - }; - // items themselves still exist but the path is different - return previousItems; - }, - onError: (error: Error) => { - notifier?.({ type: recycleItemsRoutine.FAILURE, payload: { error } }); - - // does not settled since endpoint is async - }, + onMutate: async (itemIds: UUID[]) => { + // get path from first item and invalidate parent's children + const itemKey = itemKeys.single(itemIds[0]).content; + const itemData = queryClient.getQueryData(itemKey); + const itemPath = itemData?.path; + const newParent = itemPath + ? { + parent: await mutateParentChildren( + { + childPath: itemPath, + value: (old: DiscriminatedItem[]) => + old.filter(({ id }) => !itemIds.includes(id)), + }, + queryClient, + ), + } + : {}; + const previousItems = { + ...newParent, + }; + // items themselves still exist but the path is different + return previousItems; + }, + onError: (error: Error) => { + notifier?.({ type: recycleItemsRoutine.FAILURE, payload: { error } }); + + // does not settled since endpoint is async }, - ); + }); }; const useDeleteItems = () => { const queryClient = useQueryClient(); - return useMutation( - (itemIds) => + return useMutation({ + mutationFn: (itemIds) => splitRequestByIds( itemIds, MAX_TARGETS_FOR_MODIFY_REQUEST, (chunk) => Api.deleteItems(chunk, queryConfig), ), - - { - onMutate: async (itemIds: UUID[]) => { - // get path from first item - const itemKey = memberKeys.current().recycled; - const itemData = - queryClient.getQueryData(itemKey); - queryClient.setQueryData( - itemKey, - itemData?.filter(({ item: { id } }) => !itemIds.includes(id)), - ); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const previousItems: any = { - parent: itemData, - }; - - itemIds.forEach(async (id) => { - previousItems[id] = await mutateItem({ - queryClient, - id, - value: null, - }); + onMutate: async (itemIds: UUID[]) => { + // get path from first item + const itemKey = memberKeys.current().recycled; + const itemData = queryClient.getQueryData(itemKey); + queryClient.setQueryData( + itemKey, + itemData?.filter(({ item: { id } }) => !itemIds.includes(id)), + ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const previousItems: any = { + parent: itemData, + }; + + itemIds.forEach(async (id) => { + previousItems[id] = await mutateItem({ + queryClient, + id, + value: null, }); - return previousItems; - }, - onError: (error: Error) => { - notifier?.({ type: deleteItemsRoutine.FAILURE, payload: { error } }); + }); + return previousItems; + }, + onError: (error: Error) => { + notifier?.({ type: deleteItemsRoutine.FAILURE, payload: { error } }); - // does not settled since endpoint is async - }, + // does not settled since endpoint is async }, - ); + }); }; const useCopyItems = () => - useMutation( - ({ ids, to }: { ids: UUID[]; to?: UUID }) => + useMutation({ + mutationFn: ({ ids, to }: { ids: UUID[]; to?: UUID }) => splitRequestByIds(ids, MAX_TARGETS_FOR_MODIFY_REQUEST, (chunk) => Api.copyItems({ ids: chunk, to }, queryConfig), ), // cannot mutate because it needs the id - { - onError: (error: Error) => { - notifier?.({ type: copyItemsRoutine.FAILURE, payload: { error } }); + onError: (error: Error) => { + notifier?.({ type: copyItemsRoutine.FAILURE, payload: { error } }); - // does not settled since endpoint is async - }, + // does not settled since endpoint is async }, - ); + }); const useMoveItems = () => { const queryClient = useQueryClient(); - return useMutation( - ({ items, to }: { items: PackedItem[]; to?: UUID }) => + return useMutation({ + mutationFn: ({ items, to }: { items: PackedItem[]; to?: UUID }) => splitRequestByIds( items.map((i) => i.id), MAX_TARGETS_FOR_MODIFY_REQUEST, (chunk) => Api.moveItems({ ids: chunk, to }, queryConfig), ), - { - onMutate: async ({ items, to }) => { - const itemIds = items.map((i) => i.id); - if (items.length) { - // suppose items are at the same level - const { path } = items[0]; - // add item in target item - await mutateParentChildren( - { - id: to, - value: (old: PackedItem[]) => old?.concat(items), - }, - queryClient, - ); - - // remove item in original item - await mutateParentChildren( - { - childPath: path, - value: (old: PackedItem[]) => - old?.filter(({ id: oldId }) => !itemIds.includes(oldId)), - }, - queryClient, - ); - } + onMutate: async ({ items, to }) => { + const itemIds = items.map((i) => i.id); + if (items.length) { + // suppose items are at the same level + const { path } = items[0]; + // add item in target item + await mutateParentChildren( + { + id: to, + value: (old: PackedItem[]) => old?.concat(items), + }, + queryClient, + ); - const toData = queryClient.getQueryData( - itemKeys.single(to).content, + // remove item in original item + await mutateParentChildren( + { + childPath: path, + value: (old: PackedItem[]) => + old?.filter(({ id: oldId }) => !itemIds.includes(oldId)), + }, + queryClient, ); - if (toData?.id) { - const toDataId = toData.id; - // update item's path - itemIds.forEach(async (itemId: UUID) => { - await mutateItem({ - queryClient, - id: itemId, - value: (item: PackedItem) => ({ - ...item, - path: buildPathFromIds(toDataId, itemId), - }), - }); + } + + const toData = queryClient.getQueryData( + itemKeys.single(to).content, + ); + if (toData?.id) { + const toDataId = toData.id; + // update item's path + itemIds.forEach(async (itemId: UUID) => { + await mutateItem({ + queryClient, + id: itemId, + value: (item: PackedItem) => ({ + ...item, + path: buildPathFromIds(toDataId, itemId), + }), }); - } - }, - // If the mutation fails, use the context returned from onMutate to roll back - onError: (error: Error) => { - notifier?.({ type: moveItemsRoutine.FAILURE, payload: { error } }); - }, + }); + } }, - ); + // If the mutation fails, use the context returned from onMutate to roll back + onError: (error: Error) => { + notifier?.({ type: moveItemsRoutine.FAILURE, payload: { error } }); + }, + }); }; const useRestoreItems = () => { const queryClient = useQueryClient(); - return useMutation( - (itemIds: UUID[]) => + return useMutation({ + mutationFn: (itemIds: UUID[]) => splitRequestByIds(itemIds, MAX_TARGETS_FOR_MODIFY_REQUEST, (chunk) => Api.restoreItems(chunk, queryConfig), ), - { - onMutate: async (itemIds) => { - const key = memberKeys.current().recycled; - const recycleItemData = - queryClient.getQueryData(key); - if (recycleItemData) { - queryClient.setQueryData( - key, - recycleItemData.filter( - ({ item: { id } }) => !itemIds.includes(id), - ), - ); - } - return recycleItemData; - }, - - onError: (error: Error, _itemId) => { - notifier?.({ type: restoreItemsRoutine.FAILURE, payload: { error } }); - }, + onMutate: async (itemIds) => { + const key = memberKeys.current().recycled; + const recycleItemData = + queryClient.getQueryData(key); + if (recycleItemData) { + queryClient.setQueryData( + key, + recycleItemData.filter(({ item: { id } }) => !itemIds.includes(id)), + ); + } + return recycleItemData; + }, + + onError: (error: Error, _itemId) => { + notifier?.({ type: restoreItemsRoutine.FAILURE, payload: { error } }); }, - ); + }); // invalidate only on error since endpoint is async }; diff --git a/src/item/reorder/mutations.ts b/src/item/reorder/mutations.ts index e179cd2e8..41910e21f 100644 --- a/src/item/reorder/mutations.ts +++ b/src/item/reorder/mutations.ts @@ -11,24 +11,25 @@ import { reorderItem } from './api.js'; export const useReorderItem = (queryConfig: QueryClientConfig) => () => { const { notifier } = queryConfig; const queryClient = useQueryClient(); - return useMutation( - (args: { id: UUID; parentItemId: UUID; previousItemId?: UUID }) => - reorderItem(args, queryConfig), - { - onSuccess: () => { - notifier?.({ - type: reorderItemRoutine.SUCCESS, - payload: { message: SUCCESS_MESSAGES.REORDER_ITEM }, - }); - }, - onError: (error: Error) => { - notifier?.({ type: reorderItemRoutine.FAILURE, payload: { error } }); - }, - onSettled: (_data, _error, args) => { - queryClient.invalidateQueries( - itemKeys.single(args.parentItemId).allChildren, - ); - }, + return useMutation({ + mutationFn: (args: { + id: UUID; + parentItemId: UUID; + previousItemId?: UUID; + }) => reorderItem(args, queryConfig), + onSuccess: () => { + notifier?.({ + type: reorderItemRoutine.SUCCESS, + payload: { message: SUCCESS_MESSAGES.REORDER_ITEM }, + }); }, - ); + onError: (error: Error) => { + notifier?.({ type: reorderItemRoutine.FAILURE, payload: { error } }); + }, + onSettled: (_data, _error, args) => { + queryClient.invalidateQueries({ + queryKey: itemKeys.single(args.parentItemId).allChildren, + }); + }, + }); }; diff --git a/src/item/thumbnail/mutations.ts b/src/item/thumbnail/mutations.ts index a308fba19..380a8e1af 100644 --- a/src/item/thumbnail/mutations.ts +++ b/src/item/thumbnail/mutations.ts @@ -16,8 +16,8 @@ export const useUploadItemThumbnail = (queryConfig: QueryClientConfig) => () => { const { notifier } = queryConfig; const queryClient = useQueryClient(); - return useMutation( - (args: { + return useMutation({ + mutationFn: (args: { id: UUID; file: Blob; onUploadProgress?: (progressEvent: AxiosProgressEvent) => void; @@ -28,26 +28,28 @@ export const useUploadItemThumbnail = return uploadItemThumbnail(args, queryConfig); }, - { - onSuccess: () => { - notifier?.({ - type: uploadItemThumbnailRoutine.SUCCESS, - payload: { message: SUCCESS_MESSAGES.UPLOAD_ITEM_THUMBNAIL }, - }); - }, - onError: (error: Error) => { - notifier?.({ - type: uploadItemThumbnailRoutine.FAILURE, - payload: { error }, - }); - }, - onSettled: (_data, _error, { id }) => { - // invalidate item to update settings.hasThumbnail - queryClient.invalidateQueries(itemKeys.single(id).content); - queryClient.invalidateQueries(itemKeys.single(id).allThumbnails); - }, + onSuccess: () => { + notifier?.({ + type: uploadItemThumbnailRoutine.SUCCESS, + payload: { message: SUCCESS_MESSAGES.UPLOAD_ITEM_THUMBNAIL }, + }); + }, + onError: (error: Error) => { + notifier?.({ + type: uploadItemThumbnailRoutine.FAILURE, + payload: { error }, + }); }, - ); + onSettled: (_data, _error, { id }) => { + // invalidate item to update settings.hasThumbnail + queryClient.invalidateQueries({ + queryKey: itemKeys.single(id).content, + }); + queryClient.invalidateQueries({ + queryKey: itemKeys.single(id).allThumbnails, + }); + }, + }); }; /** * @deprecated use useUploadItemThumbnail @@ -59,63 +61,68 @@ export const useUploadItemThumbnailFeedback = (queryConfig: QueryClientConfig) => () => { const queryClient = useQueryClient(); const { notifier } = queryConfig; - return useMutation( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async ({ error }: { id: string; error?: Error; data?: any }) => { - if (error) throw new Error(JSON.stringify(error)); - }, - { - onSuccess: () => { - notifier?.({ - type: uploadItemThumbnailRoutine.SUCCESS, - payload: { message: SUCCESS_MESSAGES.UPLOAD_ITEM_THUMBNAIL }, - }); - }, - onError: (_error, { error }) => { - notifier?.({ - type: uploadItemThumbnailRoutine.FAILURE, - payload: { error }, - }); - }, - onSettled: (_data, _error, { id }) => { - // invalidate item to update settings.hasThumbnail - queryClient.invalidateQueries(itemKeys.single(id).content); - queryClient.invalidateQueries(itemKeys.single(id).allThumbnails); + return useMutation({ + mutationFn: + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async ({ error }: { id: string; error?: Error; data?: any }) => { + if (error) throw new Error(JSON.stringify(error)); }, + onSuccess: () => { + notifier?.({ + type: uploadItemThumbnailRoutine.SUCCESS, + payload: { message: SUCCESS_MESSAGES.UPLOAD_ITEM_THUMBNAIL }, + }); }, - ); + onError: (_error, { error }) => { + notifier?.({ + type: uploadItemThumbnailRoutine.FAILURE, + payload: { error }, + }); + }, + onSettled: (_data, _error, { id }) => { + // invalidate item to update settings.hasThumbnail + queryClient.invalidateQueries({ + queryKey: itemKeys.single(id).content, + }); + queryClient.invalidateQueries({ + queryKey: itemKeys.single(id).allThumbnails, + }); + }, + }); }; export const useDeleteItemThumbnail = (queryConfig: QueryClientConfig) => () => { const { notifier } = queryConfig; const queryClient = useQueryClient(); - return useMutation( - (itemId: UUID) => deleteItemThumbnail(itemId, queryConfig), - { - onSuccess: () => { - notifier?.({ - type: deleteItemThumbnailRoutine.SUCCESS, - payload: { message: SUCCESS_MESSAGES.DELETE_ITEM_THUMBNAIL }, - }); - }, - onError: (error: Error) => { - notifier?.({ - type: deleteItemThumbnailRoutine.FAILURE, - payload: { error }, - }); - }, - onSettled: (_data, _error, id) => { - // invalidateQueries doesn't invalidate if the query is disabled - // so reset the query to avoid issues in the frontend (getting dirty cache). - queryClient.resetQueries({ - queryKey: itemKeys.single(id).allThumbnails, - }); - // try to invalidate the thumbnail (the invalidateQueries doesn't invalidate disabled queries) - queryClient.invalidateQueries(itemKeys.single(id).allThumbnails); - // invalidate item to update settings.hasThumbnail - queryClient.invalidateQueries(itemKeys.single(id).content); - }, + return useMutation({ + mutationFn: (itemId: UUID) => deleteItemThumbnail(itemId, queryConfig), + onSuccess: () => { + notifier?.({ + type: deleteItemThumbnailRoutine.SUCCESS, + payload: { message: SUCCESS_MESSAGES.DELETE_ITEM_THUMBNAIL }, + }); + }, + onError: (error: Error) => { + notifier?.({ + type: deleteItemThumbnailRoutine.FAILURE, + payload: { error }, + }); + }, + onSettled: (_data, _error, id) => { + // invalidateQueries doesn't invalidate if the query is disabled + // so reset the query to avoid issues in the frontend (getting dirty cache). + queryClient.resetQueries({ + queryKey: itemKeys.single(id).allThumbnails, + }); + // try to invalidate the thumbnail (the invalidateQueries doesn't invalidate disabled queries) + queryClient.invalidateQueries({ + queryKey: itemKeys.single(id).allThumbnails, + }); + // invalidate item to update settings.hasThumbnail + queryClient.invalidateQueries({ + queryKey: itemKeys.single(id).content, + }); }, - ); + }); }; diff --git a/src/member/hooks.ts b/src/member/hooks.ts index d57fb9a41..775bd8b3d 100644 --- a/src/member/hooks.ts +++ b/src/member/hooks.ts @@ -16,11 +16,12 @@ import { import { UndefinedArgument } from '../config/errors.js'; import { memberKeys } from '../keys.js'; import { QueryClientConfig } from '../types.js'; +import { useOnDataChanged } from '../utils/useOnDataChanged.js'; import * as Api from './api.js'; import { getMembersRoutine } from './routines.js'; export default (queryConfig: QueryClientConfig) => { - const { defaultQueryOptions, notifier } = queryConfig; + const { defaultQueryOptions } = queryConfig; return { useCurrentMember: () => @@ -45,29 +46,33 @@ export default (queryConfig: QueryClientConfig) => { useMembers: (ids: UUID[]) => { const queryClient = useQueryClient(); - return useQuery({ - queryKey: memberKeys.many(ids), - queryFn: async () => - splitRequestByIdsAndReturn( - ids, - MAX_TARGETS_FOR_READ_REQUEST, - (chunk) => Api.getMembers({ ids: chunk }, queryConfig), - ), - onSuccess: async (members) => { - // save members in their own key - if (members?.data) { - Object.values(members?.data).forEach(async (member) => { - const { id } = member; - queryClient.setQueryData(memberKeys.single(id).content, member); - }); - } - }, - onError: (error) => { - notifier?.({ type: getMembersRoutine.FAILURE, payload: { error } }); + return useOnDataChanged( + useQuery({ + queryKey: memberKeys.many(ids), + queryFn: async () => + splitRequestByIdsAndReturn( + ids, + MAX_TARGETS_FOR_READ_REQUEST, + (chunk) => Api.getMembers({ ids: chunk }, queryConfig), + ), + meta: { + typeError: getMembersRoutine.FAILURE, + }, + enabled: Boolean(ids?.length), + ...defaultQueryOptions, + }), + { + onSuccess: (members) => { + // save members in their own key + if (members?.data) { + Object.values(members?.data).forEach((member) => { + const { id } = member; + queryClient.setQueryData(memberKeys.single(id).content, member); + }); + } + }, }, - enabled: Boolean(ids?.length), - ...defaultQueryOptions, - }); + ); }, useAvatar: ({ diff --git a/src/member/mutations.ts b/src/member/mutations.ts index 8d73520ff..ea5db0ec6 100644 --- a/src/member/mutations.ts +++ b/src/member/mutations.ts @@ -25,39 +25,39 @@ export default (queryConfig: QueryClientConfig) => { */ const useDeleteMember = () => { const queryClient = useQueryClient(); - return useMutation( - (payload: { id: UUID }) => Api.deleteMember(payload, queryConfig), - { - onSuccess: () => { - notifier?.({ - type: deleteMemberRoutine.SUCCESS, - payload: { message: SUCCESS_MESSAGES.DELETE_MEMBER }, - }); + return useMutation({ + mutationFn: (payload: { id: UUID }) => + Api.deleteMember(payload, queryConfig), + onSuccess: () => { + notifier?.({ + type: deleteMemberRoutine.SUCCESS, + payload: { message: SUCCESS_MESSAGES.DELETE_MEMBER }, + }); - queryClient.resetQueries(); + queryClient.resetQueries(); - // remove cookies from browser when logout succeeds - if (queryConfig.DOMAIN) { - // todo: find a way to do this with an httpOnly cookie - // removeSession(id, queryConfig.DOMAIN); - // setCurrentSession(null, queryConfig.DOMAIN); - } + // remove cookies from browser when logout succeeds + if (queryConfig.DOMAIN) { + // todo: find a way to do this with an httpOnly cookie + // removeSession(id, queryConfig.DOMAIN); + // setCurrentSession(null, queryConfig.DOMAIN); + } - // Update when the server confirmed the logout, instead optimistically updating the member - // This prevents logout loop (redirect to logout -> still cookie -> logs back in) - queryClient.setQueryData(memberKeys.current().content, undefined); - }, - // If the mutation fails, use the context returned from onMutate to roll back - onError: (error: Error, _args, _context) => { - notifier?.({ type: deleteMemberRoutine.FAILURE, payload: { error } }); - }, + // Update when the server confirmed the logout, instead optimistically updating the member + // This prevents logout loop (redirect to logout -> still cookie -> logs back in) + queryClient.setQueryData(memberKeys.current().content, undefined); }, - ); + // If the mutation fails, use the context returned from onMutate to roll back + onError: (error: Error, _args, _context) => { + notifier?.({ type: deleteMemberRoutine.FAILURE, payload: { error } }); + }, + }); }; const useDeleteCurrentMember = () => { const queryClient = useQueryClient(); - return useMutation(() => Api.deleteCurrentMember(queryConfig), { + return useMutation({ + mutationFn: () => Api.deleteCurrentMember(queryConfig), onSuccess: () => { notifier?.({ type: deleteCurrentMemberRoutine.SUCCESS, @@ -83,62 +83,64 @@ export default (queryConfig: QueryClientConfig) => { // suppose you can only edit yourself const useEditMember = () => { const queryClient = useQueryClient(); - return useMutation( - (payload: { + return useMutation({ + mutationFn: (payload: { id: string; name?: string; enableSaveActions?: boolean; extra?: CompleteMember['extra']; }) => Api.editMember(payload, queryConfig), - { - onMutate: async (member) => { - // Cancel any outgoing refetches (so they don't overwrite our optimistic update) - await queryClient.cancelQueries(memberKeys.current().content); + onMutate: async (member) => { + // Cancel any outgoing refetches (so they don't overwrite our optimistic update) + await queryClient.cancelQueries({ + queryKey: memberKeys.current().content, + }); - // Snapshot the previous value - const previousMember = queryClient.getQueryData( - memberKeys.current().content, - ); + // Snapshot the previous value + const previousMember = queryClient.getQueryData( + memberKeys.current().content, + ); - // Optimistically update to the new value - const newMember = previousMember; - if (newMember) { - if (member.name) { - newMember.name = member.name.trim(); - } - if (typeof member.enableSaveActions === 'boolean') { - newMember.enableSaveActions = member.enableSaveActions; - } - if (member.extra) { - newMember.extra = member.extra; - } - queryClient.setQueryData(memberKeys.current().content, newMember); + // Optimistically update to the new value + const newMember = previousMember; + if (newMember) { + if (member.name) { + newMember.name = member.name.trim(); + } + if (typeof member.enableSaveActions === 'boolean') { + newMember.enableSaveActions = member.enableSaveActions; } + if (member.extra) { + newMember.extra = member.extra; + } + queryClient.setQueryData(memberKeys.current().content, newMember); + } - // Return a context object with the snapshotted value - return { previousMember }; - }, - onSuccess: () => { - notifier?.({ - type: editMemberRoutine.SUCCESS, - payload: { message: SUCCESS_MESSAGES.EDIT_MEMBER }, - }); - }, - // If the mutation fails, use the context returned from onMutate to roll back - onError: (error: Error, _, context) => { - notifier?.({ type: editMemberRoutine.FAILURE, payload: { error } }); - queryClient.setQueryData( - memberKeys.current().content, - context?.previousMember, - ); - }, - // Always refetch after error or success: - onSettled: () => { - // invalidate all queries - queryClient.invalidateQueries(memberKeys.current().content); - }, + // Return a context object with the snapshotted value + return { previousMember }; + }, + onSuccess: () => { + notifier?.({ + type: editMemberRoutine.SUCCESS, + payload: { message: SUCCESS_MESSAGES.EDIT_MEMBER }, + }); }, - ); + // If the mutation fails, use the context returned from onMutate to roll back + onError: (error: Error, _, context) => { + notifier?.({ type: editMemberRoutine.FAILURE, payload: { error } }); + queryClient.setQueryData( + memberKeys.current().content, + context?.previousMember, + ); + }, + // Always refetch after error or success: + onSettled: () => { + // invalidate all queries + queryClient.invalidateQueries({ + queryKey: memberKeys.current().content, + }); + }, + }); }; // this mutation is used for its callback and invalidate the keys @@ -148,27 +150,28 @@ export default (queryConfig: QueryClientConfig) => { */ const useUploadAvatar = () => { const queryClient = useQueryClient(); - return useMutation( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async ({ error, data }: { error?: any; data?: any; id: UUID }) => { - throwIfArrayContainsErrorOrReturn(data); - if (error) throw new Error(JSON.stringify(error)); - }, - { - onSuccess: () => { - notifier?.({ - type: uploadAvatarRoutine.SUCCESS, - payload: { message: SUCCESS_MESSAGES.UPLOAD_AVATAR }, - }); - }, - onError: (_error, { error }) => { - notifier?.({ type: uploadAvatarRoutine.FAILURE, payload: { error } }); - }, - onSettled: (_data, _error, { id }) => { - queryClient.invalidateQueries(memberKeys.single(id).allAvatars); + return useMutation({ + mutationFn: + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async ({ error, data }: { error?: any; data?: any; id: UUID }) => { + throwIfArrayContainsErrorOrReturn(data); + if (error) throw new Error(JSON.stringify(error)); }, + onSuccess: () => { + notifier?.({ + type: uploadAvatarRoutine.SUCCESS, + payload: { message: SUCCESS_MESSAGES.UPLOAD_AVATAR }, + }); + }, + onError: (_error, { error }) => { + notifier?.({ type: uploadAvatarRoutine.FAILURE, payload: { error } }); }, - ); + onSettled: (_data, _error, { id }) => { + queryClient.invalidateQueries({ + queryKey: memberKeys.single(id).allAvatars, + }); + }, + }); }; /** @@ -177,51 +180,50 @@ export default (queryConfig: QueryClientConfig) => { * @param {Password} currentPassword current password already stored, needs to match old password */ const useUpdatePassword = () => - useMutation( - (payload: { password: Password; currentPassword: Password }) => - Api.updatePassword(payload, queryConfig), - { - onSuccess: () => { - notifier?.({ - type: updatePasswordRoutine.SUCCESS, - payload: { message: SUCCESS_MESSAGES.UPDATE_PASSWORD }, - }); - }, - onError: (error: Error) => { - notifier?.({ - type: updatePasswordRoutine.FAILURE, - payload: { error }, - }); - }, + useMutation({ + mutationFn: (payload: { + password: Password; + currentPassword: Password; + }) => Api.updatePassword(payload, queryConfig), + onSuccess: () => { + notifier?.({ + type: updatePasswordRoutine.SUCCESS, + payload: { message: SUCCESS_MESSAGES.UPDATE_PASSWORD }, + }); }, - ); + onError: (error: Error) => { + notifier?.({ + type: updatePasswordRoutine.FAILURE, + payload: { error }, + }); + }, + }); /** * Mutation to create a member password * @param {Password} password new password to set on current member */ const useCreatePassword = () => - useMutation( - (payload: { password: Password }) => + useMutation({ + mutationFn: (payload: { password: Password }) => Api.createPassword(payload, queryConfig), - { - onSuccess: () => { - notifier?.({ - type: updatePasswordRoutine.SUCCESS, - payload: { message: SUCCESS_MESSAGES.UPDATE_PASSWORD }, - }); - }, - onError: (error: Error) => { - notifier?.({ - type: updatePasswordRoutine.FAILURE, - payload: { error }, - }); - }, + onSuccess: () => { + notifier?.({ + type: updatePasswordRoutine.SUCCESS, + payload: { message: SUCCESS_MESSAGES.UPDATE_PASSWORD }, + }); }, - ); + onError: (error: Error) => { + notifier?.({ + type: updatePasswordRoutine.FAILURE, + payload: { error }, + }); + }, + }); const useUpdateMemberEmail = () => - useMutation((newEmail: string) => Api.updateEmail(newEmail, queryConfig), { + useMutation({ + mutationFn: (newEmail: string) => Api.updateEmail(newEmail, queryConfig), onSuccess: () => { notifier?.({ type: updateEmailRoutine.SUCCESS, @@ -237,26 +239,26 @@ export default (queryConfig: QueryClientConfig) => { }); const useValidateEmailUpdate = () => - useMutation( - (token: string) => Api.validateEmailUpdate(token, queryConfig), - { - onSuccess: () => { - notifier?.({ - type: updateEmailRoutine.SUCCESS, - payload: { message: SUCCESS_MESSAGES.VALIDATE_EMAIL }, - }); - }, - onError: (error: Error) => { - notifier?.({ - type: updateEmailRoutine.FAILURE, - payload: { error }, - }); - }, + useMutation({ + mutationFn: (token: string) => + Api.validateEmailUpdate(token, queryConfig), + onSuccess: () => { + notifier?.({ + type: updateEmailRoutine.SUCCESS, + payload: { message: SUCCESS_MESSAGES.VALIDATE_EMAIL }, + }); }, - ); + onError: (error: Error) => { + notifier?.({ + type: updateEmailRoutine.FAILURE, + payload: { error }, + }); + }, + }); const useExportMemberData = () => - useMutation(() => Api.exportMemberData(queryConfig), { + useMutation({ + mutationFn: () => Api.exportMemberData(queryConfig), onSuccess: () => { notifier?.({ type: exportMemberDataRoutine.SUCCESS, diff --git a/src/member/publicProfile/mutations.ts b/src/member/publicProfile/mutations.ts index 6b4e9f35e..53b1afef7 100644 --- a/src/member/publicProfile/mutations.ts +++ b/src/member/publicProfile/mutations.ts @@ -17,50 +17,50 @@ export default (queryConfig: QueryClientConfig) => { const usePostPublicProfile = () => { const queryClient = useQueryClient(); - return useMutation( - async (profileData: PostPublicProfilePayloadType) => + return useMutation({ + mutationFn: async (profileData: PostPublicProfilePayloadType) => Api.postPublicProfile(profileData, queryConfig), - { - onSuccess: () => { - notifier?.({ - type: postPublicProfileRoutine.SUCCESS, - payload: { message: SUCCESS_MESSAGES.POST_PROFILE }, - }); - // refetch profile information - queryClient.invalidateQueries(memberKeys.current().profile); - }, - onError: (error: Error) => { - notifier?.({ - type: postPublicProfileRoutine.FAILURE, - payload: { error }, - }); - }, + onSuccess: () => { + notifier?.({ + type: postPublicProfileRoutine.SUCCESS, + payload: { message: SUCCESS_MESSAGES.POST_PROFILE }, + }); + // refetch profile information + queryClient.invalidateQueries({ + queryKey: memberKeys.current().profile, + }); }, - ); + onError: (error: Error) => { + notifier?.({ + type: postPublicProfileRoutine.FAILURE, + payload: { error }, + }); + }, + }); }; const usePatchPublicProfile = () => { const queryClient = useQueryClient(); - return useMutation( - (payload: Partial) => + return useMutation({ + mutationFn: (payload: Partial) => Api.patchPublicProfile(payload, queryConfig), - { - onSuccess: () => { - notifier?.({ - type: patchPublicProfileRoutine.SUCCESS, - payload: { message: SUCCESS_MESSAGES.PATCH_PROFILE }, - }); - // refetch profile information - queryClient.invalidateQueries(memberKeys.current().profile); - }, - onError: (error: Error) => { - notifier?.({ - type: patchPublicProfileRoutine.FAILURE, - payload: { error }, - }); - }, + onSuccess: () => { + notifier?.({ + type: patchPublicProfileRoutine.SUCCESS, + payload: { message: SUCCESS_MESSAGES.PATCH_PROFILE }, + }); + // refetch profile information + queryClient.invalidateQueries({ + queryKey: memberKeys.current().profile, + }); + }, + onError: (error: Error) => { + notifier?.({ + type: patchPublicProfileRoutine.FAILURE, + payload: { error }, + }); }, - ); + }); }; return { diff --git a/src/member/subscription/mutations.ts b/src/member/subscription/mutations.ts index 4264acfb6..5e1cfe5ed 100644 --- a/src/member/subscription/mutations.ts +++ b/src/member/subscription/mutations.ts @@ -14,25 +14,26 @@ export default (queryConfig: QueryClientConfig) => { const useChangePlan = () => { const queryClient = useQueryClient(); - return useMutation( - (payload: { planId: string; cardId?: string }) => + return useMutation({ + mutationFn: (payload: { planId: string; cardId?: string }) => Api.changePlan(payload, queryConfig), - { - onSuccess: () => { - notifier?.({ type: changePlanRoutine.SUCCESS }); - }, - onError: (error: Error) => { - notifier?.({ type: changePlanRoutine.FAILURE, payload: { error } }); - }, - onSettled: () => { - queryClient.invalidateQueries(memberKeys.current().subscription); - }, + onSuccess: () => { + notifier?.({ type: changePlanRoutine.SUCCESS }); + }, + onError: (error: Error) => { + notifier?.({ type: changePlanRoutine.FAILURE, payload: { error } }); }, - ); + onSettled: () => { + queryClient.invalidateQueries({ + queryKey: memberKeys.current().subscription, + }); + }, + }); }; const useCreateSetupIntent = () => - useMutation(() => Api.createSetupIntent(queryConfig), { + useMutation({ + mutationFn: () => Api.createSetupIntent(queryConfig), onSuccess: () => { notifier?.({ type: createSetupIntentRoutine.SUCCESS }); }, @@ -46,23 +47,22 @@ export default (queryConfig: QueryClientConfig) => { const useSetDefaultCard = () => { const queryClient = useQueryClient(); - return useMutation( - (payload: { cardId: string }) => Api.setDefaultCard(payload, queryConfig), - { - onSuccess: () => { - notifier?.({ type: setDefaultCardRoutine.SUCCESS }); - }, - onError: (error: Error) => { - notifier?.({ - type: setDefaultCardRoutine.FAILURE, - payload: { error }, - }); - }, - onSettled: () => { - queryClient.invalidateQueries(CURRENT_CUSTOMER_KEY); - }, + return useMutation({ + mutationFn: (payload: { cardId: string }) => + Api.setDefaultCard(payload, queryConfig), + onSuccess: () => { + notifier?.({ type: setDefaultCardRoutine.SUCCESS }); }, - ); + onError: (error: Error) => { + notifier?.({ + type: setDefaultCardRoutine.FAILURE, + payload: { error }, + }); + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: CURRENT_CUSTOMER_KEY }); + }, + }); }; return { diff --git a/src/queryClient.ts b/src/queryClient.ts index 87cbe5438..89df00da5 100644 --- a/src/queryClient.ts +++ b/src/queryClient.ts @@ -1,7 +1,8 @@ import { configureWebsocketClient } from '@graasp/sdk'; import { - Hydrate, + HydrationBoundary, + QueryCache, QueryClient, QueryClientProvider, dehydrate, @@ -76,7 +77,7 @@ export default ( useMutation: typeof useMutation; ReactQueryDevtools: typeof ReactQueryDevtools; dehydrate: typeof dehydrate; - Hydrate: typeof Hydrate; + Hydrate: typeof HydrationBoundary; mutations: typeof mutations; axios: AxiosStatic; focusManager: typeof focusManager; @@ -116,7 +117,19 @@ export default ( }; // create queryclient - const queryClient = new QueryClient(); + const queryClient = new QueryClient({ + queryCache: new QueryCache({ + onError(error, query) { + const { typeError } = query.meta ?? {}; + if (typeError) { + queryConfig.notifier?.({ + type: typeError, + payload: { error }, + }); + } + }, + }), + }); // set up mutations given config // mutations are attached to queryClient @@ -139,7 +152,7 @@ export default ( useMutation, ReactQueryDevtools, dehydrate, - Hydrate, + Hydrate: HydrationBoundary, mutations, axios, focusManager, diff --git a/src/utils/useOnDataChanged.ts b/src/utils/useOnDataChanged.ts new file mode 100644 index 000000000..6b2336268 --- /dev/null +++ b/src/utils/useOnDataChanged.ts @@ -0,0 +1,39 @@ +import { UseInfiniteQueryResult, UseQueryResult } from '@tanstack/react-query'; +import { useEffect } from 'react'; + +type OnChangedCallBack = { onSuccess: (newData: T | undefined) => void }; + +// Use function overload to accept UseInfiniteQueryResult or UseQueryResult and return the correct type, depending on the input's type. +export function useOnDataChanged( + query: UseInfiniteQueryResult, + { onSuccess }: OnChangedCallBack, +): UseInfiniteQueryResult; + +export function useOnDataChanged( + query: UseQueryResult, + { onSuccess }: OnChangedCallBack, +): UseQueryResult; + +/** + * Tanstack Query 5 removed onSuccess, onError and onSettled in useQuery. + * Read https://tkdodo.eu/blog/breaking-react-querys-api-on-purpose for more information. + * This function allows to call a callback when the useQuery succeed. + * + * @param query The useQuery result to observe. + * @param onDataChanged The callback to call once the query is a success. + * @returns The UseQueryResult to faciliate the usage. + */ +export function useOnDataChanged( + query: UseQueryResult | UseInfiniteQueryResult, + { onSuccess }: OnChangedCallBack, +): UseQueryResult | UseInfiniteQueryResult { + const { data } = query; + + useEffect(() => { + if (data && !query.error) { + onSuccess(data); + } + }, [data]); + + return query; +} diff --git a/src/ws/hooks/item.ts b/src/ws/hooks/item.ts index 02da1a449..c4a3921e6 100644 --- a/src/ws/hooks/item.ts +++ b/src/ws/hooks/item.ts @@ -43,8 +43,10 @@ type ItemOpFeedbackEvent< const InvalidateItemOpFeedback = (queryClient: QueryClient) => ({ [FeedBackOperation.DELETE]: () => { // invalidate data displayed in the Trash screen - queryClient.invalidateQueries(memberKeys.current().recycled); - queryClient.invalidateQueries(memberKeys.current().recycledItems); + queryClient.invalidateQueries({ queryKey: memberKeys.current().recycled }); + queryClient.invalidateQueries({ + queryKey: memberKeys.current().recycledItems, + }); }, [FeedBackOperation.MOVE]: ( event: ItemOpFeedbackEvent, @@ -54,8 +56,8 @@ const InvalidateItemOpFeedback = (queryClient: QueryClient) => ({ const oldParentKey = getKeyForParentId(getParentFromPath(items[0].path)); const newParentKey = getKeyForParentId(getParentFromPath(moved[0].path)); // invalidate queries for the source and destination - queryClient.invalidateQueries(oldParentKey); - queryClient.invalidateQueries(newParentKey); + queryClient.invalidateQueries({ queryKey: oldParentKey }); + queryClient.invalidateQueries({ queryKey: newParentKey }); } }, [FeedBackOperation.COPY]: ( @@ -66,7 +68,7 @@ const InvalidateItemOpFeedback = (queryClient: QueryClient) => ({ const newParentKey = getKeyForParentId(getParentFromPath(copies[0].path)); // invalidate queries for the destination - queryClient.invalidateQueries(newParentKey); + queryClient.invalidateQueries({ queryKey: newParentKey }); } }, [FeedBackOperation.RECYCLE]: ( @@ -78,20 +80,26 @@ const InvalidateItemOpFeedback = (queryClient: QueryClient) => ({ getParentFromPath(Object.values(items)[0].path), ); // invalidate queries for the parent - queryClient.invalidateQueries(parentKey); + queryClient.invalidateQueries({ queryKey: parentKey }); } }, [FeedBackOperation.RESTORE]: () => { - queryClient.invalidateQueries(memberKeys.current().recycledItems); + queryClient.invalidateQueries({ + queryKey: memberKeys.current().recycledItems, + }); }, [FeedBackOperation.VALIDATE]: (itemIds: string[]) => { itemIds.forEach((itemId) => { // Invalidates the publication status to get the new status after the validation. - queryClient.invalidateQueries(itemKeys.single(itemId).publicationStatus); - queryClient.invalidateQueries(itemKeys.single(itemId).validation); - queryClient.invalidateQueries( - itemKeys.single(itemId).publishedInformation, - ); + queryClient.invalidateQueries({ + queryKey: itemKeys.single(itemId).publicationStatus, + }); + queryClient.invalidateQueries({ + queryKey: itemKeys.single(itemId).validation, + }); + queryClient.invalidateQueries({ + queryKey: itemKeys.single(itemId).publishedInformation, + }); }); }, }); diff --git a/tsconfig.json b/tsconfig.json index 38ad42e4f..d6ade62ab 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,5 +23,5 @@ "isolatedModules": true, "noEmit": false }, - "include": ["src"] + "include": ["src", "src/.d.ts"] }