Passed
Push — feature/checkbox-group-field ( e7fdbd )
by Tristan
07:41 queued 01:19
created

resources/assets/js/components/Application/ExperienceModals/EducationExperienceModal.tsx   A

Complexity

Total Complexity 11
Complexity/F 0

Size

Lines of Code 478
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 11
eloc 388
mnd 11
bc 11
fnc 0
dl 0
loc 478
rs 10
bpm 0
cpm 0
noi 0
c 0
b 0
f 0
1
import React from "react";
2
import { FastField, Field, Formik, Form } from "formik";
3
import { defineMessages, useIntl, IntlShape } from "react-intl";
4
import * as Yup from "yup";
5
import {
6
  EducationFormValues,
7
  EducationSubform,
8
  validationShape as educationValidationShape,
9
} from "./EducationSubform";
10
import TextInput from "../../Form/TextInput";
11
import CheckboxInput from "../../Form/CheckboxInput";
12
import { validationMessages } from "../../Form/Messages";
13
import SkillSubform, {
14
  SkillFormValues,
15
  validationShape as skillValidationShape,
16
} from "./SkillSubform";
17
import { Skill, ExperienceEducation } from "../../../models/types";
18
import {
19
  ExperienceModalHeader,
20
  ExperienceDetailsIntro,
21
  ExperienceModalFooter,
22
} from "./ExperienceModalCommon";
23
import Modal from "../../Modal";
24
import DateInput from "../../Form/DateInput";
25
import { toInputDateString, fromInputDateString } from "../../../helpers/dates";
26
import {
27
  Locales,
28
  localizeFieldNonNull,
29
  getLocale,
30
  matchValueToModel,
31
} from "../../../helpers/localize";
32
import { notEmpty } from "../../../helpers/queries";
33
import { localizedFieldNonNull } from "../../../models/app";
34
import SelectInput from "../../Form/SelectInput";
35
36
export interface EducationType {
37
  id: number;
38
  name: localizedFieldNonNull;
39
}
40
41
export interface EducationStatus {
42
  id: number;
43
  name: localizedFieldNonNull;
44
}
45
46
interface EducationExperienceModalProps {
47
  modalId: string;
48
  experienceEducation: ExperienceEducation | null;
49
  educationTypes: EducationType[];
50
  educationStatuses: EducationStatus[];
51
  jobId: number;
52
  jobClassification: string;
53
  requiredSkills: Skill[];
54
  savedRequiredSkills: Skill[];
55
  optionalSkills: Skill[];
56
  savedOptionalSkills: Skill[];
57
  experienceableId: number;
58
  experienceableType: ExperienceEducation["experienceable_type"];
59
  parentElement: Element | null;
60
  visible: boolean;
61
  onModalCancel: () => void;
62
  onModalConfirm: (data: EducationExperienceSubmitData) => Promise<void>;
63
}
64
65
export const messages = defineMessages({
66
  modalTitle: {
67
    id: "application.educationExperienceModal.modalTitle",
68
    defaultMessage: "Add Education Experience",
69
  },
70
  modalDescription: {
71
    id: "application.educationExperienceModal.modalDescription",
72
    defaultMessage:
73
      'Got creds? Share your degree, certificates, online courses, a trade apprenticeship, licences or alternative credentials. If you’ve learned something from a recognized educational provider, include your experiences here.  (Learned something from your community or on your own? Share this as a "Community Experience" or "Personal Experience".)',
74
  },
75
  educationTypeLabel: {
76
    id: "application.educationExperienceModal.educationTypeLabel",
77
    defaultMessage: "Type of Education",
78
  },
79
  educationTypeDefault: {
80
    id: "application.educationExperienceModal.educationTypeDefault",
81
    defaultMessage: "Select an education type...",
82
    description: "Default selection in the Education Type dropdown menu.",
83
  },
84
  areaStudyLabel: {
85
    id: "application.educationExperienceModal.areaStudyLabel",
86
    defaultMessage: "Area of Study",
87
  },
88
  areaStudyPlaceholder: {
89
    id: "application.educationExperienceModal.areaStudyPlaceholder",
90
    defaultMessage: "e.g. Organic Chemistry",
91
  },
92
  institutionLabel: {
93
    id: "application.educationExperienceModal.institutionLabel",
94
    defaultMessage: "Institution",
95
  },
96
  institutionPlaceholder: {
97
    id: "application.educationExperienceModal.institutionPlaceholder",
98
    defaultMessage: "e.g. Bishop's University",
99
  },
100
  completionLabel: {
101
    id: "application.educationExperienceModal.completionLabel",
102
    defaultMessage: "Completion Status",
103
  },
104
  completionDefault: {
105
    id: "application.educationExperienceModal.completionDefault",
106
    defaultMessage: "Select a completion status...",
107
  },
108
  thesisLabel: {
109
    id: "application.educationExperienceModal.thesisLabel",
110
    defaultMessage: "Thesis Title (Optional)",
111
  },
112
  thesisPlaceholder: {
113
    id: "application.educationExperienceModal.thesisPlaceholder",
114
    defaultMessage: "e.g. How bats navigate between each other during flight",
115
  },
116
  blockcertLabel: {
117
    id: "application.educationExperienceModal.blockcertLabel",
118
    defaultMessage: "Blockcert Link (Optional)",
119
  },
120
  blockcertInlineLabel: {
121
    id: "application.educationExperienceModal.blockcertInlineLabel",
122
    defaultMessage:
123
      "I have a Blockcert and can provide it on request. (Optional)",
124
  },
125
  startDateLabel: {
126
    id: "application.educationExperienceModal.startDateLabel",
127
    defaultMessage: "Select a Start Date",
128
  },
129
  datePlaceholder: {
130
    id: "application.educationExperienceModal.datePlaceholder",
131
    defaultMessage: "yyyy-mm-dd",
132
  },
133
  isActiveLabel: {
134
    id: "application.educationExperienceModal.isActiveLabel",
135
    defaultMessage: "This experience is still ongoing, or...",
136
    description: "Label for checkbox that indicates work is still ongoing.",
137
  },
138
  endDateLabel: {
139
    id: "application.educationExperienceModal.endDateLabel",
140
    defaultMessage: "Select an End Date",
141
  },
142
});
143
144
export interface EducationDetailsFormValues {
145
  educationTypeId: number | "";
146
  areaOfStudy: string;
147
  institution: string;
148
  educationStatusId: number | "";
149
  thesisTitle: string;
150
  startDate: string;
151
  isActive: boolean;
152
  endDate: string;
153
  hasBlockcert: boolean;
154
}
155
156
type EducationExperienceFormValues = SkillFormValues &
157
  EducationFormValues &
