Passed
Push — dev ( 12f067...0cbe54 )
by
unknown
05:50
created

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

Complexity

Total Complexity 21
Complexity/F 0

Size

Lines of Code 850
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 21
eloc 680
mnd 21
bc 21
fnc 0
dl 0
loc 850
rs 9.92
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
  getExperienceOfExperienceSkill,
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
export 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
        enableReinitialize
276
        innerRef={formRef}
277
        initialValues={initialValues}
278
        validationSchema={experienceSkillSchema}
279
        onSubmit={(values, { setSubmitting, resetForm }): Promise<void> => {
280
          if (initialValues.justification === values.justification) {
281
            return Promise.resolve();
282
          }
283
          const experienceSkillJustification = updateExperienceSkill(
284
            experienceSkill,
285
            values,
286
          );
287
          return handleUpdateExperienceJustification(
288
            experienceSkillJustification,
289
          )
290
            .then(() => {
291
              handleUpdateStatus({
292
                payload: {
293
                  skillId: experienceSkill.skill_id,
294
                  experienceId: experienceSkill.experience_id,
295
                  experienceType: experienceSkill.experience_type,
296
                  status: IconStatus.COMPLETE,
297
                },
298
              });
299
              setSubmitting(false);
300
              resetForm();
301
            })
302
            .catch(() => {
303
              setSubmitting(false);
304
            });
305
        }}
306
      >
307
        {({ dirty, isSubmitting, isValid, submitForm }): React.ReactElement => (
308
          <div
309
            aria-hidden={isExpanded ? "false" : "true"}
310
            data-c-accordion-content=""
311
            data-c-background="gray(10)"
312
          >
313
            <Form>
314
              <AlertWhenUnsaved />
315
              <hr data-c-hr="thin(gray)" data-c-margin="bottom(1)" />
316
              <div data-c-padding="lr(1)">
317
                <FastField
318
                  id={`experience-skill-textarea-${experienceSkill.id}`}
319
                  name="justification"
320
                  label={label}
321
                  component={TextAreaInput}
322
                  placeholder={intl.formatMessage(
323
                    skillMessages.experienceSkillPlaceholder,
324
                  )}
325
                  required
326
                />
327
              </div>
328
              <div data-c-padding="all(1)">
329
                <div data-c-grid="gutter(all, 1) middle">
330
                  <div
331
                    data-c-grid-item="tp(1of2)"
332
                    data-c-align="base(center) tp(left)"
333
                  >
334
                    <button
335
                      data-c-button="outline(c1)"
336
                      data-c-radius="rounded"
337
                      type="button"
338
                      onClick={handleRemoveButtonClick}
339
                    >
340
                      <span>
341
                        <FormattedMessage
342
                          id="application.skills.deleteExperienceButtonText"
343
                          defaultMessage="Remove Experience From Skill"
344
                          description="Text for the delete experience button."
345
                        />
346
                      </span>
347
                    </button>
348
                  </div>
349
                  <div
350
                    data-c-grid-item="tp(1of2)"
351
                    data-c-align="base(center) tp(right)"
352
                  >
353
                    <WordCounter
354
                      elementId={`experience-skill-textarea-${experienceSkill.id}`}
355
                      maxWords={JUSTIFICATION_WORD_LIMIT}
356
                      minWords={0}
357
                      absoluteValue
358
                      dataAttributes={{ "data-c-margin": "right(1)" }}
359
                      underMaxMessage={intl.formatMessage(
360
                        displayMessages.wordCountUnderMax,
361
                      )}
362
                      overMaxMessage={intl.formatMessage(
363
                        displayMessages.wordCountOverMax,
364
                      )}
365
                    />
366
                    <button
367
                      data-c-button="solid(c1)"
368
                      data-c-radius="rounded"
369
                      type="button"
370
                      disabled={!dirty || isSubmitting}
371
                      onClick={(): void => {
372
                        if (!isValid) {
373
                          handleUpdateStatus({
374
                            payload: {
375
                              skillId: experienceSkill.skill_id,
376
                              experienceId: experienceSkill.experience_id,
377
                              experienceType: experienceSkill.experience_type,
378
                              status: IconStatus.ERROR,
379
                            },
380
                          });
381
                        } else {
382
                          submitForm();
383
                        }
384
                      }}
385
                    >
386
                      <span>
387
                        {dirty
388
                          ? intl.formatMessage(displayMessages.save)
389
                          : intl.formatMessage(displayMessages.saved)}
390
                      </span>
391
                    </button>
392
                  </div>
393
                </div>
394
              </div>
395
            </Form>
396
          </div>
397
        )}
