Passed
Push — dev ( 916021...d73362 )
by
unknown
04:36
created

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

Complexity

Total Complexity 11
Complexity/F 0

Size

Lines of Code 476
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 11
eloc 386
mnd 11
bc 11
fnc 0
dl 0
loc 476
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
interface EducationType {
38
  id: number;
39
  name: localizedFieldNonNull;
40
}
41
42
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
const messages = defineMessages({
67
  modalTitle: {
68
    id: "educationExperienceModal.modalTitle",
69
    defaultMessage: "Add Edcuation Experience",
70
  },
71
  modalDescription: {
72
    id: "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: "educationExperienceModal.educationTypeLabel",
78
    defaultMessage: "Type of Education",
79
  },
80
  educationTypeDefault: {
81
    id: "educationExperienceModal.educationTypeDefault",
82
    defaultMessage: "Select an education type...",
83
    description: "Default selection in the Education Type dropdown menu.",
84
  },
85
  areaStudyLabel: {
86
    id: "educationExperienceModal.areaStudyLabel",
87
    defaultMessage: "Area of Study",
88
  },
89
  areaStudyPlaceholder: {
90
    id: "educationExperienceModal.areaStudyPlaceholder",
91
    defaultMessage: "e.g. Organic Chemistry",
92
  },
93
  institutionLabel: {
94
    id: "educationExperienceModal.institutionLabel",
95
    defaultMessage: "Institution",
96
  },
97
  institutionPlaceholder: {
98
    id: "educationExperienceModal.institutionPlaceholder",
99
    defaultMessage: "e.g. Bishop's University",
100
  },
101
  completionLabel: {
102
    id: "educationExperienceModal.completionLabel",
103
    defaultMessage: "Completion Status",
104
  },
105
  completionDefault: {
106
    id: "educationExperienceModal.completionDefault",
107
    defaultMessage: "Select a completion status...",
108
  },
109
  thesisLabel: {
110
    id: "educationExperienceModal.thesisLabel",
111
    defaultMessage: "Thesis Title (Optional)",
112
  },
113
  thesisPlaceholder: {
114
    id: "educationExperienceModal.thesisPlaceholder",
115
    defaultMessage: "e.g. How bats navigate between each other during flight",
116
  },
117
  blockcertLabel: {
118
    id: "educationExperienceModal.blockcertLabel",
119
    defaultMessage: "Blockcert Link (Optional)",
120
  },
121
  blockcertInlineLabel: {
122
    id: "educationExperienceModal.blockcertInlineLabel",
123
    defaultMessage:
124
      "I have a Blockcert and can provide it on request. (Optional)",
125
  },
126
  startDateLabel: {
127
    id: "educationExperienceModal.startDateLabel",
128
    defaultMessage: "Select a Start Date",
129
  },
130
  datePlaceholder: {
131
    id: "educationExperienceModal.datePlaceholder",
132
    defaultMessage: "yyyy-mm-dd",
133
  },
134
  isActiveLabel: {
135
    id: "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: "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
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
  area_of_study: "",
272
  institution: "",
273
  education_status_id: 0,
274
  thesis_title: "",
275
  has_blockcert: false,
276
  is_active: false,
277
  start_date: new Date(),
278
  end_date: null,
279
  is_education_requirement: false,
280
  experienceable_id: experienceableId,
281
  experienceable_type: experienceableType,
282
  type: "experience_education",
283
});
284
/* eslint-enable @typescript-eslint/camelcase */
285
286
export const EducationExperienceModal: React.FC<EducationExperienceModalProps> = ({
287
  modalId,
288
  experienceEducation,
289
  educationTypes,
290
  educationStatuses,
291
  jobId,
292
  requiredSkills,
293
  savedRequiredSkills,
294
  optionalSkills,
295
  savedOptionalSkills,
296
  experienceRequirments,
297
  experienceableId,
298
  experienceableType,
299
  parentElement,
300
  visible,
301
  onModalCancel,
302
  onModalConfirm,
303
}) => {
304
  const intl = useIntl();
305
  const locale = getLocale(intl.locale);
306
307
  const originalExperience =
308
    experienceEducation ??
309
    newExperienceEducation(experienceableId, experienceableType);
310
311
  const skillToName = (skill: Skill): string =>
312
    localizeFieldNonNull(locale, skill, "name");
313
314
  const initialFormValues = dataToFormValues(
315
    {
316
      experienceEducation: originalExperience,
317
      savedRequiredSkills,
318
      savedOptionalSkills,
319
    },
320
    locale,
321
    experienceEducation === null,
322
  );
323
324
  const validationSchema = Yup.object().shape({
325
    ...skillValidationShape,
326
    ...educationValidationShape,
327
    ...validationShape(intl),
328
  });
329
330
  const detailsSubform = (
331
    <div data-c-container="medium">
332
      <div data-c-grid="gutter(all, 1) middle">
333
        <FastField
334
          id="educationTypeId"
335
          name="educationTypeId"
336
          label={intl.formatMessage(messages.educationTypeLabel)}
337
          grid="tl(1of2)"
338
          component={SelectInput}
339
          required
340
          nullSelection={intl.formatMessage(messages.educationTypeDefault)}
341
          options={educationTypes.map((type) => ({
342
            value: type.id,
343
            label: localizeFieldNonNull(locale, type, "name"),
344
          }))}
345
        />
346
        <FastField
347
          id="areaOfStudy"
348
          type="text"
349
          name="areaOfStudy"
350
          component={TextInput}
351
          required
352
          grid="tl(1of2)"
353
          label={intl.formatMessage(messages.areaStudyLabel)}
354
          placeholder={intl.formatMessage(messages.areaStudyPlaceholder)}
355
        />
356
        <FastField
357
          id="institution"
358
          type="text"
359
          name="institution"
360
          component={TextInput}
361
          required
362
          grid="tl(1of2)"
363
          label={intl.formatMessage(messages.institutionLabel)}
364
          placeholder={intl.formatMessage(messages.institutionPlaceholder)}
365
        />
366
        <FastField
367
          id="educationStatusId"
368
          name="educationStatusId"
369
          label={intl.formatMessage(messages.completionLabel)}
370
          grid="tl(1of2)"
371
          component={SelectInput}
372
          required
373
          nullSelection={intl.formatMessage(messages.completionDefault)}
374
          options={educationStatuses.map((status) => ({
375
            value: status.id,
376
            label: localizeFieldNonNull(locale, status, "name"),
377
          }))}
378
        />
379
        <FastField
380
          id="startDate"
381
          name="startDate"
382
          component={DateInput}
383
          required
384
          grid="base(1of1)"
385
          label={intl.formatMessage(messages.startDateLabel)}
386
          placeholder={intl.formatMessage(messages.datePlaceholder)}
387
        />
388
        <Field
389
          id="isActive"
390
          name="isActive"
391
          component={CheckboxInput}
392
          grid="tl(1of2)"
393
          label={intl.formatMessage(messages.isActiveLabel)}
394
        />
395
        <Field
396
          id="endDate"
397
          name="endDate"
398
          component={DateInput}
399
          grid="base(1of2)"
400
          label={intl.formatMessage(messages.endDateLabel)}
401
          placeholder={intl.formatMessage(messages.datePlaceholder)}
402
        />
403
        <FastField
404
          id="thesisTitle"
405
          type="text"
406
          name="thesisTitle"
407
          component={TextInput}
408
          grid="base(1of1)"
409
          label={intl.formatMessage(messages.thesisLabel)}
410
          placeholder={intl.formatMessage(messages.thesisPlaceholder)}
411
        />
412
        <div data-c-input="checkbox(group)" data-c-grid-item="base(1of1)">
413
          <label>{intl.formatMessage(messages.blockcertLabel)}</label>
414
          <FastField
415
            id="hasBlockcert"
416
            name="hasBlockcert"
417
            component={CheckboxInput}
418
            grid="base(1of1)"
419
            label={intl.formatMessage(messages.blockcertInlineLabel)}
420
          />
421
        </div>
422
      </div>
423
    </div>
424
  );
425
426
  return (
427
    <Modal
428
      id={modalId}
429
      parentElement={parentElement}
430
      visible={visible}
431
      onModalCancel={onModalCancel}
432
      onModalConfirm={onModalCancel}
433
      className="application-experience-dialog"
434
    >
435
      <ExperienceModalHeader
436
        title={intl.formatMessage(messages.modalTitle)}
437
        iconClass="fa-book"
438
      />
439
      <Formik
440
        enableReinitialize
441
        initialValues={initialFormValues}
442
        onSubmit={async (values, actions): Promise<void> => {
443
          await onModalConfirm(
444
            formValuesToData(values, originalExperience, locale, [
445
              ...requiredSkills,
446
              ...optionalSkills,
447
            ]),
448
          );
449
          actions.setSubmitting(false);
450
        }}
451
        validationSchema={validationSchema}
452
      >
453
        {(formikProps): React.ReactElement => (
454
          <Form>
455
            <Modal.Body>
456
              <ExperienceDetailsIntro
457
                description={intl.formatMessage(messages.modalDescription)}
458
              />
459
              {detailsSubform}
460
              <SkillSubform
461
                jobId={jobId}
462
                jobRequiredSkills={requiredSkills.map(skillToName)}
463
                jobOptionalSkills={optionalSkills.map(skillToName)}
464
              />
465
              <EducationSubform {...experienceRequirments} />
466
            </Modal.Body>
467
            <ExperienceModalFooter buttonsDisabled={formikProps.isSubmitting} />
468
          </Form>
469
        )}
470
      </Formik>
471
    </Modal>
472
  );
473
};
474
475
export default EducationExperienceModal;
476