spree_admin 5.3.3 → 5.4.0.beta

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. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/app/controllers/concerns/spree/admin/order_concern.rb +1 -1
  4. data/app/controllers/spree/admin/action_text/video_embeds_controller.rb +1 -1
  5. data/app/controllers/spree/admin/addresses_controller.rb +1 -1
  6. data/app/controllers/spree/admin/admin_users_controller.rb +3 -3
  7. data/app/controllers/spree/admin/api_keys_controller.rb +56 -0
  8. data/app/controllers/spree/admin/assets_controller.rb +2 -2
  9. data/app/controllers/spree/admin/base_controller.rb +4 -4
  10. data/app/controllers/spree/admin/classifications_controller.rb +5 -4
  11. data/app/controllers/spree/admin/coupon_codes_controller.rb +1 -1
  12. data/app/controllers/spree/admin/customer_group_users_controller.rb +3 -2
  13. data/app/controllers/spree/admin/dashboard_controller.rb +2 -1
  14. data/app/controllers/spree/admin/digital_assets_controller.rb +1 -1
  15. data/app/controllers/spree/admin/gift_cards_controller.rb +5 -5
  16. data/app/controllers/spree/admin/import_mappings_controller.rb +1 -1
  17. data/app/controllers/spree/admin/import_rows_controller.rb +1 -1
  18. data/app/controllers/spree/admin/integrations_controller.rb +1 -1
  19. data/app/controllers/spree/admin/invitations_controller.rb +6 -5
  20. data/app/controllers/spree/admin/line_items_controller.rb +1 -1
  21. data/app/controllers/spree/admin/markets_controller.rb +28 -0
  22. data/app/controllers/spree/admin/option_values_controller.rb +1 -1
  23. data/app/controllers/spree/admin/orders/adjustments_controller.rb +4 -4
  24. data/app/controllers/spree/admin/orders/billing_address_controller.rb +4 -4
  25. data/app/controllers/spree/admin/orders/customer_returns_controller.rb +1 -1
  26. data/app/controllers/spree/admin/orders/shipping_address_controller.rb +3 -3
  27. data/app/controllers/spree/admin/orders/user_controller.rb +4 -4
  28. data/app/controllers/spree/admin/orders_controller.rb +8 -4
  29. data/app/controllers/spree/admin/payments_controller.rb +3 -3
  30. data/app/controllers/spree/admin/price_list_products_controller.rb +1 -1
  31. data/app/controllers/spree/admin/price_rules_controller.rb +1 -1
  32. data/app/controllers/spree/admin/products_controller.rb +24 -15
  33. data/app/controllers/spree/admin/profile_controller.rb +1 -1
  34. data/app/controllers/spree/admin/promotion_actions_controller.rb +1 -1
  35. data/app/controllers/spree/admin/promotion_rules_controller.rb +1 -1
  36. data/app/controllers/spree/admin/promotions_controller.rb +1 -1
  37. data/app/controllers/spree/admin/refunds_controller.rb +1 -1
  38. data/app/controllers/spree/admin/reimbursements_controller.rb +2 -2
  39. data/app/controllers/spree/admin/resource_controller.rb +30 -11
  40. data/app/controllers/spree/admin/shipments_controller.rb +3 -3
  41. data/app/controllers/spree/admin/shipping_methods_controller.rb +1 -1
  42. data/app/controllers/spree/admin/store_credits_controller.rb +5 -5
  43. data/app/controllers/spree/admin/stores_controller.rb +1 -32
  44. data/app/controllers/spree/admin/taxons_controller.rb +3 -3
  45. data/app/controllers/spree/admin/translations_controller.rb +1 -0
  46. data/app/controllers/spree/admin/users_controller.rb +2 -2
  47. data/app/helpers/spree/admin/api_keys_helper.rb +32 -0
  48. data/app/helpers/spree/admin/base_helper.rb +6 -1
  49. data/app/helpers/spree/admin/json_preview_helper.rb +29 -25
  50. data/app/helpers/spree/admin/orders_filters_helper.rb +1 -1
  51. data/app/helpers/spree/admin/sortable_tree_helper.rb +1 -1
  52. data/app/helpers/spree/admin/stores_helper.rb +0 -4
  53. data/app/helpers/spree/admin/table_helper.rb +1 -1
  54. data/app/javascript/spree/admin/controllers/autocomplete_select_controller.js +5 -1
  55. data/app/javascript/spree/admin/controllers/bulk_editor_controller.js +0 -86
  56. data/app/javascript/spree/admin/controllers/money_field_controller.js +1 -23
  57. data/app/javascript/spree/admin/controllers/select_controller.js +4 -0
  58. data/app/javascript/spree/admin/controllers/variants_form_controller.js +30 -91
  59. data/app/models/spree/admin/table/column.rb +1 -1
  60. data/app/models/spree/admin/table/query_builder.rb +7 -2
  61. data/app/views/spree/admin/api_keys/_details.html.erb +51 -0
  62. data/app/views/spree/admin/api_keys/_form.html.erb +26 -0
  63. data/app/views/spree/admin/api_keys/_token_card.html.erb +28 -0
  64. data/app/views/spree/admin/api_keys/_usage_info.html.erb +16 -0
  65. data/app/views/spree/admin/api_keys/index.html.erb +9 -0
  66. data/app/views/spree/admin/api_keys/show.html.erb +26 -0
  67. data/app/views/spree/admin/classifications/_classification.html.erb +2 -2
  68. data/app/views/spree/admin/classifications/index.html.erb +1 -1
  69. data/app/views/spree/admin/classifications/new.html.erb +1 -1
  70. data/app/views/spree/admin/json_previews/show.html.erb +6 -6
  71. data/app/views/spree/admin/markets/_form.html.erb +28 -0
  72. data/app/views/spree/admin/markets/edit.html.erb +1 -0
  73. data/app/views/spree/admin/markets/index.html.erb +9 -0
  74. data/app/views/spree/admin/markets/new.html.erb +1 -0
  75. data/app/views/spree/admin/orders/billing_address/_form.html.erb +2 -2
  76. data/app/views/spree/admin/orders/shipping_address/_form.html.erb +2 -2
  77. data/app/views/spree/admin/payment_methods/_form.html.erb +0 -12
  78. data/app/views/spree/admin/price_rules/forms/_market_rule.html.erb +7 -0
  79. data/app/views/spree/admin/products/_form.html.erb +0 -1
  80. data/app/views/spree/admin/products/form/_variants.html.erb +4 -13
  81. data/app/views/spree/admin/products/form/variants/_variant_template.html.erb +1 -1
  82. data/app/views/spree/admin/promotion_rules/forms/_country.html.erb +1 -1
  83. data/app/views/spree/admin/shared/_content_header.html.erb +1 -1
  84. data/app/views/spree/admin/shared/sidebar/_store_dropdown.html.erb +0 -33
  85. data/app/views/spree/admin/shared/sidebar/_store_nav.html.erb +2 -2
  86. data/app/views/spree/admin/shared/sortable_tree/_taxonomy.html.erb +2 -2
  87. data/app/views/spree/admin/stores/form/_basic.html.erb +10 -7
  88. data/app/views/spree/admin/stores/form/_checkout.html.erb +5 -8
  89. data/app/views/spree/admin/tables/_table.html.erb +3 -4
  90. data/app/views/spree/admin/tables/columns/_api_key_status.html.erb +2 -0
  91. data/app/views/spree/admin/tables/columns/_api_key_type.html.erb +2 -0
  92. data/app/views/spree/admin/taxonomies/show.html.erb +1 -1
  93. data/app/views/spree/admin/taxons/_form.html.erb +2 -2
  94. data/app/views/spree/admin/taxons/edit.html.erb +1 -2
  95. data/app/views/spree/admin/taxons/update.turbo_stream.erb +1 -1
  96. data/app/views/spree/admin/users/_billing.html.erb +2 -2
  97. data/app/views/spree/admin/users/_shipping.html.erb +1 -1
  98. data/app/views/spree/admin/variants/_variant.html.erb +1 -1
  99. data/config/brakeman.ignore +28 -0
  100. data/config/initializers/spree_admin_navigation.rb +16 -16
  101. data/config/initializers/spree_admin_tables.rb +112 -18
  102. data/config/locales/en.yml +31 -0
  103. data/config/routes.rb +6 -6
  104. data/lib/spree/admin/engine.rb +1 -0
  105. data/lib/spree/admin/tailwind_helper.rb +11 -1
  106. data/lib/spree/admin/testing_support/tom_select.rb +1 -1
  107. metadata +30 -27
  108. data/LICENSE.md +0 -13
  109. data/app/controllers/spree/admin/custom_domains_controller.rb +0 -21
  110. data/app/controllers/spree/admin/oauth_applications_controller.rb +0 -23
  111. data/app/views/spree/admin/custom_domains/_custom_domain.html.erb +0 -11
  112. data/app/views/spree/admin/custom_domains/_custom_domains.html.erb +0 -19
  113. data/app/views/spree/admin/custom_domains/_form.html.erb +0 -7
  114. data/app/views/spree/admin/custom_domains/index.html.erb +0 -65
  115. data/app/views/spree/admin/oauth_applications/_form.html.erb +0 -6
  116. data/app/views/spree/admin/oauth_applications/_table_header.html.erb +0 -7
  117. data/app/views/spree/admin/oauth_applications/_table_row.html.erb +0 -34
  118. data/app/views/spree/admin/oauth_applications/create.turbo_stream.erb +0 -31
  119. data/app/views/spree/admin/oauth_applications/edit.html.erb +0 -1
  120. data/app/views/spree/admin/oauth_applications/index.html.erb +0 -21
  121. data/app/views/spree/admin/oauth_applications/new.html.erb +0 -1
  122. data/app/views/spree/admin/products/form/_stores.html.erb +0 -27
  123. data/app/views/spree/admin/stores/new.html.erb +0 -128
  124. data/app/views/spree/admin/stores/new.turbo_stream.erb +0 -1
  125. /data/app/views/spree/admin/{custom_domains → api_keys}/edit.html.erb +0 -0
  126. /data/app/views/spree/admin/{custom_domains → api_keys}/new.html.erb +0 -0
