Skip to content
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 @@ -183,6 +183,67 @@ describe('DraftGenerationService', () => {
}));
});

describe('getLastPreTranslationBuild', () => {
it('should get last pre-translation build and return an observable of BuildDto', fakeAsync(() => {
// SUT
service.getLastPreTranslationBuild(projectId).subscribe(result => {
expect(result).toEqual(buildDto);
});
tick();

// Setup the HTTP request
const req = httpTestingController.expectOne(
`${MACHINE_API_BASE_URL}translation/engines/project:${projectId}/actions/getLastPreTranslationBuild`
);
expect(req.request.method).toEqual('GET');
req.flush(buildDto);
tick();
}));

it('should return undefined when no build has ever run', fakeAsync(() => {
// SUT
service.getLastPreTranslationBuild(projectId).subscribe(result => {
expect(result).toBeUndefined();
});
tick();

// Setup the HTTP request
const req = httpTestingController.expectOne(
`${MACHINE_API_BASE_URL}translation/engines/project:${projectId}/actions/getLastPreTranslationBuild`
);
expect(req.request.method).toEqual('GET');
req.flush(null, { status: HttpStatusCode.NoContent, statusText: 'No Content' });
tick();
}));

it('should return undefined if offline', fakeAsync(() => {
testOnlineStatusService.setIsOnline(false);

// SUT
service.getLastPreTranslationBuild(projectId).subscribe(result => {
expect(result).toBeUndefined();
});
tick();
}));

it('should return undefined and show error when server returns 500', fakeAsync(() => {
// SUT
service.getLastPreTranslationBuild(projectId).subscribe(result => {
expect(result).toBeUndefined();
verify(mockNoticeService.showError(anything())).once();
});
tick();

// Setup the HTTP request
const req = httpTestingController.expectOne(
`${MACHINE_API_BASE_URL}translation/engines/project:${projectId}/actions/getLastPreTranslationBuild`
);
expect(req.request.method).toEqual('GET');
req.flush(null, { status: HttpStatusCode.InternalServerError, statusText: 'Server Error' });
tick();
}));
});

describe('getBuildHistory', () => {
it('should get project builds and return an observable array of BuildDto', fakeAsync(() => {
// SUT
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,33 @@ export class DraftGenerationService {
);
}

/**
* Gets the last pre-translation build regardless of state (Completed, Running, Queued, Faulted, or Canceled).
* This is a simpler accessor than getLastCompletedBuild() and can be used when the consumer
* wants the most recent build even if it has not yet completed.
* @param projectId The SF project id for the target translation.
* @returns An observable BuildDto for the last pre-translation build, or undefined if no build has ever run.
*/
getLastPreTranslationBuild(projectId: string): Observable<BuildDto | undefined> {
if (!this.onlineStatusService.isOnline) {
return of(undefined);
}
return this.httpClient
.get<BuildDto>(`translation/engines/project:${projectId}/actions/getLastPreTranslationBuild`)
.pipe(
map(res => res.data),
catchError(err => {
// If project doesn't exist on Serval, return undefined
if (err.status === 403 || err.status === 404) {
return of(undefined);
}

this.noticeService.showError(this.i18n.translateStatic('draft_generation.temporarily_unavailable'));
return of(undefined);
})
);
}

/** Gets the build exactly as Serval returns it */
getRawBuild(buildId: string): Observable<Object | undefined> {
if (!this.onlineStatusService.isOnline) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,9 @@
</mat-form-field>
}
<div class="apply-draft-button-container">
@if (showDraftOptionsButton$ | async) {
<span
[matTooltip]="t(doesLatestHaveDraft ? 'format_draft_can' : 'format_draft_cannot')"
[style.cursor]="doesLatestHaveDraft ? 'pointer' : 'not-allowed'"
>
<button mat-button (click)="navigateToFormatting()" [disabled]="!doesLatestHaveDraft">
@if ((isFormattingSupportedForLatest$ | async) && canConfigureFormatting) {
<span [matTooltip]="t('format_draft_tooltip')">
<button mat-button (click)="navigateToFormatting()">
<mat-icon>build</mat-icon>
<transloco key="editor_draft_tab.format_draft"></transloco>
</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ describe('EditorDraftComponent', () => {
buildProgress$.next({ state: BuildStates.Completed } as BuildDto);
when(mockActivatedProjectService.projectId$).thenReturn(of('targetProjectId'));
when(mockDraftGenerationService.getLastCompletedBuild(anything())).thenReturn(of(undefined));
const defaultProjectDoc: SFProjectProfileDoc = { data: createTestProjectProfile() } as SFProjectProfileDoc;
when(mockActivatedProjectService.projectDoc$).thenReturn(of(defaultProjectDoc));
when(mockDraftGenerationService.getLastPreTranslationBuild(anything())).thenReturn(
of({ state: BuildStates.Completed } as BuildDto)
);

fixture = TestBed.createComponent(EditorDraftComponent);
component = fixture.componentInstance;
Expand Down Expand Up @@ -559,6 +564,129 @@ describe('EditorDraftComponent', () => {
}));
});

describe('canConfigureFormatting', () => {
beforeEach(() => {
when(mockFeatureFlagService.newDraftHistory).thenReturn(createTestFeatureFlag(true));
when(mockDraftGenerationService.getGeneratedDraftHistory(anything(), anything(), anything())).thenReturn(
of(draftHistory)
);
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!);
});

it('should be true when latest build has draft and selected revision is latest', 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));
when(mockActivatedProjectService.projectDoc$).thenReturn(of(testProjectDoc));
when(mockActivatedProjectService.changes$).thenReturn(of(testProjectDoc));

when(mockDraftGenerationService.getLastPreTranslationBuild(anything())).thenReturn(
of({ state: BuildStates.Completed } as BuildDto)
);
fixture.detectChanges();
tick(EDITOR_READY_TIMEOUT);

expect(component.isSelectedDraftLatest).toBe(true);
expect(component.canConfigureFormatting).toBe(true);
flush();
}));

it('should be false when selected revision is not the latest', 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));
when(mockActivatedProjectService.projectDoc$).thenReturn(of(testProjectDoc));
when(mockActivatedProjectService.changes$).thenReturn(of(testProjectDoc));

when(mockDraftGenerationService.getLastPreTranslationBuild(anything())).thenReturn(
of({ state: BuildStates.Completed } as BuildDto)
);
fixture.detectChanges();
tick(EDITOR_READY_TIMEOUT);
expect(component.canConfigureFormatting).toBe(true);

// Select earlier revision
component.onSelectionChanged({ value: draftHistory[1] } as MatSelectChange);
fixture.detectChanges();
tick(EDITOR_READY_TIMEOUT);

expect(component.isSelectedDraftLatest).toBe(false);
expect(component.canConfigureFormatting).toBe(false);
flush();
}));

