Skip to content

SF-3486 Hide completed drafts when content is not available #3348

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,15 @@ <h2>{{ lastCompletedBuildMessage }}</h2>
/>
}

@if (historicalBuilds.length > 0) {
@if (savedHistoricalBuilds.length > 0) {
<h2>{{ t("previously_generated_drafts") }}</h2>
@for (entry of historicalBuilds; track entry.id) {
@for (entry of savedHistoricalBuilds; track entry.id) {
<app-draft-history-entry [entry]="entry" />
}
}
@if (showOlderDraftsNotSupportedWarning && nonActiveBuilds.length > 0) {
<app-notice [icon]="'summarize'" class="older-drafts">{{
t("older_drafts_not_available", { date: draftHistoryCutOffDateFormatted })
}}</app-notice>
}
</ng-container>
Original file line number Diff line number Diff line change
Expand Up @@ -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('');
Expand All @@ -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('');
Expand All @@ -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('');
Expand All @@ -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('');
Expand All @@ -92,19 +95,22 @@ 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('');
expect(env.component.nonActiveBuilds).toEqual(buildHistory);
}));

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('');
Expand All @@ -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('');
Expand All @@ -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<DraftHistoryListComponent>;

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');
}
}
});
Original file line number Diff line number Diff line change
@@ -1,36 +1,47 @@
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';

@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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
Loading