@vendure/dashboard 3.4.3-master-202509260228 → 3.5.0-minor-202510012036

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 (295) hide show
  1. package/README.md +4 -0
  2. package/dist/plugin/api/api-extensions.js +11 -14
  3. package/dist/plugin/api/metrics.resolver.d.ts +2 -2
  4. package/dist/plugin/api/metrics.resolver.js +2 -2
  5. package/dist/plugin/config/metrics-strategies.d.ts +9 -9
  6. package/dist/plugin/config/metrics-strategies.js +6 -6
  7. package/dist/plugin/constants.d.ts +2 -0
  8. package/dist/plugin/constants.js +3 -1
  9. package/dist/plugin/dashboard.plugin.js +13 -0
  10. package/dist/plugin/service/metrics.service.d.ts +3 -3
  11. package/dist/plugin/service/metrics.service.js +37 -53
  12. package/dist/plugin/types.d.ts +9 -12
  13. package/dist/plugin/types.js +7 -11
  14. package/dist/vite/vite-plugin-config.js +13 -9
  15. package/dist/vite/vite-plugin-translations.d.ts +22 -0
  16. package/dist/vite/vite-plugin-translations.js +66 -0
  17. package/dist/vite/vite-plugin-vendure-dashboard.js +10 -8
  18. package/lingui.config.js +25 -2
  19. package/package.json +159 -156
  20. package/src/app/app-providers.tsx +0 -4
  21. package/src/app/common/delete-bulk-action.tsx +6 -5
  22. package/src/app/common/duplicate-bulk-action.tsx +4 -5
  23. package/src/app/common/duplicate-entity-dialog.tsx +1 -1
  24. package/src/app/common/set-document-direction.ts +7 -0
  25. package/src/app/main.tsx +50 -17
  26. package/src/app/routes/_authenticated/_administrators/administrators.tsx +8 -6
  27. package/src/app/routes/_authenticated/_administrators/administrators_.$id.tsx +17 -6
  28. package/src/app/routes/_authenticated/_administrators/components/role-permissions-display.tsx +2 -2
  29. package/src/app/routes/_authenticated/_assets/assets.tsx +1 -1
  30. package/src/app/routes/_authenticated/_assets/assets_.$id.tsx +4 -4
  31. package/src/app/routes/_authenticated/_assets/components/asset-bulk-actions.tsx +8 -6
  32. package/src/app/routes/_authenticated/_assets/components/asset-tag-filter.tsx +1 -1
  33. package/src/app/routes/_authenticated/_assets/components/asset-tags-editor.tsx +1 -1
  34. package/src/app/routes/_authenticated/_assets/components/manage-tags-dialog.tsx +3 -8
  35. package/src/app/routes/_authenticated/_channels/channels.tsx +3 -6
  36. package/src/app/routes/_authenticated/_channels/channels_.$id.tsx +5 -5
  37. package/src/app/routes/_authenticated/_collections/collections.tsx +10 -6
  38. package/src/app/routes/_authenticated/_collections/collections_.$id.tsx +16 -5
  39. package/src/app/routes/_authenticated/_collections/components/collection-bulk-actions.tsx +1 -1
  40. package/src/app/routes/_authenticated/_collections/components/collection-contents-sheet.tsx +1 -1
  41. package/src/app/routes/_authenticated/_collections/components/move-collections-dialog.tsx +6 -6
  42. package/src/app/routes/_authenticated/_countries/countries.graphql.ts +2 -0
  43. package/src/app/routes/_authenticated/_countries/countries.tsx +2 -3
  44. package/src/app/routes/_authenticated/_countries/countries_.$id.tsx +4 -4
  45. package/src/app/routes/_authenticated/_customer-groups/components/customer-group-members-sheet.tsx +1 -1
  46. package/src/app/routes/_authenticated/_customer-groups/components/customer-group-members-table.tsx +4 -4
  47. package/src/app/routes/_authenticated/_customer-groups/customer-groups.tsx +2 -4
  48. package/src/app/routes/_authenticated/_customer-groups/customer-groups_.$id.tsx +13 -6
  49. package/src/app/routes/_authenticated/_customers/components/customer-address-card.tsx +8 -8
  50. package/src/app/routes/_authenticated/_customers/components/customer-address-form.tsx +3 -3
  51. package/src/app/routes/_authenticated/_customers/components/customer-history/customer-history-container.tsx +1 -1
  52. package/src/app/routes/_authenticated/_customers/components/customer-history/customer-history-utils.tsx +1 -1
  53. package/src/app/routes/_authenticated/_customers/components/customer-history/default-customer-history-components.tsx +1 -1
  54. package/src/app/routes/_authenticated/_customers/components/customer-history/use-customer-history.ts +1 -1
  55. package/src/app/routes/_authenticated/_customers/components/customer-status-badge.tsx +1 -1
  56. package/src/app/routes/_authenticated/_customers/customers.graphql.ts +4 -0
  57. package/src/app/routes/_authenticated/_customers/customers.tsx +23 -11
  58. package/src/app/routes/_authenticated/_customers/customers_.$id.tsx +10 -8
  59. package/src/app/routes/_authenticated/_facets/components/edit-facet-value.tsx +1 -1
  60. package/src/app/routes/_authenticated/_facets/components/facet-bulk-actions.tsx +6 -5
  61. package/src/app/routes/_authenticated/_facets/components/facet-values-sheet.tsx +1 -1
  62. package/src/app/routes/_authenticated/_facets/components/facet-values-table.tsx +1 -1
  63. package/src/app/routes/_authenticated/_facets/facets.tsx +5 -5
  64. package/src/app/routes/_authenticated/_facets/facets_.$facetId.values_.$id.tsx +7 -5
  65. package/src/app/routes/_authenticated/_facets/facets_.$id.tsx +18 -6
  66. package/src/app/routes/_authenticated/_global-settings/global-settings.tsx +5 -5
  67. package/src/app/routes/_authenticated/_orders/components/add-manual-payment-dialog.tsx +19 -21
  68. package/src/app/routes/_authenticated/_orders/components/customer-address-selector.tsx +1 -1
  69. package/src/app/routes/_authenticated/_orders/components/edit-order-table.tsx +22 -22
  70. package/src/app/routes/_authenticated/_orders/components/fulfill-order-dialog.tsx +6 -6
  71. package/src/app/routes/_authenticated/_orders/components/fulfillment-details.tsx +15 -9
  72. package/src/app/routes/_authenticated/_orders/components/order-address.tsx +1 -1
  73. package/src/app/routes/_authenticated/_orders/components/order-detail-shared.tsx +11 -9
  74. package/src/app/routes/_authenticated/_orders/components/order-history/default-order-history-components.tsx +1 -1
  75. package/src/app/routes/_authenticated/_orders/components/order-history/order-history-container.tsx +1 -1
  76. package/src/app/routes/_authenticated/_orders/components/order-history/order-history-utils.tsx +1 -1
  77. package/src/app/routes/_authenticated/_orders/components/order-history/use-order-history.ts +1 -1
  78. package/src/app/routes/_authenticated/_orders/components/order-line-custom-fields-form.tsx +1 -1
  79. package/src/app/routes/_authenticated/_orders/components/order-modification-preview-dialog.tsx +4 -4
  80. package/src/app/routes/_authenticated/_orders/components/order-modification-summary.tsx +1 -1
  81. package/src/app/routes/_authenticated/_orders/components/order-table-totals.tsx +27 -27
  82. package/src/app/routes/_authenticated/_orders/components/order-table.tsx +2 -2
  83. package/src/app/routes/_authenticated/_orders/components/order-tax-summary.tsx +1 -1
  84. package/src/app/routes/_authenticated/_orders/components/payment-details.tsx +26 -20
  85. package/src/app/routes/_authenticated/_orders/components/seller-orders-card.tsx +3 -1
  86. package/src/app/routes/_authenticated/_orders/components/settle-refund-dialog.tsx +6 -6
  87. package/src/app/routes/_authenticated/_orders/components/shipping-method-selector.tsx +1 -1
  88. package/src/app/routes/_authenticated/_orders/components/state-transition-control.tsx +1 -1
  89. package/src/app/routes/_authenticated/_orders/components/use-transition-order-to-state.tsx +3 -2
  90. package/src/app/routes/_authenticated/_orders/orders.tsx +5 -9
  91. package/src/app/routes/_authenticated/_orders/orders_.$aggregateOrderId_.seller-orders.$sellerOrderId.tsx +1 -1
  92. package/src/app/routes/_authenticated/_orders/orders_.$id.tsx +1 -1
  93. package/src/app/routes/_authenticated/_orders/orders_.$id_.modify.tsx +4 -4
  94. package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +17 -17
  95. package/src/app/routes/_authenticated/_orders/utils/order-detail-loaders.tsx +1 -1
  96. package/src/app/routes/_authenticated/_payment-methods/payment-methods.tsx +5 -6
  97. package/src/app/routes/_authenticated/_payment-methods/payment-methods_.$id.tsx +13 -6
  98. package/src/app/routes/_authenticated/_product-variants/components/product-variant-bulk-actions.tsx +1 -1
  99. package/src/app/routes/_authenticated/_product-variants/components/variant-price-detail.tsx +1 -1
  100. package/src/app/routes/_authenticated/_product-variants/product-variants.graphql.ts +10 -0
  101. package/src/app/routes/_authenticated/_product-variants/product-variants.tsx +9 -2
  102. package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +13 -6
  103. package/src/app/routes/_authenticated/_products/components/add-option-group-dialog.tsx +5 -5
  104. package/src/app/routes/_authenticated/_products/components/add-product-variant-dialog.tsx +5 -5
  105. package/src/app/routes/_authenticated/_products/components/assign-facet-values-dialog.tsx +5 -4
  106. package/src/app/routes/_authenticated/_products/components/create-product-options-dialog.tsx +9 -12
  107. package/src/app/routes/_authenticated/_products/components/create-product-variants-dialog.tsx +1 -1
  108. package/src/app/routes/_authenticated/_products/components/create-product-variants.tsx +4 -4
  109. package/src/app/routes/_authenticated/_products/components/option-groups-editor.tsx +1 -1
  110. package/src/app/routes/_authenticated/_products/components/product-bulk-actions.tsx +1 -1
  111. package/src/app/routes/_authenticated/_products/components/product-option-group-badge.tsx +19 -0
  112. package/src/app/routes/_authenticated/_products/components/product-option-select.tsx +3 -3
  113. package/src/app/routes/_authenticated/_products/components/product-options-table.tsx +114 -0
  114. package/src/app/routes/_authenticated/_products/product-option-groups.graphql.ts +103 -0
  115. package/src/app/routes/_authenticated/_products/products.graphql.ts +44 -32
  116. package/src/app/routes/_authenticated/_products/products.tsx +34 -5
  117. package/src/app/routes/_authenticated/_products/products_.$id.tsx +29 -12
  118. package/src/app/routes/_authenticated/_products/products_.$id_.variants.tsx +11 -11
  119. package/src/app/routes/_authenticated/_products/products_.$productId.option-groups.$id.tsx +177 -0
  120. package/src/app/routes/_authenticated/_products/products_.$productId.option-groups.$productOptionGroupId.options_.$id.tsx +208 -0
  121. package/src/app/routes/_authenticated/_profile/profile.tsx +4 -4
  122. package/src/app/routes/_authenticated/_promotions/promotions.tsx +2 -4
  123. package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +16 -9
  124. package/src/app/routes/_authenticated/_roles/components/permissions-table-grid.tsx +1 -1
  125. package/src/app/routes/_authenticated/_roles/roles.tsx +3 -6
  126. package/src/app/routes/_authenticated/_roles/roles_.$id.tsx +4 -6
  127. package/src/app/routes/_authenticated/_sellers/sellers.tsx +3 -4
  128. package/src/app/routes/_authenticated/_sellers/sellers_.$id.tsx +4 -4
  129. package/src/app/routes/_authenticated/_shipping-methods/components/price-display.tsx +5 -5
  130. package/src/app/routes/_authenticated/_shipping-methods/components/shipping-method-test-result-wrapper.tsx +1 -1
  131. package/src/app/routes/_authenticated/_shipping-methods/components/test-address-form.tsx +11 -11
  132. package/src/app/routes/_authenticated/_shipping-methods/components/test-order-builder.tsx +1 -1
  133. package/src/app/routes/_authenticated/_shipping-methods/components/test-shipping-methods-result.tsx +8 -8
  134. package/src/app/routes/_authenticated/_shipping-methods/components/test-shipping-methods-sheet.tsx +1 -1
  135. package/src/app/routes/_authenticated/_shipping-methods/components/test-single-method-result.tsx +8 -8
  136. package/src/app/routes/_authenticated/_shipping-methods/components/test-single-shipping-method-sheet.tsx +4 -4
  137. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.tsx +2 -3
  138. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +2 -2
  139. package/src/app/routes/_authenticated/_stock-locations/stock-locations.tsx +3 -4
  140. package/src/app/routes/_authenticated/_stock-locations/stock-locations_.$id.tsx +13 -6
  141. package/src/app/routes/_authenticated/_system/healthchecks.tsx +10 -4
  142. package/src/app/routes/_authenticated/_system/job-queue.tsx +10 -13
  143. package/src/app/routes/_authenticated/_system/scheduled-tasks.tsx +18 -16
  144. package/src/app/routes/_authenticated/_tax-categories/tax-categories.tsx +2 -4
  145. package/src/app/routes/_authenticated/_tax-categories/tax-categories_.$id.tsx +13 -6
  146. package/src/app/routes/_authenticated/_tax-rates/tax-rates.tsx +8 -12
  147. package/src/app/routes/_authenticated/_tax-rates/tax-rates_.$id.tsx +6 -4
  148. package/src/app/routes/_authenticated/_zones/components/zone-countries-sheet.tsx +4 -1
  149. package/src/app/routes/_authenticated/_zones/zones.tsx +4 -4
  150. package/src/app/routes/_authenticated/_zones/zones_.$id.tsx +8 -5
  151. package/src/app/routes/_authenticated/index.tsx +46 -25
  152. package/src/app/styles.css +4 -0
  153. package/src/i18n/common-strings.ts +111 -0
  154. package/src/i18n/locales/ar.po +4777 -0
  155. package/src/i18n/locales/cs.po +4777 -0
  156. package/src/i18n/locales/de.po +4299 -1101
  157. package/src/i18n/locales/en.po +3857 -659
  158. package/src/i18n/locales/es.po +4777 -0
  159. package/src/i18n/locales/fa.po +4777 -0
  160. package/src/i18n/locales/fr.po +4777 -0
  161. package/src/i18n/locales/he.po +4777 -0
  162. package/src/i18n/locales/hr.po +4777 -0
  163. package/src/i18n/locales/it.po +4777 -0
  164. package/src/i18n/locales/ja.po +4777 -0
  165. package/src/i18n/locales/ko.po +4628 -0
  166. package/src/i18n/locales/nb.po +4777 -0
  167. package/src/i18n/locales/ne.po +4777 -0
  168. package/src/i18n/locales/nl.po +4628 -0
  169. package/src/i18n/locales/pl.po +4777 -0
  170. package/src/i18n/locales/pt_BR.po +4777 -0
  171. package/src/i18n/locales/pt_PT.po +4777 -0
  172. package/src/i18n/locales/ru.po +4777 -0
  173. package/src/i18n/locales/sv.po +4777 -0
  174. package/src/i18n/locales/tr.po +4777 -0
  175. package/src/i18n/locales/uk.po +4777 -0
  176. package/src/i18n/locales/zh_Hans.po +4777 -0
  177. package/src/i18n/locales/zh_Hant.po +4777 -0
  178. package/src/lib/components/data-display/json.tsx +16 -1
  179. package/src/lib/components/data-input/combination-mode-input.tsx +1 -1
  180. package/src/lib/components/data-input/custom-field-list-input.tsx +11 -7
  181. package/src/lib/components/data-input/customer-group-input.tsx +27 -33
  182. package/src/lib/components/data-input/datetime-input.tsx +40 -1
  183. package/src/lib/components/data-input/default-relation-input.tsx +5 -4
  184. package/src/lib/components/data-input/index.ts +3 -0
  185. package/src/lib/components/data-input/product-multi-selector-input.tsx +14 -14
  186. package/src/lib/components/data-input/relation-selector.tsx +1 -1
  187. package/src/lib/components/data-input/select-with-options.tsx +1 -1
  188. package/src/lib/components/data-input/slug-input.tsx +290 -0
  189. package/src/lib/components/data-table/add-filter-menu.tsx +17 -10
  190. package/src/lib/components/data-table/data-table-bulk-action-item.tsx +45 -8
  191. package/src/lib/components/data-table/data-table-bulk-actions.tsx +4 -4
  192. package/src/lib/components/data-table/data-table-column-header.tsx +13 -8
  193. package/src/lib/components/data-table/data-table-context.tsx +91 -0
  194. package/src/lib/components/data-table/data-table-faceted-filter.tsx +2 -1
  195. package/src/lib/components/data-table/data-table-filter-badge.tsx +9 -5
  196. package/src/lib/components/data-table/data-table-filter-dialog.tsx +1 -1
  197. package/src/lib/components/data-table/data-table-utils.ts +21 -4
  198. package/src/lib/components/data-table/data-table-view-options.tsx +21 -10
  199. package/src/lib/components/data-table/data-table.tsx +146 -94
  200. package/src/lib/components/data-table/filters/data-table-boolean-filter.tsx +4 -4
  201. package/src/lib/components/data-table/global-views-bar.tsx +97 -0
  202. package/src/lib/components/data-table/global-views-sheet.tsx +11 -0
  203. package/src/lib/components/data-table/human-readable-operator.tsx +1 -1
  204. package/src/lib/components/data-table/manage-global-views-button.tsx +26 -0
  205. package/src/lib/components/data-table/my-views-button.tsx +47 -0
  206. package/src/lib/components/data-table/refresh-button.tsx +12 -3
  207. package/src/lib/components/data-table/save-view-button.tsx +41 -0
  208. package/src/lib/components/data-table/save-view-dialog.tsx +113 -0
  209. package/src/lib/components/data-table/use-generated-columns.tsx +13 -8
  210. package/src/lib/components/data-table/user-views-sheet.tsx +11 -0
  211. package/src/lib/components/data-table/views-sheet.tsx +305 -0
  212. package/src/lib/components/date-range-picker.tsx +186 -0
  213. package/src/lib/components/layout/app-sidebar.tsx +3 -1
  214. package/src/lib/components/layout/channel-switcher.tsx +8 -10
  215. package/src/lib/components/layout/dev-mode-indicator.tsx +1 -1
  216. package/src/lib/components/layout/generated-breadcrumbs.tsx +10 -8
  217. package/src/lib/components/layout/language-dialog.tsx +34 -13
  218. package/src/lib/components/layout/manage-languages-dialog.tsx +1 -1
  219. package/src/lib/components/layout/nav-main.tsx +23 -13
  220. package/src/lib/components/layout/nav-user.tsx +19 -23
  221. package/src/lib/components/login/login-form.tsx +1 -1
  222. package/src/lib/components/shared/asset/asset-bulk-actions.tsx +4 -4
  223. package/src/lib/components/shared/asset/asset-focal-point-editor.tsx +1 -1
  224. package/src/lib/components/shared/asset/asset-gallery.tsx +15 -14
  225. package/src/lib/components/shared/assign-to-channel-bulk-action.tsx +11 -11
  226. package/src/lib/components/shared/assign-to-channel-dialog.tsx +6 -5
  227. package/src/lib/components/shared/channel-code-label.tsx +1 -1
  228. package/src/lib/components/shared/channel-selector.tsx +4 -4
  229. package/src/lib/components/shared/configurable-operation-multi-selector.tsx +16 -14
  230. package/src/lib/components/shared/configurable-operation-selector.tsx +1 -1
  231. package/src/lib/components/shared/confirmation-dialog.tsx +8 -8
  232. package/src/lib/components/shared/country-selector.tsx +1 -1
  233. package/src/lib/components/shared/currency-selector.tsx +4 -4
  234. package/src/lib/components/shared/custom-fields-form.tsx +8 -24
  235. package/src/lib/components/shared/customer-address-form.tsx +3 -3
  236. package/src/lib/components/shared/customer-group-selector.tsx +1 -1
  237. package/src/lib/components/shared/customer-selector.tsx +1 -1
  238. package/src/lib/components/shared/error-page.tsx +1 -1
  239. package/src/lib/components/shared/facet-value-selector.tsx +10 -10
  240. package/src/lib/components/shared/history-timeline/history-note-checkbox.tsx +1 -1
  241. package/src/lib/components/shared/history-timeline/history-note-editor.tsx +1 -1
  242. package/src/lib/components/shared/history-timeline/history-note-entry.tsx +1 -1
  243. package/src/lib/components/shared/language-selector.tsx +4 -4
  244. package/src/lib/components/shared/navigation-confirmation.tsx +1 -1
  245. package/src/lib/components/shared/paginated-list-data-table.tsx +64 -34
  246. package/src/lib/components/shared/remove-from-channel-bulk-action.tsx +6 -5
  247. package/src/lib/components/shared/rich-text-editor/image-dialog.tsx +1 -1
  248. package/src/lib/components/shared/rich-text-editor/link-dialog.tsx +1 -1
  249. package/src/lib/components/shared/rich-text-editor/responsive-toolbar.tsx +1 -1
  250. package/src/lib/components/shared/rich-text-editor/table-edit-icons.tsx +1 -1
  251. package/src/lib/components/shared/role-code-label.tsx +1 -1
  252. package/src/lib/components/shared/role-selector.tsx +4 -4
  253. package/src/lib/components/shared/seller-selector.tsx +1 -1
  254. package/src/lib/components/shared/stock-level-label.tsx +3 -5
  255. package/src/lib/components/shared/table-cell/order-table-cell-components.tsx +3 -1
  256. package/src/lib/components/shared/tax-category-selector.tsx +1 -1
  257. package/src/lib/components/shared/translatable-form-field.tsx +15 -15
  258. package/src/lib/components/shared/zone-selector.tsx +1 -1
  259. package/src/lib/components/ui/button.tsx +1 -1
  260. package/src/lib/framework/dashboard-widget/base-widget.tsx +11 -9
  261. package/src/lib/framework/dashboard-widget/latest-orders-widget/index.tsx +35 -6
  262. package/src/lib/framework/dashboard-widget/metrics-widget/index.tsx +18 -12
  263. package/src/lib/framework/dashboard-widget/metrics-widget/metrics-widget.graphql.ts +9 -3
  264. package/src/lib/framework/dashboard-widget/orders-summary/index.tsx +26 -79
  265. package/src/lib/framework/dashboard-widget/widget-filters-context.tsx +35 -0
  266. package/src/lib/framework/defaults.ts +34 -63
  267. package/src/lib/framework/document-introspection/add-custom-fields.spec.ts +319 -9
  268. package/src/lib/framework/document-introspection/add-custom-fields.ts +60 -31
  269. package/src/lib/framework/document-introspection/get-document-structure.spec.ts +1 -159
  270. package/src/lib/framework/document-introspection/include-only-selected-list-fields.spec.ts +1840 -0
  271. package/src/lib/framework/document-introspection/include-only-selected-list-fields.ts +940 -0
  272. package/src/lib/framework/document-introspection/testing-utils.ts +161 -0
  273. package/src/lib/framework/extension-api/display-component-extensions.tsx +2 -0
  274. package/src/lib/framework/extension-api/types/data-table.ts +62 -4
  275. package/src/lib/framework/extension-api/types/navigation.ts +16 -0
  276. package/src/lib/framework/form-engine/utils.ts +34 -0
  277. package/src/lib/framework/layout-engine/page-layout.tsx +36 -36
  278. package/src/lib/framework/page/detail-page.tsx +10 -10
  279. package/src/lib/framework/page/list-page.tsx +289 -4
  280. package/src/lib/framework/page/use-extended-router.tsx +101 -34
  281. package/src/lib/graphql/api.ts +6 -2
  282. package/src/lib/graphql/graphql-env.d.ts +38 -26
  283. package/src/lib/hooks/use-display-locale.ts +40 -0
  284. package/src/lib/hooks/use-dynamic-translations.ts +46 -0
  285. package/src/lib/hooks/use-extended-detail-query.ts +1 -1
  286. package/src/lib/hooks/use-extended-list-query.ts +6 -1
  287. package/src/lib/hooks/use-local-format.ts +15 -1
  288. package/src/lib/hooks/use-saved-views.ts +230 -0
  289. package/src/lib/hooks/use-ui-language-loader.ts +30 -0
  290. package/src/lib/index.ts +15 -0
  291. package/src/lib/lib/load-i18n-messages.ts +17 -0
  292. package/src/lib/lib/trans.tsx +15 -11
  293. package/src/lib/providers/i18n-provider.tsx +7 -14
  294. package/src/lib/types/saved-views.ts +39 -0
  295. package/src/lib/utils/saved-views-utils.ts +40 -0
