Skip to content
Merged
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 @@ -21,7 +21,7 @@
</mat-panel-description>
</mat-expansion-panel-header>
<div class="draft-entry-body">
@if (this.featureFlags.usfmFormat.enabled && !formattingOptionsSelected && isLatestBuild && draftIsAvailable) {
@if (formattingOptionsSupported && !formattingOptionsSelected && isLatestBuild && draftIsAvailable) {
<p class="require-formatting-options">
{{ t("select_formatting_options") }}
</p>
Expand Down Expand Up @@ -49,7 +49,7 @@
</p>
<div class="draft-options">
<app-draft-download-button [build]="entry" [flat]="true" />
@if (featureFlags.usfmFormat.enabled && isLatestBuild) {
@if (formattingOptionsSupported && isLatestBuild) {
<button mat-button class="format-usfm" [routerLink]="['format']">
<mat-icon>build</mat-icon> {{ t("formatting_options") }}
</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { SFProjectService } from '../../../../core/sf-project.service';
import { BuildDto } from '../../../../machine-api/build-dto';
import { BuildStates } from '../../../../machine-api/build-states';
import { DraftGenerationService } from '../../draft-generation.service';
import { FORMATTING_OPTIONS_SUPPORTED_DATE } from '../../draft-utils';
import { TrainingDataService } from '../../training-data/training-data.service';
import { DraftHistoryEntryComponent } from './draft-history-entry.component';

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

const oneDay = 1000 * 60 * 60 * 24;
const dateBeforeFormattingSupported = new Date(FORMATTING_OPTIONS_SUPPORTED_DATE.getTime() - oneDay).toISOString();
const dateAfterFormattingSupported = new Date(FORMATTING_OPTIONS_SUPPORTED_DATE.getTime() + oneDay).toISOString();

describe('DraftHistoryEntryComponent', () => {
let component: DraftHistoryEntryComponent;
let fixture: ComponentFixture<DraftHistoryEntryComponent>;
Expand Down Expand Up @@ -99,7 +104,7 @@ describe('DraftHistoryEntryComponent', () => {
it('should handle builds with additional info', fakeAsync(() => {
when(mockedI18nService.enumerateList(anything())).thenReturn('src');
const user = 'user-display-name';
const date = 'formatted-date';
const date = dateAfterFormattingSupported;
const trainingBooks = ['EXO'];
const translateBooks = ['GEN'];
const trainingDataFiles: Map<string, string> = new Map([['file01', 'training-data.txt']]);
Expand Down Expand Up @@ -144,7 +149,7 @@ describe('DraftHistoryEntryComponent', () => {
it('should state that the model did not have training configuration', fakeAsync(() => {
when(mockedI18nService.enumerateList(anything())).thenReturn('src');
const user = 'user-display-name';
const date = 'formatted-date';
const date = dateAfterFormattingSupported;
const trainingBooks = [];
const translateBooks = ['GEN'];
const trainingDataFiles = [];
Expand All @@ -161,7 +166,7 @@ describe('DraftHistoryEntryComponent', () => {

it('should show the USFM format option when the project is the latest draft', fakeAsync(() => {
const user = 'user-display-name';
const date = 'formatted-date';
const date = dateAfterFormattingSupported;
const trainingBooks = ['EXO'];
const translateBooks = ['GEN'];
const trainingDataFiles = ['file01'];
Expand Down Expand Up @@ -244,8 +249,9 @@ describe('DraftHistoryEntryComponent', () => {
when(mockedActivatedProjectService.changes$).thenReturn(of(targetProjectDoc));
const entry = {
additionalInfo: {
dateGenerated: new Date().toISOString(),
dateRequested: new Date().toISOString(),
dateGenerated: dateAfterFormattingSupported,
dateRequested: dateAfterFormattingSupported,
dateFinished: dateAfterFormattingSupported,
requestedByUserId: 'sf-user-id',
translationScriptureRanges: [{ projectId: 'project01', scriptureRange: 'GEN' }]
}
Expand Down Expand Up @@ -337,14 +343,16 @@ describe('DraftHistoryEntryComponent', () => {
const projectDoc = getProjectProfileDoc();
when(mockedActivatedProjectService.projectDoc).thenReturn(projectDoc);
when(mockedActivatedProjectService.changes$).thenReturn(of(projectDoc));
when(mockedI18nService.formatDate(anything())).thenReturn('formatted-date');
});

it('should show set draft format UI', fakeAsync(() => {
const date = dateAfterFormattingSupported;
component.entry = {
id: 'build01',
state: BuildStates.Completed,
message: 'Completed',
additionalInfo: { dateGenerated: '2025-09-01' }
additionalInfo: { dateGenerated: date, dateFinished: date }
} as BuildDto;
component.isLatestBuild = true;
component.draftIsAvailable = true;
Expand All @@ -360,7 +368,8 @@ describe('DraftHistoryEntryComponent', () => {
state: BuildStates.Completed,
message: 'Completed',
additionalInfo: {
dateGenerated: '2025-09-01',
dateGenerated: dateAfterFormattingSupported,
dateFinished: dateAfterFormattingSupported,
translationScriptureRanges: [{ projectId: 'source01', scriptureRange: 'EXO' }]
}
} as BuildDto;
Expand All @@ -377,7 +386,8 @@ describe('DraftHistoryEntryComponent', () => {
state: BuildStates.Completed,
message: 'Completed',
additionalInfo: {
dateGenerated: '2025-09-01',
dateGenerated: dateAfterFormattingSupported,
dateFinished: dateAfterFormattingSupported,
translationScriptureRanges: [{ projectId: 'source01', scriptureRange: 'EXO' }]
}
} as BuildDto;
Expand All @@ -389,13 +399,38 @@ describe('DraftHistoryEntryComponent', () => {
}));

it('should hide draft format UI if the draft is not completed', fakeAsync(() => {
component.entry = { id: 'build01', state: BuildStates.Canceled, message: 'Cancelled' } as BuildDto;
component.entry = {
id: 'build01',
state: BuildStates.Canceled,
message: 'Cancelled',
additionalInfo: { dateGenerated: dateAfterFormattingSupported, dateFinished: dateAfterFormattingSupported }
} as BuildDto;
component.isLatestBuild = true;
component.draftIsAvailable = false;
tick();
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('.require-formatting-options')).toBeNull();
}));

it('should not show the USFM format option for drafts created before the supported date', fakeAsync(() => {
const user = 'user-display-name';
const date = dateBeforeFormattingSupported;
const trainingBooks = ['EXO'];
const translateBooks = ['GEN'];
const trainingDataFiles = ['file01'];
const entry = getStandardBuildDto({ user, date, trainingBooks, translateBooks, trainingDataFiles });

// SUT
component.entry = entry;
component.isLatestBuild = true;
tick();
fixture.detectChanges();

expect(component.scriptureRange).toEqual('GEN');
expect(component.draftIsAvailable).toBe(true);
expect(fixture.nativeElement.querySelector('.format-usfm')).toBeNull();
expect(component.formattingOptionsSupported).toBe(false);
}));
});

describe('formatDate', () => {
Expand Down Expand Up @@ -457,8 +492,9 @@ describe('DraftHistoryEntryComponent', () => {
id: 'project01'
},
additionalInfo: {
dateGenerated: new Date().toISOString(),
dateRequested: new Date().toISOString(),
dateGenerated: new Date(date).toISOString(),
dateFinished: new Date(date).toISOString(),
dateRequested: new Date(date).toISOString(),
requestedByUserId: 'sf-user-id',
trainingScriptureRanges:
trainingBooks.length > 0 ? [{ projectId: 'project02', scriptureRange: trainingBooks.join(';') }] : [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { BuildStates } from '../../../../machine-api/build-states';
import { RIGHT_TO_LEFT_MARK } from '../../../../shared/utils';
import { DraftDownloadButtonComponent } from '../../draft-download-button/draft-download-button.component';
import { DraftPreviewBooksComponent } from '../../draft-preview-books/draft-preview-books.component';
import { FORMATTING_OPTIONS_SUPPORTED_DATE } from '../../draft-utils';
import { TrainingDataService } from '../../training-data/training-data.service';

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

get formattingOptionsSupported(): boolean {
return this.featureFlags.usfmFormat.enabled && this.entry?.additionalInfo?.dateFinished != null
? new Date(this.entry.additionalInfo.dateFinished) > FORMATTING_OPTIONS_SUPPORTED_DATE
: false;
}

@Input() isLatestBuild: boolean = false;
trainingConfigurationOpen = false;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { TranslateSource } from 'realtime-server/lib/esm/scriptureforge/models/t
import language_code_mapping from '../../../../../language_code_mapping.json';
import { SelectableProjectWithLanguageCode } from '../../core/paratext.service';

// Corresponds to Serval 1.11.0 release
export const FORMATTING_OPTIONS_SUPPORTED_DATE: Date = new Date('2025-09-25T00:00:00Z');

/** Represents draft sources as a set of two {@link TranslateSource} arrays, and one {@link SFProjectProfile} array. */
export interface DraftSourcesAsTranslateSourceArrays {
trainingSources: TranslateSource[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,12 @@
<transloco class="hide-lt-md" key="editor_draft_tab.format_draft"></transloco>
<transloco class="hide-gt-md" key="editor_draft_tab.formatting"></transloco>
</button>
} @else {
} @else if (formattingOptionsSupported) {
<span
[matTooltip]="t(doesLatestHaveDraft ? 'format_draft_can' : 'format_draft_cannot')"
[style.cursor]="doesLatestHaveDraft ? 'pointer' : 'not-allowed'"
[matTooltip]="t(doesLatestBuildHaveDraft ? 'format_draft_can' : 'format_draft_cannot')"
[style.cursor]="doesLatestBuildHaveDraft ? 'pointer' : 'not-allowed'"
>
<button mat-button (click)="navigateToFormatting()" [disabled]="!doesLatestHaveDraft">
<button mat-button (click)="navigateToFormatting()" [disabled]="!doesLatestBuildHaveDraft">
<mat-icon>build</mat-icon>
<transloco class="hide-lt-md" key="editor_draft_tab.format_draft"></transloco>
<transloco class="hide-gt-md" key="editor_draft_tab.formatting"></transloco>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,7 @@ describe('EditorDraftComponent', () => {
flush();
}));

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

expect(component.mustChooseFormattingOptions).toBe(false);
flush();
}));

it('should guide user to select formatting options when formatting not selected', fakeAsync(() => {
const testProjectDoc: SFProjectProfileDoc = {
data: createTestProjectProfile({
texts: [
{
bookNum: 1,
chapters: [{ number: 1, permissions: { user01: SFProjectRole.ParatextAdministrator }, hasDraft: true }]
}
]
})
} as SFProjectProfileDoc;
when(mockDraftGenerationService.draftExists(anything(), anything(), anything())).thenReturn(of(true));
const historyAfterFormattingOptions: Revision[] = [{ timestamp: '2025-10-01T12:00:00.000Z' }];
when(mockDraftGenerationService.getGeneratedDraftHistory(anything(), anything(), anything())).thenReturn(
of(historyAfterFormattingOptions)
);
when(mockActivatedProjectService.changes$).thenReturn(of(testProjectDoc));
when(mockDialogService.confirm(anything(), anything())).thenResolve(true);
spyOn<any>(component, 'getTargetOps').and.returnValue(of(targetDelta.ops));
when(mockDraftHandlingService.getDraft(anything(), anything())).thenReturn(of(draftDelta.ops!));
when(mockDraftHandlingService.draftDataToOps(anything(), anything())).thenReturn(draftDelta.ops!);

fixture.detectChanges();
tick(EDITOR_READY_TIMEOUT);

expect(component.mustChooseFormattingOptions).toBe(true);
flush();
}));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { BuildStates } from '../../../machine-api/build-states';
import { TextComponent } from '../../../shared/text/text.component';
import { DraftGenerationService } from '../../draft-generation/draft-generation.service';
import { DraftHandlingService } from '../../draft-generation/draft-handling.service';
import { FORMATTING_OPTIONS_SUPPORTED_DATE } from '../../draft-generation/draft-utils';
@Component({
selector: 'app-editor-draft',
templateUrl: './editor-draft.component.html',
Expand All @@ -60,7 +61,6 @@ export class EditorDraftComponent implements AfterViewInit, OnChanges {

inputChanged$ = new Subject<void>();
draftCheckState: 'draft-unknown' | 'draft-present' | 'draft-legacy' | 'draft-empty' = 'draft-unknown';
draftRevisions: Revision[] = [];
selectedRevision: Revision | undefined;
generateDraftUrl?: string;
targetProject?: SFProjectProfile;
Expand All @@ -69,10 +69,13 @@ export class EditorDraftComponent implements AfterViewInit, OnChanges {
isDraftApplied = false;
userAppliedDraft = false;
hasFormattingSelected = true;
formattingOptionsSupported = true;

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

private _draftRevisions: Revision[] = [];

// 'asyncScheduler' prevents ExpressionChangedAfterItHasBeenCheckedError
private loading$ = new BehaviorSubject<boolean>(false);
isLoading$: Observable<boolean> = this.loading$.pipe(observeOn(asyncScheduler));
Expand Down Expand Up @@ -112,15 +115,29 @@ export class EditorDraftComponent implements AfterViewInit, OnChanges {
return this.draftHandlingService.canApplyDraft(this.targetProject, this.bookNum, this.chapter, this.draftDelta.ops);
}

get doesLatestHaveDraft(): boolean {
get doesLatestBuildHaveDraft(): boolean {
return (
this.targetProject?.texts.find(t => t.bookNum === this.bookNum)?.chapters.find(c => c.number === this.chapter)
?.hasDraft ?? false
);
}

get mustChooseFormattingOptions(): boolean {
return this.featureFlags.usfmFormat.enabled && !this.hasFormattingSelected && this.doesLatestHaveDraft;
return (
this.featureFlags.usfmFormat.enabled &&
!this.hasFormattingSelected &&
this.formattingOptionsSupported &&
this.doesLatestBuildHaveDraft
);
}

set draftRevisions(value: Revision[]) {
this._draftRevisions = value;
this.formattingOptionsSupported = value.some(rev => new Date(rev.timestamp) > FORMATTING_OPTIONS_SUPPORTED_DATE);
}

get draftRevisions(): Revision[] {
return this._draftRevisions;
}

ngOnChanges(): void {
Expand Down
Loading