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 @@ -27,7 +27,7 @@ describe('SFProjectService', () => {
const project = {
translateConfig: { draftConfig: { draftedScriptureRange: 'GEN;EXO;LEV', currentScriptureRange: 'MAT;MRK' } }
} as SFProjectProfile;
const actual = env.service.hasDraft(project, 2);
const actual = env.service.hasDraft(project, 2, 1);
expect(actual).toBe(true);
}));

Expand All @@ -36,7 +36,7 @@ describe('SFProjectService', () => {
const project = {
translateConfig: { draftConfig: { draftedScriptureRange: 'MAT;MRK', currentScriptureRange: 'GEN;EXO;LEV' } }
} as SFProjectProfile;
const actual = env.service.hasDraft(project, 2, true);
const actual = env.service.hasDraft(project, 2, undefined, true);
expect(actual).toBe(true);
}));

Expand All @@ -54,7 +54,7 @@ describe('SFProjectService', () => {
const project = {
translateConfig: { draftConfig: { currentScriptureRange: 'GEN;EXO;LEV' } }
} as SFProjectProfile;
const actual = env.service.hasDraft(project, undefined, true);
const actual = env.service.hasDraft(project, undefined, undefined, true);
expect(actual).toBe(true);
}));

Expand All @@ -67,12 +67,30 @@ describe('SFProjectService', () => {
expect(actual).toBe(false);
}));

it('should return true if the book and chapter are in the drafted scripture range', fakeAsync(() => {
const env = new TestEnvironment();
const project = {
translateConfig: { draftConfig: { draftedScriptureRange: 'GEN;EXO1-4' } }
} as SFProjectProfile;
const actual = env.service.hasDraft(project, 2, 3);
expect(actual).toBe(true);
}));

it('should return false if the book is but the chapter is not in the drafted scripture range', fakeAsync(() => {
const env = new TestEnvironment();
const project = {
translateConfig: { draftConfig: { draftedScriptureRange: 'GEN;EXO2-4' } }
} as SFProjectProfile;
const actual = env.service.hasDraft(project, 2, 1);
expect(actual).toBe(false);
}));

it('should return false if the book is not in the current scripture range when current build is true', fakeAsync(() => {
const env = new TestEnvironment();
const project = {
translateConfig: { draftConfig: { draftedScriptureRange: 'GEN;EXO;LEV', currentScriptureRange: 'MAT;MRK' } }
} as SFProjectProfile;
const actual = env.service.hasDraft(project, 2, true);
const actual = env.service.hasDraft(project, 2, undefined, true);
expect(actual).toBe(false);
}));

Expand All @@ -90,7 +108,7 @@ describe('SFProjectService', () => {
const project = {
translateConfig: { draftConfig: { draftedScriptureRange: 'GEN;EXO;LEV' } }
} as SFProjectProfile;
const actual = env.service.hasDraft(project, undefined, true);
const actual = env.service.hasDraft(project, undefined, undefined, true);
expect(actual).toBe(false);
}));
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DestroyRef, Injectable } from '@angular/core';
import { VerseRef } from '@sillsdev/scripture';
import { Canon, VerseRef } from '@sillsdev/scripture';
import { Operation } from 'realtime-server/lib/esm/common/models/project-rights';
import { obj } from 'realtime-server/lib/esm/common/utils/obj-path';
import { AudioTiming } from 'realtime-server/lib/esm/scriptureforge/models/audio-timing';
Expand All @@ -25,7 +25,7 @@ import { RealtimeService } from 'xforge-common/realtime.service';
import { RetryingRequest, RetryingRequestService } from 'xforge-common/retrying-request.service';
import { EventMetric } from '../event-metrics/event-metric';
import { BookProgress } from '../shared/progress-service/progress.service';
import { booksFromScriptureRange } from '../shared/utils';
import { expandNumbers } from '../shared/utils';
import { BiblicalTermDoc } from './models/biblical-term-doc';
import { InviteeStatus } from './models/invitee-status';
import { NoteThreadDoc } from './models/note-thread-doc';
Expand Down Expand Up @@ -57,27 +57,50 @@ export class SFProjectService extends ProjectService<SFProject, SFProjectDoc> {
}

