Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
8b9b61c
LF-4980 Add filteredProductsSelector
SayakaOno Oct 10, 2025
fae28cd
LF-4980 Replace products selector for inventory
SayakaOno Oct 10, 2025
7985513
LF-4980 Add isLibraryProduct function
SayakaOno Oct 10, 2025
1497067
LF-4980 Use isLibraryProduct in products selector
SayakaOno Oct 10, 2025
4b7b938
LF-4980 Adjust products for SoilAmdnementProductForm read-only
SayakaOno Oct 10, 2025
f597a1b
LF-4980 Adjust new product name validation
SayakaOno Oct 10, 2025
70e9d7c
LF-4980 Adjust products selector for task creation
SayakaOno Oct 10, 2025
fb48e8d
LF-4980 Reset product form values when mounting SoilAmendmentProductCard
SayakaOno Oct 10, 2025
dcd6947
LF-4980 Add translation string
SayakaOno Oct 10, 2025
0bcaadc
LF-4980 Update Product type
SayakaOno Oct 10, 2025
f8158c3
LF-4980 Adjust productOptions generation in SoilAmendmentProductCard
SayakaOno Oct 10, 2025
8568d8f
LF-4980 Fix productOptions generation logic
SayakaOno Oct 15, 2025
731f927
LF-4980 Fix products selector factory and memoize selectors
SayakaOno Oct 16, 2025
b46644c
LF-4980 Tweaks
SayakaOno Oct 16, 2025
efc6632
LF-4980 Add missing includeLibrary default value in makeFilteredProdu…
SayakaOno Oct 16, 2025
02147f7
LF-4980 Update ProductForm's productsSelectorArgs
SayakaOno Oct 16, 2025
aa6e712
LF-4980 Improve makeFilteredProductsSelector
SayakaOno Oct 17, 2025
cacdef4
LF-4980 Fix productsSelectorArgs in TaskDetails
SayakaOno Oct 17, 2025
a390fbd
LF-4980 Add comment
SayakaOno Oct 17, 2025
87c7cfd
LF-4980 Add ProductInventorySelector and use it in ProductInventory
SayakaOno Oct 20, 2025
d63722f
LF-4980 Replace makeFilteredProductsSelector with global selector
SayakaOno Oct 20, 2025
6eda9e6
LF-4980 Update productsForTaskTypeSelector from factory to global sel…
SayakaOno Oct 20, 2025
0ab2718
Merge branch 'integration' into LF-4980/Show_valid_soil_amendment_pro…
SayakaOno Oct 23, 2025
abc5e21
LF-4980 Filter products in PureSoilAmendmentTask
SayakaOno Oct 23, 2025
3a692e6
LF-4980 Update hasAvailableProductsSelector to use productInventorySe…
SayakaOno Oct 23, 2025
4945585
LF-4980 Remove products filter in TaskDetails
SayakaOno Oct 23, 2025
845a2bd
LF-4980 Simplify customProductNames in PureSoilAmendmentProductForm
SayakaOno Oct 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/webapp/public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"QUANTITY": "Quantity",
"REMOVE": "Remove",
"REMOVE_ITEM": "Remove item",
"REMOVED": "Removed",
"REQUIRED": "Required",
"RETIRE": "Retire",
"REVISION_INFO": "Revised on <strong>{{date}}</strong> <i>by</i> <strong>{{user}}</strong>",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@
* GNU General Public License for more details, see <https://www.gnu.org/licenses/>.
*/

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';
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';
Expand Down Expand Up @@ -79,7 +80,25 @@ const PureSoilAmendmentProductForm = ({
}
}, [mode]);

const productNames: SoilAmendmentProduct['name'][] = products.map(({ name }) => name);
const { customProductsInInventory } = useMemo(() => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

libraryProductsOutsideInventory is it defined somewhere? I imagine that a user could duplicate a library product multiple times to tweak values. Is this in preparation for showing a library without the already duplicated library products?

Also is there a chance this will be reused and should this be a createSelector?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think users would be able to duplicate a library product directly. They would first add it to the inventory, then duplicate it.

I expect libraryProductsOutsideInventory to be used as the options to add to the inventory:

Screenshot 2025-10-22 at 11 59 20 AM

I can’t think of a use case where we’d want to reuse it right now... can you?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I am wrong but I think what I expect is that we always show allLibraryProducts instead of removing already added libraryProductsOutsideInventory.

How I understood library products was that the user can checkout their own copy and make edits to it as they choose? Adding a library product multiple times makes sense if the user is allowed to edit the values of the local copy. Maybe I misunderstood how library products work. At least currently, a library product is editable and so if I wanted the original unedited values I could not find it in libraryProductsOutsideInventory if it is removed.

Reuse: Maybe I was wishful thinking but I thought that this function looked generic enough for pest control and cleaning products too. Hopefully we don't have multiple product architecture for a long time!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least currently, a library product is editable

We haven’t handled library products yet, but eventually we’ll need to prevent them from being modified!

I removed libraryProductsOutsideInventory for now since it's hard to envision without the actual UI/UX. I could have created a function to generate customProductNames, but I'm leaving that for later too!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh we might need to align on that! I thought for sure the library products ARE modifiable .. as a copy of the core library product. Anyways thanks for removing for now as we discuss!

// 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 };
}, [products]);

