Passed
Push — feature/experience-form-modals ( 37703b...ac040e )
by Tristan
03:52
created

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

Complexity

Total Complexity 13
Complexity/F 0

Size

Lines of Code 465
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

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