/**
* Determines if there is a draft in the project for the specified scripture range or book number.
* Determines if there is a draft in the project, optionally for the specified book and chapter number.
*
* @param project The project.
* @param scriptureRange The scripture range or book number.
* @param bookNum The book number.
* @param chapterNum The chapter number.
* @param currentBuild If true, only return true if the current build on serval contains the scripture range.
* @returns true if the project contains a draft for the specified scripture range or book number.
*/
hasDraft(
project: SFProjectProfile | undefined,
bookNum: number | undefined = undefined,
chapterNum: number | undefined = undefined,
currentBuild: boolean = false
): boolean {
const books: number[] = booksFromScriptureRange(
currentBuild
const scriptureRange: string | undefined = (
(currentBuild
? project?.translateConfig.draftConfig.currentScriptureRange
: project?.translateConfig.draftConfig.draftedScriptureRange
);
if (bookNum == null) {
return books.length > 0;
} else {
return books.includes(bookNum);
: project?.translateConfig.draftConfig.draftedScriptureRange) ?? ''
).trim();
if (scriptureRange === '') return false;

// If no book number is specified, we are checking for the presence of any draft.
if (bookNum == null) return true;

const bookId: string = Canon.bookNumberToId(bookNum);
const scriptureRanges: string[] = scriptureRange.split(';').filter(book => book.startsWith(bookId));

// If the book is not present, there is no draft
if (scriptureRanges.length === 0) return false;

// If no chapter number is specified, we are checking for the draft for the specified book number
if (chapterNum == null) return true;

// Parse each of the occurrences of the book for the chapter number
for (const range of scriptureRanges) {
// If the book is present without chapters, then the chapter is present
if (range.length === bookId.length) return true;

// Expand the chapter range, and see if the specified chapter number is present
if (expandNumbers(range.slice(bookId.length)).includes(chapterNum)) return true;
}

// The chapter was not present in the book
return false;
}

async onlineCreate(settings: SFProjectCreateSettings): Promise<string> {
Expand Down
19 changes: 18 additions & 1 deletion src/SIL.XForge.Scripture/ClientApp/src/app/shared/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { SelectableProject } from '../core/models/selectable-project';
import {
booksFromScriptureRange,
compareProjectsForSorting,
expandNumbers,
getBookFileNameDigits,
getUnsupportedTags,
isBadDelta,
Expand Down Expand Up @@ -106,11 +107,27 @@ describe('shared utils', () => {

it('should return numbers for valid scripture book values', () => {
expect(booksFromScriptureRange('GEN')).toEqual([1]);
expect(booksFromScriptureRange('GEN;EXO')).toEqual([1, 2]);
expect(booksFromScriptureRange('GEN10,11,16-19;EXO')).toEqual([1, 2]);
expect(booksFromScriptureRange('GEN;NOT_A_BOOK;EXO')).toEqual([1, 2]);
});
});

describe('expandNumbers', () => {
it('should handle valid number ranges', () => {
expect(expandNumbers('1')).toEqual([1]);
expect(expandNumbers('1,2,3')).toEqual([1, 2, 3]);
expect(expandNumbers('1-3')).toEqual([1, 2, 3]);
expect(expandNumbers('4,1-3,5,invalid,6,0,7-9')).toEqual([4, 1, 2, 3, 5, 6, 0, 7, 8, 9]);
});
it('should return an empty array for invalid number ranges', () => {
expect(expandNumbers('')).toEqual([]);
expect(expandNumbers('3-1')).toEqual([]);
expect(expandNumbers('invalid-1')).toEqual([]);
expect(expandNumbers('1-invalid')).toEqual([]);
expect(expandNumbers('invalid-invalid')).toEqual([]);
});
});

describe('Xml Utils', () => {
it('should convert plain text to xml', () => {
expect(XmlUtils.encodeForXml('')).toEqual('');
Expand Down
22 changes: 21 additions & 1 deletion src/SIL.XForge.Scripture/ClientApp/src/app/shared/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,10 +173,30 @@ export function booksFromScriptureRange(scriptureRange: string | undefined): num
if (scriptureRange == null) return [];
return scriptureRange
.split(';')
.map(book => Canon.bookIdToNumber(book))
.map(book => Canon.bookIdToNumber(book.slice(0, 3)))
.filter(bookId => bookId > 0);
}

/**
* Expands a number range separated by hyphens and commas into a list of numbers.
*
* @param numberRange The number range, e.g. 1,2,5-8,10
* @returns The number range as a list of numbers, e.g. [1,2,5,6,7,8,10]
*/
export function expandNumbers(numberRange: string): number[] {
return numberRange
.split(',')
.filter(Boolean) // Remove empty strings
.flatMap(part => {
if (part.includes('-')) {
const [start, end] = part.split('-').map(Number);
return Array.from({ length: end - start + 1 }, (_, i) => start + i);
}
return [Number(part)];
})
.filter(n => !Number.isNaN(n));
}

export class XmlUtils {
/** Encode text to be valid xml text node. Escape reserved xml characters such as & and < >. */
static encodeForXml(text: string): string {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ describe('DraftUsfmFormatComponent', () => {
]
}
});
when(mockedProjectService.hasDraft(anything(), anything(), anything())).thenReturn(false);
when(mockedProjectService.hasDraft(anything(), anything(), anything(), anything())).thenReturn(false);
tick(EDITOR_READY_TIMEOUT);
env.fixture.detectChanges();
tick(EDITOR_READY_TIMEOUT);
Expand All @@ -169,7 +169,7 @@ describe('DraftUsfmFormatComponent', () => {
]
}
});
when(mockedProjectService.hasDraft(anything(), anything(), anything())).thenReturn(true);
when(mockedProjectService.hasDraft(anything(), anything(), anything(), anything())).thenReturn(true);
tick(EDITOR_READY_TIMEOUT);
env.fixture.detectChanges();
tick(EDITOR_READY_TIMEOUT);
Expand Down Expand Up @@ -369,7 +369,7 @@ class TestEnvironment {
when(mockedNoticeService.show(anything())).thenResolve();
when(mockedDialogService.confirm(anything(), anything(), anything())).thenResolve(true);
when(mockedServalAdministration.onlineRetrievePreTranslationStatus(anything())).thenResolve();
when(mockedProjectService.hasDraft(anything(), anything(), anything())).thenReturn(true);
when(mockedProjectService.hasDraft(anything(), anything(), anything(), anything())).thenReturn(true);
this.setupProject(args.project);
this.fixture = TestBed.createComponent(DraftUsfmFormatComponent);
this.component = this.fixture.componentInstance;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ export class DraftUsfmFormatComponent extends DataLoadingComponent implements Af
this.setUsfmConfig(projectDoc.data.translateConfig.draftConfig.usfmConfig);
const texts: TextInfo[] = projectDoc.data.texts;
this.booksWithDrafts = texts
.filter(t => this.projectService.hasDraft(projectDoc.data, t.bookNum, true))
.filter(t => this.projectService.hasDraft(projectDoc.data, t.bookNum, undefined, true))
.map(t => t.bookNum);

if (this.booksWithDrafts.length === 0) return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ describe('EditorDraftComponent', () => {
of({ state: BuildStates.Completed } as BuildDto)
);
when(mockDraftHandlingService.opsHaveContent(anything())).thenReturn(true);
when(mockSFProjectService.hasDraft(anything(), anything(), anything())).thenReturn(true);
when(mockSFProjectService.hasDraft(anything(), anything(), anything(), anything())).thenReturn(true);

fixture = TestBed.createComponent(EditorDraftComponent);
component = fixture.componentInstance;
Expand Down Expand Up @@ -645,7 +645,7 @@ describe('EditorDraftComponent', () => {
when(mockDraftGenerationService.draftExists(anything(), anything(), anything())).thenReturn(of(false));
when(mockActivatedProjectService.projectDoc$).thenReturn(of(testProjectDoc));
when(mockActivatedProjectService.changes$).thenReturn(of(testProjectDoc));
when(mockSFProjectService.hasDraft(anything(), anything(), anything())).thenReturn(false);
when(mockSFProjectService.hasDraft(anything(), anything(), anything(), anything())).thenReturn(false);

when(mockDraftGenerationService.getLastPreTranslationBuild(anything())).thenReturn(
of({ state: BuildStates.Completed } as BuildDto)
Expand All @@ -655,7 +655,7 @@ describe('EditorDraftComponent', () => {
tick();
expect(component.isSelectedDraftLatest).toBe(true);
expect(component.canConfigureFormatting).toBe(false);
verify(mockSFProjectService.hasDraft(anything(), anything(), anything())).atLeast(1);
verify(mockSFProjectService.hasDraft(anything(), anything(), anything(), anything())).atLeast(1);
flush();
}));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ export class EditorDraftComponent implements AfterViewInit, OnChanges {
}

get doesLatestCompletedHaveDraft(): boolean {
return this.projectService.hasDraft(this.targetProject, this.bookNum, true);
return this.projectService.hasDraft(this.targetProject, this.bookNum, this.chapter, true);
}

get hasDraftToApply(): boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@
{ provide: PermissionsService, useMock: mockedPermissionsService },
{ provide: LynxWorkspaceService, useMock: mockedLynxWorkspaceService },
{ provide: FeatureFlagService, useMock: mockedFeatureFlagService },
provideNoopAnimations()

Check warning on line 226 in src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor.component.spec.ts

View workflow job for this annotation

GitHub Actions / Lint and Prettier (22.13.0, 11.11.0)

`provideNoopAnimations` is deprecated. 20.2 Use `animate.enter` or `animate.leave` instead. Intent to remove in v23
]
}));

Expand Down Expand Up @@ -4162,7 +4162,7 @@
Object.defineProperty(env.component, 'showSource', { get: () => true });
});
when(mockedPermissionsService.canAccessDrafts(anything(), anything())).thenReturn(true);
when(mockedSFProjectService.hasDraft(anything(), anything())).thenReturn(true);
when(mockedSFProjectService.hasDraft(anything(), anything(), anything())).thenReturn(true);
env.wait();
env.routeWithParams({ projectId: 'project01', bookId: 'LUK', chapter: '1' });
env.wait();
Expand All @@ -4181,7 +4181,7 @@
Object.defineProperty(env.component, 'showSource', { get: () => false });
});
when(mockedPermissionsService.canAccessDrafts(anything(), anything())).thenReturn(true);
when(mockedSFProjectService.hasDraft(anything(), anything())).thenReturn(true);
when(mockedSFProjectService.hasDraft(anything(), anything(), anything())).thenReturn(true);
env.wait();
env.routeWithParams({ projectId: 'project01', bookId: 'LUK', chapter: '1' });
env.wait();
Expand Down Expand Up @@ -4220,15 +4220,15 @@
Object.defineProperty(env.component, 'showSource', { get: () => true });
});
when(mockedPermissionsService.canAccessDrafts(anything(), anything())).thenReturn(true);
when(mockedSFProjectService.hasDraft(anything(), anything())).thenReturn(true);
when(mockedSFProjectService.hasDraft(anything(), anything(), anything())).thenReturn(true);
env.routeWithParams({ projectId: 'project01', bookId: 'LUK', chapter: '1' });
env.wait();

