Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,43 @@ describe('Collection Object business rules', () => {
expect(result.current[0]).toStrictEqual([]);
});

// Uniqueness rule check
overrideAjax(
'/api/specify/collectionobject/?domainfilter=false&catalognumber=2022-%23%23%23%23%23%23&collection=4&offset=0',
{
objects: [],
meta: {
limit: 20,
offset: 0,
total_count: 0,
},
}
);

test('CollectionObject -> catalogNumber is reset whenever new CollectionObject -> collectionObjectType changes', async () => {
const collectionObject = new tables.CollectionObject.Resource();
expect(collectionObject.get('catalogNumber')).toBeUndefined();
collectionObject.set(
'collectionObjectType',
getResourceApiUrl('CollectionObjectType', 2)
);
expect(collectionObject.get('catalogNumber')).toBe('2022-######');
// Wait for any pending promise to complete before test finishes
await collectionObject.businessRuleManager?.pendingPromise;
});

test('CollectionObject -> catalogNumber is reset whenever existing CollectionObject -> collectionObjectType changes', async () => {
const collectionObject = getBaseCollectionObject();
expect(collectionObject.get('catalogNumber')).toBe('123');
collectionObject.set(
'collectionObjectType',
getResourceApiUrl('CollectionObjectType', 2)
);
expect(collectionObject.get('catalogNumber')).toBe('2022-######');
// Wait for any pending promise to complete before test finishes
await collectionObject.businessRuleManager?.pendingPromise;
});

