Passed
Push — feature/data-request-hooks ( 562d87...7ed4d3 )
by Tristan
06:17
created

applicationHooks.tsx ➔ useFetchAllApplicationData   B

Complexity

Conditions 2

Size

Total Lines 57
Code Lines 47

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 47
c 0
b 0
f 0
dl 0
loc 57
rs 8.7345
cc 2

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
/* eslint-disable camelcase */
2
import { useCallback, useEffect, useState } from "react";
3
import { useSelector } from "react-redux";
4
import { DispatchType } from "../configureStore";
5
import { RootState } from "../store/store";
6
import { getAwardRecipientTypes as fetchAwardRecipientTypes } from "../store/AwardRecipientType/awardRecipientTypeActions";
7
import { getAwardRecipientTypes } from "../store/AwardRecipientType/awardRecipientTypeSelector";
8
import { getAwardRecognitionTypes as fetchAwardRecognitionTypes } from "../store/AwardRecognitionType/awardRecognitionTypeActions";
9
import { getAwardRecognitionTypes } from "../store/AwardRecognitionType/awardRecognitionTypeSelector";
10
import { getEducationTypes as fetchEducationTypes } from "../store/EducationType/educationTypeActions";
11
import { getEducationTypes } from "../store/EducationType/educationTypeSelector";
12
import { getEducationStatuses as fetchEducationStatuses } from "../store/EducationStatus/educationStatusActions";
13
import { getEducationStatuses } from "../store/EducationStatus/educationStatusSelector";
14
import {
15
  AwardRecipientType,
16
  AwardRecognitionType,
17
  EducationType,
18
  EducationStatus,
19
  Job,
20
  Experience as ExperienceType,
21
  Skill,
22
  ExperienceSkill,
23
  ApplicationNormalized,
24
  Criteria,
25
  JobPosterQuestion,
26
  JobApplicationAnswer,
27
  Application,
28
  User,
29
} from "../models/types";
30
import {
31
  getApplicationById,
32
  getApplicationIsUpdating,
33
  getApplicationNormalized,
34
  getJobApplicationAnswers,
35
  getJobApplicationSteps,
36
  getStepsAreUpdating,
37
} from "../store/Application/applicationSelector";
38
import {
39
  fetchApplication,
40
  touchApplicationStep,
41
} from "../store/Application/applicationActions";
42
import {
43
  getCriteriaByJob,
44
  getJob,
45
  getJobIsUpdating,
46
  getJobPosterQuestionsByJob,
47
} from "../store/Job/jobSelector";
48
import { fetchJob } from "../store/Job/jobActions";
49
import {
50
  ApplicationStatusId,
51
  ApplicationStep,
52
  ApplicationStepId,
53
  ProgressBarStatus,
54
} from "../models/lookupConstants";
55
import {
56
  getExperienceByApplicant,
57
  getExperienceByApplication,
58
  getExperienceSkillsByApplicant,
59
  getExperienceSkillsByApplication,
60
  getUpdatingByApplicant,
61
  getUpdatingByApplication,
62
} from "../store/Experience/experienceSelector";
63
import {
64
  fetchExperienceByApplicant,
65
  fetchExperienceByApplication,
66
} from "../store/Experience/experienceActions";
67
import { getSkills, getSkillsUpdating } from "../store/Skill/skillSelector";
68
import { fetchSkills } from "../store/Skill/skillActions";
69
import { getUserById } from "../store/User/userSelector";
70
import { fetchUser } from "../store/User/userActions";
71
72
export function useUser(userId: number | undefined): User | null {
73
  return useSelector((state: RootState) =>
74
    userId ? getUserById(state, { userId }) : null,
75
  );
76
}
77
export function useApplication(
78
  applicationId: number,
79
): ApplicationNormalized | null {
80
  return useSelector((state: RootState) =>
81
    getApplicationNormalized(state, { applicationId }),
82
  );
83
}
84
85
export function useReviewedApplication(
86
  applicationId: number,
87
): Application | null {
88
  return useSelector((state: RootState) =>
89
    getApplicationById(state, { id: applicationId }),
90
  );
91
}
92
93
export function useJob(jobId: number | undefined): Job | null {
94
  return useSelector((state: RootState) =>
95
    jobId ? getJob(state, { jobId }) : null,
96
  );
97
}
98
99
export function useExperienceConstants(): {
100
  awardRecipientTypes: AwardRecipientType[];
101
  awardRecognitionTypes: AwardRecognitionType[];
102
  educationTypes: EducationType[];
103
  educationStatuses: EducationStatus[];
104
} {
105
  const awardRecipientTypes = useSelector(getAwardRecipientTypes);
106
  const awardRecognitionTypes = useSelector(getAwardRecognitionTypes);
107
  const educationTypes = useSelector(getEducationTypes);
108
  const educationStatuses = useSelector(getEducationStatuses);
109
  return {
110
    awardRecipientTypes,
111
    awardRecognitionTypes,
112
    educationTypes,
113
    educationStatuses,
114
  };
115
}
116
117
export function useSkills(): Skill[] {
118
  return useSelector(getSkills);
119
}
120
121
export function useCriteria(jobId: number | undefined): Criteria[] {
122
  return useSelector((state: RootState) =>
123
    jobId ? getCriteriaByJob(state, { jobId }) : [],
124
  );
125
}
126
127
export function useExperiences(
128
  applicationId: number,
129
  application: ApplicationNormalized | null,
130
): ExperienceType[] {
131
  const applicantId = application?.applicant_id ?? 0;
132
133
  // When an Application is still a draft, use Experiences associated with the applicant profile.
134
  // When an Application has been submitted and is no longer a draft, display Experience associated with the Application directly.
135
  const useProfileExperience =
136
    application === null ||
137
    application.application_status_id === ApplicationStatusId.draft;
138
139
  // This selector must be memoized because getExperienceByApplicant/Application uses reselect, and not re-reselect, so it needs to preserve its state.
140
  const experienceSelector = useCallback(
141
    (state: RootState) =>
142
      useProfileExperience
143
        ? getExperienceByApplicant(state, { applicantId })
144
        : getExperienceByApplication(state, { applicationId }),
145
    [applicationId, applicantId, useProfileExperience],
146
  );
147
  const experiencesByType = useSelector(experienceSelector);
148
  const experiences: ExperienceType[] = [
149
    ...experiencesByType.award,
150
    ...experiencesByType.community,
151
    ...experiencesByType.education,
152
    ...experiencesByType.personal,
153
    ...experiencesByType.work,
154
  ];
155
  return experiences;
156
}
157
158
export function useExperienceSkills(
159
  applicationId: number,
160
  application: ApplicationNormalized | null,
161
): ExperienceSkill[] {
162
  // ExperienceSkills don't need to be fetched because they are returned in the Experiences API calls.
163
  const applicantId = application?.applicant_id ?? 0;
164
  const useProfileExperience =
165
    application === null ||
166
    application.application_status_id === ApplicationStatusId.draft;
167
  const expSkillSelector = (state: RootState) =>
168
    useProfileExperience
169
      ? getExperienceSkillsByApplicant(state, { applicantId })
170
      : getExperienceSkillsByApplication(state, { applicationId });
171
  const experienceSkills = useSelector(expSkillSelector);
172
  return experienceSkills;
173
}
174
175
export function useJobPosterQuestions(
176
  jobId: number | undefined,
177
): JobPosterQuestion[] {
178
  return useSelector((state: RootState) =>
179
    jobId ? getJobPosterQuestionsByJob(state, { jobId }) : [],
180
  );
181
}
182
183
export function useJobApplicationAnswers(
184
  applicationId: number,
185
): JobApplicationAnswer[] {
186
  return useSelector((state: RootState) =>
187
    getJobApplicationAnswers(state, { applicationId }),
188
  );
189
}
190
191
export function useApplicationUser(applicationId: number): User | null {
192
  const application = useApplication(applicationId);
193
  const user = application?.applicant?.user ?? null;
194
  return user;
195
}
196
197
/**
198
 * Return all skills from the redux store, and fetch the skills from backend if they are not yet in the store.
199
 * @param dispatch
200
 */
