From 1a4307efac5377636f460d17b0be4e9b3eda0152 Mon Sep 17 00:00:00 2001 From: Raymond Luong Date: Tue, 5 Aug 2025 16:58:54 -0600 Subject: [PATCH 1/2] SF-3486 Hide completed drafts when content is not available --- .../draft-history-list.component.html | 9 ++- .../draft-history-list.component.spec.ts | 71 ++++++++++++++++--- .../draft-history-list.component.ts | 23 ++++-- .../src/assets/i18n/non_checking_en.json | 1 + 4 files changed, 87 insertions(+), 17 deletions(-) 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 62fe1fe401f..3a46a68a324 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 (nonActiveBuilds.length > 0) { + {{ + t("older_drafts_not_available", { date: draftHistoryCutoffDate }) + }} + } 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 4d8e55bc0ce..b39dd36bfc6 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: { dateGenerated: '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,6 +145,49 @@ 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: { dateGenerated: '2025-08-01T12:00:00.000Z' } + } as BuildDto; + const olderBuild = { + state: BuildStates.Completed + } as BuildDto; + const buildHistory = [olderBuild, build]; + const env = new TestEnvironment(buildHistory); + 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: { dateGenerated: '2025-08-01T12:00:00.000Z' } + } as BuildDto; + const canceled = { + id: 'canceled', + state: BuildStates.Canceled + } as BuildDto; + const faulted = { + id: 'faulted', + state: BuildStates.Faulted + } 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); + })); + class TestEnvironment { component: DraftHistoryListComponent; fixture: ComponentFixture; @@ -153,5 +202,9 @@ describe('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 617789fc512..03f549d8745 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 @@ -2,8 +2,10 @@ import { Component, DestroyRef } from '@angular/core'; import { MatIconModule } from '@angular/material/icon'; import { TranslocoModule, TranslocoService } from '@ngneat/transloco'; import { take } from 'rxjs'; +import { NoticeComponent } from 'src/app/shared/notice/notice.component'; import { ActivatedProjectService } from 'xforge-common/activated-project.service'; import { filterNullish, quietTakeUntilDestroyed } from 'xforge-common/util/rxjs-util'; +import { I18nService } from '../../../../xforge-common/i18n.service'; import { ProjectNotificationService } from '../../../core/project-notification.service'; import { BuildDto } from '../../../machine-api/build-dto'; import { BuildStates } from '../../../machine-api/build-states'; @@ -14,19 +16,21 @@ 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 { history: BuildDto[] = []; + readonly draftHistoryCutoffDate: string = this.i18n.formatDate(new Date('2024-12-03T12:00:00.000Z')); 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)) @@ -75,14 +79,21 @@ 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 date generated is available if the draft has been stored in the realtime database. + return this.historicalBuilds.filter( + entry => entry.state !== BuildStates.Completed || entry.additionalInfo?.dateGenerated != null + ); + } + + 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 0d3974ac35b..88496d80075 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 @@ -294,6 +294,7 @@ "draft_canceled": "The draft was canceled", "draft_completed": "The draft is ready", "draft_faulted": "The draft failed", + "older_drafts_not_available": "Older drafts generated before {{ date }} are not available.", "previously_generated_drafts": "Previously generated drafts" }, "draft_preview_books": { From 3fa81e314cde9e09009e2d1a7912a7f4f86393c3 Mon Sep 17 00:00:00 2001 From: Peter Chapman Date: Tue, 12 Aug 2025 10:02:16 +1200 Subject: [PATCH 2/2] Add project date cut off calculation and code review fixes --- .../draft-history-list.component.html | 4 +- .../draft-history-list.component.spec.ts | 57 ++++++++++++++++--- .../draft-history-list.component.ts | 19 +++++-- .../src/assets/i18n/non_checking_en.json | 2 +- 4 files changed, 66 insertions(+), 16 deletions(-) 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 3a46a68a324..44d7c575079 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 @@ -14,9 +14,9 @@

{{ t("previously_generated_drafts") }}

} } - @if (nonActiveBuilds.length > 0) { + @if (showOlderDraftsNotSupportedWarning && nonActiveBuilds.length > 0) { {{ - t("older_drafts_not_available", { date: draftHistoryCutoffDate }) + 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 b39dd36bfc6..9514f0a803a 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 @@ -68,7 +68,7 @@ describe('DraftHistoryListComponent', () => { const activeBuild = { state: BuildStates.Active } as BuildDto; const completedBuild = { state: BuildStates.Completed, - additionalInfo: { dateGenerated: '2025-08-01T12:00:00.000Z' } + additionalInfo: { dateRequested: '2025-08-01T12:00:00.000Z' } } as BuildDto; const env = new TestEnvironment([completedBuild, activeBuild]); expect(env.component.history).toEqual([activeBuild, completedBuild]); @@ -148,13 +148,18 @@ describe('DraftHistoryListComponent', () => { it('should filter draft history and hide builds that are not saved to the database', fakeAsync(() => { const build = { state: BuildStates.Completed, - additionalInfo: { dateGenerated: '2025-08-01T12:00:00.000Z' } + 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); @@ -168,15 +173,17 @@ describe('DraftHistoryListComponent', () => { const build = { id: 'completed', state: BuildStates.Completed, - additionalInfo: { dateGenerated: '2025-08-01T12:00:00.000Z' } + additionalInfo: { dateRequested: '2025-08-01T12:00:00.000Z' } } as BuildDto; const canceled = { id: 'canceled', - state: BuildStates.Canceled + state: BuildStates.Canceled, + additionalInfo: { dateRequested: '2025-07-02T12:00:00.000Z' } } as BuildDto; const faulted = { id: 'faulted', - state: BuildStates.Faulted + state: BuildStates.Faulted, + additionalInfo: { dateRequested: '2025-07-01T12:00:00.000Z' } } as BuildDto; const buildHistory = [faulted, canceled, build]; const env = new TestEnvironment(buildHistory); @@ -188,14 +195,48 @@ describe('DraftHistoryListComponent', () => { 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); 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 03f549d8745..1e2c72a1440 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,14 +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 { NoticeComponent } from 'src/app/shared/notice/notice.component'; 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 { I18nService } from '../../../../xforge-common/i18n.service'; 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'; @@ -21,8 +22,11 @@ import { DraftHistoryEntryComponent } from './draft-history-entry/draft-history- styleUrl: './draft-history-list.component.scss' }) export class DraftHistoryListComponent { + showOlderDraftsNotSupportedWarning: boolean = false; history: BuildDto[] = []; - readonly draftHistoryCutoffDate: string = this.i18n.formatDate(new Date('2024-12-03T12:00:00.000Z')); + // 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, @@ -35,6 +39,9 @@ export class DraftHistoryListComponent { 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 @@ -84,9 +91,11 @@ export class DraftHistoryListComponent { } get savedHistoricalBuilds(): BuildDto[] { - // The date generated is available if the draft has been stored in the realtime database. + // 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.state !== BuildStates.Completed || entry.additionalInfo?.dateGenerated != null + entry => + entry.additionalInfo?.dateRequested != null && + new Date(entry.additionalInfo.dateRequested) > this.draftHistoryCutOffDate ); } 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 88496d80075..f7a828d2634 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 @@ -294,7 +294,7 @@ "draft_canceled": "The draft was canceled", "draft_completed": "The draft is ready", "draft_faulted": "The draft failed", - "older_drafts_not_available": "Older drafts generated before {{ date }} are not available.", + "older_drafts_not_available": "Older drafts requested before {{ date }} are not available.", "previously_generated_drafts": "Previously generated drafts" }, "draft_preview_books": {