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

Complexity

Total Complexity 76
Complexity/F 76

Size

Lines of Code 863
Function Count 1

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

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