Merge pull request #11382 from jibees/11273-buu-remove-new_products_page-feature-toggle

🔧 Remove `new_products_page` and all associated controllers/components/...
This commit is contained in:
Rachel Arnould
2023-08-11 11:43:49 +02:00
committed by GitHub
32 changed files with 0 additions and 820 deletions

View File

@@ -188,7 +188,6 @@ Metrics/BlockNesting:
# Configuration parameters: CountComments, Max, CountAsOne.
Metrics/ClassLength:
Exclude:
- 'app/components/products_table_component.rb'
- 'app/controllers/admin/enterprises_controller.rb'
- 'app/controllers/admin/order_cycles_controller.rb'
- 'app/controllers/admin/product_import_controller.rb'
@@ -1002,7 +1001,6 @@ Style/GuardClause:
# SupportedStyles: braces, no_braces
Style/HashAsLastArrayItem:
Exclude:
- 'app/components/products_table_component.rb'
# Offense count: 12
# This cop supports unsafe autocorrection (--autocorrect-all).
@@ -1035,8 +1033,6 @@ Style/HashSyntax:
Exclude:
- 'app/components/confirm_modal_component.rb'
- 'app/components/help_modal_component.rb'
- 'app/components/products_table_component.rb'
- 'app/components/selector_with_filter_component.rb'
- 'app/controllers/admin/customers_controller.rb'
- 'app/controllers/admin/enterprise_fees_controller.rb'
- 'app/controllers/admin/enterprise_groups_controller.rb'
@@ -1178,7 +1174,6 @@ Style/HashSyntax:
- 'lib/tasks/sample_data/user_factory.rb'
- 'lib/tasks/subscriptions/debug.rake'
- 'lib/tasks/subscriptions/test.rake'
- 'spec/components/product_component_spec.rb'
- 'spec/constraints/feature_toggle_constraint_spec.rb'
- 'spec/controllers/admin/bulk_line_items_controller_spec.rb'
- 'spec/controllers/admin/customers_controller_spec.rb'

View File

@@ -1,15 +0,0 @@
# frozen_string_literal: true
class PaginationComponent < ViewComponentReflex::Component
def initialize(pagy:, data:)
super
@count = pagy.count
@page = pagy.page
@per_page = pagy.items
@pages = pagy.pages
@next = pagy.next
@prev = pagy.prev
@data = data
@series = pagy.series
end
end

View File

@@ -1,16 +0,0 @@
= component_controller do
%nav{"aria-label": "pagination"}
.pagination
.pagination-prev{data: @prev.nil? ? nil : @data, "data-page": @prev, class: "#{'inactive' if @prev.nil?}"}
= I18n.t "components.pagination.previous"
.pagination-pages
- @series.each do |page|
- if page == :gap
.pagination-gap
- else
.pagination-page{data: @data, "data-page": page, class: "#{'active' if page.to_i == @page}"}
= page
.pagination-next{data: @next.nil? ? nil : @data, "data-page": @next, class: "#{'inactive' if @next.nil?}"}
= I18n.t "components.pagination.next"

View File

@@ -1,69 +0,0 @@
nav {
.pagination {
display: flex;
justify-content: space-between;
align-items: flex-start;
font-size: 14px;
.pagination-prev, .pagination-next {
cursor: pointer;
&:after, &:before {
font-size: 2em;
position: relative;
top: 3px;
}
&.inactive {
cursor: default;
color: $dark-grey;
}
}
.pagination-prev {
margin-left: 10px;
&:before {
content: "";
margin-left: 10px;
margin-right: 10px;
}
}
.pagination-next {
margin-right: 10px;
&:after {
content: "";
margin-left: 10px;
margin-right: 10px;
}
}
.pagination-pages {
display: flex;
align-items: flex-end;
.pagination-gap, .pagination-page {
padding: 0 0.5rem;
margin-left: 10px;
margin-right: 10px;
}
.pagination-gap {
color: $dark-grey;
}
.pagination-page {
color: $color-4;
cursor: pointer;
&.active {
border-top: 3px solid $spree-blue;
color: $spree-blue;
cursor: default;
}
}
}
}
}

View File

