Passed
Push — task/ci-browser-test-actions ( 454b63...ccadfd )
by Yonathan
03:57
created

resources/assets/js/components/Application/Experience/Experience.tsx   F

Complexity

Total Complexity 71
Complexity/F 71

Size

Lines of Code 838
Function Count 1

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 71
eloc 692
mnd 70
bc 70
fnc 1
dl 0
loc 838
rs 2.628
bpm 70
cpm 71
noi 0
c 0
b 0
f 0

How to fix   Complexity   

Complexity

Complex classes like resources/assets/js/components/Application/Experience/Experience.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, { useState, useEffect } from "react";
3
import { FormattedMessage, useIntl, IntlShape } from "react-intl";
4
import {
5
  Skill,
6
  ExperienceEducation,
7
  ExperienceWork,
8
  ExperienceCommunity,
9
  Experience,
10
  ExperiencePersonal,
11
  ExperienceAward,
12
  ExperienceSkill,
13
  Criteria,
14
} from "../../../models/types";
15
import { localizeFieldNonNull, getLocale } from "../../../helpers/localize";
16
import { SkillTypeId, CriteriaTypeId } from "../../../models/lookupConstants";
17
import EducationExperienceModal, {
18
  messages as educationMessages,
19
  EducationType,
20
  EducationStatus,
21
  EducationExperienceSubmitData,
22
} from "../ExperienceModals/EducationExperienceModal";
23
24
import WorkExperienceModal, {
25
  messages as workMessages,
26
  WorkExperienceSubmitData,
27
} from "../ExperienceModals/WorkExperienceModal";
28
import CommunityExperienceModal, {
29
  messages as communityMessages,
30
  CommunityExperienceSubmitData,
31
} from "../ExperienceModals/CommunityExperienceModal";
32
import PersonalExperienceModal, {
33
  messages as personalMessages,
34
  PersonalExperienceSubmitData,
35
} from "../ExperienceModals/PersonalExperienceModal";
36
import AwardExperienceModal, {
37
  messages as awardMessages,
38
  AwardRecipientType,
39
  AwardRecognitionType,
40
  AwardExperienceSubmitData,
41
} from "../ExperienceModals/AwardExperienceModal";
42
import { ExperienceEducationAccordion } from "../ExperienceAccordions/ExperienceEducationAccordion";
43
import { ExperienceWorkAccordion } from "../ExperienceAccordions/ExperienceWorkAccordion";
44
import { ExperienceCommunityAccordion } from "../ExperienceAccordions/ExperienceCommunityAccordion";
45
import { ExperiencePersonalAccordion } from "../ExperienceAccordions/ExperiencePersonalAccordion";
46
import { ExperienceAwardAccordion } from "../ExperienceAccordions/ExperienceAwardAccordion";
47
import {
48
  getSkillOfCriteria,
49
  getSkillsOfExperience,
50
  getDisconnectedRequiredSkills,
51
} from "../helpers";
52
import { navigationMessages, experienceMessages } from "../applicationMessages";
53
import { notEmpty, removeDuplicatesById } from "../../../helpers/queries";
54
import { focusOnElement } from "../../../helpers/forms";
55
56
export function modalButtonProps(
57
  intl: IntlShape,
58
): { [key: string]: { id: Experience["type"]; title: string; icon: string } } {
59
  return {
60
    education: {
61
      id: "experience_education",
62
      title: intl.formatMessage(educationMessages.modalTitle),
63
      icon: "fas fa-book",
64
    },
65
    work: {
66
      id: "experience_work",
67
      title: intl.formatMessage(workMessages.modalTitle),
68
      icon: "fas fa-briefcase",
69
    },
70
    community: {
71
      id: "experience_community",
72
      title: intl.formatMessage(communityMessages.modalTitle),
73
      icon: "fas fa-people-carry",
74
    },
75
    personal: {
76
      id: "experience_personal",
77
      title: intl.formatMessage(personalMessages.modalTitle),
78
      icon: "fas fa-mountain",
79
    },
80
    award: {
81
      id: "experience_award",
82
      title: intl.formatMessage(awardMessages.modalTitle),
83
      icon: "fas fa-trophy",
84
    },
85
  };
86
}
87
88
export const ModalButton: React.FunctionComponent<{
89
  id: Experience["type"];
90
  title: string;
91
  icon: string;
92
  openModal: (id: Experience["type"]) => void;
93
}> = ({ id, title, icon, openModal }) => {
94
  return (
95
    <div key={id} data-c-grid-item="base(1of2) tp(1of3) tl(1of5)">
96
      <button
97
        className="application-experience-trigger"
98
        data-c-card
99
        data-c-background="c1(100)"
100
        data-c-radius="rounded"
101
        title={title}
102
        data-c-dialog-id={id}
103
        data-c-dialog-action="open"
104
        type="button"
105
        onClick={(): void => openModal(id)}
106
      >
107
        <i className={icon} aria-hidden="true" />
108
        <span data-c-font-size="regular" data-c-font-weight="bold">
109
          {title}
110
        </span>
111
      </button>
112
    </div>
113
  );
114
};
115
116
const applicationExperienceAccordion = (
117
  experience: Experience,
118
  irrelevantSkillCount: number,
119
  relevantSkills: ExperienceSkill[],
120
  skills: Skill[],
121
  handleEdit: () => void,
122
  handleDelete: () => Promise<void>,
123
): React.ReactElement | null => {
124
  switch (experience.type) {
125
    case "experience_education":
126
      return (
127
        <ExperienceEducationAccordion
128
          key={`${experience.id}-${experience.type}`}
129
          experience={experience}
130
          handleDelete={handleDelete}
131
          handleEdit={handleEdit}
132
          irrelevantSkillCount={irrelevantSkillCount}
133
          relevantSkills={relevantSkills}
134
          skills={skills}
135
          showButtons
136
          showSkillDetails
137
        />
138
      );
139
    case "experience_work":
140
      return (
141
        <ExperienceWorkAccordion
142
          key={`${experience.id}-${experience.type}`}
143
          experience={experience}
144
          handleDelete={handleDelete}
145
          handleEdit={handleEdit}
146
          irrelevantSkillCount={irrelevantSkillCount}
147
          relevantSkills={relevantSkills}
148
          skills={skills}
149
          showButtons
150
          showSkillDetails
151
        />
152
      );
153
    case "experience_community":
154
      return (
155
        <ExperienceCommunityAccordion
156
          key={`${experience.id}-${experience.type}`}
157
          experience={experience}
158
          handleDelete={handleDelete}
159
          handleEdit={handleEdit}
160
          irrelevantSkillCount={irrelevantSkillCount}
161
          relevantSkills={relevantSkills}
162
          skills={skills}
163
          showButtons
164
          showSkillDetails
165
        />
166
      );
167
    case "experience_personal":
168
      return (
169
        <ExperiencePersonalAccordion
170
          key={`${experience.id}-${experience.type}`}
171
          experience={experience}
172
          handleEdit={handleEdit}
173
          handleDelete={handleDelete}
174
          irrelevantSkillCount={irrelevantSkillCount}
175
          relevantSkills={relevantSkills}
176
          skills={skills}
177
          showButtons
178
          showSkillDetails
179
        />
180
      );
181
    case "experience_award":
182
      return (
183
        <ExperienceAwardAccordion
184
          key={`${experience.id}-${experience.type}`}
185
          experience={experience}
186
          handleDelete={handleDelete}
187
          handleEdit={handleEdit}
188
          irrelevantSkillCount={irrelevantSkillCount}
189
          relevantSkills={relevantSkills}
190
          skills={skills}
191
          showButtons
192
          showSkillDetails
193
        />
194
      );
195
    default:
196
      return null;
197
  }
198
};
199
200
export type ExperienceSubmitData =
201
  | EducationExperienceSubmitData
