spree_admin 5.5.3 → 5.6.0.rc1
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/app/assets/images/vercel-deploy-button.svg +14 -0
- data/app/assets/tailwind/spree/admin/components/_alerts.css +5 -1
- data/app/assets/tailwind/spree/admin/components/_badges.css +1 -1
- data/app/assets/tailwind/spree/admin/components/_breadcrumbs.css +1 -1
- data/app/assets/tailwind/spree/admin/components/_buttons.css +1 -1
- data/app/assets/tailwind/spree/admin/components/_cards.css +1 -1
- data/app/assets/tailwind/spree/admin/components/_dialogs.css +3 -1
- data/app/assets/tailwind/spree/admin/components/_dropdowns.css +2 -2
- data/app/assets/tailwind/spree/admin/components/_filters.css +1 -1
- data/app/assets/tailwind/spree/admin/components/_forms.css +10 -6
- data/app/assets/tailwind/spree/admin/components/_layout.css +30 -23
- data/app/assets/tailwind/spree/admin/components/_rtl.css +53 -0
- data/app/assets/tailwind/spree/admin/components/_search-picker.css +2 -2
- data/app/assets/tailwind/spree/admin/components/_tables.css +1 -1
- data/app/assets/tailwind/spree/admin/index.css +1 -0
- data/app/assets/tailwind/spree/admin/plugins/_tom-select.css +6 -6
- data/app/assets/tailwind/spree/admin/plugins/_trix.css +1 -1
- data/app/assets/tailwind/spree/admin/plugins/_uppy.css +1 -1
- data/app/assets/tailwind/spree/admin/views/_page-builder.css +8 -5
- data/app/controllers/concerns/spree/admin/locale_concern.rb +76 -0
- data/app/controllers/spree/admin/base_controller.rb +38 -2
- data/app/controllers/spree/admin/countries_controller.rb +18 -4
- data/app/controllers/spree/admin/exports_controller.rb +7 -0
- data/app/controllers/spree/admin/imports_controller.rb +8 -0
- data/app/controllers/spree/admin/products_controller.rb +1 -0
- data/app/controllers/spree/admin/storefront_controller.rb +100 -0
- data/app/controllers/spree/admin/user_sessions_controller.rb +10 -0
- data/app/controllers/spree/admin/variants_controller.rb +3 -1
- data/app/helpers/spree/admin/base_helper.rb +10 -0
- data/app/helpers/spree/admin/channels_helper.rb +17 -0
- data/app/helpers/spree/admin/dropdown_helper.rb +14 -0
- data/app/helpers/spree/admin/navigation_helper.rb +1 -1
- data/app/helpers/spree/admin/onboarding_helper.rb +1 -1
- data/app/helpers/spree/admin/orders_helper.rb +9 -34
- data/app/helpers/spree/admin/products_helper.rb +1 -1
- data/app/helpers/spree/admin/rtl_helper.rb +15 -0
- data/app/helpers/spree/admin/shipment_helper.rb +1 -1
- data/app/helpers/spree/admin/storefront_helper.rb +46 -0
- data/app/helpers/spree/admin/stores_helper.rb +2 -1
- data/app/javascript/spree/admin/application.js +2 -0
- data/app/javascript/spree/admin/controllers/autocomplete_select_controller.js +4 -0
- data/app/javascript/spree/admin/controllers/calendar_range_controller.js +7 -3
- data/app/javascript/spree/admin/controllers/display_name_controller.js +18 -0
- data/app/javascript/spree/admin/controllers/query_builder_controller.js +39 -28
- data/app/javascript/spree/admin/controllers/variants_form_controller.js +49 -1
- data/app/javascript/spree/admin/helpers/display_names.js +54 -0
- data/app/models/spree/admin/table/column.rb +15 -1
- data/app/models/spree/admin/table/filter.rb +23 -16
- data/app/models/spree/admin/table/query_builder.rb +11 -2
- data/app/views/active_storage/_upload_form.html.erb +1 -1
- data/app/views/layouts/spree/admin.html.erb +1 -1
- data/app/views/layouts/spree/admin_settings.html.erb +1 -1
- data/app/views/layouts/spree/admin_wizard.html.erb +2 -2
- data/app/views/layouts/spree/minimal.html.erb +1 -1
- data/app/views/spree/admin/admin_users/_audit_log.html.erb +2 -2
- data/app/views/spree/admin/admin_users/new.html.erb +1 -1
- data/app/views/spree/admin/assets/edit.html.erb +1 -1
- data/app/views/spree/admin/channels/_form.html.erb +10 -0
- data/app/views/spree/admin/dashboard/_setup_progress.html.erb +4 -4
- data/app/views/spree/admin/dashboard/_setup_tasks.html.erb +5 -4
- data/app/views/spree/admin/dashboard/_updater.html.erb +4 -4
- data/app/views/spree/admin/dashboard/_visits.html.erb +12 -12
- data/app/views/spree/admin/dashboard/analytics.html.erb +5 -5
- data/app/views/spree/admin/dashboard/setup_tasks/_add_products.html.erb +4 -4
- data/app/views/spree/admin/dashboard/setup_tasks/_set_customer_support_email.html.erb +3 -4
- data/app/views/spree/admin/dashboard/setup_tasks/_setup_storefront.html.erb +18 -0
- data/app/views/spree/admin/gift_card_batches/_form.html.erb +1 -1
- data/app/views/spree/admin/import_rows/show.html.erb +2 -2
- data/app/views/spree/admin/imports/_footer.html.erb +1 -1
- data/app/views/spree/admin/imports/_mapping_form.html.erb +1 -1
- data/app/views/spree/admin/imports/show.html.erb +3 -3
- data/app/views/spree/admin/integrations/_integration.html.erb +1 -1
- data/app/views/spree/admin/integrations/index.html.erb +2 -4
- data/app/views/spree/admin/invitations/_invitation.html.erb +1 -1
- data/app/views/spree/admin/invitations/new.html.erb +1 -1
- data/app/views/spree/admin/invitations/show.html.erb +1 -1
- data/app/views/spree/admin/line_items/new.html.erb +1 -1
- data/app/views/spree/admin/markets/_form.html.erb +8 -6
- data/app/views/spree/admin/metafields/edit.html.erb +1 -1
- data/app/views/spree/admin/option_types/_form.html.erb +9 -6
- data/app/views/spree/admin/option_types/index.html.erb +2 -3
- data/app/views/spree/admin/orders/_customer.html.erb +1 -1
- data/app/views/spree/admin/orders/_customer_summary.html.erb +1 -1
- data/app/views/spree/admin/orders/_payments.html.erb +1 -1
- data/app/views/spree/admin/orders/_shipment.html.erb +7 -2
- data/app/views/spree/admin/orders/_shipments.html.erb +1 -1
- data/app/views/spree/admin/orders/_user_overview.html.erb +9 -6
- data/app/views/spree/admin/orders/return_authorizations/edit.html.erb +1 -1
- data/app/views/spree/admin/payment_methods/_form.html.erb +1 -1
- data/app/views/spree/admin/payment_methods/index.html.erb +2 -2
- data/app/views/spree/admin/payments/_payment.html.erb +1 -1
- data/app/views/spree/admin/price_lists/index.html.erb +2 -2
- data/app/views/spree/admin/products/form/_inventory.html.erb +19 -0
- data/app/views/spree/admin/products/form/_publishing.html.erb +2 -3
- data/app/views/spree/admin/products/index.html.erb +1 -2
- data/app/views/spree/admin/profile/edit.html.erb +3 -0
- data/app/views/spree/admin/promotion_rules/forms/_channel.html.erb +7 -0
- data/app/views/spree/admin/promotion_rules/forms/_country.html.erb +1 -1
- data/app/views/spree/admin/promotion_rules/forms/_item_total.html.erb +1 -1
- data/app/views/spree/admin/promotion_rules/forms/_market.html.erb +7 -0
- data/app/views/spree/admin/promotions/_form.html.erb +2 -2
- data/app/views/spree/admin/promotions/_sidebar.html.erb +1 -1
- data/app/views/spree/admin/promotions/edit.html.erb +2 -2
- data/app/views/spree/admin/promotions/form/_kind.html.erb +11 -11
- data/app/views/spree/admin/promotions/form/_settings.html.erb +4 -5
- data/app/views/spree/admin/reports/new.html.erb +2 -2
- data/app/views/spree/admin/shared/_address.html.erb +1 -1
- data/app/views/spree/admin/shared/_analytics_loader_skeleton.html.erb +5 -5
- data/app/views/spree/admin/shared/_calendar_range_picker.html.erb +11 -1
- data/app/views/spree/admin/shared/_content_header.html.erb +2 -2
- data/app/views/spree/admin/shared/_edit_resource_links.html.erb +1 -1
- data/app/views/spree/admin/shared/_filters_search_bar.html.erb +1 -1
- data/app/views/spree/admin/shared/_growth_rate_badge.html.erb +1 -1
- data/app/views/spree/admin/shared/_head.html.erb +4 -1
- data/app/views/spree/admin/shared/_index_table_options.html.erb +2 -2
- data/app/views/spree/admin/shared/_media_asset.html.erb +1 -1
- data/app/views/spree/admin/shared/_media_form.html.erb +1 -1
- data/app/views/spree/admin/shared/_seo.html.erb +1 -1
- data/app/views/spree/admin/shared/_sidebar.html.erb +1 -1
- data/app/views/spree/admin/shared/sidebar/_enterprise_edition_notice.html.erb +2 -2
- data/app/views/spree/admin/shared/sortable_tree/_taxonomy.html.erb +1 -1
- data/app/views/spree/admin/shipping_methods/form/_display.html.erb +2 -2
- data/app/views/spree/admin/shipping_methods/form/_estimated_transit_business_days.html.erb +1 -2
- data/app/views/spree/admin/shipping_methods/form/_tracking_url.html.erb +1 -1
- data/app/views/spree/admin/shipping_methods/form/_zones.html.erb +1 -1
- data/app/views/spree/admin/shipping_methods/index.html.erb +1 -1
- data/app/views/spree/admin/storefront/show.html.erb +150 -0
- data/app/views/spree/admin/stores/form/_basic.html.erb +2 -2
- data/app/views/spree/admin/stores/form/_checkout.html.erb +4 -0
- data/app/views/spree/admin/stores/form/_emails.html.erb +4 -6
- data/app/views/spree/admin/tables/_table.html.erb +3 -2
- data/app/views/spree/admin/taxons/_form.html.erb +2 -2
- data/app/views/spree/admin/user_sessions/new.html.erb +11 -2
- data/app/views/spree/admin/users/_tabs.html.erb +1 -1
- data/app/views/spree/admin/variants/_search_result.html.erb +1 -1
- data/app/views/spree/admin/variants/_variant.html.erb +1 -1
- data/app/views/spree/admin/variants/form/_inventory.html.erb +17 -0
- data/app/views/spree/admin/webhook_endpoints/_form.html.erb +2 -2
- data/app/views/spree/admin/zones/index.html.erb +1 -1
- data/config/i18n-tasks.yml +3 -0
- data/config/initializers/spree_admin_navigation.rb +9 -0
- data/config/initializers/spree_admin_tables.rb +20 -18
- data/config/locales/en.yml +647 -0
- data/config/routes.rb +5 -0
- metadata +18 -6
|
@@ -226,22 +226,10 @@ export default class extends Controller {
|
|
|
226
226
|
const operatorSelect = filterRow.querySelector("[data-operator-select]")
|
|
227
227
|
operatorSelect.innerHTML = ""
|
|
228
228
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
not_cont: "does not contain",
|
|
234
|
-
start: "starts with",
|
|
235
|
-
end: "ends with",
|
|
236
|
-
gt: "greater than",
|
|
237
|
-
gteq: "greater than or equal",
|
|
238
|
-
lt: "less than",
|
|
239
|
-
lteq: "less than or equal",
|
|
240
|
-
in: "is any of",
|
|
241
|
-
not_in: "is none of",
|
|
242
|
-
null: "is empty",
|
|
243
|
-
not_null: "is not empty"
|
|
244
|
-
}
|
|
229
|
+
// Labels come from the server already translated (see Filter.operators_for_select).
|
|
230
|
+
const operatorLabels = Object.fromEntries(
|
|
231
|
+
this.operatorsValue.map(op => [op.value, op.label])
|
|
232
|
+
)
|
|
245
233
|
|
|
246
234
|
operators.forEach(op => {
|
|
247
235
|
const option = document.createElement("option")
|
|
@@ -355,8 +343,21 @@ export default class extends Controller {
|
|
|
355
343
|
|
|
356
344
|
if (fieldConfig.search_url) {
|
|
357
345
|
wrapper.dataset.selectUrlValue = fieldConfig.search_url
|
|
358
|
-
|
|
359
|
-
|
|
346
|
+
|
|
347
|
+
if (fieldConfig.preload_options) {
|
|
348
|
+
// Small, fixed option set: load it once and filter client-side on
|
|
349
|
+
// the localized `label` while submitting the underlying value.
|
|
350
|
+
wrapper.dataset.selectLabelFieldValue = "label"
|
|
351
|
+
wrapper.dataset.selectSearchFieldValue = "label"
|
|
352
|
+
// Preserve the server's localized-name ordering. The label is
|
|
353
|
+
// flag-prefixed, so sorting by it would order by flag codepoints
|
|
354
|
+
// (ISO) instead of name; "$order" keeps the received order.
|
|
355
|
+
wrapper.dataset.selectSortFieldValue = "$order"
|
|
356
|
+
} else {
|
|
357
|
+
// Remote search mode so it doesn't pre-fetch and overwrite our
|
|
358
|
+
// preloaded options (datasets here are large / server-limited).
|
|
359
|
+
wrapper.dataset.selectRemoteSearchValue = "true"
|
|
360
|
+
}
|
|
360
361
|
}
|
|
361
362
|
|
|
362
363
|
// Use name as value field for tags (tags_name ransack attribute expects tag names)
|
|
@@ -371,17 +372,27 @@ export default class extends Controller {
|
|
|
371
372
|
input.dataset.selectTarget = "input"
|
|
372
373
|
input.dataset.valueInput = "" // Mark as value input for serialization
|
|
373
374
|
|
|
374
|
-
// Restore existing values - we store as array of {id, name} objects
|
|
375
|
-
// Use remoteSearchActiveOption which properly preloads options and selects them
|
|
375
|
+
// Restore existing values - we store as array of {id, name} objects.
|
|
376
376
|
if (existingValue !== null && existingValue !== undefined && Array.isArray(existingValue) && existingValue.length > 0) {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
377
|
+
if (fieldConfig.preload_options) {
|
|
378
|
+
// The full option list is preloaded, so we only need the selected
|
|
379
|
+
// values; Tom Select resolves their labels from the loaded options.
|
|
380
|
+
const selectedValues = existingValue.map(item =>
|
|
381
|
+
String(typeof item === 'object' ? item.id : item)
|
|
382
|
+
)
|
|
383
|
+
wrapper.dataset.selectActiveOptionValue = JSON.stringify(selectedValues)
|
|
384
|
+
} else {
|
|
385
|
+
// Remote search: preload the selected options so they render with
|
|
386
|
+
// their labels without first issuing a search.
|
|
387
|
+
const preloadedOptions = existingValue.map(item => {
|
|
388
|
+
if (typeof item === 'object') {
|
|
389
|
+
return { id: useNameAsValue ? item.name : item.id, name: item.name }
|
|
390
|
+
} else {
|
|
391
|
+
return { id: item, name: item }
|
|
392
|
+
}
|
|
393
|
+
})
|
|
394
|
+
wrapper.dataset.selectRemoteSearchActiveOptionValue = JSON.stringify(preloadedOptions)
|
|
395
|
+
}
|
|
385
396
|
}
|
|
386
397
|
|
|
387
398
|
// Set up change listener on select element directly
|
|
@@ -72,6 +72,10 @@ export default class extends CheckboxSelectAll {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
this.inventoryFormTarget = document.querySelector('.inventory-form');
|
|
75
|
+
|
|
76
|
+
// Drop stale removal inputs (e.g. restored from the Turbo cache) so a fresh
|
|
77
|
+
// session never re-submits removals the user didn't make in it.
|
|
78
|
+
this.syncRemovedVariantIdInputs()
|
|
75
79
|
}
|
|
76
80
|
|
|
77
81
|
toggleQuantityTracked() {
|
|
@@ -119,6 +123,37 @@ export default class extends CheckboxSelectAll {
|
|
|
119
123
|
}
|
|
120
124
|
}
|
|
121
125
|
|
|
126
|
+
// The set of persisted variant ids the user removed from the matrix. Submitted as
|
|
127
|
+
// `product[removed_variant_ids][]` — the server only ever destroys ids listed there,
|
|
128
|
+
// never variants that are merely missing from the re-submitted form.
|
|
129
|
+
get removedVariantIds() {
|
|
130
|
+
if (!this._removedVariantIds) this._removedVariantIds = new Set()
|
|
131
|
+
|
|
132
|
+
return this._removedVariantIds
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
markVariantRemoved(internalName) {
|
|
136
|
+
const variantId = this.variantIdsValue[internalName]
|
|
137
|
+
if (variantId) this.removedVariantIds.add(String(variantId))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
markVariantKept(internalName) {
|
|
141
|
+
const variantId = this.variantIdsValue[internalName]
|
|
142
|
+
if (variantId) this.removedVariantIds.delete(String(variantId))
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
syncRemovedVariantIdInputs() {
|
|
146
|
+
this.element.querySelectorAll('input[name="product[removed_variant_ids][]"]').forEach((input) => input.remove())
|
|
147
|
+
|
|
148
|
+
this.removedVariantIds.forEach((variantId) => {
|
|
149
|
+
const input = document.createElement('input')
|
|
150
|
+
input.type = 'hidden'
|
|
151
|
+
input.name = 'product[removed_variant_ids][]'
|
|
152
|
+
input.value = variantId
|
|
153
|
+
this.element.appendChild(input)
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
122
157
|
deleteSelected() {
|
|
123
158
|
const newStockValue = this.stockValue
|
|
124
159
|
const newPricesValue = this.pricesValue
|
|
@@ -126,6 +161,7 @@ export default class extends CheckboxSelectAll {
|
|
|
126
161
|
const internalName = checkbox.value
|
|
127
162
|
|
|
128
163
|
this.ignoredVariants.add(internalName)
|
|
164
|
+
this.markVariantRemoved(internalName)
|
|
129
165
|
const variant = this.variantsContainerTarget.querySelector(`[data-variant-name="${internalName}"]`)
|
|
130
166
|
|
|
131
167
|
const nestingLevel = internalName.split('/').length
|
|
@@ -157,7 +193,8 @@ export default class extends CheckboxSelectAll {
|
|
|
157
193
|
|
|
158
194
|
delete newStockValue[internalName]
|
|
159
195
|
delete newPricesValue[internalName]
|
|
160
|
-
|
|
196
|
+
// the row may already be gone when deleting an option value re-rendered the matrix
|
|
197
|
+
variant?.remove()
|
|
161
198
|
})
|
|
162
199
|
|
|
163
200
|
this.stockValue = newStockValue
|
|
@@ -166,6 +203,7 @@ export default class extends CheckboxSelectAll {
|
|
|
166
203
|
this.checkboxAllTarget.checked = false
|
|
167
204
|
this.refresh()
|
|
168
205
|
this.refreshParentInputs()
|
|
206
|
+
this.syncRemovedVariantIdInputs()
|
|
169
207
|
}
|
|
170
208
|
|
|
171
209
|
reorderOptions(event) {
|
|
@@ -498,6 +536,14 @@ export default class extends CheckboxSelectAll {
|
|
|
498
536
|
}
|
|
499
537
|
currentVariants.add(internalName)
|
|
500
538
|
|
|
539
|
+
// Reconcile the removal ledger with what this render keeps: leaf rows
|
|
540
|
+
// re-submit their variant id, parent (grouping) rows never do.
|
|
541
|
+
if (i === nestingLevel - 1) {
|
|
542
|
+
this.markVariantKept(internalName)
|
|
543
|
+
} else if (i === 0 && nestingLevel > 1) {
|
|
544
|
+
this.markVariantRemoved(internalName)
|
|
545
|
+
}
|
|
546
|
+
|
|
501
547
|
const existingVariant = this.variantsContainerTarget.querySelector(`[data-variant-name="${internalName}"]`)
|
|
502
548
|
if (existingVariant) {
|
|
503
549
|
if (i === 0) {
|
|
@@ -599,6 +645,7 @@ export default class extends CheckboxSelectAll {
|
|
|
599
645
|
this.variantsContainerTarget.querySelectorAll('[data-variants-form-target="variant"]').forEach((variant) => {
|
|
600
646
|
const variantName = variant.dataset.variantName
|
|
601
647
|
if (!currentVariants.has(variantName)) {
|
|
648
|
+
this.markVariantRemoved(variantName)
|
|
602
649
|
variant.remove()
|
|
603
650
|
}
|
|
604
651
|
})
|
|
@@ -609,6 +656,7 @@ export default class extends CheckboxSelectAll {
|
|
|
609
656
|
}
|
|
610
657
|
|
|
611
658
|
this.refreshParentInputs()
|
|
659
|
+
this.syncRemovedVariantIdInputs()
|
|
612
660
|
}
|
|
613
661
|
|
|
614
662
|
refreshParentInputs() {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Mirrors the React dashboard's `CODE — Localized Name` format. */
|
|
2
|
+
export function formatCodeName(code, name) {
|
|
3
|
+
const upper = code.toUpperCase()
|
|
4
|
+
if (!name || name.toUpperCase() === upper) return upper
|
|
5
|
+
return `${upper} — ${name}`
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Intl.DisplayNames construction is the expensive part (locale resolution +
|
|
9
|
+
// table setup); .of() is cheap. Cache one formatter per (type, locale) so
|
|
10
|
+
// localizing a large select doesn't rebuild it for every option.
|
|
11
|
+
const formatterCache = new Map()
|
|
12
|
+
|
|
13
|
+
function displayNameFormatter(type, locale) {
|
|
14
|
+
const key = `${type}:${locale}`
|
|
15
|
+
if (formatterCache.has(key)) return formatterCache.get(key)
|
|
16
|
+
|
|
17
|
+
let formatter
|
|
18
|
+
try {
|
|
19
|
+
formatter = new Intl.DisplayNames([locale, 'en'], { type })
|
|
20
|
+
} catch {
|
|
21
|
+
formatter = null
|
|
22
|
+
}
|
|
23
|
+
formatterCache.set(key, formatter)
|
|
24
|
+
return formatter
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Resolve a code via Intl.DisplayNames in the admin UI language. */
|
|
28
|
+
export function intlDisplayName(type, code, locale) {
|
|
29
|
+
if (!code) return undefined
|
|
30
|
+
|
|
31
|
+
const formatter = displayNameFormatter(type, locale)
|
|
32
|
+
if (!formatter) return undefined
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
return formatter.of(code)
|
|
36
|
+
} catch {
|
|
37
|
+
return undefined
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Returns the admin UI locale from `<html lang>`, defaulting to `en` when unset. */
|
|
42
|
+
export function adminUiLocale() {
|
|
43
|
+
return document.documentElement.lang || 'en'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Rewrite <option> labels using Intl.DisplayNames before Tom Select init. */
|
|
47
|
+
export function localizeSelectOptions(select, type, locale = adminUiLocale()) {
|
|
48
|
+
for (const option of select.options) {
|
|
49
|
+
if (!option.value) continue
|
|
50
|
+
|
|
51
|
+
const name = intlDisplayName(type, option.value, locale)
|
|
52
|
+
if (name) option.textContent = formatCodeName(option.value, name)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -44,6 +44,11 @@ module Spree
|
|
|
44
44
|
attribute :operators, default: -> { [] }
|
|
45
45
|
attribute :value_options
|
|
46
46
|
attribute :search_url
|
|
47
|
+
# When true, the autocomplete filter loads the full option list from
|
|
48
|
+
# `search_url` once and filters it client-side on each option's `label`
|
|
49
|
+
# field (instead of querying the server per keystroke). Only suitable for
|
|
50
|
+
# small, fixed option sets whose endpoint returns a `label` attribute.
|
|
51
|
+
attribute :preload_options, :boolean, default: false
|
|
47
52
|
attribute :sort_scope_asc
|
|
48
53
|
attribute :sort_scope_desc
|
|
49
54
|
|
|
@@ -69,7 +74,16 @@ module Spree
|
|
|
69
74
|
|
|
70
75
|
# Resolve label (handles i18n keys)
|
|
71
76
|
def resolve_label
|
|
72
|
-
|
|
77
|
+
if label.is_a?(String) && label.include?('.')
|
|
78
|
+
# Dotted keys may live under the `spree` namespace (e.g. `admin.num_orders`,
|
|
79
|
+
# `price_list_statuses.active`) or at the I18n root (e.g. `activerecord.attributes.*`).
|
|
80
|
+
# Prefer the `spree`-scoped lookup, then fall back to the root lookup.
|
|
81
|
+
scoped = Spree.t(label, default: '')
|
|
82
|
+
return scoped if scoped.present?
|
|
83
|
+
|
|
84
|
+
return I18n.t(label, default: label.split('.').last.humanize)
|
|
85
|
+
end
|
|
86
|
+
|
|
73
87
|
return label if label.is_a?(String) && label.present?
|
|
74
88
|
|
|
75
89
|
key_to_translate = label || key
|
|
@@ -3,20 +3,20 @@ module Spree
|
|
|
3
3
|
class Table
|
|
4
4
|
class Filter
|
|
5
5
|
OPERATORS = {
|
|
6
|
-
eq: {
|
|
7
|
-
not_eq: {
|
|
8
|
-
cont: {
|
|
9
|
-
not_cont: {
|
|
10
|
-
start: {
|
|
11
|
-
end: {
|
|
12
|
-
gt: {
|
|
13
|
-
gteq: {
|
|
14
|
-
lt: {
|
|
15
|
-
lteq: {
|
|
16
|
-
in: {
|
|
17
|
-
not_in: {
|
|
18
|
-
null: {
|
|
19
|
-
not_null: {
|
|
6
|
+
eq: { predicate: '_eq' },
|
|
7
|
+
not_eq: { predicate: '_not_eq' },
|
|
8
|
+
cont: { predicate: '_cont' },
|
|
9
|
+
not_cont: { predicate: '_not_cont' },
|
|
10
|
+
start: { predicate: '_start' },
|
|
11
|
+
end: { predicate: '_end' },
|
|
12
|
+
gt: { predicate: '_gt' },
|
|
13
|
+
gteq: { predicate: '_gteq' },
|
|
14
|
+
lt: { predicate: '_lt' },
|
|
15
|
+
lteq: { predicate: '_lteq' },
|
|
16
|
+
in: { predicate: '_in' },
|
|
17
|
+
not_in: { predicate: '_not_in' },
|
|
18
|
+
null: { predicate: '_null', no_value: true },
|
|
19
|
+
not_null: { predicate: '_not_null', no_value: true }
|
|
20
20
|
}.freeze
|
|
21
21
|
|
|
22
22
|
attr_accessor :field, :operator, :value, :id
|
|
@@ -73,7 +73,7 @@ module Spree
|
|
|
73
73
|
# Get human-readable operator label
|
|
74
74
|
# @return [String]
|
|
75
75
|
def operator_label
|
|
76
|
-
|
|
76
|
+
self.class.translate_operator(@operator)
|
|
77
77
|
end
|
|
78
78
|
|
|
79
79
|
# Check if this operator requires a value
|
|
@@ -107,9 +107,16 @@ module Spree
|
|
|
107
107
|
# @return [Array<Hash>]
|
|
108
108
|
def self.operators_for_select
|
|
109
109
|
OPERATORS.map do |key, config|
|
|
110
|
-
{ value: key.to_s, label:
|
|
110
|
+
{ value: key.to_s, label: translate_operator(key), no_value: config[:no_value] || false }
|
|
111
111
|
end
|
|
112
112
|
end
|
|
113
|
+
|
|
114
|
+
# Translate an operator key to its human-readable label
|
|
115
|
+
# @param operator [Symbol]
|
|
116
|
+
# @return [String]
|
|
117
|
+
def self.translate_operator(operator)
|
|
118
|
+
Spree.t("admin.table.operators.#{operator}", default: operator.to_s.humanize)
|
|
119
|
+
end
|
|
113
120
|
end
|
|
114
121
|
end
|
|
115
122
|
end
|
|
@@ -80,7 +80,8 @@ module Spree
|
|
|
80
80
|
type: column.filter_type.to_s,
|
|
81
81
|
operators: column.operators.map(&:to_s),
|
|
82
82
|
value_options: format_value_options(column.value_options),
|
|
83
|
-
search_url: resolve_search_url(column.search_url, view_context)
|
|
83
|
+
search_url: resolve_search_url(column.search_url, view_context),
|
|
84
|
+
preload_options: column.preload_options
|
|
84
85
|
}
|
|
85
86
|
end
|
|
86
87
|
end
|
|
@@ -107,12 +108,20 @@ module Spree
|
|
|
107
108
|
|
|
108
109
|
resolved_options.map do |opt|
|
|
109
110
|
if opt.is_a?(Hash)
|
|
110
|
-
{ value: opt[:value] || opt['value'], label: opt[:label] || opt['label'] }
|
|
111
|
+
{ value: opt[:value] || opt['value'], label: resolve_option_label(opt[:label] || opt['label']) }
|
|
111
112
|
else
|
|
112
113
|
{ value: opt.to_s, label: opt.to_s.humanize }
|
|
113
114
|
end
|
|
114
115
|
end
|
|
115
116
|
end
|
|
117
|
+
|
|
118
|
+
# Translate a value-option label when it is given as an i18n key (a dotted
|
|
119
|
+
# string like 'admin.products.draft'); plain strings are returned as-is.
|
|
120
|
+
def resolve_option_label(label)
|
|
121
|
+
return label unless label.is_a?(String) && label.include?('.')
|
|
122
|
+
|
|
123
|
+
I18n.t(label, default: label.split('.').last.humanize)
|
|
124
|
+
end
|
|
116
125
|
end
|
|
117
126
|
end
|
|
118
127
|
end
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
</button>
|
|
53
53
|
|
|
54
54
|
<% if show_delete_button %>
|
|
55
|
-
<button type="button" class="btn btn-danger
|
|
55
|
+
<button type="button" class="btn btn-danger ms-auto" data-action="active-storage-upload#remove" data-turbo-confirm="<%= Spree.t(:are_you_sure) %>">
|
|
56
56
|
<%= icon('trash') %>
|
|
57
57
|
|
|
58
58
|
<%= Spree.t('actions.destroy') %>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html lang="<%= I18n.locale %>">
|
|
2
|
+
<html lang="<%= I18n.locale %>" dir="<%= html_dir %>">
|
|
3
3
|
<head>
|
|
4
4
|
<%= render "spree/admin/shared/head" %>
|
|
5
5
|
</head>
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
<%= link_to_with_icon 'arrow-back', Spree.t('admin.back_to_dashboard'), spree.admin_path, class: 'hover:bg-gray-100 flex justify-start gap-2 px-8 no-underline h-16 items-center' %>
|
|
15
15
|
|
|
16
16
|
<% if content_for?(:steps) %>
|
|
17
|
-
<ol class="flex items-center list-none p-0 grow justify-between h-16 border-
|
|
17
|
+
<ol class="flex items-center list-none p-0 grow justify-between h-16 border-s border-gray-200" id="steps">
|
|
18
18
|
<%= yield :steps %>
|
|
19
19
|
</ol>
|
|
20
20
|
<% end %>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<div class="text-gray-600 py-12 gap-4 flex flex-col items-center justify-center">
|
|
2
|
-
<p
|
|
2
|
+
<p><%= Spree.t('admin.admin_users.audit_log_enterprise_notice') %></p>
|
|
3
3
|
|
|
4
|
-
<%= external_link_to
|
|
4
|
+
<%= external_link_to Spree.t('admin.admin_users.upgrade_to_enterprise'), 'https://spreecommerce.org/pricing', class: 'btn btn-primary' %>
|
|
5
5
|
</div>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
<div class="rounded-md border p-6 mb-6 flex items-center gap-2">
|
|
5
5
|
<%= render_avatar(@invitation.inviter, height: 32, width: 32) %>
|
|
6
|
-
<%= @invitation.inviter.name
|
|
6
|
+
<%= Spree.t('admin.invitations.invited_you_to_join_html', inviter: tag.strong(@invitation.inviter.name), resource: tag.strong(@invitation.resource.name)) %>
|
|
7
7
|
</div>
|
|
8
8
|
|
|
9
9
|
<%= render 'form', f: f %>
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
<%= drawer_discard_button %>
|
|
67
67
|
<%= link_to Spree.t('actions.destroy'), object_url(@asset),
|
|
68
68
|
data: { turbo_method: :delete, turbo_confirm: Spree.t(:are_you_sure_delete), turbo_frame: '_top' },
|
|
69
|
-
class: 'btn btn-danger
|
|
69
|
+
class: 'btn btn-danger ms-auto' if can?(:destroy, @asset) %>
|
|
70
70
|
</div>
|
|
71
71
|
<% end %>
|
|
72
72
|
<% end %>
|
|
@@ -15,5 +15,15 @@
|
|
|
15
15
|
{ label: Spree.t('admin.channels.order_routing_strategy'),
|
|
16
16
|
help_bubble: Spree.t('admin.channels.order_routing_strategy_hint'),
|
|
17
17
|
include_blank: Spree.t('admin.channels.order_routing_strategy_inherit') } %>
|
|
18
|
+
<%= f.spree_select :preferred_storefront_access,
|
|
19
|
+
options_for_select(channel_storefront_access_options, f.object.preferred_storefront_access),
|
|
20
|
+
{ label: Spree.t('admin.channels.storefront_access'),
|
|
21
|
+
help_bubble: Spree.t('admin.channels.storefront_access_hint'),
|
|
22
|
+
include_blank: Spree.t('admin.channels.storefront_access_inherit') } %>
|
|
23
|
+
<%= f.spree_select :preferred_guest_checkout,
|
|
24
|
+
options_for_select(channel_guest_checkout_options, f.object.preferred_guest_checkout&.to_s || ''),
|
|
25
|
+
{ label: Spree.t('admin.channels.guest_checkout'),
|
|
26
|
+
help_bubble: Spree.t('admin.channels.guest_checkout_hint'),
|
|
27
|
+
include_blank: Spree.t('admin.channels.guest_checkout_inherit') } %>
|
|
18
28
|
</div>
|
|
19
29
|
</div>
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
<div class="card mb-6" id="setup_progress">
|
|
2
2
|
<div class="card-body">
|
|
3
3
|
<div class="mb-4">
|
|
4
|
-
|
|
5
|
-
<span class="
|
|
4
|
+
<%= Spree.t('admin.dashboard.setup_progress.title') %>
|
|
5
|
+
<span class="badge badge-<%= current_store.setup_completed? ? 'success' : 'info' %> inline-block">
|
|
6
6
|
<% if current_store.setup_completed? %>
|
|
7
7
|
<%= icon('check', class: 'text-green-700') %>
|
|
8
8
|
<% end %>
|
|
9
9
|
|
|
10
|
-
<span>
|
|
11
|
-
|
|
10
|
+
<span dir="auto">
|
|
11
|
+
<%= Spree.t('admin.dashboard.setup_progress.steps_done_html', done: current_store.setup_tasks_done, total: current_store.setup_tasks_total) %>
|
|
12
12
|
</span>
|
|
13
13
|
</span>
|
|
14
14
|
</div>
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
<div id="setup_tasks" class="flex flex-col gap-4">
|
|
2
2
|
<% task_in_progress = nil %>
|
|
3
|
-
<% current_store.
|
|
4
|
-
<%
|
|
3
|
+
<% current_store.setup_tasks.each do |item| %>
|
|
4
|
+
<% task = item.name %>
|
|
5
|
+
<% done = item.done %>
|
|
5
6
|
<% unless done %>
|
|
6
7
|
<% task_in_progress ||= task %>
|
|
7
8
|
<% end %>
|
|
8
9
|
<% is_expanded = task_in_progress == task %>
|
|
9
10
|
|
|
10
11
|
<div class="card overflow-hidden" data-controller="reveal" data-reveal-hidden-class="is-collapsed" id="setup_task_<%= task %>">
|
|
11
|
-
<div class="card-header card-header--collapsible border-0 h-auto no-underline flex items-center p-3 text-gray-900 font-normal w-full bg-transparent cursor-pointer focus:outline-none focus:ring-0 hover:bg-gray-50"
|
|
12
|
+
<div class="card-header card-header--collapsible border-0 h-auto no-underline flex items-center gap-2 p-3 text-gray-900 font-normal w-full bg-transparent cursor-pointer focus:outline-none focus:ring-0 hover:bg-gray-50"
|
|
12
13
|
role="button"
|
|
13
14
|
tabindex="0"
|
|
14
15
|
data-action="click->reveal#toggle keydown.enter->reveal#toggle keydown.space->reveal#toggle">
|
|
15
16
|
<%= onboarding_check_circle(done) %>
|
|
16
17
|
<span class="ml-6 font-medium"><%= Spree.t("admin.store_setup_tasks.#{task}") %></span>
|
|
17
|
-
<%= icon('chevron-right', class: 'arrow
|
|
18
|
+
<%= icon('chevron-right', class: 'arrow ms-auto transition-transform duration-200') %>
|
|
18
19
|
</div>
|
|
19
20
|
<div data-reveal-target="item" class="collapsible-content <%= 'is-collapsed' unless is_expanded %>">
|
|
20
21
|
<div>
|
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
<%= image_tag 'favicon_256x256.png', alt: 'Spree', width: 32, height: 32 %>
|
|
4
4
|
<div class="flex flex-col">
|
|
5
5
|
<p class="mb-1">
|
|
6
|
-
|
|
7
|
-
<br
|
|
6
|
+
<%= Spree.t('admin.dashboard.updater.available_html', version: tag.strong("Spree #{spree_updater.latest_release['name']}")) %>
|
|
7
|
+
<br /><%= Spree.t('admin.dashboard.updater.current_version_html', version: tag.strong("Spree #{spree_updater.current_release}")) %>
|
|
8
8
|
</p>
|
|
9
9
|
|
|
10
10
|
<% if spree_updater.latest_release['url'] %>
|
|
11
11
|
<div>
|
|
12
|
-
<%= external_link_to
|
|
12
|
+
<%= external_link_to Spree.t('admin.dashboard.updater.view_release_notes'), spree_updater.latest_release['url'], target: '_blank', class: 'btn btn-sm btn-secondary' %>
|
|
13
13
|
</div>
|
|
14
14
|
<% end %>
|
|
15
15
|
</div>
|
|
16
16
|
|
|
17
|
-
<%= link_to '', spree.admin_dismiss_updater_notice_path, data: { turbo_method: :patch }, class: 'btn-close
|
|
17
|
+
<%= link_to '', spree.admin_dismiss_updater_notice_path, data: { turbo_method: :patch }, class: 'btn-close ms-auto' %>
|
|
18
18
|
</div>
|
|
19
19
|
<% end %>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<div class="card h-full">
|
|
4
4
|
<div class="card-header">
|
|
5
5
|
<h5 class="card-title">
|
|
6
|
-
|
|
6
|
+
<%= Spree.t('admin.dashboard.visits.referrers_title') %>
|
|
7
7
|
</h5>
|
|
8
8
|
</div>
|
|
9
9
|
<% if @top_referrers.any? %>
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
<p class="m-12 text-center text-gray-600">
|
|
20
20
|
<%= icon 'share', height: 50, class: 'mb-4', style: 'opacity: 0.5' %>
|
|
21
21
|
<br />
|
|
22
|
-
<strong
|
|
22
|
+
<strong><%= Spree.t('admin.dashboard.visits.no_data') %></strong>
|
|
23
23
|
<br />
|
|
24
|
-
|
|
24
|
+
<%= Spree.t('admin.dashboard.visits.no_data_for_period') %>
|
|
25
25
|
</p>
|
|
26
26
|
<% end %>
|
|
27
27
|
</div>
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<div class="card h-full">
|
|
31
31
|
<div class="card-header">
|
|
32
32
|
<h5 class="card-title">
|
|
33
|
-
|
|
33
|
+
<%= Spree.t('admin.dashboard.visits.landing_pages_title') %>
|
|
34
34
|
</h5>
|
|
35
35
|
</div>
|
|
36
36
|
<% if @top_landing_pages.any? %>
|
|
@@ -46,9 +46,9 @@
|
|
|
46
46
|
<p class="m-12 text-center text-gray-600">
|
|
47
47
|
<%= icon 'pointer', height: 50, class: 'mb-4', style: 'opacity: 0.5' %>
|
|
48
48
|
<br />
|
|
49
|
-
<strong
|
|
49
|
+
<strong><%= Spree.t('admin.dashboard.visits.no_data') %></strong>
|
|
50
50
|
<br />
|
|
51
|
-
|
|
51
|
+
<%= Spree.t('admin.dashboard.visits.no_data_for_period') %>
|
|
52
52
|
</p>
|
|
53
53
|
<% end %>
|
|
54
54
|
</div>
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
<div class="card h-full">
|
|
58
58
|
<div class="card-header">
|
|
59
59
|
<h5 class="card-title">
|
|
60
|
-
|
|
60
|
+
<%= Spree.t('admin.dashboard.visits.locations_title') %>
|
|
61
61
|
</h5>
|
|
62
62
|
</div>
|
|
63
63
|
<% if @top_locations.any? %>
|
|
@@ -80,9 +80,9 @@
|
|
|
80
80
|
<p class="m-12 text-center text-gray-600">
|
|
81
81
|
<%= icon 'world', height: 50, class: 'mb-4', style: 'opacity: 0.5' %>
|
|
82
82
|
<br />
|
|
83
|
-
<strong
|
|
83
|
+
<strong><%= Spree.t('admin.dashboard.visits.no_data') %></strong>
|
|
84
84
|
<br />
|
|
85
|
-
|
|
85
|
+
<%= Spree.t('admin.dashboard.visits.no_data_for_period') %>
|
|
86
86
|
</p>
|
|
87
87
|
<% end %>
|
|
88
88
|
</div>
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
<div class="card">
|
|
92
92
|
<div class="card-header">
|
|
93
93
|
<h5 class="card-title">
|
|
94
|
-
|
|
94
|
+
<%= Spree.t('admin.dashboard.visits.devices_title') %>
|
|
95
95
|
</h5>
|
|
96
96
|
</div>
|
|
97
97
|
<% if @top_devices.count > 0 %>
|
|
@@ -102,9 +102,9 @@
|
|
|
102
102
|
<p class="m-12 text-center text-gray-600">
|
|
103
103
|
<%= icon 'device-mobile', height: 50, class: 'mb-4', style: 'opacity: 0.5' %>
|
|
104
104
|
<br />
|
|
105
|
-
<strong
|
|
105
|
+
<strong><%= Spree.t('admin.dashboard.visits.no_data') %></strong>
|
|
106
106
|
<br />
|
|
107
|
-
|
|
107
|
+
<%= Spree.t('admin.dashboard.visits.no_data_for_period') %>
|
|
108
108
|
</p>
|
|
109
109
|
<% end %>
|
|
110
110
|
</div>
|