Passed
Push — dev ( 143aae...31b479 )
by
unknown
05:11
created

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

Complexity

Total Complexity 11
Complexity/F 0

Size

Lines of Code 486
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

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