Skip to content

Commit

Permalink
SF-3103 Warn user when project does not contain all draft chapters (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
RaymondLuong3 authored Jan 17, 2025
1 parent f5423e5 commit 762d684
Show file tree
Hide file tree
Showing 8 changed files with 122 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export enum CustomValidatorState {
InvalidProject,
BookNotFound,
NoWritePermissions,
MissingChapters,
None
}

Expand Down Expand Up @@ -87,6 +88,8 @@ export class SFValidators {
return { bookNotFound: true };
case CustomValidatorState.NoWritePermissions:
return { noWritePermissions: true };
case CustomValidatorState.MissingChapters:
return { missingChapters: true };
default:
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ <h2 mat-dialog-title>{{ t("select_alternate_project") }}</h2>
<div class="target-project-content">
@if (targetChapters$ | async; as chapters) {
<app-notice icon="warning" type="warning"
>{{ t("project_has_text_in_chapters", { bookName, numChapters: chapters, projectName: project.name }) }}
>{{
i18n.getPluralRule(chapters) !== "one"
? t("project_has_text_in_chapters", { bookName, numChapters: chapters, projectName: project.name })
: t("project_has_text_in_one_chapter", { bookName, projectName: project.name })
}}
</app-notice>
} @else {
<app-notice [icon]="'verified'">{{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ describe('DraftApplyDialogComponent', () => {
{ provide: I18nService, useMock: mockedI18nService },
{ provide: OnlineStatusService, useClass: TestOnlineStatusService },
{ provide: MatDialogRef, useMock: mockedDialogRef },
{ provide: MAT_DIALOG_DATA, useValue: { bookNum: 1 } }
{ provide: MAT_DIALOG_DATA, useValue: { bookNum: 1, chapters: [1, 2] } }
]
}));

Expand Down Expand Up @@ -147,6 +147,52 @@ describe('DraftApplyDialogComponent', () => {
expect(env.component['getCustomErrorState']()).toBe(CustomValidatorState.InvalidProject);
}));

it('notifies user if book has missing chapters', fakeAsync(() => {
const projectDoc = {
id: 'project03',
data: createTestProjectProfile({
paratextId: 'paratextId3',
userRoles: { user01: SFProjectRole.ParatextAdministrator },
texts: [
{
bookNum: 1,
chapters: [{ number: 1, permissions: { user01: TextInfoPermission.Write }, lastVerse: 31 }],
permissions: { user01: TextInfoPermission.Write }
}
]
})
} as SFProjectProfileDoc;
env = new TestEnvironment({ projectDoc });
env.selectParatextProject('paratextId3');
expect(env.component['targetProjectId']).toBe('project03');
tick();
env.fixture.detectChanges();
expect(env.component['getCustomErrorState']()).toBe(CustomValidatorState.MissingChapters);
}));

it('notifies user if book is empty', fakeAsync(() => {
const projectDoc = {
id: 'project03',
data: createTestProjectProfile({
paratextId: 'paratextId3',
userRoles: { user01: SFProjectRole.ParatextAdministrator },
texts: [
{
bookNum: 1,
chapters: [{ number: 1, permissions: { user01: TextInfoPermission.Write }, lastVerse: 0 }],
permissions: { user01: TextInfoPermission.Write }
}
]
})
} as SFProjectProfileDoc;
env = new TestEnvironment({ projectDoc });
env.selectParatextProject('paratextId3');
expect(env.component['targetProjectId']).toBe('project03');
tick();
env.fixture.detectChanges();
expect(env.component['getCustomErrorState']()).toBe(CustomValidatorState.MissingChapters);
}));

it('updates the target project info when updating the project in the selector', fakeAsync(() => {
env.selectParatextProject('paratextId1');
expect(env.targetProjectContent.textContent).toContain('Test project 1');
Expand Down Expand Up @@ -176,10 +222,10 @@ class TestEnvironment {

onlineStatusService = TestBed.inject(OnlineStatusService) as TestOnlineStatusService;

constructor() {
constructor(args: { projectDoc?: SFProjectProfileDoc } = {}) {
when(mockedUserService.currentUserId).thenReturn('user01');
when(mockedI18nService.localizeBook(anything())).thenReturn('Genesis');
this.setupProject();
this.setupProject(args.projectDoc);
this.fixture = TestBed.createComponent(DraftApplyDialogComponent);
this.loader = TestbedHarnessEnvironment.loader(this.fixture);
this.component = this.fixture.componentInstance;
Expand Down Expand Up @@ -230,7 +276,7 @@ class TestEnvironment {
this.fixture.detectChanges();
}

private setupProject(): void {
private setupProject(projectDoc?: SFProjectProfileDoc): void {
const projectPermissions = [
{ id: 'project01', permission: TextInfoPermission.Write },
{ id: 'project02', permission: TextInfoPermission.Read },
Expand All @@ -248,7 +294,10 @@ class TestEnvironment {
texts: [
{
bookNum: 1,
chapters: [{ number: 1, permissions: { user01: permission } }],
chapters: [
{ number: 1, permissions: { user01: permission }, lastVerse: 31 },
{ number: 2, permissions: { user01: permission }, lastVerse: 25 }
],
permissions: { user01: permission }
}
]
Expand All @@ -258,6 +307,9 @@ class TestEnvironment {
} as SFProjectProfileDoc;
mockProjectDocs.push(mockedProject);
}
if (projectDoc != null) {
mockProjectDocs.push(projectDoc);
}
when(mockedUserProjectsService.projectDocs$).thenReturn(of(mockProjectDocs));
const mockedTextDoc = {
getNonEmptyVerses: (): string[] => ['verse_1_1', 'verse_1_2', 'verse_1_3']
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ export interface DraftApplyDialogResult {
projectId: string;
}

export interface DraftApplyDialogConfig {
bookNum: number;
chapters: number[];
}

@Component({
selector: 'app-draft-apply-dialog',
standalone: true,
Expand All @@ -48,6 +53,7 @@ export class DraftApplyDialogComponent implements OnInit {
targetChapters$: BehaviorSubject<number> = new BehaviorSubject<number>(0);
canEditProject: boolean = true;
targetBookExists: boolean = true;
projectHasMissingChapters: boolean = false;
addToProjectClicked: boolean = false;
/** An observable that emits the target project profile if the user has permission to write to the book. */
targetProject$: BehaviorSubject<SFProjectProfile | undefined> = new BehaviorSubject<SFProjectProfile | undefined>(
Expand All @@ -56,15 +62,16 @@ export class DraftApplyDialogComponent implements OnInit {
invalidMessageMapper: { [key: string]: string } = {
invalidProject: translate('draft_apply_dialog.please_select_valid_project'),
bookNotFound: translate('draft_apply_dialog.book_does_not_exist', { bookName: this.bookName }),
noWritePermissions: translate('draft_apply_dialog.no_write_permissions')
noWritePermissions: translate('draft_apply_dialog.no_write_permissions'),
missingChapters: translate('draft_apply_dialog.project_has_chapters_missing', { bookName: this.bookName })
};

// the project id to add the draft to
private targetProjectId?: string;
private paratextIdToProjectId: Map<string, string> = new Map<string, string>();

constructor(
@Inject(MAT_DIALOG_DATA) private data: { bookNum: number },
@Inject(MAT_DIALOG_DATA) private data: DraftApplyDialogConfig,
@Inject(MatDialogRef) private dialogRef: MatDialogRef<DraftApplyDialogComponent, DraftApplyDialogResult>,
private readonly userProjectsService: SFUserProjectsService,
private readonly projectService: SFProjectService,
Expand Down Expand Up @@ -154,8 +161,13 @@ export class DraftApplyDialogComponent implements OnInit {
this.textDocService.userHasGeneralEditRight(project) &&
targetBook?.permissions[this.userService.currentUserId] === TextInfoPermission.Write;

// also check if this is an empty book
const bookIsEmpty: boolean = targetBook?.chapters.length === 1 && targetBook?.chapters[0].lastVerse < 1;
const targetBookChapters: number[] = targetBook?.chapters.map(c => c.number) ?? [];
this.projectHasMissingChapters =
bookIsEmpty || this.data.chapters.filter(c => !targetBookChapters.includes(c)).length > 0;
// emit the project profile document
if (this.canEditProject) {
if (this.canEditProject && !this.projectHasMissingChapters) {
this.targetProject$.next(project);
} else {
this.targetProject$.next(undefined);
Expand Down Expand Up @@ -194,6 +206,9 @@ export class DraftApplyDialogComponent implements OnInit {
if (!this.canEditProject) {
return CustomErrorState.NoWritePermissions;
}
if (this.projectHasMissingChapters) {
return CustomErrorState.MissingChapters;
}
return CustomErrorState.None;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ import { UICommonModule } from 'xforge-common/ui-common.module';
import { UserService } from 'xforge-common/user.service';
import { filterNullish } from 'xforge-common/util/rxjs-util';
import { TextDocId } from '../../../core/models/text-doc';
import { DraftApplyDialogComponent, DraftApplyDialogResult } from '../draft-apply-dialog/draft-apply-dialog.component';
import {
DraftApplyDialogComponent,
DraftApplyDialogConfig as DraftApplyDialogData,
DraftApplyDialogResult
} from '../draft-apply-dialog/draft-apply-dialog.component';
import {
DraftApplyProgress,
DraftApplyProgressDialogComponent
Expand Down Expand Up @@ -104,9 +108,13 @@ export class DraftPreviewBooksComponent {
}

async chooseAlternateProjectToAddDraft(bookWithDraft: BookWithDraft): Promise<void> {
const dialogData: DraftApplyDialogData = {
bookNum: bookWithDraft.bookNumber,
chapters: bookWithDraft.chaptersWithDrafts
};
const dialogRef: MatDialogRef<DraftApplyDialogComponent, DraftApplyDialogResult> = this.dialogService.openMatDialog(
DraftApplyDialogComponent,
{ data: { bookNum: bookWithDraft.bookNumber }, width: '600px' }
{ data: dialogData, width: '600px' }
);
const result: DraftApplyDialogResult | undefined = await firstValueFrom(dialogRef.afterClosed());
if (result == null || result.projectId == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,9 @@
"looking_for_unlisted_project": "Looking for a project that is not listed? Connect it on {{ underlineStart }}the projects page{{ underlineEnd }} first.",
"no_write_permissions": "You do not have permission to write to this book on this project. Contact the project's administrator to get permission.",
"please_select_valid_project": "Please select a valid project",
"project_has_chapters_missing": "{{ bookName }} in this project has chapters missing. Please add the missing chapters in Paratext",
"project_has_text_in_chapters": "{{ bookName }} in {{ projectName }} has text in {{ numChapters }} chapters",
"project_has_text_in_one_chapter": "{{ bookName }} in {{ projectName }} has text in 1 chapter",
"select_alternate_project": "Select the project you want to add the draft to"
},
"draft_apply_progress-dialog": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,26 @@ describe('I18nService', () => {
expect(service.getLanguageDisplayName('123')).toBe('123');
});
});

describe('getPluralRule', () => {
it('should return rule for zero, one and other', () => {
const service = getI18nService();
service.setLocale('en');
expect(service.getPluralRule(0)).toEqual('other');
expect(service.getPluralRule(1)).toEqual('one');
expect(service.getPluralRule(2)).toEqual('other');
});

it('supports arabic plural rules', () => {
const service = getI18nService();
service.setLocale('ar');
expect(service.getPluralRule(0)).toEqual('zero');
expect(service.getPluralRule(1)).toEqual('one');
expect(service.getPluralRule(2)).toEqual('two');
expect(service.getPluralRule(6)).toEqual('few');
expect(service.getPluralRule(18)).toEqual('many');
});
});
});

function getI18nService(): I18nService {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,13 @@ export class I18nService {
.find(e => e.type === 'timeZoneName').value;
}

/** Takes a number and returns a rule representing the plural-related rule for the current locale.
* Possible values include 'zero', 'one', 'two', 'few', 'many', and 'other'.
*/
getPluralRule(number: number): Intl.LDMLPluralRule {
return new Intl.PluralRules(this.locale.canonicalTag).select(number);
}

private getTranslation(key: I18nKey): string {
return (
this.transloco.getTranslation(this.transloco.getActiveLang())[key] ??
Expand Down

0 comments on commit 762d684

Please sign in to comment.