Passed
Push — feature/update-managers-applca... ( b38314...b92f9a )
by Yonathan
04:52
created

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

Complexity

Total Complexity 11
Complexity/F 0

Size

Lines of Code 475
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 11
eloc 385
mnd 11
bc 11
fnc 0
dl 0
loc 475
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
});
283
/* eslint-enable @typescript-eslint/camelcase */
284
285
export const EducationExperienceModal: React.FC<EducationExperienceModalProps> = ({
286
  modalId,
287
  experienceEducation,
288
  educationTypes,
289
  educationStatuses,
290
  jobId,
291
  requiredSkills,
292
  savedRequiredSkills,
293
  optionalSkills,
294
  savedOptionalSkills,
295
  experienceRequirments,
296
  experienceableId,
297
  experienceableType,
298
  parentElement,
299
  visible,
300
  onModalCancel,
301
  onModalConfirm,
302
}) => {
303
  const intl = useIntl();
304
  const locale = getLocale(intl.locale);
305
306
  const originalExperience =
307
    experienceEducation ??
308
    newExperienceEducation(experienceableId, experienceableType);
309
310
  const skillToName = (skill: Skill): string =>
311
    localizeFieldNonNull(locale, skill, "name");
312
313
  const initialFormValues = dataToFormValues(
314
    {
315
      experienceEducation: originalExperience,
316
      savedRequiredSkills,
317
      savedOptionalSkills,
318
    },
319
    locale,
320
    experienceEducation === null,
321
  );
322
323
  const validationSchema = Yup.object().shape({
324
    ...skillValidationShape,
325
    ...educationValidationShape,
326
    ...validationShape(intl),
327
  });
328
329
  const detailsSubform = (
330
    <div data-c-container="medium">
331
      <div data-c-grid="gutter(all, 1) middle">
332
        <FastField
333
          id="educationTypeId"
334
          name="educationTypeId"
335
          label={intl.formatMessage(messages.educationTypeLabel)}
336
          grid="tl(1of2)"
337
          component={SelectInput}
338
          required
339
          nullSelection={intl.formatMessage(messages.educationTypeDefault)}
340
          options={educationTypes.map((type) => ({
341
            value: type.id,
342
            label: localizeFieldNonNull(locale, type, "name"),
343
          }))}
344
        />
345
        <FastField
346
          id="areaOfStudy"
347
          type="text"
348
          name="areaOfStudy"
349
          component={TextInput}
350
          required
351
          grid="tl(1of2)"
352
          label={intl.formatMessage(messages.areaStudyLabel)}
353
          placeholder={intl.formatMessage(messages.areaStudyPlaceholder)}
354
        />
355
        <FastField
356
          id="institution"
357
          type="text"
358
          name="institution"
359
          component={TextInput}
360
          required
361
          grid="tl(1of2)"
362
          label={intl.formatMessage(messages.institutionLabel)}
363
          placeholder={intl.formatMessage(messages.institutionPlaceholder)}
364
        />
365
        <FastField
366
          id="educationStatusId"
367
          name="educationStatusId"
368
          label={intl.formatMessage(messages.completionLabel)}
369
          grid="tl(1of2)"
370
          component={SelectInput}
371
          required
372
          nullSelection={intl.formatMessage(messages.completionDefault)}
373
          options={educationStatuses.map((status) => ({
374
            value: status.id,
375
            label: localizeFieldNonNull(locale, status, "name"),
376
          }))}
377
        />
378
        <FastField
379
          id="startDate"
380
          name="startDate"
381
          component={DateInput}
382
          required
383
          grid="base(1of1)"
384
          label={intl.formatMessage(messages.startDateLabel)}
385
          placeholder={intl.formatMessage(messages.datePlaceholder)}
386
        />
387
        <Field
388
          id="isActive"
389
          name="isActive"
390
          component={CheckboxInput}
391
          grid="tl(1of2)"
392
          label={intl.formatMessage(messages.isActiveLabel)}
393
        />
394
        <Field
395
          id="endDate"
396
          name="endDate"
397
          component={DateInput}
398
          grid="base(1of2)"
399
          label={intl.formatMessage(messages.endDateLabel)}
400
          placeholder={intl.formatMessage(messages.datePlaceholder)}
401
        />
402
        <FastField
403
          id="thesisTitle"
404
          type="text"
405
          name="thesisTitle"
406
          component={TextInput}
407
          grid="base(1of1)"
408
          label={intl.formatMessage(messages.thesisLabel)}
409
          placeholder={intl.formatMessage(messages.thesisPlaceholder)}
410
        />
411
        <div data-c-input="checkbox(group)" data-c-grid-item="base(1of1)">
412
          <label>{intl.formatMessage(messages.blockcertLabel)}</label>
413
          <FastField
414
            id="hasBlockcert"
415
            name="hasBlockcert"
416
            component={CheckboxInput}
417
            grid="base(1of1)"
418
            label={intl.formatMessage(messages.blockcertInlineLabel)}
419
          />
420
        </div>
421
      </div>
422
    </div>
423
  );
424
425
  return (
426
    <Modal
427
      id={modalId}
428
      parentElement={parentElement}
429
      visible={visible}
430
      onModalCancel={onModalCancel}
431
      onModalConfirm={onModalCancel}
432
      className="application-experience-dialog"
433
    >
434
      <ExperienceModalHeader
435
        title={intl.formatMessage(messages.modalTitle)}
436
        iconClass="fa-book"
437
      />
438
      <Formik
439
        enableReinitialize
440
        initialValues={initialFormValues}
441
        onSubmit={async (values, actions): Promise<void> => {
442
          await onModalConfirm(
443
            formValuesToData(values, originalExperience, locale, [
444
              ...requiredSkills,
445
              ...optionalSkills,
446
            ]),
447
          );
448
          actions.setSubmitting(false);
449
        }}
450
        validationSchema={validationSchema}
451
      >
452
        {(formikProps): React.ReactElement => (
453
          <Form>
454
            <Modal.Body>
455
              <ExperienceDetailsIntro
456
                description={intl.formatMessage(messages.modalDescription)}
457
              />
458
              {detailsSubform}
459
              <SkillSubform
460
                jobId={jobId}
461
                jobRequiredSkills={requiredSkills.map(skillToName)}
462
                jobOptionalSkills={optionalSkills.map(skillToName)}
463
              />
464
              <EducationSubform {...experienceRequirments} />
465
            </Modal.Body>
466
            <ExperienceModalFooter buttonsDisabled={formikProps.isSubmitting} />
467
          </Form>
468
        )}
469
      </Formik>
470
    </Modal>
471
  );
472
};
473
474
export default EducationExperienceModal;
475