diff --git a/app/frontend/src/javascript/api/product.ts b/app/frontend/src/javascript/api/product.ts index f760e1a29..3df9615a7 100644 --- a/app/frontend/src/javascript/api/product.ts +++ b/app/frontend/src/javascript/api/product.ts @@ -1,13 +1,12 @@ import apiClient from './clients/api-client'; import { AxiosResponse } from 'axios'; import { serialize } from 'object-to-formdata'; -import { Product, ProductIndexFilter, ProductsIndex, ProductStockMovement } from '../models/product'; +import { Product, ProductIndexFilterIds, ProductsIndex, ProductStockMovement } from '../models/product'; import ApiLib from '../lib/api'; -import ProductLib from '../lib/product'; export default class ProductAPI { - static async index (filters?: ProductIndexFilter): Promise { - const res: AxiosResponse = await apiClient.get(`/api/products${ApiLib.filtersToQuery(ProductLib.indexFiltersToIds(filters))}`); + static async index (filters?: ProductIndexFilterIds): Promise { + const res: AxiosResponse = await apiClient.get(`/api/products${ApiLib.filtersToQuery(filters)}`); return res?.data; } diff --git a/app/frontend/src/javascript/components/store/products.tsx b/app/frontend/src/javascript/components/store/products.tsx index a1cef29f6..7ec96c1fb 100644 --- a/app/frontend/src/javascript/components/store/products.tsx +++ b/app/frontend/src/javascript/components/store/products.tsx @@ -70,7 +70,7 @@ const Products: React.FC = ({ onSuccess, onError }) => { */ const fetchProducts = async (): Promise => { try { - const data = await ProductAPI.index(filters); + const data = await ProductAPI.index(ProductLib.indexFiltersToIds(filters)); setCurrentPage(data.page); setProductList(data.data); setPageCount(data.total_pages); diff --git a/app/frontend/src/javascript/components/store/store.tsx b/app/frontend/src/javascript/components/store/store.tsx index 9d9a7ea33..46ba018f7 100644 --- a/app/frontend/src/javascript/components/store/store.tsx +++ b/app/frontend/src/javascript/components/store/store.tsx @@ -20,6 +20,7 @@ import { Machine } from '../../models/machine'; import { KeywordFilter } from './filters/keyword-filter'; import { ActiveFiltersTags } from './filters/active-filters-tags'; import ProductLib from '../../lib/product'; +import { UIRouter } from '@uirouter/angularjs'; declare const Application: IApplication; @@ -27,6 +28,7 @@ interface StoreProps { onError: (message: string) => void, onSuccess: (message: string) => void, currentUser: User, + uiRouter: UIRouter, } /** * Option format, expected by react-select @@ -37,7 +39,7 @@ interface StoreProps { /** * This component shows public store */ -const Store: React.FC = ({ onError, onSuccess, currentUser }) => { +const Store: React.FC = ({ onError, onSuccess, currentUser, uiRouter }) => { const { t } = useTranslation('public'); const { cart, setCart } = useCart(currentUser); @@ -51,6 +53,8 @@ const Store: React.FC = ({ onError, onSuccess, currentUser }) => { const [filters, setFilters] = useImmer(initFilters); useEffect(() => { + // TODO, set the filters in the state + console.log(ProductLib.readFiltersFromUrl(location.href)); fetchProducts().then(scrollToProducts); ProductCategoryAPI.index().then(data => { setProductCategories(data); @@ -62,6 +66,7 @@ const Store: React.FC = ({ onError, onSuccess, currentUser }) => { useEffect(() => { fetchProducts().then(scrollToProducts); + uiRouter.stateService.transitionTo(uiRouter.globals.current, ProductLib.indexFiltersToRouterParams(filters)); }, [filters]); /** @@ -173,7 +178,7 @@ const Store: React.FC = ({ onError, onSuccess, currentUser }) => { */ const fetchProducts = async (): Promise => { try { - const data = await ProductAPI.index(filters); + const data = await ProductAPI.index(ProductLib.indexFiltersToIds(filters)); setCurrentPage(data.page); setProducts(data.data); setPageCount(data.total_pages); @@ -287,7 +292,7 @@ const StoreWrapper: React.FC = (props) => { ); }; -Application.Components.component('store', react2angular(StoreWrapper, ['onError', 'onSuccess', 'currentUser'])); +Application.Components.component('store', react2angular(StoreWrapper, ['onError', 'onSuccess', 'currentUser', 'uiRouter'])); interface CategoryTree { parent: ProductCategory, diff --git a/app/frontend/src/javascript/controllers/store.js b/app/frontend/src/javascript/controllers/store.js index fef6b91c2..7045cfe6a 100644 --- a/app/frontend/src/javascript/controllers/store.js +++ b/app/frontend/src/javascript/controllers/store.js @@ -1,15 +1,12 @@ -/* eslint-disable - no-return-assign, - no-undef, -*/ 'use strict'; -Application.Controllers.controller('StoreController', ['$scope', 'CSRF', 'growl', '$state', - function ($scope, CSRF, growl, $state) { - /* PRIVATE SCOPE */ - +Application.Controllers.controller('StoreController', ['$scope', 'CSRF', 'growl', '$uiRouter', + function ($scope, CSRF, growl, $uiRouter) { /* PUBLIC SCOPE */ + // the following item is used by the Store component to store the filters in te URL + $scope.uiRouter = $uiRouter; + /** * Callback triggered in case of error */ diff --git a/app/frontend/src/javascript/lib/product.ts b/app/frontend/src/javascript/lib/product.ts index 51acb0cef..d32e8dfb6 100644 --- a/app/frontend/src/javascript/lib/product.ts +++ b/app/frontend/src/javascript/lib/product.ts @@ -1,6 +1,7 @@ import { ProductCategory } from '../models/product-category'; import { - ProductIndexFilter, ProductIndexFilterIds, + ProductIndexFilter, + ProductIndexFilterIds, ProductIndexFilterUrl, stockMovementInReasons, stockMovementOutReasons, StockMovementReason @@ -102,4 +103,46 @@ export default class ProductLib { machines: filters.machines?.map(m => m.id) }; }; + + /** + * Prepare the filtering data from the filters to pass them to the router URL + */ + static indexFiltersToRouterParams = (filters: ProductIndexFilter): ProductIndexFilterUrl => { + let categoryTypeUrl = null; + let category = null; + if (filters.categories.length > 0) { + categoryTypeUrl = filters.categories[0].parent_id === null ? 'c' : 'sc'; + category = filters.categories.map(c => c.slug)[0]; + } + return { + ...filters, + machines: filters.machines?.map(m => m.slug), + category, + categoryTypeUrl + }; + }; + + /** + * Parse the provided URL and return a ready-to-use filter object + * FIXME + */ + static readFiltersFromUrl = (url: string): ProductIndexFilterIds => { + const res: ProductIndexFilterIds = {}; + for (const [key, value] of new URLSearchParams(url.split('?')[1])) { + let parsedValue: string|number|boolean = value; + if (['true', 'false'].includes(value)) { + parsedValue = (value === 'true'); + } else if (parseInt(value, 10).toString() === value) { + parsedValue = parseInt(value, 10); + } + if (res[key] === undefined) { + res[key] = parsedValue; + } else if (Array.isArray(res[key])) { + res[key] = [...res[key] as Array, parsedValue]; + } else { + res[key] = [res[key], parsedValue]; + } + } + return res; + }; } diff --git a/app/frontend/src/javascript/models/product.ts b/app/frontend/src/javascript/models/product.ts index 33eda5da7..5fa6d549f 100644 --- a/app/frontend/src/javascript/models/product.ts +++ b/app/frontend/src/javascript/models/product.ts @@ -22,6 +22,12 @@ export interface ProductIndexFilterIds extends Omit, } +export interface ProductIndexFilterUrl extends Omit, 'machines'> { + categoryTypeUrl?: 'c' | 'sc', + category?: string, + machines?: Array, +} + export type StockType = 'internal' | 'external' | 'all'; export const stockMovementInReasons = ['inward_stock', 'returned', 'cancelled', 'inventory_fix', 'other_in'] as const; diff --git a/app/frontend/src/javascript/router.js b/app/frontend/src/javascript/router.js index 52f225e22..ae3e1536a 100644 --- a/app/frontend/src/javascript/router.js +++ b/app/frontend/src/javascript/router.js @@ -620,12 +620,50 @@ angular.module('application.router', ['ui.router']) // store .state('app.public.store', { - url: '/store', + url: '/store/:categoryTypeUrl/:category?{machines:string}{keywords:string}{is_active:string}{page:string}{sort:string}', views: { 'main@': { templateUrl: '/store/index.html', controller: 'StoreController' } + }, + params: { + categoryTypeUrl: { + dynamic: true, + raw: true, + type: 'path', + value: null, + squash: true + }, + category: { + dynamic: true, + type: 'path', + raw: true, + value: null, + squash: true + }, + machines: { + array: true, + dynamic: true, + type: 'query', + raw: true + }, + keywords: { + dynamic: true, + type: 'query' + }, + is_active: { + dynamic: true, + type: 'query' + }, + page: { + dynamic: true, + type: 'query' + }, + sort: { + dynamic: true, + type: 'query' + } } }) diff --git a/app/frontend/templates/store/index.html b/app/frontend/templates/store/index.html index 56d615e78..fb2ce5966 100644 --- a/app/frontend/templates/store/index.html +++ b/app/frontend/templates/store/index.html @@ -13,5 +13,5 @@
- +
diff --git a/tsconfig.json b/tsconfig.json index 8a0c68a81..696ff4212 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ "declaration": false, "emitDecoratorMetadata": true, "experimentalDecorators": true, - "lib": ["es6", "dom", "es2015.collection", "es2015.iterable"], + "lib": ["dom", "dom.iterable", "es2019"], "module": "ES2020", "moduleResolution": "node", "sourceMap": true,