diff --git a/PR_diff_base-qa_to_head-qa.diff b/PR_diff_base-qa_to_head-qa.diff new file mode 100644 index 000000000..c6c988a39 --- /dev/null +++ b/PR_diff_base-qa_to_head-qa.diff @@ -0,0 +1,672 @@ +diff --git a/src/app/file-browser/components/edit-date-time-modal/edit-date-time-modal.component.scss b/src/app/file-browser/components/edit-date-time-modal/edit-date-time-modal.component.scss +index 628a98be..78fc1427 100644 +--- a/src/app/file-browser/components/edit-date-time-modal/edit-date-time-modal.component.scss ++++ b/src/app/file-browser/components/edit-date-time-modal/edit-date-time-modal.component.scss +@@ -251,13 +251,7 @@ + } + + .pr-edtf-error { +- font-size: 12px; +- line-height: 16px; +- color: $red; +- word-wrap: break-word; +- overflow-wrap: break-word; +- white-space: normal; +- min-width: 0; ++ @include edtf-error-message; + } + } + +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 c441a243..ba24ee97 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) { ++
| Date | +-
+- |
+-
| Date | ++
++ |
++
| Uploaded | +{{ currentRecord.createdDT | date }} | +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 d06c6281..f8e3f8db 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 +@@ -16,7 +16,13 @@ import { FeatureFlagService } from '@root/app/feature-flag/services/feature-flag + import { MockComponent } from 'ng-mocks'; + import { GetThumbnailPipe } from '@shared/pipes/get-thumbnail.pipe'; + import { environment } from '@root/environments/environment'; ++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 }) +@@ -238,6 +244,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(); +@@ -253,6 +272,163 @@ 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', ++ }); ++ ++ beforeEach(() => { ++ featureFlagsEnabled.set('edtf-date', true); ++ }); ++ ++ it('should not parse the date or show an error when the edtf-date flag is disabled', async () => { ++ featureFlagsEnabled.set('edtf-date', false); ++ activatedRouteData.currentRecord = new RecordVO({ ++ type: 'document', ++ displayName: 'Invalid Date Doc', ++ TagVOs: [], ++ displayTime: 'not-a-valid-edtf-date', ++ }); ++ const showErrorSpy = spyOn(TestBed.inject(MessageService), 'showError'); ++ await recreateComponent(); ++ ++ expect(component.displayTimeObject).toBeNull(); ++ expect(showErrorSpy).not.toHaveBeenCalled(); ++ }); ++ ++ 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 save null when the date is cleared to empty', async () => { ++ activatedRouteData.currentRecord = recordWithDate(); ++ await recreateComponent(); ++ ++ await component.onDateSaved({ ++ date: { year: '', month: '', day: '' }, ++ time: { format: 'am' }, ++ }); ++ ++ expect(savedProperty).toEqual({ name: 'displayTime', value: null }); ++ }); ++ ++ it('should save the EDTF string unchanged for a non-empty date', async () => { ++ activatedRouteData.currentRecord = recordWithDate(); ++ await recreateComponent(); ++ ++ await component.onDateSaved({ ++ date: { year: '1990', month: '06', day: '15' }, ++ time: { format: 'am' }, ++ }); ++ ++ expect(savedProperty).toEqual({ ++ name: 'displayTime', ++ value: '1990-06-15', ++ }); ++ }); ++ ++ it('should re-sync the picker to the reverted value after a failed backend save', async () => { ++ activatedRouteData.currentRecord = recordWithDate(); ++ await recreateComponent(); ++ ++ // Mimic EditService on a server failure: optimistic update now, ++ // revert on a later macrotask. The re-sync must wait for this. ++ spyOn(TestBed.inject(EditService), 'saveItemVoProperty').and.callFake( ++ async (item, _property, value) => { ++ item.displayTime = value; ++ await new Promise((resolve) => { ++ setTimeout(resolve); ++ }); ++ item.displayTime = '1985-05-20'; ++ }, ++ ); ++ ++ await component.onDateSaved({ ++ date: { year: '1990', month: '06', day: '15' }, ++ time: { format: 'am' }, ++ }); ++ ++ expect(component.displayTimeObject?.date.year).toBe('1985'); ++ }); ++ ++ it('should show an empty date when displayTime is explicitly null, ignoring displayDT', async () => { ++ activatedRouteData.currentRecord = new RecordVO({ ++ type: 'document', ++ displayName: 'Cleared Doc', ++ TagVOs: [], ++ displayTime: null, ++ displayDT: '1985-05-20T00:00:00Z', ++ }); ++ await recreateComponent(); ++ ++ expect(component.displayTimeObject).toBeNull(); ++ }); ++ ++ it('should fall back to displayDT when displayTime is undefined', async () => { ++ activatedRouteData.currentRecord = new RecordVO({ ++ type: 'document', ++ displayName: 'Legacy Doc', ++ TagVOs: [], ++ displayDT: '1985-05-20T00:00:00Z', ++ }); ++ await recreateComponent(); ++ ++ 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 f3a9e136..ea5a2a0a 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 +@@ -31,7 +31,13 @@ import { ShareLinksService } from '@root/app/share-links/services/share-links.se + import { ApiService } from '@shared/services/api/api.service'; + import { FeatureFlagService } from '@root/app/feature-flag/services/feature-flag.service'; + import { environment } from '@root/environments/environment'; ++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', +@@ -63,6 +69,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; +@@ -78,10 +90,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( +@@ -89,6 +101,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, +@@ -98,10 +111,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(); + +@@ -187,6 +204,7 @@ export class FileViewerComponent implements OnInit, OnDestroy { + }); + this.itemTagsSubscription.unsubscribe(); + this.tagsSubscription.unsubscribe(); ++ this.dateModalSubscription?.unsubscribe(); + } + + private setRecordsToPreview(resolvedRecord: RecordVO) { +@@ -245,6 +263,7 @@ export class FileViewerComponent implements OnInit, OnDestroy { + this.replayUrl = this.getReplayUrl(); + } + this.setCurrentTags(); ++ this.updateDisplayTimeObject(); + } + + toggleSwipe(value: boolean) { +@@ -433,11 +452,62 @@ export class FileViewerComponent implements OnInit, OnDestroy { + } + } + ++ private updateDisplayTimeObject(): void { ++ if (!this.showEdtfDatePicker) { ++ return; ++ } ++ const record = this.currentRecord; ++ const hasExplicitlyClearedDate = record?.displayTime === null; ++ const timeSource = hasExplicitlyClearedDate ++ ? null ++ : record?.displayTime || record?.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