201
export function useFetchSkills(dispatch: DispatchType): Skill[] {
202
  const skills = useSelector(getSkills);
203
  const skillsUpdating = useSelector(getSkillsUpdating);
204
  useEffect(() => {
205
    if (skills.length === 0 && !skillsUpdating) {
206
      dispatch(fetchSkills());
207
    }
208
  }, [skills.length, skillsUpdating, dispatch]);
209
  return skills;
210
}
211
212
/**
213
 * Return all Experience constants from the redux store, and fetch them from backend if they are not yet in the store.
214
 * @param dispatch
215
 */
216
export function useFetchExperienceConstants(
217
  dispatch: DispatchType,
218
): {
219
  awardRecipientTypes: AwardRecipientType[];
220
  awardRecognitionTypes: AwardRecognitionType[];
221
  educationTypes: EducationType[];
222
  educationStatuses: EducationStatus[];
223
} {
224
  const awardRecipientTypes = useSelector(getAwardRecipientTypes);
225
  const awardRecipientTypesLoading = useSelector(
226
    (state: RootState) => state.awardRecipientType.loading,
227
  );
228
  useEffect(() => {
229
    if (awardRecipientTypes.length === 0 && !awardRecipientTypesLoading) {
230
      dispatch(fetchAwardRecipientTypes());
231
    }
232
  }, [awardRecipientTypes, awardRecipientTypesLoading, dispatch]);
233
234
  const awardRecognitionTypes = useSelector(getAwardRecognitionTypes);
235
  const awardRecognitionTypesLoading = useSelector(
236
    (state: RootState) => state.awardRecognitionType.loading,
237
  );
238
  useEffect(() => {
239
    if (awardRecognitionTypes.length === 0 && !awardRecognitionTypesLoading) {
240
      dispatch(fetchAwardRecognitionTypes());
241
    }
242
  }, [awardRecognitionTypes, awardRecognitionTypesLoading, dispatch]);
243
244
  const educationTypes = useSelector(getEducationTypes);
245
  const educationTypesLoading = useSelector(
246
    (state: RootState) => state.educationType.loading,
247
  );
248
  useEffect(() => {
249
    if (educationTypes.length === 0 && !educationTypesLoading) {
250
      dispatch(fetchEducationTypes());
251
    }
252
  }, [educationTypes, educationTypesLoading, dispatch]);
253
254
  const educationStatuses = useSelector(getEducationStatuses);
255
  const educationStatusesLoading = useSelector(
256
    (state: RootState) => state.educationStatus.loading,
257
  );
258
  useEffect(() => {
259
    if (educationStatuses.length === 0 && !educationStatusesLoading) {
260
      dispatch(fetchEducationStatuses());
261
    }
262
  }, [educationStatuses, educationStatusesLoading, dispatch]);
263
264
  return {
265
    awardRecipientTypes,
266
    awardRecognitionTypes,
267
    educationTypes,
268
    educationStatuses,
269
  };
270
}
271
272
export function useJobApplicationSteps(): {
273
  [step in ApplicationStep]: ProgressBarStatus;
274
} {
275
  return useSelector(getJobApplicationSteps);
276
}
277
278
/**
279
 * Dispatches an api request that will record the step as touched, setting it to "complete" or "error",
280
 * and gets the validation status of the application steps.
281
 *
282
 * Returns true if the request is currently in progress, false otherwise.
283
 *
284
 * NOTE: this hook only runs once, when the component is first mounted.
285
 */
