Passed
Push — dev ( e21187...4e4b2b )
by
unknown
04:43
created

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

Complexity

Total Complexity 13
Complexity/F 0

Size

Lines of Code 592
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 13
eloc 472
mnd 13
bc 13
fnc 0
dl 0
loc 592
rs 10
bpm 0
cpm 0
noi 0
c 0
b 0
f 0
1
import React, { FunctionComponent } 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
  classificationEducationRequirements: string | null;
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",
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
      "Yes, 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
const DetailsSubform: FunctionComponent<{
146
  educationTypes: EducationType[];
147
  educationStatuses: EducationStatus[];
148
}> = ({ educationTypes, educationStatuses }) => {
149
  const intl = useIntl();
150
  const locale = getLocale(intl.locale);
151
  return (
152
    <div data-c-container="medium">
153
      <div data-c-grid="gutter(all, 1) middle">
154
        <FastField
155
          id="education-educationTypeId"
156
          name="educationTypeId"
157
          label={intl.formatMessage(messages.educationTypeLabel)}
158
          grid="tl(1of2)"
159
          component={SelectInput}
160
          required
161
          nullSelection={intl.formatMessage(messages.educationTypeDefault)}
162
          options={educationTypes.map((type) => ({
163
            value: type.id,
164
            label: localizeFieldNonNull(locale, type, "name"),
165
          }))}
166
        />
167
        <FastField
168
          id="education-areaOfStudy"
169
          type="text"
170
          name="areaOfStudy"
171
          component={TextInput}
172
          required
173
          grid="tl(1of2)"
174
          label={intl.formatMessage(messages.areaStudyLabel)}
175
          placeholder={intl.formatMessage(messages.areaStudyPlaceholder)}
176
        />
177
        <FastField
178
          id="education-institution"
179
          type="text"
180
          name="institution"
181
          component={TextInput}
182
          required
183
          grid="tl(1of2)"
184
          label={intl.formatMessage(messages.institutionLabel)}
185
          placeholder={intl.formatMessage(messages.institutionPlaceholder)}
186
        />
187
        <FastField
188
          id="education-educationStatusId"
189
          name="educationStatusId"
190
          label={intl.formatMessage(messages.completionLabel)}
191
          grid="tl(1of2)"
192
          component={SelectInput}
193
          required
194
          nullSelection={intl.formatMessage(messages.completionDefault)}
195
          options={educationStatuses.map((status) => ({
196
            value: status.id,
197
            label: localizeFieldNonNull(locale, status, "name"),
198
          }))}
199
        />
200
        <FastField
201
          id="education-startDate"
202
          name="startDate"
203
          component={DateInput}
204
          required
205
          grid="base(1of1)"
206
          label={intl.formatMessage(messages.startDateLabel)}
207
          placeholder={intl.formatMessage(messages.datePlaceholder)}
208
        />
209
        <Field
210
          id="education-isActive"
211
          name="isActive"
212
          component={CheckboxInput}
213
          grid="tl(1of2)"
214
          label={intl.formatMessage(messages.isActiveLabel)}
215
        />
216
        <Field
217
          id="education-endDate"
218
          name="endDate"
219
          component={DateInput}
220
          grid="base(1of2)"
221
          label={intl.formatMessage(messages.endDateLabel)}
222
          placeholder={intl.formatMessage(messages.datePlaceholder)}
223
        />
224
        <FastField
225
          id="education-thesisTitle"
226
          type="text"
227
          name="thesisTitle"
228
          component={TextInput}
229
          grid="base(1of1)"
230
          label={intl.formatMessage(messages.thesisLabel)}
231
          placeholder={intl.formatMessage(messages.thesisPlaceholder)}
232
        />
233
        <div data-c-grid-item="base(1of1)">
234
          <FastField
235
            id="education-hasBlockcert"
236
            name="hasBlockcert"
237
            component={CheckboxInput}
238
            grid="base(1of1)"
239
            label={intl.formatMessage(messages.blockcertInlineLabel)}
240
            checkboxBorder
241
            borderLabel={intl.formatMessage(messages.blockcertLabel)}
242
          />
243
        </div>
244
      </div>
245
    </div>
246
  );
247
};
248
249
export interface EducationDetailsFormValues {
250
  educationTypeId: number | "";
251
  areaOfStudy: string;
252
  institution: string;
253
  educationStatusId: number | "";
254
  thesisTitle: string;
255
  startDate: string;
256
  isActive: boolean;
257
  endDate: string;
258
  hasBlockcert: boolean;
259
}
260
261
const experienceToDetails = (
262
  experience: ExperienceEducation,
263
  creatingNew: boolean,
264
): EducationDetailsFormValues => {
265
  return {
266
    educationTypeId: creatingNew ? "" : experience.education_type_id,
267
    areaOfStudy: experience.area_of_study,
268
    institution: experience.institution,
269
    educationStatusId: creatingNew ? "" : experience.education_status_id,
270
    thesisTitle: experience.thesis_title ?? "",
271
    hasBlockcert: experience.has_blockcert,
272
    startDate: toInputDateString(experience.start_date),
273
    isActive: experience.is_active,
274
    endDate: experience.end_date ? toInputDateString(experience.end_date) : "",
275
  };
276
};
277
278
const detailsToExperience = (
279
  formValues: EducationDetailsFormValues,
280
  originalExperience: ExperienceEducation,
281
): ExperienceEducation => {
282
  return {
283
    ...originalExperience,
284
    education_type_id: formValues.educationTypeId
285
      ? Number(formValues.educationTypeId)
286
      : 1,
287
    area_of_study: formValues.areaOfStudy,
288
    institution: formValues.institution,
289
    education_status_id: formValues.educationStatusId
290
      ? Number(formValues.educationStatusId)
291
      : 1,
292
    thesis_title: formValues.thesisTitle ? formValues.thesisTitle : "",
293
    has_blockcert: formValues.hasBlockcert,
294
    start_date: fromInputDateString(formValues.startDate),
295
    is_active: formValues.isActive,
296
    end_date: formValues.endDate
297
      ? fromInputDateString(formValues.endDate)
298
      : null,
299
  };
300
};
301
302
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
303
export const validationShape = (intl: IntlShape) => {
304
  const requiredMsg = intl.formatMessage(validationMessages.required);
305
  const conditionalRequiredMsg = intl.formatMessage(
306
    validationMessages.endDateRequiredIfNotOngoing,
307
  );
308
  const inPastMsg = intl.formatMessage(validationMessages.dateMustBePast);
309
  const afterStartDateMsg = intl.formatMessage(
310
    validationMessages.endDateAfterStart,
311
  );
312
  return {
313
    educationTypeId: Yup.number().required(requiredMsg),
314
    areaOfStudy: Yup.string().required(requiredMsg),
315
    institution: Yup.string().required(requiredMsg),
316
    educationStatusId: Yup.number().required(requiredMsg),
317
    thesisTitle: Yup.string(),
318
    hasBlockcert: Yup.boolean(),
319
    startDate: Yup.date().required(requiredMsg).max(new Date(), inPastMsg),
320
    isActive: Yup.boolean(),
321
    endDate: Yup.date().when("isActive", {
322
      is: false,
323
      then: Yup.date()
324
        .required(conditionalRequiredMsg)
325
        .min(Yup.ref("startDate"), afterStartDateMsg),
326
      otherwise: Yup.date().min(Yup.ref("startDate"), afterStartDateMsg),
327
    }),
328
  };
329
};
330
331
const newExperienceEducation = (
332
  experienceableId: number,
333
  experienceableType: ExperienceEducation["experienceable_type"],
334
): ExperienceEducation => ({
335
  id: 0,
336
  education_type_id: 0,
337
  education_type: { en: "", fr: "" },
338
  area_of_study: "",
339
  institution: "",
340
  education_status_id: 0,
341
  education_status: { en: "", fr: "" },
342
  thesis_title: "",
343
  has_blockcert: false,
344
  is_active: false,
345
  start_date: new Date(),
346
  end_date: null,
347
  is_education_requirement: false,
348
  experienceable_id: experienceableId,
349
  experienceable_type: experienceableType,
350
  type: "experience_education",
351
});
352
353
interface ProfileEducationModalProps {
354
  modalId: string;
355
  experienceEducation: ExperienceEducation | null;
356
  educationTypes: EducationType[];
357
  educationStatuses: EducationStatus[];
358
  experienceableId: number;
359
  experienceableType: ExperienceEducation["experienceable_type"];
360
  parentElement: Element | null;
361
  visible: boolean;
362
  onModalCancel: () => void;
363
  onModalConfirm: (experience: ExperienceEducation) => Promise<void>;
364
}
365
366
export const ProfileEducationModal: FunctionComponent<ProfileEducationModalProps> = ({
367
  modalId,
368
  experienceEducation,
369
  educationTypes,
370
  educationStatuses,
371
  experienceableId,
372
  experienceableType,
373
  parentElement,
374
  visible,
375
  onModalCancel,
376
  onModalConfirm,
377
}) => {
378
  const intl = useIntl();
379
380
  const originalExperience =
381
    experienceEducation ??
382
    newExperienceEducation(experienceableId, experienceableType);
383
384
  const initialFormValues = experienceToDetails(
385
    originalExperience,
386
    experienceEducation === null,
387
  );
388
389
  const validationSchema = Yup.object().shape({
390
    ...validationShape(intl),
391
  });
392
393
  return (
394
    <Modal
395
      id={modalId}
396
      parentElement={parentElement}
397
      visible={visible}
398
      onModalCancel={onModalCancel}
399
      onModalConfirm={onModalCancel}
400
      className="application-experience-dialog"
401
    >
402
      <ExperienceModalHeader
403
        title={intl.formatMessage(messages.modalTitle)}
404
        iconClass="fa-book"
405
      />
406
      <Formik
407
        enableReinitialize
408
        initialValues={initialFormValues}
409
        onSubmit={async (values, actions): Promise<void> => {
410
          await onModalConfirm(detailsToExperience(values, originalExperience));
411
          actions.setSubmitting(false);
412
          actions.resetForm();
413
        }}
414
        validationSchema={validationSchema}
415
      >
416
        {(formikProps): React.ReactElement => (
417
          <Form>
418
            <Modal.Body>
419
              <ExperienceDetailsIntro
420
                description={intl.formatMessage(messages.modalDescription)}
421
              />
422
              <DetailsSubform
423
                educationTypes={educationTypes}
424
                educationStatuses={educationStatuses}
425
              />
426
            </Modal.Body>
427
            <ExperienceModalFooter buttonsDisabled={formikProps.isSubmitting} />
428
          </Form>
429
        )}
430
      </Formik>
431
    </Modal>
432
  );
433
};
434
435
type EducationExperienceFormValues = SkillFormValues &
436
  EducationFormValues &