@@ -79,21 +79,16 @@ export default class extends Controller {
79
79
  this.boundHandleBeforeUnload = this.handleBeforeUnload.bind(this)
80
80
  this.boundHandleMouseUp = this.handleMouseUp.bind(this)
81
81
  this.boundHandleMouseMove = this.handleMouseMove.bind(this)
82
- this.boundNormalizeBeforeSubmit = this.normalizeBeforeSubmit.bind(this)
83
82
 
84
83
  window.addEventListener('beforeunload', this.boundHandleBeforeUnload)
85
84
  document.addEventListener('mouseup', this.boundHandleMouseUp)
86
85
  document.addEventListener('mousemove', this.boundHandleMouseMove)
87
-
88
- // Listen for form submission to normalize values
89
- this.element.addEventListener('submit', this.boundNormalizeBeforeSubmit)
90
86
  }
91
87
 
92
88
  disconnect() {
93
89
  window.removeEventListener('beforeunload', this.boundHandleBeforeUnload)
94
90
  document.removeEventListener('mouseup', this.boundHandleMouseUp)
95
91
  document.removeEventListener('mousemove', this.boundHandleMouseMove)
96
- this.element.removeEventListener('submit', this.boundNormalizeBeforeSubmit)
97
92
  }
98
93
 
99
94
  // ==================== Fill Handle ====================
