feat(): Translation settings + user configuration + admin hook and js sdk + dashboard (#14355)
* feat(): Translation settings + user configuration * feat(): Translation settings + user configuration * Create gentle-bees-grow.md * add entities end point * add entities end point * add admin hook and js sdk method * update changeset * fix tests * fix tests * rm unnecessary copy * update dashboard to use the new resources * update dashboard to use the new resources * update dashboard to use the new resources * allow type inference through interface augmentation in the defineConfig of medusa-config * allow type inference through interface augmentation in the defineConfig of medusa-config * exclude id and _id props --------- Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
797878af26
commit
b21a599d11
@@ -2,25 +2,16 @@ import { FetchError } from "@medusajs/js-sdk"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import {
|
||||
QueryKey,
|
||||
useInfiniteQuery,
|
||||
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"
|
||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||
|
||||
const TRANSLATIONS_QUERY_KEY = "translations" as const
|
||||
export const translationsQueryKeys = queryKeysFactory(TRANSLATIONS_QUERY_KEY)
|
||||
@@ -35,177 +26,77 @@ export const translationStatisticsQueryKeys = queryKeysFactory(
|
||||
TRANSLATION_STATISTICS_QUERY_KEY
|
||||
)
|
||||
|
||||
const TRANSLATION_ENTITIES_QUERY_KEY = "translation_entities" as const
|
||||
export const translationEntitiesQueryKeys = queryKeysFactory(
|
||||
TRANSLATION_ENTITIES_QUERY_KEY
|
||||
)
|
||||
|
||||
const DEFAULT_PAGE_SIZE = 20
|
||||
|
||||
/**
|
||||
* Hook to fetch entities with their translatable fields and all translations.
|
||||
* Uses the /admin/translations/entities endpoint which returns entities
|
||||
* with all their translations for all locales.
|
||||
*
|
||||
* @param reference - The entity type (e.g., "product", "product_variant")
|
||||
* @param referenceId - Optional ID(s) to filter specific entities
|
||||
* @param options - React Query options
|
||||
*/
|
||||
export const useReferenceTranslations = (
|
||||
reference: string,
|
||||
translatableFields: string[],
|
||||
referenceId?: string | string[],
|
||||
options?: Omit<
|
||||
UseInfiniteQueryOptions<any, FetchError, any, any, QueryKey, number>,
|
||||
UseInfiniteQueryOptions<
|
||||
HttpTypes.AdminTranslationEntitiesResponse,
|
||||
FetchError,
|
||||
{
|
||||
pages: HttpTypes.AdminTranslationEntitiesResponse[]
|
||||
pageParams: number[]
|
||||
},
|
||||
HttpTypes.AdminTranslationEntitiesResponse,
|
||||
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 } = useInfiniteQuery({
|
||||
queryKey: translationEntitiesQueryKeys.list({
|
||||
type: reference,
|
||||
id: referenceId,
|
||||
}),
|
||||
queryFn: async ({ pageParam = 0 }) => {
|
||||
return sdk.admin.translation.entities({
|
||||
type: reference,
|
||||
id: referenceId,
|
||||
limit: DEFAULT_PAGE_SIZE,
|
||||
offset: pageParam,
|
||||
})
|
||||
},
|
||||
initialPageParam: 0,
|
||||
getNextPageParam: (lastPage) => {
|
||||
const nextOffset = lastPage.offset + lastPage.limit
|
||||
return nextOffset < lastPage.count ? nextOffset : undefined
|
||||
},
|
||||
...options,
|
||||
})
|
||||
|
||||
const { data, ...rest } = useInfiniteProducts(
|
||||
{ fields, id: referenceId ?? [] },
|
||||
options
|
||||
)
|
||||
const products = data?.pages.flatMap((page) => page.products) ?? []
|
||||
const entitiesWithTranslations =
|
||||
data?.pages.flatMap((page) => page.data) ?? []
|
||||
const translations = entitiesWithTranslations.flatMap(
|
||||
(entity) => entity.translations ?? []
|
||||
)
|
||||
const references = entitiesWithTranslations.map(
|
||||
({ translations: _, ...entity }) => entity
|
||||
)
|
||||
const count = data?.pages[0]?.count ?? 0
|
||||
|
||||
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}`)
|
||||
return {
|
||||
references,
|
||||
translations,
|
||||
count,
|
||||
...rest,
|
||||
}
|
||||
const { data, ...rest } = referenceHook()
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useTranslations = (
|
||||
@@ -229,15 +120,6 @@ export const useTranslations = (
|
||||
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<
|
||||
@@ -258,7 +140,7 @@ export const useBatchTranslations = (
|
||||
const invalidateQueries = async () => {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: referenceInvalidationKeysMap.get(reference),
|
||||
queryKey: translationEntitiesQueryKeys.list({ type: reference }),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: translationStatisticsQueryKeys.lists(),
|
||||
@@ -293,6 +175,27 @@ export const useTranslationSettings = (
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useTranslationEntities = (
|
||||
query: HttpTypes.AdminTranslationEntitiesParams,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
HttpTypes.AdminTranslationEntitiesResponse,
|
||||
FetchError,
|
||||
HttpTypes.AdminTranslationEntitiesResponse,
|
||||
QueryKey
|
||||
>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryKey: translationEntitiesQueryKeys.list(query),
|
||||
queryFn: () => sdk.admin.translation.entities(query),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useTranslationStatistics = (
|
||||
query?: HttpTypes.AdminTranslationStatisticsParams,
|
||||
options?: Omit<
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { keepPreviousData } from "@tanstack/react-query"
|
||||
import { useEffect } from "react"
|
||||
import { useNavigate, useSearchParams } from "react-router-dom"
|
||||
import { RouteFocusModal } from "../../../components/modals"
|
||||
import {
|
||||
useReferenceTranslations,
|
||||
useStore,
|
||||
useTranslationSettings,
|
||||
} from "../../../hooks/api"
|
||||
import { TranslationsEditForm } from "./components/translations-edit-form"
|
||||
import { useEffect } from "react"
|
||||
import { RouteFocusModal } from "../../../components/modals"
|
||||
import { useFeatureFlag } from "../../../providers/feature-flag-provider"
|
||||
import { keepPreviousData } from "@tanstack/react-query"
|
||||
import { TranslationsEditForm } from "./components/translations-edit-form"
|
||||
|
||||
export const TranslationsEdit = () => {
|
||||
const isTranslationsEnabled = useFeatureFlag("translation")
|
||||
@@ -41,15 +41,10 @@ export const TranslationsEdit = () => {
|
||||
isPending,
|
||||
isError,
|
||||
error,
|
||||
} = useReferenceTranslations(
|
||||
reference!,
|
||||
translatable_fields?.[reference!] ?? [],
|
||||
referenceIdParam,
|
||||
{
|
||||
enabled: !!translatable_fields && !!reference,
|
||||
placeholderData: keepPreviousData,
|
||||
}
|
||||
)
|
||||
} = useReferenceTranslations(reference!, referenceIdParam, {
|
||||
enabled: !!reference,
|
||||
placeholderData: keepPreviousData,
|
||||
})
|
||||
const {
|
||||
store,
|
||||
isPending: isStorePending,
|
||||
|
||||
Reference in New Issue
Block a user