Passed
Push — dev ( ae764f...672c2b )
by
unknown
04:10 queued 10s
created

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

Complexity

Total Complexity 7
Complexity/F 0

Size

Lines of Code 438
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 438
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
const updateCriteriaWithValues = (
129
  locale: "en" | "fr",
130
  criteria: Criteria,
131
  skill: Skill,
132
  values: FormValues,
133
): Criteria => {
134
  return {
135
    ...criteria,
136
    criteria_type_id:
137
      values.level === "asset"
138
        ? CriteriaTypeId.Asset
139
        : CriteriaTypeId.Essential,
140
    skill_level_id: essentialKeyToId(values.level),
141
    description: {
142
      en: skill.description.en,
143
      fr: skill.description.fr,
144
    },
145
    specificity: {
146
      ...criteria.specificity,
147
      [locale]: values.specificity,
148
    },
149
  };
150
};
151
152
const newCriteria = (jobPosterId: number, skillId: number): Criteria => ({
153
  id: 0,
154
  criteria_type_id: CriteriaTypeId.Essential,
155
  job_poster_id: jobPosterId,
156
  skill_id: skillId,
157
  skill_level_id: SkillLevelId.Basic,
158
  description: {
159
    en: null,
160
    fr: null,
161
  },
162
  specificity: {
163
    en: null,
164
    fr: null,
165
  },
166
});
167
168
export const CriteriaForm: React.FunctionComponent<CriteriaFormProps> = ({
169
  jobPosterId,
170
  criteria,
171
  skill,
172
  handleSubmit,
173
  handleCancel,
174
}): React.ReactElement => {
175
  const intl = useIntl();
176
  const locale = getLocale(intl.locale);
177
  const stringNotEmpty = (value: string | null): boolean =>
178
    value !== null && (value as string).length !== 0;
179
  const [showSpecificity, setShowSpecificity] = useState(
180
    criteria !== undefined &&
181
      stringNotEmpty(localizeField(locale, criteria, "specificity")),
182
  );
183
184
  const initialValues: FormValues =
185
    criteria !== undefined
186
      ? criteriaToValues(criteria, locale)
187
      : {
188
          specificity: "",
189
          level: "",
190
        };
191
  const skillSchema = Yup.object().shape({
192
    specificity: Yup.string(),
193
    level: Yup.string()
194
      .oneOf(
195
        [...Object.keys(essentialSkillLevels(skill.skill_type_id)), "asset"],
196
        intl.formatMessage(validationMessages.invalidSelection),
197
      )
198
      .required(intl.formatMessage(validationMessages.required)),
199
  });
200
201
  return (
202
    <Formik
203
      enableReinitialize
204
      initialValues={initialValues}
205
      validationSchema={skillSchema}
206
      onSubmit={(values, { setSubmitting }): void => {
207
        const oldCriteria =
208
          criteria !== undefined
209
            ? criteria
210
            : newCriteria(jobPosterId, skill.id);
211
        const updatedCriteria = updateCriteriaWithValues(
212
          locale,
213
          oldCriteria,
214
          skill,
215
          values,
216
        );
217
        handleSubmit(updatedCriteria);
218
        setSubmitting(false);
219
      }}
220
    >
221
      {({
222
        errors,
223
        touched,
224
        isSubmitting,
225
        values,
226
        setFieldValue,
227
      }): React.ReactElement => (
228
        <>
229
          <Form id="jpbSkillsForm">
230
            {/* Skill Definition */}
231
            <div data-c-padding="all(normal)" data-c-background="grey(10)">
232
              <p data-c-font-weight="bold" data-c-margin="bottom(normal)">
233
                <FormattedMessage
234
                  id="jobBuilder.criteriaForm.skillDefinition"
235
                  defaultMessage="Skill Definition"
236
                  description="Label for Skill Definition heading on Add Skill modal."
237
                />
238
              </p>
239
              <div>
240
                <p data-c-margin="bottom(normal)">
241
                  {localizeField(locale, skill, "name")}
242
                </p>
243
                <p data-c-margin="bottom(normal)">
244
                  {localizeField(locale, skill, "description")}
245
                </p>
246
                {showSpecificity ? (
247
                  <>
248
                    <FastField
249
                      id="skillSpecificity"
250
                      type="textarea"
251
                      name="specificity"
252
                      label={intl.formatMessage(
253
                        criteriaFormMessages.skillSpecificityLabel,
254
                      )}
255
                      placeholder={intl.formatMessage(
256
                        criteriaFormMessages.skillSpecificityPlaceholder,
257
                      )}
258
                      component={TextAreaInput}
259
                    />
260
                    <button
261
                      className="job-builder-add-skill-definition-trigger"
262
                      type="button"
263
                      onClick={(): void => {
264
                        // Clear the field before hiding it
265
                        setFieldValue("specificity", "");
266
                        setShowSpecificity(false);
267
                      }}
268
                    >
269
                      <span>
270
                        <i className="fas fa-minus-circle" data-c-colour="c1" />
271
                        <FormattedMessage
272
                          id="jobBuilder.criteriaForm.removeSpecificity"
273
                          defaultMessage="Remove additional specificity."
274
                          description="Label for 'Remove additional specificity' button on Add Skill modal."
275
                        />
276
                      </span>
277
                    </button>
278
                  </>
279
                ) : (
280
                  <button
281
                    className="job-builder-add-skill-definition-trigger"
282
                    type="button"
283
                    onClick={(): void => setShowSpecificity(true)}
284
                  >
285
                    <span>
286
                      <i className="fas fa-plus-circle" data-c-colour="c1" />
287
                      <FormattedMessage
288
                        id="jobBuilder.criteriaForm.addSpecificity"
289
                        defaultMessage="I would like to add details to this definition that are specific to this position."
290
                        description="Label for 'Add additional specificity' button on Add Skill modal."
291
                      />
292
                    </span>
293
                  </button>
294
                )}
295
              </div>
296
            </div>
297
            {/* Skill Level */}
298
            <div
299
              className="job-builder-culture-block"
300
              data-c-grid-item="base(1of1)"
301
              data-c-padding="all(normal)"
302
            >
303
              <p data-c-font-weight="bold" data-c-margin="bottom(normal)">
304
                <FormattedMessage
305
                  id="jobBuilder.criteriaForm.chooseSkillLevel"
306
                  defaultMessage="Choose a Skill Level"
307
                  description="Label for 'Choose a Skill Level' radio group heading on Add Skill modal."
308
                />
309
              </p>
310
              <div data-c-grid="gutter">
311
                <RadioGroup
312
                  id="skillLevelSelection"
313
                  label={intl.formatMessage(
314
                    criteriaFormMessages.skillLevelSelectionLabel,
315
                  )}
316
                  required
317
                  touched={touched.level}
318
                  error={errors.level}
319
                  value={values.level}
320
                  grid="base(1of1) tl(1of3)"
321
                >
322
                  {Object.entries(
323
                    essentialSkillLevels(skill.skill_type_id),
324
                  ).map(
325
                    ([key, { name }]): React.ReactElement => {
326
                      return (
327
                        <FastField
328
                          key={key}
329
                          id={key}
330
                          name="level"
331
                          component={RadioInput}
332
                          label={intl.formatMessage(name)}
333
                          value={key}
334
                          trigger
335
                        />
336
                      );
337
                    },
338
                  )}
339
                  <div
340
                    className="job-builder-skill-level-or-block"
341
                    data-c-alignment="base(centre)"
342
                  >
343
                    {/** This empty div is required for CSS magic */}
344
                    <div />
345
                    <span>
346
                      <FormattedMessage
347
                        id="jobBuilder.criteriaForm.or"
348
                        defaultMessage="or"
349
                        description="Label for 'or' between essential/asset levels on Add Skill modal."
350
                      />
351
                    </span>
352
                  </div>
353
                  <FastField
354
                    key="asset"
355
                    id="asset"
356
                    name="level"
357
                    component={RadioInput}
358
                    label={intl.formatMessage(assetSkillName())}
359
                    value="asset"
360
                    trigger
361
                  />
362
                </RadioGroup>
363
                <ContextBlock
364
                  className="job-builder-context-block"
365
                  grid="base(1of1) tl(2of3)"
366
                >
367
                  {Object.entries(
368
                    essentialSkillLevels(skill.skill_type_id),
369
                  ).map(
370
                    ([key, { name, context }]): React.ReactElement => {
371
                      return (
372
                        <ContextBlockItem
373
                          key={key}
374
                          contextId={key}
375
                          title={intl.formatMessage(name)}
376
                          subtext={intl.formatMessage(context)}
377
                          className="job-builder-context-item"
378
                          active={values.level === key}
379
                        />
380
                      );
381
                    },
382
                  )}
383
                  <ContextBlockItem
384
                    key="asset"
385
                    contextId="asset"
386
                    title={intl.formatMessage(assetSkillName())}
387
                    subtext={intl.formatMessage(assetSkillDescription())}
388
                    className="job-builder-context-item"
389
                    active={values.level === "asset"}
390
                  />
391
                </ContextBlock>
392
              </div>
393
            </div>
394
            <div data-c-padding="normal">
395
              <div data-c-grid="gutter middle">
396
                <div data-c-grid-item="base(1of2)">
397
                  <button
398
                    data-c-button="outline(c2)"
399
                    data-c-radius="rounded"
400
                    type="button"
401
                    disabled={isSubmitting}
402
                    onClick={handleCancel}
403
                  >
404
                    <FormattedMessage
405
                      id="jobBuilder.criteriaForm.button.cancel"
406
                      defaultMessage="Cancel"
407
                      description="Label for Cancel button on Add Skill modal."
408
                    />
409
                  </button>
410
                </div>
411
                <div
412
                  data-c-alignment="base(right)"
413
                  data-c-grid-item="base(1of2)"
414
                >
415
                  <button
416
                    data-c-button="solid(c2)"
417
                    data-c-radius="rounded"
418
                    disabled={isSubmitting}
419
                    type="submit"
420
                  >
421
                    <FormattedMessage
422
                      id="jobBuilder.criteriaForm.button.add"
423
                      defaultMessage="Add Skill"
424
                      description="Label for Add Skill button on Add Skill modal."
425
                    />
426
                  </button>
427
                </div>
428
              </div>
429
            </div>
430
          </Form>
431
        </>
432
      )}
433
    </Formik>
434
  );
435
};
436
437
export default CriteriaForm;
438