From e200eccc426b4e3d8f8f099df35d937787d90427 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Bellet Date: Thu, 29 Jun 2023 09:16:59 +0200 Subject: [PATCH 01/11] Replace angular editor by trix editor --- app/views/spree/admin/products/_form.html.haml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/views/spree/admin/products/_form.html.haml b/app/views/spree/admin/products/_form.html.haml index 2935fa0dc5..cb68a2dbff 100644 --- a/app/views/spree/admin/products/_form.html.haml +++ b/app/views/spree/admin/products/_form.html.haml @@ -7,9 +7,8 @@ = f.field_container :description do = f.label :description, t(:description) - %text-angular{'id' => 'product_description', 'name' => 'product[description]', 'class' => 'text-angular', 'textangular-unsafe-sanitizer' => true, "textangular-links-target-blank" => true, 'ta-toolbar' => "[['bold','italics','underline','clear'],['insertLink']]"} - = sanitize(@product.description, scrubber: ContentScrubber.new) - = f.error_message_on :description + = f.hidden_field :description, id: "product_description", value: sanitize(@product.description, scrubber: TrixScrubber.new) + %trix-editor{ input: "product_description", "data-controller": "trixeditor" } .right.four.columns.omega .variant_units_form{ 'ng-app' => 'admin.products', 'ng-controller' => 'editUnitsCtrl' } From b6e047086a12bde3e1bab56ee76d9712bc0e6ad3 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Bellet Date: Thu, 29 Jun 2023 09:17:56 +0200 Subject: [PATCH 02/11] Update trixEditor rendering `` element: should be green --- app/webpacker/css/admin/trix.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/webpacker/css/admin/trix.scss b/app/webpacker/css/admin/trix.scss index 7ee927a2f3..e582277143 100644 --- a/app/webpacker/css/admin/trix.scss +++ b/app/webpacker/css/admin/trix.scss @@ -12,7 +12,7 @@ trix-editor { } a { - color: #f27052; + color: #9fc820; // Equivalent to text-angular a } // Copy/pasted from _type.scss From f9bc00e5cddf7a26187ce4a8055290017836a42a Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Bellet Date: Thu, 29 Jun 2023 09:19:54 +0200 Subject: [PATCH 03/11] We used to use `

` to seperate block between them. now use `

` trix doesn't allow the use of `

` as block separator since it can not contain `

`: use `
` and _emulate_ as `

` with margin bottom --- app/services/trix_scrubber.rb | 2 +- app/webpacker/css/admin/trix.scss | 4 ++++ app/webpacker/css/darkswarm/_shop-modals.scss | 4 ++++ app/webpacker/css/darkswarm/_shop-product-rows.scss | 4 ++++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/app/services/trix_scrubber.rb b/app/services/trix_scrubber.rb index b8328ffc1c..a8aed288c0 100644 --- a/app/services/trix_scrubber.rb +++ b/app/services/trix_scrubber.rb @@ -2,7 +2,7 @@ class TrixScrubber < Rails::Html::PermitScrubber ALLOWED_TAGS = ["p", "b", "strong", "em", "i", "a", "u", "br", "del", "h1", "blockquote", "pre", - "ul", "ol", "li"].freeze + "ul", "ol", "li", "div"].freeze ALLOWED_ATTRIBUTES = ["href", "target", "src", "alt"].freeze def initialize diff --git a/app/webpacker/css/admin/trix.scss b/app/webpacker/css/admin/trix.scss index e582277143..39caa36dae 100644 --- a/app/webpacker/css/admin/trix.scss +++ b/app/webpacker/css/admin/trix.scss @@ -15,6 +15,10 @@ trix-editor { color: #9fc820; // Equivalent to text-angular a } + div { + margin-bottom: 1.5rem; // Equivalent to text-angular p (trix doesn't use p as default one, since we could not include figures inside p) + } + // Copy/pasted from _type.scss blockquote { line-height: 1.6; diff --git a/app/webpacker/css/darkswarm/_shop-modals.scss b/app/webpacker/css/darkswarm/_shop-modals.scss index bb6b6480dc..812dfa60ff 100644 --- a/app/webpacker/css/darkswarm/_shop-modals.scss +++ b/app/webpacker/css/darkswarm/_shop-modals.scss @@ -11,6 +11,10 @@ .product-description { margin: 1rem 0.25rem 0.25rem 0; + + .text-small div { + margin-bottom: 1.5rem; // Equivalent to p (trix doesn't use p as separator by default, so emulate div as p to be backward compatible) + } } .property-selectors li { diff --git a/app/webpacker/css/darkswarm/_shop-product-rows.scss b/app/webpacker/css/darkswarm/_shop-product-rows.scss index 401db4f8a0..39051651d1 100644 --- a/app/webpacker/css/darkswarm/_shop-product-rows.scss +++ b/app/webpacker/css/darkswarm/_shop-product-rows.scss @@ -158,6 +158,10 @@ // line-clamp is not supported in Safari line-height: 1rem; height: 1.75rem; + + > div { + margin-bottom: 1.5rem; // Equivalent to p (trix doesn't use p as separator by default, so emulate div as p to be backward compatible) + } } .product-properties { From 4c27e79519166479da1bcff920581b77978710fb Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Bellet Date: Thu, 29 Jun 2023 09:20:48 +0200 Subject: [PATCH 04/11] Sanitize content when display it on shop Use the TrixSanitizer | TrixScrubber --- app/serializers/api/product_serializer.rb | 6 +++++- app/services/trix_sanitizer.rb | 11 +++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 app/services/trix_sanitizer.rb diff --git a/app/serializers/api/product_serializer.rb b/app/serializers/api/product_serializer.rb index cf37d7464a..11f228c1e6 100644 --- a/app/serializers/api/product_serializer.rb +++ b/app/serializers/api/product_serializer.rb @@ -21,7 +21,7 @@ class Api::ProductSerializer < ActiveModel::Serializer # return a sanitized html description def description_html - sanitizer.sanitize_content(object.description)&.html_safe + trix_sanitizer.sanitize_content(object.description) end def properties_with_values @@ -37,4 +37,8 @@ class Api::ProductSerializer < ActiveModel::Serializer def sanitizer @sanitizer ||= ContentSanitizer.new end + + def trix_sanitizer + @trix_sanitizer ||= TrixSanitizer.new + end end diff --git a/app/services/trix_sanitizer.rb b/app/services/trix_sanitizer.rb new file mode 100644 index 0000000000..5b4d83b4e6 --- /dev/null +++ b/app/services/trix_sanitizer.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class TrixSanitizer + include ActionView::Helpers::SanitizeHelper + + def sanitize_content(content) + return if content.blank? + + sanitize(content.to_s, scrubber: TrixScrubber.new) + end +end From a8a35318f596b768853f7a1c947e10a58e7d5c8f Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Bellet Date: Thu, 6 Jul 2023 22:24:04 +0200 Subject: [PATCH 05/11] Add horizontal rule to trix editor --- app/services/trix_scrubber.rb | 2 +- .../controllers/trixeditor_controller.js | 33 +++++++++++++++++++ app/webpacker/css/admin/trix.scss | 4 +++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/app/services/trix_scrubber.rb b/app/services/trix_scrubber.rb index a8aed288c0..e8ab55afe2 100644 --- a/app/services/trix_scrubber.rb +++ b/app/services/trix_scrubber.rb @@ -2,7 +2,7 @@ class TrixScrubber < Rails::Html::PermitScrubber ALLOWED_TAGS = ["p", "b", "strong", "em", "i", "a", "u", "br", "del", "h1", "blockquote", "pre", - "ul", "ol", "li", "div"].freeze + "ul", "ol", "li", "div", "hr"].freeze ALLOWED_ATTRIBUTES = ["href", "target", "src", "alt"].freeze def initialize diff --git a/app/webpacker/controllers/trixeditor_controller.js b/app/webpacker/controllers/trixeditor_controller.js index 7975d213ed..506086fb2b 100644 --- a/app/webpacker/controllers/trixeditor_controller.js +++ b/app/webpacker/controllers/trixeditor_controller.js @@ -3,10 +3,43 @@ import { Controller } from "stimulus"; export default class extends Controller { connect() { window.addEventListener("trix-change", this.#trixChange); + this.#trixInitialize(); + window.addEventListener("trix-initialize", this.#trixInitialize); } #trixChange = (event) => { // trigger a change event on the form that contains the Trix editor event.target.form.dispatchEvent(new Event("change", { bubbles: true })); }; + + #trixActionInvoke = (event) => { + if (event.actionName === "hr") { + this.element.editor.insertAttachment( + new Trix.Attachment({ content: "


", contentType: "text/html" }) + ); + } + }; + + #trixInitialize = () => { + // Add HR button to the Trix toolbar if it's not already there and the editor is present + if ( + this.element.editor && + !this.element.toolbarElement.querySelector(".trix-button--icon-hr") + ) { + this.#addHRButton(); + } + }; + + #addHRButton = () => { + const button_html = ` + `; + const buttonGroup = this.element.toolbarElement.querySelector( + ".trix-button-group--block-tools" + ); + buttonGroup.insertAdjacentHTML("beforeend", button_html); + buttonGroup.querySelector(".trix-button--icon-hr").addEventListener("click", (event) => { + event.actionName = "hr"; + this.#trixActionInvoke(event); + }); + }; } diff --git a/app/webpacker/css/admin/trix.scss b/app/webpacker/css/admin/trix.scss index 39caa36dae..59c46b7451 100644 --- a/app/webpacker/css/admin/trix.scss +++ b/app/webpacker/css/admin/trix.scss @@ -2,6 +2,10 @@ trix-toolbar [data-trix-button-group="file-tools"] { display: none; } +trix-toolbar .trix-button--icon-hr::before { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M3 12h18v2H3z'/%3E%3C/svg%3E"); +} + // Match the rendering into the shopfront (see ../darkswarm/) trix-editor { color: #222; From 3225c52898f794b560ea97967731e4f7b33e207b Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Bellet Date: Thu, 6 Jul 2023 22:24:27 +0200 Subject: [PATCH 06/11] Actually, this should not be sanitize before going through trix editor --- app/views/spree/admin/products/_form.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/spree/admin/products/_form.html.haml b/app/views/spree/admin/products/_form.html.haml index cb68a2dbff..1bd362b7e7 100644 --- a/app/views/spree/admin/products/_form.html.haml +++ b/app/views/spree/admin/products/_form.html.haml @@ -7,7 +7,7 @@ = f.field_container :description do = f.label :description, t(:description) - = f.hidden_field :description, id: "product_description", value: sanitize(@product.description, scrubber: TrixScrubber.new) + = f.hidden_field :description, id: "product_description", value: @product.description %trix-editor{ input: "product_description", "data-controller": "trixeditor" } .right.four.columns.omega From ca6d12e8ed78186bec3bd837de5046556681f5de Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Bellet Date: Fri, 11 Aug 2023 15:55:25 +0200 Subject: [PATCH 07/11] Actually, used in the shopfront context, links are orange --- app/webpacker/css/admin/trix.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/webpacker/css/admin/trix.scss b/app/webpacker/css/admin/trix.scss index 59c46b7451..4f5cb3f533 100644 --- a/app/webpacker/css/admin/trix.scss +++ b/app/webpacker/css/admin/trix.scss @@ -16,7 +16,7 @@ trix-editor { } a { - color: #9fc820; // Equivalent to text-angular a + color: #f27052; // Equivalent to text-angular a } div { From c402093081891c42e2e40d92e9d6d4a649eaf063 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Bellet Date: Fri, 11 Aug 2023 15:57:45 +0200 Subject: [PATCH 08/11] Special case for unordered list in trix editor: should be overwritten --- app/webpacker/css/admin/trix.scss | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/webpacker/css/admin/trix.scss b/app/webpacker/css/admin/trix.scss index 4f5cb3f533..4d9d6dc963 100644 --- a/app/webpacker/css/admin/trix.scss +++ b/app/webpacker/css/admin/trix.scss @@ -32,3 +32,15 @@ trix-editor { border-left: 1px solid #dddddd; } } + +// Override app/webpacker/css/admin/shared/forms.scss#L81 +.field trix-editor ul { + border-top: none; + list-style: disc; + padding-top: 0; + + li { + padding-right: 0; + display: list-item; + } +} From 34dab4003d4cfb9d61e373f43f7bde2702616055 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Bellet Date: Fri, 11 Aug 2023 15:57:21 +0200 Subject: [PATCH 09/11] Create a mixin that include list of styles for trix editor + content Will be used elsewhere (in shopfront in particular) --- app/webpacker/css/admin/all.scss | 2 ++ app/webpacker/css/admin/trix.scss | 18 +------------- app/webpacker/css/admin_v3/all.scss | 2 ++ app/webpacker/css/darkswarm/all.scss | 1 + app/webpacker/css/shared/trix.scss | 35 ++++++++++++++++++++++++++++ 5 files changed, 41 insertions(+), 17 deletions(-) create mode 100644 app/webpacker/css/shared/trix.scss diff --git a/app/webpacker/css/admin/all.scss b/app/webpacker/css/admin/all.scss index eba04c98ad..35e4270d8f 100644 --- a/app/webpacker/css/admin/all.scss +++ b/app/webpacker/css/admin/all.scss @@ -30,6 +30,8 @@ @import "shared/layout"; @import "shared/scroll_bar"; +@import "../shared/trix"; + @import "plugins/flatpickr-customization"; @import "plugins/powertip"; @import "plugins/jstree"; diff --git a/app/webpacker/css/admin/trix.scss b/app/webpacker/css/admin/trix.scss index 4d9d6dc963..78c149dc32 100644 --- a/app/webpacker/css/admin/trix.scss +++ b/app/webpacker/css/admin/trix.scss @@ -10,27 +10,11 @@ trix-toolbar .trix-button--icon-hr::before { trix-editor { color: #222; - ol, - ul { - margin-left: 1.5em; - } - a { color: #f27052; // Equivalent to text-angular a } - div { - margin-bottom: 1.5rem; // Equivalent to text-angular p (trix doesn't use p as default one, since we could not include figures inside p) - } - - // Copy/pasted from _type.scss - blockquote { - line-height: 1.6; - color: #6f6f6f; - margin: 0 0 1.25rem; - padding: 0.5625rem 1.25rem 0 1.1875rem; - border-left: 1px solid #dddddd; - } + @include trix-styles; } // Override app/webpacker/css/admin/shared/forms.scss#L81 diff --git a/app/webpacker/css/admin_v3/all.scss b/app/webpacker/css/admin_v3/all.scss index e83df91661..b6a12acf66 100644 --- a/app/webpacker/css/admin_v3/all.scss +++ b/app/webpacker/css/admin_v3/all.scss @@ -35,6 +35,8 @@ @import "shared/layout"; // admin_v3 @import "../admin/shared/scroll_bar"; +@import "../shared/trix"; + @import "../admin/plugins/flatpickr-customization"; @import "../admin/plugins/powertip"; @import "../admin/plugins/jstree"; diff --git a/app/webpacker/css/darkswarm/all.scss b/app/webpacker/css/darkswarm/all.scss index a642ac38fd..a78b2c30e2 100644 --- a/app/webpacker/css/darkswarm/all.scss +++ b/app/webpacker/css/darkswarm/all.scss @@ -10,6 +10,7 @@ @import 'branding'; @import 'typography'; @import 'mixins'; +@import '../shared/trix'; @import 'base/colors'; @import 'animations'; diff --git a/app/webpacker/css/shared/trix.scss b/app/webpacker/css/shared/trix.scss new file mode 100644 index 0000000000..7953954df7 --- /dev/null +++ b/app/webpacker/css/shared/trix.scss @@ -0,0 +1,35 @@ +// A mixin used to include some trix styles in the shopfront that could have been reseted +@mixin trix-styles { + ol, + ul { + margin-left: 1.5em; + } + + ol { + list-style-type: decimal; + } + + ul { + list-style-type: disc; + } + + div, + pre, + h1 { + margin-bottom: 1.5rem; // Equivalent to text-angular p (trix doesn't use p as default one, since we could not include figures inside p) + } + + h1 { + color: #222222; + font-size: 1.1rem; + } + + // Copy/pasted from _type.scss + blockquote { + line-height: 1.6; + color: #6f6f6f; + margin: 0 0 1.25rem; + padding: 0.5625rem 1.25rem 0 1.1875rem; + border-left: 1px solid #dddddd; + } +} From 1d5d1c54057ed5be6f3784451f6f04a75f963f85 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Bellet Date: Fri, 11 Aug 2023 15:58:19 +0200 Subject: [PATCH 10/11] Include trix-styles mixin in where content from trix is displayed --- app/webpacker/css/darkswarm/_shop-modals.scss | 2 ++ app/webpacker/css/darkswarm/_shop-product-rows.scss | 2 ++ app/webpacker/css/darkswarm/shop_tabs.scss | 12 +----------- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/app/webpacker/css/darkswarm/_shop-modals.scss b/app/webpacker/css/darkswarm/_shop-modals.scss index 812dfa60ff..1beed67851 100644 --- a/app/webpacker/css/darkswarm/_shop-modals.scss +++ b/app/webpacker/css/darkswarm/_shop-modals.scss @@ -15,6 +15,8 @@ .text-small div { margin-bottom: 1.5rem; // Equivalent to p (trix doesn't use p as separator by default, so emulate div as p to be backward compatible) } + + @include trix-styles; } .property-selectors li { diff --git a/app/webpacker/css/darkswarm/_shop-product-rows.scss b/app/webpacker/css/darkswarm/_shop-product-rows.scss index 39051651d1..f731ec4ce6 100644 --- a/app/webpacker/css/darkswarm/_shop-product-rows.scss +++ b/app/webpacker/css/darkswarm/_shop-product-rows.scss @@ -162,6 +162,8 @@ > div { margin-bottom: 1.5rem; // Equivalent to p (trix doesn't use p as separator by default, so emulate div as p to be backward compatible) } + + @include trix-styles; } .product-properties { diff --git a/app/webpacker/css/darkswarm/shop_tabs.scss b/app/webpacker/css/darkswarm/shop_tabs.scss index 490231366e..e87b3ae0e1 100644 --- a/app/webpacker/css/darkswarm/shop_tabs.scss +++ b/app/webpacker/css/darkswarm/shop_tabs.scss @@ -121,17 +121,7 @@ } .custom-tab { - ol, ul { - margin-left: 1.5em; - } - - ol { - list-style-type: decimal; - } - - ul { - list-style-type: disc; - } + @include trix-styles; } } From b6b64e943957cd4743045130207f4cba60b94b84 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Bellet Date: Fri, 11 Aug 2023 16:31:17 +0200 Subject: [PATCH 11/11] Add trix editor to new product form --- app/views/spree/admin/products/new.html.haml | 4 ++-- spec/system/admin/products_spec.rb | 23 ++++++++++---------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/app/views/spree/admin/products/new.html.haml b/app/views/spree/admin/products/new.html.haml index a885d77300..71710c1258 100644 --- a/app/views/spree/admin/products/new.html.haml +++ b/app/views/spree/admin/products/new.html.haml @@ -92,8 +92,8 @@ = f.field_container :description do = f.label :product_description, t(".product_description") %br/ - %text-angular{'id' => 'product_description', 'name' => 'product[description]', 'class' => 'text-angular', "textangular-links-target-blank" => true, 'ta-toolbar' => "[['bold','italics','underline','clear'],['insertLink']]", "ng-model": "product.description"} - = sanitize(@product.description) + = f.hidden_field :description, id: "product_description", value: @product.description + %trix-editor{ input: "product_description", "data-controller": "trixeditor" } = f.error_message_on :description .four.columns.omega{ style: "text-align: center" } %fieldset.no-border-bottom{ id: "image" } diff --git a/spec/system/admin/products_spec.rb b/spec/system/admin/products_spec.rb index a6c2183b6e..d01ff3b5c3 100644 --- a/spec/system/admin/products_spec.rb +++ b/spec/system/admin/products_spec.rb @@ -45,7 +45,7 @@ describe ' fill_in 'product_on_hand', with: 5 check 'product_on_demand' select 'Test Tax Category', from: 'product_tax_category_id' - page.find("div[id^='taTextElement']").native.send_keys('A description...') + fill_in_trix_editor 'product_description', with: 'A description...' click_button 'Create' @@ -58,7 +58,8 @@ describe ' expect(page).to have_field 'product_on_hand', with: 5 expect(page).to have_field 'product_on_demand', checked: true expect(page).to have_field 'product_tax_category_id', with: tax_category.id - expect(page.find("div[id^='taTextElement']")).to have_content 'A description...' + expect(page.find("#product_description", + visible: false).value).to eq('
A description...
') expect(page.find("#product_variant_unit_field")).to have_content 'Weight (kg)' expect(page).to have_content "Name can't be blank" @@ -80,7 +81,7 @@ describe ' fill_in 'product_on_hand', with: 5 check 'product_on_demand' select 'Test Tax Category', from: 'product_tax_category_id' - page.find("div[id^='taTextElement']").native.send_keys('A description...') + fill_in_trix_editor 'product_description', with: 'A description...' click_button 'Create' @@ -93,7 +94,8 @@ describe ' expect(page).to have_field 'product_on_hand', with: 5 expect(page).to have_field 'product_on_demand', checked: true expect(page).to have_field 'product_tax_category_id', with: tax_category.id - expect(page.find("div[id^='taTextElement']")).to have_content 'A description...' + expect(page.find("#product_description", + visible: false).value).to eq('
A description...
') expect(page.find("#product_variant_unit_field")).to have_content 'Weight (kg)' expect(page).to have_content "Unit value must be greater than 0" @@ -128,7 +130,7 @@ describe ' fill_in 'product_price', with: '19.99' fill_in 'product_on_hand', with: 5 select 'Test Tax Category', from: 'product_tax_category_id' - page.find("div[id^='taTextElement']").native.send_keys('A description...') + fill_in_trix_editor 'product_description', with: 'A description...' click_button 'Create' @@ -146,7 +148,7 @@ describe ' expect(product.on_hand).to eq(5) expect(product.variants.first.tax_category_id).to eq(tax_category.id) expect(product.variants.first.shipping_category).to eq(shipping_category) - expect(product.description).to eq("

A description...

") + expect(product.description).to eq("
A description...
") expect(product.group_buy).to be_falsey expect(product.variants.first.unit_presentation).to eq("5kg") end @@ -166,8 +168,8 @@ describe ' fill_in 'product_on_hand', with: 0 check 'product_on_demand' select 'Test Tax Category', from: 'product_tax_category_id' - page.find("div[id^='taTextElement']").native - .send_keys('In demand, and on_demand! The hottest cakes in town.') + fill_in_trix_editor 'product_description', + with: 'In demand, and on_demand! The hottest cakes in town.' click_button 'Create' @@ -193,9 +195,8 @@ describe ' fill_in 'product_on_hand', with: 0 check 'product_on_demand' select 'Test Tax Category', from: 'product_tax_category_id' - find("div[id^='taTextElement']").native - .send_keys('In demand, and on_demand! The hottest cakes in town.') - + fill_in_trix_editor 'product_description', + with: 'In demand, and on_demand! The hottest cakes in town.' click_button 'Create' expect(current_path).to eq spree.admin_products_path