- @if (
- showThumbnail &&
- isZoomableImage &&
- !isDocument &&
- !isVideo &&
- !isAudio &&
- !isWebArchive
- ) {
-
- }
- @if (isWebArchive && replayUrl) {
-
- }
- @if (
- showThumbnail &&
- !isZoomableImage &&
- !isDocument &&
- !isVideo &&
- !isAudio &&
- !isWebArchive
- ) {
-
- }
- @if (isVideo) {
-
- }
- @if (isAudio) {
-
- }
- @if (isDocument && documentUrl) {
-
+ @if (previewState === RecordPreviewState.Ready) {
+ @if (
+ showThumbnail &&
+ isZoomableImage &&
+ !isDocument &&
+ !isVideo &&
+ !isAudio &&
+ !isWebArchive
+ ) {
+
+ }
+ @if (isWebArchive && replayUrl) {
+
+ }
+ @if (
+ showThumbnail &&
+ !isZoomableImage &&
+ !isDocument &&
+ !isVideo &&
+ !isAudio &&
+ !isWebArchive
+ ) {
+
+ }
+ @if (isVideo) {
+
+ }
+ @if (isAudio) {
+
+ }
+ @if (isDocument && documentUrl) {
+
+ }
+ } @else {
+
}
}
diff --git a/src/app/file-browser/components/file-viewer/file-viewer.component.spec.ts b/src/app/file-browser/components/file-viewer/file-viewer.component.spec.ts
index fd4aa5dc3..7c0568324 100644
--- a/src/app/file-browser/components/file-viewer/file-viewer.component.spec.ts
+++ b/src/app/file-browser/components/file-viewer/file-viewer.component.spec.ts
@@ -1,5 +1,12 @@
import { CUSTOM_ELEMENTS_SCHEMA, Pipe, PipeTransform } from '@angular/core';
-import { ComponentFixture, TestBed } from '@angular/core/testing';
+import {
+ ComponentFixture,
+ fakeAsync,
+ flush,
+ flushMicrotasks,
+ TestBed,
+ tick,
+} from '@angular/core/testing';
import { Router, ActivatedRoute } from '@angular/router';
import { Subject } from 'rxjs';
import { HttpClientTestingModule } from '@angular/common/http/testing';
@@ -21,9 +28,15 @@ import {
DateTimeModel,
EdtfService,
} from '@shared/services/edtf-service/edtf.service';
-import { TagsComponent } from '../../../shared/components/tags/tags.component';
+import { GeneratedFileStatus } from '@models/generated-file-status';
+import { RecordPreviewState } from '@models/record-preview-state';
+import { FileFormat } from '@models/file-vo';
import { EditDateTimeModalService } from '../edit-date-time-modal/edit-date-time-modal.service';
-import { FileViewerComponent } from './file-viewer.component';
+import { TagsComponent } from '../../../shared/components/tags/tags.component';
+import {
+ FileViewerComponent,
+ PREPARING_RECORD_REFRESH_INTERVAL,
+} from './file-viewer.component';
@Pipe({ name: 'dsFileSize', standalone: false })
class MockFileSizePipe implements PipeTransform {
@@ -880,4 +893,144 @@ describe('FileViewerComponent', () => {
expect(displayValue).toBe('2024-01-01T00:00:00.000Z');
});
});
+
+ describe('access copy preview states', () => {
+ const originalDocx = {
+ fileId: 1,
+ size: 1024,
+ format: FileFormat.Original,
+ fileURL: 'https://example.com/original.docx',
+ downloadURL: 'https://example.com/original.docx?download',
+ type: 'type.file.document.docx',
+ };
+
+ const makeDocxRecord = (
+ accessCopyStatus: GeneratedFileStatus | null,
+ extraFiles: RecordVO['FileVOs'] = [],
+ ) =>
+ new RecordVO({
+ displayName: "Grandma Ruth's Recipes",
+ type: 'type.record.document',
+ TagVOs: [],
+ archiveNbr: '0001-0001',
+ FileVOs: [originalDocx, ...extraFiles],
+ accessCopyStatus,
+ });
+
+ const showRecord = (record: RecordVO) => {
+ component.currentRecord = record;
+ component.records = [record];
+ component.currentIndex = 0;
+ component.initRecord();
+ fixture.detectChanges();
+ };
+
+ const renderedPreviewState = (): HTMLElement | null =>
+ fixture.nativeElement.querySelector('pr-record-preview-state');
+
+ it('shows the preparing state while the access copy is processing', () => {
+ showRecord(makeDocxRecord(GeneratedFileStatus.Processing));
+
+ expect(component.previewState).toBe(RecordPreviewState.Preparing);
+ expect(component.originalFileExtension).toBe('docx');
+ expect(renderedPreviewState()).toBeTruthy();
+ });
+
+ it('shows the failed state instead of the video player', () => {
+ showRecord(
+ new RecordVO({
+ type: 'type.record.video',
+ TagVOs: [],
+ FileVOs: [{ ...originalDocx, type: 'type.file.video.mp4' }],
+ accessCopyStatus: GeneratedFileStatus.Failed,
+ }),
+ );
+
+ expect(renderedPreviewState()).toBeTruthy();
+ expect(fixture.nativeElement.querySelector('pr-video')).toBeNull();
+ });
+
+ it('keeps showing a legacy converted copy even when Stela says it failed', () => {
+ showRecord(
+ makeDocxRecord(GeneratedFileStatus.Failed, [
+ {
+ ...originalDocx,
+ fileId: 2,
+ format: FileFormat.Converted,
+ type: 'type.file.pdf.pdf',
+ },
+ ]),
+ );
+
+ expect(component.previewState).toBe(RecordPreviewState.Ready);
+ expect(renderedPreviewState()).toBeNull();
+ });
+
+ it('refreshes a preparing record until its copy is ready', fakeAsync(() => {
+ const preparingRecord = makeDocxRecord(GeneratedFileStatus.Processing);
+ const readyRecord = makeDocxRecord(GeneratedFileStatus.Ok, [
+ {
+ ...originalDocx,
+ fileId: 2,
+ format: FileFormat.ArchivematicaAccess,
+ type: 'type.file.pdf.pdf',
+ },
+ ]);
+ const getRecords = spyOn(
+ TestBed.inject(ApiService).record,
+ 'get',
+ ).and.resolveTo({ getRecordVO: () => readyRecord } as any);
+
+ showRecord(preparingRecord);
+ tick(PREPARING_RECORD_REFRESH_INTERVAL - 1);
+
+ expect(getRecords).not.toHaveBeenCalled();
+
+ tick(1);
+ flushMicrotasks();
+
+ expect(getRecords).toHaveBeenCalledOnceWith([preparingRecord], null);
+ expect(component.previewState).toBe(RecordPreviewState.Ready);
+
+ tick(PREPARING_RECORD_REFRESH_INTERVAL);
+
+ expect(getRecords).toHaveBeenCalledTimes(1);
+ component.ngOnDestroy();
+ flush();
+ }));
+
+ it('stops refreshing once the viewer is closed', fakeAsync(() => {
+ const getRecords = spyOn(TestBed.inject(ApiService).record, 'get');
+
+ showRecord(makeDocxRecord(GeneratedFileStatus.Processing));
+ component.ngOnDestroy();
+ tick(PREPARING_RECORD_REFRESH_INTERVAL);
+ flush();
+
+ expect(getRecords).not.toHaveBeenCalled();
+ }));
+
+ it('does not refresh a record that is already ready', fakeAsync(() => {
+ const getRecords = spyOn(TestBed.inject(ApiService).record, 'get');
+
+ showRecord(makeDocxRecord(null));
+ tick(PREPARING_RECORD_REFRESH_INTERVAL);
+
+ expect(component.previewState).toBe(RecordPreviewState.Unavailable);
+ expect(getRecords).not.toHaveBeenCalled();
+ component.ngOnDestroy();
+ flush();
+ }));
+
+ it('hides the download button in a public archive that disallows downloads', () => {
+ component.isPublicArchive = true;
+ component.allowDownloads = false;
+
+ expect(component.canDownload()).toBeFalse();
+
+ component.allowDownloads = true;
+
+ expect(component.canDownload()).toBeTrue();
+ });
+ });
});
diff --git a/src/app/file-browser/components/file-viewer/file-viewer.component.ts b/src/app/file-browser/components/file-viewer/file-viewer.component.ts
index 82af4c5ee..4d1889e25 100644
--- a/src/app/file-browser/components/file-viewer/file-viewer.component.ts
+++ b/src/app/file-browser/components/file-viewer/file-viewer.component.ts
@@ -27,6 +27,11 @@ import { SearchService } from '@search/services/search.service';
import { ZoomingImageViewerComponent } from '@shared/components/zooming-image-viewer/zooming-image-viewer.component';
import { FileFormat } from '@models/file-vo';
import { GetAccessFile } from '@models/get-access-file';
+import {
+ getOriginalFileExtension,
+ getRecordPreviewState,
+ RecordPreviewState,
+} from '@models/record-preview-state';
import { ShareLinksService } from '@root/app/share-links/services/share-links.service';
import { ApiService } from '@shared/services/api/api.service';
import { environment } from '@root/environments/environment';
@@ -39,6 +44,8 @@ import { FeatureFlagService } from '@root/app/feature-flag/services/feature-flag
import { TagsService } from '../../../core/services/tags/tags.service';
import { EditDateTimeModalService } from '../edit-date-time-modal/edit-date-time-modal.service';
+export const PREPARING_RECORD_REFRESH_INTERVAL = 10000;
+
@Component({
selector: 'pr-file-viewer',
templateUrl: './file-viewer.component.html',
@@ -58,6 +65,9 @@ export class FileViewerComponent implements OnInit, OnDestroy {
public isAudio = false;
public isDocument = false;
public isWebArchive = false;
+ public previewState = RecordPreviewState.Ready;
+ public originalFileExtension: string | undefined;
+ public readonly RecordPreviewState = RecordPreviewState;
public showThumbnail = true;
public isPublicArchive: boolean = false;
public allowDownloads: boolean = false;
@@ -95,6 +105,8 @@ export class FileViewerComponent implements OnInit, OnDestroy {
private tagsSubscription: Subscription;
private dateModalSubscription?: Subscription;
private isUnlistedShare = true;
+ private preparingRecordRefreshTimeout?: ReturnType