From 0fd9d4e4986a09f7036a2c390083663f45cdff0a Mon Sep 17 00:00:00 2001 From: alec_dev Date: Thu, 19 Mar 2026 00:04:02 -0500 Subject: [PATCH 1/5] Preserve Agent Type when merging agents without auto-populate --- .../lib/components/Merging/autoMerge.ts | 14 ++++++++++++ .../js_src/lib/components/Merging/index.tsx | 22 +++++++++---------- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Merging/autoMerge.ts b/specifyweb/frontend/js_src/lib/components/Merging/autoMerge.ts index 0c405b0cb38..480ffdca3cc 100644 --- a/specifyweb/frontend/js_src/lib/components/Merging/autoMerge.ts +++ b/specifyweb/frontend/js_src/lib/components/Merging/autoMerge.ts @@ -54,6 +54,20 @@ export function autoMerge( ); } +export async function buildInitialMergedResource( + table: SpecifyTable, + resources: RA>, + shouldAutoPopulate: boolean, + targetId?: number +): Promise> { + return (shouldAutoPopulate + ? await postMergeResource( + resources, + autoMerge(table, resources, false, targetId) + ) + : autoMerge(table, resources, true, targetId)) as SerializedResource; +} + /** * Sort from newest to oldest */ diff --git a/specifyweb/frontend/js_src/lib/components/Merging/index.tsx b/specifyweb/frontend/js_src/lib/components/Merging/index.tsx index 2c3f280a947..130d9d530fb 100644 --- a/specifyweb/frontend/js_src/lib/components/Merging/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/Merging/index.tsx @@ -22,7 +22,6 @@ import { icons } from '../Atoms/Icons'; import { Link } from '../Atoms/Link'; import { Submit } from '../Atoms/Submit'; import { LoadingContext } from '../Core/Contexts'; -import { addMissingFields } from '../DataModel/addMissingFields'; import { runAllFieldChecks } from '../DataModel/businessRules'; import type { AnySchema, SerializedResource } from '../DataModel/helperTypes'; import type { SpecifyResource } from '../DataModel/legacyTypes'; @@ -35,7 +34,11 @@ import { Dialog, dialogClassNames } from '../Molecules/Dialog'; import { userPreferences } from '../Preferences/userPreferences'; import { formatUrl } from '../Router/queryString'; import { OverlayContext, OverlayLocation } from '../Router/Router'; -import { autoMerge, postMergeResource } from './autoMerge'; +import { + autoMerge, + buildInitialMergedResource, + postMergeResource, +} from './autoMerge'; import { CompareRecords } from './Compare'; import { recordMergingTableSpec } from './definitions'; import { InvalidMergeRecordsDialog } from './InvalidMergeRecords'; @@ -225,15 +228,12 @@ function Merging({ 'autoPopulate' ); - const mergedPayload = shouldAutoPopulate - ? await postMergeResource( - initialRecords.current, - autoMerge(table, initialRecords.current, false, target.id) - ) - : addMissingFields( - table.name, - {} as Partial> - ); + const mergedPayload = await buildInitialMergedResource( + table, + initialRecords.current, + shouldAutoPopulate, + target.id + ); const mergedResource = deserializeResource( mergedPayload as SerializedResource From 7fae15d1e237a3c4ac3b86f9f157b3d47a589aaf Mon Sep 17 00:00:00 2001 From: alec_dev Date: Thu, 19 Mar 2026 05:10:14 +0000 Subject: [PATCH 2/5] Lint code with ESLint and Prettier Triggered by 6c3a929bf43c2cc5eb43cc28c8941ca8d5d9d11e on branch refs/heads/issue-5072 --- specifyweb/frontend/js_src/lib/hooks/useValidation.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/hooks/useValidation.tsx b/specifyweb/frontend/js_src/lib/hooks/useValidation.tsx index 511c3f97fa3..0bf10bd8848 100644 --- a/specifyweb/frontend/js_src/lib/hooks/useValidation.tsx +++ b/specifyweb/frontend/js_src/lib/hooks/useValidation.tsx @@ -2,7 +2,10 @@ import React from 'react'; import { InFormEditorContext } from '../components/FormEditor/Context'; import type { Input } from '../components/Forms/validationHelpers'; -import { hasNativeErrors, isInputTouched } from '../components/Forms/validationHelpers'; +import { + hasNativeErrors, + isInputTouched, +} from '../components/Forms/validationHelpers'; import { listen } from '../utils/events'; import type { RA } from '../utils/types'; From b85b5938ec8f6a8fdce960b1de57f3d59296c783 Mon Sep 17 00:00:00 2001 From: alec_dev Date: Thu, 21 May 2026 12:49:45 -0500 Subject: [PATCH 3/5] Preserve agent type when merge auto populate is disabled --- .../lib/components/Merging/autoMerge.ts | 34 +++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Merging/autoMerge.ts b/specifyweb/frontend/js_src/lib/components/Merging/autoMerge.ts index 480ffdca3cc..1c3842e18a0 100644 --- a/specifyweb/frontend/js_src/lib/components/Merging/autoMerge.ts +++ b/specifyweb/frontend/js_src/lib/components/Merging/autoMerge.ts @@ -60,14 +60,36 @@ export async function buildInitialMergedResource( shouldAutoPopulate: boolean, targetId?: number ): Promise> { - return (shouldAutoPopulate - ? await postMergeResource( - resources, - autoMerge(table, resources, false, targetId) - ) - : autoMerge(table, resources, true, targetId)) as SerializedResource; + return ( + shouldAutoPopulate + ? await postMergeResource( + resources, + autoMerge(table, resources, false, targetId) + ) + : addMissingFields( + table.name, + getNonAutoPopulatedFields(table, resources, targetId) + ) + ) as SerializedResource; } +const getNonAutoPopulatedFields = ( + table: SpecifyTable, + resources: RA>, + targetId?: number +): Partial> => + table.name === 'Agent' + ? { agentType: getTargetResource(resources, targetId)?.agentType } + : {}; + +const getTargetResource = ( + resources: RA>, + targetId?: number +): SerializedResource | undefined => + targetId === undefined + ? resources[0] + : (resources.find(({ id }) => id === targetId) ?? resources[0]); + /** * Sort from newest to oldest */ From 0da4d16b59f0d049fb915f04c4ab5410d0baaba8 Mon Sep 17 00:00:00 2001 From: alec_dev Date: Fri, 22 May 2026 11:14:57 -0500 Subject: [PATCH 4/5] Preserve shared agent type when merge auto populate is disabled --- .../lib/components/Merging/autoMerge.ts | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Merging/autoMerge.ts b/specifyweb/frontend/js_src/lib/components/Merging/autoMerge.ts index 1c3842e18a0..f845a6e05fc 100644 --- a/specifyweb/frontend/js_src/lib/components/Merging/autoMerge.ts +++ b/specifyweb/frontend/js_src/lib/components/Merging/autoMerge.ts @@ -68,19 +68,33 @@ export async function buildInitialMergedResource( ) : addMissingFields( table.name, - getNonAutoPopulatedFields(table, resources, targetId) + getPreservedFieldsWithoutAutoPopulate(table, resources, targetId) ) ) as SerializedResource; } -const getNonAutoPopulatedFields = ( +const preservedFieldsWithoutAutoPopulate: Partial< + RR< + keyof Tables, + ( + resources: RA>, + targetId?: number + ) => Partial> + > +> = { + Agent: (resources, targetId) => ({ + agentType: + getSharedFieldValue(resources, 'agentType') ?? + getTargetResource(resources, targetId)?.agentType, + }), +}; + +const getPreservedFieldsWithoutAutoPopulate = ( table: SpecifyTable, resources: RA>, targetId?: number ): Partial> => - table.name === 'Agent' - ? { agentType: getTargetResource(resources, targetId)?.agentType } - : {}; + preservedFieldsWithoutAutoPopulate[table.name]?.(resources, targetId) ?? {}; const getTargetResource = ( resources: RA>, @@ -90,6 +104,18 @@ const getTargetResource = ( ? resources[0] : (resources.find(({ id }) => id === targetId) ?? resources[0]); +const getSharedFieldValue = ( + resources: RA>, + fieldName: string +): boolean | number | string | null | undefined => { + const values = f.unique( + resources + .map((resource) => resource[fieldName]) + .filter((value) => value !== null && value !== undefined) + ); + return values.length === 1 ? values[0] : undefined; +}; + /** * Sort from newest to oldest */ From 24b54991987608f543d6a134fefa3c37217028da Mon Sep 17 00:00:00 2001 From: alec_dev Date: Fri, 22 May 2026 13:52:20 -0500 Subject: [PATCH 5/5] Preserve Agent Type through merge form defaults --- .../lib/components/Merging/autoMerge.ts | 24 +++++++++++++------ .../lib/hooks/useParserDefaultValue.tsx | 7 ++++-- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Merging/autoMerge.ts b/specifyweb/frontend/js_src/lib/components/Merging/autoMerge.ts index f845a6e05fc..c60f8707562 100644 --- a/specifyweb/frontend/js_src/lib/components/Merging/autoMerge.ts +++ b/specifyweb/frontend/js_src/lib/components/Merging/autoMerge.ts @@ -83,9 +83,10 @@ const preservedFieldsWithoutAutoPopulate: Partial< > > = { Agent: (resources, targetId) => ({ - agentType: - getSharedFieldValue(resources, 'agentType') ?? - getTargetResource(resources, targetId)?.agentType, + agentType: parseNumber( + getSharedNumberFieldValue(resources, 'agentType') ?? + getTargetResource(resources, targetId)?.agentType + ), }), }; @@ -104,18 +105,27 @@ const getTargetResource = ( ? resources[0] : (resources.find(({ id }) => id === targetId) ?? resources[0]); -const getSharedFieldValue = ( +const getSharedNumberFieldValue = ( resources: RA>, fieldName: string -): boolean | number | string | null | undefined => { +): number | undefined => { const values = f.unique( resources - .map((resource) => resource[fieldName]) - .filter((value) => value !== null && value !== undefined) + .map((resource) => parseNumber(resource[fieldName])) + .filter((value) => value !== undefined) ); return values.length === 1 ? values[0] : undefined; }; +const parseNumber = ( + value: boolean | number | string | null | undefined +): number | undefined => + typeof value === 'number' + ? value + : typeof value === 'string' + ? f.parseInt(value) + : undefined; + /** * Sort from newest to oldest */ diff --git a/specifyweb/frontend/js_src/lib/hooks/useParserDefaultValue.tsx b/specifyweb/frontend/js_src/lib/hooks/useParserDefaultValue.tsx index 353d957e442..c5f71128221 100644 --- a/specifyweb/frontend/js_src/lib/hooks/useParserDefaultValue.tsx +++ b/specifyweb/frontend/js_src/lib/hooks/useParserDefaultValue.tsx @@ -35,9 +35,12 @@ export function useParserDefaultValue( * Don't auto set numeric to "0", unless it is the default value * in the form definition */ + const parserUsesNumberValue = + parser.type === 'number' || typeof parser.value === 'number'; + const hasDefault = parser.value !== undefined && - (parser.type !== 'number' || parser.value !== 0); + (!parserUsesNumberValue || parser.value !== 0); const fieldValue = resource.get(field.name) as | boolean @@ -54,7 +57,7 @@ export function useParserDefaultValue( * should overwrite that of the resource */ resource.isNew() && - (parser.type !== 'number' || + (!parserUsesNumberValue || typeof fieldValue !== 'number' || fieldValue === 0) && ((parser.type !== 'text' && parser.type !== 'date') ||