@@ -1,5 +1,20 @@
1
1
  import { JsonEditor } from 'json-edit-react';
2
+ import { FileJson } from 'lucide-react';
3
+
4
+ import { Button } from '../ui/button.js';
5
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '../ui/dropdown-menu.js';
2
6
 
3
7
  export function Json({ value }: Readonly<{ value: any }>) {
4
- return <JsonEditor data={value} />;
8
+ return (
9
+ <DropdownMenu>
10
+ <DropdownMenuTrigger asChild>
11
+ <Button variant="secondary" size="icon">
12
+ <FileJson />
13
+ </Button>
14
+ </DropdownMenuTrigger>
15
+ <DropdownMenuContent className="w-96 max-h-96 overflow-auto p-2">
16
+ <JsonEditor viewOnly data={value} collapse={1} rootFontSize={12} />
17
+ </DropdownMenuContent>
18
+ </DropdownMenu>
19
+ );
5
20
  }
@@ -1,5 +1,5 @@
1
1
  import { DashboardFormComponentProps } from '@/vdb/framework/form-engine/form-engine-types.js';
2
- import { Trans } from '@/vdb/lib/trans.js';
2
+ import { Trans } from '@lingui/react/macro';
3
3
 
4
4
  export type CombinationModeInputProps = DashboardFormComponentProps & {
5
5
  position?: number;
@@ -1,6 +1,5 @@
1
1
  import { Button } from '@/vdb/components/ui/button.js';
2
2
  import { DashboardFormComponentProps } from '@/vdb/framework/form-engine/form-engine-types.js';
3
- import { useLingui } from '@/vdb/lib/trans.js';
4
3
  import {
5
4
  closestCenter,
6
5
  DndContext,
@@ -18,6 +17,7 @@ import {
18
17
  verticalListSortingStrategy,
19
18
  } from '@dnd-kit/sortable';
20
19
  import { CSS } from '@dnd-kit/utilities';
20
+ import { useLingui } from '@lingui/react/macro';
21
21
  import { GripVertical, Plus, X } from 'lucide-react';
22
22
  import React, { useCallback, useEffect, useMemo, useState } from 'react';
23
23
  import { ControllerRenderProps } from 'react-hook-form';
@@ -53,7 +53,7 @@ function SortableItem({
53
53
  field,
54
54
  isFullWidth = false,
55
55
  }: Readonly<SortableItemProps>) {
56
- const { i18n } = useLingui();
56
+ const { t } = useLingui();
57
57
  const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
58
58
  id: itemWithId._id,
59
59
  disabled,
@@ -69,7 +69,7 @@ function SortableItem({
69
69
  {...attributes}
70
70
  {...listeners}
71
71
  className="cursor-move text-muted-foreground hover:text-foreground transition-colors"
72
- title={i18n.t('Drag to reorder')}
72
+ title={t`Drag to reorder`}
73
73
  >
74
74
  <GripVertical className="h-4 w-4" />
75
75
  </div>
@@ -82,7 +82,7 @@ function SortableItem({
82
82
  size="sm"
83
83
  onClick={() => onRemove(itemWithId._id)}
84
84
  className="h-6 w-6 p-0 text-muted-foreground hover:text-destructive transition-colors opacity-0 group-hover:opacity-100"
85
- title={i18n.t('Remove item')}
85
+ title={t`Remove item`}
86
86
  >
87
87
  <X className="h-3 w-3" />
88
88
  </Button>
@@ -169,9 +169,13 @@ function convertToFlatArray(itemsWithIds: ListItemWithId[]): any[] {
169
169
  return itemsWithIds.map(item => item.value);
170
170
  }
171
171
 
172
- export const CustomFieldListInput = ({ renderInput, defaultValue, ...fieldProps }: CustomFieldListInputProps) => {
172
+ export const CustomFieldListInput = ({
173
+ renderInput,
174
+ defaultValue,
175
+ ...fieldProps
176
+ }: CustomFieldListInputProps) => {
173
177
  const { value, onChange, disabled } = fieldProps;
174
- const { i18n } = useLingui();
178
+ const { t } = useLingui();
175
179
  const sensors = useSensors(
176
180
  useSensor(PointerSensor),
177
181
  useSensor(KeyboardSensor, {
@@ -282,7 +286,7 @@ export const CustomFieldListInput = ({ renderInput, defaultValue, ...fieldProps
282
286
  {!disabled && (
283
287
  <Button type="button" variant="outline" size="sm" onClick={handleAddItem} className="w-full">
284
288
  <Plus className="h-4 w-4 mr-2" />
285
- {i18n.t('Add item')}
289
+ {t`Add item`}
286
290
  </Button>
287
291
  )}
288
292
  </div>
@@ -6,13 +6,11 @@ import { CustomerGroupSelector } from '../shared/customer-group-selector.js';
6
6
 
7
7
  import { DashboardFormComponentProps } from '@/vdb/framework/form-engine/form-engine-types.js';
8
8
 
9
- const customerGroupsDocument = graphql(`
10
- query GetCustomerGroups($options: CustomerGroupListOptions) {
11
- customerGroups(options: $options) {
12
- items {
13
- id
14
- name
15
- }
9
+ const customerGroupDocument = graphql(`
10
+ query GetCustomerGroup($id: ID!) {
11
+ customerGroup(id: $id) {
12
+ id
13
+ name
16
14
  }
17
15
  }
18
16
  `);
@@ -22,47 +20,43 @@ export interface CustomerGroup {
22
20
  name: string;
23
21
  }
24
22
 
25
- export function CustomerGroupInput({ value, onChange, disabled }: Readonly<DashboardFormComponentProps>) {
26
- const ids = decodeIds(value);
27
- const { data: groups } = useQuery({
28
- queryKey: ['customerGroups', ids],
23
+ export function CustomerGroupInput({
24
+ value,
25
+ onChange,
26
+ disabled,
27
+ fieldDef,
28
+ }: Readonly<DashboardFormComponentProps>) {
29
+ console.log(fieldDef);
30
+ const { data } = useQuery({
31
+ queryKey: ['customerGroups', value],
29
32
  queryFn: () =>
30
- api.query(customerGroupsDocument, {
31
- options: {
32
- filter: {
33
- id: { in: ids },
34
- },
35
- },
33
+ api.query(customerGroupDocument, {
34
+ id: value,
36
35
  }),
36
+ enabled: !!value,
37
37
  });
38
38
 
39
39
  const onValueSelectHandler = (value: CustomerGroup) => {
40
- const newIds = new Set([...ids, value.id]);
41
- onChange(JSON.stringify(Array.from(newIds)));
40
+ onChange(value.id);
42
41
  };
43
42
 
44
- const onValueRemoveHandler = (id: string) => {
45
- const newIds = new Set(ids.filter(existingId => existingId !== id));
46
- onChange(JSON.stringify(Array.from(newIds)));
43
+ const onValueRemoveHandler = () => {
44
+ onChange(null);
47
45
  };
48
46
 
49
47
  return (
50
48
  <div>
51
49
  <div className="flex flex-wrap gap-2 mb-2">
52
- {groups?.customerGroups.items.map(group => (
53
- <CustomerGroupChip key={group.id} group={group} onRemove={onValueRemoveHandler} />
54
- ))}
50
+ {data?.customerGroup ? (
51
+ <CustomerGroupChip
52
+ key={data.customerGroup.id}
53
+ group={data.customerGroup}
54
+ onRemove={onValueRemoveHandler}
55
+ />
56
+ ) : null}
55
57
  </div>
56
58
 
57
59
  <CustomerGroupSelector onSelect={onValueSelectHandler} readOnly={disabled} />
58
60
  </div>
59
61
  );
60
62
  }
61
-
62
- function decodeIds(idsString: string): string[] {
63
- try {
64
- return JSON.parse(idsString);
65
- } catch (error) {
66
- return [];
67
- }
68
- }
@@ -2,15 +2,34 @@
2
2
 
3
3
  import { format } from 'date-fns';
4
4
  import * as React from 'react';
5
+ import { useEffect, useState } from 'react';
5
6
 
6
7
  import { Button } from '@/vdb/components/ui/button.js';
7
8
  import { Calendar } from '@/vdb/components/ui/calendar.js';
8
9
  import { Popover, PopoverContent, PopoverTrigger } from '@/vdb/components/ui/popover.js';
9
10
  import { ScrollArea, ScrollBar } from '@/vdb/components/ui/scroll-area.js';
10
11
  import { DashboardFormComponentProps } from '@/vdb/framework/form-engine/form-engine-types.js';
12
+ import { isReadonlyField } from '@/vdb/framework/form-engine/utils.js';
13
+ import { useDisplayLocale } from '@/vdb/hooks/use-display-locale.js';
11
14
  import { cn } from '@/vdb/lib/utils.js';
15
+ import type { Locale } from 'date-fns/locale';
12
16
  import { CalendarClock } from 'lucide-react';
13
- import { isReadonlyField } from '@/vdb/framework/form-engine/utils.js';
17
+
18
+ /**
19
+ * @description
20
+ * Returns a `Locale` object that can be passed to the react-day-picker
21
+ * `locale` prop.
22
+ */
23
+ export function useDayPickerLocale() {
24
+ const { bcp47Tag } = useDisplayLocale();
25
+ const [calendarLocale, setCalendarLocale] = useState<Locale | undefined>(undefined);
26
+ useEffect(() => {
27
+ import('react-day-picker/locale').then(mod => {
28
+ setCalendarLocale(bcpTagToDatePickerLocale(bcp47Tag, mod));
29
+ });
30
+ }, [bcp47Tag]);
31
+ return calendarLocale;
32
+ }
14
33
 
15
34
  /**
16
35
  * @description
@@ -21,6 +40,7 @@ import { isReadonlyField } from '@/vdb/framework/form-engine/utils.js';
21
40
  */
22
41
  export function DateTimeInput({ value, onChange, fieldDef }: Readonly<DashboardFormComponentProps>) {
23
42
  const readOnly = isReadonlyField(fieldDef);
43
+ const locale = useDayPickerLocale();
24
44
  const date = value && value instanceof Date ? value.toISOString() : (value ?? '');
25
45
  const [isOpen, setIsOpen] = React.useState(false);
26
46
 
@@ -65,6 +85,7 @@ export function DateTimeInput({ value, onChange, fieldDef }: Readonly<DashboardF
65
85
  <div className="sm:flex">
66
86
  <Calendar
67
87
  mode="single"
88
+ locale={locale}
68
89
  selected={new Date(date)}
69
90
  onSelect={handleDateSelect}
70
91
  initialFocus
@@ -137,3 +158,21 @@ export function DateTimeInput({ value, onChange, fieldDef }: Readonly<DashboardF
137
158
  </Popover>
138
159
  );
139
160
  }
161
+
162
+ function bcpTagToDatePickerLocale(
163
+ tag: string,
164
+ module: typeof import('react-day-picker/locale'),
165
+ ): Locale | undefined {
166
+ switch (tag) {
167
+ case 'zh-Hans':
168
+ return module.zhCN;
169
+ case 'zh-Hant':
170
+ return module.zhTW;
171
+ case 'pt-BR':
172
+ return module.ptBR;
173
+ default: {
174
+ const lang = tag.split('-').at(0);
175
+ return lang ? module[lang as keyof typeof module] : undefined;
176
+ }
177
+ }
178
+ }
@@ -1,5 +1,5 @@
1
1
  import { graphql } from '@/vdb/graphql/graphql.js';
2
- import { Trans, useLingui } from '@/vdb/lib/trans.js';
2
+ import { Trans, useLingui } from '@lingui/react/macro';
3
3
  import { RelationCustomFieldConfig } from '@vendure/common/lib/generated-types';
4
4
  import { ControllerRenderProps } from 'react-hook-form';
5
5
  import { MultiRelationInput, SingleRelationInput } from './relation-input.js';
@@ -100,10 +100,11 @@ function createBaseEntityConfig(
100
100
  labelKey: 'name' | 'code' | 'emailAddress' = 'name',
101
101
  searchField: string = 'name',
102
102
  ) {
103
+ const entityNameLower = entityName.toLowerCase();
103
104
  return {
104
105
  idKey: 'id',
105
106
  labelKey,
106
- placeholder: i18n.t(`Search ${entityName.toLowerCase()}s...`),
107
+ placeholder: i18n`Search ${entityNameLower}...`,
107
108
  buildSearchFilter: (term: string) => ({
108
109
  [searchField]: { contains: term },
109
110
  }),
@@ -563,12 +564,12 @@ export function DefaultRelationInput({
563
564
  ref,
564
565
  disabled,
565
566
  }: Readonly<DashboardFormComponentProps>) {
566
- const { i18n } = useLingui();
567
+ const { t } = useLingui();
567
568
  if (!fieldDef || !isRelationCustomFieldConfig(fieldDef)) {
568
569
  return null;
569
570
  }
570
571
  const entityName = fieldDef.entity;
571
- const ENTITY_CONFIGS = createEntityConfigs(i18n);
572
+ const ENTITY_CONFIGS = createEntityConfigs(t);
572
573
  const config = ENTITY_CONFIGS[entityName as keyof typeof ENTITY_CONFIGS];
573
574
 
574
575
  if (!config) {
@@ -15,3 +15,6 @@ export * from './product-multi-selector-input.js';
15
15
  // Relation selector components
16
16
  export * from './relation-input.js';
17
17
  export * from './relation-selector.js';
18
+
19
+ // Slug input component
20
+ export * from './slug-input.js';
@@ -13,7 +13,7 @@ import { Input } from '@/vdb/components/ui/input.js';
13
13
  import { DashboardFormComponent } from '@/vdb/framework/form-engine/form-engine-types.js';
14
14
  import { api } from '@/vdb/graphql/api.js';
15
15
  import { graphql } from '@/vdb/graphql/graphql.js';
16
- import { Trans } from '@/vdb/lib/trans.js';
16
+ import { Trans } from '@lingui/react/macro';
17
17
  import { useQuery } from '@tanstack/react-query';
18
18
  import { useDebounce } from '@uidotdev/usehooks';
19
19
  import { Plus, X } from 'lucide-react';
@@ -93,13 +93,13 @@ function EmptyState() {
93
93
  }
94
94
 
95
95
  function ProductList({
96
- items,
97
- mode,
98
- selectedIds,
99
- getItemId,
100
- getItemName,
101
- toggleSelection,
102
- }: Readonly<{
96
+ items,
97
+ mode,
98
+ selectedIds,
99
+ getItemId,
100
+ getItemName,
101
+ toggleSelection,
102
+ }: Readonly<{
103
103
  items: SearchItem[];
104
104
  mode: 'product' | 'variant';
105
105
  selectedIds: Set<string>;
@@ -163,12 +163,12 @@ function ProductList({
163
163
  }
164
164
 
165
165
  function ProductMultiSelectorDialog({
166
- mode,
167
- initialSelectionIds = [],
168
- onSelectionChange,
169
- open,
170
- onOpenChange,
171
- }: Readonly<ProductMultiSelectorProps>) {
166
+ mode,
167
+ initialSelectionIds = [],
168
+ onSelectionChange,
169
+ open,
170
+ onOpenChange,
171
+ }: Readonly<ProductMultiSelectorProps>) {
172
172
  const [searchTerm, setSearchTerm] = useState('');
173
173
  const [selectedItems, setSelectedItems] = useState<SearchItem[]>([]);
174
174
  const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
@@ -10,7 +10,7 @@ import {
10
10
  import { Popover, PopoverContent, PopoverTrigger } from '@/vdb/components/ui/popover.js';
11
11
  import { getQueryName } from '@/vdb/framework/document-introspection/get-document-structure.js';
12
12
  import { api } from '@/vdb/graphql/api.js';
13
- import { Trans } from '@/vdb/lib/trans.js';
13
+ import { Trans } from '@lingui/react/macro';
14
14
  import { cn } from '@/vdb/lib/utils.js';
15
15
  import { useInfiniteQuery } from '@tanstack/react-query';
16
16
  import { useDebounce } from '@uidotdev/usehooks';
@@ -6,7 +6,7 @@ import {
6
6
  } from '@/vdb/framework/form-engine/form-engine-types.js';
7
7
  import { isReadonlyField, isStringFieldWithOptions } from '@/vdb/framework/form-engine/utils.js';
8
8
  import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
9
- import { Trans } from '@/vdb/lib/trans.js';
9
+ import { Trans } from '@lingui/react/macro';
10
10
  import React from 'react';
11
11
  import { MultiSelect } from '../shared/multi-select.js';
12
12
 
@@ -0,0 +1,290 @@
1
+ import { Button } from '@/vdb/components/ui/button.js';
2
+ import { Input } from '@/vdb/components/ui/input.js';
3
+ import { DashboardFormComponentProps } from '@/vdb/framework/form-engine/form-engine-types.js';
4
+ import { isReadonlyField } from '@/vdb/framework/form-engine/utils.js';
5
+ import { api } from '@/vdb/graphql/api.js';
6
+ import { graphql } from '@/vdb/graphql/graphql.js';
7
+ import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
8
+ import { cn } from '@/vdb/lib/utils.js';
9
+ import { useLingui } from '@lingui/react/macro';
10
+ import { useQuery } from '@tanstack/react-query';
11
+ import { useDebounce } from '@uidotdev/usehooks';
12
+ import { Edit, Lock, RefreshCw } from 'lucide-react';
13
+ import { useEffect, useState } from 'react';
14
+ import { useFormContext, useWatch } from 'react-hook-form';
15
+
16
+ const slugForEntityDocument = graphql(`
17
+ query SlugForEntity($input: SlugForEntityInput!) {
18
+ slugForEntity(input: $input)
19
+ }
20
+ `);
21
+
22
+ function resolveWatchFieldPath(
23
+ currentFieldName: string,
24
+ watchFieldName: string,
25
+ formValues: any,
26
+ contentLanguage: string,
27
+ ): string {
28
+ const translationsMatch = currentFieldName.match(/^translations\.(\d+)\./);
29
+
30
+ if (translationsMatch) {
31
+ const index = translationsMatch[1];
32
+
33
+ if (formValues?.translations?.[index]?.hasOwnProperty(watchFieldName)) {
34
+ return `translations.${index}.${watchFieldName}`;
35
+ }
36
+
37
+ if (formValues?.hasOwnProperty(watchFieldName)) {
38
+ return watchFieldName;
39
+ }
40
+
41
+ return `translations.${index}.${watchFieldName}`;
42
+ }
43
+
44
+ if (formValues?.translations && Array.isArray(formValues.translations)) {
45
+ const translations = formValues.translations;
46
+ const existingIndex = translations.findIndex(
47
+ (translation: any) => translation?.languageCode === contentLanguage,
48
+ );
49
+ const index = existingIndex === -1 ? (translations.length > 0 ? 0 : -1) : existingIndex;
50
+
51
+ if (index >= 0 && translations[index]?.hasOwnProperty(watchFieldName)) {
52
+ return `translations.${index}.${watchFieldName}`;
53
+ }
54
+ }
55
+
56
+ return watchFieldName;
57
+ }
58
+
59
+ export interface SlugInputProps extends DashboardFormComponentProps {
60
+ /**
61
+ * @description
62
+ * The name of the entity (e.g., 'Product', 'Collection')
63
+ */
64
+ entityName: string;
65
+ /**
66
+ * @description
67
+ * The name of the field to check for uniqueness (e.g., 'slug', 'code')
68
+ */
69
+ fieldName: string;
70
+ /**
71
+ * @description
72
+ * The name of the field to watch for changes (e.g., 'name', 'title', 'enabled').
73
+ * The component automatically resolves whether this field exists in translations
74
+ * or on the base entity. For translatable fields like 'name', it will watch
75
+ * 'translations.X.name'. For non-translatable fields like 'enabled', it will
76
+ * watch 'enabled' directly.
77
+ */
78
+ watchFieldName: string;
79
+ /**
80
+ * @description
81
+ * Optional entity ID for updates (excludes from uniqueness check)
82
+ */
83
+ entityId?: string | number;
84
+ /**
85
+ * @description
86
+ * Whether the input should start in readonly mode. Default: true
87
+ */
88
+ defaultReadonly?: boolean;
89
+
90
+ /**
91
+ * @description Class name for the <Input> component
92
+ */
93
+ className?: string;
94
+ }
95
+
96
+ /**
97
+ * @description
98
+ * A component for generating and displaying slugs based on a watched field.
99
+ * The component watches a source field for changes, debounces the input,
100
+ * and generates a unique slug via the Admin API. The slug is only auto-generated
101
+ * when it's empty. For existing slugs, a regenerate button allows manual regeneration.
102
+ * The input is readonly by default but can be made editable with a toggle button.
103
+ *
104
+ * @example
105
+ * ```tsx
106
+ * // In a TranslatableFormFieldWrapper context with translatable field
107
+ * <SlugInput
108
+ * {...field}
109
+ * entityName="Product"
110
+ * fieldName="slug"
111
+ * watchFieldName="name" // Automatically resolves to "translations.X.name"
112
+ * entityId={productId}
113
+ * />
114
+ *
115
+ * // In a TranslatableFormFieldWrapper context with non-translatable field
116
+ * <SlugInput
117
+ * {...field}
118
+ * entityName="Product"
119
+ * fieldName="slug"
120
+ * watchFieldName="enabled" // Uses "enabled" directly (base entity field)
121
+ * entityId={productId}
122
+ * />
123
+ *
124
+ * // For non-translatable entities
125
+ * <SlugInput
126
+ * {...field}
127
+ * entityName="Channel"
128
+ * fieldName="code"
129
+ * watchFieldName="name" // Uses "name" directly
130
+ * entityId={channelId}
131
+ * />
132
+ * ```
133
+ *
134
+ * @docsCategory form-components
135
+ * @docsPage SlugInput
136
+ */
137
+ export function SlugInput({
138
+ value,
139
+ onChange,
140
+ fieldDef,
141
+ entityName,
142
+ fieldName,
143
+ watchFieldName,
144
+ entityId,
145
+ defaultReadonly = true,
146
+ className,
147
+ name,
148
+ ...props
149
+ }: SlugInputProps) {
150
+ const { t } = useLingui();
151
+ const form = useFormContext();
152
+ const { contentLanguage } = useUserSettings().settings;
153
+ const isFormReadonly = isReadonlyField(fieldDef);
154
+ const [isManuallyReadonly, setIsManuallyReadonly] = useState(defaultReadonly);
155
+ const isReadonly = isFormReadonly || isManuallyReadonly;
156
+
157
+ const actualWatchFieldName = resolveWatchFieldPath(
158
+ name || '',
159
+ watchFieldName,
160
+ form?.getValues(),
161
+ contentLanguage,
162
+ );
163
+
164
+ const watchedValue = useWatch({
165
+ control: form?.control,
166
+ name: actualWatchFieldName,
167
+ });
168
+
169
+ const watchFieldState = form.getFieldState(actualWatchFieldName);
170
+ const debouncedWatchedValue = useDebounce(watchedValue, 500);
171
+
172
+ const shouldAutoGenerate = isReadonly && !value && watchFieldState.isDirty;
173
+
174
+ const {
175
+ data: generatedSlug,
176
+ isLoading,
177
+ refetch,
178
+ } = useQuery({
179
+ queryKey: ['slugForEntity', entityName, fieldName, debouncedWatchedValue, entityId],
180
+ queryFn: async () => {
181
+ if (!debouncedWatchedValue) {
182
+ return '';
183
+ }
184
+
185
+ const result = await api.query(slugForEntityDocument, {
186
+ input: {
187
+ entityName,
188
+ fieldName,
189
+ inputValue: debouncedWatchedValue,
190
+ entityId: entityId?.toString(),
191
+ },
192
+ });
193
+
194
+ return result.slugForEntity;
195
+ },
196
+ enabled: !!debouncedWatchedValue && shouldAutoGenerate,
197
+ });
198
+
199
+ useEffect(() => {
200
+ if (isReadonly && generatedSlug && generatedSlug !== value) {
201
+ onChange?.(generatedSlug);
202
+ }
203
+ }, [generatedSlug, isReadonly, value, onChange]);
204
+
205
+ const toggleReadonly = () => {
206
+ if (!isFormReadonly) {
207
+ setIsManuallyReadonly(!isManuallyReadonly);
208
+ }
209
+ };
210
+
211
+ const handleRegenerate = async () => {
212
+ if (watchedValue) {
213
+ const result = await refetch();
214
+ if (result.data) {
215
+ onChange?.(result.data);
216
+ }
217
+ }
218
+ };
219
+
220
+ const handleChange = (newValue: string) => {
221
+ onChange?.(newValue);
222
+ };
223
+
224
+ const displayValue = isReadonly && generatedSlug ? generatedSlug : value || '';
225
+ const showLoading = isLoading && isReadonly;
226
+
227
+ return (
228
+ <div className="relative flex items-center gap-2">
229
+ <div className="flex-1 relative">
230
+ <Input
231
+ value={displayValue}
232
+ onChange={e => handleChange(e.target.value)}
233
+ disabled={isReadonly}
234
+ placeholder={
235
+ isReadonly
236
+ ? value
237
+ ? t`Slug is set`
238
+ : t`Slug will be generated automatically...`
239
+ : t`Enter slug manually`
240
+ }
241
+ className={cn(
242
+ 'pr-8',
243
+ isReadonly && 'bg-muted text-muted-foreground',
244
+ showLoading && 'text-muted-foreground',
245
+ className,
246
+ )}
247
+ {...props}
248
+ />
249
+ {showLoading && (
250
+ <div className="absolute right-3 top-1/2 -translate-y-1/2">
251
+ <div className="h-4 w-4 animate-spin rounded-full border-2 border-muted-foreground border-t-transparent" />
252
+ </div>
253
+ )}
254
+ </div>
255
+
256
+ {!isFormReadonly && (
257
+ <>
258
+ {isManuallyReadonly && value && (
259
+ <Button
260
+ type="button"
261
+ variant="outline"
262
+ size="sm"
263
+ onClick={handleRegenerate}
264
+ className="shrink-0"
265
+ title={t`Regenerate slug from source field`}
266
+ aria-label={t`Regenerate slug from source field`}
267
+ disabled={!watchedValue || isLoading}
268
+ >
269
+ <RefreshCw className="h-4 w-4" />
270
+ </Button>
271
+ )}
272
+
273
+ <Button
274
+ type="button"
275
+ variant="outline"
276
+ size="sm"
277
+ onClick={toggleReadonly}
278
+ className="shrink-0"
279
+ title={isManuallyReadonly ? t`Edit slug manually` : t`Generate slug automatically`}
280
+ aria-label={
281
+ isManuallyReadonly ? t`Edit slug manually` : t`Generate slug automatically`
282
+ }
283
+ >
284
+ {isManuallyReadonly ? <Edit className="h-4 w-4" /> : <Lock className="h-4 w-4" />}
285
+ </Button>
286
+ </>
287
+ )}
288
+ </div>
289
+ );
290
+ }