-
-
Notifications
You must be signed in to change notification settings - Fork 729
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce backoffice UI uplift #8996
Changes from 47 commits
d7d29e3
1869536
7692ceb
2b7bccf
8758a27
461d31b
5ea7bea
8ce3c9f
5c5a0c9
d4cfa7b
1adb22b
4fa88b9
224daf2
8adcdf1
7716d27
069b314
c719736
c23d4f6
cb24efb
d37cd09
1de7fb6
d75c62a
e801bb5
5e689fc
265b482
22d1362
122677b
27c1fe2
3ae5db9
3e03208
98391b6
758c40c
56b9434
18adcb7
20218c0
428256a
fbf2315
91d1ece
940b554
274ee2c
3cf0162
9dfc224
ec31d63
f0e0bac
d97e9ae
5002870
500c4e8
10bfe00
a827844
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
# 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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
= 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" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
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: $disabled-dark; | ||
} | ||
} | ||
|
||
.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: $disabled-dark; | ||
} | ||
|
||
.pagination-page { | ||
color: $color-4; | ||
cursor: pointer; | ||
&.active { | ||
border-top: 3px solid $spree-blue; | ||
color: $spree-blue; | ||
cursor: default; | ||
} | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
# frozen_string_literal: true | ||
|
||
class ProductComponent < ViewComponentReflex::Component | ||
def initialize(product:, columns:) | ||
super | ||
@product = product | ||
@image = @product.images[0] if product.images.any? | ||
@name = @product.name | ||
@columns = columns.map { |c| | ||
{ | ||
id: c[:value], | ||
value: column_value(c[:value]) | ||
} | ||
} | ||
end | ||
|
||
def column_value(column) | ||
case column | ||
when 'price' | ||
@product.price | ||
when 'unit' | ||
"#{@product.unit_value} #{@product.variant_unit}" | ||
when 'producer' | ||
@product.supplier.name | ||
when 'category' | ||
@product.taxons.map(&:name).join(', ') | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,11 @@ | ||||||||||||||||||||||||||
%tr | ||||||||||||||||||||||||||
- @columns.each do |column| | ||||||||||||||||||||||||||
- if column[:id] == "name" | ||||||||||||||||||||||||||
%td.products_column.title | ||||||||||||||||||||||||||
- if @image | ||||||||||||||||||||||||||
.image | ||||||||||||||||||||||||||
= image_tag @image.url(:mini) | ||||||||||||||||||||||||||
= @name | ||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is looks good. Just wondering if it'll be better to add a case for openfoodnetwork/app/components/product_component.rb Lines 17 to 28 in 500c4e8
then you'll add one guard clause for the image, this will maybe require some CSS changes, something like: %td.products_column{class: column[:id]}
- if column[:id] == "name" && @image
.image
= image_tag @image.url(:mini)
= column[:value] There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Way simpler. Thanks! 🙏 |
||||||||||||||||||||||||||
- else | ||||||||||||||||||||||||||
%td.products_column{class: column[:id]} | ||||||||||||||||||||||||||
= column[:value] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
# frozen_string_literal: true | ||
|
||
class ProductsTableComponent < ViewComponentReflex::Component | ||
mkllnk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
include Pagy::Backend | ||
|
||
SORTABLE_COLUMNS = ["name"].freeze | ||
SELECTABLE_COMUMNS = [{ 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" }].sort { |a, b| | ||
a[:label] <=> b[:label] | ||
}.freeze | ||
PER_PAGE_VALUE = [10, 25, 50, 100].freeze | ||
PER_PAGE = PER_PAGE_VALUE.map { |value| { label: value, value: value } } | ||
ALL_COLUMN = { label: I18n.t("admin.products_page.columns.name"), value: "name", | ||
sortable: true }.freeze | ||
|
||
def initialize(user:) | ||
super | ||
@user = user | ||
@selectable_columns = SELECTABLE_COMUMNS | ||
@columns_selected = ["price", "unit"] | ||
@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 | ||
|
||
def before_render | ||
fetch_products | ||
refresh_columns | ||
end | ||
|
||
def search_term | ||
@search_term = element.dataset['value'] | ||
end | ||
|
||
def toggle_column | ||
column = element.dataset['value'] | ||
@columns_selected = if @columns_selected.include?(column) | ||
@columns_selected - [column] | ||
else | ||
@columns_selected + [column] | ||
end | ||
end | ||
|
||
def click_sort | ||
@sort = { column: element.dataset['sort-value'], | ||
direction: element.dataset['sort-direction'] == "asc" ? "desc" : "asc" } | ||
end | ||
|
||
def toggle_per_page | ||
selected = element.dataset['value'].to_i | ||
@per_page_selected = [selected] if PER_PAGE_VALUE.include?(selected) | ||
end | ||
|
||
def toggle_category | ||
category_clicked = element.dataset['value'] | ||
@categories_selected = toggle_selector_with_filter(category_clicked, @categories_selected) | ||
end | ||
|
||
def toggle_producer | ||
producer_clicked = element.dataset['value'] | ||
@producers_selected = toggle_selector_with_filter(producer_clicked, @producers_selected) | ||
end | ||
|
||
def change_page | ||
page = element.dataset['page'].to_i | ||
@page = page if page > 0 | ||
end | ||
|
||
private | ||
|
||
def refresh_columns | ||
@columns = @columns_selected.map { |column| | ||
{ label: I18n.t("admin.products_page.columns.#{column}"), value: column, | ||
sortable: SORTABLE_COLUMNS.include?(column) } | ||
}.sort! { |a, b| a[:label] <=> b[:label] } | ||
@columns.unshift(ALL_COLUMN) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please correct me if I'm wrong. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right! |
||
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 | ||
[ | ||
master: [:images], | ||
variants: [:default_price, :stock_locations, :stock_items, :variant_overrides, | ||
{ option_values: :option_type }] | ||
] | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
= 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))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it make sense to just store the pagy object and access that from the view?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanted the component to be pagy agnostic