1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-20 14:54:15 +01:00

(feat) restore the filters from the URL

This commit is contained in:
Sylvain 2022-09-26 15:23:07 +02:00
parent 6b7daade5f
commit 1b930d2005
8 changed files with 230 additions and 78 deletions

View File

@ -6,7 +6,8 @@
],
"rules": {
"semi": ["error", "always"],
"no-use-before-define": "off"
"no-use-before-define": "off",
"no-case-declarations": "off"
},
"globals": {
"Application": true,

View File

@ -1,12 +1,13 @@
import apiClient from './clients/api-client';
import { AxiosResponse } from 'axios';
import { serialize } from 'object-to-formdata';
import { Product, ProductIndexFilterIds, ProductsIndex, ProductStockMovement } from '../models/product';
import { Product, ProductIndexFilter, ProductsIndex, ProductStockMovement } from '../models/product';
import ApiLib from '../lib/api';
import ProductLib from '../lib/product';
export default class ProductAPI {
static async index (filters?: ProductIndexFilterIds): Promise<ProductsIndex> {
const res: AxiosResponse<ProductsIndex> = await apiClient.get(`/api/products${ApiLib.filtersToQuery(filters)}`);
static async index (filters?: ProductIndexFilter): Promise<ProductsIndex> {
const res: AxiosResponse<ProductsIndex> = await apiClient.get(`/api/products${ApiLib.filtersToQuery(ProductLib.indexFiltersToIds(filters))}`);
return res?.data;
}

View File

@ -7,27 +7,30 @@ import MachineAPI from '../../../api/machine';
import _ from 'lodash';
interface MachinesFilterProps {
allMachines?: Array<Machine>,
onError: (message: string) => void,
onApplyFilters: (categories: Array<Machine>) => void,
currentFilters: Array<Machine>,
openDefault?: boolean,
instantUpdate?: boolean,
instantUpdate?: boolean
}
/**
* Component to filter the products list by associated machine
*/
export const MachinesFilter: React.FC<MachinesFilterProps> = ({ onError, onApplyFilters, currentFilters, openDefault = false, instantUpdate = false }) => {
export const MachinesFilter: React.FC<MachinesFilterProps> = ({ allMachines, onError, onApplyFilters, currentFilters, openDefault = false, instantUpdate = false }) => {
const { t } = useTranslation('admin');
const [machines, setMachines] = useState<Machine[]>([]);
const [machines, setMachines] = useState<Machine[]>(allMachines);
const [openedAccordion, setOpenedAccordion] = useState<boolean>(openDefault);
const [selectedMachines, setSelectedMachines] = useState<Machine[]>(currentFilters || []);
useEffect(() => {
MachineAPI.index({ disabled: false }).then(data => {
setMachines(data);
}).catch(onError);
if (_.isEmpty(allMachines)) {
MachineAPI.index({ disabled: false }).then(data => {
setMachines(data);
}).catch(onError);
}
}, []);
useEffect(() => {

View File

@ -17,7 +17,7 @@ import { MachinesFilter } from './filters/machines-filter';
import { KeywordFilter } from './filters/keyword-filter';
import { StockFilter } from './filters/stock-filter';
import ProductCategoryAPI from '../../api/product-category';
import ProductLib from '../../lib/product';
import ProductLib, { initFilters } from '../../lib/product';
import { ActiveFiltersTags } from './filters/active-filters-tags';
declare const Application: IApplication;
@ -70,7 +70,7 @@ const Products: React.FC<ProductsProps> = ({ onSuccess, onError }) => {
*/
const fetchProducts = async (): Promise<ProductsIndex> => {
try {
const data = await ProductAPI.index(ProductLib.indexFiltersToIds(filters));
const data = await ProductAPI.index(filters);
setCurrentPage(data.page);
setProductList(data.data);
setPageCount(data.total_pages);
@ -261,15 +261,3 @@ const ProductsWrapper: React.FC<ProductsProps> = ({ onSuccess, onError }) => {
};
Application.Components.component('products', react2angular(ProductsWrapper, ['onSuccess', 'onError']));
const initFilters: ProductIndexFilter = {
categories: [],
machines: [],
keywords: [],
stock_type: 'internal',
stock_from: 0,
stock_to: 0,
is_active: false,
page: 1,
sort: ''
};

View File

@ -19,8 +19,11 @@ import { useImmer } from 'use-immer';
import { Machine } from '../../models/machine';
import { KeywordFilter } from './filters/keyword-filter';
import { ActiveFiltersTags } from './filters/active-filters-tags';
import ProductLib from '../../lib/product';
import ProductLib, { initFilters } from '../../lib/product';
import { UIRouter } from '@uirouter/angularjs';
import MachineAPI from '../../api/machine';
import SettingAPI from '../../api/setting';
import { ApiResource } from '../../models/api';
declare const Application: IApplication;
@ -45,29 +48,66 @@ const Store: React.FC<StoreProps> = ({ onError, onSuccess, currentUser, uiRouter
const { cart, setCart } = useCart(currentUser);
const [products, setProducts] = useState<Array<Product>>([]);
const [productCategories, setProductCategories] = useState<ProductCategory[]>([]);
// this includes the resources fetch from the API (machines, categories) and from the URL (filters)
const [resources, setResources] = useImmer<FetchResources>(initialResources);
const [machinesModule, setMachinesModule] = useState<boolean>(false);
const [categoriesTree, setCategoriesTree] = useState<CategoryTree[]>([]);
const [pageCount, setPageCount] = useState<number>(0);
const [productsCount, setProductsCount] = useState<number>(0);
const [currentPage, setCurrentPage] = useState<number>(1);
const [filters, setFilters] = useImmer<ProductIndexFilter>(initFilters);
useEffect(() => {
// TODO, set the filters in the state
console.log(ProductLib.readFiltersFromUrl(location.href));
fetchProducts().then(scrollToProducts);
ProductCategoryAPI.index().then(data => {
setProductCategories(data);
setResources(draft => {
return {
...draft,
categories: {
data,
ready: true
}
};
});
formatCategories(data);
}).catch(error => {
onError(t('app.public.store.unexpected_error_occurred') + error);
});
MachineAPI.index({ disabled: false }).then(data => {
setResources(draft => {
return {
...draft,
machines: {
data,
ready: true
}
};
});
}).catch(onError);
SettingAPI.get('machines_module').then(data => {
setMachinesModule(data.value === 'true');
}).catch(onError);
}, []);
useEffect(() => {
fetchProducts().then(scrollToProducts);
uiRouter.stateService.transitionTo(uiRouter.globals.current, ProductLib.indexFiltersToRouterParams(filters));
}, [filters]);
if (resources.filters.ready) {
uiRouter.stateService.transitionTo(uiRouter.globals.current, ProductLib.indexFiltersToRouterParams(resources.filters.data));
}
}, [resources.filters]);
useEffect(() => {
if (resources.machines.ready && resources.categories.ready) {
setResources(draft => {
return {
...draft,
filters: {
data: ProductLib.readFiltersFromUrl(uiRouter.globals.params, resources.machines.data, resources.categories.data),
ready: true
}
};
});
}
}, [resources.machines, resources.categories]);
/**
* Create categories tree (parent/children)
@ -88,12 +128,18 @@ const Store: React.FC<StoreProps> = ({ onError, onSuccess, currentUser, uiRouter
* Filter by category: the selected category will always be first
*/
const filterCategory = (category: ProductCategory) => {
setFilters(draft => {
setResources(draft => {
return {
...draft,
categories: category
? Array.from(new Set([category, ...ProductLib.categoriesSelectionTree(productCategories, [], category, 'add')]))
: []
filters: {
...draft.filters,
data: {
...draft.filters.data,
categories: category
? Array.from(new Set([category, ...ProductLib.categoriesSelectionTree(resources.categories.data, [], category, 'add')]))
: []
}
}
};
});
};
@ -102,8 +148,17 @@ const Store: React.FC<StoreProps> = ({ onError, onSuccess, currentUser, uiRouter
* Update the list of applied filters with the given machines
*/
const applyMachineFilters = (machines: Array<Machine>) => {
setFilters(draft => {
return { ...draft, machines };
setResources(draft => {
return {
...draft,
filters: {
...draft.filters,
data: {
...draft.filters.data,
machines
}
}
};
});
};
@ -111,15 +166,32 @@ const Store: React.FC<StoreProps> = ({ onError, onSuccess, currentUser, uiRouter
* Update the list of applied filters with the given keywords (or reference)
*/
const applyKeywordFilter = (keywords: Array<string>) => {
setFilters(draft => {
return { ...draft, keywords };
setResources(draft => {
return {
...draft,
filters: {
...draft.filters,
data: {
...draft.filters.data,
keywords
}
}
};
});
};
/**
* Clear filters
*/
const clearAllFilters = () => {
setFilters(initFilters);
setResources(draft => {
return {
...draft,
filters: {
...draft.filters,
data: initFilters
}
};
});
};
/**
@ -137,10 +209,16 @@ const Store: React.FC<StoreProps> = ({ onError, onSuccess, currentUser, uiRouter
* Display option: sorting
*/
const handleSorting = (option: selectOption) => {
setFilters(draft => {
setResources(draft => {
return {
...draft,
sort: option.value
filters: {
...draft.filters,
data: {
...draft.filters.data,
sort: option.value
}
}
};
});
};
@ -149,8 +227,17 @@ const Store: React.FC<StoreProps> = ({ onError, onSuccess, currentUser, uiRouter
* Filter: toggle non-available products visibility
*/
const toggleVisible = (checked: boolean) => {
setFilters(draft => {
return { ...draft, is_active: checked };
setResources(draft => {
return {
...draft,
filters: {
...draft.filters,
data: {
...draft.filters.data,
is_active: checked
}
}
};
});
};
@ -167,8 +254,17 @@ const Store: React.FC<StoreProps> = ({ onError, onSuccess, currentUser, uiRouter
*/
const handlePagination = (page: number) => {
if (page !== currentPage) {
setFilters(draft => {
return { ...draft, page };
setResources(draft => {
return {
...draft,
filters: {
...draft.filters,
data: {
...draft.filters.data,
page
}
}
};
});
}
};
@ -178,7 +274,7 @@ const Store: React.FC<StoreProps> = ({ onError, onSuccess, currentUser, uiRouter
*/
const fetchProducts = async (): Promise<ProductsIndex> => {
try {
const data = await ProductAPI.index(ProductLib.indexFiltersToIds(filters));
const data = await ProductAPI.index(resources.filters.data);
setCurrentPage(data.page);
setProducts(data.data);
setPageCount(data.total_pages);
@ -196,8 +292,8 @@ const Store: React.FC<StoreProps> = ({ onError, onSuccess, currentUser, uiRouter
window.document.getElementById('content-main').scrollTo({ top: 100, behavior: 'smooth' });
};
const selectedCategory = filters.categories[0];
const parent = productCategories.find(c => c.id === selectedCategory?.parent_id);
const selectedCategory = resources.filters.data.categories[0];
const parent = resources.categories.data.find(c => c.id === selectedCategory?.parent_id);
return (
<div className="store">
<ul className="breadcrumbs">
@ -252,8 +348,13 @@ const Store: React.FC<StoreProps> = ({ onError, onSuccess, currentUser, uiRouter
<FabButton onClick={clearAllFilters} className="is-black">{t('app.public.store.products.filter_clear')}</FabButton>
</div>
</header>
<MachinesFilter onError={onError} onApplyFilters={applyMachineFilters} currentFilters={filters.machines} />
<KeywordFilter onApplyFilters={keyword => applyKeywordFilter([keyword])} currentFilters={filters.keywords[0]} />
{machinesModule && resources.machines.ready &&
<MachinesFilter allMachines={resources.machines.data}
onError={onError}
onApplyFilters={applyMachineFilters}
currentFilters={resources.filters.data.machines} />
}
<KeywordFilter onApplyFilters={keyword => applyKeywordFilter([keyword])} currentFilters={resources.filters.data.keywords[0]} />
</div>
</aside>
<div className='store-list'>
@ -262,13 +363,14 @@ const Store: React.FC<StoreProps> = ({ onError, onSuccess, currentUser, uiRouter
selectOptions={buildOptions()}
onSelectOptionsChange={handleSorting}
switchLabel={t('app.public.store.products.in_stock_only')}
switchChecked={filters.is_active}
switchChecked={resources.filters.data.is_active}
selectValue={resources.filters.data.sort}
onSwitch={toggleVisible}
/>
<div className='features'>
<ActiveFiltersTags filters={filters}
<ActiveFiltersTags filters={resources.filters.data}
displayCategories={false}
onRemoveMachine={(m) => applyMachineFilters(filters.machines.filter(machine => machine !== m))}
onRemoveMachine={(m) => applyMachineFilters(resources.filters.data.machines.filter(machine => machine !== m))}
onRemoveKeyword={() => applyKeywordFilter([])} />
</div>
<div className="products-grid">
@ -299,11 +401,23 @@ interface CategoryTree {
children: ProductCategory[]
}
const initFilters: ProductIndexFilter = {
categories: [],
keywords: [],
machines: [],
is_active: false,
page: 1,
sort: ''
interface FetchResources {
machines: ApiResource<Array<Machine>>,
categories: ApiResource<Array<ProductCategory>>,
filters: ApiResource<ProductIndexFilter>
}
const initialResources: FetchResources = {
machines: {
data: [],
ready: false
},
categories: {
data: [],
ready: false
},
filters: {
data: initFilters,
ready: false
}
};

View File

@ -0,0 +1,24 @@
type baseType = string|number|boolean;
type ValueOrArray<T> = T | ValueOrArray<T>[];
type NestedBaseArray = ValueOrArray<baseType>;
export default class ParsingLib {
/**
* Try to parse the given value to get the value with the matching type.
* It supports parsing arrays.
*/
static parse = (value: string|string[]): NestedBaseArray => {
let parsedValue: NestedBaseArray = value;
if (Array.isArray(value)) {
parsedValue = [];
for (const item of value) {
parsedValue.push(ParsingLib.parse(item));
}
} else if (['true', 'false'].includes(value)) {
parsedValue = (value === 'true');
} else if (parseInt(value, 10).toString() === value) {
parsedValue = parseInt(value, 10);
}
return parsedValue;
};
}

View File

@ -6,6 +6,9 @@ import {
stockMovementOutReasons,
StockMovementReason
} from '../models/product';
import { Machine } from '../models/machine';
import { StateParams } from '@uirouter/angularjs';
import ParsingLib from './parsing';
export default class ProductLib {
/**
@ -124,25 +127,38 @@ export default class ProductLib {
/**
* 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<unknown>, parsedValue];
} else {
res[key] = [res[key], parsedValue];
static readFiltersFromUrl = (params: StateParams, machines: Array<Machine>, categories: Array<ProductCategory>): ProductIndexFilter => {
const res: ProductIndexFilter = { ...initFilters };
for (const key in params) {
if (['#', 'categoryTypeUrl'].includes(key) || !Object.prototype.hasOwnProperty.call(params, key)) continue;
const value = ParsingLib.parse(params[key]) || initFilters[key];
switch (key) {
case 'category':
const parents = categories?.filter(c => (value as Array<string>)?.includes(c.slug));
// we may also add to the selection children categories
res.categories = [...parents, ...categories?.filter(c => parents.map(c => c.id).includes(c.parent_id))];
break;
case 'machines':
res.machines = machines?.filter(m => (value as Array<string>)?.includes(m.slug));
break;
default:
res[key] = value;
}
}
return res;
};
}
export const initFilters: ProductIndexFilter = {
categories: [],
keywords: [],
machines: [],
is_active: false,
stock_type: 'internal',
stock_from: 0,
stock_to: 0,
page: 1,
sort: ''
};

View File

@ -10,3 +10,8 @@ export interface PaginatedIndex<T> {
}
export type SortOption = `${string}-${'asc' | 'desc'}` | '';
export interface ApiResource<T> {
data: T,
ready: boolean
}