Passed
Push — feature/azure-webapp-pipeline-... ( 271549...3c88ad )
by Grant
07:11 queued 10s
created

resources/assets/js/components/ApplicantProfile/Experience/ProfileExperience.tsx   B

Complexity

Total Complexity 45
Complexity/F 0

Size

Lines of Code 476
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 45
eloc 398
mnd 45
bc 45
fnc 0
dl 0
loc 476
rs 8.8
bpm 0
cpm 0
noi 0
c 0
b 0
f 0

How to fix   Complexity   

Complexity

Complex classes like resources/assets/js/components/ApplicantProfile/Experience/ProfileExperience.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 React, { useEffect } from "react";
3
import { FormattedMessage, useIntl } from "react-intl";
4
import {
5
  Experience,
6
  Skill,
7
  ExperienceSkill,
8
  ExperienceEducation,
9
  ExperienceWork,
10
  ExperienceCommunity,
11
  ExperiencePersonal,
12
  ExperienceAward,
13
} from "../../../models/types";
14
import {
15
  modalButtonProps,
16
  ModalButton,
17
} from "../../Application/Experience/Experience";
18
import { mapToObject, getId } from "../../../helpers/queries";
19
import { experienceMessages } from "../../Application/applicationMessages";
20
import { getFocusableElements, toggleAccordion } from "../../../helpers/forms";
21
import { useUrlHash } from "../../../helpers/router";
22
23
import { getExperienceSkillsOfExperience } from "../../Application/helpers";
24
import { ProfileEducationAccordion } from "../../Application/ExperienceAccordions/ExperienceEducationAccordion";
25
import { ProfileWorkAccordion } from "../../Application/ExperienceAccordions/ExperienceWorkAccordion";
26
import { ProfileCommunityAccordion } from "../../Application/ExperienceAccordions/ExperienceCommunityAccordion";
27
import { ProfilePersonalAccordion } from "../../Application/ExperienceAccordions/ExperiencePersonalAccordion";
28
import { ProfileAwardAccordion } from "../../Application/ExperienceAccordions/ExperienceAwardAccordion";
29
import ProfileEducationModal from "./EducationExperienceProfileModal";
30
import ProfileWorkModal from "./WorkExperienceProfileModal";
31
import ProfileCommunityModal from "./CommunityExperienceProfileModal";
32
import ProfilePersonalModal from "./PersonalExperienceProfileModal";
33
import ProfileAwardModal from "./AwardExperienceProfileModal";
34
import {
35
  FormEducationStatus,
36
  FormEducationType,
37
} from "../../Application/ExperienceModals/EducationExperienceModal";
38
import {
39
  FormAwardRecipientType,
40
  FormAwardRecognitionType,
41
} from "../../Application/ExperienceModals/AwardExperienceModal";
42
import { ExperienceSubmitData } from "./ProfileExperienceCommon";
43
import { getApplicantSkillsUrl } from "../../../helpers/routes";
44
import { getLocale } from "../../../helpers/localize";
45
46
const profileExperienceAccordion = (
47
  experience: Experience,
48
  relevantSkills: ExperienceSkill[],
49
  skillsById: { [id: number]: Skill },
50
  handleEdit: (triggerRef: React.RefObject<HTMLButtonElement>) => void,
51
  handleDelete: () => Promise<void>,
52
): React.ReactElement | null => {
53
  switch (experience.type) {
54
    case "experience_education":
55
      return (
56
        <ProfileEducationAccordion
57
          key={`${experience.id}-${experience.type}`}
58
          experience={experience}
59
          handleDelete={handleDelete}
60
          handleEdit={handleEdit}
61
          relevantSkills={relevantSkills}
62
          skillsById={skillsById}
63
        />
64
      );
65
    case "experience_work":
66
      return (
67
        <ProfileWorkAccordion
68
          key={`${experience.id}-${experience.type}`}
69
          experience={experience}
70
          handleDelete={handleDelete}
71
          handleEdit={handleEdit}
72
          relevantSkills={relevantSkills}
73
          skillsById={skillsById}
74
        />
75
      );
76
    case "experience_community":
77
      return (
78
        <ProfileCommunityAccordion
79
          key={`${experience.id}-${experience.type}`}
80
          experience={experience}
81
          handleDelete={handleDelete}
82
          handleEdit={handleEdit}
83
          relevantSkills={relevantSkills}
84
          skillsById={skillsById}
85
        />
86
      );
87
    case "experience_personal":
88
      return (
89
        <ProfilePersonalAccordion
90
          key={`${experience.id}-${experience.type}`}
91
          experience={experience}
92
          handleDelete={handleDelete}
93
          handleEdit={handleEdit}
94
          relevantSkills={relevantSkills}
95
          skillsById={skillsById}
96
        />
97
      );
98
    case "experience_award":
99
      return (
100
        <ProfileAwardAccordion
101
          key={`${experience.id}-${experience.type}`}
102
          experience={experience}
103
          handleDelete={handleDelete}
104
          handleEdit={handleEdit}
105
          relevantSkills={relevantSkills}
106
          skillsById={skillsById}
107
        />
108
      );
109
    default:
110
      return null;
111
  }
112
};
113
114
const NoSkillsNotification: React.FC<{ applicantId: number }> = ({
115
  applicantId,
116
}) => {
117
  const intl = useIntl();
118
  const locale = getLocale(intl.locale);
119
  return (
120
    <div
121
      data-c-alert="warning"
122
      data-c-radius="rounded"
123
      role="alert"
124
      data-c-margin="bottom(1)"
125
      // data-c-grid="middle"
126
    >
127
      <div data-c-padding="half" data-c-grid="middle">
128
        <div
129
          data-c-grid-item="base(1of1) pl(1of8) tl(1of12)"
130
          data-c-align="center"
131
        >
132
          <i
133
            aria-hidden="true"
134
            className="fa fa-exclamation-circle"
135
            data-c-padding="right(.5)"
136
            data-c-font-size="h2"
137
            data-c-margin="tb(.5)"
138
          />
139
        </div>
140
        <div data-c-grid-item="base(1of1) pl(7of8) tl(11of12)">
141
          <p>
142
            <FormattedMessage
143
              id="profile.experience.noSkills"
144
              defaultMessage="<b>No skills:</b> It seems you have not added any skills yet. You can create experiences now, but this section works best when you have some skills on your profile. <a>Click here to add skills.</a>"
145
              description="Alert that appears when there are no skills yet attached to profile."
146
              values={{
147
                b: (value) => <span data-c-font-weight="bold">{value}</span>,
148
                a: (...chunks): React.ReactElement => (
149
                  <a href={getApplicantSkillsUrl(locale, applicantId)}>
150
                    {chunks}
151
                  </a>
152
                ),
153
              }}
154
            />
155
          </p>
156
        </div>
157
      </div>
158
    </div>
159
  );
160
};
161
export interface ProfileExperienceProps {
162
  applicantId: number;
163
  experiences: Experience[];
164
  educationStatuses: FormEducationStatus[];
165
  educationTypes: FormEducationType[];
166
  experienceSkills: ExperienceSkill[];
167
  userSkills: Skill[];
168
  recipientTypes: FormAwardRecipientType[];
169
  recognitionTypes: FormAwardRecognitionType[];
170
  handleCreateExperience: (
171
    data: ExperienceSubmitData<Experience>,
172
  ) => Promise<void>;
173
  handleUpdateExperience: (
174
    data: ExperienceSubmitData<Experience>,
175
  ) => Promise<void>;
176
  handleDeleteExperience: (
177
    id: number,
178
    type: Experience["type"],
179
  ) => Promise<void>;
180
}
181
182
export const ProfileExperience: React.FC<ProfileExperienceProps> = ({
183
  applicantId,
184
  experiences,
185
  educationStatuses,
186
  educationTypes,
187
  experienceSkills,
188
  userSkills,
189
  handleCreateExperience,
190
  handleUpdateExperience,
191
  handleDeleteExperience,
192
  recipientTypes,
193
  recognitionTypes,
194
}) => {
195
  const intl = useIntl();
196
197
  const [isModalVisible, setIsModalVisible] = React.useState<{
198
    id: Experience["type"] | "";
199
    visible: boolean;
200
    triggerRef: React.RefObject<HTMLButtonElement> | null;
201
  }>({
202
    id: "",
203
    visible: false,
204
    triggerRef: null,
205
  });
206
207
  const [experienceData, setExperienceData] = React.useState<Experience | null>(
208
    null,
209
  );
210
211
  const modalButtons = modalButtonProps(intl);
212
213
  const openModal = (
214
    id: Experience["type"],
215
    triggerRef: React.RefObject<HTMLButtonElement> | null,
216
  ): void => {
217
    setIsModalVisible({ id, visible: true, triggerRef });
218
  };
219
220
  const closeModal = (): void => {
221
    setExperienceData(null);
222
    if (isModalVisible.triggerRef?.current) {
223
      isModalVisible.triggerRef.current.focus();
224
    } else {
225
      const focusableElements = getFocusableElements();
226
      if (focusableElements.length > 0) {
227
        focusableElements[0].focus();
228
      }
229
    }
230
    setIsModalVisible({ id: "", visible: false, triggerRef: null });
231
  };
232
233
  const updateExperience = (data: ExperienceSubmitData<Experience>) =>
234
    handleUpdateExperience(data).then(closeModal);
235
  const createExperience = (data: ExperienceSubmitData<Experience>) =>
236
    handleCreateExperience(data).then(closeModal);
237
238
  const editExperience = (
239
    experience: Experience,
240
    triggerRef: React.RefObject<HTMLButtonElement> | null,
241
  ): void => {
242
    setExperienceData(experience);
243
    setIsModalVisible({ id: experience.type, visible: true, triggerRef });
244
  };
245
246
  const deleteExperience = (experience: Experience): Promise<void> =>
247
    handleDeleteExperience(experience.id, experience.type).then(closeModal);
248
249
  const skillsById = mapToObject(userSkills, getId);
250
251
  const modalRoot = document.getElementById("modal-root");
252
253
  // Open modal for editing if URI has a hash.
254
  const uriHash = useUrlHash();
255
  let experienceType: string;
256
  let experienceId: number;
257
258
  if (uriHash) {
259
    const uriHashFragments = uriHash.substring(1).split("_");
260
    // Get experience type from first two fragments of URI hash.
261
    experienceType = `${uriHashFragments[0]}_${uriHashFragments[1]}`;
262
    // Get experience id from third fragment of URI hash.
263
    experienceId = Number(uriHashFragments[2]);
264
  }
265
266
  const experienceCurrent: Experience | undefined = experiences.find(
267
    (experience) =>
268
      experience.type === experienceType && experience.id === experienceId,
269
  );
270
271
  useEffect(() => {
272
    if (uriHash && experienceCurrent) {
273
      toggleAccordion(uriHash.substring(1));
274
      // Open edit experience modal.
275
      editExperience(experienceCurrent, null);
276
    }
277
    // eslint-disable-next-line react-hooks/exhaustive-deps
278
  }, []); // useEffect should only run on mount and unmount.
279
280
  return (
281
    <>
282
      {userSkills.length === 0 && (
283
        <NoSkillsNotification applicantId={applicantId} />
284
      )}
285
      <div>
286
        <h2 data-c-heading="h2" data-c-margin="bottom(1)">
287
          {intl.formatMessage(experienceMessages.heading)}
288
        </h2>
289
        <p data-c-margin="bottom(1)">
290
          <FormattedMessage
291
            id="profile.experience.preamble"
292
            defaultMessage="Use the buttons below to add experiences you want to share with the manager. Experiences you have added in the past also appear below, and you can edit them to link them to skills required for this job when necessary."
293
            description="First section of text on the experience step of the Application Timeline."
294
          />
295
        </p>
296
        {/* Experience Modal Buttons */}
297
        <div data-c-grid="gutter(all, 1)">
298
          {Object.values(modalButtons).map((buttonProps) => {
299
            const { id, title, icon } = buttonProps;
300
            return (
301
              <ModalButton
302
                id={id}
303
                key={id}
304
                title={title}
305
                icon={icon}
306
                openModal={openModal}
307
              />
308
            );
309
          })}
310
        </div>
311
        {/* Experience Accordion List */}
312
        {experiences && experiences.length > 0 ? (
313
          <div className="experience-list" data-c-margin="top(2)">
314
            <div data-c-accordion-group>
315
              {experiences.map((experience) => {
316
                const relevantSkills: ExperienceSkill[] = getExperienceSkillsOfExperience(
317
                  experienceSkills,
318
                  experience,
319
                );
320
                const handleEdit = (
321
                  triggerRef: React.RefObject<HTMLButtonElement>,
322
                ) => editExperience(experience, triggerRef);
323
                const handleDelete = () => deleteExperience(experience);
324
                const errorAccordion = () => (
325
                  <div
326
                    data-c-background="gray(10)"
327
                    data-c-radius="rounded"
328
                    data-c-border="all(thin, solid, gray)"
329
                    data-c-margin="top(1)"
330
                    data-c-padding="all(1)"
331
                  >
332
                    <div data-c-align="base(center)">
333
                      <p data-c-color="stop">
334
                        {intl.formatMessage(
335
                          experienceMessages.errorRenderingExperience,
336
                        )}
337
                      </p>
338
                    </div>
339
                  </div>
340
                );
341
                return (
342
                  profileExperienceAccordion(
343
                    experience,
344
                    relevantSkills,
345
                    skillsById,
346
                    handleEdit,
347
                    handleDelete,
348
                  ) ?? errorAccordion()
349
                );
350
              })}
351
            </div>
352
          </div>
353
        ) : (
354
          <div
355
            data-c-background="gray(10)"
356
            data-c-radius="rounded"
357
            data-c-border="all(thin, solid, gray)"
358
            data-c-margin="top(2)"
359
            data-c-padding="all(1)"
360
          >
361
            <div data-c-align="base(center)">
362
              <p data-c-color="gray">
363
                <FormattedMessage
364
                  id="profile.experience.noExperiences"
365
                  defaultMessage="Looks like you don't have any experience added yet. Use the buttons above to add experience. Don't forget that experience will always be saved to your profile so that you can use it on future applications!"
366
                  description="Message displayed when application has no experiences."
367
                />
368
              </p>
369
            </div>
370
          </div>
371
        )}
372
      </div>
373
      <div data-c-dialog-overlay={isModalVisible.visible ? "active" : ""} />
374
      <ProfileEducationModal
375
        educationStatuses={educationStatuses}
376
        educationTypes={educationTypes}
377
        experienceEducation={experienceData as ExperienceEducation}
378
        experienceableId={experienceData?.experienceable_id ?? 0}
379
        experienceableType={
380
          experienceData?.experienceable_type ?? "application"
381
        }
382
        userSkills={userSkills}
383
        experienceSkills={experienceSkills}
384
        modalId={modalButtons.education.id}
385
        onModalCancel={closeModal}
386
        onModalConfirm={
387
          experienceData === null ? createExperience : updateExperience
388
        }
389
        parentElement={modalRoot}
390
        visible={
391
          isModalVisible.visible &&
392
          isModalVisible.id === modalButtons.education.id
393
        }
394
      />
395
      <ProfileWorkModal
396
        experienceWork={experienceData as ExperienceWork}
397
        experienceableId={experienceData?.experienceable_id ?? 0}
398
        experienceableType={
399
          experienceData?.experienceable_type ?? "application"
400
        }
401
        userSkills={userSkills}
402
        experienceSkills={experienceSkills}
403
        modalId={modalButtons.work.id}
404
        onModalCancel={closeModal}
405
        onModalConfirm={
406
          experienceData === null ? createExperience : updateExperience
407
        }
408
        parentElement={modalRoot}
409
        visible={
410
          isModalVisible.visible && isModalVisible.id === modalButtons.work.id
411
        }
412
      />
413
      <ProfileCommunityModal
414
        experienceCommunity={experienceData as ExperienceCommunity}
415
        experienceableId={experienceData?.experienceable_id ?? 0}
416
        experienceableType={
417
          experienceData?.experienceable_type ?? "application"
418
        }
419
        userSkills={userSkills}
420
        experienceSkills={experienceSkills}
421
        modalId={modalButtons.community.id}
422
        onModalCancel={closeModal}
423
        onModalConfirm={
424
          experienceData === null ? createExperience : updateExperience
425
        }
426
        parentElement={modalRoot}
427
        visible={
428
          isModalVisible.visible &&
429
          isModalVisible.id === modalButtons.community.id
430
        }
431
      />
432
      <ProfilePersonalModal
433
        experiencePersonal={experienceData as ExperiencePersonal}
434
        experienceableId={experienceData?.experienceable_id ?? 0}
435
        experienceableType={
436
          experienceData?.experienceable_type ?? "application"
437
        }
438
        userSkills={userSkills}
439
        experienceSkills={experienceSkills}
440
        modalId={modalButtons.personal.id}
441
        onModalCancel={closeModal}
442
        onModalConfirm={
443
          experienceData === null ? createExperience : updateExperience
444
        }
445
        parentElement={modalRoot}
446
        visible={
447
          isModalVisible.visible &&
448
          isModalVisible.id === modalButtons.personal.id
449
        }
450
      />
451
      <ProfileAwardModal
452
        experienceAward={experienceData as ExperienceAward}
453
        experienceableId={experienceData?.experienceable_id ?? 0}
454
        experienceableType={
455
          experienceData?.experienceable_type ?? "application"
456
        }
457
        userSkills={userSkills}
458
        experienceSkills={experienceSkills}
459
        modalId={modalButtons.award.id}
460
        onModalCancel={closeModal}
461
        onModalConfirm={
462
          experienceData === null ? createExperience : updateExperience
463
        }
464
        parentElement={modalRoot}
465
        recipientTypes={recipientTypes}
466
        recognitionTypes={recognitionTypes}
467
        visible={
468
          isModalVisible.visible && isModalVisible.id === modalButtons.award.id
469
        }
470
      />
471
    </>
472
  );
473
};
474
475
export default ProfileExperience;
476