From 6678412cd6810329fea3317bd5715762298ba4b5 Mon Sep 17 00:00:00 2001 From: vincent Date: Fri, 9 Sep 2022 13:48:20 +0200 Subject: [PATCH] (feat) pagination --- app/controllers/api/products_controller.rb | 1 + app/frontend/src/javascript/api/product.ts | 6 +-- .../components/base/fab-pagination.tsx | 43 +++++++++++++++++++ .../components/store/orders-dashboard.tsx | 11 ++++- .../javascript/components/store/products.tsx | 24 ++++++++--- .../src/javascript/components/store/store.tsx | 19 +++++++- app/frontend/src/javascript/models/product.ts | 8 +++- app/frontend/src/stylesheets/application.scss | 1 + .../modules/base/fab-pagination.scss | 25 +++++++++++ app/services/product_service.rb | 9 ++++ app/views/api/products/index.json.jbuilder | 3 +- 11 files changed, 136 insertions(+), 14 deletions(-) create mode 100644 app/frontend/src/javascript/components/base/fab-pagination.tsx create mode 100644 app/frontend/src/stylesheets/modules/base/fab-pagination.scss diff --git a/app/controllers/api/products_controller.rb b/app/controllers/api/products_controller.rb index ff022b3eb..c5c55e580 100644 --- a/app/controllers/api/products_controller.rb +++ b/app/controllers/api/products_controller.rb @@ -8,6 +8,7 @@ class API::ProductsController < API::ApiController def index @products = ProductService.list(params) + @pages = ProductService.pages if params[:page].present? end def show diff --git a/app/frontend/src/javascript/api/product.ts b/app/frontend/src/javascript/api/product.ts index 6abf55f4a..bf09b5286 100644 --- a/app/frontend/src/javascript/api/product.ts +++ b/app/frontend/src/javascript/api/product.ts @@ -1,12 +1,12 @@ import apiClient from './clients/api-client'; import { AxiosResponse } from 'axios'; import { serialize } from 'object-to-formdata'; -import { Product, ProductIndexFilter } from '../models/product'; +import { Product, ProductIndexFilter, ProductsIndex } from '../models/product'; import ApiLib from '../lib/api'; export default class ProductAPI { - static async index (filters?: ProductIndexFilter): Promise> { - const res: AxiosResponse> = await apiClient.get(`/api/products${ApiLib.filtersToQuery(filters)}`); + static async index (filters?: ProductIndexFilter): Promise { + const res: AxiosResponse = await apiClient.get(`/api/products${ApiLib.filtersToQuery(filters)}`); return res?.data; } diff --git a/app/frontend/src/javascript/components/base/fab-pagination.tsx b/app/frontend/src/javascript/components/base/fab-pagination.tsx new file mode 100644 index 000000000..ab43e6617 --- /dev/null +++ b/app/frontend/src/javascript/components/base/fab-pagination.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { CaretDoubleLeft, CaretLeft, CaretRight, CaretDoubleRight } from 'phosphor-react'; + +interface FabPaginationProps { + pageCount: number, + currentPage: number, + selectPage: (page: number) => void +} + +/** + * Renders a pagination navigation + */ +export const FabPagination: React.FC = ({ pageCount, currentPage, selectPage }) => { + return ( + + ); +}; diff --git a/app/frontend/src/javascript/components/store/orders-dashboard.tsx b/app/frontend/src/javascript/components/store/orders-dashboard.tsx index b06f65ebc..2460cd8f6 100644 --- a/app/frontend/src/javascript/components/store/orders-dashboard.tsx +++ b/app/frontend/src/javascript/components/store/orders-dashboard.tsx @@ -1,10 +1,11 @@ -import React from 'react'; +import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { react2angular } from 'react2angular'; import { Loader } from '../base/loader'; import { IApplication } from '../../models/application'; import { StoreListHeader } from './store-list-header'; import { OrderItem } from './order-item'; +import { FabPagination } from '../base/fab-pagination'; declare const Application: IApplication; @@ -25,6 +26,11 @@ type selectOption = { value: number, label: string }; export const OrdersDashboard: React.FC = ({ onError }) => { const { t } = useTranslation('public'); + // TODO: delete next eslint disable + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [pageCount, setPageCount] = useState(0); + const [currentPage, setCurrentPage] = useState(1); + /** * Creates sorting options to the react-select format */ @@ -56,6 +62,9 @@ export const OrdersDashboard: React.FC = ({ onError }) =>
+ {pageCount > 1 && + + } ); diff --git a/app/frontend/src/javascript/components/store/products.tsx b/app/frontend/src/javascript/components/store/products.tsx index c1ab15dad..c004d7f05 100644 --- a/app/frontend/src/javascript/components/store/products.tsx +++ b/app/frontend/src/javascript/components/store/products.tsx @@ -14,6 +14,7 @@ import MachineAPI from '../../api/machine'; import { AccordionItem } from './accordion-item'; import { X } from 'phosphor-react'; import { StoreListHeader } from './store-list-header'; +import { FabPagination } from '../base/fab-pagination'; declare const Application: IApplication; @@ -33,8 +34,6 @@ interface ProductsProps { const Products: React.FC = ({ onSuccess, onError }) => { const { t } = useTranslation('admin'); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [products, setProducts] = useState>([]); const [filteredProductsList, setFilteredProductList] = useImmer>([]); const [features, setFeatures] = useImmer(initFilters); const [filterVisible, setFilterVisible] = useState(false); @@ -44,11 +43,13 @@ const Products: React.FC = ({ onSuccess, onError }) => { const [machines, setMachines] = useState([]); const [update, setUpdate] = useState(false); const [accordion, setAccordion] = useState({}); + const [pageCount, setPageCount] = useState(0); + const [currentPage, setCurrentPage] = useState(1); useEffect(() => { - ProductAPI.index().then(data => { - setProducts(data); - setFilteredProductList(data); + ProductAPI.index({ page: 1 }).then(data => { + setPageCount(data.total_pages); + setFilteredProductList(data.products); }); ProductCategoryAPI.index().then(data => { @@ -71,6 +72,14 @@ const Products: React.FC = ({ onSuccess, onError }) => { }).catch(onError); }, []); + useEffect(() => { + ProductAPI.index({ page: currentPage }).then(data => { + setFilteredProductList(data.products); + setPageCount(data.total_pages); + window.document.getElementById('content-main').scrollTo({ top: 100, behavior: 'smooth' }); + }); + }, [currentPage]); + useEffect(() => { applyFilters(); setClearFilters(false); @@ -91,7 +100,7 @@ const Products: React.FC = ({ onSuccess, onError }) => { try { await ProductAPI.destroy(productId); const data = await ProductAPI.index(); - setProducts(data); + setFilteredProductList(data.products); onSuccess(t('app.admin.store.products.successfully_deleted')); } catch (e) { onError(t('app.admin.store.products.unable_to_delete') + e); @@ -307,6 +316,9 @@ const Products: React.FC = ({ onSuccess, onError }) => { /> ))} + {pageCount > 1 && + + } ); diff --git a/app/frontend/src/javascript/components/store/store.tsx b/app/frontend/src/javascript/components/store/store.tsx index a5d140d29..e751d16a6 100644 --- a/app/frontend/src/javascript/components/store/store.tsx +++ b/app/frontend/src/javascript/components/store/store.tsx @@ -16,6 +16,7 @@ import { User } from '../../models/user'; import { Order } from '../../models/order'; import { AccordionItem } from './accordion-item'; import { StoreListHeader } from './store-list-header'; +import { FabPagination } from '../base/fab-pagination'; declare const Application: IApplication; @@ -45,10 +46,13 @@ const Store: React.FC = ({ onError, onSuccess, currentUser }) => { const [filterVisible, setFilterVisible] = useState(false); const [machines, setMachines] = useState([]); const [accordion, setAccordion] = useState({}); + const [pageCount, setPageCount] = useState(0); + const [currentPage, setCurrentPage] = useState(1); useEffect(() => { - ProductAPI.index({ is_active: true }).then(data => { - setProducts(data); + ProductAPI.index({ page: 1 }).then(data => { + setPageCount(data.total_pages); + setProducts(data.products); }).catch(() => { onError(t('app.public.store.unexpected_error_occurred')); }); @@ -71,6 +75,14 @@ const Store: React.FC = ({ onError, onSuccess, currentUser }) => { emitCustomEvent('CartUpdate', cart); }, [cart]); + useEffect(() => { + ProductAPI.index({ page: currentPage }).then(data => { + setProducts(data.products); + setPageCount(data.total_pages); + window.document.getElementById('content-main').scrollTo({ top: 100, behavior: 'smooth' }); + }); + }, [currentPage]); + /** * Create categories tree (parent/children) */ @@ -237,6 +249,9 @@ const Store: React.FC = ({ onError, onSuccess, currentUser }) => { ))} + {pageCount > 1 && + + } ); diff --git a/app/frontend/src/javascript/models/product.ts b/app/frontend/src/javascript/models/product.ts index 27665b281..924b2bbc4 100644 --- a/app/frontend/src/javascript/models/product.ts +++ b/app/frontend/src/javascript/models/product.ts @@ -2,7 +2,8 @@ import { TDateISO } from '../typings/date-iso'; import { ApiFilter } from './api'; export interface ProductIndexFilter extends ApiFilter { - is_active: boolean, + is_active?: boolean, + page?: number } export enum StockType { @@ -15,6 +16,11 @@ export interface Stock { external: number, } +export interface ProductsIndex { + total_pages?: number, + products: Array +} + export interface Product { id: number, name: string, diff --git a/app/frontend/src/stylesheets/application.scss b/app/frontend/src/stylesheets/application.scss index f044a8d9f..04d5e9189 100644 --- a/app/frontend/src/stylesheets/application.scss +++ b/app/frontend/src/stylesheets/application.scss @@ -25,6 +25,7 @@ @import "modules/base/fab-input"; @import "modules/base/fab-modal"; @import "modules/base/fab-output-copy"; +@import "modules/base/fab-pagination"; @import "modules/base/fab-panel"; @import "modules/base/fab-popover"; @import "modules/base/fab-state-label"; diff --git a/app/frontend/src/stylesheets/modules/base/fab-pagination.scss b/app/frontend/src/stylesheets/modules/base/fab-pagination.scss new file mode 100644 index 000000000..e102cfb77 --- /dev/null +++ b/app/frontend/src/stylesheets/modules/base/fab-pagination.scss @@ -0,0 +1,25 @@ +.fab-pagination { + display: grid; + grid-auto-flow: column; + grid-template-columns: repeat(9, min-content); + justify-content: center; + gap: 1.6rem; + button { + min-width: 4rem; + height: 4rem; + display: flex; + justify-content: center; + align-items: center; + background-color: transparent; + border: none; + border-radius: var(--border-radius-sm); + @include text-lg(500); + &:hover:not(.is-active) { + background-color: var(--gray-soft); + } + } + .is-active { + background-color: var(--main); + color: var(--main-text-color); + } +} \ No newline at end of file diff --git a/app/services/product_service.rb b/app/services/product_service.rb index 811a48322..552ad859d 100644 --- a/app/services/product_service.rb +++ b/app/services/product_service.rb @@ -2,15 +2,24 @@ # Provides methods for Product class ProductService + PRODUCTS_PER_PAGE = 2 + def self.list(filters) products = Product.includes(:product_images) if filters[:is_active].present? state = filters[:disabled] == 'false' ? [nil, false] : true products = products.where(is_active: state) end + if filters[:page].present? + products = products.page(filters[:page]).per(PRODUCTS_PER_PAGE) + end products end + def self.pages + Product.page(1).per(PRODUCTS_PER_PAGE).total_pages + end + # amount params multiplied by hundred def self.amount_multiplied_by_hundred(amount) if amount.present? diff --git a/app/views/api/products/index.json.jbuilder b/app/views/api/products/index.json.jbuilder index 10be62f81..0573e95be 100644 --- a/app/views/api/products/index.json.jbuilder +++ b/app/views/api/products/index.json.jbuilder @@ -1,6 +1,7 @@ # frozen_string_literal: true -json.array! @products do |product| +json.total_pages @pages if @pages.present? +json.products @products do |product| json.extract! product, :id, :name, :slug, :sku, :is_active, :product_category_id, :quantity_min, :stock, :machine_ids, :low_stock_threshold json.amount product.amount / 100.0 if product.amount.present?