@@ -783,85 +778,4 @@ export default class extends Controller {
783
778
  return this.dirtyInputs.size > 0
784
779
  }
785
780
 
786
- // ==================== Locale-aware Number Handling ====================
787
-
788
- /**
789
- * Normalizes a locale-formatted number string to standard decimal format
790
- * e.g., "1.234,56" (German) -> "1234.56"
791
- * e.g., "1,234.56" (English) -> "1234.56"
792
- * @param {string} value - The locale-formatted number string
793
- * @returns {string} The normalized number string with "." as decimal separator
794
- */
795
- normalizeNumber(value) {
796
- if (value === null || value === undefined) return ''
797
-
798
- let stringValue = String(value).trim()
799
- if (stringValue === '') return ''
800
-
801
- // Detect the decimal separator by finding the last separator character
802
- // This handles both "1,234.56" (en) and "1.234,56" (de/pl) formats
803
- const lastComma = stringValue.lastIndexOf(',')
804
- const lastDot = stringValue.lastIndexOf('.')
805
-
806
- let decimalSeparator = '.'
807
- let thousandsSeparator = ','
808
-
809
- // If comma comes after dot, comma is the decimal separator (European format)
810
- // Also treat comma as decimal if there's no dot and comma has 1-2 digits after it
811
- if (lastComma > lastDot) {
812
- decimalSeparator = ','
813
- thousandsSeparator = '.'
814
- } else if (lastDot === -1 && lastComma !== -1) {
815
- // No dot present, check if comma looks like a decimal separator
816
- // (has 1-3 digits after it, typical for currency)
817
- const afterComma = stringValue.substring(lastComma + 1)
818
- if (/^\d{1,3}$/.test(afterComma)) {
819
- decimalSeparator = ','
820
- thousandsSeparator = '.'
821
- }
822
- }
823
-
824
- // Remove thousands separators
825
- stringValue = stringValue.split(thousandsSeparator).join('')
826
-
827
- // Replace decimal separator with standard "."
828
- if (decimalSeparator !== '.') {
829
- stringValue = stringValue.replace(decimalSeparator, '.')
830
- }
831
-
832
- // Remove any non-numeric characters except "." and "-"
833
- stringValue = stringValue.replace(/[^0-9.\-]/g, '')
834
-
835
- return stringValue
836
- }
837
-
838
- /**
839
- * Formats a number for display using the configured locale
840
- * @param {number|string} value - The number to format
841
- * @returns {string} The formatted number string
842
- */
843
- formatNumber(value) {
844
- if (value === null || value === undefined || value === '') return ''
845
-
846
- const number = parseFloat(value)
847
- if (!Number.isFinite(number)) return ''
848
-
849
- return number.toLocaleString(this.localeValue, {
850
- minimumFractionDigits: 2,
851
- maximumFractionDigits: 2,
852
- useGrouping: false
853
- })
854
- }
855
-
856
- /**
857
- * Normalizes all cell values before form submission
858
- * @param {Event} event - The form submit event
859
- */
860
- normalizeBeforeSubmit(event) {
861
- this.cellTargets.forEach((cell) => {
862
- if (cell.value) {
863
- cell.value = this.normalizeNumber(cell.value)
864
- }
865
- })
866
- }
867
781
  }
@@ -4,11 +4,10 @@ import { Controller } from '@hotwired/stimulus'
4
4
  * MoneyFieldController
5
5
  *
6
6
  * A Stimulus controller for locale-aware money/price input fields.
7
- * Handles formatting for display and normalizing values before form submission.
7
+ * Handles formatting values for display in the user's locale format.
8
8
  *
9
9
  * Features:
10
10
  * - Displays amounts in the user's locale format (e.g., "1.234,56" for German, "1,234.56" for English)
11
- * - Normalizes values to standard decimal format (with "." as decimal separator) before form submission
12
11
  * - Supports optional currency symbol display
13
12
  *
14
13
  * Usage:
@@ -32,23 +31,10 @@ export default class extends Controller {
32
31
  }
33
32
 
34
33
  connect() {
35
- this.form = this.element.closest('form')
36
-
37
- if (this.form) {
38
- this.boundNormalizeBeforeSubmit = this.normalizeBeforeSubmit.bind(this)
39
- this.form.addEventListener('submit', this.boundNormalizeBeforeSubmit)
40
- }
41
-
42
34
  // Format the initial value for display
43
35
  this.formatForDisplay()
44
36
  }
45
37
 
