Passed
Push — task/relative-resources ( 3189ee...ec397c )
by Yonathan
03:56
created

resources/assets/js/components/JobBuilder/Skills/CriteriaForm.tsx   A

Complexity

Total Complexity 7
Complexity/F 0

Size

Lines of Code 440
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 7
eloc 356
mnd 7
bc 7
fnc 0
dl 0
loc 440
rs 10
bpm 0
cpm 0
noi 0
c 0
b 0
f 0
1
import React, { useState } from "react";
2
import {
3
  MessageDescriptor,
4
  FormattedMessage,
5
  defineMessages,
6
  useIntl,
7
} from "react-intl";
8
import * as Yup from "yup";
9
import { Formik, Form, Field, FastField } from "formik";
10
import { Criteria, Skill } from "../../../models/types";
11
import { validationMessages } from "../../Form/Messages";
12
import TextAreaInput from "../../Form/TextAreaInput";
13
import RadioGroup from "../../Form/RadioGroup";
14
import { SkillLevelId, CriteriaTypeId } from "../../../models/lookupConstants";
15
import RadioInput from "../../Form/RadioInput";
16
import {
17
  skillLevelName,
18
  assetSkillName,
19
  skillLevelDescription,
20
  assetSkillDescription,
21
} from "../../../models/localizedConstants";
22
import ContextBlockItem from "../../ContextBlock/ContextBlockItem";
23
import ContextBlock from "../../ContextBlock/ContextBlock";
24
import { localizeField, getLocale } from "../../../helpers/localize";
25
26
interface CriteriaFormProps {
27
  // The Job Poster this criteria will belong to.
28
  jobPosterId: number;
29
  // The criteria being edited, if we're not creating a new one.
30
  criteria?: Criteria;
31
  // The skill this criteria will evaluate.
32
  skill: Skill;
33
  handleSubmit: (criteria: Criteria) => void;
34
  handleCancel: () => void;
35
}
36
37
const criteriaFormMessages = defineMessages({
38
  skillSpecificityLabel: {
39
    id: "criteriaForm.skillSpecificityLabel",
40
    defaultMessage: "Additional skill details",
41
    description: "Label for the skill specificity textarea.",
42
  },
43
  skillSpecificityPlaceholder: {
44
    id: "criteriaForm.skillSpecificityPlaceholder",
45
    defaultMessage:
46
      "Add context or specifics to the definition of this skill that will only appear on your job poster. This will be reviewed by your human resources advisor.",
47
    description: "Placeholder for the skill specificity textarea.",
48
  },
49
  skillLevelSelectionLabel: {
50
    id: "criteriaForm.skillLevelSelectionLabel",
51
    defaultMessage: "Select a skill level:",
52
    description: "Placeholder for the skill specificity textarea.",
53
  },
54
});
55
56
const essentialSkillLevels = (
57
  skillTypeId: number,
58
): {
59
  [key: string]: {
60
    name: MessageDescriptor;
61
    context: MessageDescriptor;
62
  };
63
} => ({
64
  basic: {
65
    name: skillLevelName(SkillLevelId.Basic, skillTypeId),
66
    context: skillLevelDescription(SkillLevelId.Basic, skillTypeId),
67
  },
68
  intermediate: {
69
    name: skillLevelName(SkillLevelId.Intermediate, skillTypeId),
70
    context: skillLevelDescription(SkillLevelId.Intermediate, skillTypeId),
71
  },
72
  advanced: {
73
    name: skillLevelName(SkillLevelId.Advanced, skillTypeId),
74
    context: skillLevelDescription(SkillLevelId.Advanced, skillTypeId),
75
  },
76
  expert: {
77
    name: skillLevelName(SkillLevelId.Expert, skillTypeId),
78
    context: skillLevelDescription(SkillLevelId.Expert, skillTypeId),
79
  },
80
});
81
82
interface FormValues {
83
  specificity: string;
84
  level: string;
85
}
86
87
export const essentialSkillIdToKey = (id: number): string => {
88
  switch (id) {
89
    case SkillLevelId.Basic:
90
      return "basic";
91
    case SkillLevelId.Intermediate:
92
      return "intermediate";
93
    case SkillLevelId.Advanced:
94
      return "advanced";
95
    case SkillLevelId.Expert:
96
      return "expert";
97
    default:
98
      return "";
99
  }
100
};
101
102
export const essentialKeyToId = (key: string): SkillLevelId => {
103
  switch (key) {
104
    case "basic":
105
      return SkillLevelId.Basic;
106
    case "intermediate":
107
      return SkillLevelId.Intermediate;
108
    case "advanced":
109
      return SkillLevelId.Advanced;
110
    case "expert":
111
      return SkillLevelId.Expert;
112
    default:
113
      return SkillLevelId.Basic;
114
  }
115
};
116
117
export const criteriaToValues = (
118
  criteria: Criteria,
119
  locale: "en" | "fr",
120
): FormValues => ({
121
  specificity: localizeField(locale, criteria, "specificity") || "",
122
  level:
123
    criteria.criteria_type_id === CriteriaTypeId.Asset
124
      ? "asset"
125
      : essentialSkillIdToKey(criteria.skill_level_id),
126
});
127
128
/* eslint-disable @typescript-eslint/camelcase */
129
const updateCriteriaWithValues = (
130
  locale: "en" | "fr",
131
  criteria: Criteria,
132
  skill: Skill,
133
  values: FormValues,
134
): Criteria => {
135
  return {
136
    ...criteria,
137
    criteria_type_id:
138
      values.level === "asset"
139
        ? CriteriaTypeId.Asset
140
        : CriteriaTypeId.Essential,
141
    skill_level_id: essentialKeyToId(values.level),
142
    description: {
143
      en: skill.description.en,
144
      fr: skill.description.fr,
145
    },
146
    specificity: {
147
      ...criteria.specificity,
148
      [locale]: values.specificity,
149
    },
150
  };
151
};
152
153
const newCriteria = (jobPosterId: number, skillId: number): Criteria => ({
154
  id: 0,
155
  criteria_type_id: CriteriaTypeId.Essential,
156
  job_poster_id: jobPosterId,
157
  skill_id: skillId,
158
  skill_level_id: SkillLevelId.Basic,
159
  description: {
160
    en: null,
161
    fr: null,
162
  },
163
  specificity: {
164
    en: null,
165
    fr: null,
166
  },
167
});
168
/* eslint-enable @typescript-eslint/camelcase */
169
170
export const CriteriaForm: React.FunctionComponent<CriteriaFormProps> = ({
171
  jobPosterId,
172
  criteria,
173
  skill,
174
  handleSubmit,
175
  handleCancel,
176
}): React.ReactElement => {
177
  const intl = useIntl();
178
  const locale = getLocale(intl.locale);
179
  const stringNotEmpty = (value: string | null): boolean =>
180
    value !== null && (value as string).length !== 0;
181
  const [showSpecificity, setShowSpecificity] = useState(
182
    criteria !== undefined &&
183
      stringNotEmpty(localizeField(locale, criteria, "specificity")),
184
  );
185
186
  const initialValues: FormValues =
187
    criteria !== undefined
188
      ? criteriaToValues(criteria, locale)
189
      : {
190
          specificity: "",
191
          level: "",
192
        };
193
  const skillSchema = Yup.object().shape({
194
    specificity: Yup.string(),
195
    level: Yup.string()
196
      .oneOf(
197
        [...Object.keys(essentialSkillLevels(skill.skill_type_id)), "asset"],
198
        intl.formatMessage(validationMessages.invalidSelection),
199
      )
200
      .required(intl.formatMessage(validationMessages.required)),
201
  });
202
203
  return (
204
    <Formik
205
      enableReinitialize
206
      initialValues={initialValues}
207
      validationSchema={skillSchema}
208
      onSubmit={(values, { setSubmitting }): void => {
209
        const oldCriteria =
210
          criteria !== undefined
211
            ? criteria
212
            : newCriteria(jobPosterId, skill.id);
213
        const updatedCriteria = updateCriteriaWithValues(
214
          locale,
215
          oldCriteria,
216
          skill,
217
          values,
218
        );
219
        handleSubmit(updatedCriteria);
220
        setSubmitting(false);
221
      }}
222
    >
223
      {({
224
        errors,
225
        touched,
226
        isSubmitting,
227
        values,
228
        setFieldValue,
229
      }): React.ReactElement => (
230
        <>
231
          <Form id="jpbSkillsForm">
232
            {/* Skill Definition */}
233
            <div data-c-padding="all(normal)" data-c-background="grey(10)">
234
              <p data-c-font-weight="bold" data-c-margin="bottom(normal)">
235
                <FormattedMessage
236
                  id="jobBuilder.criteriaForm.skillDefinition"
237
                  defaultMessage="Skill Definition"
238
                  description="Label for Skill Definition heading on Add Skill modal."
239
                />
240
              </p>
241
              <div>
242
                <p data-c-margin="bottom(normal)">
243
                  {localizeField(locale, skill, "name")}
244
                </p>
245
                <p data-c-margin="bottom(normal)">
246
                  {localizeField(locale, skill, "description")}
247
                </p>
248
                {showSpecificity ? (
249
                  <>
250
                    <FastField
251
                      id="skillSpecificity"
252
                      type="textarea"
253
                      name="specificity"
254
                      label={intl.formatMessage(
255
                        criteriaFormMessages.skillSpecificityLabel,
256
                      )}
257
                      placeholder={intl.formatMessage(
258
                        criteriaFormMessages.skillSpecificityPlaceholder,
259
                      )}
260
                      component={TextAreaInput}
261
                    />
262
                    <button
263
                      className="job-builder-add-skill-definition-trigger"
264
                      type="button"
265
                      onClick={(): void => {
266
                        // Clear the field before hiding it
267
                        setFieldValue("specificity", "");
268
                        setShowSpecificity(false);
269
                      }}
270
                    >
271
                      <span>
272
                        <i className="fas fa-minus-circle" data-c-colour="c1" />
273
                        <FormattedMessage
274
                          id="jobBuilder.criteriaForm.removeSpecificity"
275
                          defaultMessage="Remove additional specificity."
276
                          description="Label for 'Remove additional specificity' button on Add Skill modal."
277
                        />
278
                      </span>
279
                    </button>
280
                  </>
281
                ) : (
282
                  <button
283
                    className="job-builder-add-skill-definition-trigger"
284
                    type="button"
285
                    onClick={(): void => setShowSpecificity(true)}
286
                  >
287
                    <span>
288
                      <i className="fas fa-plus-circle" data-c-colour="c1" />
289
                      <FormattedMessage
290
                        id="jobBuilder.criteriaForm.addSpecificity"
291
                        defaultMessage="I would like to add details to this definition that are specific to this position."
292
                        description="Label for 'Add additional specificity' button on Add Skill modal."
293
                      />
294
                    </span>
295
                  </button>
296
                )}
297
              </div>
298
            </div>
299
            {/* Skill Level */}
300
            <div
301
              className="job-builder-culture-block"
302
              data-c-grid-item="base(1of1)"
303
              data-c-padding="all(normal)"
304
            >
305
              <p data-c-font-weight="bold" data-c-margin="bottom(normal)">
306
                <FormattedMessage
307
                  id="jobBuilder.criteriaForm.chooseSkillLevel"
308
                  defaultMessage="Choose a Skill Level"
309
                  description="Label for 'Choose a Skill Level' radio group heading on Add Skill modal."
310
                />
311
              </p>
312
              <div data-c-grid="gutter">
313
                <RadioGroup
314
                  id="skillLevelSelection"
315
                  label={intl.formatMessage(
316
                    criteriaFormMessages.skillLevelSelectionLabel,
317
                  )}
318
                  required
319
                  touched={touched.level}
320
                  error={errors.level}
321
                  value={values.level}
322
                  grid="base(1of1) tl(1of3)"
323
                >
324
                  {Object.entries(
325
                    essentialSkillLevels(skill.skill_type_id),
326
                  ).map(
327
                    ([key, { name }]): React.ReactElement => {
328
                      return (
329
                        <FastField
330
                          key={key}
331
                          id={key}
332
                          name="level"
333
                          component={RadioInput}
334
                          label={intl.formatMessage(name)}
335
                          value={key}
336
                          trigger
337
                        />
338
                      );
339
                    },
340
                  )}
341
                  <div
342
                    className="job-builder-skill-level-or-block"
343
                    data-c-alignment="base(centre)"
344
                  >
345
                    {/** This empty div is required for CSS magic */}
346
                    <div />
347
                    <span>
348
                      <FormattedMessage
349
                        id="jobBuilder.criteriaForm.or"
350
                        defaultMessage="or"
351
                        description="Label for 'or' between essential/asset levels on Add Skill modal."
352
                      />
353
                    </span>
354
                  </div>
355
                  <FastField
356
                    key="asset"
357
                    id="asset"
358
                    name="level"
359
                    component={RadioInput}
360
                    label={intl.formatMessage(assetSkillName())}
361
                    value="asset"
362
                    trigger
363
                  />
364
                </RadioGroup>
365
                <ContextBlock
366
                  className="job-builder-context-block"
367
                  grid="base(1of1) tl(2of3)"
368
                >
369
                  {Object.entries(
370
                    essentialSkillLevels(skill.skill_type_id),
371
                  ).map(
372
                    ([key, { name, context }]): React.ReactElement => {
373
                      return (
374
                        <ContextBlockItem
375
                          key={key}
376
                          contextId={key}
377
                          title={intl.formatMessage(name)}
378
                          subtext={intl.formatMessage(context)}
379
                          className="job-builder-context-item"
380
                          active={values.level === key}
381
                        />
382
                      );
383
                    },
384
                  )}
385
                  <ContextBlockItem
386
                    key="asset"
387
                    contextId="asset"
388
                    title={intl.formatMessage(assetSkillName())}
389
                    subtext={intl.formatMessage(assetSkillDescription())}
390
                    className="job-builder-context-item"
391
                    active={values.level === "asset"}
392
                  />
393
                </ContextBlock>
394
              </div>
395
            </div>
396
            <div data-c-padding="normal">
397
              <div data-c-grid="gutter middle">
398
                <div data-c-grid-item="base(1of2)">
399
                  <button
400
                    data-c-button="outline(c2)"
401
                    data-c-radius="rounded"
402
                    type="button"
403
                    disabled={isSubmitting}
404
                    onClick={handleCancel}
405
                  >
406
                    <FormattedMessage
407
                      id="jobBuilder.criteriaForm.button.cancel"
408
                      defaultMessage="Cancel"
409
                      description="Label for Cancel button on Add Skill modal."
410
                    />
411
                  </button>
412
                </div>
413
                <div
414
                  data-c-alignment="base(right)"
415
                  data-c-grid-item="base(1of2)"
416
                >
417
                  <button
418
                    data-c-button="solid(c2)"
419
                    data-c-radius="rounded"
420
                    disabled={isSubmitting}
421
                    type="submit"
422
                  >
423
                    <FormattedMessage
424
                      id="jobBuilder.criteriaForm.button.add"
425
                      defaultMessage="Add Skill"
426
                      description="Label for Add Skill button on Add Skill modal."
427
                    />
428
                  </button>
429
                </div>
430
              </div>
431
            </div>
432
          </Form>
433
        </>
434
      )}
435
    </Formik>
436
  );
437
};
438
439
export default CriteriaForm;
440