const sourceTabGroup = env.component.tabState.getTabGroup('source');
expect(sourceTabGroup?.tabs[1].type).toEqual('draft');
expect(env.component.chapter).toBe(1);

when(mockedSFProjectService.hasDraft(anything(), anything())).thenReturn(false);
when(mockedSFProjectService.hasDraft(anything(), anything(), anything())).thenReturn(false);
env.routeWithParams({ projectId: 'project01', bookId: 'MAT', chapter: '2' });
env.wait();

Expand Down Expand Up @@ -4258,15 +4258,15 @@
Object.defineProperty(env.component, 'showSource', { get: () => false });
});
when(mockedPermissionsService.canAccessDrafts(anything(), anything())).thenReturn(true);
when(mockedSFProjectService.hasDraft(anything(), anything())).thenReturn(true);
when(mockedSFProjectService.hasDraft(anything(), anything(), anything())).thenReturn(true);
env.routeWithParams({ projectId: 'project01', bookId: 'LUK', chapter: '1' });
env.wait();

const targetTabGroup = env.component.tabState.getTabGroup('target');
expect(targetTabGroup?.tabs[1].type).toEqual('draft');
expect(env.component.chapter).toBe(1);

when(mockedSFProjectService.hasDraft(anything(), anything())).thenReturn(false);
when(mockedSFProjectService.hasDraft(anything(), anything(), anything())).thenReturn(false);
env.routeWithParams({ projectId: 'project01', bookId: 'MAT', chapter: '2' });
env.wait();