398
      </Formik>
399
    </div>
400
  );
401
};
402
403
interface SkillsProps {
404
  criteria: Criteria[];
405
  experiences: Experience[];
406
  experienceSkills: ExperienceSkill[];
407
  skills: Skill[];
408
  handleUpdateExperienceJustification: (
409
    experience: ExperienceSkill,
410
  ) => Promise<ExperienceSkill>;
411
  handleRemoveExperienceJustification: (
412
    experience: ExperienceSkill,
413
  ) => Promise<void>;
414
  handleContinue: () => void;
415
  handleQuit: () => void;
416
  handleReturn: () => void;
417
}
418
419
const Skills: React.FC<SkillsProps> = ({
420
  criteria,
421
  experiences,
422
  experienceSkills,
423
  skills,
424
  handleUpdateExperienceJustification,
425
  handleRemoveExperienceJustification,
426
  handleContinue,
427
  handleQuit,
428
  handleReturn,
429
}) => {
430
  const intl = useIntl();
431
  const locale = getLocale(intl.locale);
432
  const initial = initialStatus(experienceSkills, JUSTIFICATION_WORD_LIMIT);
433
434
  const [status, dispatchStatus] = useReducer(statusReducer, initial);
435
436
  const modalId = "skill-description";
437
  const [visible, setVisible] = useState(false);
438
  const [modalHeading, setModalHeading] = useState("");
439
  const [modalBody, setModalBody] = useState("");
440
  const modalParentRef = useRef<HTMLDivElement>(null);
441
442
  // Maps ExperienceSkill ids to their accordion expansion state.
443
  const [accordionExpansions, setAccordionExpansions] = useState<{
444
    [experienceSkillId: number]: boolean;
445
  }>(mapToObjectTrans(experienceSkills, getId, () => false));
446
447
  const menuSkills = criteria.reduce(
448
    (collection: { [skillId: number]: string }, criterion: Criteria) => {
449
      const skill = getSkillOfCriteria(criterion, skills);
450
      if (skill && !collection[criterion.skill_id]) {
451
        // eslint-disable-next-line no-param-reassign
452
        collection[criterion.skill_id] = localizeFieldNonNull(
453
          locale,
454
          skill,
455
          "name",
456
        );
457
      }
458
      return collection;
459
    },
460
    [],
461
  );
462
463
  const [isSubmitting, setIsSubmitting] = useState(false);
464
  /**
465
   * Validate many Formik forms, and submit all of them if all are valid.
466
   * @param refMap A map of experienceSkill id to formik Form ref
467
   * @returns A promise that resolves if submission succeeds, and rejects if validation or submission fails.
468
   */
469
  const validateAndSubmitMany = async (
470
    refMap: Map<
471
      number,
472
      React.RefObject<FormikProps<ExperienceSkillFormValues>>
473
    >,
474
  ): Promise<void> => {
475
    setIsSubmitting(true);
476
    const refs = Array.from(refMap.values()).filter(notEmpty);
477
    const formsAreValid = await validateAllForms(refs);
478
    if (formsAreValid) {
479
      try {
480
        await submitAllForms(refs);
481
        setIsSubmitting(false);
482
        return Promise.resolve();
483
      } catch {
484
        setIsSubmitting(false);
485
        return Promise.reject();
486
      }
487
    } else {
488
      // Set icon status for all invalid form elements.
489
      Array.from(refMap.entries()).forEach(([experienceSkillId, formRef]) => {
490
        if (formRef.current !== null && !formRef.current.isValid) {
491
          const experienceSkill = find(experienceSkills, experienceSkillId);
492
          if (experienceSkill !== null) {
493
            dispatchStatus({
494
              payload: {
495
                skillId: experienceSkill.skill_id,
496
                experienceId: experienceSkill.experience_id,
497
                experienceType: experienceSkill.experience_type,
498
                status: IconStatus.ERROR,
499
              },
500
            });
501
          }
502
        }
503
      });
504
505
      // Expand and focus on the first invalid accordion form element.
506
      Array.from(refMap.entries()).some(([experienceSkillId, formRef]) => {
507
        if (formRef.current !== null && !formRef.current.isValid) {
508
          // Ensure the accordion is expanded before focussing on it.
509
          setAccordionExpansions({
510
            ...accordionExpansions,
511
            [experienceSkillId]: true,
512
          });
513
          focusOnElement(`experience-skill-textarea-${experienceSkillId}`);
514
          return true;
515
        }
516
        return false;
517
      });
518
      setIsSubmitting(false);
519
      return Promise.reject();
520
    }
521
  };
522
523
  // formRefs holds a dictionary of experienceSkill.id to refs to Formik forms.
524
  const formRefs = React.useRef<
525
    Map<number, React.RefObject<FormikProps<ExperienceSkillFormValues>>>
526
  >(new Map());
527
528
  // Ensure each experienceSkill has a corresponding form ref
529
  experienceSkills.forEach((expSkill) => {
530
    if (!formRefs.current.has(expSkill.id)) {
531
      const ref = createRef<FormikProps<ExperienceSkillFormValues>>();
532
      formRefs.current.set(expSkill.id, ref);
533
    }
534
  });
535
536
  return (
537
    <div data-c-container="large" ref={modalParentRef}>
538
      <div data-c-grid="gutter(all, 1)">
539
        <div data-c-grid-item="tl(1of4)">
540
          <Sidebar menuSkills={menuSkills} intl={intl} status={status} />
541
        </div>
542
        <div data-c-grid-item="tl(3of4)">
543
          <h2 data-c-heading="h2" data-c-margin="top(3) bottom(1)">
544
            <FormattedMessage
545
              id="application.skills.heading"
546
              defaultMessage="How You Used Each Skill"
547
              description="Heading text on the Skills step."
548
            />
549
          </h2>
550
          <p data-c-margin="bottom(1)">
551
            <FormattedMessage
552
              id="application.skills.instructionHeading"
553
              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!"
554
              description="Heading for the instruction section on the Skills step."
555
              values={{
556
                bold: (chunks) => (
557
                  <span data-c-font-weight="bold">{chunks}</span>
558
                ),
559
              }}
560
            />
561
          </p>
562
          <p data-c-margin="bottom(.5)">
563
            <FormattedMessage
564
              id="application.skills.instructionListStart"
565
              defaultMessage="Try answering one or two of the following questions:"
566
              description="Paragraph before the instruction list on the Skills step."
567
            />
568
          </p>
569
          <ul data-c-margin="bottom(1)">
570
            <FormattedMessage
571
              id="application.skills.instructionList"
572
              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>"
573
              description="List of potential justification helpers on the Skills step."
574
              values={{
575
                bullet: (chunks) => <li>{chunks}</li>,
576
              }}
577
            />
578
          </ul>
579
          <p>
580
            <FormattedMessage
581
              id="application.skills.instructionListEnd"
582
              defaultMessage="If a skill is only loosely connected to an experience, consider removing it. This can help the manager focus on your best examples."
583
              description="Paragraph after the instruction list on the Skills step."
584
            />
585
          </p>
586
          <div className="skills-list">
587
            {criteria.map((criterion) => {
588
              const skill = getSkillOfCriteria(criterion, skills);
589
              if (skill === null) {
590
                return null;
591
              }
592
              const skillName = localizeFieldNonNull(locale, skill, "name");
593
              const skillDescription = localizeFieldNonNull(
594
                locale,
595
                skill,
596
                "description",
597
              );
598
              const skillHtmlId = slugify(skillName);
599
600
              return (
601
                <div key={criterion.id}>
602
                  <h3
603
                    className="application-skill-title"
604
                    data-c-heading="h3"
605
                    data-c-padding="top(3) bottom(1)"
606
                    data-c-margin="bottom(1)"
607
                    id={skillHtmlId}
608
                  >
609
                    <button
610
                      data-c-font-size="h3"
611
                      data-c-dialog-id={modalId}
612
                      type="button"
613
                      onClick={(e): void => {
614
                        setModalHeading(skillName);
615
                        setModalBody(skillDescription);
616
                        setVisible(true);
617
                      }}
618
                    >
619
                      {skillName}
620
                    </button>
621
                    <br />
622
                    <a
623
                      data-c-font-size="normal"
624
                      data-c-font-weight="bold"
625
                      href={applicantFaq(locale, "levels")}
626
                    >
627
                      {intl.formatMessage(getSkillLevelName(criterion, skill))}
628
                    </a>
629
                  </h3>
630
                  {getExperiencesOfSkill(skill, experienceSkills).length ===
631
                  0 ? (
632
                    <div
633
                      data-c-background="gray(10)"
634
                      data-c-radius="rounded"
635
                      data-c-border="all(thin, solid, gray)"
636
                      data-c-padding="all(1)"
637
                    >
638
                      <div data-c-align="base(center)">
639
                        <p data-c-color="gray">
640
                          <FormattedMessage
641
                            id="application.skills.noLinkedExperiences"
642
                            defaultMessage="Looks like you don't have any experiences linked to this skill. You can link experiences to skills in the previous step."
643
                            description="Text displayed under a skill section with no experiences."
644
                          />
645
                        </p>
646
                      </div>
647
                    </div>
648
                  ) : (
649
                    <div data-c-accordion-group="">
650
                      {getExperiencesOfSkill(skill, experienceSkills).map(
651
                        (experienceSkill) => {
652
                          const experienceStatus = computeExperienceStatus(
653
                            status,
654
                            experienceSkill,
655
                          );
656
                          const relevantExperience = getExperienceOfExperienceSkill(
657
                            experienceSkill,
658
                            experiences,
659
                          );
660
                          const elementKey = `experience-skill-textarea-${experienceSkill.id}`;
661
                          if (relevantExperience === null) {
662
                            return (
663
                              <div
664
                                key={elementKey}
665
                                data-c-background="gray(10)"
666
                                data-c-radius="rounded"
667
                                data-c-border="all(thin, solid, gray)"
668
                                data-c-padding="all(1)"
669
                              >
670
                                <div data-c-align="base(center)">
671
                                  <p data-c-color="gray">
672
                                    <FormattedMessage
673
                                      id="application.skills.missingExperience"
674
                                      defaultMessage="Looks like something went wrong on our end and your experience can't be displayed. Please try again later."
675
                                      description="Text displayed under a skill section with a missing linked experience."
676
                                    />
677
                                  </p>
678
                                </div>
679
                              </div>
680
                            );
681
                          }
682
683
                          if (!formRefs.current.has(experienceSkill.id)) {
684
                            const ref = createRef<
685
                              FormikProps<ExperienceSkillFormValues>
686
                            >();
687
                            formRefs.current.set(experienceSkill.id, ref);
688
                          }
689
                          return (
690
                            <ExperienceSkillAccordion
691
                              key={elementKey}
692
                              experience={relevantExperience}
693
                              experienceSkill={experienceSkill}
694
                              intl={intl}
695
                              status={experienceStatus}
696
                              handleUpdateStatus={dispatchStatus}
697
                              skillName={skillName}
698
                              isExpanded={
699
                                accordionExpansions[experienceSkill.id]
700
                              }
701
                              setIsExpanded={(value: boolean): void =>
702
                                setAccordionExpansions({
703
                                  ...accordionExpansions,
704
                                  [experienceSkill.id]: value,
705
                                })
706
                              }
707
                              formRef={
708
                                formRefs.current.get(experienceSkill.id)! // Can assert this is not null, becuase if it was we just added it to map.
709
                              }
710
                              handleUpdateExperienceJustification={
711
                                handleUpdateExperienceJustification
712
                              }
713
                              handleRemoveExperience={
714
                                handleRemoveExperienceJustification
715
                              }
716
                            />
717
                          );
718
                        },
719
                      )}
720
                    </div>
721
                  )}
722
                </div>
723
              );
724
            })}
725
          </div>
726
          <div data-c-container="medium" data-c-padding="tb(2)">
727
            <hr data-c-hr="thin(c1)" data-c-margin="bottom(2)" />
728
            <div data-c-grid="gutter">
729
              <div
730
                data-c-alignment="base(centre) tp(left)"
731
                data-c-grid-item="tp(1of2)"
732
              >
733
                <button
734
                  data-c-button="outline(c2)"
735
                  data-c-radius="rounded"
736
                  type="button"
737
                  disabled={isSubmitting}
738
                  onClick={(): Promise<void> =>
739
                    validateAndSubmitMany(formRefs.current)
740
                      .then(handleReturn)
741
                      .catch(() => {
742
                        // Validation failed, do nothing.
743
                      })
744
                  }
745
                >
746
                  {intl.formatMessage(navigationMessages.return)}
747
                </button>
748
              </div>
749
              <div
750
                data-c-alignment="base(centre) tp(right)"
751
                data-c-grid-item="tp(1of2)"
752
              >
753
                <button
754
                  data-c-button="outline(c2)"
755
                  data-c-radius="rounded"
756
                  type="button"
757
                  disabled={isSubmitting}
758
                  onClick={(): Promise<void> =>
759
                    validateAndSubmitMany(formRefs.current)
760
                      .then(handleQuit)
761
                      .catch(() => {
762
                        // Validation failed, do nothing.
763
                      })
764
                  }
765
                >
766
                  {intl.formatMessage(navigationMessages.quit)}
767
                </button>
768
                <button
769
                  data-c-button="solid(c1)"
770
                  data-c-radius="rounded"
771
                  data-c-margin="left(1)"
772
                  disabled={isSubmitting}
773
                  type="button"
774
                  onClick={(): Promise<void> =>
775
                    validateAndSubmitMany(formRefs.current)
776
                      .then(handleContinue)
777
                      .catch(() => {
778
                        // Validation failed, do nothing.
779
                      })
780
                  }
781
                >
782
                  {intl.formatMessage(navigationMessages.continue)}
783
                </button>
784
              </div>
785
            </div>
786
          </div>
787
        </div>
788
      </div>
789
      <div data-c-dialog-overlay={visible ? "active" : ""} />
790
      <Modal
791
        id={modalId}
792
        parentElement={modalParentRef.current}
793
        visible={visible}
794
        onModalConfirm={(e): void => setVisible(false)}
795
        onModalCancel={(e): void => setVisible(false)}
796
      >
797
        <Modal.Header>
798
          <div
799
            data-c-padding="tb(1)"
800
            data-c-border="bottom(thin, solid, black)"
801
            data-c-background="c1(100)"
802
            className="dialog-header"
803
          >
804
            <div data-c-container="medium">
805
              <h5
806
                data-c-font-size="h3"
807
                data-c-font-weight="bold"
808
                id={`${modalId}-title`}
809
                data-c-dialog-focus=""
810
                data-c-color="white"
811
              >
812
                {modalHeading}
813
              </h5>
814
              <button
815
                data-c-dialog-action="close"
816
                data-c-dialog-id={`${modalId}`}
817
                type="button"
818
                data-c-color="white"
819
                tabIndex={0}
820
                onClick={(e): void => setVisible(false)}
821
              >
822
                <i className="fas fa-times" />
823
              </button>
824
            </div>
825
          </div>
826
        </Modal.Header>
827
        <Modal.Body>
828
          <div data-c-border="bottom(thin, solid, black)">
829
            <div id={`${modalId}-description`}>
830
              <div data-c-container="medium" data-c-padding="tb(1)">
831
                <p>{modalBody}</p>
832
              </div>
833
            </div>
834
          </div>
835
        </Modal.Body>
836
        <Modal.Footer>
837
          <Modal.FooterConfirmBtn>
838
            <FormattedMessage
839
              id="application.skills.modal.confirmButton"
840
              defaultMessage="Okay"
841
            />
842
          </Modal.FooterConfirmBtn>
843
        </Modal.Footer>
844
      </Modal>
845
    </div>
846
  );
847
};
848
849
export default Skills;
850