Passed
Push — task/hygrogen-react-components ( cc1c06...8a6f45 )
by Yonathan
04:30
created

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

Complexity

Total Complexity 72
Complexity/F 72

Size

Lines of Code 843
Function Count 1

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 72
eloc 702
mnd 71
bc 71
fnc 1
dl 0
loc 843
rs 2.5379
bpm 71
cpm 72
noi 0
c 0
b 0
f 0

1 Function

Rating   Name   Duplication   Size   Complexity  
A Experience.tsx ➔ modalButtonProps 0 29 1

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