@medusajs/dashboard 2.12.3-snapshot-20251216185234 → 2.12.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/dist/add-locales-GGNZCABB.mjs +81 -0
  2. package/dist/{api-key-management-detail-6RCDH73M.mjs → api-key-management-detail-FRUN2KFK.mjs} +1 -1
  3. package/dist/app.css +19 -0
  4. package/dist/app.js +795 -680
  5. package/dist/app.mjs +2 -2
  6. package/dist/{campaign-detail-5Q4BYCPX.mjs → campaign-detail-HM3GQJLQ.mjs} +1 -1
  7. package/dist/{categories-metadata-J7M3XWI7.mjs → categories-metadata-WKL3MGD7.mjs} +1 -1
  8. package/dist/{category-detail-S5IPXMHX.mjs → category-detail-UTWWDKFP.mjs} +2 -2
  9. package/dist/{category-products-KPW6BA5J.mjs → category-products-XXBTCXFF.mjs} +2 -2
  10. package/dist/{chunk-OL24RDYM.mjs → chunk-5F427YCP.mjs} +2 -2
  11. package/dist/{chunk-MJDHVDOW.mjs → chunk-5ISRTMYH.mjs} +1 -1
  12. package/dist/{chunk-ST4P6BQN.mjs → chunk-DQUXK4WW.mjs} +1 -1
  13. package/dist/{chunk-YYOPBKME.mjs → chunk-DTCIBQO2.mjs} +1 -1
  14. package/dist/{chunk-LZFWCKOF.mjs → chunk-FKNW5MLZ.mjs} +21 -4
  15. package/dist/{chunk-GRZSG4EP.mjs → chunk-GLBHPDR4.mjs} +21 -16
  16. package/dist/{chunk-WYATCUOM.mjs → chunk-HNJ65IND.mjs} +1 -1
  17. package/dist/{store-add-locales-VJ4RJ7UI.mjs → chunk-IKTGFXWR.mjs} +2 -67
  18. package/dist/{chunk-DBXWB3RF.mjs → chunk-KSDXSKJ7.mjs} +1 -1
  19. package/dist/{chunk-OL6MEUKW.mjs → chunk-LWYKUORZ.mjs} +108 -105
  20. package/dist/{chunk-PHLCT2HA.mjs → chunk-OK6NZN2A.mjs} +1 -1
  21. package/dist/{chunk-ZMG5B4FG.mjs → chunk-SG2JZPTG.mjs} +1 -1
  22. package/dist/{chunk-CVHJAKLQ.mjs → chunk-UMCJYHAD.mjs} +1 -1
  23. package/dist/{collection-add-products-FU2BS3D3.mjs → collection-add-products-42F7H77E.mjs} +2 -2
  24. package/dist/{collection-detail-VJE7XHLV.mjs → collection-detail-PXIS3G64.mjs} +2 -2
  25. package/dist/{collection-list-IGA6SCNF.mjs → collection-list-O74CGY24.mjs} +2 -2
  26. package/dist/{collection-metadata-QK7MI3D2.mjs → collection-metadata-U6FMA4IC.mjs} +1 -1
  27. package/dist/{customer-detail-MOV2T3LF.mjs → customer-detail-OMTFJ6CE.mjs} +1 -1
  28. package/dist/{customer-group-detail-6T7OXGQD.mjs → customer-group-detail-ADK3M5LG.mjs} +1 -1
  29. package/dist/{customer-group-list-AJEAF5D2.mjs → customer-group-list-7ZRQ2HWU.mjs} +1 -1
  30. package/dist/{customers-add-customer-group-QVTVSQYM.mjs → customers-add-customer-group-5U27WHJB.mjs} +1 -1
  31. package/dist/{edit-rules-SMVRTCUP.mjs → edit-rules-BM2ERGVJ.mjs} +1 -1
  32. package/dist/en.json +1 -2
  33. package/dist/{inventory-create-BK52VALF.mjs → inventory-create-7MA7B5N2.mjs} +2 -2
  34. package/dist/{inventory-detail-ZPSEMYI2.mjs → inventory-detail-B4PRHZK3.mjs} +1 -1
  35. package/dist/{inventory-metadata-FNEJ3RAT.mjs → inventory-metadata-C7MJ3GY5.mjs} +1 -1
  36. package/dist/{inventory-stock-6WYWLWJ7.mjs → inventory-stock-WVTYPJTX.mjs} +3 -3
  37. package/dist/{location-detail-N3GUZSY7.mjs → location-detail-KO6EBDK5.mjs} +1 -1
  38. package/dist/{location-fulfillment-providers-7ZUJAGNY.mjs → location-fulfillment-providers-IORBE3E3.mjs} +2 -2
  39. package/dist/{location-service-zone-shipping-option-create-CNRWYZQC.mjs → location-service-zone-shipping-option-create-2R3ZFLVK.mjs} +3 -3
  40. package/dist/{location-service-zone-shipping-option-pricing-OGWI7VPT.mjs → location-service-zone-shipping-option-pricing-5HN2Z5RB.mjs} +2 -2
  41. package/dist/{login-VNOLI5YG.mjs → login-XKB6OR7I.mjs} +1 -1
  42. package/dist/{order-create-claim-SCDJGM46.mjs → order-create-claim-NKCOGF4A.mjs} +1 -1
  43. package/dist/{order-create-edit-2WALBPXS.mjs → order-create-edit-UNQYXGLL.mjs} +1 -1
  44. package/dist/{order-create-exchange-LQU4YN7F.mjs → order-create-exchange-WI7OA2WO.mjs} +1 -1
  45. package/dist/{order-create-fulfillment-OWUVTZXW.mjs → order-create-fulfillment-2LJTEWDY.mjs} +1 -1
  46. package/dist/{order-create-refund-Q6HQY42R.mjs → order-create-refund-7K6UJXGP.mjs} +1 -1
  47. package/dist/{order-create-shipment-WAGGEPRW.mjs → order-create-shipment-ZTDLLUBY.mjs} +1 -1
  48. package/dist/{order-detail-PVPGEWGY.mjs → order-detail-JTRUMRLO.mjs} +1 -1
  49. package/dist/{order-edit-billing-address-UM76J4KX.mjs → order-edit-billing-address-YHYNVLOE.mjs} +1 -1
  50. package/dist/{order-edit-email-CL3KNOCM.mjs → order-edit-email-TCQPEVZY.mjs} +1 -1
  51. package/dist/{order-edit-shipping-address-PIESTGVL.mjs → order-edit-shipping-address-CFSYQLKD.mjs} +1 -1
  52. package/dist/{order-export-LE363ZLB.mjs → order-export-G4SBNEJ7.mjs} +1 -1
  53. package/dist/{order-metadata-FHBB7MTG.mjs → order-metadata-KGPB37VL.mjs} +1 -1
  54. package/dist/{order-receive-return-PRVKP6J2.mjs → order-receive-return-JER24SEV.mjs} +1 -1
  55. package/dist/{order-request-transfer-XSAGRUMT.mjs → order-request-transfer-3FBUYZNT.mjs} +1 -1
  56. package/dist/{price-list-create-K5JEZT57.mjs → price-list-create-CXZCFFTP.mjs} +4 -4
  57. package/dist/{price-list-detail-Q5VG5VGW.mjs → price-list-detail-XOMU6U5J.mjs} +2 -2
  58. package/dist/{price-list-prices-add-2MQ226U4.mjs → price-list-prices-add-SDX5CQME.mjs} +4 -4
  59. package/dist/{price-list-prices-edit-OJZLV7OS.mjs → price-list-prices-edit-EKB6NI5D.mjs} +2 -2
  60. package/dist/{product-attributes-YF4TZOIO.mjs → product-attributes-STD47BGC.mjs} +2 -2
  61. package/dist/{product-create-KJML2332.mjs → product-create-LVGWVQAT.mjs} +3 -3
  62. package/dist/{product-create-variant-5EBCLM54.mjs → product-create-variant-OTJKT6WI.mjs} +2 -2
  63. package/dist/{product-detail-QG72542C.mjs → product-detail-OYVHJH3D.mjs} +2 -2
  64. package/dist/{product-edit-DZZR775Q.mjs → product-edit-3SIUUIW4.mjs} +2 -2
  65. package/dist/{product-export-5AD7NELI.mjs → product-export-57UUAGXF.mjs} +2 -2
  66. package/dist/{product-image-variants-edit-M6QF2RLE.mjs → product-image-variants-edit-2BW5BJON.mjs} +1 -1
  67. package/dist/{product-import-V3KQN4TV.mjs → product-import-6EM4VUXP.mjs} +1 -1
  68. package/dist/{product-list-EUWZIFTM.mjs → product-list-5V5GEH5K.mjs} +2 -2
  69. package/dist/{product-metadata-GL2MVPDI.mjs → product-metadata-JZLHBLZQ.mjs} +1 -1
  70. package/dist/{product-organization-O7RHELMQ.mjs → product-organization-3PQ45C4B.mjs} +2 -2
  71. package/dist/{product-prices-YWV6MSM6.mjs → product-prices-5ZL2RP7A.mjs} +1 -1
  72. package/dist/{product-stock-AKEFMK5O.mjs → product-stock-SJJABF6I.mjs} +3 -3
  73. package/dist/{product-tag-create-PQMDDKWH.mjs → product-tag-create-XXO4AQEC.mjs} +1 -1
  74. package/dist/{product-tag-detail-I3MBZX7U.mjs → product-tag-detail-BSK64HXL.mjs} +3 -3
  75. package/dist/{product-tag-edit-K3BBQLJR.mjs → product-tag-edit-ENCGDT7E.mjs} +1 -1
  76. package/dist/{product-tag-list-JUWSOMB7.mjs → product-tag-list-SLQGCNDZ.mjs} +3 -3
  77. package/dist/{product-tag-metadata-MJH5LH7E.mjs → product-tag-metadata-EPXHMU2K.mjs} +1 -1
  78. package/dist/{product-type-detail-RKHT5NBL.mjs → product-type-detail-4CRRU7YK.mjs} +2 -2
  79. package/dist/{product-type-metadata-CDJDFFGQ.mjs → product-type-metadata-73OKOGPP.mjs} +1 -1
  80. package/dist/{product-variant-detail-XAYG5CKE.mjs → product-variant-detail-RPHLG4HU.mjs} +1 -1
  81. package/dist/{product-variant-edit-DEZEY2H2.mjs → product-variant-edit-JF7NN64Y.mjs} +1 -1
  82. package/dist/{product-variant-metadata-VTZDNWUT.mjs → product-variant-metadata-HU2CXGPO.mjs} +1 -1
  83. package/dist/{promotion-create-HWFNUQXG.mjs → promotion-create-BHA3FQG2.mjs} +1 -1
  84. package/dist/{promotion-detail-QC36KXB3.mjs → promotion-detail-F3QSR52W.mjs} +1 -1
  85. package/dist/{refund-reason-create-YHCDEHGQ.mjs → refund-reason-create-ZA5TKW2Z.mjs} +1 -1
  86. package/dist/{refund-reason-edit-CZ5QZ2SZ.mjs → refund-reason-edit-N2CRCLKZ.mjs} +1 -1
  87. package/dist/{refund-reason-list-OJYYEYJE.mjs → refund-reason-list-SE4TMGMT.mjs} +1 -1
  88. package/dist/{region-metadata-H6XXUQ4S.mjs → region-metadata-O5NZBWXP.mjs} +1 -1
  89. package/dist/{reservation-detail-LZAQL4XA.mjs → reservation-detail-UFK6XIXE.mjs} +1 -1
  90. package/dist/{reservation-metadata-5HZSDDOK.mjs → reservation-metadata-AEJEKGLV.mjs} +1 -1
  91. package/dist/{sales-channel-add-products-F7YV4MO5.mjs → sales-channel-add-products-2LMB7EF5.mjs} +2 -2
  92. package/dist/{sales-channel-detail-MXIPZCGA.mjs → sales-channel-detail-EUQ4STQI.mjs} +2 -2
  93. package/dist/{sales-channel-list-RLGL7FM3.mjs → sales-channel-list-JXKGHX4G.mjs} +1 -1
  94. package/dist/{sales-channel-metadata-M364R4RJ.mjs → sales-channel-metadata-AJMQ5SQ2.mjs} +1 -1
  95. package/dist/{shipping-option-type-create-C5WUWON7.mjs → shipping-option-type-create-YVVIA2XC.mjs} +1 -1
  96. package/dist/{shipping-option-type-detail-PENS2K73.mjs → shipping-option-type-detail-ZZW36XLK.mjs} +2 -2
  97. package/dist/{shipping-option-type-edit-CIU5EHRP.mjs → shipping-option-type-edit-O6F74T3A.mjs} +1 -1
  98. package/dist/{shipping-option-type-list-DIOX7VG7.mjs → shipping-option-type-list-SPTE7MT6.mjs} +2 -2
  99. package/dist/{shipping-profile-metadata-75G2NNMA.mjs → shipping-profile-metadata-7WFE55VG.mjs} +1 -1
  100. package/dist/store-add-locales-GWCGIXHU.mjs +81 -0
  101. package/dist/{store-detail-JSNPOB2F.mjs → store-detail-YLJLBBZE.mjs} +1 -1
  102. package/dist/{store-metadata-CYXTVJUE.mjs → store-metadata-BZ57I2E6.mjs} +1 -1
  103. package/dist/{tax-region-create-DWGL4EUT.mjs → tax-region-create-FGTV7VJL.mjs} +1 -1
  104. package/dist/{tax-region-detail-2AE2EFI3.mjs → tax-region-detail-PPIMD7OX.mjs} +5 -5
  105. package/dist/{tax-region-edit-EEVEEU2Q.mjs → tax-region-edit-ELZKA7YH.mjs} +1 -1
  106. package/dist/{tax-region-province-detail-4ERSEQFF.mjs → tax-region-province-detail-FV2NDT3E.mjs} +5 -5
  107. package/dist/{tax-region-tax-override-create-PHCGEF7V.mjs → tax-region-tax-override-create-N572MQPZ.mjs} +3 -3
  108. package/dist/{tax-region-tax-override-edit-SMRPSILC.mjs → tax-region-tax-override-edit-5DCSJW6D.mjs} +4 -4
  109. package/dist/{translation-list-UF7FLXOW.mjs → translation-list-FK7XYLHX.mjs} +227 -141
  110. package/dist/{translations-edit-USQJNMAY.mjs → translations-edit-VRXZI5KW.mjs} +224 -253
  111. package/dist/{user-metadata-2WPJOEJA.mjs → user-metadata-GRJZZ524.mjs} +1 -1
  112. package/dist/{workflow-execution-detail-H2AKEZJX.mjs → workflow-execution-detail-HXTFWGKG.mjs} +1 -1
  113. package/package.json +9 -9
  114. package/src/components/data-grid/hooks/use-data-grid-cell.tsx +1 -0
  115. package/src/components/data-grid/hooks/use-data-grid-form-handlers.tsx +1 -0
  116. package/src/components/data-grid/hooks/use-data-grid-keydown-event.tsx +22 -4
  117. package/src/dashboard-app/routes/get-route.map.tsx +4 -0
  118. package/src/hooks/api/translations.tsx +26 -17
  119. package/src/i18n/translations/en.json +1 -2
  120. package/src/i18n/translations/es.json +1 -1
  121. package/src/routes/translations/add-locales/add-locales.tsx +29 -0
  122. package/src/routes/translations/add-locales/index.tsx +1 -0
  123. package/src/routes/translations/translation-list/components/active-locales-section/active-locales-section.tsx +42 -17
  124. package/src/routes/translations/translation-list/components/translation-list-section/translation-list-section.tsx +5 -1
  125. package/src/routes/translations/translation-list/components/translations-completion-section/translations-completion-section.tsx +182 -121
  126. package/src/routes/translations/translations-edit/components/translations-edit-form/translations-edit-form.tsx +285 -330