286
export function useTouchApplicationStep(
287
  applicationId: number,
288
  step: ApplicationStep,
289
  dispatch: DispatchType,
290
): boolean {
291
  const stepsAreUpdating = useSelector(getStepsAreUpdating);
292
  useEffect(() => {
293
    dispatch(touchApplicationStep(applicationId, ApplicationStepId[step]));
294
  }, [applicationId, step, dispatch]);
295
  return stepsAreUpdating;
296
}
297
298
/**
299
 * Return an Application (normalized, ie without Review) from the redux store, and fetch it from backend if it is not yet in the store.
300
 * @param applicationId
301
 * @param dispatch
302
 */
303
export function useFetchNormalizedApplication(
304
  applicationId: number,
305
  dispatch: DispatchType,
306
): ApplicationNormalized | null {
307
  const applicationSelector = (
308
    state: RootState,
309
  ): ApplicationNormalized | null =>
310
    getApplicationNormalized(state, { applicationId });
311
  const application: ApplicationNormalized | null = useSelector(
312
    applicationSelector,
313
  );
314
  const applicationIsUpdating = useSelector((state: RootState) =>
315
    getApplicationIsUpdating(state, { applicationId }),
316
  );
317
  const [applicationFetched, setApplicationFetched] = useState(false);
318
  useEffect(() => {
319
    if (application === null && !applicationIsUpdating && !applicationFetched) {
320
      setApplicationFetched(true);
321
      dispatch(fetchApplication(applicationId));
322
    }
323
  }, [application, applicationId, applicationIsUpdating, dispatch]);
324
  return application;
325
}
326
327
/**
328
 * Return an Application from the redux store, and fetch it from backend if it is not yet in the store.
329
 * @param applicationId
330
 * @param dispatch
331
 */
