Passed
Push — feature/ie-alert ( c39468...dc7a98 )
by
unknown
07:35
created

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

Complexity

Total Complexity 51
Complexity/F 0

Size

Lines of Code 673
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 51
eloc 555
mnd 51
bc 51
fnc 0
dl 0
loc 673
rs 7.92
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/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, { FunctionComponent, useState, useEffect } from "react";
3
import { FormattedMessage, useIntl } from "react-intl";
4
import * as Yup from "yup";
5
import { Field, Form, Formik } from "formik";
6
import {
7
  Experience,
8
  Skill,
9
  ExperienceSkill,
10
  ExperienceEducation,
11
  ExperienceWork,
12
  ExperienceCommunity,
13
  ExperiencePersonal,
14
  ExperienceAward,
15
} from "../../models/types";
16
import {
17
  modalButtonProps,
18
  ModalButton,
19
} from "../Application/Experience/Experience";
20
import {
21
  AwardRecipientType,
22
  AwardRecognitionType,
23
  ProfileAwardModal,
24
} from "../Application/ExperienceModals/AwardExperienceModal";
25
import {
26
  EducationStatus,
27
  EducationType,
28
  ProfileEducationModal,
29
} from "../Application/ExperienceModals/EducationExperienceModal";
30
import { find, hasKey, mapToObject, getId } from "../../helpers/queries";
31
import Modal from "../Modal";
32
import AlertWhenUnsaved from "../Form/AlertWhenUnsaved";
33
import TextAreaInput from "../Form/TextAreaInput";
34
import {
35
  experienceMessages,
36
  skillMessages,
37
} from "../Application/applicationMessages";
38
import { validationMessages } from "../Form/Messages";
39
import { JUSTIFICATION_WORD_LIMIT } from "../Application/Skills/Skills";
40
import { countNumberOfWords } from "../WordCounter/helpers";
41
import WordCounter from "../WordCounter/WordCounter";
42
import displayMessages from "../Application/Skills/skillsMessages";
43
import {
44
  getExperienceHeading,
45
  getExperienceJustificationLabel,
46
  getExperienceSubheading,
47
} from "../../models/localizedConstants";
48
import { getLocale, localizeFieldNonNull } from "../../helpers/localize";
49
import { toggleAccordion } from "../../helpers/forms";
50
import { useUrlHash } from "../../helpers/router";
51
52
import {
53
  getExperienceOfExperienceSkill,
54
  getExperienceSkillsOfExperience,
55
} from "../Application/helpers";
56
import { ProfileWorkModal } from "../Application/ExperienceModals/WorkExperienceModal";
57
import { ProfileCommunityModal } from "../Application/ExperienceModals/CommunityExperienceModal";
58
import { ProfilePersonalModal } from "../Application/ExperienceModals/PersonalExperienceModal";
59
import { ProfileEducationAccordion } from "../Application/ExperienceAccordions/ExperienceEducationAccordion";
60
import { ProfileWorkAccordion } from "../Application/ExperienceAccordions/ExperienceWorkAccordion";
61
import { ProfileCommunityAccordion } from "../Application/ExperienceAccordions/ExperienceCommunityAccordion";
62
import { ProfilePersonalAccordion } from "../Application/ExperienceAccordions/ExperiencePersonalAccordion";
63
import { ProfileAwardAccordion } from "../Application/ExperienceAccordions/ExperienceAwardAccordion";
64
65
const profileExperienceAccordion = (
66
  experience: Experience,
67
  relevantSkills: ExperienceSkill[],
68
  skillsById: { [id: number]: Skill },
69
  handleEdit: () => void,
70
  handleDelete: () => Promise<void>,
71
  handleEditSkill: (experienceSkillId: number) => void,
72
): React.ReactElement | null => {
73
  switch (experience.type) {
74
    case "experience_education":
75
      return (
76
        <ProfileEducationAccordion
77
          key={`${experience.id}-${experience.type}`}
78
          experience={experience}
79
          handleDelete={handleDelete}
80
          handleEdit={handleEdit}
81
          handleEditSkill={handleEditSkill}
82
          relevantSkills={relevantSkills}
83
          skillsById={skillsById}
84
        />
85
      );
86
    case "experience_work":
87
      return (
88
        <ProfileWorkAccordion
89
          key={`${experience.id}-${experience.type}`}
90
          experience={experience}
91
          handleDelete={handleDelete}
92
          handleEdit={handleEdit}
93
          handleEditSkill={handleEditSkill}
94
          relevantSkills={relevantSkills}
95
          skillsById={skillsById}
96
        />
97
      );
98
    case "experience_community":
99
      return (
100
        <ProfileCommunityAccordion
101
          key={`${experience.id}-${experience.type}`}
102
          experience={experience}
103
          handleDelete={handleDelete}
104
          handleEdit={handleEdit}
105
          handleEditSkill={handleEditSkill}
106
          relevantSkills={relevantSkills}
107
          skillsById={skillsById}
108
        />
109
      );
110
    case "experience_personal":
111
      return (
112
        <ProfilePersonalAccordion
113
          key={`${experience.id}-${experience.type}`}
114
          experience={experience}
115
          handleDelete={handleDelete}
116
          handleEdit={handleEdit}
117
          handleEditSkill={handleEditSkill}
118
          relevantSkills={relevantSkills}
119
          skillsById={skillsById}
120
        />
121
      );
122
    case "experience_award":
123
      return (
124
        <ProfileAwardAccordion
125
          key={`${experience.id}-${experience.type}`}
126
          experience={experience}
127
          handleDelete={handleDelete}
128
          handleEdit={handleEdit}
129
          handleEditSkill={handleEditSkill}
130
          relevantSkills={relevantSkills}
131
          skillsById={skillsById}
132
        />
133
      );
134
    default:
135
      return null;
136
  }
137
};
138
139
const SkillExperienceModal: FunctionComponent<{
140
  experienceSkill: ExperienceSkill | null;
141
  experiences: Experience[];
142
  skillsById: { [id: number]: Skill };
143
  handleCancel: () => void;
144
  handleConfirm: (data: ExperienceSkill) => Promise<void>;
145
  handleDelete: () => Promise<void>;
146
}> = ({
147
  experienceSkill,
148
  handleCancel,
149
  handleConfirm,
150
  handleDelete,
151
  skillsById,
152
  experiences,
153
}) => {
154
  const intl = useIntl();
155
  const locale = getLocale(intl.locale);
156
157
  const [isDeleting, setIsDeleting] = useState(false);
158
159
  const initialValues = {
160
    justification: experienceSkill?.justification ?? "",
161
  };
162
163
  const experienceSkillSchema = Yup.object().shape({
164
    justification: Yup.string()
165
      .test(
166
        "wordCount",
167
        intl.formatMessage(validationMessages.overMaxWords, {
168
          numberOfWords: JUSTIFICATION_WORD_LIMIT,
169
        }),
170
        (value: string) =>
171
          countNumberOfWords(value) <= JUSTIFICATION_WORD_LIMIT,
172
      )
173
      .required(intl.formatMessage(validationMessages.required)),
174
  });
175
176
  const experience =
177
    experienceSkill !== null
178
      ? getExperienceOfExperienceSkill(experienceSkill, experiences)
179
      : null;
180
  let textareaLabel = "";
181
  let heading = "";
182
  let subheading = "";
183
184
  if (
185
    experienceSkill !== null &&
186
    experience !== null &&
187
    hasKey(skillsById, experienceSkill.skill_id)
188
  ) {
189
    const skill = skillsById[experienceSkill.skill_id];
190
    const skillName = localizeFieldNonNull(locale, skill, "name");
191
    textareaLabel = getExperienceJustificationLabel(
192
      experience,
193
      intl,
194
      skillName,
195
    );
196
    heading = getExperienceHeading(experience, intl);
197
    subheading = getExperienceSubheading(experience, intl);
198
  }
199
200
  return (
201
    <Modal
202
      id="profile-experience-skill-modal"
203
      parentElement={document.getElementById("modal-root")}
204
      visible={experienceSkill !== null}
205
      onModalConfirm={handleCancel}
206
      onModalCancel={handleCancel}
207
    >
208
      <div
209
        className="dialog-header"
210
        data-c-background="c1(100)"
211
        data-c-border="bottom(thin, solid, black)"
212
        data-c-padding="tb(1)"
213
      >
214
        <div data-c-container="medium">
215
          <h5
216
            data-c-colour="white"
217
            data-c-font-size="h3"
218
            data-c-font-weight="bold"
219
            data-c-dialog-focus
220
          >
221
            {heading}
222
          </h5>
223
          <p
224
            data-c-margin="top(quarter)"
225
            data-c-colour="white"
226
            data-c-font-size="small"
227
          >
228
            {subheading}
229
          </p>
230
        </div>
231
      </div>
232
      {experienceSkill !== null && (
233
        <Formik
234
          initialValues={initialValues}
235
          validationSchema={experienceSkillSchema}
236
          onSubmit={(values, { setSubmitting, resetForm }): void => {
237
            handleConfirm({
238
              ...experienceSkill,
239
              justification: values.justification,
240
            })
241
              .then(() => {
242
                setSubmitting(false);
243
                resetForm();
244
              })
245
              .catch(() => {
246
                // If there is an error, don't reset the form, allowing user to retry.
247
                setSubmitting(false);
248
              });
249
          }}
250
        >
251
          {({ dirty, isSubmitting, resetForm }): React.ReactElement => (
252
            <Form>
253
              <AlertWhenUnsaved />
254
              <hr data-c-hr="thin(gray)" data-c-margin="bottom(1)" />
255
              <div data-c-padding="lr(1)">
256
                <Field
257
                  id="experience-skill-textarea"
258
                  name="justification"
259
                  label={textareaLabel}
260
                  component={TextAreaInput}
261
                  placeholder={intl.formatMessage(
262
                    skillMessages.experienceSkillPlaceholder,
263
                  )}
264
                  required
265
                />
266
              </div>
267
              <div data-c-padding="all(1)">
268
                <div data-c-grid="gutter(all, 1) middle">
269
                  <div
270
                    data-c-grid-item="tp(1of2)"
271
                    data-c-align="base(center) tp(left)"
272
                  >
273
                    <button
274
                      data-c-button="outline(c1)"
275
                      data-c-radius="rounded"
276
                      data-c-margin="right(1)"
277
                      type="button"
278
                      onClick={handleCancel}
279
                      disabled={isSubmitting || isDeleting}
280
                    >
281
                      <span>
282
                        <FormattedMessage
283
                          id="profileExperience.skillExperienceModal.cancel"
284
                          defaultMessage="Cancel"
285
                          description="Cancel button text"
286
                        />
287
                      </span>
288
                    </button>
289
                    <button
290
                      data-c-button="outline(stop)"
291
                      data-c-radius="rounded"
292
                      type="button"
293
                      onClick={() => {
294
                        setIsDeleting(true);
295
                        handleDelete()
296
                          .then(() => {
297
                            setIsDeleting(false);
298
                            resetForm();
299
                          })
300
                          .catch(() => {
301
                            setIsDeleting(false);
302
                          });
303
                      }}
304
                      disabled={isSubmitting || isDeleting}
305
                    >
306
                      <span>
307
                        <FormattedMessage
308
                          id="profileExperience.skillExperienceModal.delete"
309
                          defaultMessage="Delete"
310
                          description="Delete button text"
311
                        />
312
                      </span>
313
                    </button>
314
                  </div>
315
                  <div
316
                    data-c-grid-item="tp(1of2)"
317
                    data-c-align="base(center) tp(right)"
318
                  >
319
                    <WordCounter
320
                      elementId="experience-skill-textarea"
321
                      maxWords={JUSTIFICATION_WORD_LIMIT}
322
                      minWords={0}
323
                      absoluteValue
324
                      dataAttributes={{ "data-c-margin": "right(1)" }}
325
                      underMaxMessage={intl.formatMessage(
326
                        displayMessages.wordCountUnderMax,
327
                      )}
328
                      overMaxMessage={intl.formatMessage(
329
                        displayMessages.wordCountOverMax,
330
                      )}
331
                    />
332
                    <button
333
                      data-c-button="solid(c1)"
334
                      data-c-radius="rounded"
335
                      type="submit"
336
                      disabled={!dirty || isSubmitting || isDeleting}
337
                    >
338
                      <span>
339
                        {dirty
340
                          ? intl.formatMessage(displayMessages.save)
341
                          : intl.formatMessage(displayMessages.saved)}
342
                      </span>
343
                    </button>
344
                  </div>
345
                </div>
346
              </div>
347
            </Form>
348
          )}
349
        </Formik>
350
      )}
351
    </Modal>
352
  );
353
};
354
355
export interface ProfileExperienceProps {
356
  experiences: Experience[];
357
  educationStatuses: EducationStatus[];
358
  educationTypes: EducationType[];
359
  experienceSkills: ExperienceSkill[];
360
  skills: Skill[];
361
  recipientTypes: AwardRecipientType[];
362
  recognitionTypes: AwardRecognitionType[];
363
  handleCreateExperience: (data: Experience) => Promise<void>;
364
  handleUpdateExperience: (data: Experience) => Promise<void>;
365
  handleDeleteExperience: (
366
    id: number,
367
    type: Experience["type"],
368
  ) => Promise<void>;
369
  handleUpdateExperienceSkill: (expSkill: ExperienceSkill) => Promise<void>;
370
  handleDeleteExperienceSkill: (expSkill: ExperienceSkill) => Promise<void>;
371
}
372
373
export const ProfileExperience: React.FC<ProfileExperienceProps> = ({
374
  experiences,
375
  educationStatuses,
376
  educationTypes,
377
  experienceSkills,
378
  skills,
379
  handleCreateExperience,
380
  handleUpdateExperience,
381
  handleDeleteExperience,
382
  handleUpdateExperienceSkill,
383
  handleDeleteExperienceSkill,
384
  recipientTypes,
385
  recognitionTypes,
386
}) => {
387
  const intl = useIntl();
388
389
  const [isModalVisible, setIsModalVisible] = React.useState<{
390
    id: Experience["type"] | "";
391
    visible: boolean;
392
  }>({
393
    id: "",
394
    visible: false,
395
  });
396
397
  const [experienceData, setExperienceData] = React.useState<Experience | null>(
398
    null,
399
  );
400
401
  const modalButtons = modalButtonProps(intl);
402
403
  const openModal = (id: Experience["type"]): void => {
404
    setIsModalVisible({ id, visible: true });
405
  };
406
407
  const closeModal = (): void => {
408
    setExperienceData(null);
409
    setIsModalVisible({ id: "", visible: false });
410
  };
411
412
  const updateExperience = (data) =>
413
    handleUpdateExperience(data).then(closeModal);
414
  const createExperience = (data) =>
415
    handleCreateExperience(data).then(closeModal);
416
417
  const editExperience = (experience: Experience): void => {
418
    setExperienceData(experience);
419
    setIsModalVisible({ id: experience.type, visible: true });
420
  };
421
422
  const deleteExperience = (experience: Experience): Promise<void> =>
423
    handleDeleteExperience(experience.id, experience.type).then(closeModal);
424
425
  const [editedExperienceSkillId, setEditedExperienceSkillId] = useState<
426
    number | null
427
  >(null);
428
  const editedExpSkill =
429
    editedExperienceSkillId !== null
430
      ? find(experienceSkills, editedExperienceSkillId)
431
      : null;
432
433
  const skillsById = mapToObject(skills, getId);
434
435
  const modalRoot = document.getElementById("modal-root");
436
437
  // Open modal for editing if URI has a hash.
438
  const uriHash = useUrlHash();
439
  let experienceType: string;
440
  let experienceId: number;
441
442
  if (uriHash) {
443
    const uriHashFragments = uriHash.substring(1).split("_");
444
    // Get experience type from first two fragments of URI hash.
445
    experienceType = `${uriHashFragments[0]}_${uriHashFragments[1]}`;
446
    // Get experience id from third fragment of URI hash.
447
    experienceId = Number(uriHashFragments[2]);
448
  }
449
450
  const experienceCurrent: Experience | undefined = experiences.find(
451
    (experience) =>
452
      experience.type === experienceType && experience.id === experienceId,
453
  );
454
455
  useEffect(() => {
456
    if (uriHash && experienceCurrent) {
457
      toggleAccordion(uriHash.substring(1));
458
      // Open edit experience modal.
459
      editExperience(experienceCurrent);
460
    }
461
    // eslint-disable-next-line react-hooks/exhaustive-deps
462
  }, []); // useEffect should only run on mount and unmount.
463
464
  return (
465
    <>
466
      <div>
467
        <h2 data-c-heading="h2" data-c-margin="bottom(1)">
468
          {intl.formatMessage(experienceMessages.heading)}
469
        </h2>
470
        <p data-c-margin="bottom(1)">
471
          <FormattedMessage
472
            id="profile.experience.preamble"
473
            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."
474
            description="First section of text on the experience step of the Application Timeline."
475
          />
476
        </p>
477
        {/* Experience Modal Buttons */}
478
        <div data-c-grid="gutter(all, 1)">
479
          {Object.values(modalButtons).map((buttonProps) => {
480
            const { id, title, icon } = buttonProps;
481
            return (
482
              <ModalButton
483
                key={id}
484
                id={id}
485
                title={title}
486
                icon={icon}
487
                openModal={openModal}
488
              />
489
            );
490
          })}
491
        </div>
492
        {/* Experience Accordion List */}
493
        {experiences && experiences.length > 0 ? (
494
          <div className="experience-list" data-c-margin="top(2)">
495
            <div data-c-accordion-group>
496
              {experiences.map((experience) => {
497
                const relevantSkills: ExperienceSkill[] = getExperienceSkillsOfExperience(
498
                  experienceSkills,
499
                  experience,
500
                );
501
                const handleEdit = () => editExperience(experience);
502
                const handleDelete = () => deleteExperience(experience);
503
                const errorAccordion = () => (
504
                  <div
505
                    data-c-background="gray(10)"
506
                    data-c-radius="rounded"
507
                    data-c-border="all(thin, solid, gray)"
508
                    data-c-margin="top(1)"
509
                    data-c-padding="all(1)"
510
                  >
511
                    <div data-c-align="base(center)">
512
                      <p data-c-color="stop">
513
                        {intl.formatMessage(
514
                          experienceMessages.errorRenderingExperience,
515
                        )}
516
                      </p>
517
                    </div>
518
                  </div>
519
                );
520
                return (
521
                  profileExperienceAccordion(
522
                    experience,
523
                    relevantSkills,
524
                    skillsById,
525
                    handleEdit,
526
                    handleDelete,
527
                    setEditedExperienceSkillId,
528
                  ) ?? errorAccordion()
529
                );
530
              })}
531
            </div>
532
          </div>
533
        ) : (
534
          <div
535
            data-c-background="gray(10)"
536
            data-c-radius="rounded"
537
            data-c-border="all(thin, solid, gray)"
538
            data-c-margin="top(2)"
539
            data-c-padding="all(1)"
540
          >
541
            <div data-c-align="base(center)">
542
              <p data-c-color="gray">
543
                <FormattedMessage
544
                  id="profile.experience.noExperiences"
545
                  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!"
546
                  description="Message displayed when application has no experiences."
547
                />
548
              </p>
549
            </div>
550
          </div>
551
        )}
552
      </div>
553
      <div
554
        data-c-dialog-overlay={
555
          isModalVisible.visible || editedExperienceSkillId !== null
556
            ? "active"
557
            : ""
558
        }
559
      />
560
      <ProfileEducationModal
561
        educationStatuses={educationStatuses}
562
        educationTypes={educationTypes}
563
        experienceEducation={experienceData as ExperienceEducation}
564
        experienceableId={experienceData?.experienceable_id ?? 0}
565
        experienceableType={
566
          experienceData?.experienceable_type ?? "application"
567
        }
568
        modalId={modalButtons.education.id}
569
        onModalCancel={closeModal}
570
        onModalConfirm={
571
          experienceData === null ? createExperience : updateExperience
572
        }
573
        parentElement={modalRoot}
574
        visible={
575
          isModalVisible.visible &&
576
          isModalVisible.id === modalButtons.education.id
577
        }
578
      />
579
      <ProfileWorkModal
580
        experienceWork={experienceData as ExperienceWork}
581
        experienceableId={experienceData?.experienceable_id ?? 0}
582
        experienceableType={
583
          experienceData?.experienceable_type ?? "application"
584
        }
585
        modalId={modalButtons.work.id}
586
        onModalCancel={closeModal}
587
        onModalConfirm={
588
          experienceData === null ? createExperience : updateExperience
589
        }
590
        parentElement={modalRoot}
591
        visible={
592
          isModalVisible.visible && isModalVisible.id === modalButtons.work.id
593
        }
594
      />
595
      <ProfileCommunityModal
596
        experienceCommunity={experienceData as ExperienceCommunity}
597
        experienceableId={experienceData?.experienceable_id ?? 0}
598
        experienceableType={
599
          experienceData?.experienceable_type ?? "application"
600
        }
601
        modalId={modalButtons.community.id}
602
        onModalCancel={closeModal}
603
        onModalConfirm={
604
          experienceData === null ? createExperience : updateExperience
605
        }
606
        parentElement={modalRoot}
607
        visible={
608
          isModalVisible.visible &&
609
          isModalVisible.id === modalButtons.community.id
610
        }
611
      />
612
      <ProfilePersonalModal
613
        experiencePersonal={experienceData as ExperiencePersonal}
614
        experienceableId={experienceData?.experienceable_id ?? 0}
615
        experienceableType={
616
          experienceData?.experienceable_type ?? "application"
617
        }
618
        modalId={modalButtons.personal.id}
619
        onModalCancel={closeModal}
620
        onModalConfirm={
621
          experienceData === null ? createExperience : updateExperience
622
        }
623
        parentElement={modalRoot}
624
        visible={
625
          isModalVisible.visible &&
626
          isModalVisible.id === modalButtons.personal.id
627
        }
628
      />
629
      <ProfileAwardModal
630
        experienceAward={experienceData as ExperienceAward}
631
        experienceableId={experienceData?.experienceable_id ?? 0}
632
        experienceableType={
633
          experienceData?.experienceable_type ?? "application"
634
        }
635
        modalId={modalButtons.award.id}
636
        onModalCancel={closeModal}
637
        onModalConfirm={
638
          experienceData === null ? createExperience : updateExperience
639
        }
640
        parentElement={modalRoot}
641
        recipientTypes={recipientTypes}
642
        recognitionTypes={recognitionTypes}
643
        visible={
644
          isModalVisible.visible && isModalVisible.id === modalButtons.award.id
645
        }
646
      />
647
      <SkillExperienceModal
648
        experienceSkill={editedExpSkill}
649
        handleCancel={() => setEditedExperienceSkillId(null)}
650
        handleConfirm={async (expSkill): Promise<void> => {
651
          return handleUpdateExperienceSkill(expSkill).then(() => {
652
            setEditedExperienceSkillId(null);
653
          });
654
        }}
655
        handleDelete={async (): Promise<void> => {
656
          if (editedExperienceSkillId !== null) {
657
            const expSkill = find(experienceSkills, editedExperienceSkillId);
658
            if (expSkill) {
659
              return handleDeleteExperienceSkill(expSkill).then(() => {
660
                setEditedExperienceSkillId(null);
661
              });
662
            }
663
          }
664
        }}
665
        experiences={experiences}
666
        skillsById={skillsById}
667
      />
668
    </>
669
  );
670
};
671
672
export default ProfileExperience;
673