Skip to content

Commit 2b005bf

Browse files
committed
SF-3772 Prevent duplicate onboarding requests
1 parent 9f24d06 commit 2b005bf

5 files changed

Lines changed: 44 additions & 16 deletions

File tree

src/SIL.XForge.Scripture/ClientApp/src/app/serval-administration/onboarding-request-detail/onboarding-request-detail.component.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ import { NoticeComponent } from '../../shared/notice/notice.component';
3434
import { projectLabel } from '../../shared/utils';
3535
import { normalizeLanguageCodeToISO639_3 } from '../../translate/draft-generation/draft-utils';
3636
import {
37-
DraftingSignupFormData,
3837
OnboardingRequest,
38+
OnboardingRequestFormData,
3939
OnboardingRequestResolutionKey,
4040
OnboardingRequestResolutionMetadata,
4141
OnboardingRequestService
@@ -236,7 +236,7 @@ export class OnboardingRequestDetailComponent extends DataLoadingComponent imple
236236

237237
getStatus = this.onboardingRequestService.getStatus;
238238

239-
get formData(): DraftingSignupFormData {
239+
get formData(): OnboardingRequestFormData {
240240
return this.request!.submission.formData;
241241
}
242242

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ export class DraftGenerationComponent extends DataLoadingComponent implements On
174174
protected readonly i18n: I18nService,
175175
private readonly onlineStatusService: OnlineStatusService,
176176
private readonly preTranslationSignupUrlService: PreTranslationSignupUrlService,
177-
private readonly draftingSignupService: OnboardingRequestService,
177+
private readonly onboardingRequestService: OnboardingRequestService,
178178
protected readonly noticeService: NoticeService,
179179
protected readonly urlService: ExternalUrlService,
180180
protected readonly featureFlags: FeatureFlagService,
@@ -290,7 +290,7 @@ export class DraftGenerationComponent extends DataLoadingComponent implements On
290290
// Check if user has already submitted a signup for this project
291291
if (this.activatedProject.projectId != null) {
292292
try {
293-
this.onboardingRequest = await this.draftingSignupService.getOpenOnboardingRequest(
293+
this.onboardingRequest = await this.onboardingRequestService.getOpenOnboardingRequest(
294294
this.activatedProject.projectId
295295
);
296296
} catch {

src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-signup-form/draft-onboarding-form.component.ts

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { User } from 'realtime-server/lib/esm/common/models/user';
1616
import { DevOnlyComponent } from 'src/app/shared/dev-only/dev-only.component';
1717
import { ActivatedProjectService } from 'xforge-common/activated-project.service';
1818
import { DataLoadingComponent } from 'xforge-common/data-loading-component';
19+
import { DialogService } from 'xforge-common/dialog.service';
1920
import { I18nService } from 'xforge-common/i18n.service';
2021
import { NoticeService } from 'xforge-common/notice.service';
2122
import { UserService } from 'xforge-common/user.service';
@@ -27,11 +28,11 @@ import { ProjectSelectComponent } from '../../../project-select/project-select.c
2728
import { BookMultiSelectComponent } from '../../../shared/book-multi-select/book-multi-select.component';
2829
import { JsonViewerComponent } from '../../../shared/json-viewer/json-viewer.component';
2930
import { compareProjectsForSorting, projectLabel } from '../../../shared/utils';
30-
import { DraftingSignupFormData, OnboardingRequestService } from '../onboarding-request.service';
31+
import { OnboardingRequestFormData, OnboardingRequestService } from '../onboarding-request.service';
3132

3233
export const DRAFT_SIGNUP_RESPONSE_DAYS = { min: 1, max: 3 } as const;
3334

34-
type DraftOnboardingFormUiState = 'editing' | 'submitting' | 'submitted';
35+
type OnboardingFormUiState = 'editing' | 'submitting' | 'submitted';
3536

3637
/**
3738
* Component for the in-app draft signup form.
@@ -124,7 +125,7 @@ export class DraftOnboardingFormComponent extends DataLoadingComponent implement
124125

125126
submittedData?: any;
126127

127-
uiState: DraftOnboardingFormUiState = 'editing';
128+
uiState: OnboardingFormUiState = 'editing';
128129

129130
readonly responseDays = DRAFT_SIGNUP_RESPONSE_DAYS;
130131

@@ -133,8 +134,9 @@ export class DraftOnboardingFormComponent extends DataLoadingComponent implement
133134
private readonly activatedProject: ActivatedProjectService,
134135
private readonly userService: UserService,
135136
private readonly paratextService: ParatextService,
136-
private readonly draftingSignupService: OnboardingRequestService,
137+
private readonly onboardingRequestService: OnboardingRequestService,
137138
protected readonly noticeService: NoticeService,
139+
protected readonly dialogService: DialogService,
138140
private readonly destroyRef: DestroyRef,
139141
private readonly cd: ChangeDetectorRef,
140142
private readonly i18n: I18nService
@@ -207,18 +209,23 @@ export class DraftOnboardingFormComponent extends DataLoadingComponent implement
207209

208210
async onSubmit(): Promise<void> {
209211
const projectId = this.activatedProject.projectId;
210-
if (projectId == null || this.uiState === 'submitting') {
211-
return;
212-
}
212+
if (projectId == null || this.uiState === 'submitting') return;
213+
214+
if (await this.checkAndWarnIfAlreadySubmitted()) return;
215+
216+
// Handle race condition when submit is double clicked and the second click happens before the form state is updated
217+
// to 'submitting'. The type of this.uiState has to be widened after TS narrowed it to `"editing" | "submitted"`
218+
// due to the check at the beginning of the function.
219+
if ((this.uiState as OnboardingFormUiState) === 'submitting') return;
213220

214221
if (this.signupForm.valid === true) {
215222
this.uiState = 'submitting';
216223
this.cd.markForCheck();
217224

218-
const formData: DraftingSignupFormData = this.signupForm.getRawValue() as DraftingSignupFormData;
225+
const formData = this.signupForm.getRawValue() as OnboardingRequestFormData;
219226

220227
try {
221-
const requestId = await this.draftingSignupService.submitOnboardingRequest(projectId, formData);
228+
const requestId = await this.onboardingRequestService.submitOnboardingRequest(projectId, formData);
222229

223230
// For testing purposes, store and display the submitted data
224231
this.submittedData = { requestId, projectId, formData };
@@ -313,6 +320,26 @@ export class DraftOnboardingFormComponent extends DataLoadingComponent implement
313320
}
314321
}
315322

323+
/**
324+
* If a request has already been submitted:
325+
* - Informs user with a message dialog
326+
* - Redirects to drafting page when user closes dialog
327+
* - Resolves to true
328+
*
329+
* Otherwise, resolves to false
330+
*/
331+
private async checkAndWarnIfAlreadySubmitted(): Promise<boolean> {
332+
if (this.activatedProject.projectId == null) return false;
333+
334+
if ((await this.onboardingRequestService.getOpenOnboardingRequest(this.activatedProject.projectId)) == null) {
335+
return false;
336+
} else {
337+
await this.dialogService.message('draft_sources.request_already_submitted', undefined, true);
338+
this.cancel();
339+
return true;
340+
}
341+
}
342+
316343
private async loadProjectsAndResources(): Promise<void> {
317344
try {
318345
const [projects, resources] = await Promise.all([

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export interface OnboardingRequestComment {
1010
dateCreated: string;
1111
}
1212

13-
export interface DraftingSignupFormData {
13+
export interface OnboardingRequestFormData {
1414
name: string;
1515
email: string;
1616
organization: string;
@@ -43,7 +43,7 @@ export interface OnboardingRequest {
4343
projectId: string;
4444
userId: string;
4545
timestamp: string;
46-
formData: DraftingSignupFormData;
46+
formData: OnboardingRequestFormData;
4747
};
4848
assigneeId: string;
4949
status: OnboardingRequestStatusOption;
@@ -104,7 +104,7 @@ export class OnboardingRequestService {
104104
}
105105

106106
/** Submits a new signup request. */
107-
async submitOnboardingRequest(projectId: string, formData: DraftingSignupFormData): Promise<string> {
107+
async submitOnboardingRequest(projectId: string, formData: OnboardingRequestFormData): Promise<string> {
108108
return (await this.onlineInvoke<string>('submitOnboardingRequest', { projectId, formData }))!;
109109
}
110110

src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/non_checking_en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,7 @@
403403
"select_project_to_translate": "Select the project to translate.",
404404
"some_projects_use_back_translation": "Some projects get better results by adding a back translation as another reference.",
405405
"source_side_language_codes_differ": "All source and reference projects must be in the same language. Please select different source or reference projects.",
406+
"request_already_submitted": "A request to activate drafting on this project has already been submitted. Please wait for a response from the team.",
406407
"state_connecting": "Connecting",
407408
"state_sync_failed": "There was an error syncing",
408409
"state_sync_successful": "Sync successful",

0 commit comments

Comments
 (0)