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:
parent
6b7daade5f
commit
1b930d2005
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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(() => {
|
||||
|
@ -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: ''
|
||||
};
|
||||
|
@ -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
|
||||
}
|
||||
};
|
||||
|
24
app/frontend/src/javascript/lib/parsing.ts
Normal file
24
app/frontend/src/javascript/lib/parsing.ts
Normal 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;
|
||||
};
|
||||
}
|
@ -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: ''
|
||||
};
|
||||
|
@ -10,3 +10,8 @@ export interface PaginatedIndex<T> {
|
||||
}
|
||||
|
||||
export type SortOption = `${string}-${'asc' | 'desc'}` | '';
|
||||
|
||||
export interface ApiResource<T> {
|
||||
data: T,
|
||||
ready: boolean
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user