Passed
Push — feature/review-step-functional ( 52d632 )
by Tristan
06:14
created

resources/assets/js/hooks/applicationHooks.tsx   B

Complexity

Total Complexity 45
Complexity/F 2.81

Size

Lines of Code 420
Function Count 16

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 45
eloc 323
c 0
b 0
f 0
dl 0
loc 420
rs 8.8
mnd 29
bc 29
fnc 16
bpm 1.8125
cpm 2.8125
noi 0

16 Functions

Rating   Name   Duplication   Size   Complexity  
A applicationHooks.tsx ➔ useJob 0 4 2
B applicationHooks.tsx ➔ useFetchExperienceConstants 0 58 5
A applicationHooks.tsx ➔ useJobApplicationAnswers 0 6 1
A applicationHooks.tsx ➔ useFetchJob 0 21 3
A applicationHooks.tsx ➔ useExperienceConstants 0 16 1
A applicationHooks.tsx ➔ useFetchApplication 0 28 2
B applicationHooks.tsx ➔ useFetchAllApplicationData 0 57 2
A applicationHooks.tsx ➔ useCriteria 0 4 2
B applicationHooks.tsx ➔ useFetchExperience 0 64 7
A applicationHooks.tsx ➔ useApplication 0 6 1
A applicationHooks.tsx ➔ useJobPosterQuestions 0 6 2
A applicationHooks.tsx ➔ useExperienceSkills 0 16 5
B applicationHooks.tsx ➔ useExperiences 0 30 5
A applicationHooks.tsx ➔ useApplicationUser 0 5 4
A applicationHooks.tsx ➔ useSkills 0 3 1
A applicationHooks.tsx ➔ useFetchSkills 0 14 2

How to fix   Complexity   

Complexity

Complex classes like resources/assets/js/hooks/applicationHooks.tsx often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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