From 50a65962ad78cf51afc70ae1b5263b8e717f6827 Mon Sep 17 00:00:00 2001 From: aasandei-vsp Date: Wed, 17 Jun 2026 16:20:30 +0300 Subject: [PATCH 1/2] Add the EDTF date picker to the full-screen record view behind a flag Gate the date control on the edtf-date flag: show the EDTF dropdown/modal when it's on, and the legacy inline date field when it's off. Port the sidebar's date handling to the file viewer: - Cache displayTimeObject in a field, recomputed per record in initRecord, instead of a getter that allocated on every change-detection cycle. - Re-sync after every save via a shared saveDisplayTime helper (used by both the inline picker and the modal), including on failure. - Track the modal's closed subscription so it unsubscribes on destroy. Issue: PER-10415 --- .../file-viewer/file-viewer.component.html | 50 +++++++----- .../file-viewer/file-viewer.component.spec.ts | 81 +++++++++++++++++++ .../file-viewer/file-viewer.component.ts | 69 ++++++++++++++-- 3 files changed, 176 insertions(+), 24 deletions(-) diff --git a/src/app/file-browser/components/file-viewer/file-viewer.component.html b/src/app/file-browser/components/file-viewer/file-viewer.component.html index 632471ff6..7d19ad537 100644 --- a/src/app/file-browser/components/file-viewer/file-viewer.component.html +++ b/src/app/file-browser/components/file-viewer/file-viewer.component.html @@ -125,26 +125,38 @@ > } + @if (showEdtfDatePicker) { +
+ +
+ } - - - - + @if (!showEdtfDatePicker) { + + + + + } 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 7bc80b80e..db8cb326d 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 @@ -15,7 +15,13 @@ import { ApiService } from '@shared/services/api/api.service'; import { FeatureFlagService } from '@root/app/feature-flag/services/feature-flag.service'; import { MockComponent } from 'ng-mocks'; import { GetThumbnailPipe } from '@shared/pipes/get-thumbnail.pipe'; +import { MessageService } from '@shared/services/message/message.service'; +import { + DateTimeModel, + EdtfService, +} from '@shared/services/edtf-service/edtf.service'; import { TagsComponent } from '../../../shared/components/tags/tags.component'; +import { EditDateTimeModalService } from '../edit-date-time-modal/edit-date-time-modal.service'; import { FileViewerComponent } from './file-viewer.component'; @Pipe({ name: 'dsFileSize', standalone: false }) @@ -229,6 +235,19 @@ describe('FileViewerComponent', () => { isEnabled: (flag: string) => featureFlagsEnabled.get(flag) ?? false, }, }, + { + provide: MessageService, + useValue: { + showError: () => {}, + showMessage: () => {}, + }, + }, + { + provide: EditDateTimeModalService, + useValue: { + open: () => ({ closed: { subscribe: () => {} } }), + }, + }, ], schemas: [CUSTOM_ELEMENTS_SCHEMA], }).compileComponents(); @@ -244,6 +263,68 @@ describe('FileViewerComponent', () => { expect(component).not.toBeNull(); }); + describe('edtf-date feature flag', () => { + it('should show the EDTF date picker when the edtf-date flag is enabled', async () => { + featureFlagsEnabled.set('edtf-date', true); + await recreateComponent(); + + expect(component.showEdtfDatePicker).toBe(true); + expect( + fixture.nativeElement.querySelector('pr-sidebar-date-picker'), + ).toBeTruthy(); + }); + + it('should show the legacy date field and hide the EDTF picker when the edtf-date flag is disabled', async () => { + featureFlagsEnabled.set('edtf-date', false); + await recreateComponent(); + + expect(component.showEdtfDatePicker).toBe(false); + expect( + fixture.nativeElement.querySelector('pr-sidebar-date-picker'), + ).toBeNull(); + + const dateRowLabel = Array.from( + fixture.nativeElement.querySelectorAll('.metadata-table td'), + ).find((td: HTMLElement) => td.textContent?.trim() === 'Date'); + + expect(dateRowLabel).toBeTruthy(); + }); + }); + + describe('EDTF date handling', () => { + const recordWithDate = () => + new RecordVO({ + type: 'document', + displayName: 'Dated Doc', + TagVOs: [], + displayTime: '1985-05-20', + }); + + it('should compute the cached display time from the record on init', async () => { + activatedRouteData.currentRecord = recordWithDate(); + await recreateComponent(); + + expect(component.displayTimeObject?.date.year).toBe('1985'); + }); + + it('should reset the cached display time and show one error when an invalid date is saved', async () => { + activatedRouteData.currentRecord = recordWithDate(); + await recreateComponent(); + + const edtfService = TestBed.inject(EdtfService); + spyOn(edtfService, 'toEdtfDate').and.throwError('invalid date'); + const showErrorSpy = spyOn(TestBed.inject(MessageService), 'showError'); + + await component.onDateSaved({ + date: { year: 'bad' } as never, + time: { format: 'am' }, + } as DateTimeModel); + + expect(showErrorSpy).toHaveBeenCalledTimes(1); + expect(component.displayTimeObject?.date.year).toBe('1985'); + }); + }); + it('should have two tags components', () => { const tagsComponents = fixture.nativeElement.querySelectorAll('pr-tags'); 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 9dd8b679a..c918383e8 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 @@ -30,7 +30,13 @@ import { GetAccessFile } from '@models/get-access-file'; import { ShareLinksService } from '@root/app/share-links/services/share-links.service'; import { ApiService } from '@shared/services/api/api.service'; import { FeatureFlagService } from '@root/app/feature-flag/services/feature-flag.service'; +import { + DateTimeModel, + EdtfService, +} from '@shared/services/edtf-service/edtf.service'; +import { MessageService } from '@shared/services/message/message.service'; import { TagsService } from '../../../core/services/tags/tags.service'; +import { EditDateTimeModalService } from '../edit-date-time-modal/edit-date-time-modal.service'; @Component({ selector: 'pr-file-viewer', @@ -62,6 +68,12 @@ export class FileViewerComponent implements OnInit, OnDestroy { public canEdit: boolean; + public showEdtfDatePicker = false; + + public editingDate: boolean = false; + + public displayTimeObject: DateTimeModel | null = null; + // Swiping private touchElement: HTMLElement; private thumbElement: HTMLElement; @@ -77,10 +89,10 @@ export class FileViewerComponent implements OnInit, OnDestroy { // UI public useMinimalView = false; - public editingDate: boolean = false; private bodyScrollTop: number; private itemTagsSubscription: Subscription; private tagsSubscription: Subscription; + private dateModalSubscription?: Subscription; private isUnlistedShare = true; constructor( @@ -88,6 +100,7 @@ export class FileViewerComponent implements OnInit, OnDestroy { private route: ActivatedRoute, private element: ElementRef, private dataService: DataService, + private message: MessageService, @Inject(DOCUMENT) private document: any, public sanitizer: DomSanitizer, private accountService: AccountService, @@ -97,10 +110,14 @@ export class FileViewerComponent implements OnInit, OnDestroy { private shareLinksService: ShareLinksService, private api: ApiService, private feature: FeatureFlagService, + private edtfService: EdtfService, + private editDateTimeModalService: EditDateTimeModalService, ) { // store current scroll position in file list this.bodyScrollTop = window.scrollY; + this.showEdtfDatePicker = this.feature.isEnabled('edtf-date'); + const resolvedRecord = route.snapshot.data.currentRecord; this.allTags = tagsService.getTags(); @@ -186,6 +203,7 @@ export class FileViewerComponent implements OnInit, OnDestroy { }); this.itemTagsSubscription.unsubscribe(); this.tagsSubscription.unsubscribe(); + this.dateModalSubscription?.unsubscribe(); } private setRecordsToPreview(resolvedRecord: RecordVO) { @@ -244,6 +262,7 @@ export class FileViewerComponent implements OnInit, OnDestroy { this.replayUrl = this.getReplayUrl(); } this.setCurrentTags(); + this.updateDisplayTimeObject(); } toggleSwipe(value: boolean) { @@ -432,6 +451,50 @@ export class FileViewerComponent implements OnInit, OnDestroy { } } + private updateDisplayTimeObject(): void { + const timeSource = + this.currentRecord?.displayTime || this.currentRecord?.displayDT; + try { + this.displayTimeObject = timeSource + ? this.edtfService.toDateTimeModel(timeSource) + : null; + } catch (err) { + this.displayTimeObject = null; + this.message.showError({ message: err?.message }); + } + } + + public async onDateSaved(result: DateTimeModel): Promise { + await this.saveDisplayTime(result); + } + + public async onDateMoreOptions(modalData: DateTimeModel): Promise { + const dialogRef = this.editDateTimeModalService.open(modalData); + + this.dateModalSubscription = dialogRef.closed.subscribe(async (result) => { + if (result) { + await this.saveDisplayTime(result); + } + }); + } + + private async saveDisplayTime(result: DateTimeModel): Promise { + try { + const newDisplayTime = this.edtfService.toEdtfDate(result); + await this.onFinishEditing('displayTime', newDisplayTime); + } catch (err) { + this.message.showError({ message: err?.message }); + } finally { + // Recompute so the picker re-syncs to the stored value, whether the + // save came from the inline picker or the modal, and on failure too. + this.updateDisplayTimeObject(); + } + } + + public onDateToggle(active: boolean): void { + this.editingDate = active; + } + public async onFinishEditing( property: KeysOfType, value: string, @@ -455,10 +518,6 @@ export class FileViewerComponent implements OnInit, OnDestroy { } } - public onDateToggle(active: boolean): void { - this.editingDate = active; - } - public onDownloadClick(): void { this.dataService.downloadFile(this.currentRecord); } From 67e2e7f81bda3d17c9408d9cd0567396d8e26a68 Mon Sep 17 00:00:00 2001 From: aasandei-vsp Date: Wed, 17 Jun 2026 16:21:07 +0300 Subject: [PATCH 2/2] Show the error banner above full-screen components The global error banner sat at a lower z-index than full-screen views (e.g. the file viewer), so errors rendered but were hidden behind the overlay. Raise it above the full-screen layer and the date/time picker dropdowns that open within it. Issue: PER-10415 --- src/app/shared/components/message/message.component.scss | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/shared/components/message/message.component.scss b/src/app/shared/components/message/message.component.scss index 5bf051e9c..1aafc3ab2 100644 --- a/src/app/shared/components/message/message.component.scss +++ b/src/app/shared/components/message/message.component.scss @@ -6,7 +6,10 @@ $transition-length: 0.33s; left: 0; right: 0; transform: translateY(-100%); - z-index: 6; + // Must sit above full-screen components (z-index 10, e.g. the file viewer) + // and the date/time picker dropdowns (z-index 20) that open within them, + // otherwise the error banner renders but is hidden behind the overlay. + z-index: 30; } .alert {
Date - -
Date + +
Uploaded {{ currentRecord.createdDT | date }}