@@ -18,38 +18,17 @@ import {
18
18
  import { KeyboundForm } from "../../../../../components/utilities/keybound-form"
19
19
  import { useBatchTranslations } from "../../../../../hooks/api/translations"
20
20
 
21
- /**
22
- * Schema for a single locale translation.
23
- */
24
- const LocaleTranslationSchema = z.object({
21
+ const EntityTranslationsSchema = z.object({
25
22
  id: z.string().nullish(),
26
- locale_code: z.string(),
27
23
  fields: z.record(z.string().optional()),
28
24
  })
29
- export type LocaleTranslationSchema = z.infer<typeof LocaleTranslationSchema>
30
-
31
- /**
32
- * Schema for an entity's translations (parent row in DataGrid).
33
- * Contains all locale translations for that entity.
34
- */
35
- const EntityTranslationsSchema = z.object({
36
- locales: z.record(LocaleTranslationSchema),
37
- })
38
25
  export type EntityTranslationsSchema = z.infer<typeof EntityTranslationsSchema>
39
26
 
40
- /**
41
- * Form schema
42
- * Maps each reference_id to their corresponding translations for each locale
43
- */
44
27
  export const TranslationsFormSchema = z.object({
45
28
  entities: z.record(EntityTranslationsSchema),
46
29
  })
47
30
  export type TranslationsFormSchema = z.infer<typeof TranslationsFormSchema>
48
31
 
49
- /**
50
- * Row types for the DataGrid.
51
- * Parent rows are entities, subrows are translatable fields.
52
- */
53
32
  export type TranslationRow = EntityRow | FieldRow
54
33
 
55
34
  export type EntityRow = {
@@ -72,150 +51,94 @@ export function isFieldRow(row: TranslationRow): row is FieldRow {
72
51
  return row._type === "field"
73
52
  }
74
53
 
75
- function initTranslationsFormState(
54
+ type LocaleSnapshot = {
55
+ localeCode: string
56
+ entities: Record<string, EntityTranslationsSchema>
57
+ }
58
+
59
+ function buildLocaleSnapshot(
76
60
  translations: HttpTypes.AdminTranslation[],
77
61
  references: { id: string; [key: string]: string }[],
78
- availableLocales: AdminStoreLocale[],
62
+ localeCode: string,
79
63
  translatableFields: string[]
80
- ): TranslationsFormSchema {
81
- const existingMap = new Map<string, HttpTypes.AdminTranslation>()
64
+ ): LocaleSnapshot {
65
+ const referenceTranslations = new Map<string, HttpTypes.AdminTranslation>()
82
66
  for (const t of translations) {
83
- existingMap.set(`${t.reference_id}:${t.locale_code}`, t)
67
+ if (t.locale_code === localeCode) {
68
+ referenceTranslations.set(t.reference_id, t)
69
+ }
84
70
  }
85
71
 
86
- const entitiesTranslationState: Record<string, EntityTranslationsSchema> = {}
87
-
88
- for (const reference of references) {
89
- const locales: Record<string, LocaleTranslationSchema> = {}
90
-
91
- for (const locale of availableLocales) {
92
- const key = `${reference.id}:${locale.locale_code}`
93
- const existing = existingMap.get(key)
94
-
95
- const fields: Record<string, string> = {}
96
- for (const fieldName of translatableFields) {
97
- const fieldValue = (existing?.translations?.[fieldName] as string) ?? ""
98
- fields[fieldName] = fieldValue
99
- }
72
+ const entities: Record<string, EntityTranslationsSchema> = {}
73
+ for (const ref of references) {
74
+ const existing = referenceTranslations.get(ref.id)
75
+ const fields: Record<string, string> = {}
100
76
 
101
- locales[locale.locale_code] = {
102
- id: existing?.id ?? null,
103
- locale_code: locale.locale_code,
104
- fields,
105
- }
77
+ for (const fieldName of translatableFields) {
78
+ fields[fieldName] = (existing?.translations?.[fieldName] as string) ?? ""
106
79
  }
107
80
 
108
- entitiesTranslationState[reference.id] = { locales }
81
+ entities[ref.id] = {
82
+ id: existing?.id ?? null,
83
+ fields,
84
+ }
109
85
  }
110
86
 
111
- return {
112
- entities: entitiesTranslationState,
113
- }
87
+ return { localeCode, entities }
114
88
  }
115
89
 
116
- function buildTranslationRows(
117
- references: { id: string; [key: string]: string }[],
90
+ function extendSnapshot(
91
+ snapshot: LocaleSnapshot,
92
+ translations: HttpTypes.AdminTranslation[],
93
+ newReferences: { id: string; [key: string]: string }[],
118
94
  translatableFields: string[]
119
- ): TranslationRow[] {
120
- return references.map((reference) => ({
121
- _type: "entity" as const,
122
- reference_id: reference.id,
123
- subRows: translatableFields.map((fieldName) => ({
124
- _type: "field" as const,
125
- reference_id: reference.id,
126
- field_name: fieldName,
127
- })),
128
- }))
129
- }
130
-
131
- function transformToBatchPayload(
132
- currentState: TranslationsFormSchema,
133
- initialState: TranslationsFormSchema,
134
- entityType: string
135
- ): Required<HttpTypes.AdminBatchTranslations> {
136
- const payload: Required<HttpTypes.AdminBatchTranslations> = {
137
- create: [],
138
- update: [],
139
- delete: [],
140
- }
141
-
142
- for (const [entityId, entityData] of Object.entries(currentState.entities)) {
143
- for (const [localeCode, localeTranslations] of Object.entries(
144
- entityData.locales
145
- )) {
146
- const initial = initialState.entities[entityId]?.locales[localeCode]
147
- const hasContent = Object.values(localeTranslations.fields).some(
148
- (v) => v !== undefined && v.trim() !== ""
149
- )
150
- const hadContent =
151
- initial &&
152
- Object.values(initial.fields).some(
153
- (v) => v !== undefined && v.trim() !== ""
154
- )
155
-
156
- if (!localeTranslations.id && hasContent) {
157
- payload.create.push({
158
- reference_id: entityId,
159
- reference: entityType,
160
- locale_code: localeTranslations.locale_code,
161
- translations: localeTranslations.fields,
162
- })
163
- } else if (localeTranslations.id && hasContent) {
164
- // UPDATE: Has ID and has content - check if changed
165
- const hasChanged =
166
- !initial ||
167
- JSON.stringify(localeTranslations.fields) !==
168
- JSON.stringify(initial.fields)
169
-
170
- if (hasChanged) {
171
- payload.update.push({
172
- id: localeTranslations.id,
173
- translations: localeTranslations.fields,
174
- })
175
- }
176
- } else if (localeTranslations.id && !hasContent && hadContent) {
177
- payload.delete.push(localeTranslations.id)
178
- }
95
+ ): LocaleSnapshot {
96
+ const referenceTranslations = new Map<string, HttpTypes.AdminTranslation>()
97
+ for (const t of translations) {
98
+ if (t.locale_code === snapshot.localeCode) {
99
+ referenceTranslations.set(t.reference_id, t)
179
100
  }
180
101
  }
181
102
 
182
- return payload
183
- }
184
-
185
- function hasLocaleChanges(
186
- currentState: TranslationsFormSchema,
187
- initialState: TranslationsFormSchema,
188
- localeCode: string
189
- ): boolean {
190
- for (const [entityId, entityData] of Object.entries(currentState.entities)) {
191
- const currentLocale = entityData.locales[localeCode]
192
- const initialLocale = initialState.entities[entityId]?.locales[localeCode]
103
+ const extendedEntities = { ...snapshot.entities }
193
104
 
194
- if (!currentLocale || !initialLocale) {
195
- continue
196
- }
105
+ for (const ref of newReferences) {
106
+ if (!extendedEntities[ref.id]) {
107
+ const existing = referenceTranslations.get(ref.id)
108
+ const fields: Record<string, string> = {}
197
109
 
198
- for (const [fieldName, fieldValue] of Object.entries(
199
- currentLocale.fields
200
- )) {
201
- const initialValue = initialLocale.fields[fieldName] ?? ""
202
- const currentValue = fieldValue ?? ""
110
+ for (const fieldName of translatableFields) {
111
+ fields[fieldName] =
112
+ (existing?.translations?.[fieldName] as string) ?? ""
113
+ }
203
114
 
204
- if (currentValue !== initialValue) {
205
- return true
115
+ extendedEntities[ref.id] = {
116
+ id: existing?.id ?? null,
117
+ fields,
206
118
  }
207
119
  }
208
120
  }
209
121
 
210
- return false
122
+ return { ...snapshot, entities: extendedEntities }
123
+ }
124
+
125
+ function snapshotToFormValues(
126
+ snapshot: LocaleSnapshot
127
+ ): TranslationsFormSchema {
128
+ return { entities: snapshot.entities }
211
129
  }
212
130
 
213
- function transformSingleLocaleToBatchPayload(
131
+ type ChangeDetectionResult = {
132
+ hasChanges: boolean
133
+ payload: Required<HttpTypes.AdminBatchTranslations>
134
+ }
135
+
136
+ function computeChanges(
214
137
  currentState: TranslationsFormSchema,
215
- initialState: TranslationsFormSchema,
138
+ snapshot: LocaleSnapshot,
216
139
  entityType: string,
217
140
  localeCode: string
218
- ): Required<HttpTypes.AdminBatchTranslations> {
141
+ ): ChangeDetectionResult {
219
142
  const payload: Required<HttpTypes.AdminBatchTranslations> = {
220
143
  create: [],
221
144
  update: [],
@@ -223,44 +146,43 @@ function transformSingleLocaleToBatchPayload(
223
146
  }
224
147
 
225
148
  for (const [entityId, entityData] of Object.entries(currentState.entities)) {
226
- const localeTranslations = entityData.locales[localeCode]
227
- if (!localeTranslations) continue
149
+ const baseline = snapshot.entities[entityId]
150
+ if (!baseline) {
151
+ continue
152
+ }
228
153
 
229
- const initial = initialState.entities[entityId]?.locales[localeCode]
230
- const hasContent = Object.values(localeTranslations.fields).some(
154
+ const hasContent = Object.values(entityData.fields).some(
231
155
  (v) => v !== undefined && v.trim() !== ""
232
156
  )
233
- const hadContent =
234
- initial &&
235
- Object.values(initial.fields).some(
236
- (v) => v !== undefined && v.trim() !== ""
237
- )
157
+ const hadContent = Object.values(baseline.fields).some(
158
+ (v) => v !== undefined && v.trim() !== ""
159
+ )
160
+ const hasChanged =
161
+ JSON.stringify(entityData.fields) !== JSON.stringify(baseline.fields)
238
162
 
239
- if (!localeTranslations.id && hasContent) {
163
+ if (!entityData.id && hasContent) {
240
164
  payload.create.push({
241
165
  reference_id: entityId,
242
166
  reference: entityType,
243
- locale_code: localeTranslations.locale_code,
244
- translations: localeTranslations.fields,
167
+ locale_code: localeCode,
168
+ translations: entityData.fields,
245
169
  })
246
- } else if (localeTranslations.id && hasContent) {
247
- const hasChanged =
248
- !initial ||
249
- JSON.stringify(localeTranslations.fields) !==
250
- JSON.stringify(initial.fields)
251
-
252
- if (hasChanged) {
253
- payload.update.push({
254
- id: localeTranslations.id,
255
- translations: localeTranslations.fields,
256
- })
257
- }
258
- } else if (localeTranslations.id && !hasContent && hadContent) {
259
- payload.delete.push(localeTranslations.id)
170
+ } else if (entityData.id && hasContent && hasChanged) {
171
+ payload.update.push({
172
+ id: entityData.id,
173
+ translations: entityData.fields,
174
+ })
175
+ } else if (entityData.id && !hasContent && hadContent) {
176
+ payload.delete.push(entityData.id)
260
177
  }
261
178
  }
262
179
 
263
- return payload
180
+ const hasChanges =
181
+ payload.create.length > 0 ||
182
+ payload.update.length > 0 ||
183
+ payload.delete.length > 0
184
+
185
+ return { hasChanges, payload }
264
186
  }
265
187
 
266
188
  const columnHelper = createDataGridHelper<
@@ -268,29 +190,42 @@ const columnHelper = createDataGridHelper<
268
190
  TranslationsFormSchema
269
191
  >()
270
192
 
271
- const FIELD_COLUMN_WIDTH = 150
193
+ const FIELD_COLUMN_WIDTH = 350
194
+
195
+ function buildTranslationRows(
196
+ references: { id: string; [key: string]: string }[],
197
+ translatableFields: string[]
198
+ ): TranslationRow[] {
199
+ return references.map((reference) => ({
200
+ _type: "entity" as const,
201
+ reference_id: reference.id,
202
+ subRows: translatableFields.map((fieldName) => ({
203
+ _type: "field" as const,
204
+ reference_id: reference.id,
205
+ field_name: fieldName,
206
+ })),
207
+ }))
208
+ }
272
209
 
273
210
  function useTranslationsGridColumns({
274
211
  entities,
275
- translatableFields,
276
212
  availableLocales,
277
213
  selectedLocale,
278
214
  dynamicColumnWidth,
279
215
  }: {
280
216
  entities: { id: string; [key: string]: string }[]
281
- translatableFields: string[]
282
217
  availableLocales: AdminStoreLocale[]
283
218
  selectedLocale: string
284
219
  dynamicColumnWidth: number
285
220
  }) {
286
221
  const { t } = useTranslation()
287
222
 
288
- const columns: ColumnDef<TranslationRow>[] = useMemo(() => {
223
+ return useMemo(() => {
289
224
  const selectedLocaleData = availableLocales.find(
290
225
  (l) => l.locale_code === selectedLocale
291
226
  )
292
227
 
293
- const baseColumns = [
228
+ const columns: ColumnDef<TranslationRow>[] = [
294
229
  columnHelper.column({
295
230
  id: "field",
296
231
  name: "field",
@@ -300,9 +235,7 @@ function useTranslationsGridColumns({
300
235
  const row = context.row.original
301
236
 
302
237
  if (isEntityRow(row)) {
303
- return (
304
- <DataGrid.ReadonlyCell context={context}></DataGrid.ReadonlyCell>
305
- )
238
+ return <DataGrid.ReadonlyCell context={context} />
306
239
  }
307
240
 
308
241
  return (
@@ -337,14 +270,10 @@ function useTranslationsGridColumns({
337
270
  const row = context.row.original
338
271
 
339
272
  if (isEntityRow(row)) {
340
- return (
341
- <DataGrid.ReadonlyCell context={context}></DataGrid.ReadonlyCell>
342
- )
273
+ return <DataGrid.ReadonlyCell context={context} />
343
274
  }
344
275
 
345
- const entity = entities.find(
346
- (entity) => entity.id === row.reference_id
347
- )
276
+ const entity = entities.find((e) => e.id === row.reference_id)
348
277
  if (!entity) {
349
278
  return null
350
279
  }
@@ -361,7 +290,7 @@ function useTranslationsGridColumns({
361
290
  ]
362
291
 
363
292
  if (selectedLocaleData) {
364
- baseColumns.push(
293
+ columns.push(
365
294
  columnHelper.column({
366
295
  id: selectedLocaleData.locale_code,
367
296
  name: selectedLocaleData.locale.name,
@@ -387,24 +316,15 @@ function useTranslationsGridColumns({
387
316
  return null
388
317
  }
389
318
 
390
- return `entities.${row.reference_id}.locales.${selectedLocaleData.locale_code}.fields.${row.field_name}`
319
+ return `entities.${row.reference_id}.fields.${row.field_name}`
391
320
  },
392
321
  type: "multiline-text",
393
322
  })
394
323
  )
395
324
  }
396
325
 
397
- return baseColumns
398
- }, [
399
- t,
400
- translatableFields,
401
- availableLocales,
402
- selectedLocale,
403
- entities,
404
- dynamicColumnWidth,
405
- ])
406
-
407
- return columns
326
+ return columns
327
+ }, [t, availableLocales, selectedLocale, entities, dynamicColumnWidth])
408
328
  }
409
329
 
410
330
  type TranslationsEditFormProps = {
@@ -459,74 +379,105 @@ export const TranslationsEditForm = ({
459
379
  const [selectedLocale, setSelectedLocale] = useState<string>(
460
380
  availableLocales[0]?.locale_code ?? ""
461
381
  )
462
-
463
382
  const [showUnsavedPrompt, setShowUnsavedPrompt] = useState(false)
464
383
  const [pendingLocale, setPendingLocale] = useState<string | null>(null)
384
+ const [isSwitchingLocale, setIsSwitchingLocale] = useState(false)
465
385
 
466
- const entities = useMemo(() => references, [references])
467
- const totalCount = useMemo(
468
- () => referenceCount * (translatableFields.length + 1),
469
- [referenceCount, translatableFields]
470
- )
471
-
472
- const initialState = useRef(
473
- initTranslationsFormState(
386
+ const snapshotRef = useRef<LocaleSnapshot>(
387
+ buildLocaleSnapshot(
474
388
  translations,
475
- entities,
476
- availableLocales,
389
+ references,
390
+ selectedLocale,
477
391
  translatableFields
478
392
  )
479
393
  )
480
394
 
395
+ const knownEntityIdsRef = useRef<Set<string>>(
396
+ new Set(references.map((r) => r.id))
397
+ )
398
+
399
+ const latestPropsRef = useRef({ translations, references })
400
+ useEffect(() => {
401
+ latestPropsRef.current = { translations, references }
402
+ }, [translations, references])
403
+
481
404
  const form = useForm<TranslationsFormSchema>({
482
405
  resolver: zodResolver(TranslationsFormSchema),
483
- defaultValues: initialState.current,
406
+ defaultValues: snapshotToFormValues(snapshotRef.current),
484
407
  })
485
408
 
486
- const rows = useMemo(
487
- () => buildTranslationRows(entities, translatableFields),
488
- [entities, translatableFields]
489
- )
409
+ useEffect(() => {
410
+ const currentIds = new Set(references.map((r) => r.id))
411
+ const newReferences = references.filter(
412
+ (r) => !knownEntityIdsRef.current.has(r.id)
413
+ )
490
414
 
491
- const { mutateAsync, isPending } = useBatchTranslations(entityType)
415
+ if (newReferences.length === 0) {
416
+ return
417
+ }
492
418
 
493
- const handleLocaleChange = useCallback(
494
- (newLocale: string) => {
495
- if (newLocale === selectedLocale) {
496
- return
497
- }
419
+ knownEntityIdsRef.current = currentIds
420
+ snapshotRef.current = extendSnapshot(
421
+ snapshotRef.current,
422
+ translations,
423
+ newReferences,
424
+ translatableFields
425
+ )
498
426
 
499
- const currentValues = form.getValues()
500
- const hasChanges = hasLocaleChanges(
501
- currentValues,
502
- initialState.current,
503
- selectedLocale
504
- )
427
+ const currentValues = form.getValues()
428
+ const newFormValues: TranslationsFormSchema = {
429
+ entities: { ...currentValues.entities },
430
+ }
505
431
 
506
- if (hasChanges) {
507
- setPendingLocale(newLocale)
508
- setShowUnsavedPrompt(true)
509
- } else {
510
- setSelectedLocale(newLocale)
432
+ for (const ref of newReferences) {
433
+ if (!newFormValues.entities[ref.id]) {
434
+ newFormValues.entities[ref.id] = snapshotRef.current.entities[ref.id]
511
435
  }
512
- },
513
- [selectedLocale, form]
436
+ }
437
+
438
+ form.reset(newFormValues, {
439
+ keepDirty: true,
440
+ keepDirtyValues: true,
441
+ })
442
+ }, [references, translations, translatableFields, form])
443
+
444
+ const rows = useMemo(
445
+ () => buildTranslationRows(references, translatableFields),
446
+ [references, translatableFields]
514
447
  )
515
448
 
449
+ const totalRowCount = useMemo(
450
+ () => referenceCount * (translatableFields.length + 1),
451
+ [referenceCount, translatableFields]
452
+ )
453
+
454
+ const selectedLocaleDisplay = useMemo(
455
+ () =>
456
+ availableLocales.find((l) => l.locale_code === selectedLocale)?.locale
457
+ .name,
458
+ [availableLocales, selectedLocale]
459
+ )
460
+
461
+ const columns = useTranslationsGridColumns({
462
+ entities: references,
463
+ availableLocales,
464
+ selectedLocale,
465
+ dynamicColumnWidth,
466
+ })
467
+
468
+ const { mutateAsync, isPending, invalidateQueries } =
469
+ useBatchTranslations(entityType)
470
+
516
471
  const saveCurrentLocale = useCallback(async () => {
517
472
  const currentValues = form.getValues()
518
- const payload = transformSingleLocaleToBatchPayload(
473
+ const { hasChanges, payload } = computeChanges(
519
474
  currentValues,
520
- initialState.current,
475
+ snapshotRef.current,
521
476
  entityType,
522
477
  selectedLocale
523
478
  )
524
479
 
525
- if (
526
- payload.create.length === 0 &&
527
- payload.update.length === 0 &&
528
- payload.delete.length === 0
529
- ) {
480
+ if (!hasChanges) {
530
481
  return true
531
482
  }
532
483
 
@@ -538,11 +489,13 @@ export const TranslationsEditForm = ({
538
489
 
539
490
  for (let i = 0; i < batchCount; i++) {
540
491
  let currentBatchAvailable = BATCH_SIZE
492
+
541
493
  const currentBatch: HttpTypes.AdminBatchTranslations = {
542
494
  create: [],
543
495
  update: [],
544
496
  delete: [],
545
497
  }
498
+
546
499
  if (payload.create.length > 0) {
547
500
  currentBatch.create = payload.create.splice(0, currentBatchAvailable)
548
501
  currentBatchAvailable -= currentBatch.create.length
@@ -553,22 +506,36 @@ export const TranslationsEditForm = ({
553
506
  }
554
507
  if (payload.delete.length > 0) {
555
508
  currentBatch.delete = payload.delete.splice(0, currentBatchAvailable)
556
- currentBatchAvailable -= currentBatch.delete.length
557
509
  }
558
510
 
559
- await mutateAsync(currentBatch)
511
+ const response = await mutateAsync(currentBatch, {
512
+ onError: (error) => {
513
+ toast.error(error.message)
514
+ },
515
+ })
516
+
517
+ if (response.created) {
518
+ for (const created of response.created) {
519
+ form.setValue(`entities.${created.reference_id}.id`, created.id, {
520
+ shouldDirty: false,
521
+ })
522
+ if (snapshotRef.current.entities[created.reference_id]) {
523
+ snapshotRef.current.entities[created.reference_id].id = created.id
524
+ }
525
+ }
526
+ }
560
527
  }
561
528
 
562
- const updatedInitialState = { ...initialState.current }
563
- for (const entityId of Object.keys(currentValues.entities)) {
564
- if (updatedInitialState.entities[entityId]?.locales[selectedLocale]) {
565
- updatedInitialState.entities[entityId].locales[selectedLocale] = {
566
- ...currentValues.entities[entityId].locales[selectedLocale],
529
+ const savedValues = form.getValues()
530
+ for (const entityId of Object.keys(savedValues.entities)) {
531
+ if (snapshotRef.current.entities[entityId]) {
532
+ snapshotRef.current.entities[entityId] = {
533
+ ...savedValues.entities[entityId],
567
534
  }
568
535
  }
569
536
  }
570
- initialState.current = updatedInitialState
571
- form.reset(currentValues)
537
+
538
+ form.reset(savedValues)
572
539
 
573
540
  return true
574
541
  } catch (error) {
@@ -579,19 +546,70 @@ export const TranslationsEditForm = ({
579
546
  }
580
547
  }, [form, entityType, selectedLocale, mutateAsync])
581
548
 
549
+ const switchToLocale = useCallback(
550
+ async (newLocale: string) => {
551
+ setIsSwitchingLocale(true)
552
+
553
+ try {
554
+ await invalidateQueries()
555
+
556
+ await new Promise((resolve) => requestAnimationFrame(resolve))
557
+
558
+ const { translations, references } = latestPropsRef.current
559
+
560
+ const newSnapshot = buildLocaleSnapshot(
561
+ translations,
562
+ references,
563
+ newLocale,
564
+ translatableFields
565
+ )
566
+
567
+ snapshotRef.current = newSnapshot
568
+ knownEntityIdsRef.current = new Set(references.map((r) => r.id))
569
+
570
+ form.reset(snapshotToFormValues(newSnapshot))
571
+
572
+ setSelectedLocale(newLocale)
573
+ } finally {
574
+ setIsSwitchingLocale(false)
575
+ }
576
+ },
577
+ [translatableFields, form, invalidateQueries]
578
+ )
579
+
580
+ const handleLocaleChange = useCallback(
581
+ (newLocale: string) => {
582
+ if (newLocale === selectedLocale) {
583
+ return
584
+ }
585
+
586
+ const currentValues = form.getValues()
587
+ const { hasChanges } = computeChanges(
588
+ currentValues,
589
+ snapshotRef.current,
590
+ entityType,
591
+ selectedLocale
592
+ )
593
+
594
+ if (hasChanges) {
595
+ setPendingLocale(newLocale)
596
+ setShowUnsavedPrompt(true)
597
+ } else {
598
+ switchToLocale(newLocale)
599
+ }
600
+ },
601
+ [selectedLocale, form, entityType, switchToLocale]
602
+ )
603
+
582
604
  const handleSaveAndSwitch = useCallback(async () => {
583
605
  const success = await saveCurrentLocale()
584
606
  if (success && pendingLocale) {
585
- toast.success(
586
- t("translations.edit.localeChangesSaved", {
587
- defaultValue: "Changes saved successfully",
588
- })
589
- )
590
- setSelectedLocale(pendingLocale)
607
+ toast.success(t("translations.edit.successToast"))
608
+ await switchToLocale(pendingLocale)
591
609
  }
592
610
  setShowUnsavedPrompt(false)
593
611
  setPendingLocale(null)
594
- }, [saveCurrentLocale, pendingLocale, t])
612
+ }, [saveCurrentLocale, pendingLocale, t, switchToLocale])
595
613
 
596
614
  const handleCancelSwitch = useCallback(() => {
597
615
  setShowUnsavedPrompt(false)
@@ -611,83 +629,19 @@ export const TranslationsEditForm = ({
611
629
  [saveCurrentLocale, t, handleSuccess]
612
630
  )
613
631
 
614
- const handleSubmit = form.handleSubmit(async (values) => {
615
- const payload = transformToBatchPayload(
616
- values,
617
- initialState.current,
618
- entityType
619
- )
632
+ const handleClose = useCallback(() => {
633
+ invalidateQueries()
634
+ }, [invalidateQueries])
620
635
 
621
- if (
622
- payload.create.length === 0 &&
623
- payload.update.length === 0 &&
624
- payload.delete.length === 0
625
- ) {
626
- return
627
- }
628
-
629
- const BATCH_SIZE = 150
630
- const totalItems =
631
- payload.create.length + payload.update.length + payload.delete.length
632
- const batchCount = Math.ceil(totalItems / BATCH_SIZE)
633
-
634
- for (let i = 0; i < batchCount; i++) {
635
- let currentBatchAvailable = BATCH_SIZE
636
- const currentBatch: HttpTypes.AdminBatchTranslations = {
637
- create: [],
638
- update: [],
639
- delete: [],
640
- }
641
- if (payload.create.length > 0) {
642
- currentBatch.create = payload.create.splice(0, currentBatchAvailable)
643
- currentBatchAvailable -= currentBatch.create.length
644
- }
645
- if (payload.update.length > 0) {
646
- currentBatch.update = payload.update.splice(0, currentBatchAvailable)
647
- currentBatchAvailable -= currentBatch.update.length
648
- }
649
- if (payload.delete.length > 0) {
650
- currentBatch.delete = payload.delete.splice(0, currentBatchAvailable)
651
- currentBatchAvailable -= currentBatch.delete.length
652
- }
653
-
654
- await mutateAsync(currentBatch, {
655
- onSuccess: () => {
656
- if (i === batchCount - 1) {
657
- toast.success(
658
- t("translations.edit.successToast", {
659
- defaultValue: "Translations updated successfully",
660
- })
661
- )
662
- handleSuccess()
663
- }
664
- },
665
- onError: (error) => {
666
- toast.error(error.message)
667
- },
668
- })
669
- }
670
- })
671
-
672
- const columns = useTranslationsGridColumns({
673
- entities,
674
- translatableFields,
675
- availableLocales,
676
- selectedLocale,
677
- dynamicColumnWidth,
678
- })
679
-
680
- const selectedLocaleDisplay = availableLocales.find(
681
- (l) => l.locale_code === selectedLocale
682
- )?.locale.name
636
+ const isLoading = isPending || isSwitchingLocale
683
637
 
684
638
  return (
685
- <RouteFocusModal.Form form={form}>
639
+ <RouteFocusModal.Form form={form} onClose={handleClose}>
686
640
  <KeyboundForm
687
- onSubmit={handleSubmit}
641
+ onSubmit={() => handleSave(true)}
688
642
  className="flex h-full flex-col overflow-hidden"
689
643
  >
690
- <RouteFocusModal.Header></RouteFocusModal.Header>
644
+ <RouteFocusModal.Header />
691
645
  <RouteFocusModal.Body className="size-full overflow-hidden">
692
646
  <div ref={containerRef} className="size-full">
693
647
  <DataGrid
@@ -701,12 +655,13 @@ export const TranslationsEditForm = ({
701
655
  }}
702
656
  state={form}
703
657
  onEditingChange={(editing) => setCloseOnEscape(!editing)}
704
- totalRowCount={totalCount}
658
+ totalRowCount={totalRowCount}
705
659
  onFetchMore={fetchNextPage}
706
660
  isFetchingMore={isFetchingNextPage}
707
661
  hasNextPage={hasNextPage}
708
662
  headerContent={
709
663
  <Select
664
+ disabled={isLoading}
710
665
  value={selectedLocale}
711
666
  onValueChange={handleLocaleChange}
712
667
  size="small"
@@ -732,7 +687,12 @@ export const TranslationsEditForm = ({
732
687
  <RouteFocusModal.Footer>
733
688
  <div className="flex items-center justify-end gap-x-2">
734
689
  <RouteFocusModal.Close asChild>
735
- <Button size="small" variant="secondary">
690
+ <Button
691
+ type="button"
692
+ size="small"
693
+ variant="secondary"
694
+ isLoading={isLoading}
695
+ >
736
696
  {t("actions.cancel")}
737
697
  </Button>
738
698
  </RouteFocusModal.Close>
@@ -741,16 +701,11 @@ export const TranslationsEditForm = ({
741
701
  type="button"
742
702
  variant="secondary"
743
703
  onClick={() => handleSave(false)}
744
- isLoading={isPending}
704
+ isLoading={isLoading}
745
705
  >
746
706
  {t("actions.saveChanges")}
747
707
  </Button>
748
- <Button
749
- size="small"
750
- type="button"
751
- onClick={() => handleSave(true)}
752
- isLoading={isPending}
753
- >
708
+ <Button size="small" type="submit" isLoading={isLoading}>
754
709
  {t("actions.saveAndClose")}
755
710
  </Button>
756
711
  </div>
@@ -780,7 +735,7 @@ export const TranslationsEditForm = ({
780
735
  size="small"
781
736
  onClick={handleSaveAndSwitch}
782
737
  type="button"
783
- isLoading={isPending}
738
+ isLoading={isLoading}
784
739
  >
785
740
  {t("actions.saveChanges")}
786
741
  </Button>