feat(): Introduce translation module and preliminary application of them (#14189)
* feat(): Translation first steps * feat(): locale middleware * feat(): readonly links * feat(): feature flag * feat(): modules sdk * feat(): translation module re export * start adding workflows * update typings * update typings * test(): Add integration tests * test(): centralize filters preparation * test(): centralize filters preparation * remove unnecessary importy * fix workflows * Define StoreLocale inside Store Module * Link definition to extend Store with supported_locales * store_locale migration * Add supported_locales handling in Store Module * Tests * Accept supported_locales in Store endpoints * Add locales to js-sdk * Include locale list and default locale in Store Detail section * Initialize local namespace in js-sdk * Add locales route * Make code primary key of locale table to facilitate upserts * Add locales routes * Show locale code as is * Add list translations api route * Batch endpoint * Types * New batchTranslationsWorkflow and various updates to existent ones * Edit default locale UI * WIP * Apply translation agnostically * middleware * Apply translation agnostically * fix Apply translation agnostically * apply translations to product list * Add feature flag * fetch translations by batches of 250 max * fix apply * improve and test util * apply to product list * dont manage translations if no locale * normalize locale * potential todo * Protect translations routes with feature flag * Extract normalize locale util to core/utils * Normalize locale on write * Normalize locale for read * Use feature flag to guard translations UI across the board * Avoid throwing incorrectly when locale_code not present in partial updates * move applyTranslations util * remove old tests * fix util tests * fix(): product end points * cleanup * update lock * remove unused var * cleanup * fix apply locale * missing new dep for test utils * Change entity_type, entity_id to reference, reference_id * Remove comment * Avoid registering translations route if ff not enabled * Prevent registering express handler for disabled route via defineFileConfig * Add tests * Add changeset * Update test * fix integration tests, module and internals * Add locale id plus fixed * Allow to pass array of reference_id * fix unit tests * fix link loading * fix store route * fix sales channel test * fix tests --------- Co-authored-by: Nicolas Gorga <nicogorga11@gmail.com> Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
fea3d4ec49
commit
6dc0b8bed8
@@ -1009,6 +1009,10 @@ export function getRouteMap({
|
||||
path: "currencies",
|
||||
lazy: () => import("../../routes/store/store-add-currencies"),
|
||||
},
|
||||
{
|
||||
path: "locales",
|
||||
lazy: () => import("../../routes/store/store-add-locales"),
|
||||
},
|
||||
{
|
||||
path: "metadata/edit",
|
||||
lazy: () => import("../../routes/store/store-metadata"),
|
||||
|
||||
@@ -3,6 +3,7 @@ import { sdk } from "../../lib/client"
|
||||
|
||||
export type FeatureFlags = {
|
||||
view_configurations?: boolean
|
||||
translation?: boolean
|
||||
[key: string]: boolean | undefined
|
||||
}
|
||||
|
||||
@@ -10,13 +11,16 @@ export const useFeatureFlags = () => {
|
||||
return useQuery<FeatureFlags>({
|
||||
queryKey: ["admin", "feature-flags"],
|
||||
queryFn: async () => {
|
||||
const response = await sdk.client.fetch<{ feature_flags: FeatureFlags }>("/admin/feature-flags", {
|
||||
method: "GET",
|
||||
})
|
||||
|
||||
const response = await sdk.client.fetch<{ feature_flags: FeatureFlags }>(
|
||||
"/admin/feature-flags",
|
||||
{
|
||||
method: "GET",
|
||||
}
|
||||
)
|
||||
|
||||
return response.feature_flags
|
||||
},
|
||||
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
|
||||
cacheTime: 10 * 60 * 1000, // Keep in cache for 10 minutes
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ export * from "./fulfillment-providers"
|
||||
export * from "./fulfillment-sets"
|
||||
export * from "./inventory"
|
||||
export * from "./invites"
|
||||
export * from "./locales"
|
||||
export * from "./notification"
|
||||
export * from "./orders"
|
||||
export * from "./payment-collections"
|
||||
|
||||
52
packages/admin/dashboard/src/hooks/api/locales.tsx
Normal file
52
packages/admin/dashboard/src/hooks/api/locales.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { FetchError } from "@medusajs/js-sdk"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { QueryKey, UseQueryOptions, useQuery } from "@tanstack/react-query"
|
||||
|
||||
import { sdk } from "../../lib/client"
|
||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||
|
||||
const LOCALES_QUERY_KEY = "locales" as const
|
||||
const localesQueryKeys = queryKeysFactory(LOCALES_QUERY_KEY)
|
||||
|
||||
export const useLocales = (
|
||||
query?: HttpTypes.AdminLocaleListParams,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
HttpTypes.AdminLocaleListResponse,
|
||||
FetchError,
|
||||
HttpTypes.AdminLocaleListResponse,
|
||||
QueryKey
|
||||
>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: () => sdk.admin.locale.list(query),
|
||||
queryKey: localesQueryKeys.list(query),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useLocale = (
|
||||
id: string,
|
||||
query?: HttpTypes.AdminLocaleParams,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
HttpTypes.AdminLocaleResponse,
|
||||
FetchError,
|
||||
HttpTypes.AdminLocaleResponse,
|
||||
QueryKey
|
||||
>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryKey: localesQueryKeys.detail(id),
|
||||
queryFn: async () => sdk.admin.locale.retrieve(id, query),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
@@ -9303,6 +9303,9 @@
|
||||
"defaultCurrency": {
|
||||
"type": "string"
|
||||
},
|
||||
"defaultLocale": {
|
||||
"type": "string"
|
||||
},
|
||||
"defaultRegion": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -9321,6 +9324,9 @@
|
||||
"inviteLinkTemplate": {
|
||||
"type": "string"
|
||||
},
|
||||
"locales": {
|
||||
"type": "string"
|
||||
},
|
||||
"currencies": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -9339,6 +9345,15 @@
|
||||
"removeCurrencyWarning_other": {
|
||||
"type": "string"
|
||||
},
|
||||
"removeLocaleWarning_one": {
|
||||
"type": "string"
|
||||
},
|
||||
"removeLocaleWarning_other": {
|
||||
"type": "string"
|
||||
},
|
||||
"localeAlreadyAdded": {
|
||||
"type": "string"
|
||||
},
|
||||
"currencyAlreadyAdded": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -9358,20 +9373,28 @@
|
||||
"update": {
|
||||
"type": "string"
|
||||
},
|
||||
"localesUpdated": {
|
||||
"type": "string"
|
||||
},
|
||||
"currenciesUpdated": {
|
||||
"type": "string"
|
||||
},
|
||||
"currenciesRemoved": {
|
||||
"type": "string"
|
||||
},
|
||||
"localesRemoved": {
|
||||
"type": "string"
|
||||
},
|
||||
"updatedTaxInclusivitySuccessfully": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"update",
|
||||
"localesUpdated",
|
||||
"currenciesUpdated",
|
||||
"currenciesRemoved",
|
||||
"localesRemoved",
|
||||
"updatedTaxInclusivitySuccessfully"
|
||||
],
|
||||
"additionalProperties": false
|
||||
@@ -9382,19 +9405,24 @@
|
||||
"manageYourStoresDetails",
|
||||
"editStore",
|
||||
"defaultCurrency",
|
||||
"defaultLocale",
|
||||
"defaultRegion",
|
||||
"defaultSalesChannel",
|
||||
"defaultLocation",
|
||||
"swapLinkTemplate",
|
||||
"paymentLinkTemplate",
|
||||
"inviteLinkTemplate",
|
||||
"locales",
|
||||
"currencies",
|
||||
"addCurrencies",
|
||||
"enableTaxInclusivePricing",
|
||||
"disableTaxInclusivePricing",
|
||||
"removeCurrencyWarning_one",
|
||||
"removeCurrencyWarning_other",
|
||||
"removeLocaleWarning_one",
|
||||
"removeLocaleWarning_other",
|
||||
"currencyAlreadyAdded",
|
||||
"localeAlreadyAdded",
|
||||
"edit",
|
||||
"toast"
|
||||
],
|
||||
|
||||
@@ -2497,26 +2497,33 @@
|
||||
"manageYourStoresDetails": "Manage your store's details",
|
||||
"editStore": "Edit store",
|
||||
"defaultCurrency": "Default currency",
|
||||
"defaultLocale": "Default locale",
|
||||
"defaultRegion": "Default region",
|
||||
"defaultSalesChannel": "Default sales channel",
|
||||
"defaultLocation": "Default location",
|
||||
"swapLinkTemplate": "Swap link template",
|
||||
"paymentLinkTemplate": "Payment link template",
|
||||
"inviteLinkTemplate": "Invite link template",
|
||||
"locales": "Locales",
|
||||
"currencies": "Currencies",
|
||||
"addCurrencies": "Add currencies",
|
||||
"enableTaxInclusivePricing": "Enable tax inclusive pricing",
|
||||
"disableTaxInclusivePricing": "Disable tax inclusive pricing",
|
||||
"removeCurrencyWarning_one": "You are about to remove {{count}} currency from your store. Ensure that you have removed all prices using the currency before proceeding.",
|
||||
"removeCurrencyWarning_other": "You are about to remove {{count}} currencies from your store. Ensure that you have removed all prices using the currencies before proceeding.",
|
||||
"removeLocaleWarning_one": "You are about to remove {{count}} locale from your store. Any translation using this locale will be removed.",
|
||||
"removeLocaleWarning_other": "You are about to remove {{count}} locales from your store. Any translation using these locales will be removed.",
|
||||
"currencyAlreadyAdded": "The currency has already been added to your store.",
|
||||
"localeAlreadyAdded": "The locale has already been added to your store.",
|
||||
"edit": {
|
||||
"header": "Edit Store"
|
||||
},
|
||||
"toast": {
|
||||
"update": "Store successfully updated",
|
||||
"currenciesUpdated": "Currencies updated successfully",
|
||||
"localesUpdated": "Locales updated successfully",
|
||||
"currenciesRemoved": "Removed currencies from the store successfully",
|
||||
"localesRemoved": "Removed locales from the store successfully",
|
||||
"updatedTaxInclusivitySuccessfully": "Tax inclusive pricing updated successfully"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2448,26 +2448,33 @@
|
||||
"manageYourStoresDetails": "Gestiona los detalles de tu tienda",
|
||||
"editStore": "Editar tienda",
|
||||
"defaultCurrency": "Moneda por defecto",
|
||||
"defaultLocale": "Idioma por defecto",
|
||||
"defaultRegion": "Región por defecto",
|
||||
"defaultSalesChannel": "Canal de ventas predeterminado",
|
||||
"defaultLocation": "Ubicación predeterminada",
|
||||
"swapLinkTemplate": "Plantilla de enlace de cambio",
|
||||
"paymentLinkTemplate": "Plantilla de enlace de pago",
|
||||
"inviteLinkTemplate": "Plantilla de enlace de invitación",
|
||||
"locales": "Idiomas",
|
||||
"currencies": "Monedas",
|
||||
"addCurrencies": "Agregar monedas",
|
||||
"enableTaxInclusivePricing": "Habilitar precios con impuestos incluidos",
|
||||
"disableTaxInclusivePricing": "Deshabilitar precios con impuestos incluidos",
|
||||
"removeCurrencyWarning_one": "Estás a punto de eliminar {{count}} moneda de tu tienda. Asegúrate de haber eliminado todos los precios que usen esta moneda antes de continuar.",
|
||||
"removeCurrencyWarning_other": "Estás a punto de eliminar {{count}} monedas de tu tienda. Asegúrate de haber eliminado todos los precios que usen estas monedas antes de continuar.",
|
||||
"removeLocaleWarning_one": "Estás a punto de eliminar {{count}} idioma de tu tienda. Cualquier traducción que use este idioma será eliminada.",
|
||||
"removeLocaleWarning_other": "Estás a punto de eliminar {{count}} idiomas de tu tienda. Cualquier traducción que use estos idiomas será eliminada.",
|
||||
"currencyAlreadyAdded": "La moneda ya ha sido agregada a tu tienda.",
|
||||
"localeAlreadyAdded": "El idioma ya ha sido agregado a tu tienda.",
|
||||
"edit": {
|
||||
"header": "Editar Tienda"
|
||||
},
|
||||
"toast": {
|
||||
"update": "Tienda actualizada exitosamente",
|
||||
"currenciesUpdated": "Monedas actualizadas exitosamente",
|
||||
"localesUpdated": "Idiomas actualizados exitosamente",
|
||||
"currenciesRemoved": "Monedas eliminadas de la tienda exitosamente",
|
||||
"localesRemoved": "Idiomas eliminados de la tienda exitosamente",
|
||||
"updatedTaxInclusivitySuccessfully": "Precios con impuestos incluidos actualizados exitosamente"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { createColumnHelper } from "@tanstack/react-table"
|
||||
import { useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import {
|
||||
TextCell,
|
||||
TextHeader,
|
||||
} from "../../../../components/table/table-cells/common/text-cell"
|
||||
|
||||
const columnHelper = createColumnHelper<HttpTypes.AdminLocale>()
|
||||
|
||||
export const useLocalesTableColumns = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.accessor("code", {
|
||||
header: () => <TextHeader text={t("fields.code")} />,
|
||||
cell: ({ getValue }) => <TextCell text={getValue()} />,
|
||||
}),
|
||||
columnHelper.accessor("name", {
|
||||
header: () => <TextHeader text={t("fields.name")} />,
|
||||
cell: ({ getValue }) => <TextCell text={getValue()} />,
|
||||
}),
|
||||
],
|
||||
[t]
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useQueryParams } from "../../../../hooks/use-query-params"
|
||||
|
||||
export const useLocalesTableQuery = ({
|
||||
pageSize = 10,
|
||||
prefix,
|
||||
}: {
|
||||
pageSize?: number
|
||||
prefix?: string
|
||||
}) => {
|
||||
const raw = useQueryParams(["order", "q", "offset"], prefix)
|
||||
|
||||
const { offset, ...rest } = raw
|
||||
|
||||
const searchParams = {
|
||||
limit: pageSize,
|
||||
offset: offset ? parseInt(offset) : 0,
|
||||
...rest,
|
||||
}
|
||||
|
||||
return { searchParams, raw }
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { z } from "zod"
|
||||
import { useLocalesTableQuery } from "../../../common/hooks/use-locales-table-query"
|
||||
import { useRouteModal } from "../../../../../components/modals/route-modal-provider"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useLocales, useUpdateStore } from "../../../../../hooks/api"
|
||||
import { keepPreviousData } from "@tanstack/react-query"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import {
|
||||
createColumnHelper,
|
||||
OnChangeFn,
|
||||
RowSelectionState,
|
||||
} from "@tanstack/react-table"
|
||||
import { useMemo, useState } from "react"
|
||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||
import { Button, Checkbox, Hint, toast, Tooltip } from "@medusajs/ui"
|
||||
import { RouteFocusModal } from "../../../../../components/modals/route-focus-modal"
|
||||
import { KeyboundForm } from "../../../../../components/utilities/keybound-form"
|
||||
import { _DataTable } from "../../../../../components/table/data-table"
|
||||
import { useLocalesTableColumns } from "../../../common/hooks/use-locales-table-columns"
|
||||
|
||||
type AddLocalesFormProps = {
|
||||
store: HttpTypes.AdminStore
|
||||
}
|
||||
|
||||
const AddLocalesSchema = z.object({
|
||||
locales: z.array(z.string()).min(1),
|
||||
})
|
||||
|
||||
const PAGE_SIZE = 50
|
||||
const PREFIX = "al"
|
||||
|
||||
export const AddLocalesForm = ({ store }: AddLocalesFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
|
||||
const { raw, searchParams } = useLocalesTableQuery({
|
||||
pageSize: PAGE_SIZE,
|
||||
prefix: PREFIX,
|
||||
})
|
||||
|
||||
const {
|
||||
locales,
|
||||
count,
|
||||
isPending: isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useLocales(searchParams, {
|
||||
placeholderData: keepPreviousData,
|
||||
})
|
||||
|
||||
const form = useForm<z.infer<typeof AddLocalesSchema>>({
|
||||
defaultValues: {
|
||||
locales: [],
|
||||
},
|
||||
resolver: zodResolver(AddLocalesSchema),
|
||||
})
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||
|
||||
const { setValue } = form
|
||||
|
||||
const updater: OnChangeFn<RowSelectionState> = (fn) => {
|
||||
const updated = typeof fn === "function" ? fn(rowSelection) : fn
|
||||
|
||||
const ids = Object.keys(updated)
|
||||
setValue("locales", ids, {
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
})
|
||||
|
||||
setRowSelection(updated)
|
||||
}
|
||||
|
||||
const preSelectedRows =
|
||||
store.supported_locales?.map((l) => l.locale_code) ?? []
|
||||
|
||||
const columns = useColumns()
|
||||
|
||||
const { table } = useDataTable({
|
||||
data: locales ?? [],
|
||||
columns,
|
||||
count: count,
|
||||
getRowId: (row) => row.code,
|
||||
enableRowSelection: (row) => !preSelectedRows.includes(row.original.code),
|
||||
enablePagination: true,
|
||||
pageSize: PAGE_SIZE,
|
||||
prefix: PREFIX,
|
||||
rowSelection: {
|
||||
state: rowSelection,
|
||||
updater,
|
||||
},
|
||||
})
|
||||
|
||||
const { mutateAsync, isPending } = useUpdateStore(store.id)
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (data) => {
|
||||
const locales = Array.from(
|
||||
new Set([...data.locales, ...preSelectedRows])
|
||||
) as string[]
|
||||
|
||||
let defaultLocale = store.supported_locales?.find(
|
||||
(l) => l.is_default
|
||||
)?.locale_code
|
||||
|
||||
if (!locales.includes(defaultLocale ?? "")) {
|
||||
defaultLocale = locales?.[0]
|
||||
}
|
||||
|
||||
await mutateAsync(
|
||||
{
|
||||
supported_locales: locales.map((l) => ({
|
||||
locale_code: l,
|
||||
is_default: l === defaultLocale,
|
||||
})),
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success(t("store.toast.localesUpdated"))
|
||||
handleSuccess()
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message)
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<RouteFocusModal.Form form={form}>
|
||||
<KeyboundForm
|
||||
onSubmit={handleSubmit}
|
||||
className="flex h-full flex-col overflow-hidden"
|
||||
>
|
||||
<RouteFocusModal.Header>
|
||||
<div className="flex flex-1 items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
{form.formState.errors.locales && (
|
||||
<Hint variant="error">
|
||||
{form.formState.errors.locales.message}
|
||||
</Hint>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</RouteFocusModal.Header>
|
||||
<RouteFocusModal.Body className="flex flex-1 flex-col overflow-hidden">
|
||||
<_DataTable
|
||||
table={table}
|
||||
pageSize={PAGE_SIZE}
|
||||
count={count}
|
||||
columns={columns}
|
||||
layout="fill"
|
||||
pagination
|
||||
search="autofocus"
|
||||
prefix={PREFIX}
|
||||
orderBy={[
|
||||
{ key: "name", label: t("fields.name") },
|
||||
{ key: "code", label: t("fields.code") },
|
||||
]}
|
||||
isLoading={isLoading}
|
||||
queryObject={raw}
|
||||
/>
|
||||
</RouteFocusModal.Body>
|
||||
<RouteFocusModal.Footer>
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<RouteFocusModal.Close asChild>
|
||||
<Button size="small" variant="secondary">
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</RouteFocusModal.Close>
|
||||
<Button size="small" type="submit" isLoading={isPending}>
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</RouteFocusModal.Footer>
|
||||
</KeyboundForm>
|
||||
</RouteFocusModal.Form>
|
||||
)
|
||||
}
|
||||
|
||||
const columnHelper = createColumnHelper<HttpTypes.AdminLocale>()
|
||||
|
||||
const useColumns = () => {
|
||||
const { t } = useTranslation()
|
||||
const base = useLocalesTableColumns()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.display({
|
||||
id: "select",
|
||||
header: ({ table }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsSomePageRowsSelected()
|
||||
? "indeterminate"
|
||||
: table.getIsAllPageRowsSelected()
|
||||
}
|
||||
onCheckedChange={(value) =>
|
||||
table.toggleAllPageRowsSelected(!!value)
|
||||
}
|
||||
/>
|
||||
)
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const isPreSelected = !row.getCanSelect()
|
||||
const isSelected = row.getIsSelected() || isPreSelected
|
||||
|
||||
const Component = (
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
disabled={isPreSelected}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
if (isPreSelected) {
|
||||
return (
|
||||
<Tooltip content={t("store.localeAlreadyAdded")} side="right">
|
||||
{Component}
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return Component
|
||||
},
|
||||
}),
|
||||
...base,
|
||||
],
|
||||
[t, base]
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { StoreAddLocales as Component } from "./store-add-locales"
|
||||
@@ -0,0 +1,29 @@
|
||||
import { RouteFocusModal } from "../../../components/modals/route-focus-modal"
|
||||
import { useStore } from "../../../hooks/api"
|
||||
import { AddLocalesForm } from "./components/add-locales-form/add-locales-form"
|
||||
import { useFeatureFlag } from "../../../providers/feature-flag-provider"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
|
||||
export const StoreAddLocales = () => {
|
||||
const isEnabled = useFeatureFlag("translation")
|
||||
const navigate = useNavigate()
|
||||
|
||||
if (!isEnabled) {
|
||||
navigate(-1)
|
||||
return null
|
||||
}
|
||||
|
||||
const { store, isPending, isError, error } = useStore()
|
||||
|
||||
const ready = !!store && !isPending
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<RouteFocusModal>
|
||||
{ready && <AddLocalesForm store={store} />}
|
||||
</RouteFocusModal>
|
||||
)
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { Link } from "react-router-dom"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { useSalesChannel, useStockLocation } from "../../../../../hooks/api"
|
||||
import { useRegion } from "../../../../../hooks/api/regions"
|
||||
import { useFeatureFlag } from "../../../../../providers/feature-flag-provider"
|
||||
|
||||
type StoreGeneralSectionProps = {
|
||||
store: AdminStore
|
||||
@@ -14,12 +15,14 @@ type StoreGeneralSectionProps = {
|
||||
|
||||
export const StoreGeneralSection = ({ store }: StoreGeneralSectionProps) => {
|
||||
const { t } = useTranslation()
|
||||
const isTranslationsEnabled = useFeatureFlag("translation")
|
||||
|
||||
const { region } = useRegion(store.default_region_id!, undefined, {
|
||||
enabled: !!store.default_region_id,
|
||||
})
|
||||
|
||||
const defaultCurrency = store.supported_currencies?.find((c) => c.is_default)
|
||||
const defaultLocale = store.supported_locales?.find((l) => l.is_default)
|
||||
|
||||
const { sales_channel } = useSalesChannel(store.default_sales_channel_id!, {
|
||||
enabled: !!store.default_sales_channel_id,
|
||||
@@ -85,6 +88,27 @@ export const StoreGeneralSection = ({ store }: StoreGeneralSectionProps) => {
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
{isTranslationsEnabled && (
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("store.defaultLocale")}
|
||||
</Text>
|
||||
{defaultLocale ? (
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Badge size="2xsmall">
|
||||
{defaultLocale.locale_code?.toUpperCase()}
|
||||
</Badge>
|
||||
<Text size="small" leading="compact">
|
||||
{defaultLocale.locale?.name}
|
||||
</Text>
|
||||
</div>
|
||||
) : (
|
||||
<Text size="small" leading="compact">
|
||||
-
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("store.defaultRegion")}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./store-locale-section"
|
||||
@@ -0,0 +1,290 @@
|
||||
import { Plus, Trash } from "@medusajs/icons"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import {
|
||||
Checkbox,
|
||||
CommandBar,
|
||||
Container,
|
||||
Heading,
|
||||
toast,
|
||||
usePrompt,
|
||||
} from "@medusajs/ui"
|
||||
import { keepPreviousData } from "@tanstack/react-query"
|
||||
import { RowSelectionState, createColumnHelper } from "@tanstack/react-table"
|
||||
import { useMemo, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { _DataTable } from "../../../../../components/table/data-table"
|
||||
import { useUpdateStore } from "../../../../../hooks/api/store"
|
||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||
import { useLocalesTableColumns } from "../../../common/hooks/use-locales-table-columns"
|
||||
import { useLocalesTableQuery } from "../../../common/hooks/use-locales-table-query"
|
||||
import { useLocales } from "../../../../../hooks/api"
|
||||
|
||||
type StoreLocaleSectionProps = {
|
||||
store: HttpTypes.AdminStore
|
||||
}
|
||||
|
||||
const PAGE_SIZE = 10
|
||||
|
||||
export const StoreLocaleSection = ({ store }: StoreLocaleSectionProps) => {
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||
|
||||
const { searchParams, raw } = useLocalesTableQuery({ pageSize: PAGE_SIZE })
|
||||
|
||||
const { locales, count, isPending, isError, error } = useLocales(
|
||||
{
|
||||
code: store.supported_locales?.map((l) => l.locale_code),
|
||||
...searchParams,
|
||||
},
|
||||
{
|
||||
placeholderData: keepPreviousData,
|
||||
enabled: !!store.supported_locales?.length,
|
||||
}
|
||||
)
|
||||
|
||||
const columns = useColumns()
|
||||
|
||||
const { table } = useDataTable({
|
||||
data: locales ?? [],
|
||||
columns,
|
||||
count: count,
|
||||
getRowId: (row) => row.code,
|
||||
rowSelection: {
|
||||
state: rowSelection,
|
||||
updater: setRowSelection,
|
||||
},
|
||||
enablePagination: true,
|
||||
enableRowSelection: true,
|
||||
pageSize: PAGE_SIZE,
|
||||
meta: {
|
||||
storeId: store.id,
|
||||
supportedLocales: store.supported_locales,
|
||||
defaultLocaleCode: store.supported_locales?.find((l) => l.is_default)
|
||||
?.locale_code,
|
||||
},
|
||||
})
|
||||
|
||||
const { mutateAsync } = useUpdateStore(store.id)
|
||||
const { t } = useTranslation()
|
||||
const prompt = usePrompt()
|
||||
|
||||
const handleDeleteLocales = async () => {
|
||||
const ids = Object.keys(rowSelection)
|
||||
|
||||
const result = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("store.removeLocaleWarning", {
|
||||
count: ids.length,
|
||||
}),
|
||||
confirmText: t("actions.remove"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!result) {
|
||||
return
|
||||
}
|
||||
|
||||
await mutateAsync(
|
||||
{
|
||||
supported_locales:
|
||||
store.supported_locales?.filter(
|
||||
(l) => !ids.includes(l.locale_code)
|
||||
) ?? [],
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setRowSelection({})
|
||||
toast.success(t("store.toast.localesRemoved"))
|
||||
},
|
||||
onError: (e) => {
|
||||
toast.error(e.message)
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
const isLoading = isPending
|
||||
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading level="h2">{t("store.locales")}</Heading>
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <Plus />,
|
||||
label: t("actions.add"),
|
||||
to: "locales",
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<_DataTable
|
||||
orderBy={[
|
||||
{ key: "name", label: t("fields.name") },
|
||||
{ key: "code", label: t("fields.code") },
|
||||
]}
|
||||
search
|
||||
pagination
|
||||
table={table}
|
||||
pageSize={PAGE_SIZE}
|
||||
columns={columns}
|
||||
count={!store.supported_locales?.length ? 0 : count}
|
||||
isLoading={!store.supported_locales?.length ? false : isLoading}
|
||||
queryObject={raw}
|
||||
/>
|
||||
<CommandBar open={!!Object.keys(rowSelection).length}>
|
||||
<CommandBar.Bar>
|
||||
<CommandBar.Value>
|
||||
{t("general.countSelected", {
|
||||
count: Object.keys(rowSelection).length,
|
||||
})}
|
||||
</CommandBar.Value>
|
||||
<CommandBar.Seperator />
|
||||
<CommandBar.Command
|
||||
action={handleDeleteLocales}
|
||||
shortcut="r"
|
||||
label={t("actions.remove")}
|
||||
/>
|
||||
</CommandBar.Bar>
|
||||
</CommandBar>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const LocaleActions = ({
|
||||
storeId,
|
||||
locale,
|
||||
supportedLocales,
|
||||
defaultLocaleCode,
|
||||
}: {
|
||||
storeId: string
|
||||
locale: HttpTypes.AdminLocale
|
||||
supportedLocales: HttpTypes.AdminStoreLocale[]
|
||||
defaultLocaleCode: string
|
||||
}) => {
|
||||
const { mutateAsync } = useUpdateStore(storeId)
|
||||
const { t } = useTranslation()
|
||||
const prompt = usePrompt()
|
||||
|
||||
const handleRemove = async () => {
|
||||
const result = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("store.removeLocaleWarning", {
|
||||
count: 1,
|
||||
}),
|
||||
verificationInstruction: t("general.typeToConfirm"),
|
||||
verificationText: locale.name,
|
||||
confirmText: t("actions.remove"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!result) {
|
||||
return
|
||||
}
|
||||
|
||||
await mutateAsync(
|
||||
{
|
||||
supported_locales: supportedLocales.filter(
|
||||
(l) => l.locale_code !== locale.code
|
||||
),
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success(t("store.toast.localesRemoved"))
|
||||
},
|
||||
onError: (e) => {
|
||||
toast.error(e.message)
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <Trash />,
|
||||
label: t("actions.remove"),
|
||||
onClick: handleRemove,
|
||||
disabled: locale.code === defaultLocaleCode,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const columnHelper = createColumnHelper<HttpTypes.AdminLocale>()
|
||||
|
||||
const useColumns = () => {
|
||||
const base = useLocalesTableColumns()
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.display({
|
||||
id: "select",
|
||||
header: ({ table }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsSomePageRowsSelected()
|
||||
? "indeterminate"
|
||||
: table.getIsAllPageRowsSelected()
|
||||
}
|
||||
onCheckedChange={(value) =>
|
||||
table.toggleAllPageRowsSelected(!!value)
|
||||
}
|
||||
/>
|
||||
)
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}),
|
||||
...base,
|
||||
columnHelper.display({
|
||||
id: "actions",
|
||||
cell: ({ row, table }) => {
|
||||
const { supportedLocales, storeId, defaultLocaleCode } = table.options
|
||||
.meta as {
|
||||
defaultLocaleCode: string
|
||||
supportedLocales: HttpTypes.AdminStoreLocale[]
|
||||
storeId: string
|
||||
}
|
||||
|
||||
return (
|
||||
<LocaleActions
|
||||
storeId={storeId}
|
||||
locale={row.original}
|
||||
supportedLocales={supportedLocales}
|
||||
defaultLocaleCode={defaultLocaleCode}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}),
|
||||
],
|
||||
[base, t]
|
||||
)
|
||||
}
|
||||
@@ -8,9 +8,12 @@ import { SingleColumnPageSkeleton } from "../../../components/common/skeleton"
|
||||
import { SingleColumnPage } from "../../../components/layout/pages"
|
||||
import { useExtension } from "../../../providers/extension-provider"
|
||||
import { StoreCurrencySection } from "./components/store-currency-section"
|
||||
import { StoreLocaleSection } from "./components/store-locale-section"
|
||||
import { useFeatureFlag } from "../../../providers/feature-flag-provider"
|
||||
|
||||
export const StoreDetail = () => {
|
||||
const initialData = useLoaderData() as Awaited<ReturnType<typeof storeLoader>>
|
||||
const isTranslationsEnabled = useFeatureFlag("translation")
|
||||
|
||||
const { store, isPending, isError, error } = useStore(undefined, {
|
||||
initialData,
|
||||
@@ -39,6 +42,7 @@ export const StoreDetail = () => {
|
||||
>
|
||||
<StoreGeneralSection store={store} />
|
||||
<StoreCurrencySection store={store} />
|
||||
{isTranslationsEnabled && <StoreLocaleSection store={store} />}
|
||||
</SingleColumnPage>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useUpdateStore } from "../../../../../hooks/api/store"
|
||||
import { useComboboxData } from "../../../../../hooks/use-combobox-data"
|
||||
import { sdk } from "../../../../../lib/client"
|
||||
import { useDocumentDirection } from "../../../../../hooks/use-document-direction"
|
||||
import { useFeatureFlag } from "../../../../../providers/feature-flag-provider"
|
||||
|
||||
type EditStoreFormProps = {
|
||||
store: HttpTypes.AdminStore
|
||||
@@ -21,6 +22,7 @@ type EditStoreFormProps = {
|
||||
const EditStoreSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
default_currency_code: z.string().optional(),
|
||||
default_locale_code: z.string().optional(),
|
||||
default_region_id: z.string().optional(),
|
||||
default_sales_channel_id: z.string().optional(),
|
||||
default_location_id: z.string().optional(),
|
||||
@@ -28,12 +30,16 @@ const EditStoreSchema = z.object({
|
||||
|
||||
export const EditStoreForm = ({ store }: EditStoreFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const isTranslationsEnabled = useFeatureFlag("translation")
|
||||
const { handleSuccess } = useRouteModal()
|
||||
const direction = useDocumentDirection()
|
||||
const direction = useDocumentDirection()
|
||||
const form = useForm<z.infer<typeof EditStoreSchema>>({
|
||||
defaultValues: {
|
||||
name: store.name,
|
||||
default_region_id: store.default_region_id || undefined,
|
||||
default_locale_code:
|
||||
store.supported_locales?.find((l) => l.is_default)?.locale_code ||
|
||||
undefined,
|
||||
default_currency_code:
|
||||
store.supported_currencies?.find((c) => c.is_default)?.currency_code ||
|
||||
undefined,
|
||||
@@ -73,10 +79,14 @@ export const EditStoreForm = ({ store }: EditStoreFormProps) => {
|
||||
})
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (values) => {
|
||||
const { default_currency_code, ...rest } = values
|
||||
const { default_currency_code, default_locale_code, ...rest } = values
|
||||
|
||||
const normalizedMutation: HttpTypes.AdminUpdateStore = {
|
||||
...rest,
|
||||
supported_locales: store.supported_locales?.map((l) => ({
|
||||
...l,
|
||||
is_default: l.locale_code === default_locale_code,
|
||||
})),
|
||||
supported_currencies: store.supported_currencies?.map((c) => ({
|
||||
...c,
|
||||
is_default: c.currency_code === default_currency_code,
|
||||
@@ -95,7 +105,10 @@ export const EditStoreForm = ({ store }: EditStoreFormProps) => {
|
||||
|
||||
return (
|
||||
<RouteDrawer.Form form={form}>
|
||||
<KeyboundForm onSubmit={handleSubmit} className="flex h-full flex-col overflow-hidden">
|
||||
<KeyboundForm
|
||||
onSubmit={handleSubmit}
|
||||
className="flex h-full flex-col overflow-hidden"
|
||||
>
|
||||
<RouteDrawer.Body className="overflow-y-auto">
|
||||
<div className="flex flex-col gap-y-8">
|
||||
<Form.Field
|
||||
@@ -143,6 +156,40 @@ export const EditStoreForm = ({ store }: EditStoreFormProps) => {
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{isTranslationsEnabled && (
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="default_locale_code"
|
||||
render={({ field: { onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("store.defaultLocale")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Select
|
||||
dir={direction}
|
||||
{...field}
|
||||
onValueChange={onChange}
|
||||
>
|
||||
<Select.Trigger ref={field.ref}>
|
||||
<Select.Value />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{store.supported_locales?.map((locale) => (
|
||||
<Select.Item
|
||||
key={locale.locale_code}
|
||||
value={locale.locale_code}
|
||||
>
|
||||
{locale.locale_code}
|
||||
</Select.Item>
|
||||
))}
|
||||
</Select.Content>
|
||||
</Select>
|
||||
</Form.Control>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="default_region_id"
|
||||
|
||||
Reference in New Issue
Block a user