Skip to content

Commit 12c8136

Browse files
SF-3601 Navigate to the first chapter of a draft when formatting draft (#3532)
1 parent 79b3470 commit 12c8136

File tree

8 files changed

+146
-114
lines changed

8 files changed

+146
-114
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 { provideNoopAnimations } 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';
@@ -78,8 +81,15 @@ describe('DraftUsfmFormatComponent', () => {
7881

7982
it('shows message if user is not online', fakeAsync(async () => {
8083
const env = new TestEnvironment({
81-
config: { paragraphFormat: ParagraphBreakFormat.MoveToEnd, quoteFormat: QuoteFormat.Denormalized }
84+
project: {
85+
translateConfig: {
86+
draftConfig: {
87+
usfmConfig: { paragraphFormat: ParagraphBreakFormat.MoveToEnd, quoteFormat: QuoteFormat.Normalized }
88+
} as DraftConfig
89+
} as TranslateConfig
90+
}
8291
});
92+
8393
expect(env.offlineMessage).toBeNull();
8494

8595
env.onlineStatusService.setIsOnline(false);
@@ -102,19 +112,63 @@ describe('DraftUsfmFormatComponent', () => {
102112
verify(mockedDraftHandlingService.getDraft(anything(), anything())).once();
103113
}));
104114

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

114168
env.component.bookChanged(2);
115169
tick();
116170
env.fixture.detectChanges();
117-
expect(env.component.chapters.length).toEqual(2);
171+
expect(env.component.chaptersWithDrafts.length).toEqual(2);
118172
verify(mockedDraftHandlingService.getDraft(anything(), anything())).twice();
119173

120174
env.component.chapterChanged(2);
@@ -133,15 +187,27 @@ describe('DraftUsfmFormatComponent', () => {
133187

134188
it('should show the currently selected format options', fakeAsync(() => {
135189
const env = new TestEnvironment({
136-
config: { paragraphFormat: ParagraphBreakFormat.MoveToEnd, quoteFormat: QuoteFormat.Normalized }
190+
project: {
191+
translateConfig: {
192+
draftConfig: {
193+
usfmConfig: { paragraphFormat: ParagraphBreakFormat.MoveToEnd, quoteFormat: QuoteFormat.Normalized }
194+
} as DraftConfig
195+
} as TranslateConfig
196+
}
137197
});
138198
expect(env.component.paragraphFormat.value).toBe(ParagraphBreakFormat.MoveToEnd);
139199
expect(env.component.quoteFormat.value).toBe(QuoteFormat.Normalized);
140200
}));
141201

142202
it('goes back if user chooses different configurations and then goes back', fakeAsync(async () => {
143203
const env = new TestEnvironment({
144-
config: { paragraphFormat: ParagraphBreakFormat.MoveToEnd, quoteFormat: QuoteFormat.Denormalized }
204+
project: {
205+
translateConfig: {
206+
draftConfig: {
207+
usfmConfig: { paragraphFormat: ParagraphBreakFormat.MoveToEnd, quoteFormat: QuoteFormat.Denormalized }
208+
} as DraftConfig
209+
} as TranslateConfig
210+
}
145211
});
146212
verify(mockedDraftHandlingService.getDraft(anything(), anything())).once();
147213
expect(env.harnesses?.length).toEqual(5);
@@ -162,7 +228,13 @@ describe('DraftUsfmFormatComponent', () => {
162228

163229
it('should save changes to the draft format', fakeAsync(async () => {
164230
const env = new TestEnvironment({
165-
config: { paragraphFormat: ParagraphBreakFormat.MoveToEnd, quoteFormat: QuoteFormat.Denormalized }
231+
project: {
232+
translateConfig: {
233+
draftConfig: {
234+
usfmConfig: { paragraphFormat: ParagraphBreakFormat.MoveToEnd, quoteFormat: QuoteFormat.Denormalized }
235+
} as DraftConfig
236+
} as TranslateConfig
237+
}
166238
});
167239
verify(mockedDraftHandlingService.getDraft(anything(), anything())).once();
168240
expect(env.harnesses?.length).toEqual(5);
@@ -212,7 +284,7 @@ class TestEnvironment {
212284
readonly projectId = 'project01';
213285
onlineStatusService: TestOnlineStatusService;
214286

215-
constructor(args: { config?: DraftUsfmConfig; quotationAnalysis?: QuotationAnalysis } = {}) {
287+
constructor(args: { project?: Partial<SFProjectProfile>; quotationAnalysis?: QuotationAnalysis } = {}) {
216288
const userDoc = mock(UserDoc);
217289
this.onlineStatusService = TestBed.inject(OnlineStatusService) as TestOnlineStatusService;
218290
when(mockedDraftGenerationService.getLastCompletedBuild(anything())).thenReturn(
@@ -232,7 +304,7 @@ class TestEnvironment {
232304
when(mockedNoticeService.show(anything())).thenResolve();
233305
when(mockedDialogService.confirm(anything(), anything(), anything())).thenResolve(true);
234306
when(mockedServalAdministration.onlineRetrievePreTranslationStatus(anything())).thenResolve();
235-
this.setupProject(args.config);
307+
this.setupProject(args.project);
236308
this.fixture = TestBed.createComponent(DraftUsfmFormatComponent);
237309
this.component = this.fixture.componentInstance;
238310
const loader = TestbedHarnessEnvironment.loader(this.fixture);
@@ -257,7 +329,7 @@ class TestEnvironment {
257329
return this.fixture.nativeElement.querySelector('.quote-format-warning');
258330
}
259331

260-
setupProject(config?: DraftUsfmConfig): void {
332+
setupProject(project?: Partial<SFProjectProfile>): void {
261333
const texts: TextInfo[] = [
262334
{
263335
bookNum: 1,
@@ -283,7 +355,7 @@ class TestEnvironment {
283355
];
284356
const projectDoc = {
285357
id: this.projectId,
286-
data: createTestProjectProfile({ translateConfig: { draftConfig: { usfmConfig: config } }, texts })
358+
data: createTestProjectProfile({ translateConfig: project?.translateConfig, texts: project?.texts ?? texts })
287359
} as SFProjectProfileDoc;
288360
when(mockedActivatedProjectService.projectId).thenReturn(this.projectId);
289361
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
@@ -10,6 +10,7 @@ import { ActivatedRoute } from '@angular/router';
1010
import { TranslocoModule } from '@ngneat/transloco';
1111
import { Canon } from '@sillsdev/scripture';
1212
import { Delta } from 'quill';
13+
import { SFProjectProfile } from 'realtime-server/lib/esm/scriptureforge/models/sf-project';
1314
import { TextInfo } from 'realtime-server/lib/esm/scriptureforge/models/text-info';
1415
import {
1516
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
}

0 commit comments

Comments
 (0)