Passed
Push — dev ( 296074...090c7f )
by
unknown
04:50
created

resources/assets/js/components/Application/Skills/Skills.tsx   A

Complexity

Total Complexity 20
Complexity/F 0

Size

Lines of Code 846
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 20
eloc 677
mnd 20
bc 20
fnc 0
dl 0
loc 846
rs 9.923
bpm 0
cpm 0
noi 0
c 0
b 0
f 0
1
/* eslint-disable no-unused-vars, @typescript-eslint/no-unused-vars */
2
/* eslint camelcase: "off", @typescript-eslint/camelcase: "off" */
3
import React, {
4
  useState,
5
  useReducer,
6
  useRef,
7
  createRef,
8
  RefObject,
9
} from "react";
10
import { FormattedMessage, useIntl, IntlShape } from "react-intl";
11
import { Formik, Form, FastField, FormikProps } from "formik";
12
import * as Yup from "yup";
13
import Swal, { SweetAlertResult } from "sweetalert2";
14
import {
15
  ExperienceSkill,
16
  Skill,
17
  Criteria,
18
  Experience,
19
} from "../../../models/types";
20
import { slugify, applicantFaq } from "../../../helpers/routes";
21
import { getLocale, localizeFieldNonNull } from "../../../helpers/localize";
22
import { validationMessages } from "../../Form/Messages";
23
import {
24
  getExperienceHeading,
25
  getExperienceSubheading,
26
  getExperienceJustificationLabel,
27
} from "../../../models/localizedConstants";
28
import { getSkillLevelName } from "../../../models/jobUtil";
29
import StatusIcon, { IconStatus } from "../../StatusIcon";
30
import AlertWhenUnsaved from "../../Form/AlertWhenUnsaved";
31
import TextAreaInput from "../../Form/TextAreaInput";
32
import WordCounter from "../../WordCounter/WordCounter";
33
import { countNumberOfWords } from "../../WordCounter/helpers";
34
import { navigationMessages, skillMessages } from "../applicationMessages";
35
import displayMessages from "./skillsMessages";
36
import {
37
  getSkillOfCriteria,
38
  getExperiencesOfSkill,
39
  getExperienceOfExperienceSkills,
40
} from "../helpers";
41
import {
42
  statusReducer,
43
  initialStatus,
44
  computeParentStatus,
45
  SkillStatus,
46
  computeExperienceStatus,
47
} from "./skillsHelpers";
48
import Modal from "../../Modal";
49
import {
50
  validateAllForms,
51
  submitAllForms,
52
  focusOnElement,
53
} from "../../../helpers/forms";
54
import {
55
  find,
56
  getId,
57
  mapToObjectTrans,
58
  notEmpty,
59
} from "../../../helpers/queries";
60
61
const JUSTIFICATION_WORD_LIMIT = 100;
62
63
interface SidebarProps {
64
  menuSkills: { [skillId: number]: string };
65
  intl: IntlShape;
66
  status: SkillStatus;
67
}
68
69
const Sidebar: React.FC<SidebarProps> = ({ menuSkills, intl, status }) => {
70
  return (
71
    <div data-c-padding="top(3)" className="application-skill-navigation">
72
      <p
73
        data-c-font-size="h3"
74
        data-c-font-weight="bold"
75
        data-c-margin="bottom(1)"
76
      >
77
        <FormattedMessage
78
          id="application.skills.sidebarHeading"
79
          defaultMessage="On this page:"
80
          description="Heading for the sidebar on the Skills page."
81
        />
82
      </p>
83
      <ul>
84
        {Object.keys(menuSkills).map((skillId) => (
85
          <li key={skillId}>
86
            <StatusIcon
87
              status={computeParentStatus(status, Number(skillId))}
88
              size=""
89
            />
90
            <a
91
              href={`#${slugify(menuSkills[skillId])}`}
92
              title={intl.formatMessage(displayMessages.sidebarLinkTitle)}
93
            >
94
              {menuSkills[skillId]}
95
            </a>
96
          </li>
97
        ))}
98
      </ul>
99
    </div>
100
  );
101
};
102
103
interface ExperienceSkillAccordionProps {
104
  experience: Experience;
105
  experienceSkill: ExperienceSkill;
106
  intl: IntlShape;
107
  status: IconStatus;
108
  skillName: string;
109
  isExpanded: boolean;
110
  setIsExpanded: (value: boolean) => void;
111
  formRef: RefObject<FormikProps<ExperienceSkillFormValues>>;
112
  handleUpdateExperienceJustification: (
113
    experience: ExperienceSkill,
114
  ) => Promise<ExperienceSkill>;
115
  handleUpdateStatus: (action: {
116
    payload: {
117
      skillId: number;
118
      experienceId: number;
119
      experienceType: string;
120
      status: IconStatus;
121
    };
122
  }) => void;
123
  handleRemoveExperience: (experience: ExperienceSkill) => Promise<void>;
124
}
125
126
interface ExperienceSkillFormValues {
127
  justification: string;
128
}
129
130
const ExperienceSkillAccordion: React.FC<ExperienceSkillAccordionProps> = ({
131
  experience,
132
  experienceSkill,
133
  intl,
134
  status,
135
  skillName,
136
  isExpanded,
137
  setIsExpanded,
138
  handleUpdateExperienceJustification,
139
  handleUpdateStatus,
140
  handleRemoveExperience,
141
  formRef,
142
}) => {
143
  let heading = "";
144
  let subHeading = "";
145
  let label = "";
146
147
  const initialValues: ExperienceSkillFormValues = {
148
    justification: experienceSkill.justification || "",
149
  };
150
151
  const experienceSkillSchema = Yup.object().shape({
152
    justification: Yup.string()
153
      .test(
154
        "wordCount",
155
        intl.formatMessage(validationMessages.overMaxWords, {
156
          numberOfWords: JUSTIFICATION_WORD_LIMIT,
157
        }),
158
        (value: string) =>
159
          countNumberOfWords(value) <= JUSTIFICATION_WORD_LIMIT,
160
      )
161
      .required(intl.formatMessage(validationMessages.required)),
162
  });
163
164
  if (experience !== null) {
165
    heading = getExperienceHeading(experience, intl);
166
    subHeading = getExperienceSubheading(experience, intl);
167
    label = getExperienceJustificationLabel(experience, intl, skillName);
168
  }
169
170
  const handleExpandClick = (): void => {
171
    setIsExpanded(!isExpanded);
172
  };
173
174
  const handleRemoveButtonClick = (): void => {
175
    Swal.fire({
176
      title: intl.formatMessage(displayMessages.modalConfirmHeading),
177
      text: intl.formatMessage(displayMessages.modalConfirmBody),
178
      icon: "warning",
179
      showCancelButton: true,
180
      confirmButtonColor: "#0A6CBC",
181
      cancelButtonColor: "#F94D4D",
182
      cancelButtonText: intl.formatMessage(displayMessages.cancel),
183
      confirmButtonText: intl.formatMessage(displayMessages.confirm),
184
    }).then((result: SweetAlertResult) => {
185
      if (result && result.value !== undefined) {
186
        handleRemoveExperience(experienceSkill);
187
      }
188
    });
189
  };
190
191
  const updateExperienceSkill = (
192
    oldExperienceSkill: ExperienceSkill,
193
    values: ExperienceSkillFormValues,
194
  ): ExperienceSkill => {
195
    const experienceJustification: ExperienceSkill = {
196
      ...oldExperienceSkill,
197
      justification: values.justification || "",
198
    };
199
    return experienceJustification;
200
  };
201
202
  return (
203
    <div
204
      data-c-accordion
205
      data-c-background="white(100)"
206
      data-c-card=""
207
      data-c-margin="bottom(.5)"
208
      className={`application-skill-explanation${isExpanded ? " active" : ""}`}
209
    >
210
      <button
211
        aria-expanded={isExpanded ? "true" : "false"}
212
        data-c-accordion-trigger
213
        tabIndex={0}
214
        type="button"
215
        onClick={handleExpandClick}
216
      >
217
        <div data-c-grid="">
218
          <div data-c-grid-item="base(1of4) tl(1of6) equal-col">
219
            <div className="skill-status-indicator">
220
              <StatusIcon status={status} size="h4" />
221
            </div>
222
          </div>
223
          <div data-c-grid-item="base(3of4) tl(5of6)">
224
            <div data-c-padding="all(1)">
225
              <div data-c-grid="middle">
226
                <div data-c-grid-item="tl(3of4)">
227
                  <p>{heading}</p>
228
                  <p
229
                    data-c-margin="top(quarter)"
230
                    data-c-colour="c1"
231
                    data-c-font-size="small"
232
                  >
233
                    {subHeading}
234
                  </p>
235
                </div>
236
                <div
237
                  data-c-grid-item="tl(1of4)"
238
                  data-c-align="base(left) tl(center)"
239
                >
240
                  {(experienceSkill.justification === null ||
241
                    experienceSkill.justification.length === 0) && (
242
                    <span data-c-color="stop" className="missing-info">
243
                      <FormattedMessage
244
                        id="application.skills.justificationMissing"
245
                        defaultMessage="Missing Information"
246
                        description="Accordion heading error that displays when the justification is empty."
247
                      />
248
                    </span>
249
                  )}
250
                </div>
251
              </div>
252
            </div>
253
          </div>
254
        </div>
255
        <span data-c-visibility="invisible">
256
          {intl.formatMessage(displayMessages.accessibleAccordionButtonText)}
257
        </span>
258
        {isExpanded ? (
259
          <i
260
            aria-hidden="true"
261
            className="fas fa-angle-up"
262
            data-c-colour="black"
263
            data-c-accordion-remove=""
264
          />
265
        ) : (
266
          <i
267
            aria-hidden="true"
268
            className="fas fa-angle-down"
269
            data-c-colour="black"
270
            data-c-accordion-add=""
271
          />
272
        )}
273
      </button>
274
      <Formik
275
        innerRef={formRef}
276
        initialValues={initialValues}
277
        validationSchema={experienceSkillSchema}
278
        onSubmit={(values, { setSubmitting, resetForm }): Promise<void> => {
279
          const experienceSkillJustification = updateExperienceSkill(
280
            experienceSkill,
281
            values,
282
          );
283
          return handleUpdateExperienceJustification(
284
            experienceSkillJustification,
285
          )
286
            .then(() => {
287
              handleUpdateStatus({
288
                payload: {
289
                  skillId: experienceSkill.skill_id,
290
                  experienceId: experienceSkill.experience_id,
291
                  experienceType: experienceSkill.experience_type,
292
                  status: IconStatus.COMPLETE,
293
                },
294
              });
295
              setSubmitting(false);
296
              resetForm();
297
            })
298
            .catch(() => {
299
              setSubmitting(false);
300
            });
301
        }}
302
      >
303
        {({ dirty, isSubmitting, isValid, submitForm }): React.ReactElement => (
304
          <div
305
            aria-hidden={isExpanded ? "false" : "true"}
306
            data-c-accordion-content=""
307
            data-c-background="gray(10)"
308
          >
309
            <Form>
310
              <AlertWhenUnsaved />
311
              <hr data-c-hr="thin(gray)" data-c-margin="bottom(1)" />
312
              <div data-c-padding="lr(1)">
313
                <FastField
314
                  id={`experience-skill-textarea-${experienceSkill.id}`}
315
                  name="justification"
316
                  label={label}
317
                  component={TextAreaInput}
318
                  placeholder={intl.formatMessage(
319
                    skillMessages.experienceSkillPlaceholder,
320
                  )}
321
                  required
322
                />
323
              </div>
324
              <div data-c-padding="all(1)">
325
                <div data-c-grid="gutter(all, 1) middle">
326
                  <div
327
                    data-c-grid-item="tp(1of2)"
328
                    data-c-align="base(center) tp(left)"
329
                  >
330
                    <button
331
                      data-c-button="outline(c1)"
332
                      data-c-radius="rounded"
333
                      type="button"
334
                      onClick={handleRemoveButtonClick}
335
                    >
336
                      <span>
337
                        <FormattedMessage
338
                          id="application.skills.deleteExperienceButtonText"
339
                          defaultMessage="Remove Experience From Skill"
340
                          description="Text for the delete experience button."
341
                        />
342
                      </span>
343
                    </button>
344
                  </div>
345
                  <div
346
                    data-c-grid-item="tp(1of2)"
347
                    data-c-align="base(center) tp(right)"
348
                  >
349
                    <WordCounter
350
                      elementId={`experience-skill-textarea-${experienceSkill.id}`}
351
                      maxWords={JUSTIFICATION_WORD_LIMIT}
352
                      minWords={0}
353
                      absoluteValue
354
                      dataAttributes={{ "data-c-margin": "right(1)" }}
355
                      underMaxMessage={intl.formatMessage(
356
                        displayMessages.wordCountUnderMax,
357
                      )}
358
                      overMaxMessage={intl.formatMessage(
359
                        displayMessages.wordCountOverMax,
360
                      )}
361
                    />
362
                    <button
363
                      data-c-button="solid(c1)"
364
                      data-c-radius="rounded"
365
                      type="button"
366
                      disabled={!dirty || isSubmitting}
367
                      onClick={(): void => {
368
                        if (!isValid) {
369
                          handleUpdateStatus({
370
                            payload: {
371
                              skillId: experienceSkill.skill_id,
372
                              experienceId: experienceSkill.experience_id,
373
                              experienceType: experienceSkill.experience_type,
374
                              status: IconStatus.ERROR,
375
                            },
376
                          });
377
                        } else {
378
                          submitForm();
379
                        }
380
                      }}
381
                    >
382
                      <span>
383
                        {dirty
384
                          ? intl.formatMessage(displayMessages.save)
385
                          : intl.formatMessage(displayMessages.saved)}
386
                      </span>
387
                    </button>
388
                  </div>
389
                </div>
390
              </div>
391
            </Form>
392
          </div>
393
        )}
394
      </Formik>
395
    </div>
396
  );
397
};
398
399
interface SkillsProps {
400
  criteria: Criteria[];
401
  experiences: Experience[];
402
  experienceSkills: ExperienceSkill[];
403
  skills: Skill[];
404
  handleUpdateExperienceJustification: (
405
    experience: ExperienceSkill,
406
  ) => Promise<ExperienceSkill>;
407
  handleRemoveExperienceJustification: (
408
    experience: ExperienceSkill,
409
  ) => Promise<void>;
410
  handleContinue: () => void;
411
  handleQuit: () => void;
412
  handleReturn: () => void;
413
}
414
415
const Skills: React.FC<SkillsProps> = ({
416
  criteria,
417
  experiences,
418
  experienceSkills,
419
  skills,
420
  handleUpdateExperienceJustification,
421
  handleRemoveExperienceJustification,
422
  handleContinue,
423
  handleQuit,
424
  handleReturn,
425
}) => {
426
  const intl = useIntl();
427
  const locale = getLocale(intl.locale);
428
  const initial = initialStatus(experienceSkills, JUSTIFICATION_WORD_LIMIT);
429
430
  const [status, dispatchStatus] = useReducer(statusReducer, initial);
431
432
  const modalId = "skill-description";
433
  const [visible, setVisible] = useState(false);
434
  const [modalHeading, setModalHeading] = useState("");
435
  const [modalBody, setModalBody] = useState("");
436
  const modalParentRef = useRef<HTMLDivElement>(null);
437
438
  // Maps ExperienceSkill ids to their accordion expansion state.
439
  const [accordionExpansions, setAccordionExpansions] = useState<{
440
    [experienceSkillId: number]: boolean;
441
  }>(mapToObjectTrans(experienceSkills, getId, () => false));
442
443
  const menuSkills = criteria.reduce(
444
    (collection: { [skillId: number]: string }, criterion: Criteria) => {
445
      const skill = getSkillOfCriteria(criterion, skills);
446
      if (skill && !collection[criterion.skill_id]) {
447
        // eslint-disable-next-line no-param-reassign
448
        collection[criterion.skill_id] = localizeFieldNonNull(
449
          locale,
450
          skill,
451
          "name",
452
        );
453
      }
454
      return collection;
455
    },
456
    [],
457
  );
458
459
  const [isSubmitting, setIsSubmitting] = useState(false);
460
  /**
461
   * Validate many Formik forms, and submit all of them if all are valid.
462
   * @param refMap A map of experienceSkill id to formik Form ref
463
   * @returns A promise that resolves if submission succeeds, and rejects if validation or submission fails.
464
   */
465
  const validateAndSubmitMany = async (
466
    refMap: Map<
467
      number,
468
      React.RefObject<FormikProps<ExperienceSkillFormValues>>
469
    >,
470
  ): Promise<void> => {
471
    setIsSubmitting(true);
472
    const refs = Array.from(refMap.values()).filter(notEmpty);
473
    const formsAreValid = await validateAllForms(refs);
474
    if (formsAreValid) {
475
      try {
476
        await submitAllForms(refs);
477
        setIsSubmitting(false);
478
        return Promise.resolve();
479
      } catch {
480
        setIsSubmitting(false);
481
        return Promise.reject();
482
      }
483
    } else {
484
      // Set icon status for all invalid form elements.
485
      Array.from(refMap.entries()).forEach(([experienceSkillId, formRef]) => {
486
        if (formRef.current !== null && !formRef.current.isValid) {
487
          const experienceSkill = find(experienceSkills, experienceSkillId);
488
          if (experienceSkill !== null) {
489
            dispatchStatus({
490
              payload: {
491
                skillId: experienceSkill.skill_id,
492
                experienceId: experienceSkill.experience_id,
493
                experienceType: experienceSkill.experience_type,
494
                status: IconStatus.ERROR,
495
              },
496
            });
497
          }
498
        }
499
      });
500
501
      // Expand and focus on the first invalid accordion form element.
502
      Array.from(refMap.entries()).some(([experienceSkillId, formRef]) => {
503
        if (formRef.current !== null && !formRef.current.isValid) {
504
          // Ensure the accordion is expanded before focussing on it.
505
          setAccordionExpansions({
506
            ...accordionExpansions,
507
            [experienceSkillId]: true,
508
          });
509
          focusOnElement(`experience-skill-textarea-${experienceSkillId}`);
510
          return true;
511
        }
512
        return false;
513
      });
514
      setIsSubmitting(false);
515
      return Promise.reject();
516
    }
517
  };
518
519
  // formRefs holds a dictionary of experienceSkill.id to refs to Formik forms.
520
  const formRefs = React.useRef<
521
    Map<number, React.RefObject<FormikProps<ExperienceSkillFormValues>>>
522
  >(new Map());
523
524
  // Ensure each experienceSkill has a corresponding form ref
525
  experienceSkills.forEach((expSkill) => {
526
    if (!formRefs.current.has(expSkill.id)) {
527
      const ref = createRef<FormikProps<ExperienceSkillFormValues>>();
528
      formRefs.current.set(expSkill.id, ref);
529
    }
530
  });
531
532
  return (
533
    <div data-c-container="large" ref={modalParentRef}>
534
      <div data-c-grid="gutter(all, 1)">
535
        <div data-c-grid-item="tl(1of4)">
536
          <Sidebar menuSkills={menuSkills} intl={intl} status={status} />
537
        </div>
538
        <div data-c-grid-item="tl(3of4)">
539
          <h2 data-c-heading="h2" data-c-margin="top(3) bottom(1)">
540
            <FormattedMessage
541
              id="application.skills.heading"
542
              defaultMessage="How You Used Each Skill"
543
              description="Heading text on the Skills step."
544
            />
545
          </h2>
546
          <p data-c-margin="bottom(1)">
547
            <FormattedMessage
548
              id="application.skills.instructionHeading"
549
              defaultMessage="<bold>This is the most important part of your application.</bold> Each box only needs a couple of sentences, but make them good ones!"
550
              description="Heading for the instruction section on the Skills step."
551
              values={{
552
                bold: (chunks) => (
553
                  <span data-c-font-weight="bold">{chunks}</span>
554
                ),
555
              }}
556
            />
557
          </p>
558
          <p data-c-margin="bottom(.5)">
559
            <FormattedMessage
560
              id="application.skills.instructionListStart"
561
              defaultMessage="Try answering one or two of the following questions:"
562
              description="Paragraph before the instruction list on the Skills step."
563
            />
564
          </p>
565
          <ul data-c-margin="bottom(1)">
566
            <FormattedMessage
567
              id="application.skills.instructionList"
568
              defaultMessage="<bullet>What did you accomplish, create, or deliver using this skill?</bullet><bullet>What tasks or activities did you do that relate to this skill?</bullet><bullet>Were there any special techniques or approaches that you used?</bullet><bullet>How much responsibility did you have in this role?</bullet>"
569
              description="List of potential justification helpers on the Skills step."
570
              values={{
571
                bullet: (chunks) => <li>{chunks}</li>,
572
              }}
573
            />
574
          </ul>
575
          <p>
576
            <FormattedMessage
577
              id="application.skills.instructionListEnd"
578
              defaultMessage="If a skill is only loosely connected to an experience, consider removing it. This can help the manager focus on your best examples."
579
              description="Paragraph after the instruction list on the Skills step."
580
            />
581
          </p>
582
          <div className="skills-list">
583
            {criteria.map((criterion) => {
584
              const skill = getSkillOfCriteria(criterion, skills);
585
              if (skill === null) {
586
                return null;
587
              }
588
              const skillName = localizeFieldNonNull(locale, skill, "name");
589
              const skillDescription = localizeFieldNonNull(
590
                locale,
591
                skill,
592
                "description",
593
              );
594
              const skillHtmlId = slugify(skillName);
595
596
              return (
597
                <div key={criterion.id}>
598
                  <h3
599
                    className="application-skill-title"
600
                    data-c-heading="h3"
601
                    data-c-padding="top(3) bottom(1)"
602
                    data-c-margin="bottom(1)"
603
                    id={skillHtmlId}
604
                  >
605
                    <button
606
                      data-c-font-size="h3"
607
                      data-c-dialog-id={modalId}
608
                      type="button"
609
                      onClick={(e): void => {
610
                        setModalHeading(skillName);
611
                        setModalBody(skillDescription);
612
                        setVisible(true);
613
                      }}
614
                    >
615
                      {skillName}
616
                    </button>
617
                    <br />
618
                    <a
619
                      data-c-font-size="normal"
620
                      data-c-font-weight="bold"
621
                      href={applicantFaq(locale, "levels")}
622
                    >
623
                      {intl.formatMessage(getSkillLevelName(criterion, skill))}
624
                    </a>
625
                  </h3>
626
                  {getExperiencesOfSkill(skill, experienceSkills).length ===
627
                  0 ? (
628
                    <div
629
                      data-c-background="gray(10)"
630
                      data-c-radius="rounded"
631
                      data-c-border="all(thin, solid, gray)"
632
                      data-c-padding="all(1)"
633
                    >
634
                      <div data-c-align="base(center)">
635
                        <p data-c-color="gray">
636
                          <FormattedMessage
637
                            id="application.skills.noLinkedExperiences"
638
                            defaultMessage="Looks like you don't have any experiences linked to this skill. You can link experiences to skills in the previous step."
639
                            description="Text displayed under a skill section with no experiences."
640
                          />
641
                        </p>
642
                      </div>
643
                    </div>
644
                  ) : (
645
                    <div data-c-accordion-group="">
646
                      {getExperiencesOfSkill(skill, experienceSkills).map(
647
                        (experienceSkill) => {
648
                          const experienceStatus = computeExperienceStatus(
649
                            status,
650
                            experienceSkill,
651
                          );
652
                          const relevantExperience = getExperienceOfExperienceSkills(
653
                            experienceSkill,
654
                            experiences,
655
                          );
656
                          const elementKey = `experience-skill-textarea-${experienceSkill.id}`;
657
                          if (relevantExperience === null) {
658
                            return (
659
                              <div
660
                                key={elementKey}
661
                                data-c-background="gray(10)"
662
                                data-c-radius="rounded"
663
                                data-c-border="all(thin, solid, gray)"
664
                                data-c-padding="all(1)"
665
                              >
666
                                <div data-c-align="base(center)">
667
                                  <p data-c-color="gray">
668
                                    <FormattedMessage
669
                                      id="application.skills.missingExperience"
670
                                      defaultMessage="Looks like something went wrong on our end and your experience can't be displayed. Please try again later."
671
                                      description="Text displayed under a skill section with a missing linked experience."
672
                                    />
673
                                  </p>
674
                                </div>
675
                              </div>
676
                            );
677
                          }
678
679
                          if (!formRefs.current.has(experienceSkill.id)) {
680
                            const ref = createRef<
681
                              FormikProps<ExperienceSkillFormValues>
682
                            >();
683
                            formRefs.current.set(experienceSkill.id, ref);
684
                          }
685
                          return (
686
                            <ExperienceSkillAccordion
687
                              key={elementKey}
688
                              experience={relevantExperience}
689
                              experienceSkill={experienceSkill}
690
                              intl={intl}
691
                              status={experienceStatus}
692
                              handleUpdateStatus={dispatchStatus}
693
                              skillName={skillName}
694
                              isExpanded={
695
                                accordionExpansions[experienceSkill.id]
696
                              }
697
                              setIsExpanded={(value: boolean): void =>
698
                                setAccordionExpansions({
699
                                  ...accordionExpansions,
700
                                  [experienceSkill.id]: value,
701
                                })
702
                              }
703
                              formRef={
704
                                formRefs.current.get(experienceSkill.id)! // Can assert this is not null, becuase if it was we just added it to map.
705
                              }
706
                              handleUpdateExperienceJustification={
707
                                handleUpdateExperienceJustification
708
                              }
709
                              handleRemoveExperience={
710
                                handleRemoveExperienceJustification
711
                              }
712
                            />
713
                          );
714
                        },
715
                      )}
716
                    </div>
717
                  )}
718
                </div>
719
              );
720
            })}
721
          </div>
722
          <div data-c-container="medium" data-c-padding="tb(2)">
723
            <hr data-c-hr="thin(c1)" data-c-margin="bottom(2)" />
724
            <div data-c-grid="gutter">
725
              <div
726
                data-c-alignment="base(centre) tp(left)"
727
                data-c-grid-item="tp(1of2)"
728
              >
729
                <button
730
                  data-c-button="outline(c2)"
731
                  data-c-radius="rounded"
732
                  type="button"
733
                  disabled={isSubmitting}
734
                  onClick={(): Promise<void> =>
735
                    validateAndSubmitMany(formRefs.current)
736
                      .then(handleReturn)
737
                      .catch(() => {
738
                        // Validation failed, do nothing.
739
                      })
740
                  }
741
                >
742
                  {intl.formatMessage(navigationMessages.return)}
743
                </button>
744
              </div>
745
              <div
746
                data-c-alignment="base(centre) tp(right)"
747
                data-c-grid-item="tp(1of2)"
748
              >
749
                <button
750
                  data-c-button="outline(c2)"
751
                  data-c-radius="rounded"
752
                  type="button"
753
                  disabled={isSubmitting}
754
                  onClick={(): Promise<void> =>
755
                    validateAndSubmitMany(formRefs.current)
756
                      .then(handleQuit)
757
                      .catch(() => {
758
                        // Validation failed, do nothing.
759
                      })
760
                  }
761
                >
762
                  {intl.formatMessage(navigationMessages.quit)}
763
                </button>
764
                <button
765
                  data-c-button="solid(c1)"
766
                  data-c-radius="rounded"
767
                  data-c-margin="left(1)"
768
                  disabled={isSubmitting}
769
                  type="button"
770
                  onClick={(): Promise<void> =>
771
                    validateAndSubmitMany(formRefs.current)
772
                      .then(handleContinue)
773
                      .catch(() => {
774
                        // Validation failed, do nothing.
775
                      })
776
                  }
777
                >
778
                  {intl.formatMessage(navigationMessages.continue)}
779
                </button>
780
              </div>
781
            </div>
782
          </div>
783
        </div>
784
      </div>
785
      <div data-c-dialog-overlay={visible ? "active" : ""} />
786
      <Modal
787
        id={modalId}
788
        parentElement={modalParentRef.current}
789
        visible={visible}
790
        onModalConfirm={(e): void => setVisible(false)}
791
        onModalCancel={(e): void => setVisible(false)}
792
      >
793
        <Modal.Header>
794
          <div
795
            data-c-padding="tb(1)"
796
            data-c-border="bottom(thin, solid, black)"
797
            data-c-background="c1(100)"
798
            className="dialog-header"
799
          >
800
            <div data-c-container="medium">
801
              <h5
802
                data-c-font-size="h3"
803
                data-c-font-weight="bold"
804
                id={`${modalId}-title`}
805
                data-c-dialog-focus=""
806
                data-c-color="white"
807
              >
808
                {modalHeading}
809
              </h5>
810
              <button
811
                data-c-dialog-action="close"
812
                data-c-dialog-id={`${modalId}`}
813
                type="button"
814
                data-c-color="white"
815
                tabIndex={0}
816
                onClick={(e): void => setVisible(false)}
817
              >
818
                <i className="fas fa-times" />
819
              </button>
820
            </div>
821
          </div>
822
        </Modal.Header>
823
        <Modal.Body>
824
          <div data-c-border="bottom(thin, solid, black)">
825
            <div id={`${modalId}-description`}>
826
              <div data-c-container="medium" data-c-padding="tb(1)">
827
                <p>{modalBody}</p>
828
              </div>
829
            </div>
830
          </div>
831
        </Modal.Body>
832
        <Modal.Footer>
833
          <Modal.FooterConfirmBtn>
834
            <FormattedMessage
835
              id="application.skills.modal.confirmButton"
836
              defaultMessage="Okay"
837
            />
838
          </Modal.FooterConfirmBtn>
839
        </Modal.Footer>
840
      </Modal>
841
    </div>
842
  );
843
};
844
845
export default Skills;
846