From 3e334cb153a28bc841119f3d96f74107ae88ea9d Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Thu, 2 Jul 2026 12:20:31 -0500 Subject: [PATCH 1/3] [test]: verify needsSaved propagates from independent collection to parent --- .../DataModel/__tests__/resourceApi.test.ts | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts index 9b4fda0164d..6be0a2e7cb1 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts @@ -15,6 +15,7 @@ import type { CollectionObjectAttribute, CollectionObjectType, Determination, + Accession, } from '../types'; requireContext(); @@ -469,6 +470,51 @@ test('save', async () => { expect(newDetermination.get('number1')).toBe(2); }); +describe('independent resource change propagation', () => { + overrideAjax( + '/api/specify/collectionobject/?domainfilter=false&accession=11&offset=0', + emptyCollection + ); + overrideAjax(accessionUrl, accessionResponse, { method: 'PUT' }); + + test('needsSaved propagates from independent collection to parent', async () => { + const parentResource = new tables.Accession.Resource({ id: accessionId }); + expect(parentResource.needsSaved).toBe(false); + + const collectionObjectRel = + tables.CollectionObject.strictGetRelationship('accession')!; + + const independentCollection = + new tables.CollectionObject.IndependentCollection({ + related: parentResource, + field: collectionObjectRel, + }) as Collection; + + await independentCollection.fetch(); + + // Connect the collection to the parent's event system, as rgetCollection would do internally. + // storeIndependent is not in the public TS types so cast to any. + (parentResource as any).storeIndependent( + collectionObjectRel.getReverse(), + independentCollection + ); + + const newCollectionObject = new tables.CollectionObject.Resource({ + id: 998, + }); + independentCollection.add(newCollectionObject); + + // Adding an existing resource to an independent collection does not mark the resource itself needsSaved; + // only the parent is notified via saverequired + expect(newCollectionObject.needsSaved).toBe(false); + expect(parentResource.needsSaved).toBe(true); + + await parentResource.save(); + + expect(parentResource.needsSaved).toBe(false); + }); +}); + describe('resource initialization', () => { test('Initialization with dependent resources does not trigger saveRequired', () => { const resource = new tables.CollectionObject.Resource({ From 86ca02ecd5865f76d5f01f4f11d8f6fd316791b1 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Thu, 2 Jul 2026 12:21:19 -0500 Subject: [PATCH 2/3] [test]: verify independent resource field changes propagate needsSaved and persist after save --- .../DataModel/__tests__/resourceApi.test.ts | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts index 6be0a2e7cb1..77a0b0ab5b6 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts @@ -515,6 +515,52 @@ describe('independent resource change propagation', () => { }); }); +describe('save base record which has independent subviews', () => { + overrideAjax( + '/api/specify/collectionobject/?domainfilter=false&accession=11&offset=0', + { + objects: [collectionObjectResponse], + meta: { limit: 20, offset: 0, total_count: 1 }, + } + ); + overrideAjax(accessionUrl, accessionResponse, { method: 'PUT' }); + + async function setupParentWithIndependentCollection() { + const parentResource = new tables.Accession.Resource({ id: accessionId }); + const collectionObjectRel = + tables.CollectionObject.strictGetRelationship('accession')!; + const independentCollection = + new tables.CollectionObject.IndependentCollection({ + related: parentResource, + field: collectionObjectRel, + }) as Collection; + await independentCollection.fetch(); + (parentResource as any).storeIndependent( + collectionObjectRel.getReverse(), + independentCollection + ); + return { parentResource, independentCollection }; + } + + test('modifying a field on an independent resource marks base record as needsSaved and save applies the change', async () => { + const { parentResource, independentCollection } = + await setupParentWithIndependentCollection(); + + expect(parentResource.needsSaved).toBe(false); + + const existingCollectionObject = independentCollection.models[0]; + existingCollectionObject.set('text1', 'changed-value'); + + expect(parentResource.needsSaved).toBe(true); + + await parentResource.save(); + + expect(parentResource.needsSaved).toBe(false); + // Change is preserved in memory after save + expect(existingCollectionObject.get('text1')).toBe('changed-value'); + }); +}); + describe('resource initialization', () => { test('Initialization with dependent resources does not trigger saveRequired', () => { const resource = new tables.CollectionObject.Resource({ From a6e4744de3bc6a00352f92346c38d4d0480f2f76 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Fri, 3 Jul 2026 13:54:17 -0500 Subject: [PATCH 3/3] verify independent subview sub-record changes propagate needsSaved and persist --- .../DataModel/__tests__/resourceApi.test.ts | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts index 77a0b0ab5b6..1442e62f761 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts @@ -15,7 +15,6 @@ import type { CollectionObjectAttribute, CollectionObjectType, Determination, - Accession, } from '../types'; requireContext(); @@ -531,6 +530,7 @@ describe('save base record which has independent subviews', () => { tables.CollectionObject.strictGetRelationship('accession')!; const independentCollection = new tables.CollectionObject.IndependentCollection({ + //creates a new form with related: parentResource, field: collectionObjectRel, }) as Collection; @@ -543,21 +543,43 @@ describe('save base record which has independent subviews', () => { } test('modifying a field on an independent resource marks base record as needsSaved and save applies the change', async () => { + const { parentResource, independentCollection } = + await setupParentWithIndependentCollection(); //creates fake parent form and fake list of independent subviews + + expect(parentResource.needsSaved).toBe(false); //since we just created a new record, the needsSaved needs to be false + + const existingCollectionObject = independentCollection.models[0]; //get the first collection object from collection + existingCollectionObject.set('text1', 'changed-value'); //change a field in the collection object + + expect(parentResource.needsSaved).toBe(true); //after you change, the save button should light up + + await parentResource.save(); //save + + expect(parentResource.needsSaved).toBe(false); //after saving, save button should be disabled + // Change is preserved in memory after save + expect(existingCollectionObject.get('text1')).toBe('changed-value'); //the change we made is still in memory + }); + + test('modifying a sub-record of an independent resource propagates needsSaved to base record and save completes', async () => { const { parentResource, independentCollection } = await setupParentWithIndependentCollection(); expect(parentResource.needsSaved).toBe(false); - const existingCollectionObject = independentCollection.models[0]; - existingCollectionObject.set('text1', 'changed-value'); + const collectionObject = independentCollection.models[0]; + const determinations = + collectionObject.getDependentResource('determinations'); + const determination = determinations!.models[0]; + determination.set('number1', 99); + // Change to sub-record propagates needsSaved all the way up to base record expect(parentResource.needsSaved).toBe(true); await parentResource.save(); expect(parentResource.needsSaved).toBe(false); - // Change is preserved in memory after save - expect(existingCollectionObject.get('text1')).toBe('changed-value'); + // Change to sub-record is preserved in memory after save + expect(determination.get('number1')).toBe(99); }); });