@@ -1,57 +0,0 @@
# frozen_string_literal: true
class ProductComponent < ViewComponentReflex::Component
DATETIME_FORMAT = '%F %T'
def initialize(product:, columns:)
super
@product = product
@image = @product.image if product.image.present?
@columns = columns.map do |c|
{
id: c[:value],
value: column_value(c[:value])
}
end
end
# This must be define when using ProductComponent.with_collection()
def collection_key
@product.id
end
# rubocop:disable Metrics/CyclomaticComplexity
def column_value(column)
case column
when 'name'
@product.name
when 'price'
@product.price
when 'unit'
"#{@product.variants.first.unit_value} #{@product.variant_unit}"
when 'producer'
@product.supplier.name
when 'category'
@product.taxons.map(&:name).join(', ')
when 'sku'
@product.sku
when 'on_hand'
@product.on_hand || 0
when 'on_demand'
@product.on_demand
when 'tax_category'
@product.variant.tax_category.name
when 'inherits_properties'
@product.inherits_properties
when 'import_date'
format_date(@product.import_date)
end
end
# rubocop:enable Metrics/CyclomaticComplexity
private
def format_date(date)
date&.strftime(DATETIME_FORMAT) || ''
end
end

View File

@@ -1,6 +0,0 @@
%tr
- @columns.each do |column|
%td.products_column{class: column[:id]}
- if column[:id] == "name" && @image&.attachment.present?
= image_tag @image.url(:mini)
= column[:value]

View File

