Passed
Push — task/skillcategory-api ( 08d836...b0c2ad )
by
unknown
04:25
created

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

Complexity

Total Complexity 49
Complexity/F 0

Size

Lines of Code 643
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

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