Passed
Push — dev ( 0fcacc...5cec22 )
by Yonathan
04:14
created

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

Complexity

Total Complexity 11
Complexity/F 0

Size

Lines of Code 479
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 11
eloc 389
mnd 11
bc 11
fnc 0
dl 0
loc 479
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
  EducationSubformProps,
7
  EducationFormValues,
8
  EducationSubform,
9
  validationShape as educationValidationShape,
10
} from "./EducationSubform";
11
import TextInput from "../../Form/TextInput";
12
import CheckboxInput from "../../Form/CheckboxInput";
13
import { validationMessages } from "../../Form/Messages";
14
import SkillSubform, {
15
  SkillFormValues,
16
  validationShape as skillValidationShape,
17
} from "./SkillSubform";
18
import { Skill, ExperienceEducation } from "../../../models/types";
19
import {
20
  ExperienceModalHeader,
21
  ExperienceDetailsIntro,
22
  ExperienceModalFooter,
23
} from "./ExperienceModalCommon";
24
import Modal from "../../Modal";
25
import DateInput from "../../Form/DateInput";
26
import { toInputDateString, fromInputDateString } from "../../../helpers/dates";
27
import {
28
  Locales,
29
  localizeFieldNonNull,
30
  getLocale,
31
  matchValueToModel,
32
} from "../../../helpers/localize";
33
import { notEmpty } from "../../../helpers/queries";
34
import { localizedFieldNonNull } from "../../../models/app";
35
import SelectInput from "../../Form/SelectInput";
36
37
export interface EducationType {
38
  id: number;
39
  name: localizedFieldNonNull;
40
}
41
42
export interface EducationStatus {
43
  id: number;
44
  name: localizedFieldNonNull;
45
}
46
47
interface EducationExperienceModalProps {
48
  modalId: string;
49
  experienceEducation: ExperienceEducation | null;
50
  educationTypes: EducationType[];
51
  educationStatuses: EducationStatus[];
52
  jobId: number;
53
  requiredSkills: Skill[];
54
  savedRequiredSkills: Skill[];
55
  optionalSkills: Skill[];
56
  savedOptionalSkills: Skill[];
57
  experienceRequirments: EducationSubformProps;
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
  requiredSkills,
295
  savedRequiredSkills,
296
  optionalSkills,
297
  savedOptionalSkills,
298
  experienceRequirments,
299
  experienceableId,
300
  experienceableType,
301
  parentElement,
302
  visible,
303
  onModalCancel,
304
  onModalConfirm,
305
}) => {
306
  const intl = useIntl();
307
  const locale = getLocale(intl.locale);
308
309
  const originalExperience =
310
    experienceEducation ??
311
    newExperienceEducation(experienceableId, experienceableType);
312
313
  const skillToName = (skill: Skill): string =>
314
    localizeFieldNonNull(locale, skill, "name");
315
316
  const initialFormValues = dataToFormValues(
317
    {
318
      experienceEducation: originalExperience,
319
      savedRequiredSkills,
320
      savedOptionalSkills,
321
    },
322
    locale,
323
    experienceEducation === null,
324
  );
325
326
  const validationSchema = Yup.object().shape({
327
    ...skillValidationShape,
328
    ...educationValidationShape,
329
    ...validationShape(intl),
330
  });
331
332
  const detailsSubform = (
333
    <div data-c-container="medium">
334
      <div data-c-grid="gutter(all, 1) middle">
335
        <FastField
336
          id="educationTypeId"
337
          name="educationTypeId"
338
          label={intl.formatMessage(messages.educationTypeLabel)}
339
          grid="tl(1of2)"
340
          component={SelectInput}
341
          required
342
          nullSelection={intl.formatMessage(messages.educationTypeDefault)}
343
          options={educationTypes.map((type) => ({
344
            value: type.id,
345
            label: localizeFieldNonNull(locale, type, "name"),
346
          }))}
347
        />
348
        <FastField
349
          id="areaOfStudy"
350
          type="text"
351
          name="areaOfStudy"
352
          component={TextInput}
353
          required
354
          grid="tl(1of2)"
355
          label={intl.formatMessage(messages.areaStudyLabel)}
356
          placeholder={intl.formatMessage(messages.areaStudyPlaceholder)}
357
        />
358
        <FastField
359
          id="institution"
360
          type="text"
361
          name="institution"
362
          component={TextInput}
363
          required
364
          grid="tl(1of2)"
365
          label={intl.formatMessage(messages.institutionLabel)}
366
          placeholder={intl.formatMessage(messages.institutionPlaceholder)}
367
        />
368
        <FastField
369
          id="educationStatusId"
370
          name="educationStatusId"
371
          label={intl.formatMessage(messages.completionLabel)}
372
          grid="tl(1of2)"
373
          component={SelectInput}
374
          required
375
          nullSelection={intl.formatMessage(messages.completionDefault)}
376
          options={educationStatuses.map((status) => ({
377
            value: status.id,
378
            label: localizeFieldNonNull(locale, status, "name"),
379
          }))}
380
        />
381
        <FastField
382
          id="startDate"
383
          name="startDate"
384
          component={DateInput}
385
          required
386
          grid="base(1of1)"
387
          label={intl.formatMessage(messages.startDateLabel)}
388
          placeholder={intl.formatMessage(messages.datePlaceholder)}
389
        />
390
        <Field
391
          id="isActive"
392
          name="isActive"
393
          component={CheckboxInput}
394
          grid="tl(1of2)"
395
          label={intl.formatMessage(messages.isActiveLabel)}
396
        />
397
        <Field
398
          id="endDate"
399
          name="endDate"
400
          component={DateInput}
401
          grid="base(1of2)"
402
          label={intl.formatMessage(messages.endDateLabel)}
403
          placeholder={intl.formatMessage(messages.datePlaceholder)}
404
        />
405
        <FastField
406
          id="thesisTitle"
407
          type="text"
408
          name="thesisTitle"
409
          component={TextInput}
410
          grid="base(1of1)"
411
          label={intl.formatMessage(messages.thesisLabel)}
412
          placeholder={intl.formatMessage(messages.thesisPlaceholder)}
413
        />
414
        <div data-c-grid-item="base(1of1)">
415
          <FastField
416
            id="hasBlockcert"
417
            name="hasBlockcert"
418
            component={CheckboxInput}
419
            grid="base(1of1)"
420
            label={intl.formatMessage(messages.blockcertInlineLabel)}
421
            checkboxBorder
422
            borderLabel={intl.formatMessage(messages.blockcertLabel)}
423
          />
424
        </div>
425
      </div>
426
    </div>
427
  );
428
429
  return (
430
    <Modal
431
      id={modalId}
432
      parentElement={parentElement}
433
      visible={visible}
434
      onModalCancel={onModalCancel}
435
      onModalConfirm={onModalCancel}
436
      className="application-experience-dialog"
437
    >
438
      <ExperienceModalHeader
439
        title={intl.formatMessage(messages.modalTitle)}
440
        iconClass="fa-book"
441
      />
442
      <Formik
443
        enableReinitialize
444
        initialValues={initialFormValues}
445
        onSubmit={async (values, actions): Promise<void> => {
446
          await onModalConfirm(
447
            formValuesToData(values, originalExperience, locale, [
448
              ...requiredSkills,
449
              ...optionalSkills,
450
            ]),
451
          );
452
          actions.setSubmitting(false);
453
        }}
454
        validationSchema={validationSchema}
455
      >
456
        {(formikProps): React.ReactElement => (
457
          <Form>
458
            <Modal.Body>
459
              <ExperienceDetailsIntro
460
                description={intl.formatMessage(messages.modalDescription)}
461
              />
462
              {detailsSubform}
463
              <SkillSubform
464
                jobId={jobId}
465
                jobRequiredSkills={requiredSkills.map(skillToName)}
466
                jobOptionalSkills={optionalSkills.map(skillToName)}
467
              />
468
              <EducationSubform {...experienceRequirments} />
469
            </Modal.Body>
470
            <ExperienceModalFooter buttonsDisabled={formikProps.isSubmitting} />
471
          </Form>
472
        )}
473
      </Formik>
474
    </Modal>
475
  );
476
};
477
478
export default EducationExperienceModal;
479