mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-02-06 22:36:07 +00:00
Merge pull request #11163 from jibees/productsV3-searching-filtering-pagination
🚧 Products v3: viewing, searching, filtering & pagination
This commit is contained in:
@@ -191,6 +191,8 @@ module Spree
|
||||
OpenFoodNetwork::Permissions.new(user).managed_product_enterprises.include? product.supplier
|
||||
end
|
||||
|
||||
can [:admin, :index], :products_v3
|
||||
|
||||
can [:create], Spree::Variant
|
||||
can [:admin, :index, :read, :edit,
|
||||
:update, :search, :delete, :destroy], Spree::Variant do |variant|
|
||||
|
||||
@@ -21,6 +21,12 @@ module Spree
|
||||
|
||||
NAME_FIELDS = ["display_name", "display_as", "weight", "unit_value", "unit_description"].freeze
|
||||
|
||||
SEARCH_KEY = "#{%w(name
|
||||
meta_keywords
|
||||
variants_display_as
|
||||
variants_display_name
|
||||
supplier_name).join('_or_')}_cont".freeze
|
||||
|
||||
belongs_to :product, -> { with_deleted }, touch: true, class_name: 'Spree::Product'
|
||||
|
||||
delegate_belongs_to :product, :name, :description, :tax_category_id, :shipping_category_id,
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class ProductsV3Reflex < ApplicationReflex
|
||||
before_reflex :fetch_products, only: [:fetch]
|
||||
|
||||
def fetch
|
||||
cable_ready.replace(
|
||||
selector: "#products-content",
|
||||
html: render(partial: "admin/products_v3/content", locals: { products: @products })
|
||||
).broadcast
|
||||
|
||||
morph :nothing
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# copied from ProductsTableComponent
|
||||
def fetch_products
|
||||
product_query = OpenFoodNetwork::Permissions.new(current_user)
|
||||
.editable_products.merge(product_scope)
|
||||
@products = product_query.order(:name).limit(50)
|
||||
end
|
||||
|
||||
def product_scope
|
||||
scope = if current_user.has_spree_role?("admin") || current_user.enterprises.present?
|
||||
Spree::Product
|
||||
else
|
||||
Spree::Product.active
|
||||
end
|
||||
|
||||
scope.includes(product_query_includes)
|
||||
end
|
||||
|
||||
# Optimise by pre-loading required columns
|
||||
def product_query_includes
|
||||
# TODO: add other fields used in columns? (eg supplier: [:name])
|
||||
[
|
||||
# variants: [
|
||||
# :default_price,
|
||||
# :stock_locations,
|
||||
# :stock_items,
|
||||
# :variant_overrides
|
||||
# ]
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
133
app/reflexes/products_reflex.rb
Normal file
133
app/reflexes/products_reflex.rb
Normal file
@@ -0,0 +1,133 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ProductsReflex < ApplicationReflex
|
||||
include Pagy::Backend
|
||||
|
||||
before_reflex :init_filters_params, :init_pagination_params
|
||||
|
||||
def fetch
|
||||
fetch_and_render_products
|
||||
end
|
||||
|
||||
def change_per_page
|
||||
@per_page = element.value.to_i
|
||||
@page = 1
|
||||
|
||||
fetch_and_render_products
|
||||
end
|
||||
|
||||
def filter
|
||||
@page = 1
|
||||
|
||||
fetch_and_render_products
|
||||
end
|
||||
|
||||
def clear_search
|
||||
@search_term = nil
|
||||
@producer_id = nil
|
||||
@category_id = nil
|
||||
@page = 1
|
||||
|
||||
fetch_and_render_products
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def init_filters_params
|
||||
# params comes from the form
|
||||
# _params comes from the url
|
||||
# priority is given to params from the form (if present) over url params
|
||||
@search_term = params[:search_term] || params[:_search_term]
|
||||
@producer_id = params[:producer_id] || params[:_producer_id]
|
||||
@category_id = params[:category_id] || params[:_category_id]
|
||||
end
|
||||
|
||||
def init_pagination_params
|
||||
# prority is given to element dataset (if present) over url params
|
||||
@page = element.dataset.page || params[:_page] || 1
|
||||
@per_page = element.dataset.perpage || params[:_per_page] || 15
|
||||
end
|
||||
|
||||
def fetch_and_render_products
|
||||
fetch_products
|
||||
render_products
|
||||
end
|
||||
|
||||
def render_products
|
||||
cable_ready.replace(
|
||||
selector: "#products-content",
|
||||
html: render(partial: "admin/products_v3/content",
|
||||
locals: { products: @products, pagy: @pagy, search_term: @search_term,
|
||||
producer_options: producers, producer_id: @producer_id,
|
||||
category_options: categories, category_id: @category_id })
|
||||
).broadcast
|
||||
|
||||
cable_ready.replace_state(
|
||||
url: current_url,
|
||||
).broadcast_later
|
||||
|
||||
morph :nothing
|
||||
end
|
||||
|
||||
def producers
|
||||
producers = OpenFoodNetwork::Permissions.new(current_user)
|
||||
.managed_product_enterprises.is_primary_producer.by_name
|
||||
producers.map { |p| [p.name, p.id] }
|
||||
end
|
||||
|
||||
def categories
|
||||
Spree::Taxon.order(:name).map { |c| [c.name, c.id] }
|
||||
end
|
||||
|
||||
# copied from ProductsTableComponent
|
||||
def fetch_products
|
||||
product_query = OpenFoodNetwork::Permissions.new(current_user)
|
||||
.editable_products.merge(product_scope).ransack(ransack_query).result
|
||||
@pagy, @products = pagy(product_query.order(:name), items: @per_page, page: @page)
|
||||
end
|
||||
|
||||
def product_scope
|
||||
scope = if current_user.has_spree_role?("admin") || current_user.enterprises.present?
|
||||
Spree::Product
|
||||
else
|
||||
Spree::Product.active
|
||||
end
|
||||
|
||||
scope.includes(product_query_includes)
|
||||
end
|
||||
|
||||
def ransack_query
|
||||
query = { s: "name desc" }
|
||||
query.merge!(supplier_id_in: @producer_id) if @producer_id.present?
|
||||
if @search_term.present?
|
||||
query.merge!(Spree::Variant::SEARCH_KEY => @search_term)
|
||||
end
|
||||
query.merge!(primary_taxon_id_in: @category_id) if @category_id.present?
|
||||
query
|
||||
end
|
||||
|
||||
# Optimise by pre-loading required columns
|
||||
def product_query_includes
|
||||
# TODO: add other fields used in columns? (eg supplier: [:name])
|
||||
[
|
||||
# variants: [
|
||||
# :default_price,
|
||||
# :stock_locations,
|
||||
# :stock_items,
|
||||
# :variant_overrides
|
||||
# ]
|
||||
]
|
||||
end
|
||||
|
||||
def current_url
|
||||
url = URI(request.original_url)
|
||||
url.query = url.query.present? ? "#{url.query}&" : ""
|
||||
# add params with _ to avoid conflicts with params from the form
|
||||
url.query += "_page=#{@page}"
|
||||
url.query += "&_per_page=#{@per_page}"
|
||||
url.query += "&_search_term=#{@search_term}" if @search_term.present?
|
||||
url.query += "&_producer_id=#{@producer_id}" if @producer_id.present?
|
||||
url.query += "&_category_id=#{@category_id}" if @category_id.present?
|
||||
url.to_s
|
||||
end
|
||||
end
|
||||
@@ -1,10 +1,17 @@
|
||||
- if products.any?
|
||||
= render partial: 'table', locals: { products: products }
|
||||
- else
|
||||
#no-products
|
||||
= t('.no_products_found')
|
||||
#no-products-actions
|
||||
%a{ href: "/admin/products/new", class: "button icon-plus", icon: "icon-plus" }
|
||||
= t(:new_product)
|
||||
%a{ href: "/admin/products/import", class: "button icon-upload secondary", icon: "icon-upload" }
|
||||
= t(".import_products")
|
||||
#products-content
|
||||
.container
|
||||
.sixteen.columns
|
||||
= render partial: 'filters', locals: { search_term: search_term,
|
||||
producer_id: producer_id,
|
||||
producer_options: producer_options,
|
||||
category_options: category_options,
|
||||
category_id: category_id }
|
||||
- if products.any?
|
||||
.container
|
||||
.sixteen.columns
|
||||
= render partial: 'sort', locals: { pagy: pagy, search_term: search_term, producer_id: producer_id, category_id: category_id }
|
||||
= render partial: 'table', locals: { products: products }
|
||||
= render partial: 'admin/shared/v3/pagy', locals: { pagy: pagy, reflex: "click->Products#fetch" }
|
||||
- else
|
||||
#no-products
|
||||
= render partial: "no_products", locals: { search_term: search_term, producer_id: producer_id, category_id: category_id }
|
||||
|
||||
13
app/views/admin/products_v3/_filters.html.haml
Normal file
13
app/views/admin/products_v3/_filters.html.haml
Normal file
@@ -0,0 +1,13 @@
|
||||
%form{ id: "filters", 'data-reflex-serialize-form': true, 'data-reflex': 'submit->products#filter' }
|
||||
.query
|
||||
.search-input
|
||||
= text_field_tag :search_term, search_term, placeholder: t('.search_products')
|
||||
.producers
|
||||
.label= t('.producers.label')
|
||||
= select_tag :producer_id, options_for_select(producer_options, producer_id), include_blank: t('.all_producers')
|
||||
.categories
|
||||
.label= t('.categories.label')
|
||||
= select_tag :category_id, options_for_select(category_options, category_id), include_blank: t('.all_categories')
|
||||
.submit
|
||||
.search-button
|
||||
= button_tag t(".search"), class: "secondary icon-search"
|
||||
11
app/views/admin/products_v3/_no_products.html.haml
Normal file
11
app/views/admin/products_v3/_no_products.html.haml
Normal file
@@ -0,0 +1,11 @@
|
||||
- if search_term.present? || producer_id.present? || category_id.present?
|
||||
= t('.no_products_found_for_search')
|
||||
%a{ href: "#", class: "button disruptive", data: { reflex: "click->products#clear_search" } }
|
||||
= t("admin.products_v3.sort.pagination.clear_search")
|
||||
- else
|
||||
= t('.no_products_found')
|
||||
#no-products-actions
|
||||
%a{ href: "/admin/products/new", class: "button icon-plus", icon: "icon-plus" }
|
||||
= t(:new_product)
|
||||
%a{ href: "/admin/products/import", class: "button icon-upload secondary", icon: "icon-upload" }
|
||||
= t(".import_products")
|
||||
9
app/views/admin/products_v3/_sort.html.haml
Normal file
9
app/views/admin/products_v3/_sort.html.haml
Normal file
@@ -0,0 +1,9 @@
|
||||
#sort
|
||||
%div
|
||||
= t(".pagination.total_html", total: pagy.count, from: pagy.from, to: pagy.to)
|
||||
- if search_term.present? || producer_id.present? || category_id.present?
|
||||
%a{ href: "#", class: "button disruptive medium", data: { reflex: "click->products#clear_search" } }
|
||||
= t(".pagination.clear_search")
|
||||
%div.with-dropdown
|
||||
= t(".pagination.per_page.show")
|
||||
= select_tag :per_page, options_for_select([15, 25, 50, 100].collect{|i| [t('.pagination.per_page.per_page', num: i), i]}, pagy.items), data: { reflex: "change->products#change_per_page" }
|
||||
@@ -8,19 +8,17 @@
|
||||
%col{ width:"10%" }
|
||||
%col{ width:"5%" }
|
||||
%col{ width:"5%", style: "max-width:5em" }
|
||||
%col{ width:"8%", style: "max-width:8em" }
|
||||
%thead
|
||||
%tr
|
||||
%th.align-left= t('admin.product.name')
|
||||
%th.align-right= t('admin.sku')
|
||||
%th.align-right= t('admin.unit')
|
||||
%th.align-right= t('admin.price')
|
||||
%th.align-right= t('admin.on_hand')
|
||||
%th.align-left= t('admin.producer')
|
||||
%th.align-left= t('admin.category')
|
||||
%th.align-left= t('admin.tax_category')
|
||||
%th.align-left= t('admin.inherits_properties')
|
||||
%th.align-right= t('admin.available_on')
|
||||
%th.align-left= t('admin.products_page.columns.name')
|
||||
%th.align-right= t('admin.products_page.columns.sku')
|
||||
%th.align-right= t('admin.products_page.columns.unit')
|
||||
%th.align-right= t('admin.products_page.columns.price')
|
||||
%th.align-right= t('admin.products_page.columns.on_hand')
|
||||
%th.align-left= t('admin.products_page.columns.producer')
|
||||
%th.align-left= t('admin.products_page.columns.category')
|
||||
%th.align-left= t('admin.products_page.columns.tax_category')
|
||||
%th.align-left= t('admin.products_page.columns.inherits_properties')
|
||||
- products.each do |product|
|
||||
%tbody.relaxed
|
||||
%tr
|
||||
@@ -46,8 +44,6 @@
|
||||
.line-clamp-1= product.tax_category&.name
|
||||
%td.align-left
|
||||
.line-clamp-1= product.inherits_properties ? 'YES' : 'NO' #TODO: consider using https://github.com/RST-J/human_attribute_values, else use I18n.t (also below)
|
||||
%td.align-right
|
||||
.line-clamp-1= product.available_on&.strftime('%F')
|
||||
- product.variants.each do |variant|
|
||||
%tr.condensed
|
||||
%td.align-left
|
||||
@@ -68,6 +64,4 @@
|
||||
.line-clamp-1= variant.tax_category&.name
|
||||
%td.align-left
|
||||
.line-clamp-1= variant.product.inherits_properties ? 'YES' : 'NO' # same as product
|
||||
%td.align-right
|
||||
.line-clamp-1= variant.available_on&.strftime('%F')
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
= render partial: 'spree/admin/shared/product_sub_menu'
|
||||
|
||||
#products_v3_page{"data-controller": "productsV3"}
|
||||
#loading-spinner.spinner-container{"data-productsV3-target": "loading"}
|
||||
#products_v3_page{ "data-controller": "products" }
|
||||
#loading-spinner.spinner-container{ "data-controller": "loading", "data-products-target": "loading" }
|
||||
.spinner
|
||||
= t('.loading')
|
||||
#products-content
|
||||
|
||||
21
app/views/admin/shared/v3/_pagy.html.haml
Normal file
21
app/views/admin/shared/v3/_pagy.html.haml
Normal file
@@ -0,0 +1,21 @@
|
||||
%nav.pagy_nav.pagination{"aria-label" => "pager", :role => "navigation"}
|
||||
- if pagy.prev
|
||||
%a.page.prev{ href: "#", id: "pagy-prev", "data-reflex": reflex, "data-perPage": pagy.items, "data-page": pagy.prev || 1, "aria-label": "previous"}
|
||||
%i.icon-chevron-left
|
||||
- else
|
||||
%span.page.prev.disabled
|
||||
%i.icon-chevron-left
|
||||
- pagy.series.each do |item| # series example: [1, :gap, 7, 8, "9", 10, 11, :gap, 36]
|
||||
- if item.is_a?(Integer) # page link
|
||||
%a.page{ href: "#", id:"pagy-#{item}", "data-reflex": reflex, "data-perPage": pagy.items, "data-page": item, "aria-label": "page #{item}"}
|
||||
= item
|
||||
- elsif item.is_a?(String) # current page
|
||||
%span.page.current= item
|
||||
- elsif item == :gap # page gap
|
||||
%span.page.gap …
|
||||
- if pagy.next
|
||||
%a.page.next{ href: "#", id:"pagy-next", "data-reflex": reflex, "data-perPage": pagy.items, "data-page": pagy.next || pagy.last, "aria-label": "next"}
|
||||
%i.icon-chevron-right
|
||||
- else
|
||||
%span.page.next.disabled
|
||||
%i.icon-chevron-right
|
||||
15
app/webpacker/controllers/loading_controller.js
Normal file
15
app/webpacker/controllers/loading_controller.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import ApplicationController from "./application_controller";
|
||||
|
||||
export default class extends ApplicationController {
|
||||
connect() {
|
||||
super.connect();
|
||||
}
|
||||
|
||||
hideLoading = () => {
|
||||
this.element.classList.add("hidden");
|
||||
};
|
||||
|
||||
showLoading = () => {
|
||||
this.element.classList.remove("hidden");
|
||||
};
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import ApplicationController from "./application_controller";
|
||||
|
||||
export default class extends ApplicationController {
|
||||
static targets = ["loading"];
|
||||
|
||||
connect() {
|
||||
super.connect();
|
||||
// Fetch the products on page load
|
||||
this.load();
|
||||
}
|
||||
|
||||
load = () => {
|
||||
this.showLoading();
|
||||
this.stimulate("Admin::ProductsV3#fetch").then(() => this.hideLoading());
|
||||
};
|
||||
|
||||
hideLoading = () => {
|
||||
this.loadingTarget.classList.add("hidden");
|
||||
};
|
||||
|
||||
showLoading = () => {
|
||||
this.loadingTarget.classList.remove("hidden");
|
||||
};
|
||||
}
|
||||
38
app/webpacker/controllers/products_controller.js
Normal file
38
app/webpacker/controllers/products_controller.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import ApplicationController from "./application_controller";
|
||||
|
||||
export default class extends ApplicationController {
|
||||
static targets = ["loading"];
|
||||
|
||||
connect() {
|
||||
super.connect();
|
||||
// Fetch the products on page load
|
||||
this.stimulate("Products#fetch");
|
||||
}
|
||||
|
||||
beforeReflex() {
|
||||
this.showLoading();
|
||||
}
|
||||
|
||||
afterReflex() {
|
||||
this.hideLoading();
|
||||
}
|
||||
|
||||
showLoading = () => {
|
||||
if (this.getLoadingController()) {
|
||||
this.getLoadingController().showLoading();
|
||||
}
|
||||
};
|
||||
|
||||
hideLoading = () => {
|
||||
if (this.getLoadingController()) {
|
||||
this.getLoadingController().hideLoading();
|
||||
}
|
||||
};
|
||||
|
||||
getLoadingController = () => {
|
||||
return (this.loadongController = this.application.getControllerForElementAndIdentifier(
|
||||
this.loadingTarget,
|
||||
"loading"
|
||||
));
|
||||
};
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
// Customisations for the new Bulk Edit Products page only
|
||||
.products_v3_page {
|
||||
#content .container {
|
||||
#content > .row:first-child > .container:first-child {
|
||||
// Allow table to extend to full width of available screen space
|
||||
// TODO: move this to a generic rule, eg body.full-width{}. Then it can be included on any page.
|
||||
// or even better, create a switch that allows you to yield the page content without the surrounding content class. then you still have control to add the .content div where needed.
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
#products-content > .container:first-child {
|
||||
position: static;
|
||||
}
|
||||
|
||||
// Hopefully these rules will be moved to component(s).
|
||||
table.products {
|
||||
table-layout: fixed; // Column widths are based solely on col definitions (not content). This allows more efficient rendering.
|
||||
@@ -78,4 +82,98 @@
|
||||
gap: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
#sort,
|
||||
#filters {
|
||||
margin-bottom: 1em;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#sort {
|
||||
line-height: $btn-medium-height;
|
||||
height: $btn-medium-height;
|
||||
|
||||
.with-dropdown {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
#filters {
|
||||
gap: 20px;
|
||||
align-items: flex-end;
|
||||
|
||||
.producers,
|
||||
.categories {
|
||||
> .label {
|
||||
margin-left: 3px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.query {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.producers,
|
||||
.categories {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.submit {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.query {
|
||||
.search-input {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
background-color: $lighter-grey;
|
||||
border: 1px solid $lighter-grey;
|
||||
border-radius: 4px;
|
||||
height: $btn-height;
|
||||
line-height: $btn-height;
|
||||
|
||||
&:has(input:focus),
|
||||
&:has(input:active) {
|
||||
border: 1px solid $dark-blue;
|
||||
}
|
||||
|
||||
> input {
|
||||
background-color: $lighter-grey;
|
||||
}
|
||||
|
||||
&:before {
|
||||
font-family: FontAwesome;
|
||||
content: "\f002";
|
||||
color: $near-black;
|
||||
font-size: 16px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.producers,
|
||||
.categories {
|
||||
select {
|
||||
width: 150px;
|
||||
height: $btn-height;
|
||||
}
|
||||
}
|
||||
|
||||
.submit {
|
||||
text-align: right;
|
||||
|
||||
.search-button {
|
||||
position: relative;
|
||||
> input {
|
||||
padding-left: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,5 +16,3 @@ $admin-table-border: $pale-blue;
|
||||
$modal-close-button-color: #de6060;
|
||||
$modal-close-button-hover-color: #bf4545;
|
||||
$disabled-button: $light-grey;
|
||||
|
||||
$border-radius: 3px;
|
||||
|
||||
@@ -22,15 +22,16 @@
|
||||
@import "globals/variables"; // admin_v3
|
||||
@import "../admin/variables";
|
||||
@import "../admin/globals/mixins";
|
||||
@import "mixins"; // admin_v3
|
||||
|
||||
@import "../admin/plugins/font-awesome";
|
||||
|
||||
@import "../shared/variables/layout";
|
||||
@import "../shared/variables/variables";
|
||||
@import "../shared/utilities";
|
||||
@import "../admin/shared/typography";
|
||||
@import "shared/typography"; // admin_v3
|
||||
@import "shared/tables"; // admin_v3
|
||||
@import "../admin/shared/icons";
|
||||
@import "shared/icons"; // admin_v3
|
||||
@import "../admin/shared/forms";
|
||||
@import "shared/layout"; // admin_v3
|
||||
@import "../admin/shared/scroll_bar";
|
||||
@@ -50,7 +51,7 @@
|
||||
@import "../admin/components/actions";
|
||||
@import "../admin/components/alert-box";
|
||||
@import "../admin/components/alert_row";
|
||||
@import "../admin/components/buttons";
|
||||
@import "components/buttons"; // admin_v3
|
||||
@import "../admin/components/date-picker";
|
||||
@import "../admin/components/dialogs";
|
||||
@import "../admin/components/input";
|
||||
|
||||
108
app/webpacker/css/admin_v3/components/buttons.scss
Normal file
108
app/webpacker/css/admin_v3/components/buttons.scss
Normal file
@@ -0,0 +1,108 @@
|
||||
input[type="submit"],
|
||||
input[type="button"]:not(.trix-button),
|
||||
button:not(.plain):not(.trix-button),
|
||||
.button {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
@include border-radius($border-radius);
|
||||
display: inline-block;
|
||||
padding: 0px 12px;
|
||||
background-color: $color-btn-bg;
|
||||
border: 1px solid $color-btn-bg;
|
||||
color: $color-btn-text;
|
||||
text-transform: uppercase;
|
||||
line-height: 40px;
|
||||
height: 40px;
|
||||
font-weight: bold;
|
||||
|
||||
&:before {
|
||||
font-weight: normal !important;
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
outline: none;
|
||||
border: 1px solid $color-btn-hover-border;
|
||||
}
|
||||
|
||||
&:active:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $color-btn-hover-bg;
|
||||
border: 1px solid $color-btn-hover-bg;
|
||||
color: $color-btn-hover-text;
|
||||
}
|
||||
|
||||
&.fullwidth {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
background-color: transparent;
|
||||
border: 1px solid $color-btn-bg;
|
||||
color: $color-btn-bg;
|
||||
|
||||
&:hover {
|
||||
background-color: $color-11;
|
||||
border: 1px solid $color-10;
|
||||
color: $color-10;
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
background-color: $color-11;
|
||||
border: 1px solid $color-4;
|
||||
color: $color-4;
|
||||
}
|
||||
}
|
||||
|
||||
&.disruptive {
|
||||
background-color: transparent;
|
||||
border: 1px solid $color-5;
|
||||
color: $color-5;
|
||||
|
||||
&:hover {
|
||||
background-color: $fair-pink;
|
||||
border: 1px solid $color-5;
|
||||
color: $color-5;
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
background-color: $fair-pink;
|
||||
border: 1px solid $roof-terracotta;
|
||||
color: $roof-terracotta;
|
||||
}
|
||||
}
|
||||
|
||||
&.medium {
|
||||
line-height: $btn-medium-height;
|
||||
height: $btn-medium-height;
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
transform: translateY(-50%);
|
||||
font-size: 10px;
|
||||
text-transform: capitalize;
|
||||
padding: 0px 5px;
|
||||
border-radius: 3px;
|
||||
|
||||
&:before {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
background-color: $warning-red;
|
||||
}
|
||||
&.success {
|
||||
background-color: $spree-green;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,46 +61,9 @@ nav.menu {
|
||||
}
|
||||
|
||||
#admin-menu {
|
||||
box-shadow: 0px 1px 0px rgba(0, 0, 0, 0.05), 0px 2px 2px rgba(0, 0, 0, 0.07);
|
||||
|
||||
ul {
|
||||
display: flex;
|
||||
}
|
||||
@include defaultBoxShadow;
|
||||
|
||||
li {
|
||||
min-width: 90px;
|
||||
flex-grow: 1;
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
padding: 25px 5px;
|
||||
color: $dark-grey !important;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
|
||||
i {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $red !important;
|
||||
border-bottom: 2px solid $red;
|
||||
}
|
||||
|
||||
span.text {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
a::before {
|
||||
font-weight: normal;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
width: 300px;
|
||||
background-color: $teal;
|
||||
@@ -115,31 +78,71 @@ nav.menu {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.selected a {
|
||||
@extend a, :hover;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#sub-menu {
|
||||
padding-bottom: 0;
|
||||
box-shadow: 0px 1px 0px $light-grey;
|
||||
box-shadow: 0px 1px 0px $color-7;
|
||||
}
|
||||
|
||||
li {
|
||||
a {
|
||||
display: block;
|
||||
padding: 12px 20px;
|
||||
color: $dark-grey;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
font-size: 14px;
|
||||
// Factorized rules on menu item for admin menu and sub menu
|
||||
#admin-menu,
|
||||
#sub-menu {
|
||||
.container {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
ul {
|
||||
display: flex;
|
||||
|
||||
li {
|
||||
a {
|
||||
display: inline-block;
|
||||
padding: 16px 20px;
|
||||
color: $color-9 !important;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
|
||||
&:hover {
|
||||
color: $red !important;
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
height: 3px;
|
||||
background: $red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.selected a {
|
||||
@extend a, :hover;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.selected a,
|
||||
a:hover {
|
||||
color: $red;
|
||||
border-bottom: 2px solid $red;
|
||||
// Specific rules on menu item for admin menu and sub menu
|
||||
#admin-menu {
|
||||
ul {
|
||||
justify-content: space-between;
|
||||
li a {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#sub-menu {
|
||||
ul li a:hover {
|
||||
&:after {
|
||||
height: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -148,6 +151,11 @@ nav.menu {
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
|
||||
#header .container {
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
#login-nav {
|
||||
line-height: 1.75em;
|
||||
}
|
||||
|
||||
@@ -1,21 +1,37 @@
|
||||
.pagination {
|
||||
text-align: center;
|
||||
margin: 2em 0 1em;
|
||||
margin: 0 0 1em;
|
||||
padding: 10px 0;
|
||||
|
||||
background-color: $light-grey;
|
||||
background-color: $color-7;
|
||||
|
||||
.page {
|
||||
padding: 5px 8px;
|
||||
width: 40px;
|
||||
line-height: 40px;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
background-color: $color-1;
|
||||
@include defaultBoxShadow;
|
||||
border-radius: 4px;
|
||||
color: $color-9;
|
||||
|
||||
&.current {
|
||||
background-color: $green;
|
||||
border-radius: 3px;
|
||||
background-color: $color-5;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&.prev {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
&.next {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
gap: 40px;
|
||||
font-size: 24px;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
z-index: 2;
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
|
||||
@@ -2,12 +2,17 @@
|
||||
$white: #ffffff !default; // White
|
||||
$green: #9fc820 !default; // Green
|
||||
$teal: #008397 !default; // Teal (Allports)
|
||||
$orient: #006878 !default; // Orient (Cerulean)
|
||||
$dark-blue: #004e5b !default; // Dark Blue (Sherpa)
|
||||
$red: #c85136 !default; // Red/Orange (Mojo)
|
||||
$yellow: #ff9300 !default; // Yellow
|
||||
$mystic: #d9e8eb !default; // Mystic
|
||||
$lighter-grey: #f8f9fa !default; // Lighter grey
|
||||
$light-grey: #eff1f2 !default; // Light grey
|
||||
$near-black: #191c1d !default; // Near-black
|
||||
$dark-grey: #2e3132 !default; // Dark Grey
|
||||
$fair-pink: #ffefeb !default; // Fair Pink
|
||||
$roof-terracotta: #b83b1f !default; // Roof Terracotta
|
||||
|
||||
// Old colour variables for backwards compatibility
|
||||
$color-1: $white;
|
||||
@@ -19,3 +24,7 @@ $color-6: $yellow;
|
||||
$color-7: $light-grey;
|
||||
$color-8: $near-black;
|
||||
$color-9: $dark-grey;
|
||||
$color-10: $orient;
|
||||
$color-11: $mystic;
|
||||
$color-12: $fair-pink;
|
||||
$color-13: $roof-terracotta;
|
||||
|
||||
@@ -39,8 +39,9 @@ $padding-tbl-cell-relaxed: 16px 12px;
|
||||
$color-btn-bg: $teal !default;
|
||||
$color-btn-text: $white !default;
|
||||
$color-btn-shadow: 0px 1px 0px rgba(0, 0, 0, 0.05), 0px 2px 2px rgba(0, 0, 0, 0.07) !default;
|
||||
$color-btn-hover-bg: lighten($color-btn-bg, 2) !default;
|
||||
$color-btn-hover-bg: $orient !default;
|
||||
$color-btn-hover-text: $white !default;
|
||||
$color-btn-hover-border: $dark-blue !default;
|
||||
|
||||
// Actions colors
|
||||
$color-action-edit-bg: very-light($color-success, 5 ) !default;
|
||||
@@ -142,7 +143,10 @@ $h3-size: $h4-size + 2 !default;
|
||||
$h2-size: $h3-size + 2 !default;
|
||||
$h1-size: $h2-size + 2 !default;
|
||||
|
||||
$border-radius: 3px !default;
|
||||
$border-radius: 4px !default;
|
||||
|
||||
$font-weight-bold: 600 !default;
|
||||
$font-weight-normal: 400 !default;
|
||||
|
||||
$btn-height: 40px !default;
|
||||
$btn-medium-height: 32px !default;
|
||||
|
||||
3
app/webpacker/css/admin_v3/mixins.scss
Normal file
3
app/webpacker/css/admin_v3/mixins.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
@mixin defaultBoxShadow {
|
||||
box-shadow: 0px 1px 0px rgba(0, 0, 0, 0.05), 0px 2px 2px rgba(0, 0, 0, 0.07);
|
||||
}
|
||||
42
app/webpacker/css/admin_v3/shared/icons.scss
Normal file
42
app/webpacker/css/admin_v3/shared/icons.scss
Normal file
@@ -0,0 +1,42 @@
|
||||
// Some fixes for fontwesome stylesheets
|
||||
[class*="icon-"] {
|
||||
&:before {
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
&.button,
|
||||
&.icon_link {
|
||||
&:before {
|
||||
padding-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// for the button tagname as well
|
||||
button[class*="icon-"] {
|
||||
&:before {
|
||||
padding-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-email:before {
|
||||
@extend .icon-envelope, :before;
|
||||
}
|
||||
.icon-resend_authorization_email:before {
|
||||
@extend .icon-envelope, :before;
|
||||
}
|
||||
.icon-resume:before {
|
||||
@extend .icon-refresh, :before;
|
||||
}
|
||||
|
||||
.icon-cancel:before,
|
||||
.icon-void:before {
|
||||
@extend .icon-remove, :before;
|
||||
}
|
||||
|
||||
.icon-capture {
|
||||
@extend .icon-ok;
|
||||
}
|
||||
.icon-credit:before {
|
||||
@extend .icon-ok, :before;
|
||||
}
|
||||
@@ -205,3 +205,7 @@ table {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
table + .pagination {
|
||||
margin-top: -18px;
|
||||
}
|
||||
|
||||
226
app/webpacker/css/admin_v3/shared/typography.scss
Normal file
226
app/webpacker/css/admin_v3/shared/typography.scss
Normal file
@@ -0,0 +1,226 @@
|
||||
// Base
|
||||
//--------------------------------------------------------------
|
||||
body,
|
||||
div,
|
||||
dl,
|
||||
dt,
|
||||
dd,
|
||||
ul,
|
||||
ol,
|
||||
li,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
pre,
|
||||
form,
|
||||
p,
|
||||
blockquote,
|
||||
th,
|
||||
td {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: $body-font-size;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: $base-font-family;
|
||||
font-size: $body-font-size;
|
||||
font-weight: 400;
|
||||
color: $color-body-text;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-top: 1px solid $color-border;
|
||||
border-bottom: 1px solid white;
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
strong,
|
||||
b {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
// links
|
||||
//--------------------------------------------------------------
|
||||
a:not(.button) {
|
||||
color: $color-link;
|
||||
text-decoration: none;
|
||||
line-height: inherit;
|
||||
|
||||
&,
|
||||
&:hover,
|
||||
&:active,
|
||||
&:visited,
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:visited {
|
||||
color: $color-link-visited;
|
||||
}
|
||||
&:focus {
|
||||
color: $color-link-focus;
|
||||
}
|
||||
&:active {
|
||||
color: $color-link-active;
|
||||
}
|
||||
&:hover {
|
||||
color: $color-link-hover;
|
||||
}
|
||||
}
|
||||
|
||||
// Headings
|
||||
//--------------------------------------------------------------
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: 600;
|
||||
color: $color-headers;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: $h1-size;
|
||||
line-height: $h1-size + 6;
|
||||
}
|
||||
h2 {
|
||||
font-size: $h2-size;
|
||||
line-height: $h1-size + 4;
|
||||
}
|
||||
h3 {
|
||||
font-size: $h3-size;
|
||||
line-height: $h1-size + 2;
|
||||
}
|
||||
h4 {
|
||||
font-size: $h4-size;
|
||||
line-height: $h1-size;
|
||||
}
|
||||
h5 {
|
||||
font-size: $h5-size;
|
||||
line-height: $h1-size;
|
||||
}
|
||||
h6 {
|
||||
font-size: $h6-size;
|
||||
line-height: $h1-size;
|
||||
}
|
||||
|
||||
// Lists
|
||||
//--------------------------------------------------------------
|
||||
ul {
|
||||
&.inline-menu {
|
||||
li {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
&.fields {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
dl {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
margin: 5px 0;
|
||||
color: lighten($color-body-text, 15);
|
||||
|
||||
dt,
|
||||
dd {
|
||||
float: left;
|
||||
line-height: 16px;
|
||||
padding: 5px;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
dt {
|
||||
width: 40%;
|
||||
font-weight: 600;
|
||||
padding-left: 0;
|
||||
text-transform: uppercase;
|
||||
font-size: 85%;
|
||||
}
|
||||
|
||||
dd {
|
||||
width: 60%;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
dd:after {
|
||||
content: "";
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
|
||||
// Helpers
|
||||
.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
.align-left {
|
||||
text-align: left;
|
||||
}
|
||||
.align-justify {
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.uppercase {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.green {
|
||||
color: $color-2;
|
||||
}
|
||||
.blue {
|
||||
color: $color-3;
|
||||
}
|
||||
.red {
|
||||
color: $color-5;
|
||||
}
|
||||
.yellow {
|
||||
color: $color-6;
|
||||
}
|
||||
|
||||
.no-objects-found {
|
||||
text-align: center;
|
||||
font-size: 120%;
|
||||
text-transform: uppercase;
|
||||
padding: 40px 0px;
|
||||
color: lighten($color-body-text, 15);
|
||||
}
|
||||
|
||||
.text-normal {
|
||||
font-size: 1rem;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.text-big {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.text-red {
|
||||
color: $warning-red;
|
||||
}
|
||||
|
||||
input.text-big {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.pad-top {
|
||||
padding-top: 1em;
|
||||
}
|
||||
|
||||
.white-space-nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -772,9 +772,27 @@ en:
|
||||
header:
|
||||
title: Bulk Edit Products
|
||||
loading: Loading your products
|
||||
sort:
|
||||
pagination:
|
||||
total_html: "<strong>%{total} products</strong> in your catalogue. Showing %{from} to %{to}."
|
||||
per_page:
|
||||
show: Show
|
||||
per_page: "%{num} per page"
|
||||
clear_search: Clear search
|
||||
filters:
|
||||
search_products: Search for products
|
||||
all_producers: All producers
|
||||
all_categories: All categories
|
||||
producers:
|
||||
label: Producers
|
||||
categories:
|
||||
label: Categories
|
||||
search: Search
|
||||
content:
|
||||
no_products:
|
||||
no_products_found: No products found
|
||||
import_products: Import multiple products
|
||||
no_products_found_for_search: No products found for your search criteria
|
||||
product_import:
|
||||
title: Product Import
|
||||
file_not_found: File not found or could not be opened
|
||||
@@ -4248,7 +4266,6 @@ See the %{link} to find out more about %{sitename}'s features and to start using
|
||||
category: Category
|
||||
tax_category: Tax Category
|
||||
inherits_properties?: Inherits Properties?
|
||||
available_on: Available On
|
||||
av_on: "Av. On"
|
||||
import_date: "Import Date"
|
||||
products_variant:
|
||||
|
||||
161
spec/system/admin/products_v3/products_spec.rb
Normal file
161
spec/system/admin/products_v3/products_spec.rb
Normal file
@@ -0,0 +1,161 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "system_helper"
|
||||
|
||||
describe 'As an admin, I can see the new product page' do
|
||||
include WebHelper
|
||||
include AuthenticationHelper
|
||||
include FileHelper
|
||||
|
||||
# create lot of products
|
||||
70.times do |i|
|
||||
let!("product_#{i}".to_sym) { create(:simple_product, name: "product #{i}") }
|
||||
end
|
||||
# create a product with a name that can be searched
|
||||
let!(:product_by_name) { create(:simple_product, name: "searchable product") }
|
||||
# create a product with a supplier that can be searched
|
||||
let!(:producer) { create(:supplier_enterprise, name: "Producer 1") }
|
||||
let!(:product_by_supplier) { create(:simple_product, supplier: producer) }
|
||||
# create a product with a category that can be searched
|
||||
let!(:product_by_category) {
|
||||
create(:simple_product, primary_taxon: create(:taxon, name: "Category 1"))
|
||||
}
|
||||
|
||||
before do
|
||||
# activate feature toggle admin_style_v3 to use new admin interface
|
||||
Flipper.enable(:admin_style_v3)
|
||||
login_as_admin
|
||||
end
|
||||
|
||||
it "can see the new product page" do
|
||||
visit "/admin/products_v3"
|
||||
expect(page).to have_content "Bulk Edit Products"
|
||||
end
|
||||
|
||||
context "pagination" do
|
||||
before :each do
|
||||
visit "/admin/products_v3"
|
||||
end
|
||||
|
||||
it "has a pagination, has 15 products per page by default and can change the page" do
|
||||
expect(page).to have_selector ".pagination"
|
||||
expect_products_count_to_be 15
|
||||
within ".pagination" do
|
||||
click_link "2"
|
||||
end
|
||||
expect_page_to_be 2
|
||||
expect_per_page_to_be 15
|
||||
expect_products_count_to_be 15
|
||||
end
|
||||
|
||||
it "can change the number of products per page" do
|
||||
select "50", from: "per_page"
|
||||
expect_page_to_be 1
|
||||
expect_per_page_to_be 50
|
||||
expect_products_count_to_be 50
|
||||
end
|
||||
end
|
||||
|
||||
context "search" do
|
||||
before :each do
|
||||
visit "/admin/products_v3"
|
||||
end
|
||||
|
||||
context "search by search term" do
|
||||
it "can search for a product" do
|
||||
search_for "searchable product"
|
||||
|
||||
expect(page).to have_field "search_term", with: "searchable product"
|
||||
expect_page_to_be 1
|
||||
expect_products_count_to_be 1
|
||||
end
|
||||
|
||||
it "reset the page when searching" do
|
||||
within ".pagination" do
|
||||
click_link "2"
|
||||
end
|
||||
expect_page_to_be 2
|
||||
expect_per_page_to_be 15
|
||||
expect_products_count_to_be 15
|
||||
search_for "searchable product"
|
||||
expect_page_to_be 1
|
||||
expect_products_count_to_be 1
|
||||
end
|
||||
end
|
||||
|
||||
context "search by producer" do
|
||||
it "has a producer select" do
|
||||
expect(page).to have_selector "select#producer_id"
|
||||
end
|
||||
|
||||
it "can search for a product" do
|
||||
search_by_producer "Producer 1"
|
||||
|
||||
expect(page).to have_select "producer_id", selected: "Producer 1"
|
||||
expect_page_to_be 1
|
||||
expect_products_count_to_be 1
|
||||
end
|
||||
end
|
||||
|
||||
context "search by category" do
|
||||
it "can search for a product" do
|
||||
search_by_category "Category 1"
|
||||
|
||||
expect(page).to have_select "category_id", selected: "Category 1"
|
||||
expect_page_to_be 1
|
||||
expect_products_count_to_be 1
|
||||
expect(page).to have_selector "table.products tbody tr td", text: product_by_category.name
|
||||
end
|
||||
end
|
||||
|
||||
context "clear filters" do
|
||||
it "can clear filters" do
|
||||
search_for "searchable product"
|
||||
expect(page).to have_field "search_term", with: "searchable product"
|
||||
expect_page_to_be 1
|
||||
expect_products_count_to_be 1
|
||||
expect(page).to have_selector "table.products tbody tr td", text: product_by_name.name
|
||||
|
||||
click_link "Clear search"
|
||||
expect(page).to have_field "search_term", with: ""
|
||||
expect_page_to_be 1
|
||||
expect_products_count_to_be 15
|
||||
end
|
||||
end
|
||||
|
||||
context "no results" do
|
||||
it "shows a message when there are no results" do
|
||||
search_for "no results"
|
||||
expect(page).to have_content "No products found for your search criteria"
|
||||
expect(page).to have_link "Clear search"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def expect_page_to_be(page_number)
|
||||
expect(page).to have_selector ".pagination span.page.current", text: page_number.to_s
|
||||
end
|
||||
|
||||
def expect_per_page_to_be(per_page)
|
||||
expect(page).to have_selector "#per_page", text: per_page.to_s
|
||||
end
|
||||
|
||||
def expect_products_count_to_be(count)
|
||||
expect(page).to have_selector("table.products tbody", count:)
|
||||
end
|
||||
|
||||
def search_for(term)
|
||||
fill_in "search_term", with: term
|
||||
click_button "Search"
|
||||
end
|
||||
|
||||
def search_by_producer(producer)
|
||||
select producer, from: "producer_id"
|
||||
click_button "Search"
|
||||
end
|
||||
|
||||
def search_by_category(category)
|
||||
select category, from: "category_id"
|
||||
click_button "Search"
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user