@@ -1,179 +0,0 @@
# frozen_string_literal: true
class ProductsTableComponent < ViewComponentReflex::Component
include Pagy::Backend
SORTABLE_COLUMNS = ['name', 'import_date'].freeze
SELECTABLE_COLUMNS = [
{ label: I18n.t("admin.products_page.columns_selector.price"), value: "price" },
{ label: I18n.t("admin.products_page.columns_selector.unit"), value: "unit" },
{ label: I18n.t("admin.products_page.columns_selector.producer"), value: "producer" },
{ label: I18n.t("admin.products_page.columns_selector.category"), value: "category" },
{ label: I18n.t("admin.products_page.columns_selector.sku"), value: "sku" },
{ label: I18n.t("admin.products_page.columns_selector.on_hand"), value: "on_hand" },
{ label: I18n.t("admin.products_page.columns_selector.on_demand"), value: "on_demand" },
{ label: I18n.t("admin.products_page.columns_selector.tax_category"), value: "tax_category" },
{
label: I18n.t("admin.products_page.columns_selector.inherits_properties"),
value: "inherits_properties"
},
{ label: I18n.t("admin.products_page.columns_selector.import_date"), value: "import_date" }
].sort do |a, b|
a[:label] <=> b[:label]
end.freeze
PER_PAGE_VALUE = [10, 25, 50, 100].freeze
PER_PAGE = PER_PAGE_VALUE.map { |value| { label: value, value: value } }
NAME_COLUMN = {
label: I18n.t("admin.products_page.columns.name"), value: "name", sortable: true
}.freeze
def initialize(user:)
super
@user = user
@selectable_columns = SELECTABLE_COLUMNS
@columns_selected = ['unit', 'price', 'on_hand', 'category', 'import_date']
@per_page = PER_PAGE
@per_page_selected = [10]
@categories = [{ label: "All", value: "all" }] +
Spree::Taxon.order(:name)
.map { |taxon| { label: taxon.name, value: taxon.id.to_s } }
@categories_selected = ["all"]
@producers = [{ label: "All", value: "all" }] +
OpenFoodNetwork::Permissions.new(@user)
.managed_product_enterprises.is_primary_producer.by_name
.map { |producer| { label: producer.name, value: producer.id.to_s } }
@producers_selected = ["all"]
@page = 1
@sort = { column: "name", direction: "asc" }
@search_term = ""
end
# any change on a "reflex_data_attributes" (defined in the template) will trigger a re render
def before_render
fetch_products
refresh_columns
end
# Element refers to the component the data is set on
def search_term
# Element is SearchInputComponent
@search_term = element.dataset['value']
end
def toggle_column
# Element is SelectorComponent
column = element.dataset['value']
@columns_selected = if @columns_selected.include?(column)
@columns_selected - [column]
else
@columns_selected + [column]
end
end
def click_sort
# Element is TableHeaderComponent
@sort = {
column: element.dataset['sort-value'],
direction: element.dataset['sort-direction'] == "asc" ? "desc" : "asc"
}
end
def toggle_per_page
# Element is SelectorComponent
selected = element.dataset['value'].to_i
@per_page_selected = [selected] if PER_PAGE_VALUE.include?(selected)
end
def toggle_category
# Element is SelectorWithFilterComponent
category_clicked = element.dataset['value']
@categories_selected = toggle_selector_with_filter(category_clicked, @categories_selected)
end
def toggle_producer
# Element is SelectorWithFilterComponent
producer_clicked = element.dataset['value']
@producers_selected = toggle_selector_with_filter(producer_clicked, @producers_selected)
end
def change_page
# Element is PaginationComponent
page = element.dataset['page'].to_i
@page = page if page > 0
end
private
def refresh_columns
@columns = @columns_selected.map do |column|
{
label: I18n.t("admin.products_page.columns.#{column}"),
value: column,
sortable: SORTABLE_COLUMNS.include?(column)
}
end.sort! { |a, b| a[:label] <=> b[:label] }
@columns.unshift(NAME_COLUMN)
end
def toggle_selector_with_filter(clicked, selected)
selected = if selected.include?(clicked)
selected - [clicked]
else
selected + [clicked]
end
if clicked == "all" || selected.empty?
selected = ["all"]
elsif selected.include?("all") && selected.length > 1
selected -= ["all"]
end
selected
end
def fetch_products
product_query = OpenFoodNetwork::Permissions.new(@user).editable_products.merge(product_scope)
@products = product_query.ransack(ransack_query).result
@pagy, @products = pagy(@products, items: @per_page_selected.first, page: @page)
end
def product_scope
scope = if @user.has_spree_role?("admin") || @user.enterprises.present?
Spree::Product
else
Spree::Product.active
end
scope.includes(product_query_includes)
end
def ransack_query
query = { s: "#{@sort[:column]} #{@sort[:direction]}" }
query = if @producers_selected.include?("all")
query.merge({ supplier_id_eq: "" })
else
query.merge({ supplier_id_in: @producers_selected })
end
query = query.merge({ name_cont: @search_term }) if @search_term.present?
if @categories_selected.include?("all")
query.merge({ primary_taxon_id_eq: "" })
else
query.merge({ primary_taxon_id_in: @categories_selected })
end
end
def product_query_includes
[
:image,
variants: [
:default_price,
:stock_locations,
:stock_items,
:variant_overrides
]
]
end
end

View File

@@ -1,21 +0,0 @@
= component_controller(class: "products-table") do
.products-table-form
.products-table-form_filter_results
= render(SearchInputComponent.new(value: @search_term, data: reflex_data_attributes(:search_term)))
.products-table-form_categories_selector
= render(SelectorWithFilterComponent.new(title: t("admin.products_page.filters.categories.title"), selected: @categories_selected, items: @categories, data: reflex_data_attributes(:toggle_category), selected_items_i18n_key: "admin.products_page.filters.categories.selected_categories"))
.products-table-form_producers_selector
= render(SelectorWithFilterComponent.new(title: t("admin.products_page.filters.producers.title"), selected: @producers_selected, items: @producers, data: reflex_data_attributes(:toggle_producer), selected_items_i18n_key: "admin.products_page.filters.producers.selected_producers"))
.products-table-form_per-page_selector
= render(SelectorComponent.new(title: t('admin.products_page.filters.per_page', count: @per_page_selected[0]), selected: @per_page_selected, items: @per_page, data: reflex_data_attributes(:toggle_per_page)))
.products-table-form_columns_selector
= render(SelectorComponent.new(title: t("admin.products_page.filters.columns"), selected: @columns_selected, items: @selectable_columns, data: reflex_data_attributes(:toggle_column)))
.products-table_table
%table
= render(TableHeaderComponent.new(columns: @columns, sort: @sort, data: reflex_data_attributes(:click_sort)))
%tbody
= render(ProductComponent.with_collection(@products, columns: @columns))
.products-table-form_pagination
= render(PaginationComponent.new(pagy: @pagy, data: reflex_data_attributes(:change_page)))

