diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-history-list/draft-history-list.component.html b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-history-list/draft-history-list.component.html index 62fe1fe401..44d7c57507 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-history-list/draft-history-list.component.html +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-history-list/draft-history-list.component.html @@ -8,10 +8,15 @@

{{ lastCompletedBuildMessage }}

/> } - @if (historicalBuilds.length > 0) { + @if (savedHistoricalBuilds.length > 0) {

{{ t("previously_generated_drafts") }}

- @for (entry of historicalBuilds; track entry.id) { + @for (entry of savedHistoricalBuilds; track entry.id) { } } + @if (showOlderDraftsNotSupportedWarning && nonActiveBuilds.length > 0) { + {{ + t("older_drafts_not_available", { date: draftHistoryCutOffDateFormatted }) + }} + } diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-history-list/draft-history-list.component.spec.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-history-list/draft-history-list.component.spec.ts index 4d8e55bc0c..9514f0a803 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-history-list/draft-history-list.component.spec.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-history-list/draft-history-list.component.spec.ts @@ -47,7 +47,7 @@ describe('DraftHistoryListComponent', () => { it('should handle a missing build history', () => { const env = new TestEnvironment(undefined); expect(env.component.history).toEqual([]); - expect(env.component.historicalBuilds).toEqual([]); + expect(env.component.savedHistoricalBuilds).toEqual([]); expect(env.component.isBuildActive).toBe(false); expect(env.component.latestBuild).toBeUndefined(); expect(env.component.lastCompletedBuildMessage).toBe(''); @@ -57,7 +57,7 @@ describe('DraftHistoryListComponent', () => { it('should handle an empty build history', () => { const env = new TestEnvironment([]); expect(env.component.history).toEqual([]); - expect(env.component.historicalBuilds).toEqual([]); + expect(env.component.savedHistoricalBuilds).toEqual([]); expect(env.component.isBuildActive).toBe(false); expect(env.component.latestBuild).toBeUndefined(); expect(env.component.lastCompletedBuildMessage).toBe(''); @@ -66,10 +66,13 @@ describe('DraftHistoryListComponent', () => { it('should handle completed and active builds', fakeAsync(() => { const activeBuild = { state: BuildStates.Active } as BuildDto; - const completedBuild = { state: BuildStates.Completed } as BuildDto; + const completedBuild = { + state: BuildStates.Completed, + additionalInfo: { dateRequested: '2025-08-01T12:00:00.000Z' } + } as BuildDto; const env = new TestEnvironment([completedBuild, activeBuild]); expect(env.component.history).toEqual([activeBuild, completedBuild]); - expect(env.component.historicalBuilds).toEqual([completedBuild]); + expect(env.component.savedHistoricalBuilds).toEqual([completedBuild]); expect(env.component.isBuildActive).toBe(true); expect(env.component.latestBuild).toBeUndefined(); expect(env.component.lastCompletedBuildMessage).toBe(''); @@ -80,7 +83,7 @@ describe('DraftHistoryListComponent', () => { const buildHistory = [{ state: BuildStates.Active } as BuildDto]; const env = new TestEnvironment(buildHistory); expect(env.component.history).toEqual(buildHistory); - expect(env.component.historicalBuilds).toEqual([]); + expect(env.component.savedHistoricalBuilds).toEqual([]); expect(env.component.isBuildActive).toBe(true); expect(env.component.latestBuild).toBeUndefined(); expect(env.component.lastCompletedBuildMessage).toBe(''); @@ -92,7 +95,7 @@ describe('DraftHistoryListComponent', () => { const buildHistory = [build]; const env = new TestEnvironment(buildHistory); expect(env.component.history).toEqual(buildHistory); - expect(env.component.historicalBuilds).toEqual([]); + expect(env.component.savedHistoricalBuilds).toEqual([]); expect(env.component.isBuildActive).toBe(false); expect(env.component.latestBuild).toBe(build); expect(env.component.lastCompletedBuildMessage).not.toBe(''); @@ -100,11 +103,14 @@ describe('DraftHistoryListComponent', () => { })); it('should handle just one completed build', fakeAsync(() => { - const build = { state: BuildStates.Completed } as BuildDto; + const build = { + state: BuildStates.Completed, + additionalInfo: { dateGenerated: '2025-08-01T12:00:00.000Z' } + } as BuildDto; const buildHistory = [build]; const env = new TestEnvironment(buildHistory); expect(env.component.history).toEqual(buildHistory); - expect(env.component.historicalBuilds).toEqual([]); + expect(env.component.savedHistoricalBuilds).toEqual([]); expect(env.component.isBuildActive).toBe(false); expect(env.component.latestBuild).toBe(build); expect(env.component.lastCompletedBuildMessage).not.toBe(''); @@ -116,7 +122,7 @@ describe('DraftHistoryListComponent', () => { const buildHistory = [build]; const env = new TestEnvironment(buildHistory); expect(env.component.history).toEqual(buildHistory); - expect(env.component.historicalBuilds).toEqual([]); + expect(env.component.savedHistoricalBuilds).toEqual([]); expect(env.component.isBuildActive).toBe(false); expect(env.component.latestBuild).toBe(build); expect(env.component.lastCompletedBuildMessage).not.toBe(''); @@ -139,19 +145,107 @@ describe('DraftHistoryListComponent', () => { expect(env.component.history).toEqual([newBuild, build]); })); + it('should filter draft history and hide builds that are not saved to the database', fakeAsync(() => { + const build = { + state: BuildStates.Completed, + additionalInfo: { dateRequested: '2025-08-01T12:00:00.000Z' } + } as BuildDto; + const olderBuild = { + state: BuildStates.Completed + } as BuildDto; + const buildHistory = [olderBuild, build]; + const env = new TestEnvironment(buildHistory); + + // Ensure that the warning shows, even though the project ID is invalid. + env.component.showOlderDraftsNotSupportedWarning = true; + env.fixture.detectChanges(); + + expect(env.component.history).toEqual(buildHistory); + expect(env.component.savedHistoricalBuilds).toEqual([]); + expect(env.component.isBuildActive).toBe(false); + expect(env.component.latestBuild).toBe(build); + expect(env.component.lastCompletedBuildMessage).not.toBe(''); + expect(env.component.nonActiveBuilds).toEqual(buildHistory); + expect(env.olderDraftsMessage).not.toBeNull(); + })); + + it('should show history with faulted and canceled builds', fakeAsync(() => { + const build = { + id: 'completed', + state: BuildStates.Completed, + additionalInfo: { dateRequested: '2025-08-01T12:00:00.000Z' } + } as BuildDto; + const canceled = { + id: 'canceled', + state: BuildStates.Canceled, + additionalInfo: { dateRequested: '2025-07-02T12:00:00.000Z' } + } as BuildDto; + const faulted = { + id: 'faulted', + state: BuildStates.Faulted, + additionalInfo: { dateRequested: '2025-07-01T12:00:00.000Z' } + } as BuildDto; + const buildHistory = [faulted, canceled, build]; + const env = new TestEnvironment(buildHistory); + expect(env.component.history).toEqual(buildHistory); + expect(env.component.savedHistoricalBuilds).toEqual([canceled, faulted]); + expect(env.component.isBuildActive).toBe(false); + expect(env.component.latestBuild).toBe(build); + expect(env.component.lastCompletedBuildMessage).not.toBe(''); + expect(env.component.nonActiveBuilds).toEqual(buildHistory); + })); + + it('should not show the drafts not supported warning for projects created after 4 June 2025', fakeAsync(() => { + const build = { + state: BuildStates.Completed, + additionalInfo: { dateRequested: '2025-08-01T12:00:00.000Z' } + } as BuildDto; + const olderBuild = { + state: BuildStates.Completed, + additionalInfo: { dateRequested: '2025-08-01T12:00:00.000Z' } + } as BuildDto; + const buildHistory = [olderBuild, build]; + // ObjectID was created at 2025-06-03T00:00:00.000Z + const env = new TestEnvironment(buildHistory, '683e3b000000000000000000'); + expect(env.component.history).toEqual(buildHistory); + expect(env.component.nonActiveBuilds).toEqual(buildHistory); + expect(env.olderDraftsMessage).not.toBeNull(); + })); + + it('should show the drafts not supported warning for projects created before 4 June 2025', fakeAsync(() => { + const build = { + state: BuildStates.Completed, + additionalInfo: { dateRequested: '2025-08-01T12:00:00.000Z' } + } as BuildDto; + const olderBuild = { + state: BuildStates.Completed, + additionalInfo: { dateRequested: '2025-08-01T12:00:00.000Z' } + } as BuildDto; + const buildHistory = [olderBuild, build]; + // ObjectID was created at 2025-06-05T00:00:00.000Z + const env = new TestEnvironment(buildHistory, '6840de000000000000000000'); + expect(env.component.history).toEqual(buildHistory); + expect(env.component.nonActiveBuilds).toEqual(buildHistory); + expect(env.olderDraftsMessage).toBeNull(); + })); + class TestEnvironment { component: DraftHistoryListComponent; fixture: ComponentFixture; - constructor(buildHistory: BuildDto[] | undefined) { - when(mockedActivatedProjectService.projectId$).thenReturn(of('project01')); + constructor(buildHistory: BuildDto[] | undefined, projectId: string = 'project01') { + when(mockedActivatedProjectService.projectId$).thenReturn(of(projectId)); when(mockedActivatedProjectService.changes$).thenReturn(of(undefined)); // Required for DraftPreviewBooksComponent - when(mockedDraftGenerationService.getBuildHistory('project01')).thenReturn(new BehaviorSubject(buildHistory)); + when(mockedDraftGenerationService.getBuildHistory(projectId)).thenReturn(new BehaviorSubject(buildHistory)); when(mockedFeatureFlagsService.usfmFormat).thenReturn(createTestFeatureFlag(true)); this.fixture = TestBed.createComponent(DraftHistoryListComponent); this.component = this.fixture.componentInstance; this.fixture.detectChanges(); } + + get olderDraftsMessage(): HTMLElement { + return this.fixture.nativeElement.querySelector('.older-drafts'); + } } }); diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-history-list/draft-history-list.component.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-history-list/draft-history-list.component.ts index 617789fc51..1e2c72a144 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-history-list/draft-history-list.component.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-history-list/draft-history-list.component.ts @@ -1,12 +1,15 @@ import { Component, DestroyRef } from '@angular/core'; import { MatIconModule } from '@angular/material/icon'; import { TranslocoModule, TranslocoService } from '@ngneat/transloco'; +import ObjectID from 'bson-objectid'; import { take } from 'rxjs'; import { ActivatedProjectService } from 'xforge-common/activated-project.service'; +import { I18nService } from 'xforge-common/i18n.service'; import { filterNullish, quietTakeUntilDestroyed } from 'xforge-common/util/rxjs-util'; import { ProjectNotificationService } from '../../../core/project-notification.service'; import { BuildDto } from '../../../machine-api/build-dto'; import { BuildStates } from '../../../machine-api/build-states'; +import { NoticeComponent } from '../../../shared/notice/notice.component'; import { activeBuildStates } from '../draft-generation'; import { DraftGenerationService } from '../draft-generation.service'; import { DraftHistoryEntryComponent } from './draft-history-entry/draft-history-entry.component'; @@ -14,23 +17,31 @@ import { DraftHistoryEntryComponent } from './draft-history-entry/draft-history- @Component({ selector: 'app-draft-history-list', standalone: true, - imports: [MatIconModule, DraftHistoryEntryComponent, TranslocoModule], + imports: [MatIconModule, DraftHistoryEntryComponent, TranslocoModule, NoticeComponent], templateUrl: './draft-history-list.component.html', styleUrl: './draft-history-list.component.scss' }) export class DraftHistoryListComponent { + showOlderDraftsNotSupportedWarning: boolean = false; history: BuildDto[] = []; + // This is just after SFv5.33.0 was released + readonly draftHistoryCutOffDate: Date = new Date('2025-06-03T21:00:00Z'); + readonly draftHistoryCutOffDateFormatted: string = this.i18n.formatDate(this.draftHistoryCutOffDate); constructor( activatedProject: ActivatedProjectService, private destroyRef: DestroyRef, private readonly draftGenerationService: DraftGenerationService, projectNotificationService: ProjectNotificationService, - private readonly transloco: TranslocoService + private readonly transloco: TranslocoService, + private readonly i18n: I18nService ) { activatedProject.projectId$ .pipe(quietTakeUntilDestroyed(destroyRef), filterNullish(), take(1)) .subscribe(async projectId => { + // Determine whether to show or hide the older drafts warning + this.showOlderDraftsNotSupportedWarning = + ObjectID.isValid(projectId) && new ObjectID(projectId).getTimestamp() < this.draftHistoryCutOffDate; // Initially load the history this.loadHistory(projectId); // Start the connection to SignalR @@ -75,14 +86,23 @@ export class DraftHistoryListComponent { } } - get historicalBuilds(): BuildDto[] { - return this.latestBuild == null ? this.nonActiveBuilds : this.nonActiveBuilds.slice(1); - } - get isBuildActive(): boolean { return this.history.some(entry => activeBuildStates.includes(entry.state)) ?? false; } + get savedHistoricalBuilds(): BuildDto[] { + // The requested date, if not set for the build in MongoDB, will be set based on the build id in the Machine API + return this.historicalBuilds.filter( + entry => + entry.additionalInfo?.dateRequested != null && + new Date(entry.additionalInfo.dateRequested) > this.draftHistoryCutOffDate + ); + } + + private get historicalBuilds(): BuildDto[] { + return this.latestBuild == null ? this.nonActiveBuilds : this.nonActiveBuilds.slice(1); + } + loadHistory(projectId: string): void { this.draftGenerationService .getBuildHistory(projectId) diff --git a/src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/non_checking_en.json b/src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/non_checking_en.json index 29c8903270..ca6947209a 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/non_checking_en.json +++ b/src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/non_checking_en.json @@ -287,6 +287,7 @@ "draft_canceled": "The draft was canceled", "draft_completed": "The draft is ready", "draft_faulted": "The draft failed", + "older_drafts_not_available": "Older drafts requested before {{ date }} are not available.", "previously_generated_drafts": "Previously generated drafts" }, "draft_preview_books": {