Passed
Push — feature/data-request-hooks ( ad0abd...ffe931 )
by Tristan
06:29
created

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

Complexity

Total Complexity 13
Complexity/F 0

Size

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