it('should be false when latest build does not have a draft', fakeAsync(() => {
const testProjectDoc: SFProjectProfileDoc = {
data: createTestProjectProfile({
texts: [
{
bookNum: 1,
chapters: [{ number: 1, permissions: { user01: SFProjectRole.ParatextAdministrator }, hasDraft: false }]
}
]
})
} as SFProjectProfileDoc;
when(mockDraftGenerationService.draftExists(anything(), anything(), anything())).thenReturn(of(false));
when(mockActivatedProjectService.projectDoc$).thenReturn(of(testProjectDoc));
when(mockActivatedProjectService.changes$).thenReturn(of(testProjectDoc));

when(mockDraftGenerationService.getLastPreTranslationBuild(anything())).thenReturn(
of({ state: BuildStates.Completed } as BuildDto)
);
fixture.detectChanges();
tick(EDITOR_READY_TIMEOUT);
tick();
expect(component.isSelectedDraftLatest).toBe(true);
expect(component.canConfigureFormatting).toBe(false);
flush();
}));

it('should be false when latest build is canceled even if draft exists and selected revision is latest', fakeAsync(() => {
const testProjectDoc: SFProjectProfileDoc = {
data: createTestProjectProfile({
texts: [
{
bookNum: 1,
chapters: [{ number: 1, permissions: { user01: SFProjectRole.ParatextAdministrator }, hasDraft: true }]
}
]
})
} as SFProjectProfileDoc;
when(mockDraftGenerationService.getLastPreTranslationBuild(anything())).thenReturn(
of({ state: BuildStates.Canceled } as BuildDto)
);
when(mockDraftGenerationService.draftExists(anything(), anything(), anything())).thenReturn(of(true));
when(mockActivatedProjectService.projectDoc$).thenReturn(of(testProjectDoc));
when(mockActivatedProjectService.changes$).thenReturn(of(testProjectDoc));

fixture.detectChanges();
tick(EDITOR_READY_TIMEOUT);

expect(component.isSelectedDraftLatest).toBe(true);
expect(component.doesLatestCompletedHaveDraft).toBe(true);
expect(component.canConfigureFormatting).toBe(false);
flush();
}));
});

