From 5f4575910ae4a98130b8e63d3cc29fcd36123c56 Mon Sep 17 00:00:00 2001 From: aasandei-vsp Date: Sat, 19 Sep 2026 15:50:35 +0300 Subject: [PATCH 1/2] Match the folders and records fetched by id and not by position Stela leaves out records that it cannot return, so the list of records we asked for might not be what we receive. Changing the mapping to actually look for id won't mark an item that has been skipped as fully fetched. Issue: PER-10778 modified: src/app/shared/services/data/data.service.ts --- .../shared/services/data/data.service.spec.ts | 103 +++++++++++++++++- src/app/shared/services/data/data.service.ts | 59 ++++++++-- 2 files changed, 150 insertions(+), 12 deletions(-) diff --git a/src/app/shared/services/data/data.service.spec.ts b/src/app/shared/services/data/data.service.spec.ts index cfb7b47ef..83ef8ba04 100644 --- a/src/app/shared/services/data/data.service.spec.ts +++ b/src/app/shared/services/data/data.service.spec.ts @@ -5,7 +5,10 @@ import { HttpV2Service } from '@shared/services/http-v2/http-v2.service'; import { DataService } from '@shared/services/data/data.service'; import { FolderVO, FolderVOData, RecordVO } from '@root/app/models'; -import { FolderResponse } from '@shared/services/api/index.repo'; +import { + FolderResponse, + RecordResponse, +} from '@shared/services/api/index.repo'; import { of } from 'rxjs'; import { DataStatus } from '@models/data-status.enum'; @@ -234,6 +237,104 @@ describe('DataService', () => { await service.fetchFullItems([]); }); + describe('fetchFullItems', () => { + const buildRecordResponse = (recordsData: object[]) => + new RecordResponse({ + isSuccessful: true, + Results: recordsData.map((recordData) => ({ + data: [{ RecordVO: recordData }], + })), + }); + + const buildFolderVOsResponse = (foldersData: FolderVOData[]) => + new FolderResponse({ + isSuccessful: true, + Results: foldersData.map((folderData) => ({ + data: [{ FolderVO: folderData }], + })), + }); + + let service: DataService; + let recordGet: jasmine.Spy; + let getStelaFolderVOs: jasmine.Spy; + + beforeEach(() => { + service = TestBed.inject(DataService); + const api = TestBed.inject(ApiService); + recordGet = spyOn(api.record, 'get'); + getStelaFolderVOs = spyOn(api.folder, 'getStelaFolderVOs'); + service.setCurrentFolder(testFolder); + }); + + it('should match records to the response by recordId, not by position', async () => { + const firstRecord = new RecordVO({ recordId: '11', archiveNbr: 'a' }); + const secondRecord = new RecordVO({ recordId: '22', archiveNbr: 'b' }); + recordGet.and.resolveTo( + buildRecordResponse([ + { recordId: '22', displayName: 'second' }, + { recordId: '11', displayName: 'first' }, + ]), + ); + + await service.fetchFullItems([firstRecord, secondRecord]); + + expect(firstRecord.displayName).toBe('first'); + expect(secondRecord.displayName).toBe('second'); + }); + + it('should leave a record the response skipped below Full so it can be fetched again', async () => { + const returnedRecord = new RecordVO({ recordId: '11', archiveNbr: 'a' }); + const skippedRecord = new RecordVO({ + recordId: '22', + archiveNbr: 'b', + displayName: 'lean name', + }); + recordGet.and.resolveTo( + buildRecordResponse([{ recordId: '11', displayName: 'first' }]), + ); + + await service.fetchFullItems([returnedRecord, skippedRecord]); + + expect(returnedRecord.dataStatus).toBe(DataStatus.Full); + expect(skippedRecord.dataStatus).toBeLessThan(DataStatus.Full); + expect(skippedRecord.displayName).toBe('lean name'); + }); + + it('should match folders to the response by folderId, not by position', async () => { + const folderWithoutId = new FolderVO({ folder_linkId: 1 }); + const folderWithId = new FolderVO({ folderId: '33', folder_linkId: 2 }); + getStelaFolderVOs.and.resolveTo( + buildFolderVOsResponse([{ folderId: '33', displayName: 'real name' }]), + ); + + await service.fetchFullItems([folderWithoutId, folderWithId]); + + expect(folderWithId.displayName).toBe('real name'); + expect(folderWithoutId.displayName).toBeUndefined(); + expect(folderWithoutId.dataStatus).toBeLessThan(DataStatus.Full); + }); + + it('should clear isFetching once the items have been fetched', async () => { + const record = new RecordVO({ recordId: '11', archiveNbr: 'a' }); + recordGet.and.resolveTo( + buildRecordResponse([{ recordId: '11', displayName: 'first' }]), + ); + + await service.fetchFullItems([record]); + + expect(record.isFetching).toBeFalse(); + }); + + it('should clear isFetching when the request fails', async () => { + const record = new RecordVO({ recordId: '11', archiveNbr: 'a' }); + recordGet.and.rejectWith(new Error('nope')); + + await service.fetchFullItems([record]); + + expect(record.isFetching).toBeFalse(); + }); + }); + describe('refreshCurrentFolder', () => { const berlinFolderData = { folderId: '1', diff --git a/src/app/shared/services/data/data.service.ts b/src/app/shared/services/data/data.service.ts index 51b5a7b62..74860d004 100644 --- a/src/app/shared/services/data/data.service.ts +++ b/src/app/shared/services/data/data.service.ts @@ -40,6 +40,22 @@ const isSameId = (a: ItemId, b: ItemId): boolean => { return String(a) === String(b); }; +const indexItemsById = ( + items: T[] = [], + getId: (item: T) => ItemId, +): Map => { + const itemsById = new Map(); + + items.forEach((item) => { + const id = getId(item); + if (id !== null && id !== undefined) { + itemsById.set(String(id), item); + } + }); + + return itemsById; +}; + export type SelectedItemsSet = Set; export interface SelectKeyEvent { @@ -375,6 +391,7 @@ export class DataService { itemResolves.push(resolve); itemRejects.push(reject); }); + item.fetched.catch(noop); if (item.isRecord) { records.push(item); @@ -418,23 +435,42 @@ export class DataService { fullFolders = folderResponse.getFolderVOs(); } - for (let i = 0; i < records.length; i += 1) { - records[i].update(fullRecords[i]); - records[i].dataStatus = DataStatus.Full; - this.tags.checkTagsOnItem(records[i]); - } + const fullRecordsById = indexItemsById( + fullRecords, + (fullRecord) => fullRecord.recordId, + ); + const fullFoldersById = indexItemsById( + fullFolders, + (fullFolder) => fullFolder.folderId, + ); + + records.forEach((record: RecordVO) => { + const fullRecord = fullRecordsById.get(String(record.recordId)); + if (!fullRecord) { + return; + } + + record.update(fullRecord); + record.dataStatus = DataStatus.Full; + this.tags.checkTagsOnItem(record); + }); + + folders.forEach((folder: FolderVO) => { + const fullFolder = fullFoldersById.get(String(folder.folderId)); + if (!fullFolder) { + return; + } - for (let i = 0; i < folders.length; i += 1) { - const folder = folders[i] as FolderVO; folder.update( - fullFolders[i] as FolderVOData, - folders[i] === this.currentFolder, + fullFolder as FolderVOData, + folder === this.currentFolder, ); folder.dataStatus = DataStatus.Full; - this.tags.checkTagsOnItem(folders[i]); - } + this.tags.checkTagsOnItem(folder); + }); itemResolves.forEach((resolve, index) => { + items[index].isFetching = false; items[index].fetched = null; this.byArchiveNbr[items[index].archiveNbr] = items[index]; resolve(); @@ -446,6 +482,7 @@ export class DataService { }) .catch(() => { itemRejects.forEach((reject, index) => { + items[index].isFetching = false; items[index].fetched = null; reject(); }); From a112eb8d45f5343072d9cf7150a7c30be92a8d39 Mon Sep 17 00:00:00 2001 From: aasandei-vsp Date: Sat, 19 Sep 2026 15:55:31 +0300 Subject: [PATCH 2/2] Read the record when changes, not only on initialization in the video renderer Up until now, we would only read the record, in this case the video when the component was initialized. Now we read the record on every change. This means the "Video is processing" message would appear when the item was actually still not available, but then disappear when the data has finished processing. Issue: PER-10778 --- .../components/video/video.component.html | 8 ++- .../components/video/video.component.spec.ts | 71 +++++++++++++++++++ .../components/video/video.component.ts | 39 +++++----- 3 files changed, 98 insertions(+), 20 deletions(-) diff --git a/src/app/shared/components/video/video.component.html b/src/app/shared/components/video/video.component.html index 7ee198e45..6007b29d8 100644 --- a/src/app/shared/components/video/video.component.html +++ b/src/app/shared/components/video/video.component.html @@ -1,6 +1,10 @@