202
  | WorkExperienceSubmitData
203
  | CommunityExperienceSubmitData
204
  | PersonalExperienceSubmitData
205
  | AwardExperienceSubmitData;
206
207
interface ExperienceProps {
208
  assetSkills: Skill[];
209
  disconnectedRequiredSkills: Skill[];
210
  educationStatuses: EducationStatus[];
211
  educationTypes: EducationType[];
212
  hasError?: boolean;
213
  essentialSkills: Skill[];
214
  experiences: Experience[];
215
  experienceSkills: ExperienceSkill[];
216
  jobId: number;
217
  jobEducationRequirements: string | null;
218
  classificationEducationRequirements: string | null;
219
  recipientTypes: AwardRecipientType[];
220
  recognitionTypes: AwardRecognitionType[];
221
  skills: Skill[];
222
  handleDeleteExperience: (
223
    id: number,
224
    type: Experience["type"],
225
  ) => Promise<void>;
226
  handleSubmitExperience: (data: ExperienceSubmitData) => Promise<void>;
227
}
228
229
export const MyExperience: React.FunctionComponent<ExperienceProps> = ({
230
  assetSkills,
231
  disconnectedRequiredSkills,
232
  hasError,
233
  educationStatuses,
234
  educationTypes,
235
  essentialSkills,
236
  experiences,
237
  experienceSkills,
238
  jobId,
239
  jobEducationRequirements,
240
  classificationEducationRequirements,
241
  recipientTypes,
242
  recognitionTypes,
243
  skills,
244
  handleSubmitExperience,
245
  handleDeleteExperience,
246
}) => {
247
  const intl = useIntl();
248
  const locale = getLocale(intl.locale);
249
250
  const [experienceData, setExperienceData] = useState<
251
    | (Experience & {
252
        savedOptionalSkills: Skill[];
253
        savedRequiredSkills: Skill[];
254
      })
255
    | null
256
  >(null);
257
258
  const [isModalVisible, setIsModalVisible] = useState<{
259
    id: Experience["type"] | "";
260
    visible: boolean;
261
  }>({
262
    id: "",
263
    visible: false,
264
  });
265
266
  const openModal = (id: Experience["type"]): void => {
267
    setIsModalVisible({ id, visible: true });
268
  };
269
270
  const closeModal = (): void => {
271
    setExperienceData(null);
272
    setIsModalVisible({ id: "", visible: false });
273
  };
274
275
  const submitExperience = (data) =>
276
    handleSubmitExperience(data).then(closeModal);
277
278
  const editExperience = (
279
    experience: Experience,
280
    savedOptionalSkills: Skill[],
281
    savedRequiredSkills: Skill[],
282
  ): void => {
283
    setExperienceData({
284
      ...experience,
285
      savedOptionalSkills,
286
      savedRequiredSkills,
287
    });
288
    setIsModalVisible({ id: experience.type, visible: true });
289
  };
290
291
  const deleteExperience = (experience: Experience): Promise<void> =>
292
    handleDeleteExperience(experience.id, experience.type).then(closeModal);
293
294
  const softSkills = removeDuplicatesById(
295
    [...assetSkills, ...essentialSkills].filter(
296
      (skill) => skill.skill_type_id === SkillTypeId.Soft,
297
    ),
298
  );
299
300
  const modalButtons = modalButtonProps(intl);
301
  const modalRoot = document.getElementById("modal-root");
302
303
  return (
304
    <>
305
      <div data-c-container="medium">
306
        <h2 data-c-heading="h2" data-c-margin="top(3) bottom(1)">
307
          {intl.formatMessage(experienceMessages.heading)}
308
        </h2>
309
        <p data-c-margin="bottom(1)">
310
          <FormattedMessage
311
            id="application.experience.preamble"
312
            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."
313
            description="First section of text on the experience step of the Application Timeline."
314
          />
315
        </p>
316
317
        <div data-c-grid="gutter(all, 1)">
318
          {essentialSkills.length > 0 && (
319
            <div data-c-grid-item="tl(1of2)">
320
              <p data-c-margin="bottom(.5)">
321
                <FormattedMessage
322
                  id="application.experience.essentialSkillsListIntro"
323
                  description="Text before the list of essential skills on the experience step of the Application Timeline."
324
                  defaultMessage="This job <span>requires</span> the following skills:"
325
                  values={{
326
                    span: (chunks): React.ReactElement => (
327
                      <span data-c-font-weight="bold" data-c-color="c2">
328
                        {chunks}
329
                      </span>
330
                    ),
331
                  }}
332
                />
333
              </p>
334
              <ul data-c-margin="bottom(1)">
335
                {essentialSkills.map((skill) => (
336
                  <li key={skill.id}>
337
                    {localizeFieldNonNull(locale, skill, "name")}
338
                  </li>
339
                ))}
340
              </ul>
341
            </div>
342
          )}
343
          {assetSkills.length > 0 && (
344
            <div data-c-grid-item="tl(1of2)">
345
              <p data-c-margin="bottom(.5)">
346
                <FormattedMessage
347
                  id="application.experience.assetSkillsListIntro"
348
                  defaultMessage="These skills are beneficial, but not required:"
349
                  description="Text before the list of asset skills on the experience step of the Application Timeline."
350
                />
351
              </p>
352
              <ul data-c-margin="bottom(1)">
353
                {assetSkills.map((skill) => (
354
                  <li key={skill.id}>
355
                    {localizeFieldNonNull(locale, skill, "name")}
356
                  </li>
357
                ))}
358
              </ul>
359
            </div>
360
          )}
361
        </div>
362
        <p data-c-color="gray" data-c-margin="bottom(2)">
363
          <FormattedMessage
364
            id="application.experience.softSkillsList"
365
            defaultMessage="Don't forget, {skill} will be evaluated later in the hiring process."
366
            description="List of soft skills that will be evaluated later."
367
            values={{
368
              skill: (
369
                <>
370
                  {softSkills.map((skill, index) => {
371
                    const and = " and ";
372
                    const lastElement = index === softSkills.length - 1;
373
                    return (
374
                      <React.Fragment key={skill.id}>
375
                        {lastElement && softSkills.length > 1 && and}
376
                        <span key={skill.id} data-c-font-weight="bold">
377
                          {localizeFieldNonNull(locale, skill, "name")}
378
                        </span>
379
                        {!lastElement && softSkills.length > 2 && ", "}
380
                      </React.Fragment>
381
                    );
382
                  })}
383
                </>
384
              ),
385
            }}
386
          />
387
        </p>
388
        {hasError && (
389
          <div
390
            // This alert message needs the tabindex attribute for it to be focusable.
391
            // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
392
            tabIndex={0}
393
            data-c-alert="error"
394
            data-c-radius="rounded"
395
            role="alert"
396
            data-c-margin="bottom(1)"
397
            id="experience-step-form-error"
398
            style={{ display: "block" }}
399
          >
400
            <div data-c-padding="all(.5)">
401
              <p data-c-alignment="base(left)">
402
                <FormattedMessage
403
                  id="application.experience.errorMessage"
404
                  defaultMessage="To continue, please connect the following required skill(s) to an experience:"
405
                  description="Error message displayed to user when experience step validation fails"
406
                />
407
                {` `}
408
                {disconnectedRequiredSkills.map((skill, index) => {
409
                  const and = " and ";
410
                  const lastElement =
411
                    index === disconnectedRequiredSkills.length - 1;
412
                  return (
413
                    <React.Fragment key={skill.id}>
414
                      {lastElement &&
415
                        disconnectedRequiredSkills.length > 1 &&
416
                        and}
417
                      <span key={skill.id} data-c-font-weight="bold">
418
                        {localizeFieldNonNull(locale, skill, "name")}
419
                      </span>
420
                      {!lastElement &&
421
                        disconnectedRequiredSkills.length > 2 &&
422
                        ", "}
423
                    </React.Fragment>
424
                  );
425
                })}
426
              </p>
427
            </div>
428
          </div>
429
        )}
430
        {/* Experience Modal Buttons */}
431
        <div data-c-grid="gutter(all, 1)">
432
          {Object.values(modalButtons).map((buttonProps) => {
433
            const { id, title, icon } = buttonProps;
434
            return (
435
              <ModalButton
436
                key={id}
437
                id={id}
438
                title={title}
439
                icon={icon}
440
                openModal={openModal}
441
              />
442
            );
443
          })}
444
        </div>
445
        {/* Experience Accordion List */}
446
        {experiences && experiences.length > 0 ? (
447
          <div className="experience-list" data-c-margin="top(2)">
448
            <div data-c-accordion-group>
449
              {experiences.map((experience) => {
450
                const savedOptionalSkills = getSkillsOfExperience(
451
                  experienceSkills,
452
                  experience,
453
                  assetSkills,
454
                );
455
                const savedRequiredSkills = getSkillsOfExperience(
456
                  experienceSkills,
457
                  experience,
458
                  essentialSkills,
459
                );
460
                const relevantSkills: ExperienceSkill[] = savedRequiredSkills
461
                  .map((skill) => {
462
                    return experienceSkills.find(
463
                      ({ experience_id, experience_type, skill_id }) =>
464
                        experience_id === experience.id &&
465
                        skill_id === skill.id &&
466
                        experience_type === experience.type,
467
                    );
468
                  })
469
                  .filter(notEmpty);
470
471
                const handleEdit = () =>
472
                  editExperience(
473
                    experience,
474
                    savedOptionalSkills,
475
                    savedRequiredSkills,
476
                  );
477
                const handleDelete = () => deleteExperience(experience);
478
479
                const errorAccordion = () => (
480
                  <div
481
                    data-c-background="gray(10)"
482
                    data-c-radius="rounded"
483
                    data-c-border="all(thin, solid, gray)"
484
                    data-c-margin="top(1)"
485
                    data-c-padding="all(1)"
486
                  >
487
                    <div data-c-align="base(center)">
488
                      <p data-c-color="stop">
489
                        {intl.formatMessage(
490
                          experienceMessages.errorRenderingExperience,
491
                        )}
492
                      </p>
493
                    </div>
494
                  </div>
495
                );
496
497
                // Number of skills attached to Experience but are not part of the jobs skill criteria.
498
                const irrelevantSkillCount =
499
                  experienceSkills.filter(
500
                    (experienceSkill) =>
501
                      experienceSkill.experience_id === experience.id &&
502
                      experienceSkill.experience_type === experience.type,
503
                  ).length -
504
                  (savedOptionalSkills.length + savedRequiredSkills.length);
505
506
                return (
507
                  applicationExperienceAccordion(
508
                    experience,
509
                    irrelevantSkillCount,
510
                    relevantSkills,
511
                    skills,
512
                    handleEdit,
513
                    handleDelete,
514
                  ) ?? errorAccordion()
515
                );
516
              })}
517
            </div>
518
          </div>
519
        ) : (
520
          <div
521
            data-c-background="gray(10)"
522
            data-c-radius="rounded"
523
            data-c-border="all(thin, solid, gray)"
524
            data-c-margin="top(2)"
525
            data-c-padding="all(1)"
526
          >
527
            <div data-c-align="base(center)">
528
              <p data-c-color="gray">
529
                <FormattedMessage
530
                  id="application.experience.noExperiences"
531
                  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!"
532
                  description="Message displayed when application has no experiences."
533
                />
534
              </p>
535
            </div>
536
          </div>
537
        )}
538
        {disconnectedRequiredSkills && disconnectedRequiredSkills.length > 0 && (
539
          <p data-c-color="stop" data-c-margin="top(2)">
540
            <FormattedMessage
541
              id="application.experience.unconnectedSkills"
542
              defaultMessage="The following required skill(s) are not connected to your experience:"
543
              description="Message showing list of required skills that are not connected to a experience."
544
            />{" "}
545
            {disconnectedRequiredSkills.map((skill) => (
546
              <React.Fragment key={skill.id}>
547
                <span
548
                  data-c-tag="stop"
549
                  data-c-radius="pill"
550
                  data-c-font-size="small"
551
                >
552
                  {localizeFieldNonNull(locale, skill, "name")}
553
                </span>{" "}
554
              </React.Fragment>
555
            ))}
556
          </p>
557
        )}
558
      </div>
559
560
      <div data-c-dialog-overlay={isModalVisible.visible ? "active" : ""} />
561
      <EducationExperienceModal
562
        educationStatuses={educationStatuses}
563
        educationTypes={educationTypes}
564
        experienceEducation={experienceData as ExperienceEducation}
565
        experienceableId={experienceData?.experienceable_id ?? 0}
566
        experienceableType={
567
          experienceData?.experienceable_type ?? "application"
568
        }
569
        jobId={jobId}
570
        classificationEducationRequirements={
571
          classificationEducationRequirements
572
        }
573
        jobEducationRequirements={jobEducationRequirements}
574
        modalId={modalButtons.education.id}
575
        onModalCancel={closeModal}
576
        onModalConfirm={submitExperience}
577
        optionalSkills={assetSkills}
578
        parentElement={modalRoot}
579
        requiredSkills={essentialSkills}
580
        savedOptionalSkills={experienceData?.savedOptionalSkills ?? []}
581
        savedRequiredSkills={experienceData?.savedRequiredSkills ?? []}
582
        visible={
583
          isModalVisible.visible &&
584
          isModalVisible.id === modalButtons.education.id
585
        }
586
      />
587
      <WorkExperienceModal
588
        experienceWork={experienceData as ExperienceWork}
589
        experienceableId={experienceData?.experienceable_id ?? 0}
590
        experienceableType={
591
          experienceData?.experienceable_type ?? "application"
592
        }
593
        jobId={jobId}
594
        classificationEducationRequirements={
595
          classificationEducationRequirements
596
        }
597
        jobEducationRequirements={jobEducationRequirements}
598
        modalId={modalButtons.work.id}
599
        onModalCancel={closeModal}
600
        onModalConfirm={submitExperience}
601
        optionalSkills={assetSkills}
602
        parentElement={modalRoot}
603
        requiredSkills={essentialSkills}
604
        savedOptionalSkills={experienceData?.savedOptionalSkills ?? []}
605
        savedRequiredSkills={experienceData?.savedRequiredSkills ?? []}
606
        visible={
607
          isModalVisible.visible && isModalVisible.id === modalButtons.work.id
608
        }
609
      />
610
      <CommunityExperienceModal
611
        experienceCommunity={experienceData as ExperienceCommunity}
612
        experienceableId={experienceData?.experienceable_id ?? 0}
613
        experienceableType={
614
          experienceData?.experienceable_type ?? "application"
615
        }
616
        jobId={jobId}
617
        classificationEducationRequirements={
618
          classificationEducationRequirements
619
        }
620
        jobEducationRequirements={jobEducationRequirements}
621
        modalId={modalButtons.community.id}
622
        onModalCancel={closeModal}
623
        onModalConfirm={submitExperience}
624
        optionalSkills={assetSkills}
625
        parentElement={modalRoot}
626
        requiredSkills={essentialSkills}
627
        savedOptionalSkills={experienceData?.savedOptionalSkills ?? []}
628
        savedRequiredSkills={experienceData?.savedRequiredSkills ?? []}
629
        visible={
630
          isModalVisible.visible &&
631
          isModalVisible.id === modalButtons.community.id
632
        }
633
      />
634
      <PersonalExperienceModal
635
        experiencePersonal={experienceData as ExperiencePersonal}
636
        experienceableId={experienceData?.experienceable_id ?? 0}
637
        experienceableType={
638
          experienceData?.experienceable_type ?? "application"
639
        }
640
        jobId={jobId}
641
        classificationEducationRequirements={
642
          classificationEducationRequirements
643
        }
644
        jobEducationRequirements={jobEducationRequirements}
645
        modalId={modalButtons.personal.id}
646
        onModalCancel={closeModal}
647
        onModalConfirm={submitExperience}
648
        optionalSkills={assetSkills}
649
        parentElement={modalRoot}
650
        requiredSkills={essentialSkills}
651
        savedOptionalSkills={experienceData?.savedOptionalSkills ?? []}
652
        savedRequiredSkills={experienceData?.savedRequiredSkills ?? []}
653
        visible={
654
          isModalVisible.visible &&
655
          isModalVisible.id === modalButtons.personal.id
656
        }
657
      />
658
      <AwardExperienceModal
659
        experienceAward={experienceData as ExperienceAward}
660
        experienceableId={experienceData?.experienceable_id ?? 0}
661
        experienceableType={
662
          experienceData?.experienceable_type ?? "application"
663
        }
664
        jobId={jobId}
665
        classificationEducationRequirements={
666
          classificationEducationRequirements
667
        }
668
        jobEducationRequirements={jobEducationRequirements}
669
        modalId={modalButtons.award.id}
670
        onModalCancel={closeModal}
671
        onModalConfirm={submitExperience}
672
        optionalSkills={assetSkills}
673
        parentElement={modalRoot}
674
        recipientTypes={recipientTypes}
675
        recognitionTypes={recognitionTypes}
676
        requiredSkills={essentialSkills}
677
        savedOptionalSkills={experienceData?.savedOptionalSkills ?? []}
678
        savedRequiredSkills={experienceData?.savedRequiredSkills ?? []}
679
        visible={
680
          isModalVisible.visible && isModalVisible.id === modalButtons.award.id
681
        }
682
      />
683
    </>
684
  );
685
};
686
687
interface ExperienceStepProps {
688
  experiences: Experience[];
689
  educationStatuses: EducationStatus[];
690
  educationTypes: EducationType[];
691
  experienceSkills: ExperienceSkill[];
692
  criteria: Criteria[];
693
  skills: Skill[];
694
  jobId: number;
695
  jobEducationRequirements: string | null;
696
  classificationEducationRequirements: string | null;
697
  recipientTypes: AwardRecipientType[];
698
  recognitionTypes: AwardRecognitionType[];
699
  handleDeleteExperience: (
700
    id: number,
701
    type: Experience["type"],
702
  ) => Promise<void>;
703
  handleSubmitExperience: (data: ExperienceSubmitData) => Promise<void>;
704
  handleContinue: () => void;
705
  handleQuit: () => void;
706
  handleReturn: () => void;
707
}
708
709
export const ExperienceStep: React.FunctionComponent<ExperienceStepProps> = ({
710
  experiences,
711
  educationStatuses,
712
  educationTypes,
713
  experienceSkills,
714
  criteria,
715
  skills,
716
  handleSubmitExperience,
717
  handleDeleteExperience,
718
  jobId,
719
  jobEducationRequirements,
720
  classificationEducationRequirements,
721
  recipientTypes,
722
  recognitionTypes,
723
  handleContinue,
724
  handleQuit,
725
  handleReturn,
726
}) => {
727
  const intl = useIntl();
728
  const [hasError, setHasError] = useState(false);
729
730
  // Hack solution for experience step validation error message: focusOnElement is called in the onClick method (line 830) before the element is added to the dom. Therefore, the useEffect hook is needed for the first focus, after hasError triggers re-render.
731
  useEffect(() => {
732
    if (hasError) {
733
      focusOnElement("experience-step-form-error");
734
    }
735
  }, [hasError]);
736
737
  const filteredSkills = criteria.reduce(
738
    (result, criterion): { essential: Skill[]; asset: Skill[] } => {
739
      const skillOfCriterion = getSkillOfCriteria(criterion, skills);
740
      if (skillOfCriterion) {
741
        if (criterion.criteria_type_id === CriteriaTypeId.Essential) {
742
          result.essential.push(skillOfCriterion);
743
        }
744
        if (criterion.criteria_type_id === CriteriaTypeId.Asset) {
745
          result.asset.push(skillOfCriterion);
746
        }
747
      }
748
      return result;
749
    },
750
    { essential: [], asset: [] } as { essential: Skill[]; asset: Skill[] },
751
  );
752
753
  const essentialSkills = removeDuplicatesById(filteredSkills.essential);
754
  const assetSkills = removeDuplicatesById(filteredSkills.asset);
755
756
  const disconnectedRequiredSkills = getDisconnectedRequiredSkills(
757
    experiences,
758
    experienceSkills,
759
    essentialSkills,
760
  );
761
762
  return (
763
    <>
764
      <MyExperience
765
        assetSkills={assetSkills}
766
        disconnectedRequiredSkills={disconnectedRequiredSkills}
767
        hasError={hasError}
768
        experiences={experiences}
769
        educationStatuses={educationStatuses}
770
        educationTypes={educationTypes}
771
        experienceSkills={experienceSkills}
772
        essentialSkills={essentialSkills}
773
        skills={skills}
774
        jobId={jobId}
775
        jobEducationRequirements={jobEducationRequirements}
776
        classificationEducationRequirements={
777
          classificationEducationRequirements
778
        }
779
        recipientTypes={recipientTypes}
780
        recognitionTypes={recognitionTypes}
781
        handleSubmitExperience={handleSubmitExperience}
782
        handleDeleteExperience={handleDeleteExperience}
783
      />
784
      <div data-c-container="medium" data-c-padding="tb(2)">
785
        <hr data-c-hr="thin(c1)" data-c-margin="bottom(2)" />
786
        <div data-c-grid="gutter">
787
          <div
788
            data-c-alignment="base(centre) tp(left)"
789
            data-c-grid-item="tp(1of2)"
790
          >
791
            <button
792
              data-c-button="outline(c2)"
793
              data-c-radius="rounded"
794
              type="button"
795
              onClick={(): void => handleReturn()}
796
            >
797
              {intl.formatMessage(navigationMessages.return)}
798
            </button>
799
          </div>
800
          <div
801
            data-c-alignment="base(centre) tp(right)"
802
            data-c-grid-item="tp(1of2)"
803
          >
804
            <button
805
              data-c-button="outline(c2)"
806
              data-c-radius="rounded"
807
              type="button"
808
              onClick={(): void => handleQuit()}
809
            >
810
              {intl.formatMessage(navigationMessages.quit)}
811
            </button>
812
            <button
813
              data-c-button="solid(c1)"
814
              data-c-radius="rounded"
815
              data-c-margin="left(1)"
816
              type="button"
817
              onClick={(): void => {
818
                // If all required skills have been connected to an experience, then continue.
819
                // Else, show error alert.
820
                if (disconnectedRequiredSkills.length === 0) {
821
                  handleContinue();
822
                } else {
823
                  setHasError(true);
824
                  focusOnElement("experience-step-form-error");
825
                }
826
              }}
827
            >
828
              {intl.formatMessage(navigationMessages.continue)}
829
            </button>
830
          </div>
831
        </div>
832
      </div>
833
    </>
834
  );
835
};
836
837
export default ExperienceStep;
838