View File

@@ -1,47 +0,0 @@
.products-table {
.products-table-form {
display: grid;
grid-template-columns: repeat( auto-fit, minmax(250px, 1fr) );
grid-gap: 10px;
margin-bottom: 10px;
}
.products-table_table {
box-shadow: 0 10px 10px -1px rgb(0 0 0 / 10%);
}
.products-table-form_pagination {
position: relative;
top: -15px;
nav, .pagination {
margin-top: 0;
padding-top: 0;
}
}
}
.products-table.loading {
.products-table-form_pagination, .products-table_table {
position: relative;
&:before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.5);
}
}
.products-table_table {
&:before {
background-position: center;
background-repeat: no-repeat;
background-size: 50px 50px;
background-image: url("../images/spinning-circles.svg");
}
}
}

View File

@@ -1,9 +0,0 @@
# frozen_string_literal: true
class SearchInputComponent < ViewComponentReflex::Component
def initialize(value: nil, data: {})
super
@value = value
@data = data
end
end

View File

@@ -1,5 +0,0 @@
= component_controller do
%div.search-input
%input{type: 'text', placeholder: t("components.search_input.placeholder"), id: 'search_query', data: {action: 'debounced:input->search-input#search'}, value: @value}
.search-button{data: @data}
%i.fa.fa-search

View File

@@ -1,23 +0,0 @@
.search-input {
border: 1px solid $light-grey;
height: 3em;
display: flex;
line-height: 3em;
align-items: center;
input {
border: none;
height: 3em;
width: 100%;
box-sizing: border-box;
padding-right: 5px;
}
.search-button {
padding-right: 10px;
padding-left: 5px;
cursor: pointer;
color: $color-4;
}
}

View File

@@ -1,17 +0,0 @@
# frozen_string_literal: true
class SelectorComponent < ViewComponentReflex::Component
def initialize(title:, selected:, items:, data: {})
super
@title = title
@items = items.map do |item|
{
label: item[:label],
value: item[:value],
selected: selected.include?(item[:value])
}
end
@selected = selected
@data = data
end
end

View File

@@ -1,11 +0,0 @@
= component_controller do
.selector.selector-close
.selector-main{ data: { action: "click->selector#toggle" } }
.selector-main-title
= @title
.selector-arrow
.selector-wrapper
.selector-items
- @items.each do |item|
.selector-item{ class: ("selected" if item[:selected]), data: @data, "data-value": item[:value] }
= item[:label]

View File

@@ -1,86 +0,0 @@
.selector {
position: relative;
.selector-main {
border: 1px solid $light-grey;
height: 3em;
position: relative;
cursor: pointer;
.selector-main-title {
line-height: 3em;
padding-left: 10px;
padding-right: 10px;
}
.selector-arrow {
position: absolute;
right: 0px;
height: 3em;
width: 1.5em;
top: -1px;
&:after {
content: "";
position: absolute;
top: 50%;
right: 5px;
margin-top: -5px;
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 5px solid $light-grey;
}
}
}
.selector-wrapper {
position: absolute;
left: 0px;
right: 0px;
z-index: 1;
background-color: white;
margin-top: -1px;
border: 1px solid $light-grey;
.selector-items {
overflow-y: auto;
min-height: 6em;
.selector-item {
padding-left: 10px;
padding-right: 10px;
border-bottom: 1px solid $light-grey;
position: relative;
height: 3em;
line-height: 3em;
&:hover {
background-color: #eee;
cursor: pointer;
}
&:last-child {
border-bottom: none;
}
&.selected {
&:after {
content: "";
display: inline-block;
position: absolute;
right: 10px;
}
}
}
}
}
&.selector-close {
.selector-wrapper {
display: none;
}
}
}

View File

