chore(): Remove default_locale from StoreLocale (#14300)

## Summary

**What** — What changes are introduced in this PR?

Remove `default_locale` from Store <> Locale relation

**Why** — Why are these changes relevant or necessary?  

*Please provide answer here*

**How** — How have these changes been implemented?

*Please provide answer here*

**Testing** — How have these changes been tested, or how can the reviewer test the feature?

*Please provide answer here*

---

## Examples

Provide examples or code snippets that demonstrate how this feature works, or how it can be used in practice.  
This helps with documentation and ensures maintainers can quickly understand and verify the change.

```ts
// Example usage
```

---

## Checklist

Please ensure the following before requesting a review:

- [ ] I have added a **changeset** for this PR
    - Every non-breaking change should be marked as a **patch**
    - To add a changeset, run `yarn changeset` and follow the prompts
- [ ] The changes are covered by relevant **tests**
- [ ] I have verified the code works as intended locally
- [ ] I have linked the related issue(s) if applicable

---

## Additional Context

Add any additional context, related issues, or references that might help the reviewer understand this PR.


Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
Nicolas Gorga
2025-12-15 09:05:46 +00:00
committed by GitHub
co-authored by Oli Juhl
parent dd3eb10b1a
commit 8964a03fa1
23 changed files with 63 additions and 226 deletions
+9
View File
@@ -0,0 +1,9 @@
---
"@medusajs/dashboard": patch
"@medusajs/framework": patch
"@medusajs/store": patch
"@medusajs/types": patch
"@medusajs/medusa": patch
---
chore(): Remove default_locale from StoreLocale
@@ -1,11 +1,11 @@
import { medusaIntegrationTestRunner } from "@medusajs/test-utils" import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { MedusaContainer } from "@medusajs/types"
import { Modules, ProductStatus } from "@medusajs/utils" import { Modules, ProductStatus } from "@medusajs/utils"
import { import {
createAdminUser, createAdminUser,
generatePublishableKey, generatePublishableKey,
generateStoreHeaders, generateStoreHeaders,
} from "../../../../helpers/create-admin-user" } from "../../../../helpers/create-admin-user"
import { MedusaContainer } from "@medusajs/types"
jest.setTimeout(100000) jest.setTimeout(100000)
@@ -63,7 +63,7 @@ medusaIntegrationTestRunner({
) )
await storeModule.updateStores(defaultStore.id, { await storeModule.updateStores(defaultStore.id, {
supported_locales: [ supported_locales: [
{ locale_code: "en-US", is_default: true }, { locale_code: "en-US" },
{ locale_code: "fr-FR" }, { locale_code: "fr-FR" },
{ locale_code: "de-DE" }, { locale_code: "de-DE" },
], ],
@@ -45,7 +45,7 @@ medusaIntegrationTestRunner({
) )
await storeModule.updateStores(defaultStore.id, { await storeModule.updateStores(defaultStore.id, {
supported_locales: [ supported_locales: [
{ locale_code: "en-US", is_default: true }, { locale_code: "en-US" },
{ locale_code: "fr-FR" }, { locale_code: "fr-FR" },
{ locale_code: "de-DE" }, { locale_code: "de-DE" },
], ],
@@ -61,7 +61,7 @@ medusaIntegrationTestRunner({
) )
await storeModule.updateStores(defaultStore.id, { await storeModule.updateStores(defaultStore.id, {
supported_locales: [ supported_locales: [
{ locale_code: "en-US", is_default: true }, { locale_code: "en-US" },
{ locale_code: "fr-FR" }, { locale_code: "fr-FR" },
{ locale_code: "de-DE" }, { locale_code: "de-DE" },
], ],
@@ -60,7 +60,7 @@ medusaIntegrationTestRunner({
) )
await storeModule.updateStores(defaultStore.id, { await storeModule.updateStores(defaultStore.id, {
supported_locales: [ supported_locales: [
{ locale_code: "en-US", is_default: true }, { locale_code: "en-US" },
{ locale_code: "fr-FR" }, { locale_code: "fr-FR" },
{ locale_code: "de-DE" }, { locale_code: "de-DE" },
], ],
@@ -62,7 +62,7 @@ medusaIntegrationTestRunner({
) )
await storeModule.updateStores(defaultStore.id, { await storeModule.updateStores(defaultStore.id, {
supported_locales: [ supported_locales: [
{ locale_code: "en-US", is_default: true }, { locale_code: "en-US" },
{ locale_code: "fr-FR" }, { locale_code: "fr-FR" },
{ locale_code: "de-DE" }, { locale_code: "de-DE" },
], ],
@@ -1,10 +1,10 @@
import { medusaIntegrationTestRunner } from "@medusajs/test-utils" import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { MedusaContainer } from "@medusajs/types"
import { Modules } from "@medusajs/utils"
import { import {
adminHeaders, adminHeaders,
createAdminUser, createAdminUser,
} from "../../../../helpers/create-admin-user" } from "../../../../helpers/create-admin-user"
import { MedusaContainer } from "@medusajs/types"
import { Modules } from "@medusajs/utils"
jest.setTimeout(100000) jest.setTimeout(100000)
@@ -32,7 +32,7 @@ medusaIntegrationTestRunner({
) )
await storeModule.updateStores(defaultStore.id, { await storeModule.updateStores(defaultStore.id, {
supported_locales: [ supported_locales: [
{ locale_code: "en-US", is_default: true }, { locale_code: "en-US" },
{ locale_code: "fr-FR" }, { locale_code: "fr-FR" },
{ locale_code: "de-DE" }, { locale_code: "de-DE" },
], ],
@@ -100,19 +100,10 @@ export const AddLocalesForm = ({ store }: AddLocalesFormProps) => {
new Set([...data.locales, ...preSelectedRows]) new Set([...data.locales, ...preSelectedRows])
) as string[] ) as string[]
let defaultLocale = store.supported_locales?.find(
(l) => l.is_default
)?.locale_code
if (!locales.includes(defaultLocale ?? "")) {
defaultLocale = locales?.[0]
}
await mutateAsync( await mutateAsync(
{ {
supported_locales: locales.map((l) => ({ supported_locales: locales.map((l) => ({
locale_code: l, locale_code: l,
is_default: l === defaultLocale,
})), })),
}, },
{ {
@@ -7,7 +7,6 @@ import { Link } from "react-router-dom"
import { ActionMenu } from "../../../../../components/common/action-menu" import { ActionMenu } from "../../../../../components/common/action-menu"
import { useSalesChannel, useStockLocation } from "../../../../../hooks/api" import { useSalesChannel, useStockLocation } from "../../../../../hooks/api"
import { useRegion } from "../../../../../hooks/api/regions" import { useRegion } from "../../../../../hooks/api/regions"
import { useFeatureFlag } from "../../../../../providers/feature-flag-provider"
type StoreGeneralSectionProps = { type StoreGeneralSectionProps = {
store: AdminStore store: AdminStore
@@ -15,14 +14,12 @@ type StoreGeneralSectionProps = {
export const StoreGeneralSection = ({ store }: StoreGeneralSectionProps) => { export const StoreGeneralSection = ({ store }: StoreGeneralSectionProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const isTranslationsEnabled = useFeatureFlag("translation")
const { region } = useRegion(store.default_region_id!, undefined, { const { region } = useRegion(store.default_region_id!, undefined, {
enabled: !!store.default_region_id, enabled: !!store.default_region_id,
}) })
const defaultCurrency = store.supported_currencies?.find((c) => c.is_default) 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!, { const { sales_channel } = useSalesChannel(store.default_sales_channel_id!, {
enabled: !!store.default_sales_channel_id, enabled: !!store.default_sales_channel_id,
@@ -88,27 +85,6 @@ export const StoreGeneralSection = ({ store }: StoreGeneralSectionProps) => {
</Text> </Text>
)} )}
</div> </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"> <div className="text-ui-fg-subtle grid grid-cols-2 px-6 py-4">
<Text size="small" leading="compact" weight="plus"> <Text size="small" leading="compact" weight="plus">
{t("store.defaultRegion")} {t("store.defaultRegion")}
@@ -60,8 +60,6 @@ export const StoreLocaleSection = ({ store }: StoreLocaleSectionProps) => {
meta: { meta: {
storeId: store.id, storeId: store.id,
supportedLocales: store.supported_locales, supportedLocales: store.supported_locales,
defaultLocaleCode: store.supported_locales?.find((l) => l.is_default)
?.locale_code,
}, },
}) })
@@ -165,12 +163,10 @@ const LocaleActions = ({
storeId, storeId,
locale, locale,
supportedLocales, supportedLocales,
defaultLocaleCode,
}: { }: {
storeId: string storeId: string
locale: HttpTypes.AdminLocale locale: HttpTypes.AdminLocale
supportedLocales: HttpTypes.AdminStoreLocale[] supportedLocales: HttpTypes.AdminStoreLocale[]
defaultLocaleCode: string
}) => { }) => {
const { mutateAsync } = useUpdateStore(storeId) const { mutateAsync } = useUpdateStore(storeId)
const { t } = useTranslation() const { t } = useTranslation()
@@ -218,7 +214,6 @@ const LocaleActions = ({
icon: <Trash />, icon: <Trash />,
label: t("actions.remove"), label: t("actions.remove"),
onClick: handleRemove, onClick: handleRemove,
disabled: locale.code === defaultLocaleCode,
}, },
], ],
}, },
@@ -267,9 +262,7 @@ const useColumns = () => {
columnHelper.display({ columnHelper.display({
id: "actions", id: "actions",
cell: ({ row, table }) => { cell: ({ row, table }) => {
const { supportedLocales, storeId, defaultLocaleCode } = table.options const { supportedLocales, storeId } = table.options.meta as {
.meta as {
defaultLocaleCode: string
supportedLocales: HttpTypes.AdminStoreLocale[] supportedLocales: HttpTypes.AdminStoreLocale[]
storeId: string storeId: string
} }
@@ -279,7 +272,6 @@ const useColumns = () => {
storeId={storeId} storeId={storeId}
locale={row.original} locale={row.original}
supportedLocales={supportedLocales} supportedLocales={supportedLocales}
defaultLocaleCode={defaultLocaleCode}
/> />
) )
}, },
@@ -13,7 +13,6 @@ import { useUpdateStore } from "../../../../../hooks/api/store"
import { useComboboxData } from "../../../../../hooks/use-combobox-data" import { useComboboxData } from "../../../../../hooks/use-combobox-data"
import { sdk } from "../../../../../lib/client" import { sdk } from "../../../../../lib/client"
import { useDocumentDirection } from "../../../../../hooks/use-document-direction" import { useDocumentDirection } from "../../../../../hooks/use-document-direction"
import { useFeatureFlag } from "../../../../../providers/feature-flag-provider"
type EditStoreFormProps = { type EditStoreFormProps = {
store: HttpTypes.AdminStore store: HttpTypes.AdminStore
@@ -22,7 +21,6 @@ type EditStoreFormProps = {
const EditStoreSchema = z.object({ const EditStoreSchema = z.object({
name: z.string().min(1), name: z.string().min(1),
default_currency_code: z.string().optional(), default_currency_code: z.string().optional(),
default_locale_code: z.string().optional(),
default_region_id: z.string().optional(), default_region_id: z.string().optional(),
default_sales_channel_id: z.string().optional(), default_sales_channel_id: z.string().optional(),
default_location_id: z.string().optional(), default_location_id: z.string().optional(),
@@ -30,16 +28,12 @@ const EditStoreSchema = z.object({
export const EditStoreForm = ({ store }: EditStoreFormProps) => { export const EditStoreForm = ({ store }: EditStoreFormProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const isTranslationsEnabled = useFeatureFlag("translation")
const { handleSuccess } = useRouteModal() const { handleSuccess } = useRouteModal()
const direction = useDocumentDirection() const direction = useDocumentDirection()
const form = useForm<z.infer<typeof EditStoreSchema>>({ const form = useForm<z.infer<typeof EditStoreSchema>>({
defaultValues: { defaultValues: {
name: store.name, name: store.name,
default_region_id: store.default_region_id || undefined, 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: default_currency_code:
store.supported_currencies?.find((c) => c.is_default)?.currency_code || store.supported_currencies?.find((c) => c.is_default)?.currency_code ||
undefined, undefined,
@@ -79,14 +73,10 @@ export const EditStoreForm = ({ store }: EditStoreFormProps) => {
}) })
const handleSubmit = form.handleSubmit(async (values) => { const handleSubmit = form.handleSubmit(async (values) => {
const { default_currency_code, default_locale_code, ...rest } = values const { default_currency_code, ...rest } = values
const normalizedMutation: HttpTypes.AdminUpdateStore = { const normalizedMutation: HttpTypes.AdminUpdateStore = {
...rest, ...rest,
supported_locales: store.supported_locales?.map((l) => ({
...l,
is_default: l.locale_code === default_locale_code,
})),
supported_currencies: store.supported_currencies?.map((c) => ({ supported_currencies: store.supported_currencies?.map((c) => ({
...c, ...c,
is_default: c.currency_code === default_currency_code, is_default: c.currency_code === default_currency_code,
@@ -156,40 +146,6 @@ 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 <Form.Field
control={form.control} control={form.control}
name="default_region_id" name="default_region_id"
@@ -1,4 +1,4 @@
import { ContainerRegistrationKeys, normalizeLocale } from "@medusajs/utils" import { normalizeLocale } from "@medusajs/utils"
import type { import type {
MedusaNextFunction, MedusaNextFunction,
MedusaRequest, MedusaRequest,
@@ -35,30 +35,5 @@ export async function applyLocale(
return next() return next()
} }
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
const {
data: [store],
} = await query.graph(
{
entity: "store",
fields: ["id", "supported_locales"],
pagination: {
take: 1,
},
},
{
cache: {
enable: true,
},
}
)
if (store?.supported_locales?.length) {
req.locale = store.supported_locales.find(
(locale) => locale.is_default
)?.locale_code
return next()
}
return next() return next()
} }
@@ -55,10 +55,6 @@ export interface AdminStoreLocale {
* The ID of the store that the locale belongs to. * The ID of the store that the locale belongs to.
*/ */
store_id: string store_id: string
/**
* Whether the locale is the default locale for the store.
*/
is_default: boolean
/** /**
* The locale's details. * The locale's details.
*/ */
@@ -26,10 +26,6 @@ export interface AdminUpdateStoreSupportedLocale {
* The locale's BCP 47 language tag. * The locale's BCP 47 language tag.
*/ */
locale_code: string locale_code: string
/**
* Whether this locale is the default locale in the store.
*/
is_default?: boolean
} }
/** /**
@@ -40,10 +40,6 @@ export interface StoreLocaleDTO {
* The locale code of the store locale. * The locale code of the store locale.
*/ */
locale_code: string locale_code: string
/**
* Whether the locale is the default one for the store.
*/
is_default: boolean
/** /**
* The store ID associated with the locale. * The store ID associated with the locale.
*/ */
@@ -14,10 +14,6 @@ export interface CreateStoreLocaleDTO {
* The locale code of the store locale. * The locale code of the store locale.
*/ */
locale_code: string locale_code: string
/**
* Whether the locale is the default one for the store.
*/
is_default?: boolean
} }
/** /**
@@ -35,7 +35,6 @@ export const AdminUpdateStore = z.object({
.array( .array(
z.object({ z.object({
locale_code: z.string(), locale_code: z.string(),
is_default: z.boolean().optional(),
}) })
) )
.optional(), .optional(),
@@ -8,7 +8,7 @@ export const createStoreFixture: StoreTypes.CreateStoreDTO = {
], ],
supported_locales: [ supported_locales: [
{ locale_code: "fr-FR" }, { locale_code: "fr-FR" },
{ locale_code: "en-US", is_default: true }, { locale_code: "en-US" },
], ],
default_sales_channel_id: "test-sales-channel", default_sales_channel_id: "test-sales-channel",
default_region_id: "test-region", default_region_id: "test-region",
@@ -1,7 +1,7 @@
import { IStoreModuleService } from "@medusajs/framework/types" import { IStoreModuleService } from "@medusajs/framework/types"
import { Module, Modules } from "@medusajs/framework/utils" import { Module, Modules } from "@medusajs/framework/utils"
import { StoreModuleService } from "@services"
import { moduleIntegrationTestRunner } from "@medusajs/test-utils" import { moduleIntegrationTestRunner } from "@medusajs/test-utils"
import { StoreModuleService } from "@services"
import { createStoreFixture } from "../__fixtures__" import { createStoreFixture } from "../__fixtures__"
jest.setTimeout(100000) jest.setTimeout(100000)
@@ -92,19 +92,6 @@ moduleIntegrationTestRunner<IStoreModuleService>({
"There should be a default currency set for the store" "There should be a default currency set for the store"
) )
}) })
it("should fail to get created if there is no default locale", async function () {
const err = await service
.createStores({
...createStoreFixture,
supported_locales: [{ locale_code: "en-US" }],
})
.catch((err) => err.message)
expect(err).toEqual(
"There should be a default locale set for the store"
)
})
}) })
describe("upserting a store", () => { describe("upserting a store", () => {
@@ -160,19 +147,6 @@ moduleIntegrationTestRunner<IStoreModuleService>({
) )
}) })
it("should fail updating locales without a default one", async function () {
const createdStore = await service.createStores(createStoreFixture)
const updateErr = await service
.updateStores(createdStore.id, {
supported_locales: [{ locale_code: "en-US" }],
})
.catch((err) => err.message)
expect(updateErr).toEqual(
"There should be a default locale set for the store"
)
})
it("should fail updating currencies where a duplicate currency code exists", async function () { it("should fail updating currencies where a duplicate currency code exists", async function () {
const createdStore = await service.createStores(createStoreFixture) const createdStore = await service.createStores(createStoreFixture)
const updateErr = await service const updateErr = await service
@@ -214,20 +188,6 @@ moduleIntegrationTestRunner<IStoreModuleService>({
expect(updateErr).toEqual("Only one default currency is allowed") expect(updateErr).toEqual("Only one default currency is allowed")
}) })
it("should fail updating locales where there is more than 1 default locale", async function () {
const createdStore = await service.createStores(createStoreFixture)
const updateErr = await service
.updateStores(createdStore.id, {
supported_locales: [
{ locale_code: "en-US", is_default: true },
{ locale_code: "fr-FR", is_default: true },
],
})
.catch((err) => err.message)
expect(updateErr).toEqual("Only one default locale is allowed")
})
}) })
describe("deleting a store", () => { describe("deleting a store", () => {
@@ -263,16 +263,6 @@
"nullable": false, "nullable": false,
"mappedType": "text" "mappedType": "text"
}, },
"is_default": {
"name": "is_default",
"type": "boolean",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"default": "false",
"mappedType": "boolean"
},
"store_id": { "store_id": {
"name": "store_id", "name": "store_id",
"type": "text", "type": "text",
@@ -0,0 +1,13 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20251212161429 extends Migration {
override async up(): Promise<void> {
this.addSql(`alter table if exists "store_locale" drop column if exists "is_default";`);
}
override async down(): Promise<void> {
this.addSql(`alter table if exists "store_locale" add column if not exists "is_default" boolean not null default false;`);
}
}
@@ -4,7 +4,6 @@ import Store from "./store"
const StoreLocale = model.define("StoreLocale", { const StoreLocale = model.define("StoreLocale", {
id: model.id({ prefix: "stloc" }).primaryKey(), id: model.id({ prefix: "stloc" }).primaryKey(),
locale_code: model.text().searchable(), locale_code: model.text().searchable(),
is_default: model.boolean().default(false),
store: model store: model
.belongsTo(() => Store, { .belongsTo(() => Store, {
mappedBy: "supported_locales", mappedBy: "supported_locales",
@@ -227,55 +227,48 @@ export default class StoreModuleService
) { ) {
for (const store of stores) { for (const store of stores) {
if (store.supported_currencies?.length) { if (store.supported_currencies?.length) {
StoreModuleService.validateSupportedItems( StoreModuleService.validateUnique(
store.supported_currencies, store.supported_currencies.map((currency) => currency.currency_code),
(c) => c.currency_code,
"currency" "currency"
) )
let seenDefault = false
store.supported_currencies.forEach((currency) => {
if (currency.is_default) {
if (seenDefault) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Only one default currency is allowed`
)
}
seenDefault = true
}
})
if (!seenDefault) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`There should be a default currency set for the store`
)
}
} }
// TODO: If we are protecting this module behind a feature flag, we should check if the feature flag is enabled before validating the locales.
if (store.supported_locales?.length) { if (store.supported_locales?.length) {
StoreModuleService.validateSupportedItems( StoreModuleService.validateUnique(
store.supported_locales, store.supported_locales.map((locale) => locale.locale_code),
(l) => l.locale_code,
"locale" "locale"
) )
} }
} }
} }
private static validateSupportedItems<T extends { is_default?: boolean }>( private static validateUnique = (items: string[], fieldName: string) => {
items: T[], const duplicates = getDuplicates(items)
getCode: (item: T) => string,
typeName: string
) {
const duplicates = getDuplicates(items.map(getCode))
if (duplicates.length) { if (duplicates.length) {
throw new MedusaError( throw new MedusaError(
MedusaError.Types.INVALID_DATA, MedusaError.Types.INVALID_DATA,
`Duplicate ${typeName} codes: ${duplicates.join(", ")}` `Duplicate ${fieldName} codes: ${duplicates.join(", ")}`
)
}
let seenDefault = false
items.forEach((item) => {
if (item.is_default) {
if (seenDefault) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Only one default ${typeName} is allowed`
)
}
seenDefault = true
}
})
if (!seenDefault) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`There should be a default ${typeName} set for the store`
) )
} }
} }