158
  EducationDetailsFormValues;
159
export interface EducationExperienceSubmitData {
160
  experienceEducation: ExperienceEducation;
161
  savedRequiredSkills: Skill[];
162
  savedOptionalSkills: Skill[];
163
}
164
165
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
166
export const validationShape = (intl: IntlShape) => {
167
  const requiredMsg = intl.formatMessage(validationMessages.required);
168
  const conditionalRequiredMsg = intl.formatMessage(
169
    validationMessages.endDateRequiredIfNotOngoing,
170
  );
171
  const inPastMsg = intl.formatMessage(validationMessages.dateMustBePast);
172
  const afterStartDateMsg = intl.formatMessage(
173
    validationMessages.endDateAfterStart,
174
  );
175
  return {
176
    educationTypeId: Yup.number().required(requiredMsg),
177
    areaOfStudy: Yup.string().required(requiredMsg),
178
    institution: Yup.string().required(requiredMsg),
179
    educationStatusId: Yup.number().required(requiredMsg),
180
    thesisTitle: Yup.string(),
181
    hasBlockcert: Yup.boolean(),
182
    startDate: Yup.date().required(requiredMsg).max(new Date(), inPastMsg),
183
    isActive: Yup.boolean(),
184
    endDate: Yup.date().when("isActive", {
185
      is: false,
186
      then: Yup.date()
187
        .required(conditionalRequiredMsg)
188
        .min(Yup.ref("startDate"), afterStartDateMsg),
189
      otherwise: Yup.date().min(Yup.ref("startDate"), afterStartDateMsg),
190
    }),
191
  };
192
};
193
194
const dataToFormValues = (
195
  data: EducationExperienceSubmitData,
196
  locale: Locales,
197
  creatingNew: boolean,
198
): EducationExperienceFormValues => {
199
  const {
200
    experienceEducation,
201
    savedRequiredSkills,
202
    savedOptionalSkills,
203
  } = data;
204
  const skillToName = (skill: Skill): string =>
205
    localizeFieldNonNull(locale, skill, "name");
206
  return {
207
    requiredSkills: savedRequiredSkills.map(skillToName),
208
    optionalSkills: savedOptionalSkills.map(skillToName),
209
    useAsEducationRequirement: experienceEducation.is_education_requirement,
210
    educationTypeId: creatingNew ? "" : experienceEducation.education_type_id,
211
    areaOfStudy: experienceEducation.area_of_study,
212
    institution: experienceEducation.institution,
213
    educationStatusId: creatingNew
214
      ? ""
215
      : experienceEducation.education_status_id,
216
    thesisTitle: experienceEducation.thesis_title ?? "",
217
    hasBlockcert: experienceEducation.has_blockcert,
218
    startDate: toInputDateString(experienceEducation.start_date),
219
    isActive: experienceEducation.is_active,
220
    endDate: experienceEducation.end_date
221
      ? toInputDateString(experienceEducation.end_date)
222
      : "",
223
  };
224
};
225
226
/* eslint-disable @typescript-eslint/camelcase */
227
const formValuesToData = (
228
  formValues: EducationExperienceFormValues,
229
  originalExperience: ExperienceEducation,
230
  locale: Locales,
231
  skills: Skill[],
232
): EducationExperienceSubmitData => {
233
  const nameToSkill = (name: string): Skill | null =>
234
    matchValueToModel(locale, "name", name, skills);
235
  return {
236
    experienceEducation: {
237
      ...originalExperience,
238
      education_type_id: formValues.educationTypeId
239
        ? Number(formValues.educationTypeId)
240
        : 1,
241
      area_of_study: formValues.areaOfStudy,
242
      institution: formValues.institution,
243
      education_status_id: formValues.educationStatusId
244
        ? Number(formValues.educationStatusId)
245
        : 1,
246
      thesis_title: formValues.thesisTitle ? formValues.thesisTitle : "",
247
      has_blockcert: formValues.hasBlockcert,
248
      start_date: fromInputDateString(formValues.startDate),
249
      is_active: formValues.isActive,
250
      end_date: formValues.endDate
251
        ? fromInputDateString(formValues.endDate)
252
        : null,
253
      is_education_requirement: formValues.useAsEducationRequirement,
254
    },
255
    savedRequiredSkills: formValues.requiredSkills
256
      .map(nameToSkill)
257
      .filter(notEmpty),
258
    savedOptionalSkills: formValues.optionalSkills
259
      .map(nameToSkill)
260
      .filter(notEmpty),
261
  };
262
};
263
264
const newExperienceEducation = (
265
  experienceableId: number,
266
  experienceableType: ExperienceEducation["experienceable_type"],
267
): ExperienceEducation => ({
268
  id: 0,
269
  education_type_id: 0,
270
  education_type: { en: "", fr: "" },
271
  area_of_study: "",
272
  institution: "",
273
  education_status_id: 0,
274
  education_status: { en: "", fr: "" },
275
  thesis_title: "",
276
  has_blockcert: false,
277
  is_active: false,
278
  start_date: new Date(),
279
  end_date: null,
280
  is_education_requirement: false,
281
  experienceable_id: experienceableId,
282
  experienceable_type: experienceableType,
283
  type: "experience_education",
284
});
285
/* eslint-enable @typescript-eslint/camelcase */
286
287
export const EducationExperienceModal: React.FC<EducationExperienceModalProps> = ({
288
  modalId,
289
  experienceEducation,
290
  educationTypes,
291
  educationStatuses,
292
  jobId,
293
  jobClassification,
294
  requiredSkills,
295
  savedRequiredSkills,
296
  optionalSkills,
297
  savedOptionalSkills,
298
  experienceableId,
299
  experienceableType,
300
  parentElement,
301
  visible,
302
  onModalCancel,
303
  onModalConfirm,
304
}) => {
305
  const intl = useIntl();
306
  const locale = getLocale(intl.locale);
307
308
  const originalExperience =
309
    experienceEducation ??
310
    newExperienceEducation(experienceableId, experienceableType);
311
312
  const skillToName = (skill: Skill): string =>
313
    localizeFieldNonNull(locale, skill, "name");
314
315
  const initialFormValues = dataToFormValues(
316
    {
317
      experienceEducation: originalExperience,
318
      savedRequiredSkills,
319
      savedOptionalSkills,
320
    },
321
    locale,
322
    experienceEducation === null,
323
  );
324
325
  const validationSchema = Yup.object().shape({
326
    ...skillValidationShape,
327
    ...educationValidationShape,
328
    ...validationShape(intl),
329
  });
330
331
  const detailsSubform = (
332
    <div data-c-container="medium">
333
      <div data-c-grid="gutter(all, 1) middle">
334
        <FastField
335
          id="educationTypeId"
336
          name="educationTypeId"
337
          label={intl.formatMessage(messages.educationTypeLabel)}
338
          grid="tl(1of2)"
339
          component={SelectInput}
340
          required
341
          nullSelection={intl.formatMessage(messages.educationTypeDefault)}
342
          options={educationTypes.map((type) => ({
343
            value: type.id,
344
            label: localizeFieldNonNull(locale, type, "name"),
345
          }))}
346
        />
347
        <FastField
348
          id="areaOfStudy"
349
          type="text"
350
          name="areaOfStudy"
351
          component={TextInput}
352
          required
353
          grid="tl(1of2)"
354
          label={intl.formatMessage(messages.areaStudyLabel)}
355
          placeholder={intl.formatMessage(messages.areaStudyPlaceholder)}
356
        />
357
        <FastField
358
          id="institution"
359
          type="text"
360
          name="institution"
361
          component={TextInput}
362
          required
363
          grid="tl(1of2)"
364
          label={intl.formatMessage(messages.institutionLabel)}
365
          placeholder={intl.formatMessage(messages.institutionPlaceholder)}
366
        />
367
        <FastField
368
          id="educationStatusId"
369
          name="educationStatusId"
370
          label={intl.formatMessage(messages.completionLabel)}
371
          grid="tl(1of2)"
372
          component={SelectInput}
373
          required
374
          nullSelection={intl.formatMessage(messages.completionDefault)}
375
          options={educationStatuses.map((status) => ({
376
            value: status.id,
377
            label: localizeFieldNonNull(locale, status, "name"),
378
          }))}
379
        />
380
        <FastField
381
          id="startDate"
382
          name="startDate"
383
          component={DateInput}
384
          required
385
          grid="base(1of1)"
386
          label={intl.formatMessage(messages.startDateLabel)}
387
          placeholder={intl.formatMessage(messages.datePlaceholder)}
388
        />
389
        <Field
390
          id="isActive"
391
          name="isActive"
392
          component={CheckboxInput}
393
          grid="tl(1of2)"
394
          label={intl.formatMessage(messages.isActiveLabel)}
395
        />
396
        <Field
397
          id="endDate"
398
          name="endDate"
399
          component={DateInput}
400
          grid="base(1of2)"
401
          label={intl.formatMessage(messages.endDateLabel)}
402
          placeholder={intl.formatMessage(messages.datePlaceholder)}
403
        />
404
        <FastField
405
          id="thesisTitle"
406
          type="text"
407
          name="thesisTitle"
408
          component={TextInput}
409
          grid="base(1of1)"
410
          label={intl.formatMessage(messages.thesisLabel)}
411
          placeholder={intl.formatMessage(messages.thesisPlaceholder)}
412
        />
413
        <div data-c-grid-item="base(1of1)">
414
          <FastField
415
            id="hasBlockcert"
416
            name="hasBlockcert"
417
            component={CheckboxInput}
418
            grid="base(1of1)"
419
            label={intl.formatMessage(messages.blockcertInlineLabel)}
420
            checkboxBorder
421
            borderLabel={intl.formatMessage(messages.blockcertLabel)}
422
          />
423
        </div>
424
      </div>
425
    </div>
426
  );
427
428
  return (
429
    <Modal
430
      id={modalId}
431
      parentElement={parentElement}
432
      visible={visible}
433
      onModalCancel={onModalCancel}
434
      onModalConfirm={onModalCancel}
435
      className="application-experience-dialog"
436
    >
437
      <ExperienceModalHeader
438
        title={intl.formatMessage(messages.modalTitle)}
439
        iconClass="fa-book"
440
      />
441
      <Formik
442
        enableReinitialize
443
        initialValues={initialFormValues}
444
        onSubmit={async (values, actions): Promise<void> => {
445
          await onModalConfirm(
446
            formValuesToData(values, originalExperience, locale, [
447
              ...requiredSkills,
448
              ...optionalSkills,
449
            ]),
450
          );
451
          actions.setSubmitting(false);
452
        }}
453
        validationSchema={validationSchema}
454
      >
455
        {(formikProps): React.ReactElement => (
456
          <Form>
457
            <Modal.Body>
458
              <ExperienceDetailsIntro
459
                description={intl.formatMessage(messages.modalDescription)}
460
              />
461
              {detailsSubform}
462
              <SkillSubform
463
                jobId={jobId}
464
                jobRequiredSkills={requiredSkills.map(skillToName)}
465
                jobOptionalSkills={optionalSkills.map(skillToName)}
466
              />
467
              <EducationSubform jobClassification={jobClassification} />
468
            </Modal.Body>
469
            <ExperienceModalFooter buttonsDisabled={formikProps.isSubmitting} />
470
          </Form>
471
        )}
472
      </Formik>
473
    </Modal>
474
  );
475
};
476
477
export default EducationExperienceModal;
478