@@ -1,11 +0,0 @@
# frozen_string_literal: true
class SelectorWithFilterComponent < SelectorComponent
def initialize(title:, selected:, items:, data: {},
selected_items_i18n_key: 'components.selector_with_filter.selected_items')
super(title: title, selected: selected, items: items, data: data)
@selected_items = items.select { |item| @selected.include?(item[:value]) }
@selected_items_i18n_key = selected_items_i18n_key
@items = items
end
end

View File

@@ -1,22 +0,0 @@
= component_controller do
.super-selector.selector.selector-close
.selector-main{ data: { action: "click->selector-with-filter#toggle" } }
.super-selector-label
= @title
.super-selector-selected-items
- case @selected_items.length
- when 1, 2
- @selected_items.each do |item|
.super-selector-selected-item
= item[:label]
- else
.super-selector-selected-item
= t(@selected_items_i18n_key, count: @selected_items.length)
.selector-arrow
.selector-wrapper
.super-selector-search
%input{type: "text", placeholder: t("components.selector_with_filter.search_placeholder"), data: { action: "debounced:input->selector-with-filter#filter" } }
.selector-items
- @items.each do |item|
.selector-item{ class: ("selected" if item[:selected]), data: @data.merge({ "selector-with-filter-target": "items" }), "data-value": item[:value] }
= item[:label]

View File

