Skip to content
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
merged 49 commits into from
Dec 14, 2022
Merged
Show file tree
Hide file tree
Changes from 47 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 Mar 14, 2022
1869536
Install view_component_reflex + cable_ready
jibees Mar 15, 2022
7692ceb
Create Product component
jibees Mar 17, 2022
2b7bccf
Create Selector component
jibees Mar 18, 2022
8758a27
Configure CSS for VC components
jibees Mar 18, 2022
461d31b
Create ProductsTable component that handle the table + all filters
jibees Mar 21, 2022
5ea7bea
Selector now handle {label, value} instead of only string
jibees Mar 21, 2022
8ce3c9f
Introduce a new selector: # of results per page
jibees Mar 21, 2022
5c5a0c9
Rename selected to columns_selected
jibees Mar 21, 2022
d4cfa7b
Add header to the table
jibees Mar 21, 2022
1adb22b
Create SelectorWithFilter component
jibees Mar 22, 2022
4fa88b9
Fatorize CSS between Selector and SuperSelector components
jibees Mar 22, 2022
224daf2
Factorize js controller between Selectore and SuperSelector components
jibees Mar 22, 2022
8adcdf1
Include search by category and producer
jibees Mar 23, 2022
7716d27
Title of the items per page selector is clearer
jibees Mar 23, 2022
069b314
Add producer and category to a product in the table
jibees Mar 23, 2022
c719736
Add a pagination component that handle pagy
jibees Mar 23, 2022
c23d4f6
Add pagination to ProductsTable component
jibees Mar 23, 2022
cb24efb
Some styling improvments
jibees Mar 23, 2022
d37cd09
Use the reflex_data_attributes helper method
jibees Mar 23, 2022
1de7fb6
Create TableHeader component
jibees Mar 23, 2022
d75c62a
Add TableHeader component and better columns implementation
jibees Mar 23, 2022
e801bb5
some CSS linter
jibees Mar 23, 2022
5e689fc
No need to fetch as it's done inside beforeReflex lifecycle method
jibees Mar 23, 2022
265b482
Add static var + freeze
jibees Mar 23, 2022
22d1362
Only send close reflex if element is actually visible
jibees Mar 24, 2022
122677b
Create SearchInput component
jibees Mar 24, 2022
27c1fe2
Add search query input to search for product by name
jibees Mar 24, 2022
3ae5db9
Create js controller for ProductsTable component
jibees Mar 24, 2022
3e03208
Add loading state for ProductsTable component
jibees Mar 25, 2022
98391b6
Use CSS vars to match linter
jibees Mar 25, 2022
758c40c
Manage i18n for Products page
jibees Mar 28, 2022
56b9434
Manage i18n for SearchInput component
jibees Mar 28, 2022
18adcb7
Handle i18n for SuperSelector component
jibees Mar 28, 2022
20218c0
Manage i18n for ProductsTable component
jibees Mar 28, 2022
428256a
Exclude products_table_component from Metrics/ClassLength rubocop rule
jibees Mar 30, 2022
fbf2315
Rename SuperSelector to SelectorWithFilter
jibees Mar 30, 2022
91d1ece
Click the whole Selector instead of arrow only
jibees Apr 4, 2022
940b554
Toggle is now controlled by the browser (and don't requires a reflex)
jibees Apr 7, 2022
274ee2c
Toggle is now controlled by the browser (and don't requires a reflex)
jibees Apr 7, 2022
3cf0162
Handle filtering on js controller for SelectorWithFilter
jibees Apr 7, 2022
9dfc224
Selectors are close by default
jibees Sep 30, 2022
ec31d63
Do not import/initialize twice
jibees Oct 7, 2022
f0e0bac
As `components:` key already exists, use it!
jibees Oct 25, 2022
d97e9ae
Remove white space
jibees Oct 25, 2022
5002870
Add missing i18n translation for PaginationComponent
jibees Oct 25, 2022
500c4e8
Already handled by `app/webpacker/controllers/index.js`
jibees Dec 7, 2022
10bfe00
Rename column "Name"
jibees Dec 12, 2022
a827844
Simplify the way we render the `name` column
jibees Dec 12, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .rubocop_todo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,7 @@ Metrics/BlockNesting:
# Configuration parameters: CountComments, Max, CountAsOne.
Metrics/ClassLength:
Exclude:
- 'app/components/products_table_component.rb'
- 'app/controllers/admin/customers_controller.rb'
- 'app/controllers/admin/enterprises_controller.rb'
- 'app/controllers/admin/order_cycles_controller.rb'
Expand Down
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ gem 'flipper-active_record'
gem 'flipper-ui'

gem "view_component"
gem 'view_component_reflex', '3.1.14.pre9'

gem 'mini_portile2', '~> 2.8'

Expand Down
5 changes: 5 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,10 @@ GEM
activesupport (>= 5.0.0, < 8.0)
concurrent-ruby (~> 1.0)
method_source (~> 1.0)
view_component_reflex (3.1.14.pre9)
rails (>= 5.2, < 8.0)
stimulus_reflex (>= 3.5.0.pre2)
view_component (>= 2.28.0)
view_component_storybook (0.11.1)
view_component (>= 2.36)
warden (1.2.9)
Expand Down Expand Up @@ -879,6 +883,7 @@ DEPENDENCIES
valid_email2
vcr
view_component
view_component_reflex (= 3.1.14.pre9)
view_component_storybook
web!
web-console
Expand Down
15 changes: 15 additions & 0 deletions app/components/pagination_component.rb
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
Comment on lines +6 to +13
Copy link
Member

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?

Copy link
Contributor Author

@jibees jibees Oct 25, 2022

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

end
end
16 changes: 16 additions & 0 deletions app/components/pagination_component/pagination_component.html.haml
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 app/components/pagination_component/pagination_component.scss
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;
}
}
}
}
}
29 changes: 29 additions & 0 deletions app/components/product_component.rb
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
11 changes: 11 additions & 0 deletions app/components/product_component/product_component.html.haml
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
Copy link
Member

Choose a reason for hiding this comment

The 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 name

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

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]

Copy link
Contributor Author

@jibees jibees Dec 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Way simpler. Thanks! 🙏

0a19148

- else
%td.products_column{class: column[:id]}
= column[:value]
Empty file.
152 changes: 152 additions & 0 deletions app/components/products_table_component.rb
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please correct me if I'm wrong.
Looks like ALL_COLUMN only stored the name column. Maybe we can find a better name for it.

Copy link
Contributor Author

@jibees jibees Dec 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right!

918827c

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)))
Loading