diff --git a/specifyweb/frontend/js_src/lib/components/Merging/__tests__/autoMerge.test.ts b/specifyweb/frontend/js_src/lib/components/Merging/__tests__/autoMerge.test.ts index 8edeb9850bb..7e686509cb8 100644 --- a/specifyweb/frontend/js_src/lib/components/Merging/__tests__/autoMerge.test.ts +++ b/specifyweb/frontend/js_src/lib/components/Merging/__tests__/autoMerge.test.ts @@ -248,7 +248,7 @@ describe('autoMerge', () => { "agentAttachments": [], "agentGeographies": [], "agentSpecialties": [], - "agentType": 1, + "agentType": 0, "catalogerOf": "/api/specify/CollectionObject?cataloger=2305", "collContentContact": null, "collTechContact": null, @@ -257,7 +257,7 @@ describe('autoMerge', () => { "date1": null, "date1Precision": null, "date2": null, - "date2Precision": null, + "date2Precision": 2, "dateOfBirth": null, "dateOfBirthPrecision": null, "dateOfDeath": null, @@ -415,7 +415,7 @@ describe('autoMerge', () => { "agentAttachments": [], "agentGeographies": [], "agentSpecialties": [], - "agentType": 1, + "agentType": 0, "catalogerOf": "/api/specify/CollectionObject?cataloger=2305", "collContentContact": null, "collTechContact": null, @@ -424,7 +424,7 @@ describe('autoMerge', () => { "date1": "2020-01-01", "date1Precision": null, "date2": null, - "date2Precision": null, + "date2Precision": 2, "dateOfBirth": null, "dateOfBirthPrecision": null, "dateOfDeath": null, @@ -582,7 +582,7 @@ describe('autoMerge', () => { "agentAttachments": [], "agentGeographies": [], "agentSpecialties": [], - "agentType": 1, + "agentType": 0, "catalogerOf": "/api/specify/CollectionObject?cataloger=2305", "collContentContact": null, "collTechContact": null, @@ -591,7 +591,7 @@ describe('autoMerge', () => { "date1": null, "date1Precision": null, "date2": null, - "date2Precision": null, + "date2Precision": 2, "dateOfBirth": null, "dateOfBirthPrecision": null, "dateOfDeath": null, @@ -636,4 +636,37 @@ describe('autoMerge', () => { } `); }); + + test('prefers longest string values when auto populating', () => { + const merged = autoMerge( + tables.Agent, + [ + addMissingFields('Agent', { lastName: '' }), + addMissingFields('Agent', { lastName: 'Longer Value' }), + addMissingFields('Agent', { lastName: 'Mid' }), + ] as unknown as RA>, + false + ); + expect(merged.lastName).toBe('Longer Value'); + }); + + test('fills dependent precision even if newest record lacks it', () => { + const merged = autoMerge( + tables.Agent, + [ + addMissingFields('Agent', { + timestampModified: '2024-01-01', + dateOfDeath: '2023-01-01', + dateOfDeathPrecision: null, + }), + addMissingFields('Agent', { + timestampModified: '2023-01-01', + dateOfDeath: '2023-01-01', + dateOfDeathPrecision: 0, + }), + ] as unknown as RA>, + false + ); + expect(merged.dateOfDeathPrecision).toBe(0); + }); }); diff --git a/specifyweb/frontend/js_src/lib/components/Merging/autoMerge.ts b/specifyweb/frontend/js_src/lib/components/Merging/autoMerge.ts index 427a4958d3a..0c405b0cb38 100644 --- a/specifyweb/frontend/js_src/lib/components/Merging/autoMerge.ts +++ b/specifyweb/frontend/js_src/lib/components/Merging/autoMerge.ts @@ -88,7 +88,16 @@ function mergeField( resource[field.name], ]); const values = parentChildValues.map(([_, child]) => child); - const nonFalsyValues = f.unique(values.filter(Boolean)); + const nonFalsyValues = f.unique( + values.filter( + (value) => + value !== null && + value !== undefined && + value !== '' && + // Preserve zeros and false, but drop NaN + value === value + ) + ); const firstValue = nonFalsyValues[0] ?? values[0]; if (field.isRelationship) if (field.isDependent()) @@ -150,8 +159,9 @@ function mergeField( // Pick the longest value return ( Array.from(nonFalsyValues).sort( - sortFunction((string) => - typeof string === 'string' ? string.length : 0 + sortFunction( + (string) => (typeof string === 'string' ? string.length : 0), + true ) )[0] ?? firstValue ); @@ -213,10 +223,15 @@ function mergeDependentField( sourceValue: ReturnType ): ReturnType { const sourceField = strictDependentFields()[fieldName]; - const sourceResource = resources.find( + const matchingResources = resources.filter( (resource) => resource[sourceField] === sourceValue ); - return sourceResource?.[fieldName] ?? null; + const preferredResource = + matchingResources.find((resource) => { + const value = resource[fieldName]; + return value !== null && value !== undefined; + }) ?? matchingResources[0]; + return preferredResource?.[fieldName] ?? null; } /** diff --git a/specifyweb/frontend/js_src/lib/components/Merging/index.tsx b/specifyweb/frontend/js_src/lib/components/Merging/index.tsx index e81ccae2f9d..2c3f280a947 100644 --- a/specifyweb/frontend/js_src/lib/components/Merging/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/Merging/index.tsx @@ -22,6 +22,7 @@ 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'; @@ -210,31 +211,36 @@ function Merging({ const clones = sortedResources.slice(1); const [merged, setMerged] = useAsyncState( - React.useCallback( - async () => - records === undefined || initialRecords.current === undefined - ? undefined - : postMergeResource( - initialRecords.current, - autoMerge( - table, - initialRecords.current, - userPreferences.get( - 'recordMerging', - 'behavior', - 'autoPopulate' - ), - target.id - ) - ).then(async (merged) => { - const mergedResource = deserializeResource( - merged as SerializedResource - ); - if (merged !== undefined) await runAllFieldChecks(mergedResource); - return mergedResource; - }), - [table, records] - ), + React.useCallback(async () => { + if ( + records === undefined || + initialRecords.current === undefined || + target === undefined + ) + return undefined; + + const shouldAutoPopulate = userPreferences.get( + 'recordMerging', + 'behavior', + 'autoPopulate' + ); + + const mergedPayload = shouldAutoPopulate + ? await postMergeResource( + initialRecords.current, + autoMerge(table, initialRecords.current, false, target.id) + ) + : addMissingFields( + table.name, + {} as Partial> + ); + + const mergedResource = deserializeResource( + mergedPayload as SerializedResource + ); + if (mergedPayload !== undefined) await runAllFieldChecks(mergedResource); + return mergedResource; + }, [table, records, target]), true );