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.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/app/controllers/concerns/spree/admin/order_concern.rb +1 -1
- data/app/controllers/spree/admin/action_text/video_embeds_controller.rb +1 -1
- data/app/controllers/spree/admin/addresses_controller.rb +1 -1
- data/app/controllers/spree/admin/admin_users_controller.rb +3 -3
- data/app/controllers/spree/admin/api_keys_controller.rb +56 -0
- data/app/controllers/spree/admin/assets_controller.rb +2 -2
- data/app/controllers/spree/admin/base_controller.rb +4 -4
- data/app/controllers/spree/admin/classifications_controller.rb +5 -4
- data/app/controllers/spree/admin/coupon_codes_controller.rb +1 -1
- data/app/controllers/spree/admin/customer_group_users_controller.rb +3 -2
- data/app/controllers/spree/admin/dashboard_controller.rb +2 -1
- data/app/controllers/spree/admin/digital_assets_controller.rb +1 -1
- data/app/controllers/spree/admin/gift_cards_controller.rb +5 -5
- data/app/controllers/spree/admin/import_mappings_controller.rb +1 -1
- data/app/controllers/spree/admin/import_rows_controller.rb +1 -1
- data/app/controllers/spree/admin/integrations_controller.rb +1 -1
- data/app/controllers/spree/admin/invitations_controller.rb +6 -5
- data/app/controllers/spree/admin/line_items_controller.rb +1 -1
- data/app/controllers/spree/admin/markets_controller.rb +28 -0
- data/app/controllers/spree/admin/option_values_controller.rb +1 -1
- data/app/controllers/spree/admin/orders/adjustments_controller.rb +4 -4
- data/app/controllers/spree/admin/orders/billing_address_controller.rb +4 -4
- data/app/controllers/spree/admin/orders/customer_returns_controller.rb +1 -1
- data/app/controllers/spree/admin/orders/shipping_address_controller.rb +3 -3
- data/app/controllers/spree/admin/orders/user_controller.rb +4 -4
- data/app/controllers/spree/admin/orders_controller.rb +8 -4
- data/app/controllers/spree/admin/payments_controller.rb +3 -3
- data/app/controllers/spree/admin/price_list_products_controller.rb +1 -1
- data/app/controllers/spree/admin/price_rules_controller.rb +1 -1
- data/app/controllers/spree/admin/products_controller.rb +24 -15
- data/app/controllers/spree/admin/profile_controller.rb +1 -1
- data/app/controllers/spree/admin/promotion_actions_controller.rb +1 -1
- data/app/controllers/spree/admin/promotion_rules_controller.rb +1 -1
- data/app/controllers/spree/admin/promotions_controller.rb +1 -1
- data/app/controllers/spree/admin/refunds_controller.rb +1 -1
- data/app/controllers/spree/admin/reimbursements_controller.rb +2 -2
- data/app/controllers/spree/admin/resource_controller.rb +30 -11
- data/app/controllers/spree/admin/shipments_controller.rb +3 -3
- data/app/controllers/spree/admin/shipping_methods_controller.rb +1 -1
- data/app/controllers/spree/admin/store_credits_controller.rb +5 -5
- data/app/controllers/spree/admin/stores_controller.rb +1 -32
- data/app/controllers/spree/admin/taxons_controller.rb +3 -3
- data/app/controllers/spree/admin/translations_controller.rb +1 -0
- data/app/controllers/spree/admin/users_controller.rb +2 -2
- data/app/helpers/spree/admin/api_keys_helper.rb +32 -0
- data/app/helpers/spree/admin/base_helper.rb +6 -1
- data/app/helpers/spree/admin/json_preview_helper.rb +29 -25
- data/app/helpers/spree/admin/orders_filters_helper.rb +1 -1
- data/app/helpers/spree/admin/sortable_tree_helper.rb +1 -1
- data/app/helpers/spree/admin/stores_helper.rb +0 -4
- data/app/helpers/spree/admin/table_helper.rb +1 -1
- data/app/javascript/spree/admin/controllers/autocomplete_select_controller.js +5 -1
- data/app/javascript/spree/admin/controllers/bulk_editor_controller.js +0 -86
- data/app/javascript/spree/admin/controllers/money_field_controller.js +1 -23
- data/app/javascript/spree/admin/controllers/select_controller.js +4 -0
- data/app/javascript/spree/admin/controllers/variants_form_controller.js +30 -91
- data/app/models/spree/admin/table/column.rb +1 -1
- data/app/models/spree/admin/table/query_builder.rb +7 -2
- data/app/views/spree/admin/api_keys/_details.html.erb +51 -0
- data/app/views/spree/admin/api_keys/_form.html.erb +26 -0
- data/app/views/spree/admin/api_keys/_token_card.html.erb +28 -0
- data/app/views/spree/admin/api_keys/_usage_info.html.erb +16 -0
- data/app/views/spree/admin/api_keys/index.html.erb +9 -0
- data/app/views/spree/admin/api_keys/show.html.erb +26 -0
- data/app/views/spree/admin/classifications/_classification.html.erb +2 -2
- data/app/views/spree/admin/classifications/index.html.erb +1 -1
- data/app/views/spree/admin/classifications/new.html.erb +1 -1
- data/app/views/spree/admin/json_previews/show.html.erb +6 -6
- data/app/views/spree/admin/markets/_form.html.erb +28 -0
- data/app/views/spree/admin/markets/edit.html.erb +1 -0
- data/app/views/spree/admin/markets/index.html.erb +9 -0
- data/app/views/spree/admin/markets/new.html.erb +1 -0
- data/app/views/spree/admin/orders/billing_address/_form.html.erb +2 -2
- data/app/views/spree/admin/orders/shipping_address/_form.html.erb +2 -2
- data/app/views/spree/admin/payment_methods/_form.html.erb +0 -12
- data/app/views/spree/admin/price_rules/forms/_market_rule.html.erb +7 -0
- data/app/views/spree/admin/products/_form.html.erb +0 -1
- data/app/views/spree/admin/products/form/_variants.html.erb +4 -13
- data/app/views/spree/admin/products/form/variants/_variant_template.html.erb +1 -1
- data/app/views/spree/admin/promotion_rules/forms/_country.html.erb +1 -1
- data/app/views/spree/admin/shared/_content_header.html.erb +1 -1
- data/app/views/spree/admin/shared/sidebar/_store_dropdown.html.erb +0 -33
- data/app/views/spree/admin/shared/sidebar/_store_nav.html.erb +2 -2
- data/app/views/spree/admin/shared/sortable_tree/_taxonomy.html.erb +2 -2
- data/app/views/spree/admin/stores/form/_basic.html.erb +10 -7
- data/app/views/spree/admin/stores/form/_checkout.html.erb +5 -8
- data/app/views/spree/admin/tables/_table.html.erb +3 -4
- data/app/views/spree/admin/tables/columns/_api_key_status.html.erb +2 -0
- data/app/views/spree/admin/tables/columns/_api_key_type.html.erb +2 -0
- data/app/views/spree/admin/taxonomies/show.html.erb +1 -1
- data/app/views/spree/admin/taxons/_form.html.erb +2 -2
- data/app/views/spree/admin/taxons/edit.html.erb +1 -2
- data/app/views/spree/admin/taxons/update.turbo_stream.erb +1 -1
- data/app/views/spree/admin/users/_billing.html.erb +2 -2
- data/app/views/spree/admin/users/_shipping.html.erb +1 -1
- data/app/views/spree/admin/variants/_variant.html.erb +1 -1
- data/config/brakeman.ignore +28 -0
- data/config/initializers/spree_admin_navigation.rb +16 -16
- data/config/initializers/spree_admin_tables.rb +112 -18
- data/config/locales/en.yml +31 -0
- data/config/routes.rb +6 -6
- data/lib/spree/admin/engine.rb +1 -0
- data/lib/spree/admin/tailwind_helper.rb +11 -1
- data/lib/spree/admin/testing_support/tom_select.rb +1 -1
- metadata +30 -27
- data/LICENSE.md +0 -13
- data/app/controllers/spree/admin/custom_domains_controller.rb +0 -21
- data/app/controllers/spree/admin/oauth_applications_controller.rb +0 -23
- data/app/views/spree/admin/custom_domains/_custom_domain.html.erb +0 -11
- data/app/views/spree/admin/custom_domains/_custom_domains.html.erb +0 -19
- data/app/views/spree/admin/custom_domains/_form.html.erb +0 -7
- data/app/views/spree/admin/custom_domains/index.html.erb +0 -65
- data/app/views/spree/admin/oauth_applications/_form.html.erb +0 -6
- data/app/views/spree/admin/oauth_applications/_table_header.html.erb +0 -7
- data/app/views/spree/admin/oauth_applications/_table_row.html.erb +0 -34
- data/app/views/spree/admin/oauth_applications/create.turbo_stream.erb +0 -31
- data/app/views/spree/admin/oauth_applications/edit.html.erb +0 -1
- data/app/views/spree/admin/oauth_applications/index.html.erb +0 -21
- data/app/views/spree/admin/oauth_applications/new.html.erb +0 -1
- data/app/views/spree/admin/products/form/_stores.html.erb +0 -27
- data/app/views/spree/admin/stores/new.html.erb +0 -128
- data/app/views/spree/admin/stores/new.turbo_stream.erb +0 -1
- /data/app/views/spree/admin/{custom_domains → api_keys}/edit.html.erb +0 -0
- /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
|
|
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
|
|
530
|
-
if (
|
|
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/${
|
|
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 ?
|
|
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]) =>
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1212
|
-
|
|
1165
|
+
let str = String(value).trim()
|
|
1166
|
+
if (str === '') return NaN
|
|
1213
1167
|
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
stringValue = stringValue.replace(decimalSeparator, '.')
|
|
1217
|
-
}
|
|
1168
|
+
const lastComma = str.lastIndexOf(',')
|
|
1169
|
+
const lastDot = str.lastIndexOf('.')
|
|
1218
1170
|
|
|
1219
|
-
|
|
1220
|
-
|
|
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
|
|
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
|
}
|
|
@@ -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.
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
14
|
-
<%= code_block serialize_to_json(@object, api_type: :
|
|
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
|
|
26
|
-
<%= code_block serialize_to_json(@object, api_type: :
|
|
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>
|