@medusajs/dashboard 3.0.0-snapshot-20251216103925 → 3.0.0-snapshot-20251216135612

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 (125) hide show
  1. package/dist/{add-campaign-promotions-PHAHMGXP.mjs → add-campaign-promotions-OYPGISTF.mjs} +4 -4
  2. package/dist/{api-key-management-detail-NRGL7HHX.mjs → api-key-management-detail-6RCDH73M.mjs} +5 -5
  3. package/dist/{api-key-management-list-LJSWRAGE.mjs → api-key-management-list-KC5GOWAU.mjs} +5 -5
  4. package/dist/{api-key-management-sales-channels-3GRNDBZ4.mjs → api-key-management-sales-channels-LUB5G6RC.mjs} +5 -5
  5. package/dist/app.js +3752 -133
  6. package/dist/app.mjs +2 -2
  7. package/dist/{campaign-detail-22Q6XWGL.mjs → campaign-detail-5Q4BYCPX.mjs} +5 -5
  8. package/dist/{campaign-list-OH566KWW.mjs → campaign-list-PEOTTWBA.mjs} +6 -6
  9. package/dist/{category-detail-RRKJPMEG.mjs → category-detail-S5IPXMHX.mjs} +4 -4
  10. package/dist/{category-list-4HZP4FRE.mjs → category-list-QBYJ4T3R.mjs} +4 -4
  11. package/dist/{category-products-EFZRTCSF.mjs → category-products-KPW6BA5J.mjs} +4 -4
  12. package/dist/{chunk-LCEKY54O.mjs → chunk-27MGH3HR.mjs} +1 -1
  13. package/dist/{chunk-KQ4LC2YV.mjs → chunk-2DULKOPN.mjs} +1 -1
  14. package/dist/{chunk-SUYYSKCB.mjs → chunk-535OVBXR.mjs} +8 -1
  15. package/dist/{chunk-32FPYJ3S.mjs → chunk-AHZLMCZF.mjs} +1 -1
  16. package/dist/{chunk-5IFK2ZTA.mjs → chunk-APGDAT7X.mjs} +2 -2
  17. package/dist/{chunk-2X25XWOW.mjs → chunk-BTYBTKWK.mjs} +1 -1
  18. package/dist/{chunk-DNUVCBN7.mjs → chunk-CCQD65EY.mjs} +160 -58
  19. package/dist/{chunk-RL5EYTP6.mjs → chunk-CVHJAKLQ.mjs} +1 -1
  20. package/dist/{chunk-DZWH2RV6.mjs → chunk-DFFLVEZ5.mjs} +1 -1
  21. package/dist/{chunk-YTN73JYH.mjs → chunk-DTZXEQXZ.mjs} +1 -1
  22. package/dist/{chunk-7R743WZ6.mjs → chunk-EHU67PIM.mjs} +1 -1
  23. package/dist/{chunk-VX2HE7O5.mjs → chunk-FBYTX6K7.mjs} +1 -1
  24. package/dist/{chunk-RMDYKRWW.mjs → chunk-KFYQTOGB.mjs} +1 -1
  25. package/dist/{chunk-GA3UMQMZ.mjs → chunk-O7JNIATG.mjs} +1 -1
  26. package/dist/{chunk-OHY2N2Q4.mjs → chunk-OL24RDYM.mjs} +3 -3
  27. package/dist/{chunk-M4Q5Q4I3.mjs → chunk-PA3T6IWL.mjs} +2 -2
  28. package/dist/{chunk-NL376WAO.mjs → chunk-QISH26J3.mjs} +1 -1
  29. package/dist/{chunk-PVW6M64S.mjs → chunk-QOCJPBRB.mjs} +1 -1
  30. package/dist/{chunk-2GWGNMAA.mjs → chunk-R4ZOO4ON.mjs} +1 -1
  31. package/dist/{chunk-43X7ZR3P.mjs → chunk-RS7DWLEP.mjs} +1 -1
  32. package/dist/{chunk-ROKB75YP.mjs → chunk-VT2JJ5C2.mjs} +1 -1
  33. package/dist/{chunk-DK7IWUMK.mjs → chunk-WYATCUOM.mjs} +3 -3
  34. package/dist/{chunk-CV65NY6Y.mjs → chunk-YFIYCS7F.mjs} +1 -1
  35. package/dist/{chunk-O7WJSSQR.mjs → chunk-YKYVCQRS.mjs} +3346 -93
  36. package/dist/{chunk-GU5PJRPM.mjs → chunk-ZQJPHZKI.mjs} +1 -1
  37. package/dist/{collection-add-products-XUMV6XR7.mjs → collection-add-products-FU2BS3D3.mjs} +4 -4
  38. package/dist/{collection-detail-ONRBKJLN.mjs → collection-detail-VJE7XHLV.mjs} +4 -4
  39. package/dist/{collection-list-XCC4SIPJ.mjs → collection-list-IGA6SCNF.mjs} +4 -4
  40. package/dist/{customer-detail-FR6J37ZC.mjs → customer-detail-MOV2T3LF.mjs} +6 -6
  41. package/dist/{customer-group-add-customers-DM4VLTNB.mjs → customer-group-add-customers-XMR2WBXX.mjs} +6 -6
  42. package/dist/{customer-group-detail-YSKSNETG.mjs → customer-group-detail-6T7OXGQD.mjs} +6 -6
  43. package/dist/{customer-group-list-XBCD4FZH.mjs → customer-group-list-AJEAF5D2.mjs} +3 -3
  44. package/dist/{customer-list-TG4D4QOT.mjs → customer-list-UI5EQDII.mjs} +6 -6
  45. package/dist/{customers-add-customer-group-Q7FMR2Y5.mjs → customers-add-customer-group-QVTVSQYM.mjs} +4 -4
  46. package/dist/{inventory-create-3XONKYMZ.mjs → inventory-create-ANYUM4P5.mjs} +1 -1
  47. package/dist/{inventory-detail-6A6GOLB6.mjs → inventory-detail-ZPSEMYI2.mjs} +4 -4
  48. package/dist/{inventory-list-2CJLAK3X.mjs → inventory-list-RXJPSVZE.mjs} +4 -4
  49. package/dist/{inventory-stock-S3ZYYCMZ.mjs → inventory-stock-FD4ZM4BB.mjs} +2 -2
  50. package/dist/{location-fulfillment-providers-WM6DT252.mjs → location-fulfillment-providers-7ZUJAGNY.mjs} +4 -4
  51. package/dist/{location-sales-channels-WLVTMU4Z.mjs → location-sales-channels-P3QJTFDT.mjs} +5 -5
  52. package/dist/{location-service-zone-create-3FWF3DG5.mjs → location-service-zone-create-J43WN6G4.mjs} +5 -5
  53. package/dist/{location-service-zone-manage-areas-YFEAZUUN.mjs → location-service-zone-manage-areas-6ZPMKMSX.mjs} +5 -5
  54. package/dist/{location-service-zone-shipping-option-create-MJPH3WKX.mjs → location-service-zone-shipping-option-create-ZJ4GIBTJ.mjs} +2 -2
  55. package/dist/{location-service-zone-shipping-option-pricing-6IRNPWJY.mjs → location-service-zone-shipping-option-pricing-CR4BVYG3.mjs} +2 -2
  56. package/dist/{order-create-claim-GUYTLVPB.mjs → order-create-claim-SCDJGM46.mjs} +4 -4
  57. package/dist/{order-create-edit-ODIN6GRW.mjs → order-create-edit-JIE3HDHP.mjs} +4 -4
  58. package/dist/{order-create-exchange-ZT5RBRKL.mjs → order-create-exchange-LQU4YN7F.mjs} +4 -4
  59. package/dist/{order-create-return-E2KILJX2.mjs → order-create-return-52GHGW5Z.mjs} +4 -4
  60. package/dist/{order-detail-HFJONELJ.mjs → order-detail-PVPGEWGY.mjs} +2 -2
  61. package/dist/{order-export-4MZUPMGD.mjs → order-export-LE363ZLB.mjs} +3 -3
  62. package/dist/{order-list-ACSFGIPD.mjs → order-list-GRLQWN4L.mjs} +7 -7
  63. package/dist/{price-list-configuration-IHPSUNZJ.mjs → price-list-configuration-6S3MLNXQ.mjs} +5 -5
  64. package/dist/{price-list-create-YHXRQSC3.mjs → price-list-create-MFRUQADC.mjs} +7 -7
  65. package/dist/{price-list-detail-FR3FQR3H.mjs → price-list-detail-Q5VG5VGW.mjs} +5 -5
  66. package/dist/{price-list-list-YSEM6IAI.mjs → price-list-list-DG5YEZ44.mjs} +4 -4
  67. package/dist/{price-list-prices-add-GJVI47OY.mjs → price-list-prices-add-SDU5YZAT.mjs} +6 -6
  68. package/dist/{price-list-prices-edit-E4Q5TQPM.mjs → price-list-prices-edit-5USQR4D4.mjs} +2 -2
  69. package/dist/{product-attributes-QD3BWV5V.mjs → product-attributes-EFIRUBRO.mjs} +2 -2
  70. package/dist/{product-create-E2GZYQX4.mjs → product-create-K6EWZHIT.mjs} +7 -7
  71. package/dist/{product-create-variant-KEBN5OR7.mjs → product-create-variant-ERKHTEJZ.mjs} +1 -1
  72. package/dist/{product-detail-QBGGKRZ2.mjs → product-detail-DKPZDEIY.mjs} +4 -4
  73. package/dist/{product-edit-YP4KOQ4T.mjs → product-edit-55YXTIGO.mjs} +2 -2
  74. package/dist/{product-export-WUZYHPS5.mjs → product-export-5AD7NELI.mjs} +3 -3
  75. package/dist/{product-image-variants-edit-Y363J5NG.mjs → product-image-variants-edit-M6QF2RLE.mjs} +4 -4
  76. package/dist/{product-list-DNTS7WUN.mjs → product-list-EUWZIFTM.mjs} +7 -7
  77. package/dist/{product-organization-H557PLLB.mjs → product-organization-N3VBRXF4.mjs} +2 -2
  78. package/dist/{product-prices-JOG6IIQ7.mjs → product-prices-4C36AG4R.mjs} +1 -1
  79. package/dist/{product-sales-channels-ANCFZZ5S.mjs → product-sales-channels-PPXUG4KT.mjs} +5 -5
  80. package/dist/{product-stock-NYUFMEVG.mjs → product-stock-VEGE6SUZ.mjs} +2 -2
  81. package/dist/{product-tag-detail-EHBB3WUB.mjs → product-tag-detail-I3MBZX7U.mjs} +10 -10
  82. package/dist/{product-tag-list-LSW5FFVN.mjs → product-tag-list-JUWSOMB7.mjs} +10 -10
  83. package/dist/{product-type-detail-3VB6AWUW.mjs → product-type-detail-RKHT5NBL.mjs} +4 -4
  84. package/dist/{product-type-list-FD3TGPNP.mjs → product-type-list-QQKAHBJ3.mjs} +6 -6
  85. package/dist/{product-variant-detail-43T33AQP.mjs → product-variant-detail-XAYG5CKE.mjs} +4 -4
  86. package/dist/{profile-detail-BMC7IZBY.mjs → profile-detail-FRZ74HAF.mjs} +1 -1
  87. package/dist/{profile-edit-YZCUGEXF.mjs → profile-edit-ZNXO6WME.mjs} +1 -1
  88. package/dist/{promotion-detail-VJB55PJK.mjs → promotion-detail-QC36KXB3.mjs} +3 -3
  89. package/dist/{promotion-list-TMWKPLMJ.mjs → promotion-list-L22GJE3P.mjs} +4 -4
  90. package/dist/{refund-reason-list-URYYYEK6.mjs → refund-reason-list-OJYYEYJE.mjs} +8 -8
  91. package/dist/{region-add-countries-7U4J5RW6.mjs → region-add-countries-2VAVXMJQ.mjs} +4 -4
  92. package/dist/{region-create-IUGX33M5.mjs → region-create-NA7Y2LN4.mjs} +4 -4
  93. package/dist/{region-detail-D3JBW34A.mjs → region-detail-3BARMXUE.mjs} +4 -4
  94. package/dist/{region-list-JAQXIBYD.mjs → region-list-V4R2REMH.mjs} +4 -4
  95. package/dist/{reservation-list-2DN3YHIJ.mjs → reservation-list-B47DXTA7.mjs} +5 -5
  96. package/dist/{return-reason-list-IFFIDA5O.mjs → return-reason-list-SCBGTOEI.mjs} +10 -10
  97. package/dist/{sales-channel-add-products-VH5T3GDA.mjs → sales-channel-add-products-F7YV4MO5.mjs} +4 -4
  98. package/dist/{sales-channel-detail-I2ZHVXMG.mjs → sales-channel-detail-MXIPZCGA.mjs} +4 -4
  99. package/dist/{sales-channel-list-3FV4S2NN.mjs → sales-channel-list-RLGL7FM3.mjs} +5 -5
  100. package/dist/{shipping-option-type-list-ZMZMXFME.mjs → shipping-option-type-list-DIOX7VG7.mjs} +5 -5
  101. package/dist/{shipping-profiles-list-H3CBZKRH.mjs → shipping-profiles-list-WRPIJBZZ.mjs} +4 -4
  102. package/dist/{store-add-currencies-ZFS3WZHG.mjs → store-add-currencies-OX2WXFMS.mjs} +4 -4
  103. package/dist/{store-add-locales-IZOZP5C6.mjs → store-add-locales-VJ4RJ7UI.mjs} +4 -4
  104. package/dist/{store-detail-4IBAEVSD.mjs → store-detail-JSNPOB2F.mjs} +4 -4
  105. package/dist/{tax-region-detail-O2T7BI3V.mjs → tax-region-detail-2AE2EFI3.mjs} +13 -13
  106. package/dist/{tax-region-province-detail-2W7RXAM5.mjs → tax-region-province-detail-4ERSEQFF.mjs} +13 -13
  107. package/dist/{tax-region-tax-override-create-7IM4ZVPH.mjs → tax-region-tax-override-create-PHCGEF7V.mjs} +11 -11
  108. package/dist/{tax-region-tax-override-edit-3ZT5IZYR.mjs → tax-region-tax-override-edit-SMRPSILC.mjs} +12 -12
  109. package/dist/{translation-list-IAKEB7MY.mjs → translation-list-S5Z6PG2R.mjs} +8 -5
  110. package/dist/translations-edit-HUNKY7CO.mjs +708 -0
  111. package/dist/{user-invite-XB635N26.mjs → user-invite-GAGIM5DO.mjs} +4 -4
  112. package/dist/{user-list-YYUOQKQY.mjs → user-list-YTZQNYSO.mjs} +4 -4
  113. package/dist/{workflow-execution-list-IZVF2XMJ.mjs → workflow-execution-list-C3EJMVSZ.mjs} +4 -4
  114. package/package.json +9 -9
  115. package/src/components/data-grid/components/data-grid-cell-container.tsx +16 -4
  116. package/src/components/data-grid/components/data-grid-readonly-cell.tsx +16 -3
  117. package/src/components/data-grid/components/data-grid-root.tsx +19 -4
  118. package/src/components/data-grid/components/data-grid-text-cell.tsx +79 -9
  119. package/src/i18n/languages.ts +7 -0
  120. package/src/i18n/translations/index.ts +4 -0
  121. package/src/i18n/translations/zhTW.json +3249 -0
  122. package/src/routes/translations/translation-list/translation-list.tsx +9 -8
  123. package/src/routes/translations/translations-edit/components/translations-edit-form/translations-edit-form.tsx +388 -90
  124. package/src/routes/translations/translations-edit/translations-edit.tsx +0 -1
  125. package/dist/translations-edit-QKLE4L5B.mjs +0 -458