const customProductNames = customProductsInInventory.map(({ name }) => name);

return (
<div className={styles.soilAmendmentProductForm}>
Expand All @@ -95,7 +114,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');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -126,9 +133,17 @@ const SoilAmendmentProductCard = ({
const productId = getValues(PRODUCT_ID);
const purposes = watch(PURPOSES);

const initialProductId = useRef(productId);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I show it in the video but this changes on render.


const selectRef = useRef<SelectRef>(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 }) => {
if (rest.removed && product_id !== initialProductId.current) {
return [];
}

const label = name + (rest.removed ? ` (${t('common:REMOVED')})` : '');

return { value: product_id, label, data: rest };
});

useEffect(() => {
Expand All @@ -137,6 +152,11 @@ const SoilAmendmentProductCard = ({
}
}, [otherPurposeId]);

useEffect(() => {
const selectedProduct = findProduct(products, productId);
nestedFormMethods.reset(getSoilAmendmentFormValues(selectedProduct));
}, []);

return (
<div className={styles.productCard}>
{!isReadOnly && onRemove && (
Expand All @@ -154,7 +174,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')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ 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 { productsForTaskTypeSelector } from '../../productSlice';
import { TASK_TYPES } from '../../Task/constants';
import { FormMode } from '..';
import { FormContentProps } from '.';

const taskType = { task_translation_key: TASK_TYPES.SOIL_AMENDMENT.toUpperCase() };

export default function SoilAmendmentProductForm({ mode, productId }: FormContentProps) {
const { t } = useTranslation();

Expand All @@ -38,12 +40,9 @@ 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 =
/* @ts-expect-error https://github.com/reduxjs/reselect/issues/550#issuecomment-999701108 */
useSelector((state) => productsForTaskTypeSelector(state, taskType)) || [];

const isReadOnly = mode === FormMode.READ_ONLY;

Expand All @@ -53,7 +52,7 @@ export default function SoilAmendmentProductForm({ mode, productId }: FormConten
isReadOnly={isReadOnly}
farm={{ farm_id, interested, country_id }}
fertiliserTypeOptions={fertiliserTypeOptions}
products={soilAmendmentCustomProducts}
products={soilAmendmentProducts}
productId={productId}
/>
);
Expand Down
4 changes: 2 additions & 2 deletions packages/webapp/src/containers/ProductInventory/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 { productInventorySelector } from '../productSlice';
import { Product, SoilAmendmentProduct } from '../../store/api/types';
import { TASK_TYPES } from '../Task/constants';
import { SearchProps } from '../../components/Animals/Inventory';
Expand Down Expand Up @@ -65,7 +65,7 @@ export default function ProductInventory() {
dispatch(getProducts());
}, []);

const productInventory = useSelector(productsSelector);
const productInventory = useSelector(productInventorySelector);

const inventory = productInventory
.filter((product) => product.type === TASK_TYPES.SOIL_AMENDMENT)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
6 changes: 4 additions & 2 deletions packages/webapp/src/containers/Task/TaskDetails/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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((state) => productsForTaskTypeSelector(state, selectedTaskType));
const productsInInventory = products?.filter((product) => !product.removed);

const managementPlanIds = persistedFormData.managementPlans?.map(
({ management_plan_id }) => management_plan_id,
);
Expand Down Expand Up @@ -64,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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
21 changes: 13 additions & 8 deletions packages/webapp/src/containers/productSlice.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, [
Expand Down Expand Up @@ -55,24 +56,28 @@ 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 }) => {
return products.filter((product) => product.farm_id === farm_id);
},
);

export const productsForTaskTypeSelector = (taskType) => {
return createSelector([productSelectors.selectAll, loginSelector], (products, { farm_id }) => {
// Select farm products for a given type including removed ones
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);
});

export const productEntitiesSelector = productSelectors.selectEntities;

Expand Down
1 change: 1 addition & 0 deletions packages/webapp/src/store/api/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const products = [
k: 3,
elemental_unit: ElementalUnit.PERCENT,
},
removed: false,
},
{
product_id: 2,
Expand All @@ -45,6 +46,7 @@ export const products = [
k: 3.3,
elemental_unit: ElementalUnit.RATIO,
},
removed: false,
},
{
product_id: 3,
Expand All @@ -60,6 +62,7 @@ export const products = [
k: undefined,
elemental_unit: undefined,
},
removed: false,
},
{
product_id: 4,
Expand All @@ -75,6 +78,7 @@ export const products = [
k: 1,
elemental_unit: ElementalUnit.PERCENT,
},
removed: false,
},
{
product_id: 5,
Expand All @@ -90,6 +94,7 @@ export const products = [
k: 1,
elemental_unit: ElementalUnit.PERCENT,
},
removed: false,
},
{
product_id: 6,
Expand All @@ -105,5 +110,6 @@ export const products = [
k: undefined,
elemental_unit: ElementalUnit.RATIO,
},
removed: false,
},
];
21 changes: 21 additions & 0 deletions packages/webapp/src/util/product.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/

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;
};