Skip to content

Commit 3bb4468

Browse files
SF-3746 Allow navigating to any drafted book if it exists (#3740)
1 parent aadcc7d commit 3bb4468

7 files changed

Lines changed: 234 additions & 81 deletions

File tree

src/SIL.XForge.Scripture/ClientApp/src/app/shared/progress-service/progress.service.ts

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ const verseCounts: Record<string, number> = {
9797
SST: 64,
9898
DNT: 424,
9999
BLT: 42,
100-
'3ES': 944,
100+
'3ES': 434,
101101
EZA: 715,
102102
'5EZ': 88,
103103
'6EZ': 141,
@@ -115,6 +115,118 @@ const verseCounts: Record<string, number> = {
115115
LAO: 20
116116
};
117117

118+
/** The expected number of chapters per book, based primarily on the eng.vrs versification files. */
119+
const chapterCounts: Record<string, number> = {
120+
GEN: 50,
121+
EXO: 40,
122+
LEV: 27,
123+
NUM: 36,
124+
DEU: 34,
125+
JOS: 24,
126+
JDG: 21,
127+
RUT: 4,
128+
'1SA': 31,
129+
'2SA': 24,
130+
'1KI': 22,
131+
'2KI': 25,
132+
'1CH': 29,
133+
'2CH': 36,
134+
EZR: 10,
135+
NEH: 13,
136+
EST: 10,
137+
JOB: 42,
138+
PSA: 150,
139+
PRO: 31,
140+
ECC: 12,
141+
SNG: 8,
142+
ISA: 66,
143+
JER: 52,
144+
LAM: 5,
145+
EZK: 48,
146+
DAN: 12,
147+
HOS: 14,
148+
JOL: 3,
149+
AMO: 9,
150+
OBA: 1,
151+
JON: 4,
152+
MIC: 7,
153+
NAM: 3,
154+
HAB: 3,
155+
ZEP: 3,
156+
HAG: 2,
157+
ZEC: 14,
158+
MAL: 4,
159+
MAT: 28,
160+
MRK: 16,
161+
LUK: 24,
162+
JHN: 21,
163+
ACT: 28,
164+
ROM: 16,
165+
'1CO': 16,
166+
'2CO': 13,
167+
GAL: 6,
168+
EPH: 6,
169+
PHP: 4,
170+
COL: 4,
171+
'1TH': 5,
172+
'2TH': 3,
173+
'1TI': 6,
174+
'2TI': 4,
175+
TIT: 3,
176+
PHM: 1,
177+
HEB: 13,
178+
JAS: 5,
179+
'1PE': 5,
180+
'2PE': 3,
181+
'1JN': 5,
182+
'2JN': 1,
183+
'3JN': 1,
184+
JUD: 1,
185+
REV: 22,
186+
TOB: 14,
187+
JDT: 16,
188+
ESG: 10,
189+
WIS: 19,
190+
SIR: 51,
191+
BAR: 6,
192+
LJE: 1,
193+
S3Y: 1,
194+
SUS: 1,
195+
BEL: 1,
196+
'1MA': 16,
197+
'2MA': 15,
198+
'3MA': 7,
199+
'4MA': 18,
200+
'1ES': 9,
201+
'2ES': 16,
202+
MAN: 1,
203+
PS2: 1,
204+
ODA: 14,
205+
PSS: 18,
206+
JSA: 24,
207+
JDB: 21,
208+
TBS: 14,
209+
SST: 1,
210+
DNT: 12,
211+
BLT: 1,
212+
'3ES': 9,
213+
EZA: 12,
214+
'5ES': 2,
215+
'6ES': 2,
216+
DAG: 14,
217+
PS3: 4,
218+
'2BA': 77,
219+
LBA: 9,
220+
JUB: 34,
221+
ENO: 42,
222+
'1MQ': 36,
223+
'2MQ': 20,
224+
'3MQ': 10,
225+
REP: 6,
226+
'4BA': 5,
227+
LAO: 1
228+
};
229+
118230
export interface BookProgress {
119231
/** The book identifier (e.g. "GEN", "MAT"). */
120232
bookId: string;
@@ -159,6 +271,11 @@ export function estimatedActualBookProgress(bookProgress: BookProgress): number
159271
}
160272
}
161273

274+
/** Returns the expected number of chapters for a given bookId. */
275+
export function expectedBookChapters(bookId: string): number {
276+
return chapterCounts[bookId] ?? 1;
277+
}
278+
162279
@Injectable({ providedIn: 'root' })
163280
export class ProgressService {
164281
constructor(

src/SIL.XForge.Scripture/ClientApp/src/app/shared/text/text.component.spec.ts

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,11 @@ describe('TextComponent', () => {
8383
env.component.onEditorCreated(mockedQuill);
8484
env.waitForEditor();
8585
// Placeholder should be no-content, as there is nothing to show.
86-
expect(env.component.placeholder).toEqual('text.book_does_not_exist');
86+
expect(env.component.placeholder).toEqual('text.error');
8787
// The user goes offline, but 'no-content' is still the placeholder.
8888
env.onlineStatus = false;
8989
env.waitForEditor();
90-
expect(env.component.placeholder).toEqual('text.book_does_not_exist');
90+
expect(env.component.placeholder).toEqual('text.error');
9191
env.onlineStatus = true;
9292
env.waitForEditor();
9393

@@ -118,19 +118,17 @@ describe('TextComponent', () => {
118118
env.runWithDelayedGetText(env.notPresentTextDocId, () => {
119119
env.id = env.notPresentTextDocId;
120120
env.waitForEditor();
121-
expect(env.component.placeholder).toEqual('text.book_does_not_exist');
121+
expect(env.component.placeholder).toEqual('text.this_book_does_not_exist');
122122
});
123123

124124
// The user is offline and goes to a location that the project does not have. The placeholder should indicate
125125
// 'no-content'.
126-
env.id = env.matTextDocId;
127-
env.waitForEditor();
128126
env.onlineStatus = false;
129127
env.waitForEditor();
130128
env.runWithDelayedGetText(env.notPresentTextDocId, () => {
131129
env.id = env.notPresentTextDocId;
132130
env.waitForEditor();
133-
expect(env.component.placeholder).toEqual('text.book_does_not_exist');
131+
expect(env.component.placeholder).toEqual('text.this_book_does_not_exist');
134132
});
135133
env.onlineStatus = true;
136134
env.waitForEditor();
@@ -139,18 +137,16 @@ describe('TextComponent', () => {
139137
// is not present in the source text. Suppose this is so. Then the placeholder should indicate 'no-content'.
140138
env.id = undefined;
141139
env.waitForEditor();
142-
expect(env.component.placeholder).toEqual('text.book_does_not_exist');
140+
expect(env.component.placeholder).toEqual('text.error');
143141

144142
// Suppose we are offline and id was set to undefined by editor.component because the current book is not present in
145143
// the source text. The problem is that the book is not present, not that we are offline. So the placeholder should
146144
// indicate 'no-content', not 'offline'.
147145
env.onlineStatus = true;
148146
env.waitForEditor();
149-
env.id = env.matTextDocId;
150-
env.waitForEditor();
151-
env.id = undefined;
147+
env.id = env.notPresentTextDocId;
152148
env.waitForEditor();
153-
expect(env.component.placeholder).toEqual('text.book_does_not_exist');
149+
expect(env.component.placeholder).toEqual('text.this_book_does_not_exist');
154150

155151
const callback: (env: TestEnvironment) => void = (env: TestEnvironment) => {
156152
env.realtimeService.addSnapshot<User>(UserDoc.COLLECTION, {
@@ -260,7 +256,7 @@ describe('TextComponent', () => {
260256
const env = new TestEnvironment();
261257
env.hostComponent.isTextRightToLeft = true;
262258
env.waitForEditor();
263-
expect(env.component.placeholder).toEqual('text.book_does_not_exist');
259+
expect(env.component.placeholder).toEqual('text.error');
264260
expect(env.component.isRtl).toBe(false);
265261
expect(env.fixture.nativeElement.querySelector('quill-editor[dir="auto"]')).not.toBeNull();
266262
}));
@@ -1775,6 +1771,9 @@ class TestEnvironment {
17751771
when(mockedTranslocoService.translate<string>(anything())).thenCall(
17761772
(translationStringKey: string) => translationStringKey
17771773
);
1774+
when(mockedTranslocoService.translate<string>(anything(), anything())).thenCall(
1775+
(translationStringKey: string, _: any) => translationStringKey
1776+
);
17781777

17791778
this.realtimeService.addSnapshot<SFProjectProfile>(SFProjectProfileDoc.COLLECTION, {
17801779
id: 'project01',

src/SIL.XForge.Scripture/ClientApp/src/app/shared/text/text.component.ts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { QuillEditorComponent } from 'ngx-quill';
1919
import Quill, { Delta, EmitterSource, Range } from 'quill';
2020
import QuillCursors from 'quill-cursors';
2121
import { AuthType, getAuthType } from 'realtime-server/lib/esm/common/models/user';
22+
import { SFProjectProfile } from 'realtime-server/lib/esm/scriptureforge/models/sf-project';
2223
import { SFProjectRole } from 'realtime-server/lib/esm/scriptureforge/models/sf-project-role';
2324
import { TextAnchor } from 'realtime-server/lib/esm/scriptureforge/models/text-anchor';
2425
import { StringMap } from 'rich-text';
@@ -285,6 +286,7 @@ export class TextComponent implements AfterViewInit, OnDestroy {
285286
private _selectionBoundsTop: number = 0;
286287
private highlightMarkerHeight: number = 0;
287288
private _placeholder?: string;
289+
private project?: SFProjectProfile;
288290
private currentUserDoc?: UserDoc;
289291
private readonly cursorColorStorageKey = 'cursor_color';
290292
private isDestroyed: boolean = false;
@@ -347,7 +349,14 @@ export class TextComponent implements AfterViewInit, OnDestroy {
347349
case 'no-content':
348350
// There isn't any content to show, even if we were online. Setting the placeholder Input customizes this
349351
// no-content message.
350-
return this._placeholder ?? this.transloco.translate('text.book_does_not_exist');
352+
if (this._placeholder != null) return this._placeholder;
353+
if (this.id == null || this.project == null) return this.transloco.translate('text.error');
354+
355+
const bookId = Canon.bookNumberToId(this.id.bookNum);
356+
const bookName = this.transloco.translate(`canon.book_names.${bookId}`);
357+
return this.project.texts.some(t => t.bookNum === this.id!.bookNum)
358+
? this.transloco.translate('text.chapter_does_not_exist', { projectName: this.project.name })
359+
: this.transloco.translate('text.this_book_does_not_exist', { bookName, projectName: this.project.name });
351360
case 'offline-or-loading':
352361
if (this.onlineStatusService.isOnline) {
353362
return this.transloco.translate('text.loading');
@@ -1075,18 +1084,18 @@ export class TextComponent implements AfterViewInit, OnDestroy {
10751084
async projectHasText(): Promise<boolean> {
10761085
if (this.id == null) throw new Error('Invalid state. id is null.');
10771086
const id: TextDocId = this.id;
1078-
1087+
this.project = undefined;
10791088
if (!this.userProjects?.includes(this.projectId)) {
10801089
this.loadingState = 'permission-denied';
10811090
return false;
10821091
}
1083-
const profile: SFProjectProfileDoc = await this.projectService.getProfile(this.projectId);
1084-
if (profile.data == null) throw new Error('Failed to fetch project profile.');
1085-
if (
1086-
profile.data.texts.some(
1087-
text => text.bookNum === id.bookNum && text.chapters.some(chapter => chapter.number === id.chapterNum)
1088-
)
1089-
) {
1092+
const profileDoc: SFProjectProfileDoc = await this.projectService.getProfile(this.projectId);
1093+
if (profileDoc.data == null) throw new Error('Failed to fetch project profile.');
1094+
this.project = profileDoc.data;
1095+
const chapterExists = this.project.texts.some(
1096+
t => t.bookNum === id.bookNum && t.chapters.some(c => c.number === id.chapterNum)
1097+
);
1098+
if (chapterExists) {
10901099
return true;
10911100
}
10921101
this.loadingState = 'no-content';
@@ -1158,6 +1167,7 @@ export class TextComponent implements AfterViewInit, OnDestroy {
11581167
}
11591168

11601169
if (!(await this.projectHasText())) {
1170+
this.loaded.emit();
11611171
return;
11621172
}
11631173

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

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -426,14 +426,16 @@ export class EditorDraftComponent implements AfterViewInit, OnChanges {
426426

427427
private getTargetOps(): Observable<DeltaOperation[]> {
428428
return from(this.projectService.getText(this.textDocId!)).pipe(
429-
switchMap(textDoc =>
430-
textDoc.changes$.pipe(
431-
startWith(undefined),
432-
throttleTime(2000, asyncScheduler, { leading: true, trailing: true }),
433-
map(() => textDoc.data?.ops),
434-
filterNullish()
435-
)
436-
)
429+
switchMap(textDoc => {
430+
if (textDoc.isLoaded)
431+
return textDoc.changes$.pipe(
432+
startWith(undefined),
433+
throttleTime(2000, asyncScheduler, { leading: true, trailing: true }),
434+
map(() => textDoc.data?.ops),
435+
filterNullish()
436+
);
437+
else return of([] as DeltaOperation[]);
438+
})
437439
);
438440
}
439441
}

0 commit comments

Comments
 (0)