describe('getLocalizedBookChapter', () => {
it('should return an empty string if bookNum or chapter is undefined', () => {
component.bookNum = undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
startWith,
Subject,
switchMap,
take,
tap,
throttleTime
} from 'rxjs';
Expand All @@ -47,6 +48,7 @@ import { isString } from '../../../../type-utils';
import { TextDocId } from '../../../core/models/text-doc';
import { Revision } from '../../../core/paratext.service';
import { SFProjectService } from '../../../core/sf-project.service';
import { BuildDto } from '../../../machine-api/build-dto';
import { BuildStates } from '../../../machine-api/build-states';
import { NoticeComponent } from '../../../shared/notice/notice.component';
import { TextComponent } from '../../../shared/text/text.component';
Expand Down Expand Up @@ -113,14 +115,15 @@ export class EditorDraftComponent implements AfterViewInit, OnChanges {
this.onlineStatusService.onlineStatus$
]).pipe(map(([isLoading, isOnline]) => isLoading || !isOnline));

showDraftOptionsButton$: Observable<boolean> = this.activatedProjectService.projectId$.pipe(
isFormattingSupportedForLatest$: Observable<boolean> = this.activatedProjectService.projectId$.pipe(
filterNullish(),
switchMap(projectId => this.draftGenerationService.getLastCompletedBuild(projectId)),
map(build => this.draftOptionsService.areFormattingOptionsSupportedForBuild(build))
);

private draftDelta?: Delta;
private targetDelta?: Delta;
private _latestPreTranslationBuild: BuildDto | undefined;

constructor(
private readonly activatedProjectService: ActivatedProjectService,
Expand Down Expand Up @@ -150,13 +153,25 @@ export class EditorDraftComponent implements AfterViewInit, OnChanges {
return this.draftHandlingService.canApplyDraft(this.targetProject, this.bookNum, this.chapter, this.draftDelta.ops);
}

get doesLatestHaveDraft(): boolean {
get canConfigureFormatting(): boolean {
return this.doesLatestCompletedHaveDraft && this.isSelectedDraftLatest && this.isLatestBuildCompleted;
}

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

get isLatestBuildCompleted(): boolean {
return this._latestPreTranslationBuild?.state === BuildStates.Completed;
}

get isSelectedDraftLatest(): boolean {
return this.selectedRevision?.timestamp === this._draftRevisions[0].timestamp;
}

set draftRevisions(value: Revision[]) {
this._draftRevisions = value;
}
Expand Down Expand Up @@ -239,9 +254,6 @@ export class EditorDraftComponent implements AfterViewInit, OnChanges {
// Respond to project changes
return this.activatedProjectService.changes$.pipe(
filterNullish(),
tap(projectDoc => {
this.targetProject = projectDoc.data;
}),
distinctUntilChanged(),
map(() => initialTimestamp)
);
Expand Down Expand Up @@ -299,6 +311,36 @@ export class EditorDraftComponent implements AfterViewInit, OnChanges {

this.isDraftReady = this.draftCheckState === 'draft-present' || this.draftCheckState === 'draft-legacy';
});

combineLatest([
this.onlineStatusService.onlineStatus$,
this.activatedProjectService.projectDoc$,
this.draftGenerationService.pollBuildProgress(this.textDocId!.projectId),
this.draftText.editorCreated as EventEmitter<any>,
this.inputChanged$.pipe(startWith(undefined))
])
.pipe(
quietTakeUntilDestroyed(this.destroyRef),
filter(([_, projectDoc]) => projectDoc !== undefined),
tap(([_, projectDoc]) => {
this.targetProject = projectDoc!.data;
}),
filter(([isOnline, _]) => {
return isOnline && this.doesLatestCompletedHaveDraft;
}),
switchMap(() => this.refreshLastPreTranslationBuild()),
tap((build: BuildDto | undefined) => {
this._latestPreTranslationBuild = build;
})
)
.subscribe();
}

private refreshLastPreTranslationBuild(): Observable<BuildDto | undefined> {
if (this.projectId == null) {
return of<BuildDto | undefined>(undefined);
}
return this.draftGenerationService.getLastPreTranslationBuild(this.projectId).pipe(take(1));
}

navigateToFormatting(): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4905,6 +4905,7 @@ class TestEnvironment {
when(mockedDraftGenerationService.pollBuildProgress(anything())).thenReturn(
of({ state: BuildStates.Completed } as BuildDto)
);
when(mockedDraftGenerationService.getLastPreTranslationBuild(anything())).thenReturn(of(undefined));
when(
mockedDraftGenerationService.getGeneratedDraftDeltaOperations(
anything(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -410,8 +410,7 @@
"draft_legacy_warning": "We have updated our drafting functionality. You can take advantage of this by [link:generateDraftUrl]generating a new draft[/link].",
"error_applying_draft": "Failed to add the draft to the project. Try again later.",
"format_draft": "Formatting options",
"format_draft_can": "Customize formatting options for the draft",
"format_draft_cannot": "You can only change formatting for books from the latest draft",
"format_draft_tooltip": "Customize formatting options for the draft",
"no_draft_notice": "{{ bookChapterName }} has no draft.",
"offline_notice": "Generated drafts are not available offline.",
"overwrite": "Adding the draft will overwrite the current chapter. Are you sure you want to continue?",
Expand Down
Loading
Loading