332
export function useFetchApplication(
333
  applicationId: number,
334
  dispatch: DispatchType,
335
): Application | null {
336
  const applicationSelector = (state: RootState): Application | null =>
337
    getApplicationById(state, { id: applicationId });
338
  const application: Application | null = useSelector(applicationSelector);
339
  const applicationIsUpdating = useSelector((state: RootState) =>
340
    getApplicationIsUpdating(state, { applicationId }),
341
  );
342
  useEffect(() => {
343
    if (application === null && !applicationIsUpdating) {
344
      dispatch(fetchApplication(applicationId));
345
    }
346
  }, [application, applicationId, applicationIsUpdating, dispatch]);
347
  return application;
348
}
349
350
/**
351
 * Return an Job from the redux store, and fetch it from backend if it is not yet in the store.
352
 * @param jobId
353
 * @param dispatch
354
 */
355
export function useFetchJob(
356
  jobId: number | undefined,
357
  dispatch: DispatchType,
358
): Job | null {
359
  const job = useJob(jobId);
360
  const jobUpdatingSelector = (state: RootState) =>
361
    jobId ? getJobIsUpdating(state, jobId) : false;
362
  const jobIsUpdating = useSelector(jobUpdatingSelector);
363
  useEffect(() => {
364
    // If job is null and not already updating, fetch it.
365
    if (jobId && job === null && !jobIsUpdating) {
366
      dispatch(fetchJob(jobId));
367
    }
368
  }, [jobId, job, jobIsUpdating, dispatch]);
369
  return job;
370
}
371
372
/**
373
 * Return all Experience relavant to an Application from the redux store, and fetch it from backend if it is not yet in the store.
374
 * @param applicationId
375
 * @param application
376
 * @param dispatch
377
 */
378
export function useFetchExperience(
379
  applicationId: number,
380
  application: ApplicationNormalized | null,
381
  dispatch: DispatchType,
382
): {
383
  experiences: ExperienceType[];
384
  experiencesUpdating: boolean;
385
  experiencesFetched: boolean;
386
} {
387
  const applicantId = application?.applicant_id ?? 0;
388
389
  // When an Application is still a draft, use Experiences associated with the applicant profile.
390
  // When an Application has been submitted and is no longer a draft, display Experience associated with the Application directly.
391
  const applicationLoaded = application !== null;
392
  const useProfileExperience =
393
    application === null ||
394
    application.application_status_id === ApplicationStatusId.draft;
395
396
  const experiences = useExperiences(applicationId, application);
397
  const experiencesUpdating = useSelector((state: RootState) =>
398
    useProfileExperience
399
      ? getUpdatingByApplicant(state, { applicantId })
400
      : getUpdatingByApplication(state, { applicationId }),
401
  );
402
  const [experiencesFetched, setExperiencesFetched] = useState(false);
403
  useEffect(() => {
404
    // Only load experiences if they have never been fetched by this component (!experiencesFetched),
405
    //  have never been fetched by another component (length === 0),
406
    //  and are not currently being fetched (!experiencesUpdating).
407
    // Also, wait until application has been loaded so the correct source can be determined.
408
    if (
409
      applicationLoaded &&
410
      !experiencesFetched &&
411
      !experiencesUpdating &&
412
      experiences.length === 0
413
    ) {
414
      setExperiencesFetched(true);
415
      if (useProfileExperience) {
416
        dispatch(fetchExperienceByApplicant(applicantId));
417
      } else {
418
        dispatch(fetchExperienceByApplication(applicationId));
419
      }
420
    }
421
  }, [
422
    applicantId,
423
    applicationId,
424
    applicationLoaded,
425
    dispatch,
426
    experiences.length,
427
    experiencesFetched,
428
    experiencesUpdating,
429
    useProfileExperience,
430
  ]);
431
  return {
432
    experiences,
433
    experiencesUpdating,
434
    experiencesFetched,
435
  };
436
}
437
438
/**
439
 * Return an User from the redux store, and fetch it from backend if it is not yet in the store.
440
 * @param jobId
441
 * @param dispatch
442
 */
