Skip to content

Commit 3f4ef6a

Browse files
committed
SF-3772 Prevent duplicate onboarding requests
1 parent 242bb96 commit 3f4ef6a

5 files changed

Lines changed: 46 additions & 23 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
@@ -36,9 +36,9 @@ import { NoticeComponent } from '../../shared/notice/notice.component';
3636
import { projectLabel } from '../../shared/utils';
3737
import { normalizeLanguageCodeToISO639_3 } from '../../translate/draft-generation/draft-utils';
3838
import {
39-
DraftingSignupFormData,
4039
ONBOARDING_REQUEST_RESOLUTION_OPTIONS,
4140
OnboardingRequest,
41+
OnboardingRequestFormData,
4242
OnboardingRequestResolutionKey,
4343
OnboardingRequestResolutionMetadata,
4444
OnboardingRequestService
@@ -258,7 +258,7 @@ export class OnboardingRequestDetailComponent extends DataLoadingComponent imple
258258
return this.request?.resolution != null && this.request.resolution !== 'unresolved';
259259
}
260260

261-
get formData(): DraftingSignupFormData {
261+
get formData(): OnboardingRequestFormData {
262262
return this.request!.submission.formData;
263263
}
264264

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
@@ -154,7 +154,7 @@ export class DraftGenerationComponent extends DataLoadingComponent implements On
154154
private readonly nllbService: NllbLanguageService,
155155
protected readonly i18n: I18nService,
156156
private readonly onlineStatusService: OnlineStatusService,
157-
private readonly draftingSignupService: OnboardingRequestService,
157+
private readonly onboardingRequestService: OnboardingRequestService,
158158
protected readonly noticeService: NoticeService,
159159
protected readonly urlService: ExternalUrlService,
160160
protected readonly draftOptionsService: DraftOptionsService,
@@ -265,7 +265,7 @@ export class DraftGenerationComponent extends DataLoadingComponent implements On
265265
// Check if user has already submitted a signup for this project
266266
if (this.activatedProject.projectId != null) {
267267
try {
268-
this.onboardingRequest = await this.draftingSignupService.getOpenOnboardingRequest(
268+
this.onboardingRequest = await this.onboardingRequestService.getOpenOnboardingRequest(
269269
this.activatedProject.projectId
270270
);
271271
} catch {

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

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { Canon } from '@sillsdev/scripture';
1515
import { User } from 'realtime-server/lib/esm/common/models/user';
1616
import { ActivatedProjectService } from 'xforge-common/activated-project.service';
1717
import { DataLoadingComponent } from 'xforge-common/data-loading-component';
18+
import { DialogService } from 'xforge-common/dialog.service';
1819
import { I18nService } from 'xforge-common/i18n.service';
1920
import { NoticeService } from 'xforge-common/notice.service';
2021
import { UserService } from 'xforge-common/user.service';
@@ -28,15 +29,15 @@ import { DevOnlyComponent } from '../../../shared/dev-only/dev-only.component';
2829
import { JsonViewerComponent } from '../../../shared/json-viewer/json-viewer.component';
2930
import { compareProjectsForSorting, projectLabel } from '../../../shared/utils';
3031
import {
31-
DraftingSignupFormData,
32+
OnboardingRequestFormData,
3233
OnboardingRequestService,
3334
PARTNER_ORGANIZATION_OPTIONS,
3435
PartnerOrganization
3536
} from '../onboarding-request.service';
3637

3738
export const DRAFT_SIGNUP_RESPONSE_DAYS = { min: 1, max: 3 } as const;
3839

39-
type DraftOnboardingFormUiState = 'editing' | 'submitting' | 'submitted';
40+
type OnboardingFormUiState = 'editing' | 'submitting' | 'submitted';
4041

4142
/**
4243
* Component for the in-app draft signup form.
@@ -132,7 +133,7 @@ export class DraftOnboardingFormComponent extends DataLoadingComponent implement
132133

133134
submittedData?: any;
134135

135-
uiState: DraftOnboardingFormUiState = 'editing';
136+
uiState: OnboardingFormUiState = 'editing';
136137

137138
readonly responseDays = DRAFT_SIGNUP_RESPONSE_DAYS;
138139

@@ -141,8 +142,9 @@ export class DraftOnboardingFormComponent extends DataLoadingComponent implement
141142
private readonly activatedProject: ActivatedProjectService,
142143
private readonly userService: UserService,
143144
private readonly paratextService: ParatextService,
144-
private readonly draftingSignupService: OnboardingRequestService,
145+
private readonly onboardingRequestService: OnboardingRequestService,
145146
protected readonly noticeService: NoticeService,
147+
protected readonly dialogService: DialogService,
146148
private readonly destroyRef: DestroyRef,
147149
private readonly cd: ChangeDetectorRef,
148150
private readonly i18n: I18nService
@@ -153,6 +155,8 @@ export class DraftOnboardingFormComponent extends DataLoadingComponent implement
153155
ngOnInit(): void {
154156
this.loadingStarted();
155157

158+
void this.checkAndWarnIfAlreadySubmitted();
159+
156160
// Get the current user and pre-fill the form
157161
this.userService
158162
.getCurrentUser()
@@ -215,18 +219,23 @@ export class DraftOnboardingFormComponent extends DataLoadingComponent implement
215219

216220
async onSubmit(): Promise<void> {
217221
const projectId = this.activatedProject.projectId;
218-
if (projectId == null || this.uiState === 'submitting') {
219-
return;
220-
}
222+
if (projectId == null || this.uiState === 'submitting') return;
223+
224+
if (await this.checkAndWarnIfAlreadySubmitted()) return;
225+
226+
// Handle race condition when submit is double clicked and the second click happens before the form state is updated
227+
// to 'submitting'. The type of this.uiState has to be widened after TS narrowed it to `"editing" | "submitted"`
228+
// due to the check at the beginning of the function.
229+
if ((this.uiState as OnboardingFormUiState) === 'submitting') return;
221230

222231
if (this.signupForm.valid === true) {
223232
this.uiState = 'submitting';
224233
this.cd.markForCheck();
225234

226-
const formData: DraftingSignupFormData = this.signupForm.getRawValue() as DraftingSignupFormData;
235+
const formData = this.signupForm.getRawValue() as OnboardingRequestFormData;
227236

228237
try {
229-
const requestId = await this.draftingSignupService.submitOnboardingRequest(projectId, formData);
238+
const requestId = await this.onboardingRequestService.submitOnboardingRequest(projectId, formData);
230239

231240
// For testing purposes, store and display the submitted data
232241
this.submittedData = { requestId, projectId, formData };
@@ -325,6 +334,26 @@ export class DraftOnboardingFormComponent extends DataLoadingComponent implement
325334
}
326335
}
327336

337+
/**
338+
* If a request has already been submitted:
339+
* - Informs user with a message dialog
340+
* - Redirects to drafting page when user closes dialog
341+
* - Resolves to true
342+
*
343+
* Otherwise, resolves to false
344+
*/
345+
private async checkAndWarnIfAlreadySubmitted(): Promise<boolean> {
346+
if (this.activatedProject.projectId == null) return false;
347+
348+
if ((await this.onboardingRequestService.getOpenOnboardingRequest(this.activatedProject.projectId)) == null) {
349+
return false;
350+
} else {
351+
await this.dialogService.message('draft_sources.request_already_submitted', undefined, true);
352+
this.cancel();
353+
return true;
354+
}
355+
}
356+
328357
private async loadProjectsAndResources(): Promise<void> {
329358
try {
330359
const [projects, resources] = await Promise.all([

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

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,12 @@ import { CommandService } from 'xforge-common/command.service';
33
import { ONBOARDING_REQUESTS_URL } from 'xforge-common/url-constants';
44
import { SFProjectService } from '../../core/sf-project.service';
55

6-
export interface OnboardingRequestComment {
7-
id: string;
8-
userId: string;
9-
text: string;
10-
dateCreated: string;
11-
}
12-
136
/** The available partner organization options for the onboarding form. */
147
export const PARTNER_ORGANIZATION_OPTIONS = ['Bolshoi Group', 'Seed Company', 'none'] as const;
158
/** A valid partner organization selection from the onboarding form. */
169
export type PartnerOrganization = (typeof PARTNER_ORGANIZATION_OPTIONS)[number];
1710

18-
export interface DraftingSignupFormData {
11+
export interface OnboardingRequestFormData {
1912
name: string;
2013
email: string;
2114
organization: string;
@@ -50,7 +43,7 @@ export interface OnboardingRequest {
5043
projectId: string;
5144
userId: string;
5245
timestamp: string;
53-
formData: DraftingSignupFormData;
46+
formData: OnboardingRequestFormData;
5447
};
5548
assigneeId: string;
5649
status: OnboardingRequestStatusOption;
@@ -119,7 +112,7 @@ export class OnboardingRequestService {
119112
}
120113

121114
/** Submits a new onboarding request. */
122-
async submitOnboardingRequest(projectId: string, formData: DraftingSignupFormData): Promise<string> {
115+
async submitOnboardingRequest(projectId: string, formData: OnboardingRequestFormData): Promise<string> {
123116
return (await this.onlineInvoke<string>('submitOnboardingRequest', { projectId, formData }))!;
124117
}
125118

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
@@ -409,6 +409,7 @@
409409
"some_projects_use_back_translation": "Some projects get better results by adding a back translation as another reference.",
410410
"source_language": "the source language",
411411
"source_side_language_codes_differ": "All source and reference projects must be in the same language. Please select different source or reference projects.",
412+
"request_already_submitted": "A request to activate drafting on this project has already been submitted. Please wait for a response from the team.",
412413
"state_connecting": "Connecting",
413414
"state_sync_failed": "There was an error syncing",
414415
"state_sync_successful": "Sync successful",

0 commit comments

Comments
 (0)