From 8aec50498ad35b0526466d4aea0be04cf52f9131 Mon Sep 17 00:00:00 2001 From: Duncan-Brain Date: Wed, 11 Dec 2024 13:41:34 -0500 Subject: [PATCH 01/18] LF-4583 Move the formatTranslationKey function to utility vs controller --- packages/api/src/controllers/baseController.js | 9 --------- .../api/src/controllers/farmExpenseTypeController.js | 5 +++-- packages/api/src/controllers/revenueTypeController.js | 5 +++-- packages/api/src/util/util.js | 9 +++++++++ 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/api/src/controllers/baseController.js b/packages/api/src/controllers/baseController.js index 1b6e78018b..6239a8dd17 100644 --- a/packages/api/src/controllers/baseController.js +++ b/packages/api/src/controllers/baseController.js @@ -206,15 +206,6 @@ export default { return await model.query(trx).eager(subModel); }, - /** - * Format transaltion key - * @param {String} key - * @returns {String} - Formatted key - */ - formatTranslationKey(key) { - return key.toUpperCase().trim().replaceAll(' ', '_'); - }, - /** * To check if record is deleted or not * @param {Object} trx - Transaction object diff --git a/packages/api/src/controllers/farmExpenseTypeController.js b/packages/api/src/controllers/farmExpenseTypeController.js index 507365d958..3768c81ada 100644 --- a/packages/api/src/controllers/farmExpenseTypeController.js +++ b/packages/api/src/controllers/farmExpenseTypeController.js @@ -18,6 +18,7 @@ import baseController from '../controllers/baseController.js'; import ExpenseTypeModel from '../models/expenseTypeModel.js'; import FarmExpenseModel from '../models/farmExpenseModel.js'; import { transaction, Model } from 'objection'; +import { formatTranslationKey } from '../util/util.js'; const farmExpenseTypeController = { addFarmExpenseType() { @@ -26,7 +27,7 @@ const farmExpenseTypeController = { try { const farm_id = req.headers.farm_id; const data = req.body; - data.expense_translation_key = baseController.formatTranslationKey(data.expense_name); + data.expense_translation_key = formatTranslationKey(data.expense_name); //prevent empty strings data.custom_description = data.custom_description || null; @@ -168,7 +169,7 @@ const farmExpenseTypeController = { return res.status(409).send(); } - data.expense_translation_key = baseController.formatTranslationKey(data.expense_name); + data.expense_translation_key = formatTranslationKey(data.expense_name); //prevent empty strings data.custom_description = data.custom_description || null; diff --git a/packages/api/src/controllers/revenueTypeController.js b/packages/api/src/controllers/revenueTypeController.js index c716b58b0f..278e5cad92 100644 --- a/packages/api/src/controllers/revenueTypeController.js +++ b/packages/api/src/controllers/revenueTypeController.js @@ -18,6 +18,7 @@ import baseController from './baseController.js'; import RevenueTypeModel from '../models/revenueTypeModel.js'; import SaleModel from '../models/saleModel.js'; import { transaction, Model } from 'objection'; +import { formatTranslationKey } from '../util/util.js'; const revenueTypeController = { addType() { @@ -26,7 +27,7 @@ const revenueTypeController = { try { const farm_id = req.headers.farm_id; const data = req.body; - data.revenue_translation_key = baseController.formatTranslationKey(data.revenue_name); + data.revenue_translation_key = formatTranslationKey(data.revenue_name); //prevent empty strings data.custom_description = data.custom_description || null; @@ -181,7 +182,7 @@ const revenueTypeController = { return res.status(409).send(); } - data.revenue_translation_key = baseController.formatTranslationKey(data.revenue_name); + data.revenue_translation_key = formatTranslationKey(data.revenue_name); const result = await baseController.patch(RevenueTypeModel, revenue_type_id, data, req, { trx, diff --git a/packages/api/src/util/util.js b/packages/api/src/util/util.js index aac384222a..38012fe832 100644 --- a/packages/api/src/util/util.js +++ b/packages/api/src/util/util.js @@ -54,3 +54,12 @@ export const checkAndTrimString = (input) => { } return input.trim(); }; + +/** + * Format transaltion key + * @param {String} key + * @returns {String} - Formatted key + */ +export const formatTranslationKey = (key) => { + return key.toUpperCase().trim().replaceAll(' ', '_'); +}; From 40e857a636c5b6eaf6a70129524f4d27bcc1cd23 Mon Sep 17 00:00:00 2001 From: Duncan-Brain Date: Wed, 11 Dec 2024 13:45:25 -0500 Subject: [PATCH 02/18] LF-4583 Add tests for duplicate custom types and breeds --- packages/api/tests/animal.test.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/api/tests/animal.test.js b/packages/api/tests/animal.test.js index 4b70c99774..8390bff50e 100644 --- a/packages/api/tests/animal.test.js +++ b/packages/api/tests/animal.test.js @@ -1264,6 +1264,31 @@ describe('Animal Tests', () => { message: 'Default breed must use default type', }, }, + { + testName: 'Cannot create a duplicate custom type', + getPostBody: (customs) => [ + { + type_name: customs.customAnimalType.type, + }, + ], + postErr: { + code: 409, + message: 'Animal type already exists', + }, + }, + { + testName: 'Cannot create a duplicate custom breed', + getPostBody: (customs) => [ + { + default_type_id: customs.customAnimalBreed.default_type_id, + breed_name: customs.customAnimalBreed.breed, + }, + ], + postErr: { + code: 409, + message: 'Animal breed already exists', + }, + }, ], EDIT: [ { From e1b7e8d9f730b0ee10b3e405703a1c00356a5c19 Mon Sep 17 00:00:00 2001 From: Duncan-Brain Date: Wed, 11 Dec 2024 13:49:34 -0500 Subject: [PATCH 03/18] LF-4583 Refactor duplicates checks --- .../validation/checkAnimalOrBatch.js | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/packages/api/src/middleware/validation/checkAnimalOrBatch.js b/packages/api/src/middleware/validation/checkAnimalOrBatch.js index 6e55dfb6ee..feacc4950e 100644 --- a/packages/api/src/middleware/validation/checkAnimalOrBatch.js +++ b/packages/api/src/middleware/validation/checkAnimalOrBatch.js @@ -32,6 +32,7 @@ import CustomAnimalBreedModel from '../../models/customAnimalBreedModel.js'; import AnimalUseModel from '../../models/animalUseModel.js'; import AnimalOriginModel from '../../models/animalOriginModel.js'; import AnimalIdentifierType from '../../models/animalIdentifierTypeModel.js'; +import { formatTranslationKey } from '../../util/util.js'; const AnimalOrBatchModel = { animal: AnimalModel, @@ -392,30 +393,33 @@ const getRecordIfExists = async (animalOrBatch, animalOrBatchKey, farm_id) => { .withGraphFetched(relations); }; -// Post loop checks +// Checks for duplicates - no migration was made for pre-existing duplicates on beta +// Translation key would normally be used to check for duplicates const checkCustomTypeAndBreedConflicts = async (newTypesSet, newBreedsSet, farm_id, trx) => { if (newTypesSet.size) { - const record = await CustomAnimalTypeModel.getTypesByFarmAndTypes( - farm_id, - [...newTypesSet], - trx, - ); - - if (record.length) { - throw customError('Animal type already exists', 409); - } + const customTypes = await CustomAnimalTypeModel.query(trx).where('farm_id', farm_id); + const formattedCustomTypes = customTypes.map((ct) => formatTranslationKey(ct.type)); + newTypesSet.forEach((newType) => { + if ([...formattedCustomTypes].includes(formatTranslationKey(newType))) { + throw customError('Animal type already exists', 409); + } + }); } if (newBreedsSet.size) { const typeBreedPairs = [...newBreedsSet].map((breed) => breed.split('/')); - const record = await CustomAnimalBreedModel.getBreedsByFarmAndTypeBreedPairs( - farm_id, - typeBreedPairs, - trx, - ); - - if (record.length) { - throw customError('Animal breed already exists', 409); + const customBreeds = await CustomAnimalBreedModel.query(trx).where('farm_id', farm_id); + for (const newBreed of typeBreedPairs) { + const [typeColumn, typeId, breed] = newBreed; + if ( + [...customBreeds].some( + (cb) => + cb[typeColumn] === +typeId && + formatTranslationKey(cb.breed) === formatTranslationKey(breed), + ) + ) { + throw customError('Animal breed already exists', 409); + } } } }; From 9dffdc5b2a11a93fdf59aa5b1f785467015a4fec Mon Sep 17 00:00:00 2001 From: Duncan-Brain Date: Wed, 11 Dec 2024 13:50:01 -0500 Subject: [PATCH 04/18] LF-4583 Remove unused model services --- packages/api/src/models/customAnimalBreedModel.js | 14 -------------- packages/api/src/models/customAnimalTypeModel.js | 11 ----------- 2 files changed, 25 deletions(-) diff --git a/packages/api/src/models/customAnimalBreedModel.js b/packages/api/src/models/customAnimalBreedModel.js index 30b1d6aedb..e5a74aaa6d 100644 --- a/packages/api/src/models/customAnimalBreedModel.js +++ b/packages/api/src/models/customAnimalBreedModel.js @@ -49,20 +49,6 @@ class CustomAnimalBreed extends baseModel { additionalProperties: false, }; } - - static async getBreedsByFarmAndTypeBreedPairs(farm_id, typeBreeds, trx) { - const conditions = typeBreeds.map(([typeColumn, typeId, breed]) => { - return trx.raw(`(${typeColumn} = ? AND breed = ?)`, [typeId, breed]); - }); - const data = await trx.raw( - `SELECT id - FROM - custom_animal_breed - WHERE farm_id = ? AND deleted is FALSE AND (${conditions.join(' OR ')});`, - [farm_id], - ); - return data.rows; - } } export default CustomAnimalBreed; diff --git a/packages/api/src/models/customAnimalTypeModel.js b/packages/api/src/models/customAnimalTypeModel.js index 159eeec977..30dd87b535 100644 --- a/packages/api/src/models/customAnimalTypeModel.js +++ b/packages/api/src/models/customAnimalTypeModel.js @@ -65,17 +65,6 @@ class CustomAnimalType extends baseModel { ); return data.rows; } - - static async getTypesByFarmAndTypes(farm_id, types, trx) { - const data = await trx.raw( - `SELECT id - FROM - custom_animal_type - WHERE farm_id = ? AND deleted is FALSE AND type IN (${types.map(() => '?').join(',')});`, - [farm_id, ...types], - ); - return data.rows; - } } export default CustomAnimalType; From cd96deb777da19635ec9635853f18f5c05b995b3 Mon Sep 17 00:00:00 2001 From: Duncan-Brain Date: Wed, 11 Dec 2024 15:07:29 -0500 Subject: [PATCH 05/18] LF-4583 Make new hook form validation --- .../Form/hookformValidationUtils.js | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/webapp/src/components/Form/hookformValidationUtils.js b/packages/webapp/src/components/Form/hookformValidationUtils.js index b56f6c90e8..c579c2936c 100644 --- a/packages/webapp/src/components/Form/hookformValidationUtils.js +++ b/packages/webapp/src/components/Form/hookformValidationUtils.js @@ -87,3 +87,28 @@ export const hookFormUniquePropertyWithStatusValidation = ({ return true; }; }; + +/** + * Validates if a value is unique within an array of objects based on a specified property. + * + * @param {Array} objArr - The array of objects to search for duplicates in. + * @param {string} property - The property within each object to check for uniqueness. + * @param {string} message - The error message to return if the value is not unique. + * @returns {(value: any) => string|boolean} A validation function that takes a value to validate and returns + * either the error message (if not unique) or `true` (if unique). + */ +export const hookFormSelectUniquePropertyValidation = (objArr, property, message) => { + return (value) => { + if (!value?.__isNew__) { + return true; + } + const otherOptions = objArr.filter((option) => !(option.value === value?.value)); + const alreadyExists = otherOptions.some((item) => { + return item[property] === value?.label; + }); + if (alreadyExists) { + return message; + } + return true; + }; +}; From 92c7a71c629d42812868afaec661e4cb778dda03 Mon Sep 17 00:00:00 2001 From: Duncan-Brain Date: Wed, 11 Dec 2024 15:10:16 -0500 Subject: [PATCH 06/18] LF-4583 Add new react select hookFormValidation to Breed and type select --- .../AddAnimalsFormCard/AddAnimalsFormCard.tsx | 4 ++ .../AddAnimalsFormCard/AnimalSelect.tsx | 57 ++++++++++++------- 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/packages/webapp/src/components/Animals/AddAnimalsFormCard/AddAnimalsFormCard.tsx b/packages/webapp/src/components/Animals/AddAnimalsFormCard/AddAnimalsFormCard.tsx index 12ce42666d..2190d7b830 100644 --- a/packages/webapp/src/components/Animals/AddAnimalsFormCard/AddAnimalsFormCard.tsx +++ b/packages/webapp/src/components/Animals/AddAnimalsFormCard/AddAnimalsFormCard.tsx @@ -110,6 +110,10 @@ export default function AddAnimalsFormCard({ control={control} breedOptions={filteredBreeds} isTypeSelected={!!watchAnimalType} + onBreedChange={(option) => { + trigger(`${namePrefix}${BasicsFields.BREED}`); + }} + error={get(errors, `${namePrefix}${BasicsFields.BREED}`)} />
diff --git a/packages/webapp/src/components/Animals/AddAnimalsFormCard/AnimalSelect.tsx b/packages/webapp/src/components/Animals/AddAnimalsFormCard/AnimalSelect.tsx index 6be4ebe646..5ace3e308f 100644 --- a/packages/webapp/src/components/Animals/AddAnimalsFormCard/AnimalSelect.tsx +++ b/packages/webapp/src/components/Animals/AddAnimalsFormCard/AnimalSelect.tsx @@ -17,8 +17,9 @@ import { Controller, FieldError, FieldValues, UseControllerProps } from 'react-h import { CreatableSelect } from '../../Form/ReactSelect'; import { useTranslation } from 'react-i18next'; import { RefObject } from 'react'; -import { GroupBase, SelectInstance, OptionsOrGroups } from 'react-select'; +import { GroupBase, SelectInstance, OptionsOrGroups, SingleValue } from 'react-select'; import { Error } from '../../Typography'; +import { hookFormSelectUniquePropertyValidation } from '../../Form/hookformValidationUtils'; export type Option = { label: string; @@ -47,7 +48,11 @@ export function AnimalTypeSelect({ ( ; isDisabled?: boolean; + error?: FieldError; + onBreedChange?: (Option: Option | null) => void; }; export function AnimalBreedSelect({ @@ -80,25 +87,37 @@ export function AnimalBreedSelect({ isTypeSelected, breedSelectRef, isDisabled = false, + error, + onBreedChange, }: AnimalBreedSelectProps & UseControllerProps) { const { t } = useTranslation(); return ( - ( - onChange(option)} - value={value || null} - /> - )} - /> +
+ ( + { + onChange(option); + // @ts-ignore + onBreedChange?.(option); + }} + value={value || null} + /> + )} + /> + {error && {error.message}} +
); } From 22012e0ec44e2119e9e393e9eb6a2094560fb95d Mon Sep 17 00:00:00 2001 From: Duncan-Brain Date: Wed, 11 Dec 2024 15:25:13 -0500 Subject: [PATCH 07/18] LF-4583 Update react hook form util to be case insensitive --- .../src/components/Form/hookformValidationUtils.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/webapp/src/components/Form/hookformValidationUtils.js b/packages/webapp/src/components/Form/hookformValidationUtils.js index c579c2936c..e6f26ae272 100644 --- a/packages/webapp/src/components/Form/hookformValidationUtils.js +++ b/packages/webapp/src/components/Form/hookformValidationUtils.js @@ -88,6 +88,15 @@ export const hookFormUniquePropertyWithStatusValidation = ({ }; }; +/** + * Format translation key - (same as backend util) + * @param {String} key + * @returns {String} - Formatted key + */ +const formatTranslationKey = (key) => { + return key.toUpperCase().trim().replaceAll(' ', '_'); +}; + /** * Validates if a value is unique within an array of objects based on a specified property. * @@ -102,9 +111,8 @@ export const hookFormSelectUniquePropertyValidation = (objArr, property, message if (!value?.__isNew__) { return true; } - const otherOptions = objArr.filter((option) => !(option.value === value?.value)); - const alreadyExists = otherOptions.some((item) => { - return item[property] === value?.label; + const alreadyExists = objArr.some((item) => { + return formatTranslationKey(item[property]) === formatTranslationKey(value?.label); }); if (alreadyExists) { return message; From 1303352efcaab8e357b31c4c49f17a98fc691c4e Mon Sep 17 00:00:00 2001 From: Duncan-Brain Date: Wed, 11 Dec 2024 15:25:40 -0500 Subject: [PATCH 08/18] LF-4583 Update comment to reference mirror frontend function --- packages/api/src/util/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/src/util/util.js b/packages/api/src/util/util.js index 38012fe832..5c9a793cb7 100644 --- a/packages/api/src/util/util.js +++ b/packages/api/src/util/util.js @@ -56,7 +56,7 @@ export const checkAndTrimString = (input) => { }; /** - * Format transaltion key + * Format transaltion key - (same as frontend util) * @param {String} key * @returns {String} - Formatted key */ From 338c6f8e0a9883aee2a59d37c3b9979c8cb99eac Mon Sep 17 00:00:00 2001 From: Duncan-Brain Date: Wed, 11 Dec 2024 20:30:51 -0500 Subject: [PATCH 09/18] LF-4583 Add translation --- packages/webapp/public/locales/en/translation.json | 1 + .../Animals/AddAnimalsFormCard/AnimalSelect.tsx | 13 +++++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/webapp/public/locales/en/translation.json b/packages/webapp/public/locales/en/translation.json index 42f4f07fdb..85daad50a3 100644 --- a/packages/webapp/public/locales/en/translation.json +++ b/packages/webapp/public/locales/en/translation.json @@ -309,6 +309,7 @@ }, "BATCH": "Batch", "BETA_SPOTLIGHT_HEADING": "Introducing the new Animals feature", + "DUPLICATE_NAME": "This name already exists, please choose another", "EDIT_ANIMAL_FLOW": "editing", "FILTER": { "BATCHES": "Batches", diff --git a/packages/webapp/src/components/Animals/AddAnimalsFormCard/AnimalSelect.tsx b/packages/webapp/src/components/Animals/AddAnimalsFormCard/AnimalSelect.tsx index 5ace3e308f..7e1c7cf426 100644 --- a/packages/webapp/src/components/Animals/AddAnimalsFormCard/AnimalSelect.tsx +++ b/packages/webapp/src/components/Animals/AddAnimalsFormCard/AnimalSelect.tsx @@ -17,9 +17,9 @@ import { Controller, FieldError, FieldValues, UseControllerProps } from 'react-h import { CreatableSelect } from '../../Form/ReactSelect'; import { useTranslation } from 'react-i18next'; import { RefObject } from 'react'; -import { GroupBase, SelectInstance, OptionsOrGroups, SingleValue } from 'react-select'; +import { GroupBase, SelectInstance, OptionsOrGroups } from 'react-select'; import { Error } from '../../Typography'; -import { hookFormSelectUniquePropertyValidation } from '../../Form/hookformValidationUtils'; +import { hookFormUniqueOptionValidation } from '../../Form/hookformValidationUtils'; export type Option = { label: string; @@ -50,8 +50,9 @@ export function AnimalTypeSelect({ control={control} rules={{ required: { value: true, message: t('common:REQUIRED') }, + // prettier-ignore // @ts-ignore - validate: hookFormSelectUniquePropertyValidation(typeOptions, 'label', 'Already exists'), + validate: hookFormUniqueOptionValidation(typeOptions, 'label', t('ANIMAL.DUPLICATE_NAME')), }} render={({ field: { onChange, value } }) => ( ({ name={name} control={control} rules={{ - validate: hookFormSelectUniquePropertyValidation(breedOptions, 'label', 'Already exists'), + validate: hookFormUniqueOptionValidation( + breedOptions, + 'label', + t('ANIMAL.DUPLICATE_NAME'), + ), }} render={({ field: { onChange, value } }) => ( Date: Wed, 11 Dec 2024 20:33:38 -0500 Subject: [PATCH 10/18] LF-4583 Choose a different duplicate string comparison function for FE and BE --- packages/api/src/util/util.js | 16 +++++++++++++++- .../components/Form/hookformValidationUtils.js | 15 ++++++++++----- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/packages/api/src/util/util.js b/packages/api/src/util/util.js index 5c9a793cb7..925daf6d58 100644 --- a/packages/api/src/util/util.js +++ b/packages/api/src/util/util.js @@ -56,10 +56,24 @@ export const checkAndTrimString = (input) => { }; /** - * Format transaltion key - (same as frontend util) + * Format translation key * @param {String} key * @returns {String} - Formatted key */ export const formatTranslationKey = (key) => { return key.toUpperCase().trim().replaceAll(' ', '_'); }; + +export const upperCaseTrim = (a) => { + return a.toUpperCase().trim(); +}; + +/** + * Check for duplicate or matching strings - (same as frontend util) + * TODO: consider localeCompare() or not caring about case sensitivity + * @param {String} key + * @returns {String} - Formatted key + */ +export const compareUpperCaseTrim = (a, b) => { + return upperCaseTrim(a) === upperCaseTrim(b); +}; diff --git a/packages/webapp/src/components/Form/hookformValidationUtils.js b/packages/webapp/src/components/Form/hookformValidationUtils.js index e6f26ae272..0a020a7d0a 100644 --- a/packages/webapp/src/components/Form/hookformValidationUtils.js +++ b/packages/webapp/src/components/Form/hookformValidationUtils.js @@ -88,13 +88,18 @@ export const hookFormUniquePropertyWithStatusValidation = ({ }; }; +export const upperCaseTrim = (a) => { + return a.toUpperCase().trim(); +}; + /** - * Format translation key - (same as backend util) + * Check for duplicate or matching strings - (same as backend util) + * TODO: consider localeCompare() or not caring about case sensitivity * @param {String} key * @returns {String} - Formatted key */ -const formatTranslationKey = (key) => { - return key.toUpperCase().trim().replaceAll(' ', '_'); +export const compareUpperCaseTrim = (a, b) => { + return upperCaseTrim(a) === upperCaseTrim(b); }; /** @@ -106,13 +111,13 @@ const formatTranslationKey = (key) => { * @returns {(value: any) => string|boolean} A validation function that takes a value to validate and returns * either the error message (if not unique) or `true` (if unique). */ -export const hookFormSelectUniquePropertyValidation = (objArr, property, message) => { +export const hookFormUniqueOptionValidation = (objArr, property, message) => { return (value) => { if (!value?.__isNew__) { return true; } const alreadyExists = objArr.some((item) => { - return formatTranslationKey(item[property]) === formatTranslationKey(value?.label); + return compareUpperCaseTrim(item[property], value?.label); }); if (alreadyExists) { return message; From ab39d521af8d57f80e3b5e980eb43712f4858967 Mon Sep 17 00:00:00 2001 From: Duncan-Brain Date: Wed, 11 Dec 2024 20:34:46 -0500 Subject: [PATCH 11/18] LF-4583 Use new string comparitor on BE --- .../src/middleware/validation/checkAnimalOrBatch.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/api/src/middleware/validation/checkAnimalOrBatch.js b/packages/api/src/middleware/validation/checkAnimalOrBatch.js index feacc4950e..44690fecf4 100644 --- a/packages/api/src/middleware/validation/checkAnimalOrBatch.js +++ b/packages/api/src/middleware/validation/checkAnimalOrBatch.js @@ -32,7 +32,7 @@ import CustomAnimalBreedModel from '../../models/customAnimalBreedModel.js'; import AnimalUseModel from '../../models/animalUseModel.js'; import AnimalOriginModel from '../../models/animalOriginModel.js'; import AnimalIdentifierType from '../../models/animalIdentifierTypeModel.js'; -import { formatTranslationKey } from '../../util/util.js'; +import { compareUpperCaseTrim, upperCaseTrim } from '../../util/util.js'; const AnimalOrBatchModel = { animal: AnimalModel, @@ -398,9 +398,9 @@ const getRecordIfExists = async (animalOrBatch, animalOrBatchKey, farm_id) => { const checkCustomTypeAndBreedConflicts = async (newTypesSet, newBreedsSet, farm_id, trx) => { if (newTypesSet.size) { const customTypes = await CustomAnimalTypeModel.query(trx).where('farm_id', farm_id); - const formattedCustomTypes = customTypes.map((ct) => formatTranslationKey(ct.type)); + const formattedCustomTypes = customTypes.map((ct) => upperCaseTrim(ct.type)); newTypesSet.forEach((newType) => { - if ([...formattedCustomTypes].includes(formatTranslationKey(newType))) { + if ([...formattedCustomTypes].includes(upperCaseTrim(newType))) { throw customError('Animal type already exists', 409); } }); @@ -413,9 +413,7 @@ const checkCustomTypeAndBreedConflicts = async (newTypesSet, newBreedsSet, farm_ const [typeColumn, typeId, breed] = newBreed; if ( [...customBreeds].some( - (cb) => - cb[typeColumn] === +typeId && - formatTranslationKey(cb.breed) === formatTranslationKey(breed), + (cb) => cb[typeColumn] === +typeId && compareUpperCaseTrim(cb.breed, breed), ) ) { throw customError('Animal breed already exists', 409); From fdb9244a0e3d6f898837c57f40807689cde769ff Mon Sep 17 00:00:00 2001 From: Duncan-Brain Date: Wed, 11 Dec 2024 20:44:59 -0500 Subject: [PATCH 12/18] LF-4583 Updated code comments --- packages/api/src/middleware/validation/checkAnimalOrBatch.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/src/middleware/validation/checkAnimalOrBatch.js b/packages/api/src/middleware/validation/checkAnimalOrBatch.js index a66e989f0c..700b35f1fd 100644 --- a/packages/api/src/middleware/validation/checkAnimalOrBatch.js +++ b/packages/api/src/middleware/validation/checkAnimalOrBatch.js @@ -395,7 +395,7 @@ const getRecordIfExists = async (animalOrBatch, animalOrBatchKey, farm_id) => { }; // Checks for duplicates - no migration was made for pre-existing duplicates on beta -// Translation key would normally be used to check for duplicates +// Currently the only endpoint checking case const checkCustomTypeAndBreedConflicts = async (newTypesSet, newBreedsSet, farm_id, trx) => { if (newTypesSet.size) { const customTypes = await CustomAnimalTypeModel.query(trx).where('farm_id', farm_id); From afcd4747634f113fe1f627da8520be6e259592fc Mon Sep 17 00:00:00 2001 From: Duncan-Brain Date: Wed, 11 Dec 2024 20:50:20 -0500 Subject: [PATCH 13/18] LF-4583 Move spreading to intended place --- packages/api/src/middleware/validation/checkAnimalOrBatch.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/api/src/middleware/validation/checkAnimalOrBatch.js b/packages/api/src/middleware/validation/checkAnimalOrBatch.js index 700b35f1fd..7480778228 100644 --- a/packages/api/src/middleware/validation/checkAnimalOrBatch.js +++ b/packages/api/src/middleware/validation/checkAnimalOrBatch.js @@ -399,9 +399,9 @@ const getRecordIfExists = async (animalOrBatch, animalOrBatchKey, farm_id) => { const checkCustomTypeAndBreedConflicts = async (newTypesSet, newBreedsSet, farm_id, trx) => { if (newTypesSet.size) { const customTypes = await CustomAnimalTypeModel.query(trx).where('farm_id', farm_id); - const formattedCustomTypes = customTypes.map((ct) => upperCaseTrim(ct.type)); + const formattedCustomTypes = [...customTypes].map((ct) => upperCaseTrim(ct.type)); newTypesSet.forEach((newType) => { - if ([...formattedCustomTypes].includes(upperCaseTrim(newType))) { + if (formattedCustomTypes.includes(upperCaseTrim(newType))) { throw customError('Animal type already exists', 409); } }); From d842e9c406ea580a3625b2d262a6fd28879071b5 Mon Sep 17 00:00:00 2001 From: Duncan-Brain Date: Thu, 12 Dec 2024 09:45:54 -0500 Subject: [PATCH 14/18] LF-4583 Remove clear button --- .../src/components/Animals/AddAnimalsFormCard/AnimalSelect.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/webapp/src/components/Animals/AddAnimalsFormCard/AnimalSelect.tsx b/packages/webapp/src/components/Animals/AddAnimalsFormCard/AnimalSelect.tsx index 7e1c7cf426..d4ce0c4da2 100644 --- a/packages/webapp/src/components/Animals/AddAnimalsFormCard/AnimalSelect.tsx +++ b/packages/webapp/src/components/Animals/AddAnimalsFormCard/AnimalSelect.tsx @@ -64,6 +64,7 @@ export function AnimalTypeSelect({ }} value={value} isDisabled={isDisabled} + isClearable={false} /> )} /> @@ -119,6 +120,7 @@ export function AnimalBreedSelect({ onBreedChange?.(option); }} value={value || null} + isClearable={false} /> )} /> From ada2e6c1cfa2699703bb1f3c6d713006081d769f Mon Sep 17 00:00:00 2001 From: Duncan-Brain Date: Thu, 12 Dec 2024 11:26:19 -0500 Subject: [PATCH 15/18] LF-4583 Update jsdoc --- packages/api/src/util/util.js | 5 +++-- .../webapp/src/components/Form/hookformValidationUtils.js | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/api/src/util/util.js b/packages/api/src/util/util.js index 925daf6d58..f304eac915 100644 --- a/packages/api/src/util/util.js +++ b/packages/api/src/util/util.js @@ -71,8 +71,9 @@ export const upperCaseTrim = (a) => { /** * Check for duplicate or matching strings - (same as frontend util) * TODO: consider localeCompare() or not caring about case sensitivity - * @param {String} key - * @returns {String} - Formatted key + * @param {String} a + * @param {String} b + * @returns {Boolean} */ export const compareUpperCaseTrim = (a, b) => { return upperCaseTrim(a) === upperCaseTrim(b); diff --git a/packages/webapp/src/components/Form/hookformValidationUtils.js b/packages/webapp/src/components/Form/hookformValidationUtils.js index 99e4c759ff..32952bb49b 100644 --- a/packages/webapp/src/components/Form/hookformValidationUtils.js +++ b/packages/webapp/src/components/Form/hookformValidationUtils.js @@ -95,8 +95,9 @@ export const upperCaseTrim = (a) => { /** * Check for duplicate or matching strings - (same as backend util) * TODO: consider localeCompare() or not caring about case sensitivity - * @param {String} key - * @returns {String} - Formatted key + * @param {String} a + * @param {String} b + * @returns {Boolean} */ export const compareUpperCaseTrim = (a, b) => { return upperCaseTrim(a) === upperCaseTrim(b); From 717db128dfcfc9e6a1339fecdc46093edfd37b90 Mon Sep 17 00:00:00 2001 From: Duncan-Brain Date: Thu, 12 Dec 2024 13:40:11 -0500 Subject: [PATCH 16/18] LF-4583 Hide creatable option if duplicate --- .../Form/ReactSelect/CreatableSelect.tsx | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/webapp/src/components/Form/ReactSelect/CreatableSelect.tsx b/packages/webapp/src/components/Form/ReactSelect/CreatableSelect.tsx index efbd7e3207..a74c230a74 100644 --- a/packages/webapp/src/components/Form/ReactSelect/CreatableSelect.tsx +++ b/packages/webapp/src/components/Form/ReactSelect/CreatableSelect.tsx @@ -22,6 +22,19 @@ import { useTranslation } from 'react-i18next'; import { ClearIndicator, MultiValueRemove, MenuOpenDropdownIndicator } from './components'; import scss from './styles.module.scss'; +const upperCaseTrim = (a: String) => { + return a.toUpperCase().trim(); +}; + +/** + * Check for duplicate or matching strings - (same as backend util) + * TODO: consider localeCompare() or not caring about case sensitivity + **/ + +export const compareUpperCaseTrim = (a: String, b: String) => { + return upperCaseTrim(a) === upperCaseTrim(b); +}; + type CreatableSelectProps< Option = unknown, IsMulti extends boolean = false, @@ -55,11 +68,17 @@ const CreatableSelect = React.forwardRef((props, ref) => { components, createPromptText = t('common:CREATE'), placeholder = t('common:SELECT_OR_ADD_YOUR_OWN'), + options, ...restProps } = props; - const isValidNewOption = (inputValue: string) => { - return inputValue.trim().length > 0; + return ( + inputValue.trim().length > 0 && + !options?.some((opt) => { + // @ts-ignore + return compareUpperCaseTrim(opt?.label, inputValue); + }) + ); }; return ( @@ -91,6 +110,7 @@ const CreatableSelect = React.forwardRef((props, ref) => { ...components, }} ref={ref} + options={options} {...restProps} />
From 9602e65976faa22e5f7c8f60987236eff6d6603a Mon Sep 17 00:00:00 2001 From: Duncan-Brain Date: Thu, 12 Dec 2024 13:40:54 -0500 Subject: [PATCH 17/18] LF-4583 Remove hook form validation unused --- .../AddAnimalsFormCard/AnimalSelect.tsx | 11 ------ .../Form/hookformValidationUtils.js | 39 ------------------- 2 files changed, 50 deletions(-) diff --git a/packages/webapp/src/components/Animals/AddAnimalsFormCard/AnimalSelect.tsx b/packages/webapp/src/components/Animals/AddAnimalsFormCard/AnimalSelect.tsx index d4ce0c4da2..919306c36c 100644 --- a/packages/webapp/src/components/Animals/AddAnimalsFormCard/AnimalSelect.tsx +++ b/packages/webapp/src/components/Animals/AddAnimalsFormCard/AnimalSelect.tsx @@ -19,7 +19,6 @@ import { useTranslation } from 'react-i18next'; import { RefObject } from 'react'; import { GroupBase, SelectInstance, OptionsOrGroups } from 'react-select'; import { Error } from '../../Typography'; -import { hookFormUniqueOptionValidation } from '../../Form/hookformValidationUtils'; export type Option = { label: string; @@ -50,9 +49,6 @@ export function AnimalTypeSelect({ control={control} rules={{ required: { value: true, message: t('common:REQUIRED') }, - // prettier-ignore - // @ts-ignore - validate: hookFormUniqueOptionValidation(typeOptions, 'label', t('ANIMAL.DUPLICATE_NAME')), }} render={({ field: { onChange, value } }) => ( ({ ( { - return a.toUpperCase().trim(); -}; - -/** - * Check for duplicate or matching strings - (same as backend util) - * TODO: consider localeCompare() or not caring about case sensitivity - * @param {String} a - * @param {String} b - * @returns {Boolean} - */ -export const compareUpperCaseTrim = (a, b) => { - return upperCaseTrim(a) === upperCaseTrim(b); -}; - -/** - * Validates if a value is unique within an array of objects based on a specified property. - * - * @param {Array} objArr - The array of objects to search for duplicates in. - * @param {string} property - The property within each object to check for uniqueness. - * @param {string} message - The error message to return if the value is not unique. - * @returns {(value: any) => string|boolean} A validation function that takes a value to validate and returns - * either the error message (if not unique) or `true` (if unique). - */ -export const hookFormUniqueOptionValidation = (objArr, property, message) => { - return (value) => { - if (!value?.__isNew__) { - return true; - } - const alreadyExists = objArr.some((item) => { - return compareUpperCaseTrim(item[property], value?.label); - }); - if (alreadyExists) { - return message; - } - return true; - }; -}; From a7a0a06df069927d5d12cdfba1172983f06bff86 Mon Sep 17 00:00:00 2001 From: Duncan-Brain Date: Thu, 12 Dec 2024 13:58:18 -0500 Subject: [PATCH 18/18] LF-4583 remove translation --- packages/webapp/public/locales/en/translation.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/webapp/public/locales/en/translation.json b/packages/webapp/public/locales/en/translation.json index 43d3e5b26e..0e57856916 100644 --- a/packages/webapp/public/locales/en/translation.json +++ b/packages/webapp/public/locales/en/translation.json @@ -309,7 +309,6 @@ }, "BATCH": "Batch", "BETA_SPOTLIGHT_HEADING": "Introducing the new Animals feature", - "DUPLICATE_NAME": "This name already exists, please choose another", "EDIT_ANIMAL_FLOW": "editing", "FILTER": { "BATCHES": "Batches",