46
- disconnect() {
47
- if (this.form && this.boundNormalizeBeforeSubmit) {
48
- this.form.removeEventListener('submit', this.boundNormalizeBeforeSubmit)
49
- }
50
- }
51
-
52
38
  /**
53
39
  * Called on blur to format the value for display
54
40
  */
@@ -139,12 +125,4 @@ export default class extends Controller {
139
125
  return stringValue
140
126
  }
141
127
 
142
- /**
143
- * Normalizes the value before form submission
144
- * @param {Event} event - The form submit event
145
- */
146
- normalizeBeforeSubmit(event) {
147
- const normalizedValue = this.normalizeValue(this.element.value)
148
- this.element.value = normalizedValue
149
- }
150
128
  }
@@ -106,6 +106,10 @@ export default class extends Controller {
106
106
  if (this.multipleValue) {
107
107
  settings.maxItems = null
108
108
  settings.plugins = ['remove_button']
109
+ settings.onItemAdd = function() {
110
+ this.setTextboxValue('')
111
+ this.refreshOptions(false)
112
+ }
109
113
  if (this.activeOptionValue) settings.items = JSON.parse(this.activeOptionValue)
110
114
  } else {
111
115
  if (this.activeOptionValue) settings.items = [this.activeOptionValue]
@@ -33,8 +33,8 @@ export default class extends CheckboxSelectAll {
33
33
  prices: Object,
34
34
  currentCurrency: String,
35
35
  currencies: Array,
36
- currencyFormats: Object,
37
36
  variantIds: Object,
37
+ variantPrefixIds: Object,
38
38
  currentStockLocationId: String,
39
39
  stockLocations: Array,
40
40
  optionValuesSelectOptions: Array,
@@ -71,19 +71,6 @@ export default class extends CheckboxSelectAll {
71
71
  }
72
72
 
73
73
  this.inventoryFormTarget = document.querySelector('.inventory-form');
74
-
75
- // Add form submit listener to normalize price inputs before submission
76
- this.form = this.element.closest('form')
77
- if (this.form) {
78
- this.boundNormalizePricesBeforeSubmit = this.normalizePricesBeforeSubmit.bind(this)
79
- this.form.addEventListener('submit', this.boundNormalizePricesBeforeSubmit)
80
- }
81
- }
82
-
83
- disconnect() {
84
- if (this.form && this.boundNormalizePricesBeforeSubmit) {
85
- this.form.removeEventListener('submit', this.boundNormalizePricesBeforeSubmit)
86
- }
87
74
  }
88
75
 
89
76
  toggleQuantityTracked() {
@@ -387,6 +374,16 @@ export default class extends CheckboxSelectAll {
387
374
  }
388
375
  }
389
376
 
377
+ formatPrice(event) {
378
+ const value = event.target.value
379
+ if (value === '' || value === null) return
380
+
381
+ const number = this.parseLocaleNumber(value)
382
+ if (!Number.isFinite(number)) return
383
+
384
+ event.target.value = this.formatNumber(number)
385
+ }
386
+
390
387
  replaceBlankWithZero(event) {
391
388
  if (event.target.value === '') {
392
389
  event.target.value = 0
@@ -526,12 +523,12 @@ export default class extends CheckboxSelectAll {
526
523
  const variantTarget = template.querySelector('[data-variants-form-target="variant"]')
527
524
  variantTarget.dataset.variantName = internalName
528
525
 
529
- const variantId = this.variantIdsValue[internalName]
530
- if (variantId) {
526
+ const variantPrefixId = this.variantPrefixIdsValue?.[internalName] || this.variantIdsValue[internalName]
527
+ if (variantPrefixId) {
531
528
  const variantEditButton = variantTarget.querySelector('[data-slot="variantEditButton"]')
532
529
 
533
530
  if (variantEditButton) {
534
- variantEditButton.href = `${Spree.adminPath}/products/${this.productIdValue}/variants/${variantId}/edit`
531
+ variantEditButton.href = `${Spree.adminPath}/products/${this.productIdValue}/variants/${variantPrefixId}/edit`
535
532
  variantEditButton.classList.remove('invisible')
536
533
  }
537
534
  }
@@ -1100,13 +1097,13 @@ export default class extends CheckboxSelectAll {
1100
1097
  if (existingPrice) {
1101
1098
  return {
1102
1099
  ...existingPrice,
1103
- amount: existingPrice.amount ? parseFloat(existingPrice.amount) : existingPrice.amount
1100
+ amount: existingPrice.amount ? this.parseLocaleNumber(existingPrice.amount) : existingPrice.amount
1104
1101
  }
1105
1102
  } else {
1106
1103
  const parentName = variantName.split('/')[0]
1107
1104
  const parentPrices = Object.entries(this.pricesValue)
1108
1105
  .filter(([internalName, prices]) => internalName.startsWith(parentName) && prices[currency.toLowerCase()] !== undefined)
1109
- .map(([_key, prices]) => parseFloat(prices[currency.toLowerCase()].amount))
1106
+ .map(([_key, prices]) => this.parseLocaleNumber(prices[currency.toLowerCase()].amount))
1110
1107
  .sort((priceAmountA, priceAmountB) => priceAmountA - priceAmountB)
1111
1108
 
1112
1109
  return {
@@ -1124,7 +1121,7 @@ export default class extends CheckboxSelectAll {
1124
1121
  ...this.pricesValue[variantName],
1125
1122
  [currency.toLowerCase()]: {
1126
1123
  ...existingPrice,
1127
- amount: parseFloat(newPrice)
1124
+ amount: this.parseLocaleNumber(newPrice)
1128
1125
  }
1129
1126
  }
1130
1127
  }
@@ -1148,23 +1145,13 @@ export default class extends CheckboxSelectAll {
1148
1145
  }
1149
1146
  }
1150
1147
 
1151
- formatNumber(value, currency = null) {
1148
+ formatNumber(value) {
1152
1149
  if (value === null) return ''
1153
1150
  if (typeof value === 'string' && value.trim() === '') return ''
1154
1151
 
1155
1152
  const number = Number(value)
1156
1153
  if (!Number.isFinite(number)) return ''
1157
1154
 
1158
- // Use currency-specific decimal mark if available
1159
- const currencyCode = currency || this.currentCurrencyValue
1160
- const currencyFormat = this.currencyFormatsValue?.[currencyCode]
1161
-
1162
- if (currencyFormat?.decimal_mark) {
1163
- // Format with 2 decimal places and replace . with currency's decimal mark
1164
- return number.toFixed(2).replace('.', currencyFormat.decimal_mark)
1165
- }
1166
-
1167
- // Fallback to locale-based formatting
1168
1155
  return number.toLocaleString(this.localeValue, {
1169
1156
  minimumFractionDigits: 2,
1170
1157
  maximumFractionDigits: 2,
@@ -1172,70 +1159,22 @@ export default class extends CheckboxSelectAll {
1172
1159
  })
1173
1160
  }
1174
1161
 
1175
- /**
1176
- * Normalizes a locale-formatted number string to standard decimal format
1177
- * e.g., "1.234,56" (German) -> "1234.56"
1178
- * e.g., "30,99" (Polish) -> "30.99"
1179
- * @param {string} value - The locale-formatted number string
1180
- * @returns {string} The normalized number string with "." as decimal separator
1181
- */
1182
- normalizeNumber(value) {
1183
- if (value === null || value === undefined) return ''
1184
-
1185
- let stringValue = String(value).trim()
1186
- if (stringValue === '') return ''
1187
-
1188
- // Detect the decimal separator by finding the last separator character
1189
- // This handles both "1,234.56" (en) and "1.234,56" (de/pl) formats
1190
- const lastComma = stringValue.lastIndexOf(',')
1191
- const lastDot = stringValue.lastIndexOf('.')
1192
-
1193
- let decimalSeparator = '.'
1194
- let thousandsSeparator = ','
1195
-
1196
- // If comma comes after dot, comma is the decimal separator (European format)
1197
- // Also treat comma as decimal if there's no dot and comma has 1-3 digits after it
1198
- if (lastComma > lastDot) {
1199
- decimalSeparator = ','
1200
- thousandsSeparator = '.'
1201
- } else if (lastDot === -1 && lastComma !== -1) {
1202
- // No dot present, check if comma looks like a decimal separator
1203
- // (has 1-3 digits after it, typical for currency)
1204
- const afterComma = stringValue.substring(lastComma + 1)
1205
- if (/^\d{1,3}$/.test(afterComma)) {
1206
- decimalSeparator = ','
1207
- thousandsSeparator = '.'
1208
- }
1209
- }
1162
+ parseLocaleNumber(value) {
1163
+ if (value === null || value === undefined) return NaN
1210
1164
 
1211
- // Remove thousands separators
1212
- stringValue = stringValue.split(thousandsSeparator).join('')
1165
+ let str = String(value).trim()
1166
+ if (str === '') return NaN
1213
1167
 
1214
- // Replace decimal separator with standard "."
1215
- if (decimalSeparator !== '.') {
1216
- stringValue = stringValue.replace(decimalSeparator, '.')
1217
- }
1168
+ const lastComma = str.lastIndexOf(',')
1169
+ const lastDot = str.lastIndexOf('.')
1218
1170
 
1219
- // Remove any non-numeric characters except "." and "-"
1220
- stringValue = stringValue.replace(/[^0-9.\-]/g, '')
1171
+ if (lastComma > lastDot) {
1172
+ str = str.replace(/\./g, '').replace(',', '.')
1173
+ } else if (lastDot === -1 && lastComma !== -1 && /^\d{1,3}$/.test(str.substring(lastComma + 1))) {
1174
+ str = str.replace(',', '.')
1175
+ }
1221
1176
 
1222
- return stringValue
1177
+ return parseFloat(str)
1223
1178
  }
1224
1179
 
1225
- /**
1226
- * Normalizes all price inputs in the variants form before submission
1227
- * @param {Event} event - The form submit event
1228
- */
1229
- normalizePricesBeforeSubmit(event) {
1230
- // Find all price inputs in the variants container
1231
- const priceInputs = this.variantsContainerTarget.querySelectorAll(
1232
- 'input[data-slot*="[prices_attributes]"][data-slot*="[amount]_input"]'
1233
- )
1234
-
1235
- priceInputs.forEach((input) => {
1236
- if (input.value) {
1237
- input.value = this.normalizeNumber(input.value)
1238
- }
1239
- })
1240
- }
1241
1180
  }
@@ -42,7 +42,7 @@ module Spree
42
42
  attribute :ransack_attribute, :string
43
43
  attribute :operators, default: -> { [] }
44
44
  attribute :value_options
45
- attribute :search_url, :string
45
+ attribute :search_url
46
46
  attribute :sort_scope_asc
47
47
  attribute :sort_scope_desc
48
48
 
@@ -70,8 +70,9 @@ module Spree
70
70
  end
71
71
 
72
72
  # Get available fields for filtering based on table configuration
73
+ # @param view_context [ActionView::Base] optional view context for resolving dynamic URLs
73
74
  # @return [Array<Hash>]
74
- def available_fields
75
+ def available_fields(view_context = nil)
75
76
  @table.filterable_columns.map do |column|
76
77
  {
77
78
  key: column.ransack_attribute,
@@ -79,7 +80,7 @@ module Spree
79
80
  type: column.filter_type.to_s,
80
81
  operators: column.operators.map(&:to_s),
81
82
  value_options: format_value_options(column.value_options),
82
- search_url: column.search_url
83
+ search_url: resolve_search_url(column.search_url, view_context)
83
84
  }
84
85
  end
85
86
  end
@@ -92,6 +93,10 @@ module Spree
92
93
 
93
94
  private
94
95
 
96
+ def resolve_search_url(search_url, view_context)
97
+ search_url.is_a?(Proc) ? search_url.call(view_context) : search_url
98
+ end
99
+
95
100
  def format_value_options(options)
96
101
  return nil if options.blank?
97
102
 
@@ -0,0 +1,51 @@
1
+ <div class="card mb-6">
2
+ <div class="card-header">
3
+ <h5 class="card-title"><%= Spree.t(:details) %></h5>
4
+ </div>
5
+ <div class="card-body">
6
+ <dl class="space-y-4">
7
+ <div>
8
+ <dt class="text-sm text-gray-500"><%= Spree.t(:status) %></dt>
9
+ <dd class="mt-1"><%= api_key_status_badge(@api_key) %></dd>
10
+ </div>
11
+
12
+ <div>
13
+ <dt class="text-sm text-gray-500"><%= Spree.t(:type) %></dt>
14
+ <dd class="mt-1"><%= api_key_type_badge(@api_key) %></dd>
15
+ </div>
16
+
17
+ <div>
18
+ <dt class="text-sm text-gray-500"><%= Spree.t(:created_at) %></dt>
19
+ <dd class="mt-1"><%= l(@api_key.created_at, format: :long) %></dd>
20
+ </div>
21
+
22
+ <% if @api_key.created_by.present? %>
23
+ <div>
24
+ <dt class="text-sm text-gray-500"><%= Spree.t(:created_by) %></dt>
25
+ <dd class="mt-1"><%= @api_key.created_by.try(:email) || @api_key.created_by.to_s %></dd>
26
+ </div>
27
+ <% end %>
28
+
29
+ <% if @api_key.last_used_at.present? %>
30
+ <div>
31
+ <dt class="text-sm text-gray-500"><%= Spree.t('admin.api_keys.last_used_at') %></dt>
32
+ <dd class="mt-1"><%= l(@api_key.last_used_at, format: :long) %></dd>
33
+ </div>
34
+ <% end %>
35
+
36
+ <% if @api_key.revoked_at.present? %>
37
+ <div>
38
+ <dt class="text-sm text-gray-500"><%= Spree.t('admin.api_keys.revoked_at') %></dt>
39
+ <dd class="mt-1 text-red-600"><%= l(@api_key.revoked_at, format: :long) %></dd>
40
+ </div>
41
+
42
+ <% if @api_key.revoked_by.present? %>
43
+ <div>
44
+ <dt class="text-sm text-gray-500"><%= Spree.t('admin.api_keys.revoked_by') %></dt>
45
+ <dd class="mt-1"><%= @api_key.revoked_by.try(:email) || @api_key.revoked_by.to_s %></dd>
46
+ </div>
47
+ <% end %>
48
+ <% end %>
49
+ </dl>
50
+ </div>
51
+ </div>
@@ -0,0 +1,26 @@
1
+ <div class="card mb-6">
2
+ <div class="card-body">
3
+ <%= f.spree_text_field :name, required: true, autofocus: f.object.new_record?,
4
+ placeholder: Spree.t('admin.api_keys.name_placeholder') %>
5
+
6
+ <% if f.object.new_record? %>
7
+ <%= f.spree_select :key_type, api_key_type_options,
8
+ { prompt: Spree.t(:select_key_type), label: Spree.t(:type) },
9
+ { required: true } %>
10
+ <p class="text-sm text-gray-500 mt-2">
11
+ <strong><%= Spree.t('admin.api_keys.key_types.publishable') %>:</strong>
12
+ <%= Spree.t('admin.api_keys.key_type_descriptions.publishable') %>
13
+ <br>
14
+ <strong><%= Spree.t('admin.api_keys.key_types.secret') %>:</strong>
15
+ <%= Spree.t('admin.api_keys.key_type_descriptions.secret') %>
16
+ </p>
17
+ <% else %>
18
+ <div class="form-group">
19
+ <label class="form-label"><%= Spree.t(:type) %></label>
20
+ <div class="form-control-plaintext">
21
+ <%= api_key_type_badge(@api_key) %>
22
+ </div>
23
+ </div>
24
+ <% end %>
25
+ </div>
26
+ </div>
@@ -0,0 +1,28 @@
1
+ <div class="card mb-6">
2
+ <div class="card-header">
3
+ <h5 class="card-title"><%= Spree.t(:api_key) %></h5>
4
+ </div>
5
+ <div class="card-body">
6
+ <% if @api_key.active? %>
7
+ <div data-controller="password-visibility" class="input-group gap-0 pr-1">
8
+ <input type="password" class="grow text-truncate" data-password-visibility-target="input" spellcheck="false" value="<%= @api_key.token %>" readonly>
9
+
10
+ <button type="button" class="btn hover:bg-gray-100 p-1 mr-1 rounded" data-action="password-visibility#toggle">
11
+ <span data-password-visibility-target="icon"><%= icon 'eye', class: 'mr-0' %></span>
12
+ <span data-password-visibility-target="icon" class="hidden"><%= icon 'eye-off', class: 'mr-0' %></span>
13
+ </button>
14
+
15
+ <%= clipboard_component(@api_key.token,
16
+ button_class: 'btn hover:bg-gray-100 p-1 rounded',
17
+ icon_class: 'mr-0') %>
18
+ </div>
19
+ <% else %>
20
+ <div class="bg-red-50 border border-red-200 rounded-md p-4">
21
+ <div class="flex items-center">
22
+ <%= icon 'alert-triangle', class: 'text-red-500 mr-2' %>
23
+ <span class="text-red-700"><%= Spree.t('admin.api_keys.key_revoked_message') %></span>
24
+ </div>
25
+ </div>
26
+ <% end %>
27
+ </div>
28
+ </div>
@@ -0,0 +1,16 @@
1
+ <div class="card mb-6">
2
+ <div class="card-header">
3
+ <h5 class="card-title"><%= Spree.t('admin.api_keys.usage') %></h5>
4
+ </div>
5
+ <div class="card-body">
6
+ <p class="text-sm text-gray-600 mb-4">
7
+ <%= Spree.t("admin.api_keys.usage_instructions.#{@api_key.key_type}") %>
8
+ </p>
9
+
10
+ <h6 class="font-medium text-sm mb-2"><%= Spree.t('admin.api_keys.example_request') %></h6>
11
+ <div class="bg-gray-900 rounded-md p-4 overflow-x-auto">
12
+ <pre class="text-green-400 text-sm font-mono whitespace-pre-wrap"><code>curl -X GET "https://your-store.com/api/v3/store/products" \
13
+ -H "X-Spree-Api-Key: <%= @api_key.token %>"</code></pre>
14
+ </div>
15
+ </div>
16
+ </div>
@@ -0,0 +1,9 @@
1
+ <%= render 'spree/admin/shared/developers_nav' %>
2
+
3
+ <%= content_for(:title, Spree.t(:api_keys)) %>
4
+
5
+ <% content_for :page_actions do %>
6
+ <%= link_to_with_icon 'plus', Spree.t(:new_api_key), new_object_url, class: "btn btn-primary" if can? :create, Spree::ApiKey %>
7
+ <% end %>
8
+
9
+ <%= render_table @collection, :api_keys %>
@@ -0,0 +1,26 @@
1
+ <%= content_for :page_title do %>
2
+ <%= page_header_back_button(collection_url) %>
3
+ <%= @api_key.name %>
4
+ <% end %>
5
+
6
+ <% content_for :page_actions do %>
7
+ <% if @api_key.active? && can?(:update, @api_key) %>
8
+ <%= link_to Spree.t(:edit), edit_object_url(@api_key), class: 'btn btn-light' %>
9
+ <%= button_to Spree.t('admin.api_keys.revoke'),
10
+ spree.revoke_admin_api_key_path(@api_key),
11
+ method: :put,
12
+ class: 'btn btn-danger',
13
+ data: { confirm: Spree.t('admin.api_keys.revoke_confirmation') } %>
14
+ <% end %>
15
+ <% end %>
16
+
17
+ <div class="grid grid-cols-12 gap-6">
18
+ <div class="col-span-12 lg:col-span-8">
19
+ <%= render 'token_card' %>
20
+ <%= render 'usage_info' %>
21
+ </div>
22
+
23
+ <div class="col-span-12 lg:col-span-4">
24
+ <%= render 'details' %>
25
+ </div>
26
+ </div>
@@ -1,4 +1,4 @@
1
- <div class="grid grid-cols-12 gap-6 mx-0 hover:bg-gray-25 rounded" id="<%= spree_dom_id(classification) %>" data-sortable-update-url="<%= spree.admin_taxon_classification_path(classification.taxon_id, classification, format: :turbo_stream) %>">
1
+ <div class="grid grid-cols-12 gap-6 mx-0 hover:bg-gray-25 rounded" id="<%= spree_dom_id(classification) %>" data-sortable-update-url="<%= spree.admin_taxon_classification_path(classification.taxon, classification, format: :turbo_stream) %>">
2
2
  <% if classification.taxon.manual_sort_order? %>
3
3
  <div class="col-span-1 text-center self-center">
4
4
  <span class="rounded-md p-2 hover:bg-gray-100 move-handle cursor-grab">
@@ -31,7 +31,7 @@
31
31
  </div>
32
32
 
33
33
  <div class="col-span-2 text-center self-center">
34
- <%= button_to spree.admin_taxon_classification_path(classification.taxon_id, classification), class: 'btn btn-danger btn-sm', method: :delete, data: { controller: 'tooltip', turbo_confirm: Spree.t(:are_you_sure) } do %>
34
+ <%= button_to spree.admin_taxon_classification_path(classification.taxon, classification), class: 'btn btn-danger btn-sm', method: :delete, data: { controller: 'tooltip', turbo_confirm: Spree.t(:are_you_sure) } do %>
35
35
  <%= icon 'trash', class: 'mr-0', style: 'pointer-events: none' %>
36
36
  <%= tooltip(Spree.t('actions.destroy')) %>
37
37
  <% end if classification.taxon.manual? && can?(:delete, classification) %>
@@ -14,7 +14,7 @@
14
14
  </p>
15
15
  <p class="text-center">
16
16
  <% if can?(:create, Spree::Classification) && @taxon.manual? %>
17
- <%= link_to_with_icon 'plus', Spree.t(:add_products), spree.new_admin_taxon_classification_path(@taxon.id), class: 'btn btn-secondary', data: { turbo_frame: 'dialog', action: 'dialog#open' } %>
17
+ <%= link_to_with_icon 'plus', Spree.t(:add_products), spree.new_admin_taxon_classification_path(@taxon), class: 'btn btn-secondary', data: { turbo_frame: 'dialog', action: 'dialog#open' } %>
18
18
  <% end %>
19
19
  <% end %>
20
20
  <% end %>
@@ -1 +1 @@
1
- <%= render 'spree/admin/shared/multi_product_picker', parent: @parent, url: spree.admin_taxon_classifications_path(@parent.id) %>
1
+ <%= render 'spree/admin/shared/multi_product_picker', parent: @parent, url: spree.admin_taxon_classifications_path(@parent) %>
@@ -3,15 +3,15 @@
3
3
  <div class="drawer-body" data-controller="highlight tabs">
4
4
  <ul class="nav nav-pills mb-3">
5
5
  <li class="nav-item">
6
- <a class="nav-link active" data-tabs-target="tab" data-action="click->tabs#select"><%= Spree.t('apis.storefront') %></a>
6
+ <a class="nav-link active" data-tabs-target="tab" data-action="click->tabs#select"><%= Spree.t('apis.store') %></a>
7
7
  </li>
8
8
  <li class="nav-item">
9
- <a class="nav-link" data-tabs-target="tab" data-action="click->tabs#select"><%= Spree.t('apis.platform') %></a>
9
+ <a class="nav-link" data-tabs-target="tab" data-action="click->tabs#select"><%= Spree.t('apis.admin') %></a>
10
10
  </li>
11
11
  </ul>
12
12
  <div data-tabs-target="panel" class="animate-fade-in">
13
- <% if storefront_serializer_exists?(@object) %>
14
- <%= code_block serialize_to_json(@object, api_type: :storefront), style: 'height: calc(100dvh - 11rem);' %>
13
+ <% if store_serializer_exists?(@object) %>
14
+ <%= code_block serialize_to_json(@object, api_type: :store), style: 'height: calc(100dvh - 11rem);' %>
15
15
  <% else %>
16
16
  <div class="text-gray-600 p-12 flex items-center w-full justify-center flex-col">
17
17
  <%= icon 'map-search', class: 'mb-12 opacity-50 text-4xl' %>
@@ -22,8 +22,8 @@
22
22
  <% end %>
23
23
  </div>
24
24
  <div data-tabs-target="panel" class="animate-fade-in" hidden>
25
- <% if platform_serializer_exists?(@object) %>
26
- <%= code_block serialize_to_json(@object, api_type: :platform), style: 'height: calc(100dvh - 11rem);' %>
25
+ <% if admin_serializer_exists?(@object) %>
26
+ <%= code_block serialize_to_json(@object, api_type: :admin), style: 'height: calc(100dvh - 11rem);' %>
27
27
  <% else %>
28
28
  <div class="text-gray-600 p-12 flex items-center w-full justify-center flex-col">
29
29
  <%= icon 'map-search', class: 'mb-12 opacity-50 text-4xl' %>
@@ -0,0 +1,28 @@
1
+ <div class="card mb-6">
2
+ <div class="card-header">
3
+ <h5 class="card-title">
4
+ <%= Spree.t(:general_settings) %>
5
+ </h5>
6
+ </div>
7
+
8
+ <div class="card-body">
9
+ <%= f.spree_text_field :name, required: true %>
10
+
11
+ <%= f.spree_select :country_ids, @countries.map { |country| ["#{Spree::Country.iso_to_emoji_flag(country.iso)} #{country.name}", country.id] }, { include_blank: false, label: Spree.t(:default_country), autocomplete: true, multiple: true } %>
12
+
13
+ <%= f.spree_select :currency, currency_options(f.object.currency), { label: Spree.t(:currency), required: true, autocomplete: true } %>
14
+
15
+ <%= f.spree_select :default_locale,
16
+ options_from_collection_for_select(all_locales_options, :last, :first, f.object.default_locale || I18n.locale),
17
+ { required: true, autocomplete: true } %>
18
+
19
+ <%= f.spree_select :supported_locales,
20
+ options_from_collection_for_select(all_locales_options, :last, :first, f.object.supported_locales&.split(',')),
21
+ { multiple: true, autocomplete: true, hint: Spree.t('admin.markets.supported_locales_hint') } %>
22
+
23
+ <hr class="my-6">
24
+
25
+ <%= f.spree_check_box :tax_inclusive %>
26
+ <%= f.spree_check_box :default %>
27
+ </div>
28
+ </div>