443
export function useFetchUser(
444
  userId: number,
445
  dispatch: DispatchType,
446
): User | null {
447
  const user = useUser(userId);
448
  useEffect(() => {
449
    // If job is null and not already updating, fetch it.
450
    if (userId) {
451
      dispatch(fetchUser(userId));
452
    }
453
  }, [userId, dispatch]);
454
  return user;
455
}
456
457
/**
458
 * Trigger fetches for all data needed for the Application process which is not yet in the redux store, or in the process of loading.
459
 * @param applicationId
460
 */
461
export function useFetchAllApplicationData(
462
  applicationId: number,
463
  dispatch: DispatchType,
464
): {
465
  applicationLoaded: boolean;
466
  userLoaded: boolean;
467
  jobLoaded: boolean;
468
  criteriaLoaded: boolean;
469
  experiencesLoaded: boolean;
470
  experienceSkillsLoaded: boolean;
471
  jobQuestionsLoaded: boolean;
472
  applicationAnswersLoaded: boolean;
473
  experienceConstantsLoaded: boolean;
474
  skillsLoaded: boolean;
475
} {
476
  const application = useFetchNormalizedApplication(applicationId, dispatch);
477
  const jobId = application?.job_poster_id;
478
  const job = useFetchJob(jobId, dispatch);
479
  const { experiences, experiencesUpdating } = useFetchExperience(
480
    applicationId,
481
    application,
482
    dispatch,
483
  );
484
  const {
485
    awardRecipientTypes,
486
    awardRecognitionTypes,
487
    educationTypes,
488
    educationStatuses,
489
  } = useFetchExperienceConstants(dispatch);
490
  const skills = useFetchSkills(dispatch);
491
492
  const applicationLoaded = application !== null;
493
  const jobLoaded = job !== null;
494
  const experiencesLoaded = !experiencesUpdating || experiences.length > 0;
495
  const experienceConstantsLoaded =
496
    awardRecipientTypes.length > 0 &&
497
    awardRecognitionTypes.length > 0 &&
498
    educationTypes.length > 0 &&
499
    educationStatuses.length > 0;
500
  const skillsLoaded = skills.length > 0;
501
502
  return {
503
    applicationLoaded,
504
    jobLoaded,
505
    experiencesLoaded,
506
    experienceConstantsLoaded,
507
    skillsLoaded,
508
    criteriaLoaded: jobLoaded,
509
    experienceSkillsLoaded: experiencesLoaded,
510
    jobQuestionsLoaded: jobLoaded,
511
    applicationAnswersLoaded: applicationLoaded,
512
    userLoaded: applicationLoaded,
513
  };
514
}
515
516
/**
517
 * Trigger fetches for all data needed for the Application review process which is not yet in the redux store, or in the process of loading.
518
 * @param applicationId
519
 */
520
export function useFetchReviewApplicationData(
521
  applicantUserId: number,
522
  applicationId: number,
523
  jobId: number,
524
  dispatch: DispatchType,
525
): {
526
  applicationLoaded: boolean;
527
  jobLoaded: boolean;
528
  experiencesLoaded: boolean;
529
  experienceConstantsLoaded: boolean;
530
  skillsLoaded: boolean;
531
} {
532
  const application = useFetchApplication(applicationId, dispatch);
533
  const job = useFetchJob(jobId, dispatch);
534
  const { experiences, experiencesUpdating } = useFetchExperience(
535
    applicationId,
536
    application,
537
    dispatch,
538
  );
539
  const {
540
    awardRecipientTypes,
541
    awardRecognitionTypes,
542
    educationTypes,
543
    educationStatuses,
544
  } = useFetchExperienceConstants(dispatch);
545
  const skills = useFetchSkills(dispatch);
546
547
  return {
548
    applicationLoaded: application !== null,
549
    jobLoaded: job !== null,
550
    experiencesLoaded: !experiencesUpdating || experiences.length > 0,
551
    experienceConstantsLoaded:
552
      awardRecipientTypes.length > 0 &&
553
      awardRecognitionTypes.length > 0 &&
554
      educationTypes.length > 0 &&
555
      educationStatuses.length > 0,
556
    skillsLoaded: skills.length > 0,
557
  };
558
}
559