Passed
Push — task/application-handle-step-s... ( 5f39f9 )
by Yonathan
08:23
created

applicationHooks.tsx ➔ useSteps   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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