Passed
Push — task/update-build-badge ( b9b78a )
by
unknown
06:16
created

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

Complexity

Total Complexity 24
Complexity/F 0

Size

Lines of Code 887
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

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