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..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 @@ -15,6 +15,7 @@ import type { CollectionObjectAttribute, CollectionObjectType, Determination, + Accession, } from '../types'; requireContext(); @@ -469,6 +470,97 @@ 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('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({