Passed
Push — feature/add-2fa-support ( 23e818...85b1f3 )
by Chris
24:19 queued 11:16
created

resources/assets/js/components/JobBuilder/Skills/JobBuilderSkills.tsx   B

Complexity

Total Complexity 32
Complexity/F 0

Size

Lines of Code 1881
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1436
dl 0
loc 1881
rs 8.64
c 0
b 0
f 0
wmc 32
mnd 32
bc 32
fnc 0
bpm 0
cpm 0
noi 0
1
/* eslint-disable camelcase, @typescript-eslint/camelcase */
2
import React, { useState, useRef, useReducer } from "react";
3
import {
4
  WrappedComponentProps,
5
  injectIntl,
6
  FormattedMessage,
7
  defineMessages,
8
} from "react-intl";
9
import nprogress from "nprogress";
10
import { Job, Skill, Criteria, JobPosterKeyTask } from "../../../models/types";
11
import Modal from "../../Modal";
12
import CriteriaForm from "./CriteriaForm";
13
import { mapToObject, getId, hasKey, notEmpty } from "../../../helpers/queries";
14
import { CriteriaTypeId } from "../../../models/lookupConstants";
15
import Select, { SelectOption } from "../../Select";
16
import { getSkillLevelName } from "../../../models/jobUtil";
17
import Criterion from "../Criterion";
18
19
interface JobBuilderSkillsProps {
20
  // The job being built
21
  job: Job;
22
  // This job's key tasks
23
  keyTasks: JobPosterKeyTask[];
24
  // Criteria already part of the job
25
  initialCriteria: Criteria[];
26
  // The list of all possible skills
27
  skills: Skill[];
28
  // The function to run when user clicks Save. Must return the updated list of criteria if successufl.
29
  handleSubmit: (criteria: Criteria[]) => Promise<Criteria[]>;
30
  // The function to run when user clicks Prev Pag
31
  handleReturn: () => void;
32
  // The function to run when user clicks Next Page
33
  handleContinue: () => void;
34
  /** Whether the entire job is complete and valid for submission. */
35
  jobIsComplete: boolean;
36
  /** Function that skips to final review. */
37
  handleSkipToReview: () => Promise<void>;
38
}
39
40
const messages = defineMessages({
41
  emailUs: {
42
    id: "jobBuilder.skills.emailLink",
43
    defaultMessage: "get in touch with us through email",
44
    description: "Text for an email link in a larger block of text",
45
  },
46
  selectSkillLabel: {
47
    id: "jobBuilder.skills.selectSkillLabel",
48
    defaultMessage: "Please select a skill from our list",
49
    description: "Label for skill selection dropdown menu",
50
  },
51
  selectSkillNull: {
52
    id: "jobBuilder.skills.selectSkillNull",
53
    defaultMessage: "Please select a skill",
54
    description: "Label for skill selection dropdown null/default state",
55
  },
56
});
57
58
const altMessages = defineMessages({
59
  unhappyArrow: {
60
    id: "jobBuilder.skills.alt.unhappyArrow",
61
    defaultMessage: "Arrow icon highlighting the unhappy smiley icon.",
62
    description: "Alternative text describing unhappy arrow image",
63
  },
64
  unhappySmiley: {
65
    id: "jobBuilder.skills.alt.unhappySmiley",
66
    defaultMessage: "Unhappy coloured smiley icon.",
67
    description: "Alternative text describing unhappy smiley image",
68
  },
69
  unhappyGraySmiley: {
70
    id: "jobBuilder.skills.alt.unhappyGraySmiley",
71
    defaultMessage: "Unhappy grayscale smiley icon.",
72
    description: "Alternative text describing unhappy grayscale smiley image",
73
  },
74
  neutralArrow: {
75
    id: "jobBuilder.skills.alt.neutralArrow",
76
    defaultMessage: "Arrow icon highlighting the neutral smiley icon.",
77
    description: "Alternative text describing neutral arrow image",
78
  },
79
  neutralSmiley: {
80
    id: "jobBuilder.skills.alt.neutralSmiley",
81
    defaultMessage: "neutral coloured smiley icon.",
82
    description: "Alternative text describing neutral smiley image",
83
  },
84
  neutralGraySmiley: {
85
    id: "jobBuilder.skills.alt.neutralGraySmiley",
86
    defaultMessage: "neutral grayscale smiley icon.",
87
    description: "Alternative text describing neutral grayscale smiley image",
88
  },
89
  happyArrow: {
90
    id: "jobBuilder.skills.alt.happyArrow",
91
    defaultMessage: "Arrow icon highlighting the happy smiley icon.",
92
    description: "Alternative text describing happy arrow image",
93
  },
94
  happySmiley: {
95
    id: "jobBuilder.skills.alt.happySmiley",
96
    defaultMessage: "happy coloured smiley icon.",
97
    description: "Alternative text describing happy smiley image",
98
  },
99
  happyGraySmiley: {
100
    id: "jobBuilder.skills.alt.happyGraySmiley",
101
    defaultMessage: "happy grayscale smiley icon.",
102
    description: "Alternative text describing happy grayscale smiley image",
103
  },
104
});
105
// "Arrow highlighting the neutral smiley icon."
106
// function arrayMove<T>(arr: T[], fromIndex: number, toIndex: number): T[] {
107
//   const arrCopy = [...arr];
108
//   const element = arrCopy[fromIndex];
109
//   arrCopy.splice(fromIndex, 1);
110
//   arrCopy.splice(toIndex, 0, element);
111
//   return arrCopy;
112
// }
113
114
// function moveUp<T>(arr: T[], fromIndex: number): T[] {
115
//   if (fromIndex <= 0) {
116
//     return arr;
117
//   }
118
//   return arrayMove(arr, fromIndex, fromIndex - 1);
119
// }
120
121
// function moveDown<T>(arr: T[], fromIndex: number): T[] {
122
//   if (fromIndex + 1 >= arr.length) {
123
//     return arr;
124
//   }
125
//   return arrayMove(arr, fromIndex, fromIndex + 1);
126
// }
127
128
type CriteriaAction =
129
  | {
130
      type: "add";
131
      payload: Criteria;
132
    }
133
  | {
134
      type: "edit";
135
      payload: Criteria;
136
    }
137
  | {
138
      type: "remove";
139
      payload: Criteria;
140
    }
141
  | {
142
      type: "removeSkill";
143
      payload: {
144
        skillId: number;
145
      };
146
    }
147
  | {
148
      type: "replace";
149
      payload: Criteria[];
150
    };
151
const criteriaReducer = (
152
  state: Criteria[],
153
  action: CriteriaAction,
154
): Criteria[] => {
155
  switch (action.type) {
156
    case "add":
157
      return [
158
        // When adding a criterion, make sure it isn't a duplicate
159
        // Always compare criteria using skill_id, to avoid duplicate skills. (And new criteria may not have unique ids yet.)
160
        ...state.filter(
161
          (criterion): boolean =>
162
            criterion.skill_id !== action.payload.skill_id,
163
        ),
164
        action.payload,
165
      ];
166
    case "edit":
167
      // Replace the edited criterion with the one from the action payload.
168
      // This is different from "add" because it keeps the same ordering.
169
      return state.map(
170
        (criterion): Criteria =>
171
          criterion.skill_id === action.payload.skill_id
172
            ? action.payload
173
            : criterion,
174
      );
175
    case "remove":
176
      return state.filter(
177
        (criterion): boolean => criterion.skill_id !== action.payload.skill_id,
178
      );
179
    case "removeSkill":
180
      return state.filter(
181
        (criterion): boolean => criterion.skill_id !== action.payload.skillId,
182
      );
183
    case "replace":
184
      // Totally replace the saved list of criteria with the recieved payload
185
      return action.payload;
186
187
    default:
188
      return state;
189
  }
190
};
191
192
export const skillAlreadySelected = (
193
  selectedCriteria: Criteria[],
194
  skill: Skill,
195
): boolean =>
196
  selectedCriteria.find(
197
    (criterion): boolean => criterion.skill_id === skill.id,
198
  ) !== undefined;
199
200
export const JobBuilderSkills: React.FunctionComponent<
201
  JobBuilderSkillsProps & WrappedComponentProps
