-
-
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
Merged
RachL
merged 49 commits into
openfoodfoundation:master
from
jibees:introduce-backoffice-ui-uplift
Dec 14, 2022
Merged
Changes from all commits
Commits
Show all changes
49 commits
Select commit
Hold shift + click to select a range
d7d29e3
Create a new controller for new products page
jibees 1869536
Install view_component_reflex + cable_ready
jibees 7692ceb
Create Product component
jibees 2b7bccf
Create Selector component
jibees 8758a27
Configure CSS for VC components
jibees 461d31b
Create ProductsTable component that handle the table + all filters
jibees 5ea7bea
Selector now handle {label, value} instead of only string
jibees 8ce3c9f
Introduce a new selector: # of results per page
jibees 5c5a0c9
Rename selected to columns_selected
jibees d4cfa7b
Add header to the table
jibees 1adb22b
Create SelectorWithFilter component
jibees 4fa88b9
Fatorize CSS between Selector and SuperSelector components
jibees 224daf2
Factorize js controller between Selectore and SuperSelector components
jibees 8adcdf1
Include search by category and producer
jibees 7716d27
Title of the items per page selector is clearer
jibees 069b314
Add producer and category to a product in the table
jibees c719736
Add a pagination component that handle pagy
jibees c23d4f6
Add pagination to ProductsTable component
jibees cb24efb
Some styling improvments
jibees d37cd09
Use the reflex_data_attributes helper method
jibees 1de7fb6
Create TableHeader component
jibees d75c62a
Add TableHeader component and better columns implementation
jibees e801bb5
some CSS linter
jibees 5e689fc
No need to fetch as it's done inside beforeReflex lifecycle method
jibees 265b482
Add static var + freeze
jibees 22d1362
Only send close reflex if element is actually visible
jibees 122677b
Create SearchInput component
jibees 27c1fe2
Add search query input to search for product by name
jibees 3ae5db9
Create js controller for ProductsTable component
jibees 3e03208
Add loading state for ProductsTable component
jibees 98391b6
Use CSS vars to match linter
jibees 758c40c
Manage i18n for Products page
jibees 56b9434
Manage i18n for SearchInput component
jibees 18adcb7
Handle i18n for SuperSelector component
jibees 20218c0
Manage i18n for ProductsTable component
jibees 428256a
Exclude products_table_component from Metrics/ClassLength rubocop rule
jibees fbf2315
Rename SuperSelector to SelectorWithFilter
jibees 91d1ece
Click the whole Selector instead of arrow only
jibees 940b554
Toggle is now controlled by the browser (and don't requires a reflex)
jibees 274ee2c
Toggle is now controlled by the browser (and don't requires a reflex)
jibees 3cf0162
Handle filtering on js controller for SelectorWithFilter
jibees 9dfc224
Selectors are close by default
jibees ec31d63
Do not import/initialize twice
jibees f0e0bac
As `components:` key already exists, use it!
jibees d97e9ae
Remove white space
jibees 5002870
Add missing i18n translation for PaginationComponent
jibees 500c4e8
Already handled by `app/webpacker/controllers/index.js`
jibees 10bfe00
Rename column "Name"
jibees a827844
Simplify the way we render the `name` column
jibees File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
16 changes: 16 additions & 0 deletions
16
app/components/pagination_component/pagination_component.html.haml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
|
69 changes: 69 additions & 0 deletions
69
app/components/pagination_component/pagination_component.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
# frozen_string_literal: true | ||
|
||
class ProductComponent < ViewComponentReflex::Component | ||
def initialize(product:, columns:) | ||
super | ||
@product = product | ||
@image = @product.images[0] if product.images.any? | ||
@columns = columns.map { |c| | ||
{ | ||
id: c[:value], | ||
value: column_value(c[:value]) | ||
} | ||
} | ||
end | ||
|
||
def column_value(column) | ||
case column | ||
when 'name' | ||
@product.name | ||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
%tr | ||
- @columns.each do |column| | ||
%td.products_column{class: column[:id]} | ||
- if column[:id] == "name" && @image | ||
= image_tag @image.url(:mini) | ||
= column[:value] |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } } | ||
NAME_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(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 | ||
[ | ||
master: [:images], | ||
variants: [:default_price, :stock_locations, :stock_items, :variant_overrides, | ||
{ option_values: :option_type }] | ||
] | ||
end | ||
end |
21 changes: 21 additions & 0 deletions
21
app/components/products_table_component/products_table_component.html.haml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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))) |
47 changes: 47 additions & 0 deletions
47
app/components/products_table_component/products_table_component.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
.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"); | ||
} | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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