Passed
Push — task/ci-browser-test-actions ( ccadfd...bf9106 )
by
unknown
10:19 queued 11s
created

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

Complexity

Total Complexity 18
Complexity/F 0

Size

Lines of Code 583
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

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