202
> = ({
203
  job,
204
  keyTasks,
205
  initialCriteria,
206
  skills,
207
  handleSubmit,
208
  handleReturn,
209
  handleContinue,
210
  jobIsComplete,
211
  handleSkipToReview,
212
  intl,
213
}): React.ReactElement => {
214
  const { locale } = intl;
215
  if (locale !== "en" && locale !== "fr") {
216
    throw new Error("Unknown intl.locale");
217
  }
218
219
  // The ideal number of skills for each category
220
  const minOccupational = 3;
221
  const maxOccupational = 5;
222
  const minCulture = 0;
223
  const maxCulture = 4;
224
  const minFuture = 0;
225
  const maxFuture = 2;
226
227
  // This is where the edited list of criteria is stored
228
  const [jobCriteria, criteriaDispatch] = useReducer(
229
    criteriaReducer,
230
    initialCriteria,
231
  );
232
  const skillCount: number = jobCriteria.length;
233
  const essentialCriteria: Criteria[] = jobCriteria.filter(
234
    (criteria): boolean =>
235
      criteria.criteria_type_id === CriteriaTypeId.Essential,
236
  );
237
  const essentialCount: number = essentialCriteria.length;
238
  const assetCriteria: Criteria[] = jobCriteria.filter(
239
    (criteria): boolean => criteria.criteria_type_id === CriteriaTypeId.Asset,
240
  );
241
  const assetCount: number = assetCriteria.length;
242
243
  // Set this to true to show the Key Tasks modal
244
  const [tasksModalVisible, setTasksModalVisible] = useState(false);
245
246
  // When skillBeingAdded is not null, the modal to add a new skill will appear.
247
  const [skillBeingAdded, setSkillBeingAdded] = useState<Skill | null>(null);
248
249
  // Set to true if submit button is touched
250
  const [submitTouched, setSubmitTouched] = useState(false);
251
252
  // When criteriaBeingEdited is not null, the modal for editing that criterion will appear.
253
  const [
254
    criteriaBeingEdited,
255
    setCriteriaBeingEdited,
256
  ] = useState<Criteria | null>(null);
257
258
  const [isPreviewVisible, setIsPreviewVisible] = useState(false);
259
260
  // This should be true if ANY modal is visible. The modal overlay uses this.
261
  const isModalVisible =
262
    tasksModalVisible ||
263
    skillBeingAdded !== null ||
264
    criteriaBeingEdited !== null ||
265
    isPreviewVisible;
266
  const modalParentRef = useRef<HTMLDivElement>(null);
267
268
  const tasksModalId = "job-builder-review-tasks";
269
  const addModalId = "job-builder-add-skill";
270
  const editModalId = "job-builder-edit-skill";
271
  const previewModalId = "job-builder-preview-skills";
272
273
  const countInRange = (min: number, max: number, count: number): boolean => {
274
    return count >= min && count <= max;
275
  };
276
277
  const sortAlphabetically = (a: Skill, b: Skill): number => {
278
    const skillA: string = a[locale].name.toUpperCase();
279
    const skillB: string = b[locale].name.toUpperCase();
280
281
    return skillA.localeCompare(skillB, locale, { sensitivity: "base" });
282
  };
283
284
  // Map the skills into a dictionary for quicker access
285
  const skillsById = mapToObject(skills, getId);
286
  const getSkillOfCriteria = (criterion: Criteria): Skill | null => {
287
    return hasKey(skillsById, criterion.skill_id)
288
      ? skillsById[criterion.skill_id]
289
      : null;
290
  };
291
292
  const getClassifications = (skill: Skill): string[] =>
293
    skill.classifications.map((classification): string => classification.key);
294
  const isOccupational = (skill: Skill): boolean =>
295
    job.classification_code !== null &&
296
    getClassifications(skill).includes(job.classification_code);
297
  const occupationalSkills = skills
298
    .filter(isOccupational)
299
    .sort(sortAlphabetically);
300
  const occupationalCriteria = jobCriteria.filter((criterion): boolean => {
301
    const critSkill = getSkillOfCriteria(criterion);
302
    return critSkill !== null && isOccupational(critSkill);
303
  });
304
305
  const isCulture = (skill: Skill): boolean => skill.is_culture_skill;
306
  const cultureSkills = skills.filter(isCulture).sort(sortAlphabetically);
307
  const cultureCriteria = jobCriteria.filter((criterion): boolean => {
308
    const skill = getSkillOfCriteria(criterion);
309
    return skill !== null && isCulture(skill);
310
  });
311
  const isFuture = (skill: Skill): boolean => skill.is_future_skill;
312
  const futureSkills = skills.filter(isFuture).sort(sortAlphabetically);
313
  const futureCriteria = jobCriteria.filter((criterion): boolean => {
314
    const skill = getSkillOfCriteria(criterion);
315
    return skill !== null && isFuture(skill);
316
  });
317
318
  // Optional skills are those that don't fit into the other three categories
319
  const isOptional = (skill: Skill): boolean =>
320
    !isOccupational(skill) && !isCulture(skill) && !isFuture(skill);
321
  const otherSkills = skills.filter(isOptional).sort(sortAlphabetically);
322
  const selectedSkillIds = jobCriteria
323
    .map(getSkillOfCriteria)
324
    .filter(notEmpty)
325
    .map(getId);
326
  const selectedOtherSkills: Skill[] = otherSkills.filter((skill): boolean =>
327
    selectedSkillIds.includes(skill.id),
328
  );
329
  const unselectedOtherSkills: Skill[] = otherSkills.filter(
330
    (skill): boolean => !selectedSkillIds.includes(skill.id),
331
  );
332
333
  const [isSaving, setIsSaving] = useState(false);
334
335
  const errorMessage = useRef<HTMLAnchorElement>(null); // React.createRef<HTMLAnchorElement>();
336
  const focusOnError = (): void => {
337
    errorMessage.current && errorMessage.current.focus();
338
  };
339
340
  const saveAndPreview = (): void => {
341
    setSubmitTouched(true);
342
    if (essentialCount > 0) {
343
      nprogress.start();
344
      setIsSaving(true);
345
      handleSubmit(jobCriteria)
346
        .then((criteria: Criteria[]): void => {
347
          criteriaDispatch({ type: "replace", payload: criteria });
348
          nprogress.done();
349
          setIsPreviewVisible(true);
350
          setIsSaving(false);
351
        })
352
        .catch((): void => {
353
          nprogress.done();
354
          setIsSaving(false);
355
        });
356
    } else {
357
      focusOnError();
358
    }
359
  };
360
  const saveAndReturn = (): void => {
361
    nprogress.start();
362
    setIsSaving(true);
363
    handleSubmit(jobCriteria)
364
      .then((criteria: Criteria[]): void => {
365
        criteriaDispatch({ type: "replace", payload: criteria });
366
        nprogress.done();
367
        handleReturn();
368
        setIsSaving(false);
369
      })
370
      .catch((): void => {
371
        nprogress.done();
372
        setIsSaving(false);
373
      });
374
  };
375
376
  const renderNullCriteriaRow = (): React.ReactElement => (
377
    <div className="jpb-skill-null" data-c-grid="gutter middle">
378
      {/** TODO: add these back in when implementing UP/DOWN buttons again */}
379
      {/* <div data-c-grid-item="base(2of10) tl(1of10)" data-c-align="base(centre)">
380
        <button type="button" data-tc-move-up-trigger>
381
          <i className="fas fa-angle-up" />
382
        </button>
383
        <button type="button" data-tc-move-down-trigger>
384
          <i className="fas fa-angle-down" />
385
        </button>
386
      </div> */}
387
      <div data-c-grid-item="base(6of10) tl(7of10)">
388
        <div data-c-grid="gutter">
389
          <div data-c-grid-item="base(1of1) tl(2of3)">
390
            {/* <span>0</span>
391
            <span
392
              data-c-background="grey(40)"
393
              data-c-font-size="small"
394
              data-c-margin="rl(half)"
395
              data-c-padding="tb(quarter) rl(half)"
396
              data-c-radius="rounded"
397
              data-c-colour="white"
398
            >
399
              <i className="fas fa-briefcase" />
400
            </span> */}
401
            <span>
402
              <FormattedMessage
403
                id="jobBuilder.skills.addSkillBelow"
404
                defaultMessage="Add skills below to proceed."
405
                description="Placeholder skill title / instructions to add skill"
406
              />
407
            </span>
408
          </div>
409
          <div data-c-grid-item="base(1of1) tl(1of3)">
410
            <span
411
              data-c-colour="white"
412
              data-c-background="grey(40)"
413
              data-c-padding="tb(quarter) rl(half)"
414
              data-c-radius="rounded"
415
              data-c-font-size="small"
416
            >
417
              <FormattedMessage
418
                id="jobBuilder.skills.skillLevel"
419
                defaultMessage="Skill Level"
420
                description="Placeholder label"
421
              />
422
            </span>
423
          </div>
424
        </div>
425
      </div>
426
      <div data-c-grid-item="base(3of10)">
427
        <div data-c-grid="gutter">
428
          <div
429
            data-c-grid-item="base(1of1) tl(1of2)"
430
            data-c-align="base(centre)"
431
          >
432
            <button type="button">
433
              <i className="fas fa-edit" />
434
            </button>
435
          </div>
436
          <div
437
            data-c-grid-item="base(1of1) tl(1of2)"
438
            data-c-align="base(centre)"
439
          >
440
            <button type="button">
441
              <i className="fas fa-trash" />
442
            </button>
443
          </div>
444
        </div>
445
      </div>
446
    </div>
447
  );
448
449
  const renderCriteriaRow = (
450
    criterion: Criteria,
451
    index: number,
452
  ): React.ReactElement | null => {
453
    const skill = getSkillOfCriteria(criterion);
454
    if (skill === null) {
455
      return null;
456
    }
457
    return (
458
      <li
459
        key={skill.id}
460
        className={`jpb-skill ${isOccupational(skill) ? "occupational" : ""} ${
461
          isCulture(skill) ? "cultural" : ""
462
        } ${isFuture(skill) ? "future" : ""} ${
463
          isOptional(skill) ? "optional" : ""
464
        }`}
465
        data-tc-up-down-item
466
      >
467
        <div data-c-grid="gutter middle">
468
          {/** Removing the up/down buttons for now */}
469
          {/** TODO: removing the buttons messes with the row height, get Josh's help to fix it */}
470
          {/* <div
471
            data-c-grid-item="base(2of10) tl(1of10)"
472
            data-c-align="base(centre)"
473
          >
474
            <button type="button" data-tc-move-up-trigger>
475
              <i className="fas fa-angle-up" />
476
            </button>
477
            <button type="button" data-tc-move-down-trigger>
478
              <i className="fas fa-angle-down" />
479
            </button>
480
          </div> */}
481
          <div data-c-grid-item="base(6of10) tl(7of10)">
482
            <div data-c-grid="gutter">
483
              <div data-c-grid-item="base(1of1) tl(2of3)">
484
                <span data-c-margin="right(normal)">{index + 1}.</span>
485
                {/* This icon will automatically update based on the class you've specified above, on the jpb-skill. */}
486
                {/* <span
487
                  className="jpb-skill-type"
488
                  data-c-font-size="small"
489
                  data-c-margin="rl(half)"
490
                  data-c-padding="tb(quarter) rl(half)"
491
                  data-c-radius="rounded"
492
                  data-c-colour="white"
493
                  title="This is an occupational skill."
494
                >
495
                  <i className="fas fa-briefcase" />
496
                  <i className="fas fa-coffee" />
497
                  <i className="fas fa-certificate" />
498
                  <i className="fas fa-book" />
499
                </span> */}
500
                {/* The skill name. */}
501
                <span>{skill[locale].name}</span>
502
              </div>
503
              <div data-c-grid-item="base(1of1) tl(1of3)">
504
                <span
505
                  data-c-radius="pill"
506
                  data-c-padding="tb(quarter) rl(half)"
507
                  data-c-border="all(thin, solid, c1)"
508
                  data-c-colour="c1"
509
                  data-c-font-size="small"
510
                >
511
                  {intl.formatMessage(getSkillLevelName(criterion, skill))}
512
                </span>
513
              </div>
514
            </div>
515
          </div>
516
          <div data-c-grid-item="base(3of10)">
517
            <div data-c-grid="gutter">
518
              <div
519
                data-c-grid-item="base(1of1) tl(1of2)"
520
                data-c-align="base(centre)"
521
              >
522
                <button
523
                  type="button"
524
                  data-c-colour="c1"
525
                  onClick={(): void => setCriteriaBeingEdited(criterion)}
526
                >
527
                  <i className="fas fa-edit" />
528
                </button>
529
              </div>
530
              <div
531
                data-c-grid-item="base(1of1) tl(1of2)"
532
                data-c-align="base(centre)"
533
              >
534
                <button
535
                  type="button"
536
                  data-c-colour="c1"
537
                  data-c-hover-colour="stop"
538
                  onClick={(): void =>
539
                    criteriaDispatch({
540
                      type: "remove",
541
                      payload: criterion,
542
                    })
543
                  }
544
                >
545
                  <i className="fas fa-trash" />
546
                </button>
547
              </div>
548
            </div>
549
          </div>
550
        </div>
551
      </li>
552
    );
553
  };
554
555
  const renderSkillButton = (skill: Skill): React.ReactElement => {
556
    const alreadySelected = skillAlreadySelected(jobCriteria, skill);
557
    // Open the Add Skill modal, or remove it if its already added
558
    const handleClick = (): void =>
559
      alreadySelected
560
        ? criteriaDispatch({
561
            type: "removeSkill",
562
            payload: { skillId: skill.id },
563
          })
564
        : setSkillBeingAdded(skill);
565
    return (
566
      <li key={skill.id}>
567
        <button
568
          className={`jpb-skill-trigger ${alreadySelected ? "active" : ""}`}
569
          data-c-button="outline(c1)"
570
          data-c-radius="rounded"
571
          data-c-padding="all(half)"
572
          type="button"
573
          onClick={handleClick}
574
        >
575
          <span data-c-padding="right(half)">
576
            <i className="fas fa-plus-circle" />
577
            <i className="fas fa-minus-circle" />
578
          </span>
579
          {skill[locale].name}
580
        </button>
581
      </li>
582
    );
583
  };
584
585
  const submitButton = (
586
    <button
587
      data-c-button="solid(c2)"
588
      data-c-radius="rounded"
589
      type="button"
590
      disabled={isSaving}
591
      onClick={(): void => saveAndPreview()}
592
    >
593
      <FormattedMessage
594
        id="jobBuilder.skills.button.previewSkills"
595
        defaultMessage="Save &amp; Preview Skills"
596
        description="Label of Button"
597
      />
598
    </button>
599
  );
600
  return (
601
    <>
602
      <div
603
        data-c-container="form"
604
        data-c-padding="top(triple) bottom(triple)"
605
        ref={modalParentRef}
606
      >
607
        <h3
608
          data-c-font-size="h3"
609
          data-c-font-weight="bold"
610
          data-c-margin="bottom(double)"
611
        >
612
          <FormattedMessage
613
            id="jobBuilder.skills.title"
614
            defaultMessage="Skills"
615
            description="section title"
616
          />
617
        </h3>
618
        <p data-c-margin="bottom(triple)">
619
          <FormattedMessage
620
            id="jobBuilder.skills.description"
621
            defaultMessage="This is where you'll select the criteria that are required to do
622
            this job effectively. Below are two bars that indicate a measurement
623
            of your current skill selection."
624
            description="section description under title"
625
          />
626
        </p>
627
        <div
628
          data-c-margin="bottom(triple)"
629
          data-c-align="base(centre) tl(left)"
630
        >
631
          {/* We'll want this button to functionally be the exact same as the button at the bottom of the page, where it saves the data, and opens the preview modal. */}
632
          <button
633
            data-c-button="solid(c2)"
634
            data-c-radius="rounded"
635
            type="button"
636
            disabled={tasksModalVisible}
637
            onClick={(): void => setTasksModalVisible(true)}
638
          >
639
            <FormattedMessage
640
              id="jobBuilder.skills.button.keyTasks"
641
              defaultMessage="View Key Tasks"
642
              description="Button label"
643
            />
644
          </button>
645
        </div>
646
647
        {/* Total Skills List */}
648
        <h4
649
          data-c-colour="c2"
650
          data-c-font-size="h4"
651
          data-c-margin="bottom(normal)"
652
        >
653
          <FormattedMessage
654
            id="jobBuilder.skills.listTitle"
655
            defaultMessage="Your Skills List"
656
            description="List section title"
657
          />
658
        </h4>
659
        <div data-c-grid="gutter top">
660
          <div data-c-grid-item="base(1of1) tl(1of2)">
661
            <div
662
              data-c-border="all(thin, solid, black)"
663
              data-c-radius="rounded"
664
              data-c-padding="normal"
665
            >
666
              <p data-c-font-weight="bold" data-c-margin="bottom(normal)">
667
                <FormattedMessage
668
                  id="jobBuilder.skills.statusSmiley.essentialTitle"
669
                  defaultMessage="Number of Essential Skills"
670
                  description="Title of skill status tracker"
671
                />
672
              </p>
673
              {/* TODO: SmileyStatusIndicator can be extracted as its own component, since its already repeated within this page. */}
674
              {/* This is the new smiley status indicator component. It is reused twice on this page, once to indicate how many ESSENTIAL skills the user has selected, and a second time to indicate the TOTAL number of skills selected. The component functions the same way for both instances, but the ***scale is different***. There's a chance that the labels will be different too, so best to build it with that in mind. You can activate the appropriate smiley by assigning an "active" class to the relevant "jpb-skill-measure-item" element. See the UI in-browser for an example of what this looks like. */}
675
              <div
676
                data-c-grid="gutter"
677
                data-c-align="centre"
678
                data-c-padding="top(normal)"
679
              >
680
                <div
681
                  className={`jpb-skill-measure-item bad ${
682
                    countInRange(0, 1, essentialCount) ? "active" : ""
683
                  }`}
684
                  data-c-grid-item="base(1of5)"
685
                >
686
                  {/* This div appears in each step of the indicator, but we need the number inside the "span" to reflect the number of skills currently selected (within the context of the indicator, i.e. only show the number of essential skills selected in the essential indicator). */}
687
                  <div>
688
                    <img
689
                      src="\images\icon-smiley-arrow-bad.svg"
690
                      alt={intl.formatMessage(altMessages.unhappyArrow)}
691
                    />
692
                    <span
693
                      data-c-font-weight="bold"
694
                      data-c-colour="white"
695
                      data-c-font-size="small"
696
                    >
697
                      {essentialCount}
698
                    </span>
699
                  </div>
700
                  <img
701
                    src="\images\icon-smiley-bad.svg"
702
                    alt={intl.formatMessage(altMessages.unhappySmiley)}
703
                  />
704
                  <img
705
                    src="\images\icon-smiley-bad-grey.svg"
706
                    alt={intl.formatMessage(altMessages.unhappyGraySmiley)}
707
                  />
708
                  <p data-c-font-size="small" data-c-font-weight="bold">
709
                    <FormattedMessage
710
                      id="jobBuilder.skills.statusSmiley.essential.tooFew"
711
                      defaultMessage="Too Few"
712
                      description="Description of quantity of skills"
713
                    />
714
                  </p>
715
                  <p data-c-font-size="small">0 - 1</p>
716
                </div>
717
                <div
718
                  className={`jpb-skill-measure-item medium ${
719
                    countInRange(2, 3, essentialCount) ? "active" : ""
720
                  }`}
721
                  data-c-grid-item="base(1of5)"
722
                >
723
                  <div>
724
                    <img
725
                      src="\images\icon-smiley-arrow-medium.svg"
726
                      alt={intl.formatMessage(altMessages.neutralArrow)}
727
                    />
728
                    <span
729
                      data-c-font-weight="bold"
730
                      data-c-colour="white"
731
                      data-c-font-size="small"
732
                    >
733
                      {essentialCount}
734
                    </span>
735
                  </div>
736
                  <img
737
                    src="\images\icon-smiley-medium.svg"
738
                    alt={intl.formatMessage(altMessages.neutralSmiley)}
739
                  />
740
                  <img
741
                    src="\images\icon-smiley-medium-grey.svg"
742
                    alt={intl.formatMessage(altMessages.neutralGraySmiley)}
743
                  />
744
                  <p data-c-font-size="small" data-c-font-weight="bold">
745
                    <FormattedMessage
746
                      id="jobBuilder.skills.statusSmiley.essential.almost"
747
                      defaultMessage="Almost"
748
                      description="Description of quantity of skills"
749
                    />
750
                  </p>
751
                  <p data-c-font-size="small">2 - 3</p>
752
                </div>
753
                <div
754
                  className={`jpb-skill-measure-item good ${
755
                    countInRange(4, 6, essentialCount) ? "active" : ""
756
                  }`}
757
                  data-c-grid-item="base(1of5)"
758
                >
759
                  <div>
760
                    <img
761
                      src="\images\icon-smiley-arrow-good.svg"
762
                      alt={intl.formatMessage(altMessages.happyArrow)}
763
                    />
764
                    <span
765
                      data-c-font-weight="bold"
766
                      data-c-colour="white"
767
                      data-c-font-size="small"
768
                    >
769
                      {essentialCount}
770
                    </span>
771
                  </div>
772
                  <img
773
                    src="\images\icon-smiley-good.svg"
774
                    alt={intl.formatMessage(altMessages.happySmiley)}
775
                  />
776
                  <img
777
                    src="\images\icon-smiley-good-grey.svg"
778
                    alt={intl.formatMessage(altMessages.happyGraySmiley)}
779
                  />
780
                  <p data-c-font-size="small" data-c-font-weight="bold">
781
                    <FormattedMessage
782
                      id="jobBuilder.skills.statusSmiley.essential.awesome"
783
                      defaultMessage="Awesome"
784
                      description="Description of quantity of skills"
785
                    />
786
                  </p>
787
                  <p data-c-font-size="small">4 - 6</p>
788
                </div>
789
                <div
790
                  className={`jpb-skill-measure-item medium ${
791
                    countInRange(7, 8, essentialCount) ? "active" : ""
792
                  }`}
793
                  data-c-grid-item="base(1of5)"
794
                >
795
                  <div>
796
                    <img
797
                      src="\images\icon-smiley-arrow-medium.svg"
798
                      alt={intl.formatMessage(altMessages.neutralArrow)}
799
                    />
800
                    <span
801
                      data-c-font-weight="bold"
802
                      data-c-colour="white"
803
                      data-c-font-size="small"
804
                    >
805
                      {essentialCount}
806
                    </span>
807
                  </div>
808
                  <img
809
                    src="\images\icon-smiley-medium.svg"
810
                    alt={intl.formatMessage(altMessages.neutralSmiley)}
811
                  />
812
                  <img
813
                    src="\images\icon-smiley-medium-grey.svg"
814
                    alt={intl.formatMessage(altMessages.neutralGraySmiley)}
815
                  />
816
                  <p data-c-font-size="small" data-c-font-weight="bold">
817
                    <FormattedMessage
818
                      id="jobBuilder.skills.statusSmiley.essential.acceptable"
819
                      defaultMessage="Acceptable"
820
                      description="Description of quantity of skills"
821
                    />
822
                  </p>
823
                  <p data-c-font-size="small">7 - 8</p>
824
                </div>
825
                <div
826
                  className={`jpb-skill-measure-item bad ${
827
                    essentialCount >= 9 ? "active" : ""
828
                  }`}
829
                  data-c-grid-item="base(1of5)"
830
                >
831
                  <div>
832
                    {/* TODO: Alt Text Translations */}
833
                    <img
834
                      src="\images\icon-smiley-arrow-bad.svg"
835
                      alt={intl.formatMessage(altMessages.unhappyArrow)}
836
                    />
837
                    <span
838
                      data-c-font-weight="bold"
839
                      data-c-colour="white"
840
                      data-c-font-size="small"
841
                    >
842
                      {essentialCount}
843
                    </span>
844
                  </div>
845
                  <img
846
                    src="\images\icon-smiley-bad.svg"
847
                    alt={intl.formatMessage(altMessages.unhappySmiley)}
848
                  />
849
                  <img
850
                    src="\images\icon-smiley-bad-grey.svg"
851
                    alt={intl.formatMessage(altMessages.unhappyGraySmiley)}
852
                  />
853
                  <p data-c-font-size="small" data-c-font-weight="bold">
854
                    <FormattedMessage
855
                      id="jobBuilder.skills.statusSmiley.essential.tooMany"
856
                      defaultMessage="Too Many"
857
                      description="Description of quantity of skills"
858
                    />
859
                  </p>
860
                  <p data-c-font-size="small">9 +</p>
861
                </div>
862
              </div>
863
            </div>
864
          </div>
865
          <div data-c-grid-item="base(1of1) tl(1of2)">
866
            <div
867
              data-c-border="all(thin, solid, black)"
868
              data-c-radius="rounded"
869
              data-c-padding="normal"
870
            >
871
              <p data-c-font-weight="bold" data-c-margin="bottom(normal)">
872
                <FormattedMessage
873
                  id="jobBuilder.skills.statusSmiley.title"
874
                  defaultMessage="Total Number of Skills"
875
                  description="Title of skill quantity evaluator"
876
                />
877
              </p>
878
              {/* This is the second smiley indicator, used for total skills. Note the difference in the scale from the first. */}
879
              <div
880
                data-c-grid="gutter"
881
                data-c-align="centre"
882
                data-c-padding="top(normal)"
883
              >
884
                <div
885
                  className={`jpb-skill-measure-item bad ${
886
                    countInRange(0, 3, skillCount) ? "active" : ""
887
                  }`}
888
                  data-c-grid-item="base(1of5)"
889
                >
890
                  <div>
891
                    <img
892
                      src="\images\icon-smiley-arrow-bad.svg"
893
                      alt={intl.formatMessage(altMessages.unhappyArrow)}
894
                    />
895
                    <span
896
                      data-c-font-weight="bold"
897
                      data-c-colour="white"
898
                      data-c-font-size="small"
899
                    >
900
                      {skillCount}
901
                    </span>
902
                  </div>
903
                  <img
904
                    src="\images\icon-smiley-bad.svg"
905
                    alt={intl.formatMessage(altMessages.unhappySmiley)}
906
                  />
907
                  <img
908
                    src="\images\icon-smiley-bad-grey.svg"
909
                    alt={intl.formatMessage(altMessages.unhappyGraySmiley)}
910
                  />
911
                  <p data-c-font-size="small" data-c-font-weight="bold">
912
                    <FormattedMessage
913
                      id="jobBuilder.skills.statusSmiley.tooFew"
914
                      defaultMessage="Too Few"
915
                      description="Description of quantity of skills"
916
                    />
917
                  </p>
918
                  <p data-c-font-size="small">0 - 3</p>
919
                </div>
920
                <div
921
                  className={`jpb-skill-measure-item medium ${
922
                    countInRange(4, 6, skillCount) ? "active" : ""
923
                  }`}
924
                  data-c-grid-item="base(1of5)"
925
                >
926
                  <div>
927
                    <img
928
                      src="\images\icon-smiley-arrow-medium.svg"
929
                      alt={intl.formatMessage(altMessages.neutralArrow)}
930
                    />
931
                    <span
932
                      data-c-font-weight="bold"
933
                      data-c-colour="white"
934
                      data-c-font-size="small"
935
                    >
936
                      {skillCount}
937
                    </span>
938
                  </div>
939
                  <img
940
                    src="\images\icon-smiley-medium.svg"
941
                    alt={intl.formatMessage(altMessages.neutralSmiley)}
942
                  />
943
                  <img
944
                    src="\images\icon-smiley-medium-grey.svg"
945
                    alt={intl.formatMessage(altMessages.neutralGraySmiley)}
946
                  />
947
                  <p data-c-font-size="small" data-c-font-weight="bold">
948
                    <FormattedMessage
949
                      id="jobBuilder.skills.statusSmiley.almost"
950
                      defaultMessage="Almost"
951
                      description="Description of quantity of skills"
952
                    />
953
                  </p>
954
                  <p data-c-font-size="small">4 - 6</p>
955
                </div>
956
                <div
957
                  className={`jpb-skill-measure-item good ${
958
                    countInRange(7, 8, skillCount) ? "active" : ""
959
                  }`}
960
                  data-c-grid-item="base(1of5)"
961
                >
962
                  <div>
963
                    <img
964
                      src="\images\icon-smiley-arrow-good.svg"
965
                      alt={intl.formatMessage(altMessages.happyArrow)}
966
                    />
967
                    <span
968
                      data-c-font-weight="bold"
969
                      data-c-colour="white"
970
                      data-c-font-size="small"
971
                    >
972
                      {skillCount}
973
                    </span>
974
                  </div>
975
                  <img
976
                    src="\images\icon-smiley-good.svg"
977
                    alt={intl.formatMessage(altMessages.happySmiley)}
978
                  />
979
                  <img
980
                    src="\images\icon-smiley-good-grey.svg"
981
                    alt={intl.formatMessage(altMessages.happyGraySmiley)}
982
                  />
983
                  <p data-c-font-size="small" data-c-font-weight="bold">
984
                    <FormattedMessage
985
                      id="jobBuilder.skills.statusSmiley.awesome"
986
                      defaultMessage="Awesome"
987
                      description="Description of quantity of skills"
988
                    />
989
                  </p>
990
                  <p data-c-font-size="small">7 - 8</p>
991
                </div>
992
                <div
993
                  className={`jpb-skill-measure-item medium  ${
994
                    countInRange(9, 10, skillCount) ? "active" : ""
995
                  }`}
996
                  data-c-grid-item="base(1of5)"
997
                >
998
                  <div>
999
                    <img
1000
                      src="\images\icon-smiley-arrow-medium.svg"
1001
                      alt={intl.formatMessage(altMessages.neutralArrow)}
1002
                    />
1003
                    <span
1004
                      data-c-font-weight="bold"
1005
                      data-c-colour="white"
1006
                      data-c-font-size="small"
1007
                    >
1008
                      {skillCount}
1009
                    </span>
1010
                  </div>
1011
                  <img
1012
                    src="\images\icon-smiley-medium.svg"
1013
                    alt={intl.formatMessage(altMessages.neutralSmiley)}
1014
                  />
1015
                  <img
1016
                    src="\images\icon-smiley-medium-grey.svg"
1017
                    alt={intl.formatMessage(altMessages.neutralGraySmiley)}
1018
                  />
1019
                  <p data-c-font-size="small" data-c-font-weight="bold">
1020
                    <FormattedMessage
1021
                      id="jobBuilder.skills.statusSmiley.acceptable"
1022
                      defaultMessage="Acceptable"
1023
                      description="Description of quantity of skills"
1024
                    />
1025
                  </p>
1026
                  <p data-c-font-size="small">9 - 10</p>
1027
                </div>
1028
                <div
1029
                  className={`jpb-skill-measure-item bad ${
1030
                    skillCount >= 11 ? "active" : ""
1031
                  }`}
1032
                  data-c-grid-item="base(1of5)"
1033
                >
1034
                  <div>
1035
                    <img
1036
                      src="\images\icon-smiley-arrow-bad.svg"
1037
                      alt={intl.formatMessage(altMessages.unhappyArrow)}
1038
                    />
1039
                    <span
1040
                      data-c-font-weight="bold"
1041
                      data-c-colour="white"
1042
                      data-c-font-size="small"
1043
                    >
1044
                      {skillCount}
1045
                    </span>
1046
                  </div>
1047
                  <img
1048
                    src="\images\icon-smiley-bad.svg"
1049
                    alt={intl.formatMessage(altMessages.unhappySmiley)}
1050
                  />
1051
                  <img
1052
                    src="\images\icon-smiley-bad-grey.svg"
1053
                    alt={intl.formatMessage(altMessages.unhappyGraySmiley)}
1054
                  />
1055
                  <p data-c-font-size="small" data-c-font-weight="bold">
1056
                    <FormattedMessage
1057
                      id="jobBuilder.skills.statusSmiley.tooMany"
1058
                      defaultMessage="Too Many"
1059
                      description="Description of quantity of skills"
1060
                    />
1061
                  </p>
1062
                  <p data-c-font-size="small">11 +</p>
1063
                </div>
1064
              </div>
1065
            </div>
1066
          </div>
1067
        </div>
1068
        {/* This element is the skills list/management area for the user. From here they can see the skills they've added, modify the order, see the type (occupational [based on classification], cultural, future), see the level they've selected (only if the skill isn't an asset skill), edit the skill, and remove it. */}
1069
        <div
1070
          data-c-background="grey(10)"
1071
          data-c-radius="rounded"
1072
          data-c-padding="all(normal)"
1073
          data-c-margin="top(normal) bottom(normal)"
1074
        >
1075
          <p data-c-font-weight="bold" data-c-margin="bottom(normal)">
1076
            <FormattedMessage
1077
              id="jobBuilder.skills.title.essentialSkills"
1078
              defaultMessage="Essential Skills"
1079
              description="Title of essential skills list"
1080
            />
1081
          </p>
1082
          {/* This is the null state to be used when the user lands on the page for the first time. Be sure to include it in the assets list too! Note that it exists outside the skill-list div to avoid it being confused with the list of skills. */}
1083
          {/* Null state. */}
1084
          {essentialCount === 0 && renderNullCriteriaRow()}
1085
          <ol className="jpb-skill-list" data-tc-up-down-list>
1086
            {/* This is an individual skill. I've handled the up/down script and the modal trigger, but I'll leave managing the value of the skill's list number, the modal contents,  and the deletion to you folks. I've also migrated the up/down script to a universal one. When it comes to the "jpb-skill", you'll need to add a class that specifies which TYPE of skill it is (occupational, cultural, future). This will handle interior colour/icon changes. */}
1087
            {essentialCriteria.map(renderCriteriaRow)}
1088
          </ol>
1089
          {/* Repeat what you have above for asset skills. The biggest thing to note here is that the level should be empty in this list, and when the user changes the level of an essential skill to asset, it should be moved down into this list (and vice versa). */}
1090
          <p
1091
            data-c-font-weight="bold"
1092
            data-c-margin="top(normal) bottom(normal)"
1093
          >
1094
            <FormattedMessage
1095
              id="jobBuilder.skills.title.assetSkills"
1096
              defaultMessage="Asset Skills"
1097
              description="Title of asset skills list"
1098
            />
1099
          </p>
1100
          {/* Asset null state goes here. */}
1101
          {assetCount === 0 && renderNullCriteriaRow()}
1102
          <ol className="jpb-skill-list" data-tc-up-down-list>
1103
            {assetCriteria.map(renderCriteriaRow)}
1104
          </ol>
1105
        </div>
1106
        <div
1107
          data-c-margin="bottom(triple)"
1108
          data-c-align="base(centre) tl(right)"
1109
        >
1110
          {/* We'll want this button to functionally be the exact same as the button at the bottom of the page, where it saves the data, and opens the preview modal. */}
1111
          {submitButton}
1112
        </div>
1113
        {/* The 3 sections below are each functionally similar and can probably be united into one component. The biggest difference between the three is that "Cultural Skills" has a categorical breakdown between "Recommended Skills" and the rest of the category. These recommendations are based directly on the way the manager answered their work environment questions, but I'm not sure how the logic works, so you'll want to check in with Lauren/Jasmita on this. */}
1114
        <h4
1115
          data-c-colour="c2"
1116
          data-c-font-size="h4"
1117
          data-c-margin="bottom(normal)"
1118
        >
1119
          <FormattedMessage
1120
            id="jobBuilder.skills.title.skillSelection"
1121
            defaultMessage="Skill Selection"
1122
            description="Title of skill selection section"
1123
          />
1124
        </h4>
1125
        {/* Occupational Skills */}
1126
        {/* You can modify colour/icon using the category classes here again (occupational, cultural, future) on the "jpb-skill-category" element. */}
1127
        <div
1128
          id="jpb-occupational-skills"
1129
          className="jpb-skill-category occupational"
1130
          data-c-margin="bottom(normal)"
1131
          data-c-padding="normal"
1132
          data-c-radius="rounded"
1133
          data-c-background="grey(10)"
1134
        >
1135
          <div data-c-grid="gutter top">
1136
            <div data-c-grid-item="tp(2of3) ds(3of4)">
1137
              <h5
1138
                className="jpb-skill-section-title"
1139
                data-c-font-size="h4"
1140
                data-c-margin="bottom(normal)"
1141
              >
1142
                {/* These icons will change automatically based on the class specified above. */}
1143
                <span
1144
                  data-c-font-size="small"
1145
                  data-c-margin="right(half)"
1146
                  data-c-padding="tb(quarter) rl(half)"
1147
                  data-c-radius="rounded"
1148
                  data-c-colour="white"
1149
                >
1150
                  <i className="fas fa-briefcase" />
1151
                  <i className="fas fa-coffee" />
1152
                  <i className="fas fa-certificate" />
1153
                  <i className="fas fa-book" />
1154
                </span>
1155
                {/* Category Title */}
1156
                <FormattedMessage
1157
                  id="jobBuilder.skills.title.occupationalSkills"
1158
                  defaultMessage="Occupational Competencies"
1159
                  description="Title of skills category"
1160
                />
1161
              </h5>
1162
              {/* Category description - basically this outlines what the category means. */}
1163
              {/* <p>
1164
                // TODO: Add this message back in once we have copy.
1165
                <FormattedMessage
1166
                  id="jobBuilder.skills.description.occupationalSkills"
1167
                  defaultMessage=""
1168
                  description="Description of a category of skills"
1169
                />
1170
              </p> */}
1171
            </div>
1172
            <div
1173
              data-c-grid-item="tp(1of3) ds(1of4)"
1174
              data-c-align="base(centre) tp(right)"
1175
            >
1176
              {/* This target value changes depending on the category (occupational has 3 - 4, cultural and future have fewer) - you can see these values in their respective sections below. You can also add a "complete" class to this "jpb-skill-target" element to change the target icon to a checkmark to indicate to the user that they're within the range. Note that the other two categories (cultural and future) start their ranges at 0, so the "complete" class should be on those sections by default. */}
1177
              <div
1178
                className={`jpb-skill-target ${
1179
                  countInRange(
1180
                    minOccupational,
1181
                    maxOccupational,
1182
                    occupationalCriteria.length,
1183
                  )
1184
                    ? "complete"
1185
                    : ""
1186
                }`}
1187
              >
1188
                <i data-c-colour="stop" className="fas fa-bullseye" />
1189
                <i data-c-colour="go" className="fas fa-check" />
1190
                <span>
1191
                  <FormattedMessage
1192
                    id="jobBuilder.skills.range.occupationalSkills"
1193
                    defaultMessage="Aim for {minOccupational} - {maxOccupational} skills."
1194
                    description="Ranage recommendation for occupational competencies in job poster"
1195
                    values={{ minOccupational, maxOccupational }}
1196
                  />
1197
                </span>
1198
              </div>
1199
            </div>
1200
            {/* This is the list of skills. Clicking a skill button should trigger the "Edit skill" modal so that the user can edit the definition/level before adding it. If they DO add it, you can assign an "active" class to the respective button so indicate that it's selected. This will change it's colour and icon automatically. This is also the area where "Culture Skills" is split into the two categories - see the Culture Skills section below for what that looks like. */}
1201
            {(job.classification_code === "" ||
1202
              job.classification_code === null) && (
1203
              <p data-c-font-weight="bold" data-c-grid-item="base(1of1)">
1204
                <FormattedMessage
1205
                  id="jobBuilder.skills.nullText.occupationalSkills"
1206
                  defaultMessage="You must return to Step 1 and choose a Classification."
1207
                  description="Placeholder text for occupational competencies list."
1208
                />
1209
              </p>
1210
            )}
1211
1212
            <ul className="jpb-skill-cloud" data-c-grid-item="base(1of1)">
1213
              {occupationalSkills.map(renderSkillButton)}
1214
            </ul>
1215
          </div>
1216
        </div>
1217
        {/* Cultural Skills */}
1218
        {/* This section is here so that you can see the categorical division of culture skills. */}
1219
        <div
1220
          className="jpb-skill-category cultural"
1221
          data-c-margin="bottom(normal)"
1222
          data-c-padding="normal"
1223
          data-c-radius="rounded"
1224
          data-c-background="grey(10)"
1225
        >
1226
          <div data-c-grid="gutter top">
1227
            <div data-c-grid-item="tp(2of3) ds(3of4)">
1228
              <h5
1229
                className="jpb-skill-section-title"
1230
                data-c-font-size="h4"
1231
                data-c-margin="bottom(normal)"
1232
              >
1233
                <span
1234
                  data-c-font-size="small"
1235
                  data-c-margin="right(half)"
1236
                  data-c-padding="tb(quarter) rl(half)"
1237
                  data-c-radius="rounded"
1238
                  data-c-colour="white"
1239
                >
1240
                  <i className="fas fa-briefcase" />
1241
                  <i className="fas fa-coffee" />
1242
                  <i className="fas fa-certificate" />
1243
                  <i className="fas fa-book" />
1244
                </span>
1245
                <FormattedMessage
1246
                  id="jobBuilder.skills.title.culturalSkills"
1247
                  defaultMessage="Behavioural Competencies"
1248
                  description="Title of skills category"
1249
                />
1250
              </h5>
1251
              {/* <p>
1252
              // TODO: Add this message back in once we have copy.
1253
                <FormattedMessage
1254
                  id="jobBuilder.skills.description.culturalSkills"
1255
                  defaultMessage=""
1256
                  description="Description of a category of skills"
1257
                />
1258
              </p> */}
1259
            </div>
1260
            <div
1261
              data-c-grid-item="tp(1of3) ds(1of4)"
1262
              data-c-align="base(centre) tp(right)"
1263
            >
1264
              <div
1265
                className={`jpb-skill-target ${
1266
                  countInRange(minCulture, maxCulture, cultureCriteria.length)
1267
                    ? "complete"
1268
                    : ""
1269
                }`}
1270
              >
1271
                <i data-c-colour="stop" className="fas fa-bullseye" />
1272
                <i data-c-colour="go" className="fas fa-check" />
1273
                <span>
1274
                  <FormattedMessage
1275
                    id="jobBuilder.skills.range.culturalSkills"
1276
                    defaultMessage="Aim for {minCulture} - {maxCulture} skills."
1277
                    description="Range recommendation for behavioural competencies in job poster"
1278
                    values={{ minCulture, maxCulture }}
1279
                  />
1280
                </span>
1281
              </div>
1282
            </div>
1283
            {/** Culture skills are intended to be split into two lists, Recommended and Remaining. Until the recommendation logic is nailed down, its just one. */}
1284
            <ul className="jpb-skill-cloud" data-c-grid-item="base(1of1)">
1285
              {cultureSkills.map(renderSkillButton)}
1286
            </ul>
1287
            {/* So here's where culture skills get broken into categories. In theory this logic will be used down the road to break occupational skills into occupations (e.g. CS - UX Designer), but for now this the only instance where it happens. */}
1288
            {/* <ul className="jpb-skill-cloud" data-c-grid-item="base(1of1)">
1289
              // Note that this "p" tag has a different margin value than the one in the "ul" below.
1290
              <p
1291
                data-c-font-weight="bold"
1292
                data-c-margin="top(half) bottom(normal)"
1293
              >
1294
                Recommended Skills:
1295
              </p>
1296
              // This is where the skill recommendations from Work Environment go.
1297
              <li>
1298
                <button
1299
                  className="jpb-skill-trigger"
1300
                  data-c-button="outline(c1)"
1301
                  data-c-radius="rounded"
1302
                >
1303
                  <i className="fas fa-plus-circle" />
1304
                  <i className="fas fa-minus-circle" />
1305
                  Skill Name
1306
                </button>
1307
              </li>
1308
            </ul>
1309
            <ul className="jpb-skill-cloud" data-c-grid-item="base(1of1)">
1310
              <p
1311
                data-c-font-weight="bold"
1312
                data-c-margin="top(normal) bottom(normal)"
1313
              >
1314
                Remaining Skills:
1315
              </p>
1316
              // This is where the remaining culture skills go. Please make sure that the skills in the recommendation list above do not appear here.
1317
              <li>
1318
                <button
1319
                  className="jpb-skill-trigger"
1320
                  data-c-button="outline(c1)"
1321
                  data-c-radius="rounded"
1322
                >
1323
                  <i className="fas fa-plus-circle" />
1324
                  <i className="fas fa-minus-circle" />
1325
                  Skill Name
1326
                </button>
1327
              </li>
1328
            </ul> */}
1329
          </div>
1330
        </div>
1331
        {/* Future Skills */}
1332
        {/* This section is just here so you can see what it looks like with the future class. */}
1333
        <div
1334
          className="jpb-skill-category future"
1335
          data-c-margin="bottom(normal)"
1336
          data-c-padding="normal"
1337
          data-c-radius="rounded"
1338
          data-c-background="grey(10)"
1339
        >
1340
          <div data-c-grid="gutter top">
1341
            <div data-c-grid-item="tp(2of3) ds(3of4)">
1342
              <h5
1343
                className="jpb-skill-section-title"
1344
                data-c-font-size="h4"
1345
                data-c-margin="bottom(normal)"
1346
              >
1347
                <span
1348
                  data-c-font-size="small"
1349
                  data-c-margin="right(half)"
1350
                  data-c-padding="tb(quarter) rl(half)"
1351
                  data-c-radius="rounded"
1352
                  data-c-colour="white"
1353
                >
1354
                  <i className="fas fa-briefcase" />
1355
                  <i className="fas fa-coffee" />
1356
                  <i className="fas fa-certificate" />
1357
                  <i className="fas fa-book" />
1358
                </span>
1359
                <FormattedMessage
1360
                  id="jobBuilder.skills.title.futureSkills"
1361
                  defaultMessage="Public Service Competencies"
1362
                  description="Title of skills category"
1363
                />
1364
              </h5>
1365
              {/* <p>
1366
              // TODO: Add this message back in once we have copy.
1367
                <FormattedMessage
1368
                  id="jobBuilder.skills.description.futureSkills"
1369
                  defaultMessage=""
1370
                  description="Description of a category of skills"
1371
                />
1372
              </p> */}
1373
            </div>
1374
            <div
1375
              data-c-grid-item="tp(1of3) ds(1of4)"
1376
              data-c-align="base(centre) tp(right)"
1377
            >
1378
              <div
1379
                className={`jpb-skill-target ${
1380
                  countInRange(minFuture, maxFuture, futureCriteria.length)
1381
                    ? "complete"
1382
                    : ""
1383
                }`}
1384
              >
1385
                <i data-c-colour="stop" className="fas fa-bullseye" />
1386
                <i data-c-colour="go" className="fas fa-check" />
1387
                <span>
1388
                  <FormattedMessage
1389
                    id="jobBuilder.skills.range.futureSkills"
1390
                    defaultMessage="Aim for {minFuture} - {maxFuture} skills."
1391
                    description="Ranage recommendation for public service competencies in job poster"
1392
                    values={{ minFuture, maxFuture }}
1393
                  />
1394
                </span>
1395
              </div>
1396
            </div>
1397
            <ul className="jpb-skill-cloud" data-c-grid-item="base(1of1)">
1398
              {futureSkills.map(renderSkillButton)}
1399
            </ul>
1400
          </div>
1401
        </div>
1402
        {/* This section is basically just text, but it prompts the manager to get in touch with us if they can't find the skill they're looking for. */}
1403
        {/* "Custom" Skills */}
1404
        <h5 data-c-font-weight="bold" data-c-margin="top(double) bottom(half)">
1405
          <FormattedMessage
1406
            id="jobBuilder.skills.title.missingSkill"
1407
            defaultMessage="Can't find the skill you need?"
1408
            description="Title of instructions for missing skill"
1409
          />
1410
        </h5>
1411
        <p data-c-margin="bottom(normal)">
1412
          {/* TODO: Refactor for react-intl version 3, using new rich text xml tag syntax eg.
1413
          <FormattedMessage
1414
            id="jobBuilder.skills.instructions.missingSkills"
1415
            defaultMessage="Building a skills list is a huge endeavour, and it's not
1416
  surprising that Talent Cloud's list doesn't have the skill
1417
  you're looking for. To help us expand our skill list, please <link>get in touch with us through email</link>. Provide the skill's name, as well as a short description to
1418
  kick-off the discussion."
1419
            values={{
1420
              link: msg => (
1421
                <a href="mailto:[email protected]">
1422
                  {msg}
1423
                </a>
1424
              ),
1425
            }}
1426
          /> */}
1427
          <FormattedMessage
1428
            id="jobBuilder.skills.instructions.missingSkills"
1429
            defaultMessage="Building a skills list is a huge endeavour, and it's not
1430
  surprising that Talent Cloud's list doesn't have the skill
1431
  you're looking for. To help us expand our skill list, please {link}. Provide the skill's name, as well as a short description to
1432
  kick-off the discussion."
1433
            values={{
1434
              link: (
1435
                <a href="mailto:[email protected]">
1436
                  {intl.formatMessage(messages.emailUs)}
1437
                </a>
1438
              ),
1439
            }}
1440
          />
1441
        </p>
1442
        <div
1443
          className="jpb-skill-category optional"
1444
          data-c-margin="bottom(normal)"
1445
          data-c-padding="normal"
1446
          data-c-radius="rounded"
1447
          data-c-background="grey(10)"
1448
        >
1449
          <div data-c-grid="gutter top">
1450
            <div data-c-grid-item="base(1of1)">
1451
              {/** TODO: Fix the layout of the skill cloud */}
1452
              <h5 className="jpb-skill-section-title" data-c-font-size="h4">
1453
                <span
1454
                  data-c-font-size="small"
1455
                  data-c-margin="right(half)"
1456
                  data-c-padding="tb(quarter) rl(half)"
1457
                  data-c-radius="rounded"
1458
                  data-c-colour="white"
1459
                >
1460
                  <i className="fas fa-briefcase" />
1461
                  <i className="fas fa-coffee" />
1462
                  <i className="fas fa-certificate" />
1463
                  <i className="fas fa-book" />
1464
                </span>
1465
                <FormattedMessage
1466
                  id="jobBuilder.skills.title.otherSkills"
1467
                  defaultMessage="Other Skills"
1468
                  description="Title of other skills section"
1469
                />
1470
              </h5>
1471
            </div>
1472
            <div data-c-grid-item="base(1of1)">
1473
              <Select
1474
                id="jpb-all-skills-select"
1475
                name="jpbAllSkillsSelect"
1476
                label={intl.formatMessage(messages.selectSkillLabel)}
1477
                selected={null}
1478
                nullSelection={intl.formatMessage(messages.selectSkillNull)}
1479
                options={unselectedOtherSkills.map(
1480
                  (skill): SelectOption => ({
1481
                    value: skill.id,
1482
                    label: skill[locale].name,
1483
                  }),
1484
                )}
1485
                onChange={(event): void => {
1486
                  const skillId = Number(event.target.value);
1487
                  if (hasKey(skillsById, skillId)) {
1488
                    const skill = skillsById[skillId];
1489
                    setSkillBeingAdded(skill);
1490
                  }
1491
                }}
1492
              />
1493
            </div>
1494
            <ul className="jpb-skill-cloud" data-c-grid-item="base(1of1)">
1495
              {/** TODO: Get this null state text hiding/showing. */}
1496
              {selectedOtherSkills.length === 0 && (
1497
                <p>
1498
                  <FormattedMessage
1499
                    id="jobBuilder.skills.placeholder.otherSkills"
1500
                    defaultMessage="There are no extra skills added."
1501
                    description="Placeholder when there are no other skills"
1502
                  />
1503
                </p>
1504
              )}
1505
              {selectedOtherSkills.map(renderSkillButton)}
1506
            </ul>
1507
          </div>
1508
        </div>
1509
        <div data-c-grid="gutter">
1510
          <div data-c-grid-item="base(1of1)">
1511
            <hr data-c-margin="top(normal) bottom(normal)" />
1512
          </div>
1513
          <div
1514
            data-c-alignment="base(centre) tp(left)"
1515
            data-c-grid-item="tp(1of2)"
1516
          >
1517
            <button
1518
              data-c-button="outline(c2)"
1519
              data-c-radius="rounded"
1520
              type="button"
1521
              disabled={isSaving}
1522
              onClick={(): void => saveAndReturn()}
1523
            >
1524
              <FormattedMessage
1525
                id="jobBuilder.skills.button.returnToTasks"
1526
                defaultMessage="Save &amp; Return to Tasks"
1527
                description="Button Label"
1528
              />
1529
            </button>
1530
          </div>
1531
          <div
1532
            data-c-alignment="base(centre) tp(right)"
1533
            data-c-grid-item="tp(1of2)"
1534
          >
1535
            {/* Modal trigger, same as last step. */}
1536
            {submitButton}
1537
1538
            <div
1539
              role="alert"
1540
              data-c-alert="error"
1541
              data-c-radius="rounded"
1542
              data-c-margin="top(normal)"
1543
              data-c-padding="all(half)"
1544
              data-c-visibility={
1545
                essentialCount === 0 && submitTouched ? "visible" : "invisible"
1546
              }
1547
              style={{
1548
                display: `inline-block`,
1549
              }}
1550
            >
1551
              <a
1552
                href="#jpb-occupational-skills"
1553
                tabIndex={0}
1554
                ref={errorMessage}
1555
              >
1556
                <FormattedMessage
1557
                  id="jobBuilder.skills.essentialSkillRequiredError"
1558
                  defaultMessage="At least one 'Essential Skill' is required."
1559
                  description="Label of Button"
1560
                />
1561
              </a>
1562
            </div>
1563
          </div>
1564
        </div>
1565
      </div>
1566
      <div data-c-dialog-overlay={isModalVisible ? "active" : ""} />
1567
      {/** This modal simply displays key tasks. */}
1568
      <Modal
1569
        id={tasksModalId}
1570
        parentElement={modalParentRef.current}
1571
        visible={tasksModalVisible}
1572
        onModalCancel={(): void => setTasksModalVisible(false)}
1573
        onModalConfirm={(): void => setTasksModalVisible(false)}
1574
      >
1575
        <Modal.Header>
1576
          <div
1577
            data-c-background="c1(100)"
1578
            data-c-border="bottom(thin, solid, black)"
1579
            data-c-padding="normal"
1580
          >
1581
            <h5
1582
              data-c-colour="white"
1583
              data-c-font-size="h4"
1584
              id={`${tasksModalId}-title`}
1585
            >
1586
              <FormattedMessage
1587
                id="jobBuilder.skills.title.keyTasks"
1588
                defaultMessage="Key Tasks"
1589
                description="Title of Key Tasks Section"
1590
              />
1591
            </h5>
1592
          </div>
1593
        </Modal.Header>
1594
        <Modal.Body>
1595
          <div data-c-border="bottom(thin, solid, black)">
1596
            <div
1597
              data-c-border="bottom(thin, solid, black)"
1598
              data-c-padding="normal"
1599
              id={`${tasksModalId}-description`}
1600
            >
1601
              <ul>
1602
                {keyTasks.map(
1603
                  (task): React.ReactElement => (
1604
                    <li key={task.id}>{task[locale].description}</li>
1605
                  ),
1606
                )}
1607
              </ul>
1608
            </div>
1609
          </div>
1610
        </Modal.Body>
1611
        <Modal.Footer>
1612
          <Modal.FooterCancelBtn>
1613
            <FormattedMessage
1614
              id="jobBuilder.skills.tasksModalCancelLabel"
1615
              defaultMessage="Back to Skills"
1616
              description="The text displayed on the cancel button of the Key Tasks modal on the Job Builder Skills step."
1617
            />
1618
          </Modal.FooterCancelBtn>
1619
        </Modal.Footer>
1620
      </Modal>
1621
      {/** This modal is for adding brand new skills */}
1622
      <Modal
1623
        id={addModalId}
1624
        parentElement={modalParentRef.current}
1625
        visible={skillBeingAdded !== null}
1626
        onModalCancel={(): void => {
1627
          setSkillBeingAdded(null);
1628
        }}
1629
        onModalConfirm={(): void => {
1630
          setSkillBeingAdded(null);
1631
        }}
1632
      >
1633
        <Modal.Header>
1634
          <div
1635
            data-c-background="c1(100)"
1636
            data-c-border="bottom(thin, solid, black)"
1637
            data-c-padding="normal"
1638
          >
1639
            <h5
1640
              data-c-colour="white"
1641
              data-c-font-size="h4"
1642
              id={`${addModalId}-title`}
1643
            >
1644
              <FormattedMessage
1645
                id="jobBuilder.skills.title.addASkill"
1646
                defaultMessage="Add a skill"
1647
                description="Title of Add a skill Section"
1648
              />
1649
            </h5>
1650
          </div>
1651
        </Modal.Header>
1652
        <Modal.Body>
1653
          {skillBeingAdded !== null && (
1654
            <CriteriaForm
1655
              jobPosterId={job.id}
1656
              skill={skillBeingAdded}
1657
              handleCancel={(): void => {
1658
                setSkillBeingAdded(null);
1659
              }}
1660
              handleSubmit={(criteria: Criteria): void => {
1661
                criteriaDispatch({ type: "add", payload: criteria });
1662
                setSkillBeingAdded(null);
1663
              }}
1664
            />
1665
          )}
1666
        </Modal.Body>
1667
      </Modal>
1668
      {/** This modal is for editing already added skills */}
1669
      <Modal
1670
        id={editModalId}
1671
        parentElement={modalParentRef.current}
1672
        visible={criteriaBeingEdited !== null}
1673
        onModalCancel={(): void => {
1674
          setCriteriaBeingEdited(null);
1675
        }}
1676
        onModalConfirm={(): void => {
1677
          setCriteriaBeingEdited(null);
1678
        }}
1679
      >
1680
        <Modal.Header>
1681
          <div
1682
            data-c-background="c1(100)"
1683
            data-c-border="bottom(thin, solid, black)"
1684
            data-c-padding="normal"
1685
          >
1686
            <h5
1687
              data-c-colour="white"
1688
              data-c-font-size="h4"
1689
              id={`${editModalId}-title`}
1690
            >
1691
              <FormattedMessage
1692
                id="jobBuilder.skills.title.editSkill"
1693
                defaultMessage="Edit skill"
1694
                description="Title of Edit skill Modal"
1695
              />
1696
            </h5>
1697
          </div>
1698
        </Modal.Header>
1699
        <Modal.Body>
1700
          {criteriaBeingEdited !== null &&
1701
            getSkillOfCriteria(criteriaBeingEdited) !== null && (
1702
              <CriteriaForm
1703
                jobPosterId={job.id}
1704
                criteria={criteriaBeingEdited}
1705
                skill={getSkillOfCriteria(criteriaBeingEdited) as Skill} // The cast is okay here (but still not ideal) because of the !== null check a few lines up
1706
                handleCancel={(): void => {
1707
                  setCriteriaBeingEdited(null);
1708
                }}
1709
                handleSubmit={(criteria: Criteria): void => {
1710
                  criteriaDispatch({ type: "edit", payload: criteria });
1711
                  setCriteriaBeingEdited(null);
1712
                }}
1713
              />
1714
            )}
1715
        </Modal.Body>
1716
      </Modal>
1717
      {/** This modal is the preview */}
1718
      <Modal
1719
        id={previewModalId}
1720
        parentElement={modalParentRef.current}
1721
        visible={isPreviewVisible}
1722
        onModalCancel={(): void => setIsPreviewVisible(false)}
1723
        onModalConfirm={(): void => handleContinue()}
1724
        onModalMiddle={(): void => {
1725
          handleSkipToReview();
1726
        }}
1727
      >
1728
        <Modal.Header>
1729
          <div
1730
            data-c-background="c1(100)"
1731
            data-c-border="bottom(thin, solid, black)"
1732
            data-c-padding="normal"
1733
          >
1734
            <h5
1735
              data-c-colour="white"
1736
              data-c-font-size="h4"
1737
              id={`${previewModalId}-title`}
1738
            >
1739
              <FormattedMessage
1740
                id="jobBuilder.skills.title.keepItUp"
1741
                defaultMessage="Keep it up!"
1742
                description="Title of Keep it up! Modal"
1743
              />
1744
            </h5>
1745
          </div>
1746
        </Modal.Header>
1747
        <Modal.Body>
1748
          <div data-c-border="bottom(thin, solid, black)">
1749
            <div
1750
              data-c-border="bottom(thin, solid, black)"
1751
              data-c-padding="normal"
1752
              id={`${previewModalId}-description`}
1753
            >
1754
              <p>
1755
                <FormattedMessage
1756
                  id="jobBuilder.skills.description.keepItUp"
1757
                  defaultMessage="Here's a preview of the Skills you just entered. Feel free to
1758
                go back and edit things or move to the next step if you're
1759
                happy with it."
1760
                  description="Body text of Keep it up! Modal"
1761
                />
1762
              </p>
1763
            </div>
1764
1765
            <div data-c-background="grey(20)" data-c-padding="normal">
1766
              <div
1767
                className="manager-job-card"
1768
                data-c-background="white(100)"
1769
                data-c-padding="normal"
1770
                data-c-radius="rounded"
1771
              >
1772
                <h4
1773
                  data-c-border="bottom(thin, solid, black)"
1774
                  data-c-font-size="h4"
1775
                  data-c-font-weight="600"
1776
                  data-c-margin="bottom(normal)"
1777
                  data-c-padding="bottom(normal)"
1778
                >
1779
                  <FormattedMessage
1780
                    id="jobBuilder.skills.title.needsToHave"
1781
                    defaultMessage="Skills the Employee Needs to Have"
1782
                    description="Section Header in Modal"
1783
                  />
1784
                </h4>
1785
                {essentialCriteria.length === 0 ? (
1786
                  <p>
1787
                    <FormattedMessage
1788
                      id="jobBuilder.skills.nullState"
1789
                      defaultMessage="You haven't added any skills yet."
1790
                      description="The text displayed in the skills modal when you haven't added any skills."
1791
                    />
1792
                  </p>
1793
                ) : (
1794
                  essentialCriteria.map(
1795
                    (criterion): React.ReactElement | null => {
1796
                      const skill = getSkillOfCriteria(criterion);
1797
                      if (skill === null) {
1798
                        return null;
1799
                      }
1800
                      return (
1801
                        <Criterion
1802
                          criterion={criterion}
1803
                          skill={skill}
1804
                          key={skill.id}
1805
                        />
1806
                      );
1807
                    },
1808
                  )
1809
                )}
1810
                <h4
1811
                  data-c-border="bottom(thin, solid, black)"
1812
                  data-c-font-size="h4"
1813
                  data-c-font-weight="600"
1814
                  data-c-margin="top(double) bottom(normal)"
1815
                  data-c-padding="bottom(normal)"
1816
                >
1817
                  <FormattedMessage
1818
                    id="jobBuilder.skills.title.niceToHave"
1819
                    defaultMessage="Skills That Would Be Nice For the Employee to Have"
1820
                    description="Section Header in Modal"
1821
                  />
1822
                </h4>
1823
                {assetCriteria.length === 0 ? (
1824
                  <p>
1825
                    <FormattedMessage
1826
                      id="jobBuilder.skills.nullState"
1827
                      defaultMessage="You haven't added any skills yet."
1828
                      description="The text displayed in the skills modal when you haven't added any skills."
1829
                    />
1830
                  </p>
1831
                ) : (
1832
                  assetCriteria.map((criterion): React.ReactElement | null => {
1833
                    const skill = getSkillOfCriteria(criterion);
1834
                    if (skill === null) {
1835
                      return null;
1836
                    }
1837
                    return (
1838
                      <Criterion
1839
                        criterion={criterion}
1840
                        skill={skill}
1841
                        key={skill.id}
1842
                      />
1843
                    );
1844
                  })
1845
                )}
1846
              </div>
1847
            </div>
1848
          </div>
1849
        </Modal.Body>
1850
        <Modal.Footer>
1851
          <Modal.FooterCancelBtn>
1852
            <FormattedMessage
1853
              id="jobBuilder.skills.previewModalCancelLabel"
1854
              defaultMessage="Go Back"
1855
              description="The text displayed on the cancel button of the Job Builder Skills Preview modal."
1856
            />
1857
          </Modal.FooterCancelBtn>
1858
          {jobIsComplete && (
1859
            <Modal.FooterMiddleBtn>
1860
              <FormattedMessage
1861
                id="jobBuilder.skills.previewModalMiddleLabel"
1862
                defaultMessage="Skip to Review"
1863
                description="The text displayed on the 'Skip to Review' button of the Job Builder Skills Preview modal."
1864
              />
1865
            </Modal.FooterMiddleBtn>
1866
          )}
1867
          <Modal.FooterConfirmBtn>
1868
            <FormattedMessage
1869
              id="jobBuilder.skills.previewModalConfirmLabel"
1870
              defaultMessage="Next Step"
1871
              description="The text displayed on the confirm button of the Job Builder Skills Preview modal."
1872
            />
1873
          </Modal.FooterConfirmBtn>
1874
        </Modal.Footer>
1875
      </Modal>
1876
    </>
1877
  );
1878
};
1879
1880
export default injectIntl(JobBuilderSkills);
1881