@@ -1,51 +0,0 @@
.super-selector {
position: relative;
.selector-main {
.super-selector-label {
padding-left: 5px;
padding-right: 5px;
margin-left: 10px;
position: absolute;
top: -1em;
background-color: white;
}
}
.super-selector-selected-items {
margin-left: 5px;
margin-right: 2em;
margin-top: 7px;
display: flex;
.super-selector-selected-item {
border: 1px solid $pale-blue;
background-color: $spree-light-blue;
border-radius: 20px;
height: 2em;
padding-left: 10px;
padding-right: 10px;
display: inline-block;
margin-right: 5px;
padding-top: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.selector-wrapper {
.super-selector-search {
border-bottom: 1px solid $light-grey;
padding: 10px 5px;
input {
border: 1px solid $light-grey;
box-sizing: border-box;
border-radius: 4px;
width: 100%;
}
}
}
}

View File

@@ -1,10 +0,0 @@
# frozen_string_literal: true
class TableHeaderComponent < ViewComponentReflex::Component
def initialize(columns:, sort:, data: {})
super
@columns = columns
@sort = sort
@data = data
end
end

View File

@@ -1,7 +0,0 @@
= component_controller do
%thead.table-header
%tr
- @columns.each do |column|
%th{class: (column[:sortable] ? "th-sortable " : "" ) + (@sort[:column] == column[:value] ? " th-sorted-#{@sort[:direction]}" : ""), data: (@data if column[:sortable] == true), "data-sort-value": column[:value], "data-sort-direction": @sort[:direction]}
= column[:label]

View File

@@ -1,23 +0,0 @@
thead.table-header {
th {
&.th-sortable {
cursor: pointer;
}
&.th-sorted-asc, &.th-sorted-desc {
&:after {
display: inline-block;
padding-left: 10px;
}
}
&.th-sorted-asc {
&:after {
content: "";
}
}
&.th-sorted-desc {
&:after {
content: "";
}
}
}
}

View File

@@ -1,7 +0,0 @@
# frozen_string_literal: true
module Admin
class ProductsController < Spree::Admin::BaseController
def index; end
end
end

View File

@@ -79,7 +79,6 @@ class ProductsReflex < ApplicationReflex
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

View File

@@ -1,8 +0,0 @@
- content_for :page_title do
= t('admin.products_page.title')
= render partial: 'spree/admin/shared/product_sub_menu'
#products_page
= render(ProductsTableComponent.new(user: spree_current_user))

View File

@@ -4,7 +4,5 @@
= tab :properties
= tab :variant_overrides, url: main_app.admin_inventory_path, match_path: '/inventory'
= tab :import, url: main_app.admin_product_import_path, match_path: '/product_import'
- if feature?(:new_products_page, spree_current_user)
= tab :new_products, url: main_app.admin_new_products_path
- if feature?(:admin_style_v3, spree_current_user)
= tab :products_v3, url: main_app.admin_products_v3_path

View File

@@ -1,46 +0,0 @@
import ApplicationController from "./application_controller";
export default class extends ApplicationController {
connect() {
super.connect();
document.addEventListener(
"stimulus-reflex:before",
this.handleBeforeReflex.bind(this)
);
document.addEventListener(
"stimulus-reflex:after",
this.handleAfterReflex.bind(this)
);
}
disconnect() {
super.disconnect();
document.removeEventListener(
"stimulus-reflex:before",
this.handleBeforeReflex.bind(this)
);
document.removeEventListener(
"stimulus-reflex:after",
this.handleAfterReflex.bind(this)
);
}
handleBeforeReflex(event) {
if (event.detail.reflex.indexOf("ProductsTableComponent#") !== -1) {
this.showLoading();
}
}
handleAfterReflex(event) {
if (event.detail.reflex.indexOf("ProductsTableComponent#") !== -1) {
this.hideLoading();
}
}
showLoading() {
this.element.classList.add("loading");
}
hideLoading() {
this.element.classList.remove("loading");
}
}

View File

@@ -120,12 +120,5 @@
@import "components/tom_select";
@import "app/components/help_modal_component/help_modal_component";
@import "app/components/product_component/product_component";
@import "app/components/selector_component/selector_component";
@import "app/components/products_table_component/products_table_component";
@import "app/components/selector_with_filter_component/selector_with_filter_component";
@import "app/components/pagination_component/pagination_component";
@import "app/components/table_header_component/table_header_component";
@import "app/components/search_input_component/search_input_component";
@import "app/components/confirm_modal_component/confirm_modal_component";
@import "app/webpacker/css/admin/trix.scss";

View File

@@ -128,11 +128,4 @@
@import "../admin/components/tom_select";
@import "app/components/help_modal_component/help_modal_component";
@import "app/components/product_component/product_component";
@import "app/components/selector_component/selector_component";
@import "app/components/products_table_component/products_table_component";
@import "app/components/selector_with_filter_component/selector_with_filter_component";
@import "app/components/pagination_component/pagination_component";
@import "app/components/table_header_component/table_header_component";
@import "app/components/search_input_component/search_input_component";
@import "app/components/confirm_modal_component/confirm_modal_component";

View File

@@ -69,9 +69,6 @@ Openfoodnetwork::Application.routes.draw do
post '/product_import/save_data', to: 'product_import#save_data', as: 'product_import_save_async'
post '/product_import/reset_absent', to: 'product_import#reset_absent_products', as: 'product_import_reset_async'
constraints FeatureToggleConstraint.new(:new_products_page) do
get '/new_products', to: 'products#index'
end
constraints FeatureToggleConstraint.new(:admin_style_v3) do
get '/products_v3', to: 'products_v3#index'
end

View File

@@ -29,9 +29,6 @@ module OpenFoodNetwork
shipping categories. Activating this feature for an enterprise owner
will activate it for all shops of this enterprise.
DESC
"new_products_page" => <<~DESC,
Show the new (experimental) version of the admin products page.
DESC
"vouchers" => <<~DESC,
Add voucher functionality. Voucher can be managed via Enterprise settings.
DESC

View File

@@ -1,46 +0,0 @@
# frozen_string_literal: true
require "spec_helper"
describe ProductComponent, type: :component do
let(:product) { create(:simple_product) }
describe 'unit' do
before do
render_inline(
ProductComponent.new(
product: product, columns: [{ label: "Unit", value: "unit", sortable: false }]
)
)
end
it 'concatenates the unit value and the unit description' do
expect(page.find('.unit')).to have_content '1.0 weight'
end
end
describe 'on_hand' do
let(:product) { create(:simple_product, on_hand: on_hand) }
let(:on_hand) { 5 }
before do
render_inline(
ProductComponent.new(
product: product, columns: [{ label: "On Hand", value: "on_hand", sortable: false }]
)
)
end
it 'returns product on_hand' do
expect(page.find('.on_hand')).to have_content(on_hand)
end
context 'when on_hand is nil' do
let(:on_hand) { nil }
it 'returns 0' do
expect(page.find('.on_hand')).to have_content(0.to_s)
end
end
end
end