Skip to content

Commit 70af2f3

Browse files
committed
SF-3601 Navigate to the first chapter of a draft when formatting draft
1 parent 6c0ebdf commit 70af2f3

File tree

8 files changed

+122
-64
lines changed

8 files changed

+122
-64
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ export class DraftHandlingService {
280280
*/
281281
opsHaveContent(ops: DeltaOperation[]): boolean {
282282
const indexOfFirstText = ops.findIndex(op => typeof op.insert === 'string');
283-
const onlyTextOpIsTrailingNewline = indexOfFirstText === ops.length - 1 && ops[indexOfFirstText].insert === '\n';
283+
const onlyTextOpIsTrailingNewline = indexOfFirstText === ops.length - 1 && ops[indexOfFirstText]?.insert === '\n';
284284
const hasNoExistingText = indexOfFirstText === -1 || onlyTextOpIsTrailingNewline;
285285
return !hasNoExistingText;
286286
}

src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-usfm-format/draft-usfm-format.component.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,15 @@ <h1>{{ t("formatting_options") }}</h1>
7575
[book]="bookNum"
7676
(bookChange)="bookChanged($event)"
7777
[(chapter)]="chapterNum"
78-
[chapters]="chapters"
78+
[chapters]="chaptersWithDrafts"
7979
(chapterChange)="chapterChanged($event)"
8080
></app-book-chapter-chooser>
8181
<div class="viewer-container">
8282
<app-text
8383
[isReadOnly]="true"
8484
[subscribeToUpdates]="false"
8585
[isRightToLeft]="isRightToLeft"
86+
[placeholder]="isOnline ? t('chapter_draft_does_not_exist') : t('text_unavailable_offline')"
8687
[class.initializing]="isInitializing"
8788
[class.loading]="isLoadingData"
8889
[style.--project-font]="projectFont"

src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-usfm-format/draft-usfm-format.component.scss

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
flex-direction: row;
1414
column-gap: 8px;
1515
height: 100%;
16+
width: 100%;
1617
margin-top: 8px;
1718

1819
@include breakpoints.media-breakpoint-down(sm) {
@@ -48,13 +49,15 @@
4849
display: flex;
4950
flex-direction: column;
5051
overflow: hidden;
52+
width: 100%;
5153

5254
@include breakpoints.media-breakpoint-down(sm) {
5355
height: 600px;
5456
overflow: initial;
5557
}
5658

5759
.viewer-container {
60+
height: 100%;
5861
overflow-y: auto;
5962
}
6063
}

src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-usfm-format/draft-usfm-format.component.spec.ts

Lines changed: 84 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@ import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testin
44
import { MatRadioButtonHarness } from '@angular/material/radio/testing';
55
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
66
import { ActivatedRoute } from '@angular/router';
7+
import { SFProjectProfile } from 'realtime-server/lib/esm/scriptureforge/models/sf-project';
78
import { createTestProjectProfile } from 'realtime-server/lib/esm/scriptureforge/models/sf-project-test-data';
89
import { TextInfo } from 'realtime-server/lib/esm/scriptureforge/models/text-info';
910
import {
11+
DraftConfig,
1012
DraftUsfmConfig,
1113
ParagraphBreakFormat,
12-
QuoteFormat
14+
QuoteFormat,
15+
TranslateConfig
1316
} from 'realtime-server/lib/esm/scriptureforge/models/translate-config';
1417
import { of } from 'rxjs';
1518
import { anything, deepEqual, mock, verify, when } from 'ts-mockito';
@@ -81,8 +84,15 @@ describe('DraftUsfmFormatComponent', () => {
8184

8285
it('shows message if user is not online', fakeAsync(async () => {
8386
const env = new TestEnvironment({
84-
config: { paragraphFormat: ParagraphBreakFormat.MoveToEnd, quoteFormat: QuoteFormat.Denormalized }
87+
project: {
88+
translateConfig: {
89+
draftConfig: {
90+
usfmConfig: { paragraphFormat: ParagraphBreakFormat.MoveToEnd, quoteFormat: QuoteFormat.Normalized }
91+
} as DraftConfig
92+
} as TranslateConfig
93+
}
8594
});
95+
8696
expect(env.offlineMessage).toBeNull();
8797

8898
env.onlineStatusService.setIsOnline(false);
@@ -105,19 +115,63 @@ describe('DraftUsfmFormatComponent', () => {
105115
verify(mockedDraftHandlingService.getDraft(anything(), anything())).once();
106116
}));
107117

118+
it('can navigate to first book and chapter if book does not exist', fakeAsync(() => {
119+
when(mockedActivatedRoute.params).thenReturn(of({ bookId: 'NUM', chapter: '1' }));
120+
const env = new TestEnvironment();
121+
tick(EDITOR_READY_TIMEOUT);
122+
env.fixture.detectChanges();
123+
tick(EDITOR_READY_TIMEOUT);
124+
expect(env.component.bookNum).toBe(1);
125+
expect(env.component.chapterNum).toBe(1);
126+
verify(mockedDraftHandlingService.getDraft(anything(), anything())).once();
127+
}));
128+
129+
it('can navigate to book and chapter if first chapter has no draft', fakeAsync(() => {
130+
const env = new TestEnvironment({
131+
project: {
132+
texts: [
133+
{
134+
bookNum: 1,
135+
chapters: [
136+
{ number: 1, lastVerse: 15, isValid: true, permissions: {}, hasDraft: false },
137+
{ number: 2, lastVerse: 20, isValid: true, permissions: {}, hasDraft: true },
138+
{ number: 3, lastVerse: 18, isValid: true, permissions: {}, hasDraft: true }
139+
],
140+
hasSource: true,
141+
permissions: {}
142+
}
143+
]
144+
}
145+
});
146+
tick(EDITOR_READY_TIMEOUT);
147+
env.fixture.detectChanges();
148+
tick(EDITOR_READY_TIMEOUT);
149+
expect(env.component.bookNum).toBe(1);
150+
expect(env.component.chapterNum).toBe(2);
151+
expect(env.component.chaptersWithDrafts).toEqual([2, 3]);
152+
verify(mockedDraftHandlingService.getDraft(anything(), anything())).once();
153+
}));
154+
108155
// Book and chapter changed
109156
it('navigates to a different book and chapter', fakeAsync(() => {
110157
const env = new TestEnvironment({
111-
config: { paragraphFormat: ParagraphBreakFormat.MoveToEnd, quoteFormat: QuoteFormat.Denormalized }
158+
project: {
159+
translateConfig: {
160+
draftConfig: {
161+
usfmConfig: { paragraphFormat: ParagraphBreakFormat.MoveToEnd, quoteFormat: QuoteFormat.Denormalized }
162+
} as DraftConfig
163+
} as TranslateConfig
164+
}
112165
});
166+
113167
verify(mockedDraftHandlingService.getDraft(anything(), anything())).once();
114-
expect(env.component.chapters.length).toEqual(1);
168+
expect(env.component.chaptersWithDrafts.length).toEqual(1);
115169
expect(env.component.booksWithDrafts.length).toEqual(2);
116170

117171
env.component.bookChanged(2);
118172
tick();
119173
env.fixture.detectChanges();
120-
expect(env.component.chapters.length).toEqual(2);
174+
expect(env.component.chaptersWithDrafts.length).toEqual(2);
121175
verify(mockedDraftHandlingService.getDraft(anything(), anything())).twice();
122176

123177
env.component.chapterChanged(2);
@@ -136,15 +190,27 @@ describe('DraftUsfmFormatComponent', () => {
136190

137191
it('should show the currently selected format options', fakeAsync(() => {
138192
const env = new TestEnvironment({
139-
config: { paragraphFormat: ParagraphBreakFormat.MoveToEnd, quoteFormat: QuoteFormat.Normalized }
193+
project: {
194+
translateConfig: {
195+
draftConfig: {
196+
usfmConfig: { paragraphFormat: ParagraphBreakFormat.MoveToEnd, quoteFormat: QuoteFormat.Normalized }
197+
} as DraftConfig
198+
} as TranslateConfig
199+
}
140200
});
141201
expect(env.component.paragraphFormat.value).toBe(ParagraphBreakFormat.MoveToEnd);
142202
expect(env.component.quoteFormat.value).toBe(QuoteFormat.Normalized);
143203
}));
144204

145205
it('goes back if user chooses different configurations and then goes back', fakeAsync(async () => {
146206
const env = new TestEnvironment({
147-
config: { paragraphFormat: ParagraphBreakFormat.MoveToEnd, quoteFormat: QuoteFormat.Denormalized }
207+
project: {
208+
translateConfig: {
209+
draftConfig: {
210+
usfmConfig: { paragraphFormat: ParagraphBreakFormat.MoveToEnd, quoteFormat: QuoteFormat.Denormalized }
211+
} as DraftConfig
212+
} as TranslateConfig
213+
}
148214
});
149215
verify(mockedDraftHandlingService.getDraft(anything(), anything())).once();
150216
expect(env.harnesses?.length).toEqual(5);
@@ -165,7 +231,13 @@ describe('DraftUsfmFormatComponent', () => {
165231

166232
it('should save changes to the draft format', fakeAsync(async () => {
167233
const env = new TestEnvironment({
168-
config: { paragraphFormat: ParagraphBreakFormat.MoveToEnd, quoteFormat: QuoteFormat.Denormalized }
234+
project: {
235+
translateConfig: {
236+
draftConfig: {
237+
usfmConfig: { paragraphFormat: ParagraphBreakFormat.MoveToEnd, quoteFormat: QuoteFormat.Denormalized }
238+
} as DraftConfig
239+
} as TranslateConfig
240+
}
169241
});
170242
verify(mockedDraftHandlingService.getDraft(anything(), anything())).once();
171243
expect(env.harnesses?.length).toEqual(5);
@@ -215,7 +287,7 @@ class TestEnvironment {
215287
readonly projectId = 'project01';
216288
onlineStatusService: TestOnlineStatusService;
217289

218-
constructor(args: { config?: DraftUsfmConfig; quotationAnalysis?: QuotationAnalysis } = {}) {
290+
constructor(args: { project?: Partial<SFProjectProfile>; quotationAnalysis?: QuotationAnalysis } = {}) {
219291
const userDoc = mock(UserDoc);
220292
this.onlineStatusService = TestBed.inject(OnlineStatusService) as TestOnlineStatusService;
221293
when(mockedDraftGenerationService.getLastCompletedBuild(anything())).thenReturn(
@@ -235,7 +307,7 @@ class TestEnvironment {
235307
when(mockedNoticeService.show(anything())).thenResolve();
236308
when(mockedDialogService.confirm(anything(), anything(), anything())).thenResolve(true);
237309
when(mockedServalAdministration.onlineRetrievePreTranslationStatus(anything())).thenResolve();
238-
this.setupProject(args.config);
310+
this.setupProject(args.project);
239311
this.fixture = TestBed.createComponent(DraftUsfmFormatComponent);
240312
this.component = this.fixture.componentInstance;
241313
const loader = TestbedHarnessEnvironment.loader(this.fixture);
@@ -260,7 +332,7 @@ class TestEnvironment {
260332
return this.fixture.nativeElement.querySelector('.quote-format-warning');
261333
}
262334

263-
setupProject(config?: DraftUsfmConfig): void {
335+
setupProject(project?: Partial<SFProjectProfile>): void {
264336
const texts: TextInfo[] = [
265337
{
266338
bookNum: 1,
@@ -286,7 +358,7 @@ class TestEnvironment {
286358
];
287359
const projectDoc = {
288360
id: this.projectId,
289-
data: createTestProjectProfile({ translateConfig: { draftConfig: { usfmConfig: config } }, texts })
361+
data: createTestProjectProfile({ translateConfig: project?.translateConfig, texts: project?.texts ?? texts })
290362
} as SFProjectProfileDoc;
291363
when(mockedActivatedProjectService.projectId).thenReturn(this.projectId);
292364
when(mockedActivatedProjectService.projectDoc$).thenReturn(of(projectDoc));

src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-usfm-format/draft-usfm-format.component.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { ActivatedRoute } from '@angular/router';
1313
import { TranslocoModule } from '@ngneat/transloco';
1414
import { Canon } from '@sillsdev/scripture';
1515
import { Delta } from 'quill';
16+
import { SFProjectProfile } from 'realtime-server/lib/esm/scriptureforge/models/sf-project';
1617
import { TextInfo } from 'realtime-server/lib/esm/scriptureforge/models/text-info';
1718
import {
1819
DraftUsfmConfig,
@@ -63,7 +64,7 @@ export class DraftUsfmFormatComponent extends DataLoadingComponent implements Af
6364
bookNum: number = 1;
6465
booksWithDrafts: number[] = [];
6566
chapterNum: number = 1;
66-
chapters: number[] = [];
67+
chaptersWithDrafts: number[] = [];
6768
isInitializing: boolean = true;
6869
paragraphBreakFormat = ParagraphBreakFormat;
6970
quoteStyle = QuoteFormat;
@@ -156,11 +157,11 @@ export class DraftUsfmFormatComponent extends DataLoadingComponent implements Af
156157
defaultBook = Canon.bookIdToNumber(params['bookId']);
157158
}
158159
let defaultChapter = 1;
159-
this.chapters = texts.find(t => t.bookNum === defaultBook)?.chapters.map(c => c.number) ?? [];
160-
if (params['chapter'] !== undefined && this.chapters.includes(Number(params['chapter']))) {
160+
this.chaptersWithDrafts = this.getChaptersWithDrafts(defaultBook, projectDoc.data);
161+
if (params['chapter'] !== undefined && this.chaptersWithDrafts.includes(Number(params['chapter']))) {
161162
defaultChapter = Number(params['chapter']);
162-
} else if (this.chapters.length > 0) {
163-
defaultChapter = this.chapters[0];
163+
} else if (this.chaptersWithDrafts.length > 0) {
164+
defaultChapter = this.chaptersWithDrafts[0];
164165
}
165166
this.projectFont = this.fontService.getFontFamilyFromProject(projectDoc);
166167
this.bookChanged(defaultBook, defaultChapter);
@@ -190,9 +191,8 @@ export class DraftUsfmFormatComponent extends DataLoadingComponent implements Af
190191

191192
bookChanged(bookNum: number, chapterNum?: number): void {
192193
this.bookNum = bookNum;
193-
const texts = this.activatedProjectService.projectDoc!.data!.texts;
194-
this.chapters = texts.find(t => t.bookNum === this.bookNum)?.chapters.map(c => c.number) ?? [];
195-
this.chapterNum = chapterNum ?? this.chapters[0] ?? 1;
194+
this.chaptersWithDrafts = this.getChaptersWithDrafts(bookNum, this.activatedProjectService.projectDoc!.data!);
195+
this.chapterNum = chapterNum ?? this.chaptersWithDrafts[0] ?? 1;
196196
this.reloadText();
197197
}
198198

@@ -258,4 +258,13 @@ export class DraftUsfmFormatComponent extends DataLoadingComponent implements Af
258258
if (isOnline) this.reloadText();
259259
});
260260
}
261+
262+
private getChaptersWithDrafts(bookNum: number, project: SFProjectProfile): number[] {
263+
return (
264+
project.texts
265+
.find(t => t.bookNum === bookNum)
266+
?.chapters.filter(c => !!c.hasDraft && c.lastVerse > 0)
267+
.map(c => c.number) ?? []
268+
);
269+
}
261270
}

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

Lines changed: 7 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ describe('EditorDraftComponent', () => {
8585
buildProgress$.next({ state: BuildStates.Completed } as BuildDto);
8686
when(mockActivatedProjectService.projectId$).thenReturn(of('targetProjectId'));
8787
when(mockDraftGenerationService.getLastCompletedBuild(anything())).thenReturn(of(undefined));
88+
when(mockDraftHandlingService.opsHaveContent(anything())).thenReturn(true);
8889

8990
fixture = TestBed.createComponent(EditorDraftComponent);
9091
component = fixture.componentInstance;
@@ -307,6 +308,7 @@ describe('EditorDraftComponent', () => {
307308
when(mockDraftHandlingService.getDraft(anything(), anything())).thenReturn(of(cloneDeep(emptyDraftDelta.ops!)));
308309
when(mockDraftHandlingService.draftDataToOps(anything(), anything())).thenReturn(emptyDraftDelta.ops!);
309310
when(mockDraftHandlingService.isDraftSegmentMap(anything())).thenReturn(false);
311+
when(mockDraftHandlingService.opsHaveContent(anything())).thenReturn(false);
310312

311313
// SUT
312314
fixture.detectChanges();
@@ -318,35 +320,12 @@ describe('EditorDraftComponent', () => {
318320
flush();
319321
}));
320322

321-
it('should show draft empty if earlier draft exists but history is not enabled', fakeAsync(() => {
323+
it('should set editor to empty state when no revision', fakeAsync(() => {
322324
const testProjectDoc: SFProjectProfileDoc = {
323325
data: createTestProjectProfile()
324326
} as SFProjectProfileDoc;
325-
when(mockFeatureFlagService.newDraftHistory).thenReturn(createTestFeatureFlag(false));
326327
when(mockDraftGenerationService.draftExists(anything(), anything(), anything())).thenReturn(of(false));
327-
when(mockDraftGenerationService.getGeneratedDraftHistory(anything(), anything(), anything())).thenReturn(
328-
of(draftHistory)
329-
);
330-
when(mockActivatedProjectService.changes$).thenReturn(of(testProjectDoc));
331-
spyOn<any>(component, 'getTargetOps').and.returnValue(of(targetDelta.ops!));
332-
333-
fixture.detectChanges();
334-
tick(EDITOR_READY_TIMEOUT);
335-
336-
verify(mockDraftHandlingService.getDraft(anything(), anything())).never();
337-
verify(mockDraftHandlingService.draftDataToOps(anything(), anything())).never();
338-
expect(component.draftCheckState).toEqual('draft-empty');
339-
flush();
340-
}));
341-
342-
it('should return ops and update the editor when no revision', fakeAsync(() => {
343-
const testProjectDoc: SFProjectProfileDoc = {
344-
data: createTestProjectProfile()
345-
} as SFProjectProfileDoc;
346-
when(mockDraftGenerationService.draftExists(anything(), anything(), anything())).thenReturn(of(true));
347-
when(mockDraftGenerationService.getGeneratedDraftHistory(anything(), anything(), anything())).thenReturn(
348-
of(undefined)
349-
);
328+
when(mockDraftGenerationService.getGeneratedDraftHistory(anything(), anything(), anything())).thenReturn(of([]));
350329
when(mockActivatedProjectService.changes$).thenReturn(of(testProjectDoc));
351330
spyOn<any>(component, 'getTargetOps').and.returnValue(of(targetDelta.ops!));
352331
when(mockDraftHandlingService.getDraft(anything(), anything())).thenReturn(of(cloneDeep(draftDelta.ops!)));
@@ -356,10 +335,9 @@ describe('EditorDraftComponent', () => {
356335
fixture.detectChanges();
357336
tick(EDITOR_READY_TIMEOUT);
358337

359-
verify(mockDraftHandlingService.getDraft(anything(), anything())).once();
360-
verify(mockDraftHandlingService.draftDataToOps(anything(), anything())).once();
361-
expect(component.draftCheckState).toEqual('draft-present');
362-
expect(component.draftText.editor!.getContents().ops).toEqual(draftDelta.ops);
338+
verify(mockDraftHandlingService.getDraft(anything(), anything())).never();
339+
verify(mockDraftHandlingService.draftDataToOps(anything(), anything())).never();
340+
expect(component.draftCheckState).toEqual('draft-empty');
363341
flush();
364342
}));
365343

0 commit comments

Comments
 (0)