feat: Translations UI (#14217)

* Add Translations route and guard it with feature flag. Empty TranslationsList main component to test route.

* Translation list component

* Add translations namespace to js-sdk

* Translations hook

* Avoid incorrectly throwing when updating and locale not included

* Translations bulk editor component v1

* Add batch method to translations namespace in js-sdk

* Protect translations edit route with feature flag

* Handle reference_id search param

* Replace entity_type entity_id for reference reference_id

* Manage translations from product detail page

* Dynamically resolve base hook for retrieving translations

* Fix navigation from outside settings/translations

* Navigation to bulk editor from product list

* Add Translations to various product module types

* Type useVariants hook

* Handle product module entities translations in bulk editor

* Fix categories issue in datagrid due to column clash

* Translations bulk navigation from remaining entities detail pages

* Add remaining bulk editor navigation for list components. Fix invalidation query for variants

* Expandable text cell v1

* Popover approach

* Add *supported_locales.locale to default fields in stores list endpoint

* Make popover more aligned to excell approach

* Correctly tie the focused cell anchor to popover

* Rework translations main component UI

* Fix link def export

* Swap axis for translations datagrid

* Add original column to translations data grid

* Remove is_default store locale from backend

* Remove ldefault locale from ui

* Type

* Add changeset

* Comments

* Remove unused import

* Add translations to admin product categories endpoint allowed fields

* Default locale removal

* Lazy loading with infinite scroll data grid

* Infinite list hook and implementation for products and variants

* Translation bulk editor lazy loaded datagrid

* Prevent scroll when forcing focus, to avoid scrollTop reset on infinite loading

* Confgiure placeholder data

* Cleanup logs and refactor

* Infinite query hooks for translatable entities

* Batch requests for translation batch endpoint

* Clean up

* Update icon

* Add query param validator in settings endpoint

* Settings endpoint param type

* JS sdk methods for translation settings and statistics

* Retrieve translatable fields and entities dynamically. Remove hardcoded information from tranlations list

* Resolve translation aggregate completion dynamically

* Format label

* Resolve bulk editor header label dynamically

* Include type and collection in translations config

* Avoid showing product option and option values in translatable entities list

* Translations

* Make translations bulk editor content columns wider

* Disable hiding Original column in translations bulk editor

* Adjust translations completion styles

* Fix translations config screen

* Locale selector switcher with conditional locale column rendering

* Batch one locale at a time

* Hooks save actions to footer buttons

* Reset dirty state on save

* Dynamic row heights for translations bulk editor. Replace expandable cell for text cell, with additional isMultiLine config

* Make columns take as much responsive width as possible and divide equally

* more padding to avoid unnecessary horizontal scrollbar

* Update statistics graphs

* Translations

* Statistics graphs translations

* Translation, text sizes and weight in stat graphs

* Conditionally show/hide column visibility dropdown in datagrid

* Allow to pass component to place in DataGrid header and use it in translations bulk editor

* Center text regardless of multiLine config

* Apply full height to datagrid cell regardles of multiSelect config

* Colors and fonts

* Handle key down for text area in text cell

* MultilineCell with special keydown handling

* Rework form schema to match new single locale edit flow

* Update created translations to include id, to avoid duplication issue on subsequent calls

* Handle space key for text cells

* Finish hooking up multiline cell with key and mouse events

* Disable remaining buttons when batch is ongoing

* Style updates

* Update style

* Refactor to make form updates and sync/comparison with server data more comprehensive and robust

* Update styles

* Bars and labels alignment

* Add languages tooltip

* Styles and translation

* Navigation update

* Disable edit translations button when no reference count

* Invert colors

---------

Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
Co-authored-by: Adrien de Peretti <adrien.deperetti@gmail.com>
This commit is contained in:
Nicolas Gorga
2025-12-17 09:36:50 -03:00
committed by GitHub
parent c1a5390fc6
commit 3d1330ebb9
69 changed files with 3595 additions and 112 deletions
@@ -1,7 +1,9 @@
import { FetchError } from "@medusajs/js-sdk"
import { HttpTypes } from "@medusajs/types"
import {
InfiniteData,
QueryKey,
UseInfiniteQueryOptions,
UseMutationOptions,
UseQueryOptions,
useMutation,
@@ -11,6 +13,7 @@ import { sdk } from "../../lib/client"
import { queryClient } from "../../lib/query-client"
import { queryKeysFactory } from "../../lib/query-key-factory"
import { productsQueryKeys } from "./products"
import { useInfiniteList } from "../use-infinite-list"
const CATEGORIES_QUERY_KEY = "categories" as const
export const categoriesQueryKeys = queryKeysFactory(CATEGORIES_QUERY_KEY)
@@ -58,6 +61,35 @@ export const useProductCategories = (
return { ...data, ...rest }
}
export const useInfiniteCategories = (
query?: Omit<HttpTypes.AdminProductCategoryListParams, "offset" | "limit"> & {
limit?: number
},
options?: Omit<
UseInfiniteQueryOptions<
HttpTypes.AdminProductCategoryListResponse,
FetchError,
InfiniteData<HttpTypes.AdminProductCategoryListResponse, number>,
HttpTypes.AdminProductCategoryListResponse,
QueryKey,
number
>,
"queryFn" | "queryKey" | "initialPageParam" | "getNextPageParam"
>
) => {
return useInfiniteList<
HttpTypes.AdminProductCategoryListResponse,
HttpTypes.AdminProductCategoryListParams,
FetchError,
QueryKey
>({
queryKey: (params) => categoriesQueryKeys.list(params),
queryFn: (params) => sdk.admin.productCategory.list(params),
query,
options,
})
}
export const useCreateProductCategory = (
options?: UseMutationOptions<
HttpTypes.AdminProductCategoryResponse,
@@ -1,7 +1,9 @@
import { FetchError } from "@medusajs/js-sdk"
import { FindParams, HttpTypes, PaginatedResponse } from "@medusajs/types"
import {
InfiniteData,
QueryKey,
UseInfiniteQueryOptions,
UseMutationOptions,
UseQueryOptions,
useMutation,
@@ -11,6 +13,7 @@ import { sdk } from "../../lib/client"
import { queryClient } from "../../lib/query-client"
import { queryKeysFactory } from "../../lib/query-key-factory"
import { productsQueryKeys } from "./products"
import { useInfiniteList } from "../use-infinite-list"
const COLLECTION_QUERY_KEY = "collections" as const
export const collectionsQueryKeys = queryKeysFactory(COLLECTION_QUERY_KEY)
@@ -57,6 +60,34 @@ export const useCollections = (
return { ...data, ...rest }
}
export const useInfiniteCollections = (
query?: Omit<HttpTypes.AdminCollectionListParams, "offset" | "limit"> & {
limit?: number
},
options?: Omit<
UseInfiniteQueryOptions<
HttpTypes.AdminCollectionListResponse,
FetchError,
InfiniteData<HttpTypes.AdminCollectionListResponse, number>,
HttpTypes.AdminCollectionListResponse,
QueryKey,
number
>,
"queryFn" | "queryKey" | "initialPageParam" | "getNextPageParam"
>
) => {
return useInfiniteList<
HttpTypes.AdminCollectionListResponse,
HttpTypes.AdminCollectionListParams,
FetchError,
QueryKey
>({
queryKey: (params) => collectionsQueryKeys.list(params),
queryFn: (params) => sdk.admin.productCollection.list(params),
query,
options,
})
}
export const useUpdateCollection = (
id: string,
options?: UseMutationOptions<
@@ -34,6 +34,7 @@ export * from "./store"
export * from "./tags"
export * from "./tax-rates"
export * from "./tax-regions"
export * from "./translations"
export * from "./users"
export * from "./views"
export * from "./workflow-executions"
@@ -1,7 +1,9 @@
import { FetchError } from "@medusajs/js-sdk"
import { HttpTypes } from "@medusajs/types"
import {
InfiniteData,
QueryKey,
UseInfiniteQueryOptions,
UseMutationOptions,
UseQueryOptions,
useMutation,
@@ -10,6 +12,7 @@ import {
import { sdk } from "../../lib/client"
import { queryClient } from "../../lib/query-client"
import { queryKeysFactory } from "../../lib/query-key-factory"
import { useInfiniteList } from "../use-infinite-list"
const PRODUCT_TYPES_QUERY_KEY = "product_types" as const
export const productTypesQueryKeys = queryKeysFactory(PRODUCT_TYPES_QUERY_KEY)
@@ -57,6 +60,35 @@ export const useProductTypes = (
return { ...data, ...rest }
}
export const useInfiniteProductTypes = (
query?: Omit<HttpTypes.AdminProductTypeListParams, "offset" | "limit"> & {
limit?: number
},
options?: Omit<
UseInfiniteQueryOptions<
HttpTypes.AdminProductTypeListResponse,
FetchError,
InfiniteData<HttpTypes.AdminProductTypeListResponse, number>,
HttpTypes.AdminProductTypeListResponse,
QueryKey,
number
>,
"queryFn" | "queryKey" | "initialPageParam" | "getNextPageParam"
>
) => {
return useInfiniteList<
HttpTypes.AdminProductTypeListResponse,
HttpTypes.AdminProductTypeListParams,
FetchError,
QueryKey
>({
queryKey: (params) => productTypesQueryKeys.list(params),
queryFn: (params) => sdk.admin.productType.list(params),
query,
options,
})
}
export const useCreateProductType = (
options?: UseMutationOptions<
HttpTypes.AdminProductTypeResponse,
@@ -1,7 +1,15 @@
import { QueryKey, useQuery, UseQueryOptions } from "@tanstack/react-query"
import {
QueryKey,
UseInfiniteQueryOptions,
useQuery,
UseQueryOptions,
} from "@tanstack/react-query"
import { InfiniteData } from "@tanstack/query-core"
import { sdk } from "../../lib/client"
import { queryKeysFactory } from "../../lib/query-key-factory"
import { FetchError } from "@medusajs/js-sdk"
import { HttpTypes } from "@medusajs/types"
import { useInfiniteList } from "../use-infinite-list"
const PRODUCT_VARIANT_QUERY_KEY = "product_variant" as const
export const productVariantQueryKeys = queryKeysFactory(
@@ -9,9 +17,14 @@ export const productVariantQueryKeys = queryKeysFactory(
)
export const useVariants = (
query?: Record<string, any>,
query?: HttpTypes.AdminProductVariantParams,
options?: Omit<
UseQueryOptions<any, FetchError, any, QueryKey>,
UseQueryOptions<
HttpTypes.AdminProductVariantListResponse,
FetchError,
HttpTypes.AdminProductVariantListResponse,
QueryKey
>,
"queryFn" | "queryKey"
>
) => {
@@ -23,3 +36,34 @@ export const useVariants = (
return { ...data, ...rest }
}
export const useInfiniteVariants = (
query?: Omit<HttpTypes.AdminProductVariantParams, "offset" | "limit"> & {
limit?: number
},
options?: Omit<
UseInfiniteQueryOptions<
HttpTypes.AdminProductVariantListResponse,
FetchError,
InfiniteData<HttpTypes.AdminProductVariantListResponse, number>,
HttpTypes.AdminProductVariantListResponse,
QueryKey,
number
>,
"queryFn" | "queryKey" | "initialPageParam" | "getNextPageParam"
>
) => {
return useInfiniteList<
HttpTypes.AdminProductVariantListResponse,
HttpTypes.AdminProductVariantParams,
FetchError,
QueryKey
>({
queryKey: (params) => productVariantQueryKeys.list(params),
queryFn: async (params) => {
return await sdk.admin.productVariant.list(params)
},
query,
options,
})
}
@@ -2,15 +2,18 @@ import { FetchError } from "@medusajs/js-sdk"
import { HttpTypes } from "@medusajs/types"
import {
QueryKey,
UseInfiniteQueryOptions,
useMutation,
UseMutationOptions,
useQuery,
UseQueryOptions,
} from "@tanstack/react-query"
import { InfiniteData } from "@tanstack/query-core"
import { sdk } from "../../lib/client"
import { queryClient } from "../../lib/query-client"
import { queryKeysFactory } from "../../lib/query-key-factory"
import { inventoryItemsQueryKeys } from "./inventory.tsx"
import { useInfiniteList } from "../use-infinite-list.tsx"
const PRODUCTS_QUERY_KEY = "products" as const
export const productsQueryKeys = queryKeysFactory(PRODUCTS_QUERY_KEY)
@@ -310,6 +313,32 @@ export const useProducts = (
return { ...data, ...rest }
}
export const useInfiniteProducts = (
query?: HttpTypes.AdminProductListParams,
options?: Omit<
UseInfiniteQueryOptions<
HttpTypes.AdminProductListResponse,
FetchError,
InfiniteData<HttpTypes.AdminProductListResponse, number>,
HttpTypes.AdminProductListResponse,
QueryKey,
number
>,
"queryFn" | "queryKey" | "initialPageParam" | "getNextPageParam"
>
) => {
return useInfiniteList<
HttpTypes.AdminProductListResponse,
HttpTypes.AdminProductListParams,
FetchError,
QueryKey
>({
queryKey: (params) => productsQueryKeys.list(params),
queryFn: (params) => sdk.admin.product.list(params),
query,
options,
})
}
export const useCreateProduct = (
options?: UseMutationOptions<
HttpTypes.AdminProductResponse,
@@ -1,7 +1,9 @@
import { FetchError } from "@medusajs/js-sdk"
import { HttpTypes } from "@medusajs/types"
import {
InfiniteData,
QueryKey,
UseInfiniteQueryOptions,
UseMutationOptions,
UseQueryOptions,
useMutation,
@@ -10,6 +12,7 @@ import {
import { sdk } from "../../lib/client"
import { queryClient } from "../../lib/query-client"
import { queryKeysFactory } from "../../lib/query-key-factory"
import { useInfiniteList } from "../use-infinite-list"
const TAGS_QUERY_KEY = "tags" as const
export const productTagsQueryKeys = queryKeysFactory(TAGS_QUERY_KEY)
@@ -57,6 +60,35 @@ export const useProductTags = (
return { ...data, ...rest }
}
export const useInfiniteProductTags = (
query?: Omit<HttpTypes.AdminProductTagListParams, "offset" | "limit"> & {
limit?: number
},
options?: Omit<
UseInfiniteQueryOptions<
HttpTypes.AdminProductTagListResponse,
FetchError,
InfiniteData<HttpTypes.AdminProductTagListResponse, number>,
HttpTypes.AdminProductTagListResponse,
QueryKey,
number
>,
"queryFn" | "queryKey" | "initialPageParam" | "getNextPageParam"
>
) => {
return useInfiniteList<
HttpTypes.AdminProductTagListResponse,
HttpTypes.AdminProductTagListParams,
FetchError,
QueryKey
>({
queryKey: (params) => productTagsQueryKeys.list(params),
queryFn: (params) => sdk.admin.productTag.list(params),
query,
options,
})
}
export const useCreateProductTag = (
query?: HttpTypes.AdminProductTagParams,
options?: UseMutationOptions<
@@ -0,0 +1,315 @@
import { FetchError } from "@medusajs/js-sdk"
import { HttpTypes } from "@medusajs/types"
import {
QueryKey,
UseInfiniteQueryOptions,
UseInfiniteQueryResult,
useMutation,
UseMutationOptions,
useQuery,
UseQueryOptions,
} from "@tanstack/react-query"
import { sdk } from "../../lib/client"
import { queryKeysFactory } from "../../lib/query-key-factory"
import { queryClient } from "../../lib/query-client"
import { productsQueryKeys, useInfiniteProducts } from "./products"
import {
productVariantQueryKeys,
useInfiniteVariants,
} from "./product-variants"
import { categoriesQueryKeys, useInfiniteCategories } from "./categories"
import { collectionsQueryKeys, useInfiniteCollections } from "./collections"
import { productTagsQueryKeys, useInfiniteProductTags } from "./tags"
import { productTypesQueryKeys, useInfiniteProductTypes } from "./product-types"
const TRANSLATIONS_QUERY_KEY = "translations" as const
export const translationsQueryKeys = queryKeysFactory(TRANSLATIONS_QUERY_KEY)
const TRANSLATION_SETTINGS_QUERY_KEY = "translation_settings" as const
export const translationSettingsQueryKeys = queryKeysFactory(
TRANSLATION_SETTINGS_QUERY_KEY
)
const TRANSLATION_STATISTICS_QUERY_KEY = "translation_statistics" as const
export const translationStatisticsQueryKeys = queryKeysFactory(
TRANSLATION_STATISTICS_QUERY_KEY
)
export const useReferenceTranslations = (
reference: string,
translatableFields: string[],
referenceId?: string | string[],
options?: Omit<
UseInfiniteQueryOptions<any, FetchError, any, any, QueryKey, number>,
"queryFn" | "queryKey" | "initialPageParam" | "getNextPageParam"
>
) => {
const referenceHookMap = new Map<
string,
() => Omit<UseInfiniteQueryResult<any, FetchError>, "data"> & {
data: {
translations: HttpTypes.AdminTranslation[]
references: (Record<string, any> & { id: string })[]
count: number
}
}
>([
[
"product",
() => {
const fields = translatableFields.concat(["translations.*"]).join(",")
const { data, ...rest } = useInfiniteProducts(
{ fields, id: referenceId ?? [] },
options
)
const products = data?.pages.flatMap((page) => page.products) ?? []
return {
...rest,
data: {
translations:
products?.flatMap((product) => product.translations ?? []) ?? [],
references: products ?? [],
count: data?.pages[0]?.count ?? 0,
},
}
},
],
[
"product_variant",
() => {
const fields = translatableFields.concat(["translations.*"]).join(",")
const { data, ...rest } = useInfiniteVariants(
{ id: referenceId ?? [], fields },
options
)
const variants = data?.pages.flatMap((page) => page.variants) ?? []
return {
...rest,
data: {
translations:
variants?.flatMap((variant) => variant.translations ?? []) ?? [],
references: variants ?? [],
translatableFields,
count: data?.pages[0]?.count ?? 0,
},
}
},
],
[
"product_category",
() => {
const fields = translatableFields.concat(["translations.*"]).join(",")
const { data, ...rest } = useInfiniteCategories(
{ id: referenceId ?? [], fields },
options
)
const categories =
data?.pages.flatMap((page) => page.product_categories) ?? []
return {
...rest,
data: {
translations:
categories?.flatMap((category) => category.translations ?? []) ??
[],
references: categories ?? [],
translatableFields,
count: data?.pages[0]?.count ?? 0,
},
}
},
],
[
"product_collection",
() => {
const fields = translatableFields.concat(["translations.*"]).join(",")
const { data, ...rest } = useInfiniteCollections(
{ id: referenceId ?? [], fields },
options
)
const collections =
data?.pages.flatMap((page) => page.collections) ?? []
return {
...rest,
data: {
translations:
collections?.flatMap(
(collection) => collection.translations ?? []
) ?? [],
references: collections ?? [],
translatableFields,
count: data?.pages[0]?.count ?? 0,
},
}
},
],
[
"product_type",
() => {
const fields = translatableFields.concat(["translations.*"]).join(",")
const { data, ...rest } = useInfiniteProductTypes(
{ id: referenceId ?? [], fields },
options
)
const product_types =
data?.pages.flatMap((page) => page.product_types) ?? []
return {
...rest,
data: {
translations:
product_types?.flatMap((type) => type.translations ?? []) ?? [],
references: product_types ?? [],
count: data?.pages[0]?.count ?? 0,
translatableFields,
},
}
},
],
[
"product_tag",
() => {
const fields = translatableFields.concat(["translations.*"]).join(",")
const { data, ...rest } = useInfiniteProductTags(
{ id: referenceId ?? [], fields },
options
)
const product_tags =
data?.pages.flatMap((page) => page.product_tags) ?? []
return {
...rest,
data: {
translations:
product_tags?.flatMap((tag) => tag.translations ?? []) ?? [],
references: product_tags ?? [],
translatableFields,
count: data?.pages[0]?.count ?? 0,
},
}
},
],
// TODO: product option and option values
])
const referenceHook = referenceHookMap.get(reference)
if (!referenceHook) {
throw new Error(`No hook found for reference type: ${reference}`)
}
const { data, ...rest } = referenceHook()
return { ...data, ...rest }
}
export const useTranslations = (
query?: HttpTypes.AdminTranslationsListParams,
options?: Omit<
UseQueryOptions<
HttpTypes.AdminTranslationsListResponse,
FetchError,
HttpTypes.AdminTranslationsListResponse,
QueryKey
>,
"queryFn" | "queryKey"
>
) => {
const { data, ...rest } = useQuery({
queryKey: translationsQueryKeys.list(query),
queryFn: () => sdk.admin.translation.list(query),
...options,
})
return { ...data, ...rest }
}
const referenceInvalidationKeysMap = new Map<string, QueryKey>([
["product", productsQueryKeys.lists()],
["product_variant", productVariantQueryKeys.lists()],
["product_category", categoriesQueryKeys.lists()],
["product_collection", collectionsQueryKeys.lists()],
["product_type", productTypesQueryKeys.lists()],
["product_tag", productTagsQueryKeys.lists()],
])
export const useBatchTranslations = (
reference: string,
options?: UseMutationOptions<
HttpTypes.AdminTranslationsBatchResponse,
FetchError,
HttpTypes.AdminBatchTranslations
>
) => {
const mutation = useMutation({
mutationFn: (payload: HttpTypes.AdminBatchTranslations) =>
sdk.admin.translation.batch(payload),
...options,
})
/**
* Useful to call the invalidation separately from the batch request and await the refetch finishes.
*/
const invalidateQueries = async () => {
await Promise.all([
queryClient.invalidateQueries({
queryKey: referenceInvalidationKeysMap.get(reference),
}),
queryClient.invalidateQueries({
queryKey: translationStatisticsQueryKeys.lists(),
}),
])
}
return {
...mutation,
invalidateQueries,
}
}
export const useTranslationSettings = (
query?: HttpTypes.AdminTranslationSettingsParams,
options?: Omit<
UseQueryOptions<
HttpTypes.AdminTranslationSettingsResponse,
FetchError,
HttpTypes.AdminTranslationSettingsResponse,
QueryKey
>,
"queryFn" | "queryKey"
>
) => {
const { data, ...rest } = useQuery({
queryKey: translationSettingsQueryKeys.list(query),
queryFn: () => sdk.admin.translation.settings(query),
...options,
})
return { ...data, ...rest }
}
export const useTranslationStatistics = (
query?: HttpTypes.AdminTranslationStatisticsParams,
options?: Omit<
UseQueryOptions<
HttpTypes.AdminTranslationStatisticsResponse,
FetchError,
HttpTypes.AdminTranslationStatisticsResponse,
QueryKey
>,
"queryFn" | "queryKey"
>
) => {
const { data, ...rest } = useQuery({
queryKey: translationStatisticsQueryKeys.list(query),
queryFn: () => sdk.admin.translation.statistics(query),
...options,
})
return { ...data, ...rest }
}
@@ -0,0 +1,92 @@
import { FetchError } from "@medusajs/js-sdk"
import { PaginatedResponse } from "@medusajs/types"
import {
QueryKey,
UseInfiniteQueryOptions,
InfiniteData,
useInfiniteQuery,
} from "@tanstack/react-query"
/**
* Generic hook for infinite queries with pagination support.
*
* @template TResponse - The response type that must include count, offset, and limit
* @template TParams - The query parameters type (offset and limit will be handled internally)
* @template TError - The error type (defaults to FetchError)
* @template TQueryKey - The query key type (defaults to QueryKey)
*
* @param config - Configuration object
* @param config.queryKey - Function or array that generates the query key
* @param config.queryFn - Function that fetches data with offset and limit
* @param config.query - Query parameters (offset is ignored, limit is optional)
* @param config.options - Additional options for useInfiniteQuery
*
* @example
* ```ts
* const { data, fetchNextPage, hasNextPage } = useInfiniteList({
* queryKey: (params) => productVariantQueryKeys.list(params),
* queryFn: async (params) => sdk.admin.productVariant.list(params),
* query: { status: "published" },
* })
* ```
*/
export const useInfiniteList = <
TResponse extends PaginatedResponse<unknown>,
TParams extends { offset?: number; limit?: number } = {
offset?: number
limit?: number
},
TError = FetchError,
TQueryKey extends QueryKey = QueryKey
>({
queryKey,
queryFn,
query,
options,
}: {
queryKey: ((params: Omit<TParams, "limit">) => TQueryKey) | TQueryKey
queryFn: (params: TParams) => Promise<TResponse>
query?: TParams
options?: Omit<
UseInfiniteQueryOptions<
TResponse,
TError,
InfiniteData<TResponse, number>,
TResponse,
TQueryKey,
number
>,
"queryKey" | "queryFn" | "initialPageParam" | "getNextPageParam"
>
}) => {
const { limit = 50, offset: _, ..._query } = query ?? {}
const resolvedQueryKey =
typeof queryKey === "function"
? queryKey(_query as Omit<TParams, "limit">)
: queryKey
const infiniteQueryKey =
resolvedQueryKey[resolvedQueryKey.length - 1] === "__infinite"
? resolvedQueryKey
: ([...resolvedQueryKey, "__infinite"] as unknown as TQueryKey)
return useInfiniteQuery<
TResponse,
TError,
InfiniteData<TResponse, number>,
TQueryKey,
number
>({
// Infinite queries must not share the exact same queryKey as non-infinite queries,
// since the cached data shape differs (InfiniteData<T> vs T).
queryKey: infiniteQueryKey,
queryFn: ({ pageParam = 0 }) => {
return queryFn({ ..._query, limit, offset: pageParam } as TParams)
},
initialPageParam: 0,
getNextPageParam: (lastPage) => {
const moreItemsExist = lastPage.count > lastPage.offset + lastPage.limit
return moreItemsExist ? lastPage.offset + lastPage.limit : undefined
},
...options,
})
}