test('CollectionObject -> determinations: New determinations are current by default', async () => {
const collectionObject = getBaseCollectionObject();
const determinations =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,26 @@ overrideAjax(
}
);

const firstCollectionObjectUrl = getResourceApiUrl('CollectionObject', 1);
const firstCollectionObject = {
id: 1,
resource_uri: firstCollectionObjectUrl,
catalognumber: '000011111',
collection: getResourceApiUrl('Collection', 4),
};
overrideAjax(firstCollectionObjectUrl, firstCollectionObject);
overrideAjax(
'/api/specify/collectionobject/?domainfilter=false&catalognumber=000011111&collection=4&offset=0',
{
objects: [],
meta: {
limit: 20,
offset: 0,
total_count: 1,
},
}
);

const accessionNumber = '2011-IC-116';
const accessionResponse = {
resource_uri: accessionUrl,
Expand Down Expand Up @@ -251,7 +271,7 @@ describe('eventHandlerForToMany', () => {

expect(resource.needsSaved).toBe(true);
// Change to 2 in issue-6214
expect(testFunction).toHaveBeenCalledTimes(2);
expect(testFunction).toHaveBeenCalledTimes(3);
});
test('changing collection propagates to related', () => {
const resource = new tables.CollectionObject.Resource(
Expand Down Expand Up @@ -300,14 +320,14 @@ describe('eventHandlerForToMany', () => {
{ index: 1 }
);

expect(onResourceChange).toHaveBeenCalledTimes(3);
expect(onResourceChange).toHaveBeenCalledTimes(4);

resource.set('determinations', [
addMissingFields('Determination', {
taxon: getResourceApiUrl('Taxon', 1),
}),
]);
expect(onResourceChange).toHaveBeenCalledTimes(4);
expect(onResourceChange).toHaveBeenCalledTimes(5);
expect(onPrepChange).toHaveBeenCalledTimes(1);
expect(onPrepAdd).toHaveBeenCalledTimes(1);
expect(onPrepRemoval).toHaveBeenCalledTimes(1);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { resourcesText } from '../../localization/resources';
import { resolveParser } from '../../utils/parser/definitions';
import type { ValueOf } from '../../utils/types';
import type { BusinessRuleResult } from './businessRules';
import {
COG_PRIMARY_KEY,
Expand All @@ -9,7 +11,7 @@ import {
hasNoCurrentDetermination,
} from './businessRuleUtils';
import { cogTypes } from './helpers';
import type { AnySchema, TableFields } from './helperTypes';
import type { AnySchema, CommonFields, TableFields } from './helperTypes';
import {
checkPrepAvailability,
getTotalLoaned,
Expand All @@ -21,6 +23,7 @@ import {
import type { SpecifyResource } from './legacyTypes';
import { setSaveBlockers } from './saveBlockers';
import { schema } from './schema';
import type { LiteralField, Relationship } from './specifyField';
import type { Collection } from './specifyTable';
import { tables } from './tables';
import type {
Expand Down Expand Up @@ -49,7 +52,17 @@ export type BusinessRuleDefs<SCHEMA extends AnySchema> = {
readonly customInit?: (resource: SpecifyResource<SCHEMA>) => void;
readonly fieldChecks?: {
readonly [FIELD_NAME in TableFields<SCHEMA>]?: (
resource: SpecifyResource<SCHEMA>
resource: SpecifyResource<SCHEMA>,
field: (CommonFields &
SCHEMA['fields'] &
SCHEMA['toManyDependent'] &
SCHEMA['toManyIndependent'] &
SCHEMA['toOneDependent'] &
SCHEMA['toOneIndependent'])[FIELD_NAME] extends ValueOf<
AnySchema['fields']
>
? LiteralField
: Relationship
) => Promise<BusinessRuleResult | undefined> | void;
};
};
Expand Down Expand Up @@ -169,6 +182,16 @@ export const businessRuleDefs: MappedBusinessRuleDefs = {
},
fieldChecks: {
collectionObjectType: async (resource): Promise<undefined> => {
const parser = resolveParser(
resource.specifyTable.strictGetLiteralField('catalogNumber'),
undefined,
resource
);
// REFACTOR: non-silent set causes infinite loop and silent set still triggers save blocker when parser value is empty string
resource.set('catalogNumber', parser.value as never, {
silent: (parser.value ?? '') === '',
});

const determinations = resource.getDependentResource('determinations');
if (determinations === undefined || determinations.models.length === 0)
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,10 @@ export class BusinessRuleManager<SCHEMA extends AnySchema> {

const checks: RA<Promise<BusinessRuleResult<SCHEMA> | undefined>> = [
...this.checkUnique(processedFieldName),
this.invokeRule('fieldChecks', processedFieldName, [this.resource]),
this.invokeRule('fieldChecks', processedFieldName, [
this.resource,
field,
]),
isTreeResource(this.resource as SpecifyResource<AnySchema>)
? treeBusinessRules(
this.resource as SpecifyResource<AnyTree>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ export function CarryForwardConfig({
/>
)}
{isCarryForwardEnabled ? (
<BulkCloneConfig parentTable={parentTable} table={table}/>
<BulkCloneConfig parentTable={parentTable} table={table} />
) : null}
{isOpen && (
<CarryForwardConfigDialog
Expand Down Expand Up @@ -171,7 +171,6 @@ function BulkCloneConfig({

const [isOpen, handleOpen, handleClose] = useBooleanState();


return tableValidForBulkClone(table) ? (
<>
<Label.Inline className="rounded bg-[color:var(--foreground)]">
Expand Down Expand Up @@ -202,7 +201,10 @@ function BulkCloneConfig({
) : null;
}

export const tableValidForBulkClone = (table: SpecifyTable, resource?: SpecifyResource<AnySchema>): boolean =>
export const tableValidForBulkClone = (
table: SpecifyTable,
resource?: SpecifyResource<AnySchema>
): boolean =>
table === tables.CollectionObject &&
!(
tables.CollectionObject.strictGetLiteralField('catalogNumber')
Expand Down
10 changes: 6 additions & 4 deletions specifyweb/frontend/js_src/lib/components/Forms/Save.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -233,12 +233,14 @@ export function SaveButton<SCHEMA extends AnySchema = AnySchema>({
resource.specifyTable.name === 'CollectionObjectGroup' ||
resource.specifyTable.name === 'CollectionObjectGroupJoin';

// Disable bulk carry forward for COType cat num format that are undefined or one of types listed in tableValidForBulkClone()
// Disable bulk carry forward for COType cat num format that are undefined or one of types listed in tableValidForBulkClone()
const formatter =
tables.CollectionObject.strictGetLiteralField(
'catalogNumber'
).getUiFormatter(resource)!;
const disableBulk = !tableValidForBulkClone(resource.specifyTable, resource) || formatter === undefined
const disableBulk =
!tableValidForBulkClone(resource.specifyTable, resource) ||
formatter === undefined;

return (
<>
Expand All @@ -249,7 +251,8 @@ export function SaveButton<SCHEMA extends AnySchema = AnySchema>({
isSaveDisabled &&
showCarry &&
showBulkCarry &&
!isCOGorCOJO && !disableBulk? (
!isCOGorCOJO &&
!disableBulk ? (
<Input.Integer
aria-label={formsText.bulkCarryForwardCount()}
className="!w-fit"
Expand All @@ -275,7 +278,6 @@ export function SaveButton<SCHEMA extends AnySchema = AnySchema>({
resource.specifyTable.name === 'CollectionObject' &&
carryForwardAmount > 1
? async (): Promise<RA<SpecifyResource<SCHEMA>>> => {

const wildCard = formatter.valueOrWild();

const clonePromises = Array.from(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { renderHook } from '@testing-library/react';
import { tables } from '../../components/DataModel/tables';
import { requireContext } from '../../tests/helpers';
import { Parser } from '../../utils/parser/definitions';
import { useParser } from '../resource';
import { useParserDefaultValue } from '../useParserDefaultValue';

requireContext();

test('Simple parser', () => {
const resource = new tables.ExchangeOut.Resource();
const field = tables.ExchangeOut.strictGetLiteralField('number1');
expect(resource.get(field.name as never)).toBeUndefined();
const parser: Parser = {
type: 'number',
value: '2',
};
renderHook(() => useParserDefaultValue(resource, field, parser));

expect(resource.get(field.name as never)).toBe('2');
});

test('Only overwrites when needed', () => {
const resource = new tables.ExchangeOut.Resource({
number1: 42,
});
const field = tables.ExchangeOut.strictGetLiteralField('number1');
expect(resource.get(field.name as never)).toBe(42);
const parser: Parser = {
type: 'number',
value: '2',
};
renderHook(() => useParserDefaultValue(resource, field, parser));

expect(resource.get(field.name as never)).toBe(42);
});

test("Doesn't override an existing resource's values", () => {
const resource = new tables.ExchangeOut.Resource({ id: 1 });
const field = tables.ExchangeOut.strictGetLiteralField('number1');
expect(resource.get(field.name as never)).toBeUndefined();
const parser: Parser = {
type: 'number',
value: '2',
};
renderHook(() => useParserDefaultValue(resource, field, parser));

expect(resource.get(field.name as never)).toBe(undefined);
});

test("Doesn't assume default value", () => {
const resource = new tables.Accession.Resource();
const field = tables.Accession.strictGetLiteralField('integer1');
expect(resource.get(field.name as never)).toBeUndefined();
const { result } = renderHook(() => useParser(field, resource));

renderHook(() => useParserDefaultValue(resource, field, result.current));

expect(resource.get(field.name as never)).toBeUndefined();
});

test('CatalogNumber parser', () => {
const resource = new tables.CollectionObject.Resource();
const field = tables.CollectionObject.strictGetLiteralField('catalogNumber');
expect(resource.get('catalogNumber')).toBeUndefined();
const { result } = renderHook(() => useParser(field, resource));
renderHook(() => useParserDefaultValue(resource, field, result.current));

expect(resource.get(field.name as never)).toBe('#########');
});
Loading