@@ -45,7 +45,8 @@ export const TranslationList = () => {
45
45
  entity_types: Object.keys(translatable_fields ?? {}),
46
46
  },
47
47
  {
48
- enabled: !!translatable_fields && !!store,
48
+ enabled:
49
+ !!translatable_fields && !!store && store.supported_locales?.length > 0,
49
50
  }
50
51
  )
51
52
 
@@ -56,7 +57,7 @@ export const TranslationList = () => {
56
57
  const hasLocales = (store?.supported_locales ?? []).length > 0
57
58
 
58
59
  const translatableEntities: TranslatableEntity[] = useMemo(() => {
59
- if (!translatable_fields || !statistics) {
60
+ if (!translatable_fields) {
60
61
  return []
61
62
  }
62
63
 
@@ -66,7 +67,10 @@ export const TranslationList = () => {
66
67
  !["product_option", "product_option_value"].includes(entity)
67
68
  )
68
69
  .map(([entity, fields]) => {
69
- const entityStatistics = statistics[entity]
70
+ const entityStatistics = statistics?.[entity] ?? {
71
+ translated: 0,
72
+ expected: 0,
73
+ }
70
74
 
71
75
  return {
72
76
  label: entity
@@ -85,9 +89,8 @@ export const TranslationList = () => {
85
89
  !!store &&
86
90
  !isPending &&
87
91
  !isTranslationSettingsPending &&
88
- !isTranslationStatisticsPending &&
89
92
  !!translatable_fields &&
90
- !!statistics
93
+ ((!!statistics && !isTranslationStatisticsPending) || !hasLocales)
91
94
 
92
95
  if (!isReady) {
93
96
  return <TwoColumnPageSkeleton sidebarSections={2} />
@@ -122,9 +125,7 @@ export const TranslationList = () => {
122
125
  ) ?? []
123
126
  }
124
127
  ></ActiveLocalesSection>
125
- {statistics && (
126
- <TranslationsCompletionSection statistics={statistics} />
127
- )}
128
+ <TranslationsCompletionSection statistics={statistics ?? {}} />
128
129
  </TwoColumnPage.Sidebar>
129
130
  </TwoColumnPage>
130
131
  )
@@ -1,8 +1,8 @@
1
1
  import { zodResolver } from "@hookform/resolvers/zod"
2
2
  import { AdminStoreLocale, HttpTypes } from "@medusajs/types"
3
- import { Button, ProgressTabs, toast } from "@medusajs/ui"
3
+ import { Button, Prompt, Select, toast } from "@medusajs/ui"
4
4
  import { ColumnDef } from "@tanstack/react-table"
5
- import { useMemo, useRef } from "react"
5
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react"
6
6
  import { useForm } from "react-hook-form"
7
7
  import { useTranslation } from "react-i18next"
8
8
  import { z } from "zod"
@@ -17,7 +17,6 @@ import {
17
17
  } from "../../../../../components/modals"
18
18
  import { KeyboundForm } from "../../../../../components/utilities/keybound-form"
19
19
  import { useBatchTranslations } from "../../../../../hooks/api/translations"
20
- import { useDocumentDirection } from "../../../../../hooks/use-document-direction"
21
20
 
22
21
  /**
23
22
  * Schema for a single locale translation.
@@ -183,29 +182,119 @@ function transformToBatchPayload(
183
182
  return payload
184
183
  }
185
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]
193
+
194
+ if (!currentLocale || !initialLocale) {
195
+ continue
196
+ }
197
+
198
+ for (const [fieldName, fieldValue] of Object.entries(
199
+ currentLocale.fields
200
+ )) {
201
+ const initialValue = initialLocale.fields[fieldName] ?? ""
202
+ const currentValue = fieldValue ?? ""
203
+
204
+ if (currentValue !== initialValue) {
205
+ return true
206
+ }
207
+ }
208
+ }
209
+
210
+ return false
211
+ }
212
+
213
+ function transformSingleLocaleToBatchPayload(
214
+ currentState: TranslationsFormSchema,
215
+ initialState: TranslationsFormSchema,
216
+ entityType: string,
217
+ localeCode: string
218
+ ): Required<HttpTypes.AdminBatchTranslations> {
219
+ const payload: Required<HttpTypes.AdminBatchTranslations> = {
220
+ create: [],
221
+ update: [],
222
+ delete: [],
223
+ }
224
+
225
+ for (const [entityId, entityData] of Object.entries(currentState.entities)) {
226
+ const localeTranslations = entityData.locales[localeCode]
227
+ if (!localeTranslations) continue
228
+
229
+ const initial = initialState.entities[entityId]?.locales[localeCode]
230
+ const hasContent = Object.values(localeTranslations.fields).some(
231
+ (v) => v !== undefined && v.trim() !== ""
232
+ )
233
+ const hadContent =
234
+ initial &&
235
+ Object.values(initial.fields).some(
236
+ (v) => v !== undefined && v.trim() !== ""
237
+ )
238
+
239
+ if (!localeTranslations.id && hasContent) {
240
+ payload.create.push({
241
+ reference_id: entityId,
242
+ reference: entityType,
243
+ locale_code: localeTranslations.locale_code,
244
+ translations: localeTranslations.fields,
245
+ })
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)
260
+ }
261
+ }
262
+
263
+ return payload
264
+ }
265
+
186
266
  const columnHelper = createDataGridHelper<
187
267
  TranslationRow,
188
268
  TranslationsFormSchema
189
269
  >()
190
270
 
271
+ const FIELD_COLUMN_WIDTH = 150
272
+
191
273
  function useTranslationsGridColumns({
192
274
  entities,
193
275
  translatableFields,
194
276
  availableLocales,
195
- modalFields = [],
277
+ selectedLocale,
278
+ dynamicColumnWidth,
196
279
  }: {
197
280
  entities: { id: string; [key: string]: string }[]
198
281
  translatableFields: string[]
199
282
  availableLocales: AdminStoreLocale[]
200
- modalFields?: string[]
283
+ selectedLocale: string
284
+ dynamicColumnWidth: number
201
285
  }) {
202
286
  const { t } = useTranslation()
203
287
 
204
288
  const columns: ColumnDef<TranslationRow>[] = useMemo(() => {
205
- return [
289
+ const selectedLocaleData = availableLocales.find(
290
+ (l) => l.locale_code === selectedLocale
291
+ )
292
+
293
+ const baseColumns = [
206
294
  columnHelper.column({
207
295
  id: "field",
208
296
  name: "field",
297
+ size: FIELD_COLUMN_WIDTH,
209
298
  header: undefined,
210
299
  cell: (context) => {
211
300
  const row = context.row.original
@@ -233,7 +322,7 @@ function useTranslationsGridColumns({
233
322
  columnHelper.column({
234
323
  id: "original",
235
324
  name: "original",
236
- size: 300,
325
+ size: dynamicColumnWidth,
237
326
  header: t("general.original"),
238
327
  disableHiding: true,
239
328
  cell: (context) => {
@@ -253,41 +342,29 @@ function useTranslationsGridColumns({
253
342
  }
254
343
 
255
344
  return (
256
- <DataGrid.ReadonlyCell context={context}>
345
+ <DataGrid.ReadonlyCell context={context} isMultiLine>
257
346
  {entity[row.field_name]}
258
347
  </DataGrid.ReadonlyCell>
259
348
  )
260
349
  },
261
350
  }),
262
- ...availableLocales.map((locale) => {
263
- return columnHelper.column({
264
- id: locale.locale_code,
265
- name: locale.locale.name,
266
- size: 300,
267
- header: () => locale.locale.name,
351
+ ]
352
+
353
+ if (selectedLocaleData) {
354
+ baseColumns.push(
355
+ columnHelper.column({
356
+ id: selectedLocaleData.locale_code,
357
+ name: selectedLocaleData.locale.name,
358
+ size: dynamicColumnWidth,
359
+ header: () => selectedLocaleData.locale.name,
268
360
  cell: (context) => {
269
361
  const row = context.row.original
270
362
 
271
363
  if (isEntityRow(row)) {
272
- return (
273
- <DataGrid.ReadonlyCell
274
- context={context}
275
- ></DataGrid.ReadonlyCell>
276
- )
364
+ return <DataGrid.ReadonlyCell context={context} isMultiLine />
277
365
  }
278
366
 
279
- const useModal = modalFields.includes(row.field_name)
280
-
281
- if (useModal) {
282
- return (
283
- <DataGrid.ExpandableTextCell
284
- context={context}
285
- fieldLabel={row.field_name}
286
- />
287
- )
288
- }
289
-
290
- return <DataGrid.TextCell context={context} />
367
+ return <DataGrid.TextCell context={context} isMultiLine />
291
368
  },
292
369
  field: (context) => {
293
370
  const row = context.row.original
@@ -296,13 +373,22 @@ function useTranslationsGridColumns({
296
373
  return null
297
374
  }
298
375
 
299
- return `entities.${row.reference_id}.locales.${locale.locale_code}.fields.${row.field_name}`
376
+ return `entities.${row.reference_id}.locales.${selectedLocaleData.locale_code}.fields.${row.field_name}`
300
377
  },
301
378
  type: "text",
302
379
  })
303
- }),
304
- ]
305
- }, [t, translatableFields, availableLocales, modalFields])
380
+ )
381
+ }
382
+
383
+ return baseColumns
384
+ }, [
385
+ t,
386
+ translatableFields,
387
+ availableLocales,
388
+ selectedLocale,
389
+ entities,
390
+ dynamicColumnWidth,
391
+ ])
306
392
 
307
393
  return columns
308
394
  }
@@ -313,7 +399,6 @@ type TranslationsEditFormProps = {
313
399
  entityType: string
314
400
  availableLocales: AdminStoreLocale[]
315
401
  translatableFields: string[]
316
- modalFields?: string[]
317
402
  fetchNextPage: () => void
318
403
  hasNextPage: boolean
319
404
  isFetchingNextPage: boolean
@@ -326,7 +411,6 @@ export const TranslationsEditForm = ({
326
411
  entityType,
327
412
  availableLocales,
328
413
  translatableFields,
329
- modalFields = [],
330
414
  fetchNextPage,
331
415
  hasNextPage,
332
416
  isFetchingNextPage,
@@ -334,7 +418,36 @@ export const TranslationsEditForm = ({
334
418
  }: TranslationsEditFormProps) => {
335
419
  const { t } = useTranslation()
336
420
  const { handleSuccess, setCloseOnEscape } = useRouteModal()
337
- const direction = useDocumentDirection()
421
+
422
+ const containerRef = useRef<HTMLDivElement>(null)
423
+ const [dynamicColumnWidth, setDynamicColumnWidth] = useState(400)
424
+
425
+ useEffect(() => {
426
+ const calculateColumnWidth = () => {
427
+ if (containerRef.current) {
428
+ const containerWidth = containerRef.current.offsetWidth
429
+ const availableWidth = containerWidth - FIELD_COLUMN_WIDTH - 12
430
+ const columnWidth = Math.max(300, Math.floor(availableWidth / 2))
431
+ setDynamicColumnWidth(columnWidth)
432
+ }
433
+ }
434
+
435
+ calculateColumnWidth()
436
+
437
+ const resizeObserver = new ResizeObserver(calculateColumnWidth)
438
+ if (containerRef.current) {
439
+ resizeObserver.observe(containerRef.current)
440
+ }
441
+
442
+ return () => resizeObserver.disconnect()
443
+ }, [])
444
+
445
+ const [selectedLocale, setSelectedLocale] = useState<string>(
446
+ availableLocales[0]?.locale_code ?? ""
447
+ )
448
+
449
+ const [showUnsavedPrompt, setShowUnsavedPrompt] = useState(false)
450
+ const [pendingLocale, setPendingLocale] = useState<string | null>(null)
338
451
 
339
452
  const entities = useMemo(() => references, [references])
340
453
  const totalCount = useMemo(
@@ -363,6 +476,131 @@ export const TranslationsEditForm = ({
363
476
 
364
477
  const { mutateAsync, isPending } = useBatchTranslations(entityType)
365
478
 
479
+ const handleLocaleChange = useCallback(
480
+ (newLocale: string) => {
481
+ if (newLocale === selectedLocale) {
482
+ return
483
+ }
484
+
485
+ const currentValues = form.getValues()
486
+ const hasChanges = hasLocaleChanges(
487
+ currentValues,
488
+ initialState.current,
489
+ selectedLocale
490
+ )
491
+
492
+ if (hasChanges) {
493
+ setPendingLocale(newLocale)
494
+ setShowUnsavedPrompt(true)
495
+ } else {
496
+ setSelectedLocale(newLocale)
497
+ }
498
+ },
499
+ [selectedLocale, form]
500
+ )
501
+
502
+ const saveCurrentLocale = useCallback(async () => {
503
+ const currentValues = form.getValues()
504
+ const payload = transformSingleLocaleToBatchPayload(
505
+ currentValues,
506
+ initialState.current,
507
+ entityType,
508
+ selectedLocale
509
+ )
510
+
511
+ if (
512
+ payload.create.length === 0 &&
513
+ payload.update.length === 0 &&
514
+ payload.delete.length === 0
515
+ ) {
516
+ return true
517
+ }
518
+
519
+ try {
520
+ const BATCH_SIZE = 150
521
+ const totalItems =
522
+ payload.create.length + payload.update.length + payload.delete.length
523
+ const batchCount = Math.ceil(totalItems / BATCH_SIZE)
524
+
525
+ for (let i = 0; i < batchCount; i++) {
526
+ let currentBatchAvailable = BATCH_SIZE
527
+ const currentBatch: HttpTypes.AdminBatchTranslations = {
528
+ create: [],
529
+ update: [],
530
+ delete: [],
531
+ }
532
+ if (payload.create.length > 0) {
533
+ currentBatch.create = payload.create.splice(0, currentBatchAvailable)
534
+ currentBatchAvailable -= currentBatch.create.length
535
+ }
536
+ if (payload.update.length > 0) {
537
+ currentBatch.update = payload.update.splice(0, currentBatchAvailable)
538
+ currentBatchAvailable -= currentBatch.update.length
539
+ }
540
+ if (payload.delete.length > 0) {
541
+ currentBatch.delete = payload.delete.splice(0, currentBatchAvailable)
542
+ currentBatchAvailable -= currentBatch.delete.length
543
+ }
544
+
545
+ await mutateAsync(currentBatch)
546
+ }
547
+
548
+ const updatedInitialState = { ...initialState.current }
549
+ for (const entityId of Object.keys(currentValues.entities)) {
550
+ if (updatedInitialState.entities[entityId]?.locales[selectedLocale]) {
551
+ updatedInitialState.entities[entityId].locales[selectedLocale] = {
552
+ ...currentValues.entities[entityId].locales[selectedLocale],
553
+ }
554
+ }
555
+ }
556
+ initialState.current = updatedInitialState
557
+ form.reset(currentValues)
558
+
559
+ return true
560
+ } catch (error) {
561
+ toast.error(
562
+ error instanceof Error ? error.message : "Failed to save translations"
563
+ )
564
+ return false
565
+ }
566
+ }, [form, entityType, selectedLocale, mutateAsync])
567
+
568
+ const handleSaveAndSwitch = useCallback(async () => {
569
+ const success = await saveCurrentLocale()
570
+ if (success && pendingLocale) {
571
+ toast.success(
572
+ t("translations.edit.localeChangesSaved", {
573
+ defaultValue: "Changes saved successfully",
574
+ })
575
+ )
576
+ setSelectedLocale(pendingLocale)
577
+ }
578
+ setShowUnsavedPrompt(false)
579
+ setPendingLocale(null)
580
+ }, [saveCurrentLocale, pendingLocale, t])
581
+
582
+ const handleCancelSwitch = useCallback(() => {
583
+ setShowUnsavedPrompt(false)
584
+ setPendingLocale(null)
585
+ }, [])
586
+
587
+ const handleSave = useCallback(
588
+ async (closeOnSuccess: boolean = false) => {
589
+ const success = await saveCurrentLocale()
590
+ if (success) {
591
+ toast.success(
592
+ t("translations.edit.successToast", {
593
+ defaultValue: "Translations updated successfully",
594
+ })
595
+ )
596
+ if (closeOnSuccess) {
597
+ handleSuccess()
598
+ }
599
+ }
600
+ },
601
+ [saveCurrentLocale, t, handleSuccess]
602
+ )
603
+
366
604
  const handleSubmit = form.handleSubmit(async (values) => {
367
605
  const payload = transformToBatchPayload(
368
606
  values,
@@ -428,65 +666,125 @@ export const TranslationsEditForm = ({
428
666
  entities,
429
667
  translatableFields,
430
668
  availableLocales,
431
- modalFields,
669
+ selectedLocale,
670
+ dynamicColumnWidth,
432
671
  })
433
672
 
673
+ const selectedLocaleDisplay = availableLocales.find(
674
+ (l) => l.locale_code === selectedLocale
675
+ )?.locale.name
676
+
434
677
  return (
435
678
  <RouteFocusModal.Form form={form}>
436
- <KeyboundForm onSubmit={handleSubmit} className="flex h-full flex-col">
437
- <ProgressTabs
438
- dir={direction}
439
- defaultValue={entityType}
440
- className="flex h-full flex-col overflow-hidden"
441
- >
442
- <RouteFocusModal.Header>
443
- <div className="-my-2 w-full border-l">
444
- <ProgressTabs.List className="justify-start-start flex w-full items-center">
445
- <ProgressTabs.Trigger value={entityType}>
446
- {entityType
447
- .split("_")
448
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
449
- .join(" ")}
450
- </ProgressTabs.Trigger>
451
- </ProgressTabs.List>
452
- </div>
453
- </RouteFocusModal.Header>
454
- <RouteFocusModal.Body className="size-full overflow-hidden">
455
- <ProgressTabs.Content
456
- value={entityType}
457
- className="size-full overflow-y-auto"
679
+ <KeyboundForm
680
+ onSubmit={handleSubmit}
681
+ className="flex h-full flex-col overflow-hidden"
682
+ >
683
+ <RouteFocusModal.Header>
684
+ <div className="-my-2 flex w-full items-center justify-between border-l px-4">
685
+ <Select
686
+ value={selectedLocale}
687
+ onValueChange={handleLocaleChange}
688
+ size="small"
458
689
  >
459
- <DataGrid
460
- columns={columns}
461
- data={rows}
462
- getSubRows={(row) => {
463
- if (isEntityRow(row)) {
464
- return row.subRows
465
- }
466
- }}
467
- state={form}
468
- onEditingChange={(editing) => setCloseOnEscape(!editing)}
469
- totalRowCount={totalCount}
470
- onFetchMore={fetchNextPage}
471
- isFetchingMore={isFetchingNextPage}
472
- hasNextPage={hasNextPage}
473
- />
474
- </ProgressTabs.Content>
475
- </RouteFocusModal.Body>
476
- <RouteFocusModal.Footer>
477
- <div className="flex items-center justify-end gap-x-2">
478
- <RouteFocusModal.Close asChild>
479
- <Button size="small" variant="secondary">
480
- {t("actions.cancel")}
481
- </Button>
482
- </RouteFocusModal.Close>
483
- <Button size="small" type="submit" isLoading={isPending}>
484
- {t("actions.save")}
690
+ <Select.Trigger className="bg-ui-bg-base w-[200px]">
691
+ <Select.Value>{selectedLocaleDisplay}</Select.Value>
692
+ </Select.Trigger>
693
+ <Select.Content>
694
+ {availableLocales.map((locale) => (
695
+ <Select.Item
696
+ key={locale.locale_code}
697
+ value={locale.locale_code}
698
+ >
699
+ {locale.locale.name}
700
+ </Select.Item>
701
+ ))}
702
+ </Select.Content>
703
+ </Select>
704
+ </div>
705
+ </RouteFocusModal.Header>
706
+ <RouteFocusModal.Body className="size-full overflow-hidden">
707
+ <div ref={containerRef} className="size-full">
708
+ <DataGrid
709
+ columns={columns}
710
+ data={rows}
711
+ getSubRows={(row) => {
712
+ if (isEntityRow(row)) {
713
+ return row.subRows
714
+ }
715
+ }}
716
+ state={form}
717
+ onEditingChange={(editing) => setCloseOnEscape(!editing)}
718
+ totalRowCount={totalCount}
719
+ onFetchMore={fetchNextPage}
720
+ isFetchingMore={isFetchingNextPage}
721
+ hasNextPage={hasNextPage}
722
+ />
723
+ </div>
724
+ </RouteFocusModal.Body>
725
+ <RouteFocusModal.Footer>
726
+ <div className="flex items-center justify-end gap-x-2">
727
+ <RouteFocusModal.Close asChild>
728
+ <Button size="small" variant="secondary">
729
+ {t("actions.cancel")}
485
730
  </Button>
486
- </div>
487
- </RouteFocusModal.Footer>
488
- </ProgressTabs>
731
+ </RouteFocusModal.Close>
732
+ <Button
733
+ size="small"
734
+ type="button"
735
+ variant="secondary"
736
+ onClick={() => handleSave(false)}
737
+ isLoading={isPending}
738
+ >
739
+ {t("actions.saveChanges", { defaultValue: "Save changes" })}
740
+ </Button>
741
+ <Button
742
+ size="small"
743
+ type="button"
744
+ onClick={() => handleSave(true)}
745
+ isLoading={isPending}
746
+ >
747
+ {t("actions.saveAndClose", { defaultValue: "Save and close" })}
748
+ </Button>
749
+ </div>
750
+ </RouteFocusModal.Footer>
489
751
  </KeyboundForm>
752
+
753
+ <Prompt open={showUnsavedPrompt} variant="confirmation">
754
+ <Prompt.Content>
755
+ <Prompt.Header>
756
+ <Prompt.Title>
757
+ {t("translations.unsavedChanges.title", {
758
+ defaultValue: "Unsaved changes",
759
+ })}
760
+ </Prompt.Title>
761
+ <Prompt.Description>
762
+ {t("translations.unsavedChanges.description", {
763
+ defaultValue:
764
+ "You have unsaved changes for this locale. Save them before switching?",
765
+ })}
766
+ </Prompt.Description>
767
+ </Prompt.Header>
768
+ <Prompt.Footer>
769
+ <Button
770
+ size="small"
771
+ variant="secondary"
772
+ onClick={handleCancelSwitch}
773
+ type="button"
774
+ >
775
+ {t("actions.close")}
776
+ </Button>
777
+ <Button
778
+ size="small"
779
+ onClick={handleSaveAndSwitch}
780
+ type="button"
781
+ isLoading={isPending}
782
+ >
783
+ {t("actions.saveChanges", { defaultValue: "Save changes" })}
784
+ </Button>
785
+ </Prompt.Footer>
786
+ </Prompt.Content>
787
+ </Prompt>
490
788
  </RouteFocusModal.Form>
491
789
  )
492
790
  }
@@ -79,7 +79,6 @@ export const TranslationsEdit = () => {
79
79
  entityType={reference!}
80
80
  availableLocales={store?.supported_locales ?? []}
81
81
  translatableFields={translatable_fields[reference!]}
82
- modalFields={translatable_fields[reference!]}
83
82
  fetchNextPage={fetchNextPage}
84
83
  hasNextPage={hasNextPage}
85
84
  isFetchingNextPage={isFetchingNextPage}