From 8b9b61c1f77c996b64ba4dccd73494dec9cd4215 Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Fri, 10 Oct 2025 10:36:45 -0700 Subject: [PATCH 01/26] LF-4980 Add filteredProductsSelector --- .../webapp/src/containers/productSlice.js | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/webapp/src/containers/productSlice.js b/packages/webapp/src/containers/productSlice.js index 99a906b7ee..9f4548913d 100644 --- a/packages/webapp/src/containers/productSlice.js +++ b/packages/webapp/src/containers/productSlice.js @@ -62,6 +62,27 @@ export const productsSelector = createSelector( }, ); +// Filters products by library/custom, task type, farm, and removed status +export const filteredProductsSelector = ({ + includeLibrary = true, + includeCustom = true, + type = '', + farm = false, + includeRemoved = false, +}) => + createSelector([productSelectors.selectAll, loginSelector], (products, { farm_id }) => { + return products.filter((product) => { + const isLibraryProduct = !!product.product_translation_key; + + const matchesIsLibrary = isLibraryProduct ? includeLibrary : includeCustom; + const matchesType = !type || product.type === type; + const matchesFarm = !farm || product.farm_id === farm_id; + const matchesRemoved = includeRemoved || !product.removed; + + return matchesIsLibrary && matchesType && matchesFarm && matchesRemoved; + }); + }); + export const productsForTaskTypeSelector = (taskType) => { return createSelector([productSelectors.selectAll, loginSelector], (products, { farm_id }) => { if (taskType === undefined) { From fae28cdbeba6a72ddf5cdae6b9c3f1ba35347c56 Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Fri, 10 Oct 2025 10:41:51 -0700 Subject: [PATCH 02/26] LF-4980 Replace products selector for inventory --- packages/webapp/src/containers/ProductInventory/index.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/webapp/src/containers/ProductInventory/index.tsx b/packages/webapp/src/containers/ProductInventory/index.tsx index 8b926a10dd..bc7dbeef5f 100644 --- a/packages/webapp/src/containers/ProductInventory/index.tsx +++ b/packages/webapp/src/containers/ProductInventory/index.tsx @@ -24,7 +24,7 @@ import { ReactComponent as BookIcon } from '../../assets/images/book-closed.svg' import useSearchFilter from '../../containers/hooks/useSearchFilter'; import PureProductInventory from '../../components/ProductInventory'; import { getProducts } from '../Task/saga'; -import { productsSelector } from '../productSlice'; +import { filteredProductsSelector } from '../productSlice'; import { Product, SoilAmendmentProduct } from '../../store/api/types'; import { TASK_TYPES } from '../Task/constants'; import { SearchProps } from '../../components/Animals/Inventory'; @@ -65,10 +65,11 @@ export default function ProductInventory() { dispatch(getProducts()); }, []); - const productInventory = useSelector(productsSelector); + const productInventory = useSelector( + filteredProductsSelector({ farm: true, type: TASK_TYPES.SOIL_AMENDMENT }), + ); const inventory = productInventory - .filter((product) => product.type === TASK_TYPES.SOIL_AMENDMENT) /* Table requires each array object to have an "id" key */ .map((product) => ({ ...product, From 798551372d7846c8d14398f42af786820b21a274 Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Fri, 10 Oct 2025 10:56:23 -0700 Subject: [PATCH 03/26] LF-4980 Add isLibraryProduct function --- packages/webapp/src/util/product.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 packages/webapp/src/util/product.ts diff --git a/packages/webapp/src/util/product.ts b/packages/webapp/src/util/product.ts new file mode 100644 index 0000000000..8270ae85af --- /dev/null +++ b/packages/webapp/src/util/product.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2025 LiteFarm.org + * This file is part of LiteFarm. + * + * LiteFarm is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LiteFarm is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details, see . + */ + +import { Product } from '../store/api/types'; + +export const isLibraryProduct = (product: Product) => { + return !!product.product_translation_key; +}; From 1497067854cac806fef3d1d41debede44febcc94 Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Fri, 10 Oct 2025 10:57:44 -0700 Subject: [PATCH 04/26] LF-4980 Use isLibraryProduct in products selector --- packages/webapp/src/containers/productSlice.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/webapp/src/containers/productSlice.js b/packages/webapp/src/containers/productSlice.js index 9f4548913d..4c4421dfeb 100644 --- a/packages/webapp/src/containers/productSlice.js +++ b/packages/webapp/src/containers/productSlice.js @@ -2,6 +2,7 @@ import { createEntityAdapter, createSlice } from '@reduxjs/toolkit'; import { loginSelector, onLoadingFail, onLoadingStart } from './userFarmSlice'; import { pick } from '../util/pick'; import { createSelector } from 'reselect'; +import { isLibraryProduct } from '../util/product'; export const getProduct = (obj) => { return pick(obj, [ @@ -72,9 +73,7 @@ export const filteredProductsSelector = ({ }) => createSelector([productSelectors.selectAll, loginSelector], (products, { farm_id }) => { return products.filter((product) => { - const isLibraryProduct = !!product.product_translation_key; - - const matchesIsLibrary = isLibraryProduct ? includeLibrary : includeCustom; + const matchesIsLibrary = isLibraryProduct(product) ? includeLibrary : includeCustom; const matchesType = !type || product.type === type; const matchesFarm = !farm || product.farm_id === farm_id; const matchesRemoved = includeRemoved || !product.removed; From 4b7b938181c92e308f18fafbd8fb7fe7b1fb48f9 Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Fri, 10 Oct 2025 11:07:41 -0700 Subject: [PATCH 05/26] LF-4980 Adjust products for SoilAmdnementProductForm read-only --- .../ProductForm/SoilAmendmentProductForm.tsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/webapp/src/containers/ProductInventory/ProductForm/SoilAmendmentProductForm.tsx b/packages/webapp/src/containers/ProductInventory/ProductForm/SoilAmendmentProductForm.tsx index 99ddf71150..abfffb20b9 100644 --- a/packages/webapp/src/containers/ProductInventory/ProductForm/SoilAmendmentProductForm.tsx +++ b/packages/webapp/src/containers/ProductInventory/ProductForm/SoilAmendmentProductForm.tsx @@ -18,7 +18,7 @@ import { useGetSoilAmendmentFertiliserTypesQuery } from '../../../store/api/apiS import PureSoilAmendmentProductForm from '../../../components/ProductInventory/ProductForm/PureSoilAmendmentProductForm'; import { userFarmSelector } from '../../userFarmSlice'; import { certifierSurveySelector } from '../../OrganicCertifierSurvey/slice'; -import { productsSelector } from '../../productSlice'; +import { filteredProductsSelector } from '../../productSlice'; import { TASK_TYPES } from '../../Task/constants'; import { FormMode } from '..'; import { FormContentProps } from '.'; @@ -38,11 +38,8 @@ export default function SoilAmendmentProductForm({ mode, productId }: FormConten label: t(`ADD_PRODUCT.${key}_FERTILISER`), })); - const products = useSelector(productsSelector); - - // TODO: Filter out removed products - const soilAmendmentCustomProducts = products.filter( - (product) => product.type === TASK_TYPES.SOIL_AMENDMENT, + const soilAmendmentProducts = useSelector( + filteredProductsSelector({ type: TASK_TYPES.SOIL_AMENDMENT }), ); const isReadOnly = mode === FormMode.READ_ONLY; @@ -53,7 +50,7 @@ export default function SoilAmendmentProductForm({ mode, productId }: FormConten isReadOnly={isReadOnly} farm={{ farm_id, interested, country_id }} fertiliserTypeOptions={fertiliserTypeOptions} - products={soilAmendmentCustomProducts} + products={soilAmendmentProducts} productId={productId} /> ); From f597a1be5cd1c69b8faad30f2c86eb4a51a2fe71 Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Fri, 10 Oct 2025 11:08:46 -0700 Subject: [PATCH 06/26] LF-4980 Adjust new product name validation --- .../ProductForm/PureSoilAmendmentProductForm.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/webapp/src/components/ProductInventory/ProductForm/PureSoilAmendmentProductForm.tsx b/packages/webapp/src/components/ProductInventory/ProductForm/PureSoilAmendmentProductForm.tsx index 0bc3363b6f..592e12ae7c 100644 --- a/packages/webapp/src/components/ProductInventory/ProductForm/PureSoilAmendmentProductForm.tsx +++ b/packages/webapp/src/components/ProductInventory/ProductForm/PureSoilAmendmentProductForm.tsx @@ -20,6 +20,7 @@ import Input, { getInputErrors } from '../../Form/Input'; import ProductDetails, { type ProductDetailsProps } from '../../Form/ProductDetails'; import { hookFormMaxCharsValidation } from '../../Form/hookformValidationUtils'; import { getSoilAmendmentFormValues } from '../../Form/ProductDetails/utils'; +import { isLibraryProduct } from '../../../util/product'; import { productDefaultValuesByType } from '../../../containers/ProductInventory/ProductForm/constants'; import { TASK_TYPES } from '../../../containers/Task/constants'; import { PRODUCT_FIELD_NAMES } from '../../Task/AddSoilAmendmentProducts/types'; @@ -79,7 +80,9 @@ const PureSoilAmendmentProductForm = ({ } }, [mode]); - const productNames: SoilAmendmentProduct['name'][] = products.map(({ name }) => name); + const customProductNames: SoilAmendmentProduct['name'][] = products + .filter((product) => !isLibraryProduct(product)) + .map(({ name }) => name); return (
@@ -95,7 +98,7 @@ const PureSoilAmendmentProductForm = ({ // Allow duplicate check to pass if keeping the original name during edit if ( !(mode === FormMode.EDIT && value === product?.name) && - productNames.includes(value) + customProductNames.includes(value) ) { return t('ADD_TASK.DUPLICATE_NAME'); } From 70e9d7ce142b7cc669f4264a5f62da0fa354cde9 Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Fri, 10 Oct 2025 11:22:45 -0700 Subject: [PATCH 07/26] LF-4980 Adjust products selector for task creation --- packages/webapp/src/containers/Task/TaskDetails/index.jsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/webapp/src/containers/Task/TaskDetails/index.jsx b/packages/webapp/src/containers/Task/TaskDetails/index.jsx index 32556505f6..f33359754a 100644 --- a/packages/webapp/src/containers/Task/TaskDetails/index.jsx +++ b/packages/webapp/src/containers/Task/TaskDetails/index.jsx @@ -4,7 +4,7 @@ import { useEffect } from 'react'; import { useLocation, useHistory } from 'react-router-dom'; import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import { getProducts } from '../saga'; -import { productsForTaskTypeSelector } from '../../productSlice'; +import { filteredProductsSelector } from '../../productSlice'; import { taskTypeSelector } from '../../taskTypeSlice'; import { hookFormPersistSelector } from '../../hooks/useHookFormPersist/hookFormPersistSlice'; import { userFarmSelector } from '../../userFarmSlice'; @@ -28,7 +28,9 @@ function TaskDetails() { const { interested, farm_id } = useSelector(certifierSurveySelector, shallowEqual); const persistedFormData = useSelector(hookFormPersistSelector); const selectedTaskType = useSelector(taskTypeSelector(persistedFormData.task_type_id)); - const products = useSelector(productsForTaskTypeSelector(selectedTaskType)); + const products = useSelector( + filteredProductsSelector({ type: selectedTaskType.task_translation_key?.toLowerCase() }), + ); const managementPlanIds = persistedFormData.managementPlans?.map( ({ management_plan_id }) => management_plan_id, ); From fb48e8dc71d9cb6438190143a770f58eda951db8 Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Fri, 10 Oct 2025 12:57:40 -0700 Subject: [PATCH 08/26] LF-4980 Reset product form values when mounting SoilAmendmentProductCard --- .../AddSoilAmendmentProducts/ProductCard/index.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/webapp/src/components/Task/AddSoilAmendmentProducts/ProductCard/index.tsx b/packages/webapp/src/components/Task/AddSoilAmendmentProducts/ProductCard/index.tsx index 010fa6179c..e93c5e33c1 100644 --- a/packages/webapp/src/components/Task/AddSoilAmendmentProducts/ProductCard/index.tsx +++ b/packages/webapp/src/components/Task/AddSoilAmendmentProducts/ProductCard/index.tsx @@ -33,6 +33,13 @@ import { hookFormMaxCharsValidation } from '../../../Form/hookformValidationUtil import { soilAmendmentProductDetailsDefaultValues } from '../../../../containers/ProductInventory/ProductForm/constants'; import { getSoilAmendmentFormValues } from '../../../Form/ProductDetails/utils'; +const findProduct = ( + products?: SoilAmendmentProduct[], + productId?: SoilAmendmentProduct['product_id'], +) => { + return products?.find(({ product_id }) => product_id === productId); +}; + export type ProductCardProps = ProductDetailsProps & { namePrefix: string; system: 'metric' | 'imperial'; @@ -137,6 +144,11 @@ const SoilAmendmentProductCard = ({ } }, [otherPurposeId]); + useEffect(() => { + const selectedProduct = findProduct(products, productId); + nestedFormMethods.reset(getSoilAmendmentFormValues(selectedProduct)); + }, []); + return (
{!isReadOnly && onRemove && ( @@ -154,7 +166,7 @@ const SoilAmendmentProductCard = ({ options={productOptions} onChange={(e) => { onChange(e?.value); - const selectedProduct = products.find(({ product_id }) => product_id === e?.value); + const selectedProduct = findProduct(products, e?.value); nestedFormMethods.reset(getSoilAmendmentFormValues(selectedProduct)); }} placeholder={t('ADD_PRODUCT.PRESS_ENTER')} From dcd6947640ae7434d0ccd1c58c32532b2f88bcfd Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Fri, 10 Oct 2025 13:27:06 -0700 Subject: [PATCH 09/26] LF-4980 Add translation string --- packages/webapp/public/locales/en/common.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/webapp/public/locales/en/common.json b/packages/webapp/public/locales/en/common.json index b8fca7199c..a71ed790dc 100644 --- a/packages/webapp/public/locales/en/common.json +++ b/packages/webapp/public/locales/en/common.json @@ -79,6 +79,7 @@ "QUANTITY": "Quantity", "REMOVE": "Remove", "REMOVE_ITEM": "Remove item", + "REMOVED": "Removed", "REQUIRED": "Required", "RETIRE": "Retire", "REVISION_INFO": "Revised on {{date}} by {{user}}", From 0bcaadc4126a9dc4009bc437c6c8f6d873777255 Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Fri, 10 Oct 2025 13:34:53 -0700 Subject: [PATCH 10/26] LF-4980 Update Product type --- packages/webapp/src/store/api/types/index.ts | 1 + .../stories/Pages/Task/AddSoilAmendmentProducts/products.ts | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/packages/webapp/src/store/api/types/index.ts b/packages/webapp/src/store/api/types/index.ts index fa0046ad34..5f04b1d33c 100644 --- a/packages/webapp/src/store/api/types/index.ts +++ b/packages/webapp/src/store/api/types/index.ts @@ -202,6 +202,7 @@ export interface Product { | typeof TASK_TYPES.PEST_CONTROL; farm_id?: string; on_permitted_substances_list?: 'YES' | 'NO' | 'NOT_SURE' | null; + removed: boolean; } export enum ElementalUnit { diff --git a/packages/webapp/src/stories/Pages/Task/AddSoilAmendmentProducts/products.ts b/packages/webapp/src/stories/Pages/Task/AddSoilAmendmentProducts/products.ts index 9be9816cd6..bd71430455 100644 --- a/packages/webapp/src/stories/Pages/Task/AddSoilAmendmentProducts/products.ts +++ b/packages/webapp/src/stories/Pages/Task/AddSoilAmendmentProducts/products.ts @@ -30,6 +30,7 @@ export const products = [ k: 3, elemental_unit: ElementalUnit.PERCENT, }, + removed: false, }, { product_id: 2, @@ -45,6 +46,7 @@ export const products = [ k: 3.3, elemental_unit: ElementalUnit.RATIO, }, + removed: false, }, { product_id: 3, @@ -60,6 +62,7 @@ export const products = [ k: undefined, elemental_unit: undefined, }, + removed: false, }, { product_id: 4, @@ -75,6 +78,7 @@ export const products = [ k: 1, elemental_unit: ElementalUnit.PERCENT, }, + removed: false, }, { product_id: 5, @@ -90,6 +94,7 @@ export const products = [ k: 1, elemental_unit: ElementalUnit.PERCENT, }, + removed: false, }, { product_id: 6, @@ -105,5 +110,6 @@ export const products = [ k: undefined, elemental_unit: ElementalUnit.RATIO, }, + removed: false, }, ]; From f8158c3e4fe93348b29523afdc12fb8a01e24901 Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Fri, 10 Oct 2025 13:39:58 -0700 Subject: [PATCH 11/26] LF-4980 Adjust productOptions generation in SoilAmendmentProductCard --- .../AddSoilAmendmentProducts/ProductCard/index.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/webapp/src/components/Task/AddSoilAmendmentProducts/ProductCard/index.tsx b/packages/webapp/src/components/Task/AddSoilAmendmentProducts/ProductCard/index.tsx index e93c5e33c1..00858a973a 100644 --- a/packages/webapp/src/components/Task/AddSoilAmendmentProducts/ProductCard/index.tsx +++ b/packages/webapp/src/components/Task/AddSoilAmendmentProducts/ProductCard/index.tsx @@ -134,8 +134,15 @@ const SoilAmendmentProductCard = ({ const purposes = watch(PURPOSES); const selectRef = useRef(null); - const productOptions = products.map(({ product_id, name, ...rest }) => { - return { value: product_id, label: name, data: rest }; + const productOptions = products.flatMap(({ product_id, name, ...rest }) => { + const { removed } = rest; + if (removed && product_id !== productId) { + return []; + } + + const label = name + (removed ? ` (${t('common:REMOVED')})` : ''); + + return { value: product_id, label, isDisabled: removed, data: rest }; }); useEffect(() => { From 8568d8f45165982d73ff15efc72e05dbbab942e8 Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Wed, 15 Oct 2025 12:38:37 -0700 Subject: [PATCH 12/26] LF-4980 Fix productOptions generation logic --- .../Task/AddSoilAmendmentProducts/ProductCard/index.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/webapp/src/components/Task/AddSoilAmendmentProducts/ProductCard/index.tsx b/packages/webapp/src/components/Task/AddSoilAmendmentProducts/ProductCard/index.tsx index 00858a973a..219ce27b05 100644 --- a/packages/webapp/src/components/Task/AddSoilAmendmentProducts/ProductCard/index.tsx +++ b/packages/webapp/src/components/Task/AddSoilAmendmentProducts/ProductCard/index.tsx @@ -133,16 +133,17 @@ const SoilAmendmentProductCard = ({ const productId = getValues(PRODUCT_ID); const purposes = watch(PURPOSES); + const initialProductId = useRef(productId); + const selectRef = useRef(null); const productOptions = products.flatMap(({ product_id, name, ...rest }) => { - const { removed } = rest; - if (removed && product_id !== productId) { + if (rest.removed && product_id !== initialProductId.current) { return []; } - const label = name + (removed ? ` (${t('common:REMOVED')})` : ''); + const label = name + (rest.removed ? ` (${t('common:REMOVED')})` : ''); - return { value: product_id, label, isDisabled: removed, data: rest }; + return { value: product_id, label, data: rest }; }); useEffect(() => { From 731f92750d461439af32cf596005cae71189d147 Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Thu, 16 Oct 2025 10:35:32 -0700 Subject: [PATCH 13/26] LF-4980 Fix products selector factory and memoize selectors --- .../ProductForm/SoilAmendmentProductForm.tsx | 11 +++++-- .../src/containers/ProductInventory/index.tsx | 11 +++++-- .../src/containers/Task/TaskDetails/index.jsx | 13 +++++--- .../webapp/src/containers/productSlice.js | 33 ++++++++++--------- 4 files changed, 42 insertions(+), 26 deletions(-) diff --git a/packages/webapp/src/containers/ProductInventory/ProductForm/SoilAmendmentProductForm.tsx b/packages/webapp/src/containers/ProductInventory/ProductForm/SoilAmendmentProductForm.tsx index abfffb20b9..57d332dfb1 100644 --- a/packages/webapp/src/containers/ProductInventory/ProductForm/SoilAmendmentProductForm.tsx +++ b/packages/webapp/src/containers/ProductInventory/ProductForm/SoilAmendmentProductForm.tsx @@ -12,17 +12,20 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details, see . */ +import { useMemo } from 'react'; import { shallowEqual, useSelector } from 'react-redux'; import { useTranslation } from 'react-i18next'; import { useGetSoilAmendmentFertiliserTypesQuery } from '../../../store/api/apiSlice'; import PureSoilAmendmentProductForm from '../../../components/ProductInventory/ProductForm/PureSoilAmendmentProductForm'; import { userFarmSelector } from '../../userFarmSlice'; import { certifierSurveySelector } from '../../OrganicCertifierSurvey/slice'; -import { filteredProductsSelector } from '../../productSlice'; +import { makeFilteredProductsSelector } from '../../productSlice'; import { TASK_TYPES } from '../../Task/constants'; import { FormMode } from '..'; import { FormContentProps } from '.'; +const productsSelectorArgs = { type: TASK_TYPES.SOIL_AMENDMENT }; + export default function SoilAmendmentProductForm({ mode, productId }: FormContentProps) { const { t } = useTranslation(); @@ -38,8 +41,10 @@ export default function SoilAmendmentProductForm({ mode, productId }: FormConten label: t(`ADD_PRODUCT.${key}_FERTILISER`), })); - const soilAmendmentProducts = useSelector( - filteredProductsSelector({ type: TASK_TYPES.SOIL_AMENDMENT }), + const productsSelector = useMemo(() => makeFilteredProductsSelector(), []); + const soilAmendmentProducts = useSelector((state) => + /* @ts-expect-error https://github.com/reduxjs/reselect/issues/550#issuecomment-999701108 */ + productsSelector(state, productsSelectorArgs), ); const isReadOnly = mode === FormMode.READ_ONLY; diff --git a/packages/webapp/src/containers/ProductInventory/index.tsx b/packages/webapp/src/containers/ProductInventory/index.tsx index bc7dbeef5f..c677c99899 100644 --- a/packages/webapp/src/containers/ProductInventory/index.tsx +++ b/packages/webapp/src/containers/ProductInventory/index.tsx @@ -24,7 +24,7 @@ import { ReactComponent as BookIcon } from '../../assets/images/book-closed.svg' import useSearchFilter from '../../containers/hooks/useSearchFilter'; import PureProductInventory from '../../components/ProductInventory'; import { getProducts } from '../Task/saga'; -import { filteredProductsSelector } from '../productSlice'; +import { makeFilteredProductsSelector } from '../productSlice'; import { Product, SoilAmendmentProduct } from '../../store/api/types'; import { TASK_TYPES } from '../Task/constants'; import { SearchProps } from '../../components/Animals/Inventory'; @@ -34,6 +34,9 @@ import { CellKind } from '../../components/Table/types'; import ProductForm from './ProductForm'; import { isFilterCurrentlyActiveSelector, resetInventoryFilter } from '../filterSlice'; import { useFilteredInventory } from './useFilteredInventory'; +import { RootState } from '../../store/store'; + +const productsSelectorArgs = { farm: true, type: TASK_TYPES.SOIL_AMENDMENT }; export type TableProduct = SoilAmendmentProduct & { id: Extract; @@ -65,8 +68,10 @@ export default function ProductInventory() { dispatch(getProducts()); }, []); - const productInventory = useSelector( - filteredProductsSelector({ farm: true, type: TASK_TYPES.SOIL_AMENDMENT }), + const productsSelector = useMemo(() => makeFilteredProductsSelector(), []); + const productInventory = useSelector((state: RootState) => + /* @ts-expect-error https://github.com/reduxjs/reselect/issues/550#issuecomment-999701108 */ + productsSelector(state, productsSelectorArgs), ); const inventory = productInventory diff --git a/packages/webapp/src/containers/Task/TaskDetails/index.jsx b/packages/webapp/src/containers/Task/TaskDetails/index.jsx index f33359754a..b7342897e7 100644 --- a/packages/webapp/src/containers/Task/TaskDetails/index.jsx +++ b/packages/webapp/src/containers/Task/TaskDetails/index.jsx @@ -1,10 +1,10 @@ import PureTaskDetails from '../../../components/Task/PureTaskDetails'; import { HookFormPersistProvider } from '../../hooks/useHookFormPersist/HookFormPersistProvider'; -import { useEffect } from 'react'; +import { useEffect, useMemo } from 'react'; import { useLocation, useHistory } from 'react-router-dom'; import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import { getProducts } from '../saga'; -import { filteredProductsSelector } from '../../productSlice'; +import { makeFilteredProductsSelector } from '../../productSlice'; import { taskTypeSelector } from '../../taskTypeSlice'; import { hookFormPersistSelector } from '../../hooks/useHookFormPersist/hookFormPersistSlice'; import { userFarmSelector } from '../../userFarmSlice'; @@ -28,9 +28,14 @@ function TaskDetails() { const { interested, farm_id } = useSelector(certifierSurveySelector, shallowEqual); const persistedFormData = useSelector(hookFormPersistSelector); const selectedTaskType = useSelector(taskTypeSelector(persistedFormData.task_type_id)); - const products = useSelector( - filteredProductsSelector({ type: selectedTaskType.task_translation_key?.toLowerCase() }), + const productsSelector = useMemo(() => makeFilteredProductsSelector(), []); + const productsSelectorArgs = useMemo( + () => ({ type: selectedTaskType.task_translation_key?.toLowerCase() }), + [selectedTaskType.task_translation_key], ); + /* @ts-expect-error https://github.com/reduxjs/reselect/issues/550#issuecomment-999701108 */ + const products = useSelector((state) => productsSelector(state, productsSelectorArgs)); + const managementPlanIds = persistedFormData.managementPlans?.map( ({ management_plan_id }) => management_plan_id, ); diff --git a/packages/webapp/src/containers/productSlice.js b/packages/webapp/src/containers/productSlice.js index 4c4421dfeb..1aa6650e01 100644 --- a/packages/webapp/src/containers/productSlice.js +++ b/packages/webapp/src/containers/productSlice.js @@ -64,23 +64,24 @@ export const productsSelector = createSelector( ); // Filters products by library/custom, task type, farm, and removed status -export const filteredProductsSelector = ({ - includeLibrary = true, - includeCustom = true, - type = '', - farm = false, - includeRemoved = false, -}) => - createSelector([productSelectors.selectAll, loginSelector], (products, { farm_id }) => { - return products.filter((product) => { - const matchesIsLibrary = isLibraryProduct(product) ? includeLibrary : includeCustom; - const matchesType = !type || product.type === type; - const matchesFarm = !farm || product.farm_id === farm_id; - const matchesRemoved = includeRemoved || !product.removed; +export const makeFilteredProductsSelector = () => + createSelector( + [productSelectors.selectAll, loginSelector, (_state, args) => args], + ( + products, + { farm_id }, + { includeLibrary, includeCustom = true, type = '', farm = false, includeRemoved = false }, + ) => { + return products.filter((product) => { + const matchesIsLibrary = isLibraryProduct(product) ? includeLibrary : includeCustom; + const matchesType = !type || product.type === type; + const matchesFarm = !farm || product.farm_id === farm_id; + const matchesRemoved = includeRemoved || !product.removed; - return matchesIsLibrary && matchesType && matchesFarm && matchesRemoved; - }); - }); + return matchesIsLibrary && matchesType && matchesFarm && matchesRemoved; + }); + }, + ); export const productsForTaskTypeSelector = (taskType) => { return createSelector([productSelectors.selectAll, loginSelector], (products, { farm_id }) => { From b46644ca74c4db4f76d6f31159308d92f0cdd289 Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Thu, 16 Oct 2025 10:52:41 -0700 Subject: [PATCH 14/26] LF-4980 Tweaks --- .../PureSoilAmendmentProductForm.tsx | 2 +- .../webapp/src/containers/productSlice.js | 28 ++++++++++--------- packages/webapp/src/util/product.ts | 1 + 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/webapp/src/components/ProductInventory/ProductForm/PureSoilAmendmentProductForm.tsx b/packages/webapp/src/components/ProductInventory/ProductForm/PureSoilAmendmentProductForm.tsx index 592e12ae7c..f5058a548a 100644 --- a/packages/webapp/src/components/ProductInventory/ProductForm/PureSoilAmendmentProductForm.tsx +++ b/packages/webapp/src/components/ProductInventory/ProductForm/PureSoilAmendmentProductForm.tsx @@ -80,7 +80,7 @@ const PureSoilAmendmentProductForm = ({ } }, [mode]); - const customProductNames: SoilAmendmentProduct['name'][] = products + const customProductNames = products .filter((product) => !isLibraryProduct(product)) .map(({ name }) => name); diff --git a/packages/webapp/src/containers/productSlice.js b/packages/webapp/src/containers/productSlice.js index 1aa6650e01..492ceb1430 100644 --- a/packages/webapp/src/containers/productSlice.js +++ b/packages/webapp/src/containers/productSlice.js @@ -56,6 +56,7 @@ const productSelectors = productAdapter.getSelectors( (state) => state.entitiesReducer[productSlice.name], ); +// Select farm products including removed ones export const productsSelector = createSelector( [productSelectors.selectAll, loginSelector], (products, { farm_id }) => { @@ -63,7 +64,20 @@ export const productsSelector = createSelector( }, ); -// Filters products by library/custom, task type, farm, and removed status +// Select farm products for a given type including removed ones +export const productsForTaskTypeSelector = (taskType) => { + return createSelector([productSelectors.selectAll, loginSelector], (products, { farm_id }) => { + if (taskType === undefined) { + return undefined; + } + return products.filter( + (product) => + product.farm_id === farm_id && product.type === taskType.task_translation_key.toLowerCase(), + ); + }); +}; + +// Select products filtered by library/custom, type, farm, and removed status export const makeFilteredProductsSelector = () => createSelector( [productSelectors.selectAll, loginSelector, (_state, args) => args], @@ -83,18 +97,6 @@ export const makeFilteredProductsSelector = () => }, ); -export const productsForTaskTypeSelector = (taskType) => { - return createSelector([productSelectors.selectAll, loginSelector], (products, { farm_id }) => { - if (taskType === undefined) { - return undefined; - } - return products.filter( - (product) => - product.farm_id === farm_id && product.type === taskType.task_translation_key.toLowerCase(), - ); - }); -}; - export const productEntitiesSelector = productSelectors.selectEntities; export const productSelector = (product_id) => (state) => diff --git a/packages/webapp/src/util/product.ts b/packages/webapp/src/util/product.ts index 8270ae85af..75869cd980 100644 --- a/packages/webapp/src/util/product.ts +++ b/packages/webapp/src/util/product.ts @@ -15,6 +15,7 @@ import { Product } from '../store/api/types'; +// LF-4963 - confirm property that will distinguish custom from library products export const isLibraryProduct = (product: Product) => { return !!product.product_translation_key; }; From efc663281a7782d84566d16f438b21fd88dddc53 Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Thu, 16 Oct 2025 13:12:17 -0700 Subject: [PATCH 15/26] LF-4980 Add missing includeLibrary default value in makeFilteredProductsSelector --- packages/webapp/src/containers/productSlice.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/webapp/src/containers/productSlice.js b/packages/webapp/src/containers/productSlice.js index 492ceb1430..85ef58853d 100644 --- a/packages/webapp/src/containers/productSlice.js +++ b/packages/webapp/src/containers/productSlice.js @@ -84,7 +84,13 @@ export const makeFilteredProductsSelector = () => ( products, { farm_id }, - { includeLibrary, includeCustom = true, type = '', farm = false, includeRemoved = false }, + { + includeLibrary = true, + includeCustom = true, + type = '', + farm = false, + includeRemoved = false, + }, ) => { return products.filter((product) => { const matchesIsLibrary = isLibraryProduct(product) ? includeLibrary : includeCustom; From 02147f7b945591ab47070de98da222db1ef97a1d Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Thu, 16 Oct 2025 13:38:14 -0700 Subject: [PATCH 16/26] LF-4980 Update ProductForm's productsSelectorArgs filter products in PureSoilAmendmentProductForm --- .../PureSoilAmendmentProductForm.tsx | 23 +++++++++++++++---- .../ProductForm/SoilAmendmentProductForm.tsx | 2 +- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/webapp/src/components/ProductInventory/ProductForm/PureSoilAmendmentProductForm.tsx b/packages/webapp/src/components/ProductInventory/ProductForm/PureSoilAmendmentProductForm.tsx index f5058a548a..3f996d021e 100644 --- a/packages/webapp/src/components/ProductInventory/ProductForm/PureSoilAmendmentProductForm.tsx +++ b/packages/webapp/src/components/ProductInventory/ProductForm/PureSoilAmendmentProductForm.tsx @@ -13,7 +13,7 @@ * GNU General Public License for more details, see . */ -import { useEffect } from 'react'; +import { useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useFormContext } from 'react-hook-form'; import Input, { getInputErrors } from '../../Form/Input'; @@ -80,9 +80,24 @@ const PureSoilAmendmentProductForm = ({ } }, [mode]); - const customProductNames = products - .filter((product) => !isLibraryProduct(product)) - .map(({ name }) => name); + const { customProductsInInventory } = useMemo(() => { + const libraryProductsOutsideInventory: SoilAmendmentProduct[] = []; + const customProductsInInventory: SoilAmendmentProduct[] = []; + + products.forEach((product) => { + if (isLibraryProduct(product)) { + if (!product.farm_id || product.removed) { + libraryProductsOutsideInventory.push(product); + } + } else if (!product.removed) { + customProductsInInventory.push(product); + } + }); + + return { libraryProductsOutsideInventory, customProductsInInventory }; + }, [products]); + + const customProductNames = customProductsInInventory.map(({ name }) => name); return (
diff --git a/packages/webapp/src/containers/ProductInventory/ProductForm/SoilAmendmentProductForm.tsx b/packages/webapp/src/containers/ProductInventory/ProductForm/SoilAmendmentProductForm.tsx index 57d332dfb1..350bdb0ac2 100644 --- a/packages/webapp/src/containers/ProductInventory/ProductForm/SoilAmendmentProductForm.tsx +++ b/packages/webapp/src/containers/ProductInventory/ProductForm/SoilAmendmentProductForm.tsx @@ -24,7 +24,7 @@ import { TASK_TYPES } from '../../Task/constants'; import { FormMode } from '..'; import { FormContentProps } from '.'; -const productsSelectorArgs = { type: TASK_TYPES.SOIL_AMENDMENT }; +const productsSelectorArgs = { type: TASK_TYPES.SOIL_AMENDMENT, includeRemoved: true }; export default function SoilAmendmentProductForm({ mode, productId }: FormContentProps) { const { t } = useTranslation(); From aa6e71299579ffdc69d42f0bd62768e3bad93d8c Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Fri, 17 Oct 2025 08:09:56 -0700 Subject: [PATCH 17/26] LF-4980 Improve makeFilteredProductsSelector --- packages/webapp/src/containers/ProductInventory/index.tsx | 2 +- packages/webapp/src/containers/productSlice.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/webapp/src/containers/ProductInventory/index.tsx b/packages/webapp/src/containers/ProductInventory/index.tsx index c677c99899..6c940de2db 100644 --- a/packages/webapp/src/containers/ProductInventory/index.tsx +++ b/packages/webapp/src/containers/ProductInventory/index.tsx @@ -36,7 +36,7 @@ import { isFilterCurrentlyActiveSelector, resetInventoryFilter } from '../filter import { useFilteredInventory } from './useFilteredInventory'; import { RootState } from '../../store/store'; -const productsSelectorArgs = { farm: true, type: TASK_TYPES.SOIL_AMENDMENT }; +const productsSelectorArgs = { filterByFarm: true, type: TASK_TYPES.SOIL_AMENDMENT }; export type TableProduct = SoilAmendmentProduct & { id: Extract; diff --git a/packages/webapp/src/containers/productSlice.js b/packages/webapp/src/containers/productSlice.js index 85ef58853d..5baad61d6d 100644 --- a/packages/webapp/src/containers/productSlice.js +++ b/packages/webapp/src/containers/productSlice.js @@ -88,14 +88,14 @@ export const makeFilteredProductsSelector = () => includeLibrary = true, includeCustom = true, type = '', - farm = false, + filterByFarm = false, includeRemoved = false, }, ) => { return products.filter((product) => { const matchesIsLibrary = isLibraryProduct(product) ? includeLibrary : includeCustom; const matchesType = !type || product.type === type; - const matchesFarm = !farm || product.farm_id === farm_id; + const matchesFarm = !filterByFarm || product.farm_id === farm_id; const matchesRemoved = includeRemoved || !product.removed; return matchesIsLibrary && matchesType && matchesFarm && matchesRemoved; From cacdef436d3402d6fb7e8ec5a262d97f05705984 Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Fri, 17 Oct 2025 08:10:51 -0700 Subject: [PATCH 18/26] LF-4980 Fix productsSelectorArgs in TaskDetails --- packages/webapp/src/containers/Task/TaskDetails/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webapp/src/containers/Task/TaskDetails/index.jsx b/packages/webapp/src/containers/Task/TaskDetails/index.jsx index b7342897e7..51233d1040 100644 --- a/packages/webapp/src/containers/Task/TaskDetails/index.jsx +++ b/packages/webapp/src/containers/Task/TaskDetails/index.jsx @@ -30,7 +30,7 @@ function TaskDetails() { const selectedTaskType = useSelector(taskTypeSelector(persistedFormData.task_type_id)); const productsSelector = useMemo(() => makeFilteredProductsSelector(), []); const productsSelectorArgs = useMemo( - () => ({ type: selectedTaskType.task_translation_key?.toLowerCase() }), + () => ({ type: selectedTaskType.task_translation_key?.toLowerCase(), filterByFarm: true }), [selectedTaskType.task_translation_key], ); /* @ts-expect-error https://github.com/reduxjs/reselect/issues/550#issuecomment-999701108 */ From a390fbd8da220773ac3064d46f8df2608a28dd39 Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Fri, 17 Oct 2025 08:11:08 -0700 Subject: [PATCH 19/26] LF-4980 Add comment --- .../ProductForm/PureSoilAmendmentProductForm.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/webapp/src/components/ProductInventory/ProductForm/PureSoilAmendmentProductForm.tsx b/packages/webapp/src/components/ProductInventory/ProductForm/PureSoilAmendmentProductForm.tsx index 3f996d021e..a212c687c2 100644 --- a/packages/webapp/src/components/ProductInventory/ProductForm/PureSoilAmendmentProductForm.tsx +++ b/packages/webapp/src/components/ProductInventory/ProductForm/PureSoilAmendmentProductForm.tsx @@ -81,6 +81,7 @@ const PureSoilAmendmentProductForm = ({ }, [mode]); const { customProductsInInventory } = useMemo(() => { + // LF-4963 - library product select options const libraryProductsOutsideInventory: SoilAmendmentProduct[] = []; const customProductsInInventory: SoilAmendmentProduct[] = []; From 87c7cfd4427909fcb8e7ac4a1626788d68a2422b Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Mon, 20 Oct 2025 10:20:36 -0700 Subject: [PATCH 20/26] LF-4980 Add ProductInventorySelector and use it in ProductInventory --- .../webapp/src/containers/ProductInventory/index.tsx | 12 +++--------- packages/webapp/src/containers/productSlice.js | 4 ++++ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/webapp/src/containers/ProductInventory/index.tsx b/packages/webapp/src/containers/ProductInventory/index.tsx index 6c940de2db..c502eacd84 100644 --- a/packages/webapp/src/containers/ProductInventory/index.tsx +++ b/packages/webapp/src/containers/ProductInventory/index.tsx @@ -24,7 +24,7 @@ import { ReactComponent as BookIcon } from '../../assets/images/book-closed.svg' import useSearchFilter from '../../containers/hooks/useSearchFilter'; import PureProductInventory from '../../components/ProductInventory'; import { getProducts } from '../Task/saga'; -import { makeFilteredProductsSelector } from '../productSlice'; +import { productInventorySelector } from '../productSlice'; import { Product, SoilAmendmentProduct } from '../../store/api/types'; import { TASK_TYPES } from '../Task/constants'; import { SearchProps } from '../../components/Animals/Inventory'; @@ -34,9 +34,6 @@ import { CellKind } from '../../components/Table/types'; import ProductForm from './ProductForm'; import { isFilterCurrentlyActiveSelector, resetInventoryFilter } from '../filterSlice'; import { useFilteredInventory } from './useFilteredInventory'; -import { RootState } from '../../store/store'; - -const productsSelectorArgs = { filterByFarm: true, type: TASK_TYPES.SOIL_AMENDMENT }; export type TableProduct = SoilAmendmentProduct & { id: Extract; @@ -68,13 +65,10 @@ export default function ProductInventory() { dispatch(getProducts()); }, []); - const productsSelector = useMemo(() => makeFilteredProductsSelector(), []); - const productInventory = useSelector((state: RootState) => - /* @ts-expect-error https://github.com/reduxjs/reselect/issues/550#issuecomment-999701108 */ - productsSelector(state, productsSelectorArgs), - ); + const productInventory = useSelector(productInventorySelector); const inventory = productInventory + .filter((product) => product.type === TASK_TYPES.SOIL_AMENDMENT) /* Table requires each array object to have an "id" key */ .map((product) => ({ ...product, diff --git a/packages/webapp/src/containers/productSlice.js b/packages/webapp/src/containers/productSlice.js index 5baad61d6d..c620468061 100644 --- a/packages/webapp/src/containers/productSlice.js +++ b/packages/webapp/src/containers/productSlice.js @@ -77,6 +77,10 @@ export const productsForTaskTypeSelector = (taskType) => { }); }; +export const productInventorySelector = createSelector([productsSelector], (products) => { + return products.filter((product) => !product.removed); +}); + // Select products filtered by library/custom, type, farm, and removed status export const makeFilteredProductsSelector = () => createSelector( From d63722f416d2f0f4130ca6c02a70446a83639236 Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Mon, 20 Oct 2025 10:37:46 -0700 Subject: [PATCH 21/26] LF-4980 Replace makeFilteredProductsSelector with global selector --- .../ProductForm/SoilAmendmentProductForm.tsx | 11 +++----- .../src/containers/Task/TaskDetails/index.jsx | 15 ++++------- .../webapp/src/containers/productSlice.js | 26 ------------------- 3 files changed, 8 insertions(+), 44 deletions(-) diff --git a/packages/webapp/src/containers/ProductInventory/ProductForm/SoilAmendmentProductForm.tsx b/packages/webapp/src/containers/ProductInventory/ProductForm/SoilAmendmentProductForm.tsx index 350bdb0ac2..816136e873 100644 --- a/packages/webapp/src/containers/ProductInventory/ProductForm/SoilAmendmentProductForm.tsx +++ b/packages/webapp/src/containers/ProductInventory/ProductForm/SoilAmendmentProductForm.tsx @@ -12,19 +12,18 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details, see . */ -import { useMemo } from 'react'; import { shallowEqual, useSelector } from 'react-redux'; import { useTranslation } from 'react-i18next'; import { useGetSoilAmendmentFertiliserTypesQuery } from '../../../store/api/apiSlice'; import PureSoilAmendmentProductForm from '../../../components/ProductInventory/ProductForm/PureSoilAmendmentProductForm'; import { userFarmSelector } from '../../userFarmSlice'; import { certifierSurveySelector } from '../../OrganicCertifierSurvey/slice'; -import { makeFilteredProductsSelector } from '../../productSlice'; +import { productsForTaskTypeSelector } from '../../productSlice'; import { TASK_TYPES } from '../../Task/constants'; import { FormMode } from '..'; import { FormContentProps } from '.'; -const productsSelectorArgs = { type: TASK_TYPES.SOIL_AMENDMENT, includeRemoved: true }; +const taskType = { task_translation_key: TASK_TYPES.SOIL_AMENDMENT.toUpperCase() }; export default function SoilAmendmentProductForm({ mode, productId }: FormContentProps) { const { t } = useTranslation(); @@ -41,11 +40,7 @@ export default function SoilAmendmentProductForm({ mode, productId }: FormConten label: t(`ADD_PRODUCT.${key}_FERTILISER`), })); - const productsSelector = useMemo(() => makeFilteredProductsSelector(), []); - const soilAmendmentProducts = useSelector((state) => - /* @ts-expect-error https://github.com/reduxjs/reselect/issues/550#issuecomment-999701108 */ - productsSelector(state, productsSelectorArgs), - ); + const soilAmendmentProducts = useSelector(productsForTaskTypeSelector(taskType)) || []; const isReadOnly = mode === FormMode.READ_ONLY; diff --git a/packages/webapp/src/containers/Task/TaskDetails/index.jsx b/packages/webapp/src/containers/Task/TaskDetails/index.jsx index 51233d1040..1faf27c2a5 100644 --- a/packages/webapp/src/containers/Task/TaskDetails/index.jsx +++ b/packages/webapp/src/containers/Task/TaskDetails/index.jsx @@ -1,10 +1,10 @@ import PureTaskDetails from '../../../components/Task/PureTaskDetails'; import { HookFormPersistProvider } from '../../hooks/useHookFormPersist/HookFormPersistProvider'; -import { useEffect, useMemo } from 'react'; +import { useEffect } from 'react'; import { useLocation, useHistory } from 'react-router-dom'; import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import { getProducts } from '../saga'; -import { makeFilteredProductsSelector } from '../../productSlice'; +import { productsForTaskTypeSelector } from '../../productSlice'; import { taskTypeSelector } from '../../taskTypeSlice'; import { hookFormPersistSelector } from '../../hooks/useHookFormPersist/hookFormPersistSlice'; import { userFarmSelector } from '../../userFarmSlice'; @@ -28,13 +28,8 @@ function TaskDetails() { const { interested, farm_id } = useSelector(certifierSurveySelector, shallowEqual); const persistedFormData = useSelector(hookFormPersistSelector); const selectedTaskType = useSelector(taskTypeSelector(persistedFormData.task_type_id)); - const productsSelector = useMemo(() => makeFilteredProductsSelector(), []); - const productsSelectorArgs = useMemo( - () => ({ type: selectedTaskType.task_translation_key?.toLowerCase(), filterByFarm: true }), - [selectedTaskType.task_translation_key], - ); - /* @ts-expect-error https://github.com/reduxjs/reselect/issues/550#issuecomment-999701108 */ - const products = useSelector((state) => productsSelector(state, productsSelectorArgs)); + const products = useSelector(productsForTaskTypeSelector(selectedTaskType)); + const productsInInventory = products?.filter((product) => !product.removed); const managementPlanIds = persistedFormData.managementPlans?.map( ({ management_plan_id }) => management_plan_id, @@ -71,7 +66,7 @@ function TaskDetails() { persistedPaths={persistedPaths} selectedTaskType={selectedTaskType} system={system} - products={products} + products={productsInInventory} farm={{ farm_id, country_id, interested }} managementPlanByLocations={managementPlanByLocations} wildManagementPlanTiles={showWildCrops && wildManagementPlanTiles} diff --git a/packages/webapp/src/containers/productSlice.js b/packages/webapp/src/containers/productSlice.js index c620468061..e0639f17d8 100644 --- a/packages/webapp/src/containers/productSlice.js +++ b/packages/webapp/src/containers/productSlice.js @@ -81,32 +81,6 @@ export const productInventorySelector = createSelector([productsSelector], (prod return products.filter((product) => !product.removed); }); -// Select products filtered by library/custom, type, farm, and removed status -export const makeFilteredProductsSelector = () => - createSelector( - [productSelectors.selectAll, loginSelector, (_state, args) => args], - ( - products, - { farm_id }, - { - includeLibrary = true, - includeCustom = true, - type = '', - filterByFarm = false, - includeRemoved = false, - }, - ) => { - return products.filter((product) => { - const matchesIsLibrary = isLibraryProduct(product) ? includeLibrary : includeCustom; - const matchesType = !type || product.type === type; - const matchesFarm = !filterByFarm || product.farm_id === farm_id; - const matchesRemoved = includeRemoved || !product.removed; - - return matchesIsLibrary && matchesType && matchesFarm && matchesRemoved; - }); - }, - ); - export const productEntitiesSelector = productSelectors.selectEntities; export const productSelector = (product_id) => (state) => From 6eda9e6bc5a73dc88e6c9ab2133a4778bf1c1e4e Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Mon, 20 Oct 2025 11:01:16 -0700 Subject: [PATCH 22/26] LF-4980 Update productsForTaskTypeSelector from factory to global selector --- .../ProductForm/SoilAmendmentProductForm.tsx | 4 +++- .../src/containers/Task/TaskComplete/StepOne.jsx | 2 +- .../src/containers/Task/TaskDetails/index.jsx | 2 +- .../src/containers/Task/TaskReadOnly/index.jsx | 2 +- packages/webapp/src/containers/productSlice.js | 14 ++++++-------- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/webapp/src/containers/ProductInventory/ProductForm/SoilAmendmentProductForm.tsx b/packages/webapp/src/containers/ProductInventory/ProductForm/SoilAmendmentProductForm.tsx index 816136e873..ed632e599e 100644 --- a/packages/webapp/src/containers/ProductInventory/ProductForm/SoilAmendmentProductForm.tsx +++ b/packages/webapp/src/containers/ProductInventory/ProductForm/SoilAmendmentProductForm.tsx @@ -40,7 +40,9 @@ export default function SoilAmendmentProductForm({ mode, productId }: FormConten label: t(`ADD_PRODUCT.${key}_FERTILISER`), })); - const soilAmendmentProducts = useSelector(productsForTaskTypeSelector(taskType)) || []; + const soilAmendmentProducts = + /* @ts-expect-error https://github.com/reduxjs/reselect/issues/550#issuecomment-999701108 */ + useSelector((state) => productsForTaskTypeSelector(state, taskType)) || []; const isReadOnly = mode === FormMode.READ_ONLY; diff --git a/packages/webapp/src/containers/Task/TaskComplete/StepOne.jsx b/packages/webapp/src/containers/Task/TaskComplete/StepOne.jsx index 524e5c2b90..82bff3b602 100644 --- a/packages/webapp/src/containers/Task/TaskComplete/StepOne.jsx +++ b/packages/webapp/src/containers/Task/TaskComplete/StepOne.jsx @@ -22,7 +22,7 @@ function TaskCompleteStepOne() { const { task_id } = useParams(); const task = useSelector(taskWithProductSelector(task_id)); const selectedTaskType = task?.taskType; - const products = useSelector(productsForTaskTypeSelector(selectedTaskType)); + const products = useSelector((state) => productsForTaskTypeSelector(state, selectedTaskType)); const persistedPaths = [`/tasks/${task_id}/complete`]; const onContinue = (data) => { diff --git a/packages/webapp/src/containers/Task/TaskDetails/index.jsx b/packages/webapp/src/containers/Task/TaskDetails/index.jsx index 1faf27c2a5..ad76e7bddb 100644 --- a/packages/webapp/src/containers/Task/TaskDetails/index.jsx +++ b/packages/webapp/src/containers/Task/TaskDetails/index.jsx @@ -28,7 +28,7 @@ function TaskDetails() { const { interested, farm_id } = useSelector(certifierSurveySelector, shallowEqual); const persistedFormData = useSelector(hookFormPersistSelector); const selectedTaskType = useSelector(taskTypeSelector(persistedFormData.task_type_id)); - const products = useSelector(productsForTaskTypeSelector(selectedTaskType)); + const products = useSelector((state) => productsForTaskTypeSelector(state, selectedTaskType)); const productsInInventory = products?.filter((product) => !product.removed); const managementPlanIds = persistedFormData.managementPlans?.map( diff --git a/packages/webapp/src/containers/Task/TaskReadOnly/index.jsx b/packages/webapp/src/containers/Task/TaskReadOnly/index.jsx index 398ecee42a..4312c2bb9c 100644 --- a/packages/webapp/src/containers/Task/TaskReadOnly/index.jsx +++ b/packages/webapp/src/containers/Task/TaskReadOnly/index.jsx @@ -52,7 +52,7 @@ function TaskReadOnly() { const system = useSelector(measurementSelector); const task = useReadonlyTask(task_id); const selectedTaskType = task?.taskType; - const products = useSelector(productsForTaskTypeSelector(selectedTaskType)); + const products = useSelector((state) => productsForTaskTypeSelector(state, selectedTaskType)); const isIrrigationTaskWithExternalPrescription = isTaskType(selectedTaskType, 'IRRIGATION_TASK') && task?.irrigation_task?.irrigation_prescription_external_id != null; diff --git a/packages/webapp/src/containers/productSlice.js b/packages/webapp/src/containers/productSlice.js index e0639f17d8..a2a840bcf9 100644 --- a/packages/webapp/src/containers/productSlice.js +++ b/packages/webapp/src/containers/productSlice.js @@ -65,17 +65,15 @@ export const productsSelector = createSelector( ); // Select farm products for a given type including removed ones -export const productsForTaskTypeSelector = (taskType) => { - return createSelector([productSelectors.selectAll, loginSelector], (products, { farm_id }) => { +export const productsForTaskTypeSelector = createSelector( + [productsSelector, (_state, taskType) => taskType], + (products, taskType) => { if (taskType === undefined) { return undefined; } - return products.filter( - (product) => - product.farm_id === farm_id && product.type === taskType.task_translation_key.toLowerCase(), - ); - }); -}; + return products.filter(({ type }) => type === taskType.task_translation_key.toLowerCase()); + }, +); export const productInventorySelector = createSelector([productsSelector], (products) => { return products.filter((product) => !product.removed); From abc5e21746e4ba4e8c4532c7ae0ee6c885e2a43e Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Thu, 23 Oct 2025 11:09:38 -0700 Subject: [PATCH 23/26] LF-4980 Filter products in PureSoilAmendmentTask update productOptions generation in ProductCard --- .../ProductCard/index.tsx | 9 +-------- .../components/Task/SoilAmendmentTask/index.tsx | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/webapp/src/components/Task/AddSoilAmendmentProducts/ProductCard/index.tsx b/packages/webapp/src/components/Task/AddSoilAmendmentProducts/ProductCard/index.tsx index b583712006..4fa90de0b6 100644 --- a/packages/webapp/src/components/Task/AddSoilAmendmentProducts/ProductCard/index.tsx +++ b/packages/webapp/src/components/Task/AddSoilAmendmentProducts/ProductCard/index.tsx @@ -133,16 +133,9 @@ const SoilAmendmentProductCard = ({ const productId = getValues(PRODUCT_ID); const purposes = watch(PURPOSES); - const initialProductId = useRef(productId); - const selectRef = useRef(null); - const productOptions = products.flatMap(({ product_id, name, ...rest }) => { - if (rest.removed && product_id !== initialProductId.current) { - return []; - } - + const productOptions = products.map(({ product_id, name, ...rest }) => { const label = name + (rest.removed ? ` (${t('common:REMOVED')})` : ''); - return { value: product_id, label, data: rest }; }); diff --git a/packages/webapp/src/components/Task/SoilAmendmentTask/index.tsx b/packages/webapp/src/components/Task/SoilAmendmentTask/index.tsx index 597fe3328d..a528b6daba 100644 --- a/packages/webapp/src/components/Task/SoilAmendmentTask/index.tsx +++ b/packages/webapp/src/components/Task/SoilAmendmentTask/index.tsx @@ -33,10 +33,22 @@ import { furrow_hole_depth } from '../../../util/convert-units/unit'; import styles from './styles.module.scss'; import { locationsSelector } from '../../../containers/locationSlice'; +// Return products in inventory plus removed ones already used in the task. +const getAvailableProducts = ( + usedProductsInTask: SoilAmendmentProduct[] | undefined, + products: SoilAmendmentProduct[], +) => { + const usedProductIds = new Set(usedProductsInTask?.map(({ product_id }) => product_id)); + return products.filter((product) => !product.removed || usedProductIds.has(product.product_id)); +}; + type PureSoilAmendmentTaskProps = UseFormReturn & Pick & { disabled: boolean; - task?: { locations: { location_id: number }[] }; + task?: { + locations: { location_id: number }[]; + soil_amendment_task_products: SoilAmendmentProduct[]; + }; locations: { location_id: number }[]; }; @@ -158,7 +170,7 @@ const PureSoilAmendmentTask = ({ Date: Thu, 23 Oct 2025 11:21:59 -0700 Subject: [PATCH 24/26] LF-4980 Update hasAvailableProductsSelector to use productInventorySelector --- packages/webapp/src/containers/productSlice.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/webapp/src/containers/productSlice.js b/packages/webapp/src/containers/productSlice.js index 54aa55f8cf..5d05d2ecef 100644 --- a/packages/webapp/src/containers/productSlice.js +++ b/packages/webapp/src/containers/productSlice.js @@ -80,9 +80,9 @@ export const productInventorySelector = createSelector([productsSelector], (prod }); export const hasAvailableProductsSelector = createSelector( - [productsSelector, (_state, type) => type], - (products, type) => { - return products.some((product) => !product.removed && (!type || product.type === type)); + [productInventorySelector, (_state, type) => type], + (productInventory, type) => { + return productInventory.some((product) => !type || product.type === type); }, ); From 494558564db6942294580aacb6dee72e1a88e00e Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Thu, 23 Oct 2025 11:29:50 -0700 Subject: [PATCH 25/26] LF-4980 Remove products filter in TaskDetails --- packages/webapp/src/containers/Task/TaskDetails/index.jsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/webapp/src/containers/Task/TaskDetails/index.jsx b/packages/webapp/src/containers/Task/TaskDetails/index.jsx index ad76e7bddb..7228a7fd09 100644 --- a/packages/webapp/src/containers/Task/TaskDetails/index.jsx +++ b/packages/webapp/src/containers/Task/TaskDetails/index.jsx @@ -29,8 +29,6 @@ function TaskDetails() { const persistedFormData = useSelector(hookFormPersistSelector); const selectedTaskType = useSelector(taskTypeSelector(persistedFormData.task_type_id)); const products = useSelector((state) => productsForTaskTypeSelector(state, selectedTaskType)); - const productsInInventory = products?.filter((product) => !product.removed); - const managementPlanIds = persistedFormData.managementPlans?.map( ({ management_plan_id }) => management_plan_id, ); @@ -66,7 +64,7 @@ function TaskDetails() { persistedPaths={persistedPaths} selectedTaskType={selectedTaskType} system={system} - products={productsInInventory} + products={products} farm={{ farm_id, country_id, interested }} managementPlanByLocations={managementPlanByLocations} wildManagementPlanTiles={showWildCrops && wildManagementPlanTiles} From 845a2bda47b345395445cce9a9b5e63976a883aa Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Thu, 23 Oct 2025 11:55:44 -0700 Subject: [PATCH 26/26] LF-4980 Simplify customProductNames in PureSoilAmendmentProductForm --- .../PureSoilAmendmentProductForm.tsx | 22 ++++--------------- 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/packages/webapp/src/components/ProductInventory/ProductForm/PureSoilAmendmentProductForm.tsx b/packages/webapp/src/components/ProductInventory/ProductForm/PureSoilAmendmentProductForm.tsx index a212c687c2..e605a4ea95 100644 --- a/packages/webapp/src/components/ProductInventory/ProductForm/PureSoilAmendmentProductForm.tsx +++ b/packages/webapp/src/components/ProductInventory/ProductForm/PureSoilAmendmentProductForm.tsx @@ -80,26 +80,12 @@ const PureSoilAmendmentProductForm = ({ } }, [mode]); - const { customProductsInInventory } = useMemo(() => { - // LF-4963 - library product select options - const libraryProductsOutsideInventory: SoilAmendmentProduct[] = []; - const customProductsInInventory: SoilAmendmentProduct[] = []; - - products.forEach((product) => { - if (isLibraryProduct(product)) { - if (!product.farm_id || product.removed) { - libraryProductsOutsideInventory.push(product); - } - } else if (!product.removed) { - customProductsInInventory.push(product); - } - }); - - return { libraryProductsOutsideInventory, customProductsInInventory }; + const customProductNames = useMemo(() => { + return products + .filter((product) => !product.removed && !isLibraryProduct(product)) + .map(({ name }) => name); }, [products]); - const customProductNames = customProductsInInventory.map(({ name }) => name); - return (
{/* @ts-expect-error */}