From 96d0672b08f85620e4afe5d83cb3ffb1dad6a291 Mon Sep 17 00:00:00 2001 From: lme-axelor <102581501+lme-axelor@users.noreply.github.com> Date: Thu, 5 Dec 2024 15:56:59 +0100 Subject: [PATCH] refactor: improve management of fetch for product indicators (#828) * RM#87894 --- changelogs/unreleased/87894.json | 6 ++ .../product/ProductCard/ProductCard.tsx | 47 ++++++++++- .../ProductVariantCard/ProductVariantCard.tsx | 82 ++++++++++++++++--- .../src/features/asyncFunctions-index.ts | 6 +- .../src/features/productIndicatorsSlice.js | 29 ------- .../stock/src/features/productVariantSlice.js | 42 +--------- .../src/screens/products/ProductListScreen.js | 26 ++---- .../products/ProductListVariantScreen.js | 44 ++-------- 8 files changed, 135 insertions(+), 147 deletions(-) create mode 100644 changelogs/unreleased/87894.json diff --git a/changelogs/unreleased/87894.json b/changelogs/unreleased/87894.json new file mode 100644 index 0000000000..7a5a1f9aec --- /dev/null +++ b/changelogs/unreleased/87894.json @@ -0,0 +1,6 @@ +{ + "title": "Indicators: move logic to fetch available stock to card components", + "type": "refactor", + "packages": "stock", + "description": "There was a performance problem on the screens requiring the product indicators. To solve this slow performance problem, product indicators are now retrieved from the card component in the background. The old way of working retrieved the indicators for all the products in the list each time they were updated, before displaying them, which is rather cumbersome and shouldn't be used. The functions concerned have been removed. " +} diff --git a/packages/apps/stock/src/components/templates/product/ProductCard/ProductCard.tsx b/packages/apps/stock/src/components/templates/product/ProductCard/ProductCard.tsx index 295d4690b5..205d1dfd7d 100644 --- a/packages/apps/stock/src/components/templates/product/ProductCard/ProductCard.tsx +++ b/packages/apps/stock/src/components/templates/product/ProductCard/ProductCard.tsx @@ -16,31 +16,70 @@ * along with this program. If not, see . */ -import React from 'react'; +import React, {useEffect, useRef, useState} from 'react'; import {StyleSheet} from 'react-native'; import {ObjectCard, useThemeColor} from '@axelor/aos-mobile-ui'; -import {useMetafileUri, useTranslator} from '@axelor/aos-mobile-core'; +import { + useMetafileUri, + useSelector, + useTranslator, +} from '@axelor/aos-mobile-core'; +import {getProductStockIndicators} from '../../../../api'; interface ProductCardProps { style?: any; + productId: number; + productVersion: number; name: string; code: string; picture: any; - availableStock: number | null | undefined; onPress: () => void; } const ProductCard = ({ style, + productId, + productVersion, name, code, picture, - availableStock, onPress, }: ProductCardProps) => { const Colors = useThemeColor(); const I18n = useTranslator(); const formatMetaFile = useMetafileUri(); + const isMounted = useRef(true); + + const {activeCompany} = useSelector((state: any) => state.user.user); + + const [availableStock, setAvailableStock] = useState(null); + + useEffect(() => { + isMounted.current = true; + + if (productId != null) { + getProductStockIndicators({ + productId: productId, + version: productVersion, + companyId: activeCompany?.id, + stockLocationId: null, + }) + .then((res: any) => { + if (isMounted.current) { + setAvailableStock(res?.data?.object?.availableStock); + } + }) + .catch(() => { + if (isMounted.current) { + setAvailableStock(null); + } + }); + } + + return () => { + isMounted.current = false; + }; + }, [activeCompany?.id, productId, productVersion]); return ( . */ -import React, {useCallback} from 'react'; +import React, {useCallback, useEffect, useRef, useState} from 'react'; import {StyleSheet} from 'react-native'; import {ObjectCard, useDigitFormat, useThemeColor} from '@axelor/aos-mobile-ui'; import {useMetafileUri, useTranslator} from '@axelor/aos-mobile-core'; import Product from '../../../../types/product'; +import { + getProductStockIndicators, + fetchVariantAttributes, +} from '../../../../api'; interface ProductAttribut { attrName: string; @@ -33,9 +37,10 @@ interface ProductVariantCardProps { style?: any; name: string; code: string; - attributesList: {attributes: ProductAttribut[]}; + productId: number; + productVersion: number; + availabiltyData: {stockLocationId: number; companyId: number}; picture?: any; - stockAvailability: number; onPress: () => void; } @@ -43,25 +48,79 @@ const ProductVariantCard = ({ style, name, code, - attributesList, + productId, + productVersion, + availabiltyData, picture, - stockAvailability, onPress, }: ProductVariantCardProps) => { const Colors = useThemeColor(); const I18n = useTranslator(); const formatMetaFile = useMetafileUri(); const formatNumber = useDigitFormat(); + const isMounted = useRef(true); + + const [attributes, setAttributesList] = useState< + ProductAttribut[] | undefined + >(); + const [availableStock, setAvailableStock] = useState(null); + + useEffect(() => { + isMounted.current = true; + + return () => { + isMounted.current = false; + }; + }, []); + + useEffect(() => { + if (productId != null) { + fetchVariantAttributes({ + productVariantId: productId, + version: productVersion, + }) + .then((res: any) => { + if (isMounted.current) { + setAttributesList(res?.data?.object?.attributes); + } + }) + .catch(() => { + if (isMounted.current) { + setAttributesList(null); + } + }); + } + }, [availabiltyData, productId, productVersion]); + + useEffect(() => { + if (productId != null) { + getProductStockIndicators({ + productId: productId, + version: productVersion, + ...availabiltyData, + }) + .then((res: any) => { + if (isMounted.current) { + setAvailableStock(res?.data?.object?.availableStock); + } + }) + .catch(() => { + if (isMounted.current) { + setAvailableStock(null); + } + }); + } + }, [availabiltyData, productId, productVersion]); const renderAttrItems = useCallback(() => { - if (!Array.isArray(attributesList?.attributes)) { + if (!Array.isArray(attributes)) { return null; } let items = []; - for (let index = 0; index < attributesList?.attributes.length; index++) { - const attr = attributesList?.attributes[index]; + for (let index = 0; index < attributes.length; index++) { + const attr = attributes[index]; if (attr != null) { items.push({ @@ -80,7 +139,7 @@ const ProductVariantCard = ({ } return items?.length > 0 ? {items} : null; - }, [I18n, attributesList?.attributes, formatNumber]); + }, [I18n, attributes, formatNumber]); return ( 0 + availableStock > 0 ? I18n.t('Stock_Available') : I18n.t('Stock_Unavailable'), - color: - stockAvailability > 0 ? Colors.primaryColor : Colors.errorColor, + color: availableStock > 0 ? Colors.successColor : Colors.errorColor, }, ], }} diff --git a/packages/apps/stock/src/features/asyncFunctions-index.ts b/packages/apps/stock/src/features/asyncFunctions-index.ts index 7c27357602..ac59c1874f 100644 --- a/packages/apps/stock/src/features/asyncFunctions-index.ts +++ b/packages/apps/stock/src/features/asyncFunctions-index.ts @@ -55,7 +55,6 @@ export { } from './inventorySlice'; export {filterClients, filterSuppliers} from './partnerSlice'; export { - fetchProductsAvailability, fetchProductDistribution, fetchProductIndicators, } from './productIndicatorsSlice'; @@ -65,10 +64,7 @@ export { updateProductLocker, } from './productSlice'; export {searchProductTrackingNumber} from './productTrackingNumberSlice'; -export { - fetchProductsAttributes, - fetchProductVariants, -} from './productVariantSlice'; +export {fetchProductVariants} from './productVariantSlice'; export {getRacks} from './racksListSlice'; export { fetchStockConfig, diff --git a/packages/apps/stock/src/features/productIndicatorsSlice.js b/packages/apps/stock/src/features/productIndicatorsSlice.js index 868f0f7304..30baa97a71 100644 --- a/packages/apps/stock/src/features/productIndicatorsSlice.js +++ b/packages/apps/stock/src/features/productIndicatorsSlice.js @@ -47,27 +47,6 @@ async function fetchData(data, {getState}) { return await getProductAvailabilty(data, {getState}); } -export const fetchProductsAvailability = createAsyncThunk( - 'product/fetchProductsAvailability', - async function (data, {getState}) { - let promises = []; - data.productList.forEach(product => { - promises.push( - fetchData( - { - productId: product.id, - companyId: data.companyId, - stockLocationId: data.stockLocationId, - version: product.version, - }, - {getState}, - ), - ); - }); - return Promise.all(promises); - }, -); - export const fetchProductDistribution = createAsyncThunk( 'product/fetchProductDistribution', async function (data, {getState}) { @@ -93,7 +72,6 @@ const initialState = { loading: false, loadingProductIndicators: false, productIndicators: {}, - listAvailabilty: [], listAvailabiltyDistribution: [], }; @@ -108,13 +86,6 @@ const productIndicators = createSlice({ state.loadingProductIndicators = false; state.productIndicators = action.payload; }); - builder.addCase(fetchProductsAvailability.pending, state => { - state.loading = true; - }); - builder.addCase(fetchProductsAvailability.fulfilled, (state, action) => { - state.loading = false; - state.listAvailabilty = action.payload; - }); builder.addCase(fetchProductDistribution.pending, state => { state.loading = true; }); diff --git a/packages/apps/stock/src/features/productVariantSlice.js b/packages/apps/stock/src/features/productVariantSlice.js index ce2da13fa4..72f642bc64 100644 --- a/packages/apps/stock/src/features/productVariantSlice.js +++ b/packages/apps/stock/src/features/productVariantSlice.js @@ -21,7 +21,7 @@ import { generateInifiniteScrollCases, handlerApiCall, } from '@axelor/aos-mobile-core'; -import {fetchVariantAttributes, fetchVariants} from '../api/product-api'; +import {fetchVariants} from '../api/product-api'; export const fetchProductVariants = createAsyncThunk( 'product/fetchProductVariant', @@ -36,44 +36,11 @@ export const fetchProductVariants = createAsyncThunk( }, ); -var getProductAttributes = async (data, {getState}) => { - return handlerApiCall({ - fetchFunction: fetchVariantAttributes, - data, - action: 'Stock_SliceAction_FetchProductVariantAttributes', - getState, - responseOptions: {isArrayResponse: true}, - }); -}; - -async function fetchData(data, {getState}) { - return await getProductAttributes(data, {getState}); -} - -export const fetchProductsAttributes = createAsyncThunk( - 'product/fetchProductsAttributes', - async function (data, {getState}) { - let promises = []; - data.productList.forEach(product => { - promises.push( - fetchData( - {productVariantId: product.id, version: product.version}, - {getState}, - ), - ); - }); - return Promise.all(promises); - }, -); - const initialState = { loadingProductList: false, moreLoading: false, isListEnd: false, productListVariables: [], - - loading: false, - listProductsAttributes: [], }; const productSlice = createSlice({ @@ -86,13 +53,6 @@ const productSlice = createSlice({ isListEnd: 'isListEnd', list: 'productListVariables', }); - builder.addCase(fetchProductsAttributes.pending, (state, action) => { - state.loading = true; - }); - builder.addCase(fetchProductsAttributes.fulfilled, (state, action) => { - state.loading = false; - state.listProductsAttributes = action.payload; - }); }, }); diff --git a/packages/apps/stock/src/screens/products/ProductListScreen.js b/packages/apps/stock/src/screens/products/ProductListScreen.js index 7c401727eb..d2846a6352 100644 --- a/packages/apps/stock/src/screens/products/ProductListScreen.js +++ b/packages/apps/stock/src/screens/products/ProductListScreen.js @@ -16,12 +16,11 @@ * along with this program. If not, see . */ -import React, {useEffect, useState, useCallback} from 'react'; +import React, {useState, useCallback} from 'react'; import {Screen, ScrollList, HeaderContainer} from '@axelor/aos-mobile-ui'; import {useDispatch, useSelector, useTranslator} from '@axelor/aos-mobile-core'; import {ProductCard, ProductSearchBar} from '../../components'; import {searchProducts} from '../../features/productSlice'; -import {fetchProductsAvailability} from '../../features/productIndicatorsSlice'; const productScanKey = 'product_product-list'; @@ -29,8 +28,6 @@ const ProductListScreen = ({navigation}) => { const I18n = useTranslator(); const dispatch = useDispatch(); - const {activeCompany} = useSelector(state => state.user.user); - const {listAvailabilty} = useSelector(state => state.productIndicators); const {loadingProduct, moreLoadingProduct, isListEndProduct, productList} = useSelector(state => state.product); @@ -58,18 +55,6 @@ const ProductListScreen = ({navigation}) => { [navigation], ); - useEffect(() => { - if (productList != null) { - dispatch( - fetchProductsAvailability({ - productList: productList, - companyId: activeCompany?.id, - stockLocationId: null, - }), - ); - } - }, [activeCompany, dispatch, productList]); - return ( { ( + renderItem={({item}) => ( showProductDetails(item)} /> )} diff --git a/packages/apps/stock/src/screens/products/ProductListVariantScreen.js b/packages/apps/stock/src/screens/products/ProductListVariantScreen.js index 7c350bdc57..b94a288bea 100644 --- a/packages/apps/stock/src/screens/products/ProductListVariantScreen.js +++ b/packages/apps/stock/src/screens/products/ProductListVariantScreen.js @@ -16,15 +16,11 @@ * along with this program. If not, see . */ -import React, {useCallback, useEffect, useMemo} from 'react'; +import React, {useCallback, useMemo} from 'react'; import {Screen, ScrollList} from '@axelor/aos-mobile-ui'; import {useDispatch, useSelector, useTranslator} from '@axelor/aos-mobile-core'; import {ProductVariantCard} from '../../components'; -import { - fetchProductsAttributes, - fetchProductVariants, -} from '../../features/productVariantSlice'; -import {fetchProductsAvailability} from '../../features/productIndicatorsSlice'; +import {fetchProductVariants} from '../../features/productVariantSlice'; const ProductListVariantScreen = ({route, navigation}) => { const product = route.params.product; @@ -34,14 +30,8 @@ const ProductListVariantScreen = ({route, navigation}) => { () => product?.parentProduct?.id, [product?.parentProduct?.id], ); - const { - loadingProductList, - moreLoading, - isListEnd, - productListVariables, - listProductsAttributes, - } = useSelector(state => state.productVariant); - const {listAvailabilty} = useSelector(state => state.productIndicators); + const {loadingProductList, moreLoading, isListEnd, productListVariables} = + useSelector(state => state.productVariant); const I18n = useTranslator(); const dispatch = useDispatch(); @@ -59,19 +49,6 @@ const ProductListVariantScreen = ({route, navigation}) => { [dispatch, parentProductId], ); - useEffect(() => { - if (productListVariables != null) { - dispatch( - fetchProductsAvailability({ - productList: productListVariables, - companyId: companyID, - stockLocationId: stockLocationId, - }), - ); - dispatch(fetchProductsAttributes({productList: productListVariables})); - } - }, [companyID, dispatch, productListVariables, stockLocationId]); - const navigateToProductVariable = productVar => { navigation.navigate('ProductStockDetailsScreen', {product: productVar}); }; @@ -81,18 +58,15 @@ const ProductListVariantScreen = ({route, navigation}) => { ( + renderItem={({item}) => ( navigateToProductVariable(item)} /> )}