Skip to content

Commit 7c264f8

Browse files
SF-3592 Hide option to configure draft for outdated builds (#3487)
1 parent 413b58d commit 7c264f8

File tree

7 files changed

+113
-21
lines changed

7 files changed

+113
-21
lines changed

src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-history-list/draft-history-entry/draft-history-entry.component.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
</mat-panel-description>
2222
</mat-expansion-panel-header>
2323
<div class="draft-entry-body">
24-
@if (this.featureFlags.usfmFormat.enabled && !formattingOptionsSelected && isLatestBuild && draftIsAvailable) {
24+
@if (formattingOptionsSupported && !formattingOptionsSelected && isLatestBuild && draftIsAvailable) {
2525
<p class="require-formatting-options">
2626
{{ t("select_formatting_options") }}
2727
</p>
@@ -49,7 +49,7 @@
4949
</p>
5050
<div class="draft-options">
5151
<app-draft-download-button [build]="entry" [flat]="true" />
52-
@if (featureFlags.usfmFormat.enabled && isLatestBuild) {
52+
@if (formattingOptionsSupported && isLatestBuild) {
5353
<button mat-button class="format-usfm" [routerLink]="['format']">
5454
<mat-icon>build</mat-icon> {{ t("formatting_options") }}
5555
</button>

src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-history-list/draft-history-entry/draft-history-entry.component.spec.ts

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { SFProjectService } from '../../../../core/sf-project.service';
2222
import { BuildDto } from '../../../../machine-api/build-dto';
2323
import { BuildStates } from '../../../../machine-api/build-states';
2424
import { DraftGenerationService } from '../../draft-generation.service';
25+
import { FORMATTING_OPTIONS_SUPPORTED_DATE } from '../../draft-utils';
2526
import { TrainingDataService } from '../../training-data/training-data.service';
2627
import { DraftHistoryEntryComponent } from './draft-history-entry.component';
2728

@@ -33,6 +34,10 @@ const mockedTrainingDataService = mock(TrainingDataService);
3334
const mockedActivatedProjectService = mock(ActivatedProjectService);
3435
const mockedFeatureFlagsService = mock(FeatureFlagService);
3536

37+
const oneDay = 1000 * 60 * 60 * 24;
38+
const dateBeforeFormattingSupported = new Date(FORMATTING_OPTIONS_SUPPORTED_DATE.getTime() - oneDay).toISOString();
39+
const dateAfterFormattingSupported = new Date(FORMATTING_OPTIONS_SUPPORTED_DATE.getTime() + oneDay).toISOString();
40+
3641
describe('DraftHistoryEntryComponent', () => {
3742
let component: DraftHistoryEntryComponent;
3843
let fixture: ComponentFixture<DraftHistoryEntryComponent>;
@@ -99,7 +104,7 @@ describe('DraftHistoryEntryComponent', () => {
99104
it('should handle builds with additional info', fakeAsync(() => {
100105
when(mockedI18nService.enumerateList(anything())).thenReturn('src');
101106
const user = 'user-display-name';
102-
const date = 'formatted-date';
107+
const date = dateAfterFormattingSupported;
103108
const trainingBooks = ['EXO'];
104109
const translateBooks = ['GEN'];
105110
const trainingDataFiles: Map<string, string> = new Map([['file01', 'training-data.txt']]);
@@ -144,7 +149,7 @@ describe('DraftHistoryEntryComponent', () => {
144149
it('should state that the model did not have training configuration', fakeAsync(() => {
145150
when(mockedI18nService.enumerateList(anything())).thenReturn('src');
146151
const user = 'user-display-name';
147-
const date = 'formatted-date';
152+
const date = dateAfterFormattingSupported;
148153
const trainingBooks = [];
149154
const translateBooks = ['GEN'];
150155
const trainingDataFiles = [];
@@ -161,7 +166,7 @@ describe('DraftHistoryEntryComponent', () => {
161166

162167
it('should show the USFM format option when the project is the latest draft', fakeAsync(() => {
163168
const user = 'user-display-name';
164-
const date = 'formatted-date';
169+
const date = dateAfterFormattingSupported;
165170
const trainingBooks = ['EXO'];
166171
const translateBooks = ['GEN'];
167172
const trainingDataFiles = ['file01'];
@@ -244,8 +249,9 @@ describe('DraftHistoryEntryComponent', () => {
244249
when(mockedActivatedProjectService.changes$).thenReturn(of(targetProjectDoc));
245250
const entry = {
246251
additionalInfo: {
247-
dateGenerated: new Date().toISOString(),
248-
dateRequested: new Date().toISOString(),
252+
dateGenerated: dateAfterFormattingSupported,
253+
dateRequested: dateAfterFormattingSupported,
254+
dateFinished: dateAfterFormattingSupported,
249255
requestedByUserId: 'sf-user-id',
250256
translationScriptureRanges: [{ projectId: 'project01', scriptureRange: 'GEN' }]
251257
}
@@ -337,14 +343,16 @@ describe('DraftHistoryEntryComponent', () => {
337343
const projectDoc = getProjectProfileDoc();
338344
when(mockedActivatedProjectService.projectDoc).thenReturn(projectDoc);
339345
when(mockedActivatedProjectService.changes$).thenReturn(of(projectDoc));
346+
when(mockedI18nService.formatDate(anything())).thenReturn('formatted-date');
340347
});
341348

342349
it('should show set draft format UI', fakeAsync(() => {
350+
const date = dateAfterFormattingSupported;
343351
component.entry = {
344352
id: 'build01',
345353
state: BuildStates.Completed,
346354
message: 'Completed',
347-
additionalInfo: { dateGenerated: '2025-09-01' }
355+
additionalInfo: { dateGenerated: date, dateFinished: date }
348356
} as BuildDto;
349357
component.isLatestBuild = true;
350358
component.draftIsAvailable = true;
@@ -360,7 +368,8 @@ describe('DraftHistoryEntryComponent', () => {
360368
state: BuildStates.Completed,
361369
message: 'Completed',
362370
additionalInfo: {
363-
dateGenerated: '2025-09-01',
371+
dateGenerated: dateAfterFormattingSupported,
372+
dateFinished: dateAfterFormattingSupported,
364373
translationScriptureRanges: [{ projectId: 'source01', scriptureRange: 'EXO' }]
365374
}
366375
} as BuildDto;
@@ -377,7 +386,8 @@ describe('DraftHistoryEntryComponent', () => {
377386
state: BuildStates.Completed,
378387
message: 'Completed',
379388
additionalInfo: {
380-
dateGenerated: '2025-09-01',
389+
dateGenerated: dateAfterFormattingSupported,
390+
dateFinished: dateAfterFormattingSupported,
381391
translationScriptureRanges: [{ projectId: 'source01', scriptureRange: 'EXO' }]
382392
}
383393
} as BuildDto;
@@ -389,13 +399,38 @@ describe('DraftHistoryEntryComponent', () => {
389399
}));
390400

391401
it('should hide draft format UI if the draft is not completed', fakeAsync(() => {
392-
component.entry = { id: 'build01', state: BuildStates.Canceled, message: 'Cancelled' } as BuildDto;
402+
component.entry = {
403+
id: 'build01',
404+
state: BuildStates.Canceled,
405+
message: 'Cancelled',
406+
additionalInfo: { dateGenerated: dateAfterFormattingSupported, dateFinished: dateAfterFormattingSupported }
407+
} as BuildDto;
393408
component.isLatestBuild = true;
394409
component.draftIsAvailable = false;
395410
tick();
396411
fixture.detectChanges();
397412
expect(fixture.nativeElement.querySelector('.require-formatting-options')).toBeNull();
398413
}));
414+
415+
it('should not show the USFM format option for drafts created before the supported date', fakeAsync(() => {
416+
const user = 'user-display-name';
417+
const date = dateBeforeFormattingSupported;
418+
const trainingBooks = ['EXO'];
419+
const translateBooks = ['GEN'];
420+
const trainingDataFiles = ['file01'];
421+
const entry = getStandardBuildDto({ user, date, trainingBooks, translateBooks, trainingDataFiles });
422+
423+
// SUT
424+
component.entry = entry;
425+
component.isLatestBuild = true;
426+
tick();
427+
fixture.detectChanges();
428+
429+
expect(component.scriptureRange).toEqual('GEN');
430+
expect(component.draftIsAvailable).toBe(true);
431+
expect(fixture.nativeElement.querySelector('.format-usfm')).toBeNull();
432+
expect(component.formattingOptionsSupported).toBe(false);
433+
}));
399434
});
400435

401436
describe('formatDate', () => {
@@ -457,8 +492,9 @@ describe('DraftHistoryEntryComponent', () => {
457492
id: 'project01'
458493
},
459494
additionalInfo: {
460-
dateGenerated: new Date().toISOString(),
461-
dateRequested: new Date().toISOString(),
495+
dateGenerated: new Date(date).toISOString(),
496+
dateFinished: new Date(date).toISOString(),
497+
dateRequested: new Date(date).toISOString(),
462498
requestedByUserId: 'sf-user-id',
463499
trainingScriptureRanges:
464500
trainingBooks.length > 0 ? [{ projectId: 'project02', scriptureRange: trainingBooks.join(';') }] : [],

src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-history-list/draft-history-entry/draft-history-entry.component.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { BuildStates } from '../../../../machine-api/build-states';
1919
import { RIGHT_TO_LEFT_MARK } from '../../../../shared/utils';
2020
import { DraftDownloadButtonComponent } from '../../draft-download-button/draft-download-button.component';
2121
import { DraftPreviewBooksComponent } from '../../draft-preview-books/draft-preview-books.component';
22+
import { FORMATTING_OPTIONS_SUPPORTED_DATE } from '../../draft-utils';
2223
import { TrainingDataService } from '../../training-data/training-data.service';
2324

2425
const STATUS_INFO: Record<BuildStates, { icons: string; text: string; color: string }> = {
@@ -271,6 +272,12 @@ export class DraftHistoryEntryComponent {
271272
return this.activatedProjectService.projectDoc?.data?.translateConfig.draftConfig.usfmConfig != null;
272273
}
273274

275+
get formattingOptionsSupported(): boolean {
276+
return this.featureFlags.usfmFormat.enabled && this.entry?.additionalInfo?.dateFinished != null
277+
? new Date(this.entry.additionalInfo.dateFinished) > FORMATTING_OPTIONS_SUPPORTED_DATE
278+
: false;
279+
}
280+
274281
@Input() isLatestBuild: boolean = false;
275282
trainingConfigurationOpen = false;
276283

src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-utils.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import { TranslateSource } from 'realtime-server/lib/esm/scriptureforge/models/t
33
import language_code_mapping from '../../../../../language_code_mapping.json';
44
import { SelectableProjectWithLanguageCode } from '../../core/paratext.service';
55

6+
// Corresponds to Serval 1.11.0 release
7+
export const FORMATTING_OPTIONS_SUPPORTED_DATE: Date = new Date('2025-09-25T00:00:00Z');
8+
69
/** Represents draft sources as a set of two {@link TranslateSource} arrays, and one {@link SFProjectProfile} array. */
710
export interface DraftSourcesAsTranslateSourceArrays {
811
trainingSources: TranslateSource[];

src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor-draft/editor-draft.component.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,12 @@
5050
<transloco class="hide-lt-md" key="editor_draft_tab.format_draft"></transloco>
5151
<transloco class="hide-gt-md" key="editor_draft_tab.formatting"></transloco>
5252
</button>
53-
} @else {
53+
} @else if (formattingOptionsSupported) {
5454
<span
55-
[matTooltip]="t(doesLatestHaveDraft ? 'format_draft_can' : 'format_draft_cannot')"
56-
[style.cursor]="doesLatestHaveDraft ? 'pointer' : 'not-allowed'"
55+
[matTooltip]="t(doesLatestBuildHaveDraft ? 'format_draft_can' : 'format_draft_cannot')"
56+
[style.cursor]="doesLatestBuildHaveDraft ? 'pointer' : 'not-allowed'"
5757
>
58-
<button mat-button (click)="navigateToFormatting()" [disabled]="!doesLatestHaveDraft">
58+
<button mat-button (click)="navigateToFormatting()" [disabled]="!doesLatestBuildHaveDraft">
5959
<mat-icon>build</mat-icon>
6060
<transloco class="hide-lt-md" key="editor_draft_tab.format_draft"></transloco>
6161
<transloco class="hide-gt-md" key="editor_draft_tab.formatting"></transloco>

src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor-draft/editor-draft.component.spec.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,7 @@ describe('EditorDraftComponent', () => {
426426
flush();
427427
}));
428428

429-
it('should guide user to select formatting options when formatting not selected', fakeAsync(() => {
429+
it('should hide formatting options for drafts created before supported date', fakeAsync(() => {
430430
const testProjectDoc: SFProjectProfileDoc = {
431431
data: createTestProjectProfile({
432432
texts: [
@@ -450,6 +450,35 @@ describe('EditorDraftComponent', () => {
450450
fixture.detectChanges();
451451
tick(EDITOR_READY_TIMEOUT);
452452

453+
expect(component.mustChooseFormattingOptions).toBe(false);
454+
flush();
455+
}));
456+
457+
it('should guide user to select formatting options when formatting not selected', fakeAsync(() => {
458+
const testProjectDoc: SFProjectProfileDoc = {
459+
data: createTestProjectProfile({
460+
texts: [
461+
{
462+
bookNum: 1,
463+
chapters: [{ number: 1, permissions: { user01: SFProjectRole.ParatextAdministrator }, hasDraft: true }]
464+
}
465+
]
466+
})
467+
} as SFProjectProfileDoc;
468+
when(mockDraftGenerationService.draftExists(anything(), anything(), anything())).thenReturn(of(true));
469+
const historyAfterFormattingOptions: Revision[] = [{ timestamp: '2025-10-01T12:00:00.000Z' }];
470+
when(mockDraftGenerationService.getGeneratedDraftHistory(anything(), anything(), anything())).thenReturn(
471+
of(historyAfterFormattingOptions)
472+
);
473+
when(mockActivatedProjectService.changes$).thenReturn(of(testProjectDoc));
474+
when(mockDialogService.confirm(anything(), anything())).thenResolve(true);
475+
spyOn<any>(component, 'getTargetOps').and.returnValue(of(targetDelta.ops));
476+
when(mockDraftHandlingService.getDraft(anything(), anything())).thenReturn(of(draftDelta.ops!));
477+
when(mockDraftHandlingService.draftDataToOps(anything(), anything())).thenReturn(draftDelta.ops!);
478+
479+
fixture.detectChanges();
480+
tick(EDITOR_READY_TIMEOUT);
481+
453482
expect(component.mustChooseFormattingOptions).toBe(true);
454483
flush();
455484
}));

src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor-draft/editor-draft.component.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import { BuildStates } from '../../../machine-api/build-states';
4242
import { TextComponent } from '../../../shared/text/text.component';
4343
import { DraftGenerationService } from '../../draft-generation/draft-generation.service';
4444
import { DraftHandlingService } from '../../draft-generation/draft-handling.service';
45+
import { FORMATTING_OPTIONS_SUPPORTED_DATE } from '../../draft-generation/draft-utils';
4546
@Component({
4647
selector: 'app-editor-draft',
4748
templateUrl: './editor-draft.component.html',
@@ -60,7 +61,6 @@ export class EditorDraftComponent implements AfterViewInit, OnChanges {
6061

6162
inputChanged$ = new Subject<void>();
6263
draftCheckState: 'draft-unknown' | 'draft-present' | 'draft-legacy' | 'draft-empty' = 'draft-unknown';
63-
draftRevisions: Revision[] = [];
6464
selectedRevision: Revision | undefined;
6565
generateDraftUrl?: string;
6666
targetProject?: SFProjectProfile;
@@ -69,10 +69,13 @@ export class EditorDraftComponent implements AfterViewInit, OnChanges {
6969
isDraftApplied = false;
7070
userAppliedDraft = false;
7171
hasFormattingSelected = true;
72+
formattingOptionsSupported = true;
7273

7374
private selectedRevisionSubject = new BehaviorSubject<Revision | undefined>(undefined);
7475
private selectedRevision$ = this.selectedRevisionSubject.asObservable();
7576

77+
private _draftRevisions: Revision[] = [];
78+
7679
// 'asyncScheduler' prevents ExpressionChangedAfterItHasBeenCheckedError
7780
private loading$ = new BehaviorSubject<boolean>(false);
7881
isLoading$: Observable<boolean> = this.loading$.pipe(observeOn(asyncScheduler));
@@ -112,15 +115,29 @@ export class EditorDraftComponent implements AfterViewInit, OnChanges {
112115
return this.draftHandlingService.canApplyDraft(this.targetProject, this.bookNum, this.chapter, this.draftDelta.ops);
113116
}
114117

115-
get doesLatestHaveDraft(): boolean {
118+
get doesLatestBuildHaveDraft(): boolean {
116119
return (
117120
this.targetProject?.texts.find(t => t.bookNum === this.bookNum)?.chapters.find(c => c.number === this.chapter)
118121
?.hasDraft ?? false
119122
);
120123
}
121124

122125
get mustChooseFormattingOptions(): boolean {
123-
return this.featureFlags.usfmFormat.enabled && !this.hasFormattingSelected && this.doesLatestHaveDraft;
126+
return (
127+
this.featureFlags.usfmFormat.enabled &&
128+
!this.hasFormattingSelected &&
129+
this.formattingOptionsSupported &&
130+
this.doesLatestBuildHaveDraft
131+
);
132+
}
133+
134+
set draftRevisions(value: Revision[]) {
135+
this._draftRevisions = value;
136+
this.formattingOptionsSupported = value.some(rev => new Date(rev.timestamp) > FORMATTING_OPTIONS_SUPPORTED_DATE);
137+
}
138+
139+
get draftRevisions(): Revision[] {
140+
return this._draftRevisions;
124141
}
125142

126143
ngOnChanges(): void {

0 commit comments

Comments
 (0)