Expand Down Expand Up @@ -4309,7 +4309,7 @@
it('should not select the draft tab if url query param is not set', fakeAsync(() => {
const env = new TestEnvironment();
when(mockedActivatedRoute.snapshot).thenReturn({ queryParams: {} } as any);
when(mockedSFProjectService.hasDraft(anything(), anything())).thenReturn(true);
when(mockedSFProjectService.hasDraft(anything(), anything(), anything())).thenReturn(true);
when(mockedPermissionsService.canAccessDrafts(anything(), anything())).thenReturn(true);
env.wait();
env.routeWithParams({ projectId: 'project01', bookId: 'LUK', chapter: '1' });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1535,7 +1535,7 @@ export class EditorComponent extends DataLoadingComponent implements OnDestroy,

private async updateDraftTabVisibility(): Promise<void> {
const chapter: Chapter | undefined = this.text?.chapters.find(c => c.number === this.chapter);
const hasDraft: boolean = this.projectService.hasDraft(this.projectDoc?.data, this.bookNum);
const hasDraft: boolean = this.projectService.hasDraft(this.projectDoc?.data, this.bookNum, this.chapter);
const draftApplied: boolean = chapter?.draftApplied ?? false;
const existingDraftTab: { groupId: EditorTabGroupType; index: number } | undefined =
this.tabState.getFirstTabOfTypeIndex('draft');
Expand Down
Loading
Loading