From 476f085f0cbb5dbd8c56f25efd551d6ca527f499 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 | 241 +++++++++++++++--- .../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, 260 insertions(+), 45 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..f31fa0bfe 100644 --- a/src/app/file-browser/components/publish/publish.component.spec.ts +++ b/src/app/file-browser/components/publish/publish.component.spec.ts @@ -3,59 +3,89 @@ 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 { GoogleAnalyticsService } from '@shared/services/google-analytics/google-analytics.service'; import { DIALOG_DATA, DialogRef } from '@angular/cdk/dialog'; import { Router } from '@angular/router'; 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 PUBLIC_ROOT = new FolderVO({ + folderId: '140683', + 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: { + ...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: [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 +101,15 @@ describe('PublishComponent', () => { EventService, { provide: MessageService, - useValue: { - showError: () => {}, - }, + useValue: { showError: showErrorSpy }, }, { provide: Router, - useValue: { - navigate: () => {}, - }, + useValue: { navigate: () => {} }, + }, + { + provide: GoogleAnalyticsService, + useValue: { sendEvent: () => {} }, }, ], schemas: [CUSTOM_ELEMENTS_SCHEMA], @@ -95,6 +125,10 @@ describe('PublishComponent', () => { }); it('should disaple 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 +139,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([ + 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 1e8033331..e97fe0412 100644 --- a/src/app/shared/services/api/folder.repo.spec.ts +++ b/src/app/shared/services/api/folder.repo.spec.ts @@ -288,6 +288,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 ef687f720..e2ecf9a53 100644 --- a/src/app/shared/services/api/folder.repo.ts +++ b/src/app/shared/services/api/folder.repo.ts @@ -138,6 +138,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,