Passed
Push — feature/azure-webapp-pipeline-... ( f0cd11...ed5482 )
by Grant
08:04 queued 10s
created

resources/assets/js/components/FindSkillsModal.tsx   A

Complexity

Total Complexity 11
Complexity/F 0

Size

Lines of Code 552
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 11
eloc 434
mnd 11
bc 11
fnc 0
dl 0
loc 552
bpm 0
cpm 0
noi 0
c 0
b 0
f 0
rs 10
1
import React, { useState } from "react";
2
import { defineMessages, useIntl } from "react-intl";
3
import {
4
  getLocale,
5
  localizeFieldNonNull,
6
  matchStringsCaseDiacriticInsensitive,
7
} from "../helpers/localize";
8
import { addOrRemove } from "../helpers/queries";
9
import { Skill, SkillCategory } from "../models/types";
10
import SearchBar from "./SearchBar";
11
import Accordion from "./H2Components/Accordion";
12
import Dialog from "./H2Components/Dialog";
13
14
const messages = defineMessages({
15
  modalHeading: {
16
    id: "findSkillsModal.modalHeading",
17
    defaultMessage: "Find and add skills",
18
  },
19
  accordianButtonLabel: {
20
    id: "findSkillsModal.accordianButtonLabel",
21
    defaultMessage: "Click to view...",
22
  },
23
  skillsResultsHeading: {
24
    id: "findSkillsModal.skillsResultsHeading",
25
    defaultMessage: "Explore Categories",
26
  },
27
  skillResultsSubHeading: {
28
    id: "findSkillsModal.skillResultsSubHeading",
29
    defaultMessage:
30
      "Click on the categories on the left to explore skills. Only select the skills that you have experience with.",
31
  },
32
  skills: {
33
    id: "findSkillsModal.skills",
34
    defaultMessage: "Skills",
35
  },
36
  noSkills: {
37
    id: "findSkillsModal.noSkills",
38
    defaultMessage: "No skills available.",
39
  },
40
  backButton: {
41
    id: "findSkillsModal.backButton",
42
    defaultMessage: "Back",
43
  },
44
  disabledSkillButton: {
45
    id: "findSkillsModal.disabledSkillButton",
46
    defaultMessage: "Already Added",
47
  },
48
  selectSkillButton: {
49
    id: "findSkillsModal.selectSkillButton",
50
    defaultMessage: "Select",
51
  },
52
  removeSkillButton: {
53
    id: "findSkillsModal.removeSkillButton",
54
    defaultMessage: "Remove",
55
  },
56
  cancelButton: {
57
    id: "findSkillsModal.noSkills",
58
    defaultMessage: "Cancel",
59
  },
60
  saveButton: {
61
    id: "findSkillsModal.noSkills",
62
    defaultMessage: "Save Skills",
63
  },
64
  searchResultsTitle: {
65
    id: "findSkillsModal.searchResultsTitle",
66
    defaultMessage: `There are {numOfSkills} results for skills related to "{searchQuery}".`,
67
  },
68
  searchBarInputLabel: {
69
    id: "findSkillsModal.seachBarInputLabel",
70
    defaultMessage: "Search for skills by name:",
71
  },
72
  searchBarInputPlaceholder: {
73
    id: "findSkillsModal.searchBarInputPlaceholder",
74
    defaultMessage: "eg. User interface design.",
75
  },
76
  searchBarButtonLabel: {
77
    id: "findSkillsModal.searchBarButtonLabel",
78
    defaultMessage: "Search Skills",
79
  },
80
});
81
82
interface FindSkillsModalProps {
83
  dialogTrigger: React.ReactElement;
84
  oldSkills: Skill[];
85
  portal: "applicant" | "manager";
86
  skills: Skill[];
87
  skillCategories: SkillCategory[];
88
  handleSubmit: (values: Skill[]) => Promise<void>;
89
}
90
91
const FindSkillsModal: React.FunctionComponent<FindSkillsModalProps> = ({
92
  dialogTrigger,
93
  oldSkills,
94
  portal,
95
  skills,
96
  skillCategories,
97
  handleSubmit,
98
}) => {
99
  const intl = useIntl();
100
  const locale = getLocale(intl.locale);
101
  const parentSkillCategories: SkillCategory[] = skillCategories.filter(
102
    (skillCategory) =>
103
      skillCategory.depth === 1 && skillCategory.parent_id === 0,
104
  );
105
106
  // List of new Skills that will be saved to the user on submit.
107
  const [newSkills, setNewSkills] = useState<Skill[]>([]);
108
  // List of skills that displayed in the results section of the modal.
109
  const [skillsResults, setSkillsResults] = useState<Skill[]>([]);
110
  // Stores the skill category's name and description for the results section.
111
  const [resultsSectionText, setResultsSectionText] = useState<{
112
    title: string;
113
    description: string;
114
  }>({ title: "", description: "" });
115
  const [firstVisit, setFirstVisit] = useState(true);
116
  // Stores a list of skills category keys of which accordions are expanded, for styling purposes.
117
  const [expandedAccordions, setExpandedAccordions] = useState<string[]>([]);
118
  // Used to set the button color of an active skill category.
119
  const [buttonClicked, setButtonClicked] = useState("");
120
121
  /**
122
   * Filters through the skills list for the any skills that match the search query.
123
   * @param searchQuery The search query entered by the user.
124
   */
125
  const handleSkillSearch = (searchQuery: string): Promise<void> => {
126
    if (searchQuery.length === 0) {
127
      setSkillsResults([]);
128
      setFirstVisit(true);
129
      return Promise.resolve();
130
    }
131
132
    const skillNames: string[] = skills.map((skill) =>
133
      localizeFieldNonNull(locale, skill, "name"),
134
    );
135
    const skillStrings: string[] = matchStringsCaseDiacriticInsensitive(
136
      searchQuery,
137
      skillNames,
138
    );
139
    const skillMatches = skills.filter((skill) =>
140
      skillStrings.includes(localizeFieldNonNull(locale, skill, "name")),
141
    );
142
143
    // Set the skillResults state with the matches from the query.
144
    setFirstVisit(false);
145
    setResultsSectionText({
146
      title: intl.formatMessage(messages.searchResultsTitle, {
147
        numOfSkills: skillMatches.length,
148
        searchQuery,
149
      }),
150
      description: "",
151
    });
152
    setSkillsResults(skillMatches);
153
    return Promise.resolve();
154
  };
155
  return (
156
    <div>
157
      <Dialog.Trigger
158
        id="findSkills"
159
        data-h2-button="white, round, solid"
160
        data-h2-card="white, round"
161
        data-h2-padding="b(tb, .5)"
162
      >
163
        {dialogTrigger}
164
      </Dialog.Trigger>
165
      <Dialog id="findSkills" data-h2-radius="b(round)">
166
        <Dialog.Header className="gradient-left-right">
167
          <Dialog.Title
168
            data-h2-padding="b(all, 1)"
169
            data-h2-font-color="b(white)"
170
            data-h2-font-size="b(h4)"
171
          >
172
            {intl.formatMessage(messages.modalHeading)}
173
          </Dialog.Title>
174
        </Dialog.Header>
175
        <Dialog.Content
176
          data-h2-grid="b(top, expanded, flush, 0)"
177
          style={{ height: "35rem", overflow: "auto", alignItems: "stretch" }}
178
        >
179
          {/* Parent Skill Category Accordions Section */}
180
          <div data-h2-grid-item="s(2of5) b(1of1)">
181
            <SearchBar
182
              buttonLabel={intl.formatMessage(messages.searchBarButtonLabel)}
183
              searchLabel={intl.formatMessage(messages.searchBarInputLabel)}
184
              searchPlaceholder={intl.formatMessage(
185
                messages.searchBarInputPlaceholder,
186
              )}
187
              handleSubmit={handleSkillSearch}
188
            />
189
            <ul data-h2-padding="b(left, 0)" className="no-list-style-type">
190
              {parentSkillCategories.map((parentSkillCategory) => {
191
                const { id, key } = parentSkillCategory;
192
                // Get children skill categories of parent skill category.
193
                const childrenSkillCategories = skillCategories.filter(
194
                  (childSkillCategory) =>
195
                    childSkillCategory.depth === 2 &&
196
                    childSkillCategory.parent_id === id,
197
                );
198
                return (
199
                  <li
200
                    key={id}
201
                    data-h2-bg-color={`b(gray-1, ${
202
                      expandedAccordions.includes(key) ? ".5" : "0"
203
                    })`}
204
                    data-h2-padding="b(tb, 1)"
205
                    data-h2-border="b(gray-2, top, solid, thin)"
206
                    data-h2-margin="b(tb, 0)"
207
                  >
208
                    <Accordion triggerPos="left">
209
                      <Accordion.Btn
210
                        type="button"
211
                        addIcon={
212
                          <i
213
                            data-h2-font-weight="b(700)"
214
                            className="fas fa-plus"
215
                          />
216
                        }
217
                        removeIcon={
218
                          <i
219
                            data-h2-font-weight="b(700)"
220
                            className="fas fa-minus"
221
                          />
222
                        }
223
                        onClick={() =>
224
                          setExpandedAccordions(
225
                            addOrRemove(key, expandedAccordions),
226
                          )
227
                        }
228
                      >
229
                        <p data-h2-font-weight="b(700)">
230
                          {localizeFieldNonNull(
231
                            locale,
232
                            parentSkillCategory,
233
                            "name",
234
                          )}
235
                        </p>
236
                      </Accordion.Btn>
237
                      <p
238
                        data-h2-padding="b(top, .25) b(bottom, 1) b(right, .5)"
239
                        data-h2-font-color="b(black)"
240
                        data-h2-font-size="b(small)"
241
                        style={{ paddingLeft: "5rem" }}
242
                      >
243
                        {localizeFieldNonNull(
244
                          locale,
245
                          parentSkillCategory,
246
                          "description",
247
                        )}
248
                      </p>
249
                      <Accordion.Content>
250
                        <ul
251
                          data-h2-padding="b(all, 0)"
252
                          className="no-list-style-type"
253
                        >
254
                          {childrenSkillCategories.map(
255
                            (childSkillCatergory) => {
256
                              return (
257
                                <li key={childSkillCatergory.key}>
258
                                  <div
259
                                    data-h2-grid="b(middle, expanded, flush, 0)"
260
                                    data-h2-margin="b(right, .5)"
261
                                  >
262
                                    <div
263
                                      data-h2-align="b(left)"
264
                                      data-h2-grid-item="b(5of6)"
265
                                    >
266
                                      <button
267
                                        data-h2-button=""
268
                                        type="button"
269
                                        onClick={() => {
270
                                          setFirstVisit(false);
271
                                          setButtonClicked(
272
                                            childSkillCatergory.key,
273
                                          );
274
                                          setResultsSectionText({
275
                                            title: `${localizeFieldNonNull(
276
                                              locale,
277
                                              childSkillCatergory,
278
                                              "name",
279
                                            )} ${intl.formatMessage(
280
                                              messages.skills,
281
                                            )}`,
282
                                            description: localizeFieldNonNull(
283
                                              locale,
284
                                              childSkillCatergory,
285
                                              "description",
286
                                            ),
287
                                          });
288
                                          setSkillsResults(
289
                                            childSkillCatergory.skills,
290
                                          );
291
                                        }}
292
                                      >
293
                                        <p
294
                                          data-h2-button-label
295
                                          data-h2-font-weight="b(700)"
296
                                          data-h2-display="b(block)"
297
                                          data-h2-font-style={`${
298
                                            buttonClicked ===
299
                                            childSkillCatergory.key
300
                                              ? "b(none)"
301
                                              : "b(underline)"
302
                                          }`}
303
                                          data-h2-font-color={`${
304
                                            buttonClicked ===
305
                                            childSkillCatergory.key
306
                                              ? "b(theme-1)"
307
                                              : "b(black)"
308
                                          }`}
309
                                          data-h2-align="b(left)"
310
                                        >
311
                                          {localizeFieldNonNull(
312
                                            locale,
313
                                            childSkillCatergory,
314
                                            "name",
315
                                          )}
316
                                        </p>
317
                                      </button>
318
                                    </div>
319
                                    <div
320
                                      data-h2-grid-item="b(1of6)"
321
                                      data-h2-align="b(center)"
322
                                      data-h2-radius="b(round)"
323
                                      data-h2-bg-color={`${
324
                                        buttonClicked ===
325
                                        childSkillCatergory.key
326
                                          ? "b(theme-1, 1)"
327
                                          : "b(white, 1)"
328
                                      }`}
329
                                      data-h2-font-color={`${
330
                                        buttonClicked ===
331
                                        childSkillCatergory.key
332
                                          ? "b(white)"
333
                                          : "b(black)"
334
                                      }`}
335
                                    >
336
                                      <p>{childSkillCatergory.skills.length}</p>
337
                                    </div>
338
                                  </div>
339
                                </li>
340
                              );
341
                            },
342
                          )}
343
                        </ul>
344
                      </Accordion.Content>
345
                    </Accordion>
346
                  </li>
347
                );
348
              })}
349
            </ul>
350
          </div>
351
          {/* Skill Results Section */}
352
          <div
353
            data-h2-grid-item="s(3of5) b(1of1)"
354
            data-h2-border="s(gray-2, left, solid, thin) b(gray-2, top, solid, thin)"
355
          >
356
            {firstVisit ? (
357
              <div
358
                data-h2-padding="s(tb, 5) s(right, 3) s(left, 4) b(bottom, 3) b(rl, 2)"
359
                data-h2-container="b(center, large)"
360
              >
361
                <p
362
                  data-h2-font-size="b(h4)"
363
                  data-h2-font-weight="b(700)"
364
                  data-h2-padding="b(top, 3) b(bottom, 1)"
365
                >
366
                  <i
367
                    data-h2-padding="b(right, .5)"
368
                    className="fas fa-arrow-left"
369
                  />
370
                  {intl.formatMessage(messages.skillsResultsHeading)}
371
                </p>
372
                <p>{intl.formatMessage(messages.skillResultsSubHeading)}</p>
373
              </div>
374
            ) : (
375
              <div data-h2-padding="s(rl, 0) b(bottom, 3) b(rl, 1)">
376
                {/* Back Button */}
377
                <button
378
                  data-h2-button
379
                  type="button"
380
                  data-h2-padding="b(all, 1)"
381
                  onClick={() => {
382
                    setFirstVisit(true);
383
                    setButtonClicked("");
384
                    setSkillsResults([]);
385
                  }}
386
                >
387
                  <p
388
                    data-h2-button-label
389
                    data-h2-font-weight="b(700)"
390
                    data-h2-font-style="b(underline)"
391
                  >
392
                    <i
393
                      data-h2-padding="b(right, .25)"
394
                      className="fas fa-caret-left"
395
                    />
396
                    {intl.formatMessage(messages.backButton)}
397
                  </p>
398
                </button>
399
400
                <p
401
                  data-h2-font-size="b(h4)"
402
                  data-h2-font-weight="b(700)"
403
                  data-h2-padding="b(rl, 1) b(bottom, .5)"
404
                >
405
                  {resultsSectionText.title}
406
                </p>
407
                <p
408
                  data-h2-font-size="b(small)"
409
                  data-h2-padding="b(rl, 1) b(bottom, 2)"
410
                >
411
                  {resultsSectionText.description}
412
                </p>
413
                {!firstVisit && skillsResults.length > 0 ? (
414
                  <ul
415
                    data-h2-padding="b(left, 0)"
416
                    className="no-list-style-type"
417
                  >
418
                    {skillsResults.map((skill) => {
419
                      const { id } = skill;
420
                      const isAdded = newSkills.find(
421
                        (newSkill) => newSkill.id === skill.id,
422
                      );
423
                      const isOldSkill =
424
                        portal === "applicant" &&
425
                        oldSkills.find(
426
                          (oldSkill) => oldSkill.id === skill.id,
427
                        ) !== undefined;
428
                      return (
429
                        <li
430
                          key={id}
431
                          data-h2-grid="b(middle, contained, padded, 0)"
432
                        >
433
                          <Accordion data-h2-grid-item="b(3of4)">
434
                            <Accordion.Btn>
435
                              <p
436
                                data-h2-font-weight="b(700)"
437
                                data-h2-font-style="b(underline)"
438
                              >
439
                                {localizeFieldNonNull(locale, skill, "name")}
440
                                {isAdded && (
441
                                  <i
442
                                    data-h2-padding="b(left, .5)"
443
                                    data-h2-font-color="b(theme-1)"
444
                                    aria-hidden="true"
445
                                    className="fas fa-check"
446
                                  />
447
                                )}
448
                              </p>
449
                            </Accordion.Btn>
450
                            <Accordion.Content>
451
                              <p data-h2-focus>
452
                                {localizeFieldNonNull(
453
                                  locale,
454
                                  skill,
455
                                  "description",
456
                                )}
457
                              </p>
458
                            </Accordion.Content>
459
                          </Accordion>
460
                          {isOldSkill ? (
461
                            <button
462
                              data-h2-button=""
463
                              data-h2-grid-item="b(1of4)"
464
                              disabled
465
                              type="button"
466
                            >
467
                              <span data-h2-button-label>
468
                                {intl.formatMessage(
469
                                  messages.disabledSkillButton,
470
                                )}
471
                              </span>
472
                            </button>
473
                          ) : (
474
                            <button
475
                              data-h2-button=""
476
                              data-h2-grid-item="b(1of4)"
477
                              type="button"
478
                              onClick={() => {
479
                                // If the skill has been selected then remove it.
480
                                // Else, if the has not been selected then add it to newSkills list.
481
                                setNewSkills(addOrRemove(skill, newSkills));
482
                              }}
483
                            >
484
                              <p
485
                                data-h2-button-label
486
                                data-h2-font-weight="b(700)"
487
                                data-h2-font-style="b(underline)"
488
                                data-h2-font-color={`${
489
                                  isAdded ? "b(theme-1)" : "b(black)"
490
                                }`}
491
                              >
492
                                {isAdded
493
                                  ? intl.formatMessage(
494
                                      messages.removeSkillButton,
495
                                    )
496
                                  : intl.formatMessage(
497
                                      messages.selectSkillButton,
498
                                    )}
499
                              </p>
500
                            </button>
501
                          )}
502
                        </li>
503
                      );
504
                    })}
505
                  </ul>
506
                ) : (
507
                  <p data-h2-padding="b(rl, 1) b(bottom, .5)">
508
                    {intl.formatMessage(messages.noSkills)}
509
                  </p>
510
                )}
511
              </div>
512
            )}
513
          </div>
514
        </Dialog.Content>
515
        <Dialog.Actions
516
          data-h2-grid="b(middle, expanded, padded, .5)"
517
          data-h2-margin="b(all, 0)"
518
          data-h2-bg-color="b(gray-1, 1)"
519
        >
520
          <div data-h2-align="b(left)" data-h2-grid-item="b(1of2)">
521
            <Dialog.ActionBtn
522
              buttonStyling="stop, round, solid"
523
              data-h2-padding="b(rl, 2) b(tb, .5)"
524
              data-h2-bg-color="b(white, 1)"
525
            >
526
              <p>{intl.formatMessage(messages.cancelButton)}</p>
527
            </Dialog.ActionBtn>
528
          </div>
529
          <div data-h2-align="b(right)" data-h2-grid-item="b(1of2)">
530
            <Dialog.ActionBtn
531
              buttonStyling="theme-1, round, solid"
532
              data-h2-padding="b(rl, 2) b(tb, .5)"
533
              onClick={() =>
534
                handleSubmit(newSkills).then(() => {
535
                  setNewSkills([]);
536
                  setFirstVisit(true);
537
                  setSkillsResults([]);
538
                })
539
              }
540
              disabled={newSkills.length === 0}
541
            >
542
              <p>{intl.formatMessage(messages.saveButton)}</p>
543
            </Dialog.ActionBtn>
544
          </div>
545
        </Dialog.Actions>
546
      </Dialog>
547
    </div>
548
  );
549
};
550
551
export default FindSkillsModal;
552