diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts index ce4f0f489c8..2e9fdfe988a 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/businessRules.test.ts @@ -1,4 +1,4 @@ -import { renderHook } from '@testing-library/react'; +import { act, renderHook } from '@testing-library/react'; import { overrideAjax } from '../../../tests/ajax'; import { mockTime, requireContext } from '../../../tests/helpers'; @@ -9,7 +9,12 @@ import { getResourceApiUrl } from '../resource'; import { useSaveBlockers } from '../saveBlockers'; import { schema } from '../schema'; import { tables } from '../tables'; -import type { Taxon, TaxonTreeDefItem } from '../types'; +import type { + CollectionObjectType, + Determination, + Taxon, + TaxonTreeDefItem, +} from '../types'; mockTime(); requireContext(); @@ -49,6 +54,28 @@ describe('Borrow Material business rules', () => { }); describe('Collection Object business rules', () => { + const collectionObjectTypeUrl = getResourceApiUrl('CollectionObjectType', 1); + const collectionObjectType: Partial< + SerializedResource + > = { + id: 1, + name: 'Entomology', + taxonTreeDef: getResourceApiUrl('Taxon', 1), + resource_uri: collectionObjectTypeUrl, + }; + overrideAjax(collectionObjectTypeUrl, collectionObjectType); + + const otherTaxonId = 1; + const otherTaxon: Partial> = { + id: otherTaxonId, + isAccepted: true, + rankId: 10, + definition: getResourceApiUrl('TaxonTreeDef', 2), + resource_uri: getResourceApiUrl('Taxon', otherTaxonId), + }; + + overrideAjax(getResourceApiUrl('Taxon', otherTaxonId), otherTaxon); + const collectionObjectlId = 2; const collectionObjectUrl = getResourceApiUrl( 'CollectionObject', @@ -58,6 +85,13 @@ describe('Collection Object business rules', () => { const getBaseCollectionObject = () => new tables.CollectionObject.Resource({ id: collectionObjectlId, + collectionobjecttype: collectionObjectTypeUrl, + determinations: [ + { + taxon: getResourceApiUrl('Taxon', otherTaxonId), + preferredTaxon: getResourceApiUrl('Taxon', otherTaxonId), + } as SerializedResource, + ], resource_uri: collectionObjectUrl, }); @@ -80,6 +114,25 @@ describe('Collection Object business rules', () => { expect(collectionObject.get('collectingEvent')).toBeDefined(); }); + + test('Save blocked when CollectionObjectType of a CollectionObject does not have same tree definition as its associated Determination -> taxon', async () => { + const collectionObject = getBaseCollectionObject(); + + const determination = + collectionObject.getDependentResource('determinations')?.models[0]; + + const { result } = renderHook(() => + useSaveBlockers(determination, tables.Determination.getField('taxon')) + ); + + await act(async () => { + await determination?.businessRuleManager?.checkField('taxon'); + }); + + expect(result.current[0]).toStrictEqual([ + 'Taxon does not belong to the same tree as this Object Type', + ]); + }); }); describe('DNASequence business rules', () => { diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts index ec35f8065d4..9f5500a8eb0 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts @@ -1,3 +1,4 @@ +import { formsText } from '../../localization/forms'; import { resourcesText } from '../../localization/resources'; import type { BusinessRuleResult } from './businessRules'; import type { AnySchema, TableFields } from './helperTypes'; @@ -17,6 +18,7 @@ import type { Address, BorrowMaterial, CollectionObject, + CollectionObjectType, Determination, DNASequence, LoanPreparation, @@ -47,6 +49,7 @@ type MappedBusinessRuleDefs = { }; const CURRENT_DETERMINATION_KEY = 'determination-isCurrent'; +const DETERMINATION_TAXON_KEY = 'determination-taxon'; export const businessRuleDefs: MappedBusinessRuleDefs = { Address: { @@ -153,7 +156,7 @@ export const businessRuleDefs: MappedBusinessRuleDefs = { fieldChecks: { taxon: async ( determination: SpecifyResource - ): Promise => + ): Promise => determination .rgetPromise('taxon', true) .then((taxon: SpecifyResource | null) => { @@ -165,6 +168,40 @@ export const businessRuleDefs: MappedBusinessRuleDefs = { .then(async (accepted) => accepted === null ? taxon : getLastAccepted(accepted) ); + + const collectionObject = determination.collection?.related; + if ( + collectionObject !== undefined && + collectionObject.specifyTable.name === 'CollectionObject' + ) + (collectionObject as SpecifyResource) + .rgetPromise('collectionObjectType', true) + .then((coType: SpecifyResource) => { + /* + * Have to set save blockers directly here to get this working. + * Since following code has to wait for above rgetPromise to resolve, returning a Promise for validation here is too slow and + * does not get captured by business rules. + */ + if ( + coType.get('taxonTreeDef') === + (taxon?.get('definition') ?? '') + ) { + setSaveBlockers( + determination, + determination.specifyTable.field.taxon, + [], + DETERMINATION_TAXON_KEY + ); + } else { + setSaveBlockers( + determination, + determination.specifyTable.field.taxon, + [formsText.invalidTree()], + DETERMINATION_TAXON_KEY + ); + } + }); + return taxon === null ? { isValid: true, diff --git a/specifyweb/frontend/js_src/lib/localization/forms.ts b/specifyweb/frontend/js_src/lib/localization/forms.ts index 9134ba47354..74100c1982f 100644 --- a/specifyweb/frontend/js_src/lib/localization/forms.ts +++ b/specifyweb/frontend/js_src/lib/localization/forms.ts @@ -1159,4 +1159,7 @@ export const formsText = createDictionary({ 'ru-ru': 'Номер по каталогу Числовой', 'uk-ua': 'Каталожний номер Числовий', }, + invalidTree: { + 'en-us': 'Taxon does not belong to the same tree as this Object Type', + }, } as const);