437
  EducationDetailsFormValues;
438
export interface EducationExperienceSubmitData {
439
  experienceEducation: ExperienceEducation;
440
  savedRequiredSkills: Skill[];
441
  savedOptionalSkills: Skill[];
442
}
443
444
const dataToFormValues = (
445
  data: EducationExperienceSubmitData,
446
  locale: Locales,
447
  creatingNew: boolean,
448
): EducationExperienceFormValues => {
449
  const {
450
    experienceEducation,
451
    savedRequiredSkills,
452
    savedOptionalSkills,
453
  } = data;
454
  const skillToName = (skill: Skill): string =>
455
    localizeFieldNonNull(locale, skill, "name");
456
  return {
457
    requiredSkills: savedRequiredSkills.map(skillToName),
458
    optionalSkills: savedOptionalSkills.map(skillToName),
459
    useAsEducationRequirement: experienceEducation.is_education_requirement,
460
    ...experienceToDetails(data.experienceEducation, creatingNew),
461
  };
462
};
463
464
const formValuesToData = (
465
  formValues: EducationExperienceFormValues,
466
  originalExperience: ExperienceEducation,
467
  locale: Locales,
468
  skills: Skill[],
469
): EducationExperienceSubmitData => {
470
  const nameToSkill = (name: string): Skill | null =>
471
    matchValueToModel(locale, "name", name, skills);
472
  return {
473
    experienceEducation: {
474
      ...detailsToExperience(formValues, originalExperience),
475
      is_education_requirement: formValues.useAsEducationRequirement,
476
    },
477
    savedRequiredSkills: formValues.requiredSkills
478
      .map(nameToSkill)
479
      .filter(notEmpty),
480
    savedOptionalSkills: formValues.optionalSkills
481
      .map(nameToSkill)
482
      .filter(notEmpty),
483
  };
484
};
485
486
export const EducationExperienceModal: React.FC<EducationExperienceModalProps> = ({
487
  modalId,
488
  experienceEducation,
489
  educationTypes,
490
  educationStatuses,
491
  jobId,
492
  classificationEducationRequirements,
493
  jobEducationRequirements,
494
  requiredSkills,
495
  savedRequiredSkills,
496
  optionalSkills,
497
  savedOptionalSkills,
498
  experienceableId,
499
  experienceableType,
500
  parentElement,
501
  visible,
502
  onModalCancel,
503
  onModalConfirm,
504
}) => {
505
  const intl = useIntl();
506
  const locale = getLocale(intl.locale);
507
508
  const originalExperience =
509
    experienceEducation ??
510
    newExperienceEducation(experienceableId, experienceableType);
511
512
  const skillToName = (skill: Skill): string =>
513
    localizeFieldNonNull(locale, skill, "name");
514
515
  const initialFormValues = dataToFormValues(
516
    {
517
      experienceEducation: originalExperience,
518
      savedRequiredSkills,
519
      savedOptionalSkills,
520
    },
521
    locale,
522
    experienceEducation === null,
523
  );
524
525
  const validationSchema = Yup.object().shape({
526
    ...skillValidationShape,
527
    ...educationValidationShape,
528
    ...validationShape(intl),
529
  });
530
531
  return (
532
    <Modal
533
      id={modalId}
534
      parentElement={parentElement}
535
      visible={visible}
536
      onModalCancel={onModalCancel}
537
      onModalConfirm={onModalCancel}
538
      className="application-experience-dialog"
539
    >
540
      <ExperienceModalHeader
541
        title={intl.formatMessage(messages.modalTitle)}
542
        iconClass="fa-book"
543
      />
544
      <Formik
545
        enableReinitialize
546
        initialValues={initialFormValues}
547
        onSubmit={async (values, actions): Promise<void> => {
548
          await onModalConfirm(
549
            formValuesToData(values, originalExperience, locale, [
550
              ...requiredSkills,
551
              ...optionalSkills,
552
            ]),
553
          );
554
          actions.setSubmitting(false);
555
          actions.resetForm();
556
        }}
557
        validationSchema={validationSchema}
558
      >
559
        {(formikProps): React.ReactElement => (
560
          <Form>
561
            <Modal.Body>
562
              <ExperienceDetailsIntro
563
                description={intl.formatMessage(messages.modalDescription)}
564
              />
565
              <DetailsSubform
566
                educationTypes={educationTypes}
567
                educationStatuses={educationStatuses}
568
              />
569
              <SkillSubform
570
                keyPrefix="education"
571
                jobId={jobId}
572
                jobRequiredSkills={requiredSkills.map(skillToName)}
573
                jobOptionalSkills={optionalSkills.map(skillToName)}
574
              />
575
              <EducationSubform
576
                keyPrefix="education"
577
                classificationEducationRequirements={
578
                  classificationEducationRequirements
579
                }
580
                jobEducationRequirements={jobEducationRequirements}
581
              />
582
            </Modal.Body>
583
            <ExperienceModalFooter buttonsDisabled={formikProps.isSubmitting} />
584
          </Form>
585
        )}
586
      </Formik>
587
    </Modal>
588
  );
589
};
590
591
export default EducationExperienceModal;
592