From 9c89a8a7cc1b83c4ccc178aab1f4c6e452238b61 Mon Sep 17 00:00:00 2001 From: aasandei-vsp Date: Wed, 12 Aug 2026 11:46:43 +0300 Subject: [PATCH] Replace navigateLean with getWithChildren for the publish functionality The publish dialog polls the public route in order to find the copy that was just made. Now the polling will happen through getWithChildren. Issue: PER-10679 --- .../publish/publish.component.spec.ts | 238 ++++++++++++++---- .../components/publish/publish.component.ts | 15 +- .../shared/services/api/folder.repo.spec.ts | 44 ++++ src/app/shared/services/api/folder.repo.ts | 5 + 4 files changed, 256 insertions(+), 46 deletions(-) diff --git a/src/app/file-browser/components/publish/publish.component.spec.ts b/src/app/file-browser/components/publish/publish.component.spec.ts index 8319e2f87..7bf8703f6 100644 --- a/src/app/file-browser/components/publish/publish.component.spec.ts +++ b/src/app/file-browser/components/publish/publish.component.spec.ts @@ -3,7 +3,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { AccountService } from '@shared/services/account/account.service'; import { FolderVO, RecordVO } from '@models/index'; import { FolderResponse } from '@shared/services/api/folder.repo'; -import { Observable } from 'rxjs'; import { MessageService } from '@shared/services/message/message.service'; import { EventService } from '@shared/services/event/event.service'; import { DIALOG_DATA, DialogRef } from '@angular/cdk/dialog'; @@ -12,50 +11,80 @@ import { ArchiveVO } from '../../../models/archive-vo'; import { ApiService } from '../../../shared/services/api/api.service'; import { PublishComponent } from './publish.component'; -const mockAccountService = { - getArchive: () => { - const archive = new ArchiveVO({ accessRole: 'access.role.viewer' }); - return archive; - }, - getRootFolder: () => ({ - ChildItemVOs: [], - }), - refreshAccountDebounced: () => {}, -}; +const MOCK_PUBLIC_ROOT = new FolderVO({ + folderId: '123456', + folder_linkId: 55, + archiveNbr: '0001-0000', + type: 'type.folder.root.public', +}); + +// Shaped the way getWithChildren returns it: a v1-style envelope whose child items +// are the converted Stela folders. +function publicRootResponseWithChildren( + children: Array>, +): FolderResponse { + return new FolderResponse({ + isSuccessful: true, + isSystemUp: true, + Results: [ + { + data: [ + { + FolderVO: { + ...MOCK_PUBLIC_ROOT, + ChildItemVOs: children, + }, + }, + ], + }, + ], + }); +} class MockDialogRef { close() {} } -const mockApiService = { - folder: { - copy: async ( - folderVOs: FolderVO[], - destination: FolderVO, - ): Promise => await Promise.resolve(new FolderResponse({})), - navigateLean: (folder: FolderVO): Observable => - new Observable(), - }, - publish: { - getInternetArchiveLink: async () => ({ - getPublishIaVO: () => null, - }), - publishToInternetArchive: async () => ({ - getPublishIaVO: () => null, - }), - }, - record: { - copy: async () => ({ - getRecordVO: () => new RecordVO({}), - }), - }, -}; - describe('PublishComponent', () => { let component: PublishComponent; let fixture: ComponentFixture; + let mockApiService: any; + let mockAccountService: any; + let showErrorSpy: jasmine.Spy; beforeEach(async () => { + mockAccountService = { + getArchive: () => new ArchiveVO({ accessRole: 'access.role.owner' }), + getRootFolder: () => ({ ChildItemVOs: [MOCK_PUBLIC_ROOT] }), + refreshAccountDebounced: () => {}, + }; + + mockApiService = { + folder: { + copy: jasmine + .createSpy('copy') + .and.resolveTo(new FolderResponse({ isSuccessful: true })), + getWithChildren: jasmine + .createSpy('getWithChildren') + .and.resolveTo(publicRootResponseWithChildren([])), + }, + publish: { + getInternetArchiveLink: async () => ({ + getPublishIaVO: () => null, + }), + publishToInternetArchive: async () => ({ + getPublishIaVO: () => null, + }), + }, + record: { + copy: async () => ({ + getRecordVO: () => new RecordVO({}), + }), + }, + }; + + showErrorSpy = jasmine.createSpy('showError'); + await TestBed.configureTestingModule({ declarations: [PublishComponent], providers: [ @@ -71,15 +100,11 @@ describe('PublishComponent', () => { EventService, { provide: MessageService, - useValue: { - showError: () => {}, - }, + useValue: { showError: showErrorSpy }, }, { provide: Router, - useValue: { - navigate: () => {}, - }, + useValue: { navigate: () => {} }, }, ], schemas: [CUSTOM_ELEMENTS_SCHEMA], @@ -94,7 +119,11 @@ describe('PublishComponent', () => { expect(component).toBeTruthy(); }); - it('should disaple the public to internet archive button if the user does not have the correct access role', () => { + it('should disable the public to internet archive button if the user does not have the correct access role', () => { + mockAccountService.getArchive = () => + new ArchiveVO({ accessRole: 'access.role.viewer' }); + fixture = TestBed.createComponent(PublishComponent); + component = fixture.componentInstance; component.publicItem = new RecordVO({ recordId: 1 }); component.publishIa = null; component.publicLink = null; @@ -105,4 +134,129 @@ describe('PublishComponent', () => { expect(button.disabled).toBeTruthy(); }); + + describe('publishing a folder', () => { + beforeEach(() => { + component.sourceItem = new FolderVO({ + folderId: '900', + archiveNbr: '0002-0001', + folder_linkId: 12, + displayName: 'Trip to Iceland', + type: 'type.folder.private', + }); + }); + + it('should load the public root through getWithChildren', async () => { + mockApiService.folder.getWithChildren.and.resolveTo( + publicRootResponseWithChildren([ + { + folderId: '901', + archiveNbr: '0001-0002', + folder_linkId: 71, + displayName: 'Trip to Iceland', + type: 'type.folder.public', + updatedDT: '2026-08-10T10:00:00Z', + ChildItemVOs: [], + }, + ]), + ); + + await component.publishItem(); + + expect(mockApiService.folder.getWithChildren).toHaveBeenCalledWith([ + MOCK_PUBLIC_ROOT, + ]); + + expect(mockApiService.folder.getWithChildren).toHaveBeenCalledTimes(1); + }); + + it('should pick the most recently updated folder matching the source name', async () => { + mockApiService.folder.getWithChildren.and.resolveTo( + publicRootResponseWithChildren([ + { + folderId: '901', + archiveNbr: '0001-0002', + folder_linkId: 71, + displayName: 'Trip to Iceland', + type: 'type.folder.public', + updatedDT: '2026-01-01T10:00:00Z', + ChildItemVOs: [], + }, + { + folderId: '902', + archiveNbr: '0001-0003', + folder_linkId: 72, + displayName: 'Trip to Iceland', + type: 'type.folder.public', + updatedDT: '2026-08-10T10:00:00Z', + ChildItemVOs: [], + }, + ]), + ); + + await component.publishItem(); + + // The newer of the two copies. This asserts the selection given a mapped + // updatedDT; that the Stela conversion actually populates it is covered + // in folder.repo.spec.ts. + expect(component.publicItem.folder_linkId).toBe(72); + expect(component.publicLink).toContain('0001-0003'); + }); + + it('should ignore child records when looking for the copy', async () => { + mockApiService.folder.getWithChildren.and.resolveTo( + publicRootResponseWithChildren([ + { + recordId: '500', + archiveNbr: '0001-0009', + folder_linkId: 80, + displayName: 'Trip to Iceland', + updatedDT: '2026-08-10T12:00:00Z', + }, + { + folderId: '901', + archiveNbr: '0001-0002', + folder_linkId: 71, + displayName: 'Trip to Iceland', + type: 'type.folder.public', + updatedDT: '2026-08-10T10:00:00Z', + ChildItemVOs: [], + }, + ]), + ); + + await component.publishItem(); + + expect(component.publicItem instanceof FolderVO).toBeTrue(); + expect(component.publicItem.folder_linkId).toBe(71); + }); + + it('should surface a generic error when the request rejects without a message', async () => { + mockApiService.folder.getWithChildren.and.rejectWith( + new Error('500 from the server'), + ); + + await component.publishItem(); + + expect(showErrorSpy).toHaveBeenCalledWith({ + message: 'error.generic.internal', + translate: true, + }); + + expect(component.waiting).toBeFalse(); + }); + + it('should keep showing the server message when one is available', async () => { + mockApiService.folder.copy.and.rejectWith({ + getMessage: () => 'warning.record.copy_status', + }); + + await component.publishItem(); + + expect(showErrorSpy).toHaveBeenCalledWith({ + message: + 'Sorry, this record cannot be copied or published until processing completes.', + }); + }); + }); }); diff --git a/src/app/file-browser/components/publish/publish.component.ts b/src/app/file-browser/components/publish/publish.component.ts index 6c418e652..1c3b4aec1 100644 --- a/src/app/file-browser/components/publish/publish.component.ts +++ b/src/app/file-browser/components/publish/publish.component.ts @@ -8,7 +8,6 @@ import { PublicLinkPipe } from '@shared/pipes/public-link.pipe'; import { AccountService } from '@shared/services/account/account.service'; import { GoogleAnalyticsService } from '@shared/services/google-analytics/google-analytics.service'; import { EVENTS } from '@shared/services/google-analytics/events'; -import { FolderResponse } from '@shared/services/api/index.repo'; import { PublicRoutePipe } from '@shared/pipes/public-route.pipe'; import { Router } from '@angular/router'; import { PublishIaData } from '@models/publish-ia-vo'; @@ -80,9 +79,9 @@ export class PublishComponent { let tries = 0; while (!this.publicItem && tries < 10) { tries += 1; - const publicRootResponse = (await this.api.folder - .navigateLean(publicRoot) - .toPromise()) as FolderResponse; + const publicRootResponse = await this.api.folder.getWithChildren([ + publicRoot, + ]); const publicRootFull = publicRootResponse.getFolderVO(true); const publicFolders: FolderVO[] = publicRootFull.ChildItemVOs.filter( (i) => i instanceof FolderVO, @@ -125,6 +124,14 @@ export class PublishComponent { message: err.getMessage(), }); } + } else { + // getWithChildren rejects with the raw HTTP error rather than a + // FolderResponse, so there is no server message to read. Without + // this branch the failure would be swallowed silently. + this.messageService.showError({ + message: 'error.generic.internal', + translate: true, + }); } } finally { this.waiting = false; diff --git a/src/app/shared/services/api/folder.repo.spec.ts b/src/app/shared/services/api/folder.repo.spec.ts index d8dbd768d..c8a8927ed 100644 --- a/src/app/shared/services/api/folder.repo.spec.ts +++ b/src/app/shared/services/api/folder.repo.spec.ts @@ -289,6 +289,50 @@ describe('Folder repo', () => { }); }); + describe('folder timestamps', () => { + it('should map createdAt and updatedAt onto the FolderVO', async () => { + const folderVO = new FolderVO({ folderId: 123 }); + + httpV2Spy.get.and.returnValue(of([{ items: [mockStelaFolder] }])); + + const result = await folderRepo.getStelaFolderVOs([folderVO]); + const folder = result.getFolderVOs()[0]; + + expect(folder.createdDT).toBe('2024-01-01T00:00:00Z'); + expect(folder.updatedDT).toBe('2024-01-02T00:00:00Z'); + }); + + it('should map timestamps onto child folders too, so callers can pick the most recent one', async () => { + const olderChild = { + ...mockStelaFolder, + folderId: '900', + displayName: 'Older', + updatedAt: '2024-03-01T00:00:00Z', + }; + const newerChild = { + ...mockStelaFolder, + folderId: '901', + displayName: 'Newer', + updatedAt: '2024-05-01T00:00:00Z', + }; + + httpV2Spy.get.and.returnValues( + of([{ items: [mockStelaFolder] }]), + of([{ items: [olderChild, newerChild] }]), + ); + + const result = await folderRepo.getWithChildren([ + new FolderVO({ folderId: 123 }), + ]); + const children = result.getFolderVO(true).ChildItemVOs; + + expect(children.map((child) => child.updatedDT)).toEqual([ + '2024-03-01T00:00:00Z', + '2024-05-01T00:00:00Z', + ]); + }); + }); + describe('getStelaFolderVOs', () => { it('should fetch single folder and return FolderResponse', async () => { const folderVO = new FolderVO({ folderId: 123 }); diff --git a/src/app/shared/services/api/folder.repo.ts b/src/app/shared/services/api/folder.repo.ts index 364fd0235..148fb30c8 100644 --- a/src/app/shared/services/api/folder.repo.ts +++ b/src/app/shared/services/api/folder.repo.ts @@ -143,6 +143,11 @@ const convertStelaFolderToFolderVO = (stelaFolder: StelaFolder): FolderVO => { displayTime: stelaFolder.displayTime, derivedDT: stelaFolder.displayTimestamp, derivedEndDT: stelaFolder.displayEndTimestamp, + // Stela names these createdAt / updatedAt. Records already map them; folders + // did not, so anything picking the most recently updated folder was comparing + // undefined values. + createdDT: stelaFolder.createdAt, + updatedDT: stelaFolder.updatedAt, note: '', description: stelaFolder.description, sort: stelaFolder.sort,