Passed
Push — task/ci-browser-test-actions ( 169566...7c3083 )
by
unknown
06:39
created

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

Complexity

Total Complexity 6
Complexity/F 0

Size

Lines of Code 484
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 6
eloc 390
mnd 6
bc 6
fnc 0
dl 0
loc 484
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, ExperiencePersonal } 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 TextAreaInput from "../../Form/TextAreaInput";
35
import { countNumberOfWords } from "../../WordCounter/helpers";
36
37
export const messages = defineMessages({
38
  modalTitle: {
39
    id: "application.personalExperienceModal.modalTitle",
40
    defaultMessage: "Add Personal Experience",
41
  },
42
  modalDescription: {
43
    id: "application.personalExperienceModal.modalDescription",
44
    defaultMessage:
45
      "People are more than just education and work experiences. We want to make space for you to share your learning from other experiences. To protect your privacy, please don't share sensitive information about yourself or others. A good measure would be if you are comfortable with all your colleagues knowing it. (Hint: Focus on the skills for the job when you decide on what examples to share.)",
46
  },
47
  titleLabel: {
48
    id: "application.personalExperienceModal.titleLabel",
49
    defaultMessage: "Give this experience a title:",
50
  },
51
  titlePlaceholder: {
52
    id: "application.personalExperienceModal.titlePlaceholder",
53
    defaultMessage: "e.g. My Parenting Experience",
54
  },
55
  descriptionLabel: {
56
    id: "application.personalExperienceModal.descriptionLabel",
57
    defaultMessage: "Describe the project or activity:",
58
  },
59
  descriptionPlaceholder: {
60
    id: "application.personalExperienceModal.descriptionPlaceholder",
61
    defaultMessage: "e.g. I have extensive experience in...",
62
  },
63
  isShareableLabel: {
64
    id: "application.personalExperienceModal.isShareableLabel",
65
    defaultMessage: "Sharing Consent",
66
  },
67
  isShareableInlineLabel: {
68
    id: "application.personalExperienceModal.isShareableInlineLabel",
69
    defaultMessage:
70
      "Please acknowledge that this information is not sensitive in nature as it may be shared with the staff managing this job application.",
71
  },
72
  startDateLabel: {
73
    id: "application.personalExperienceModal.startDateLabel",
74
    defaultMessage: "Select a Start Date",
75
  },
76
  datePlaceholder: {
77
    id: "application.personalExperienceModal.datePlaceholder",
78
    defaultMessage: "yyyy-mm-dd",
79
  },
80
  isActiveLabel: {
81
    id: "application.personalExperienceModal.isActiveLabel",
82
    defaultMessage: "This experience is still ongoing, or...",
83
    description: "Label for checkbox that indicates work is still ongoing.",
84
  },
85
  endDateLabel: {
86
    id: "application.personalExperienceModal.endDateLabel",
87
    defaultMessage: "Select an End Date",
88
  },
89
});
90
91
const DESCRIPTION_WORD_LIMIT = 100;
92
93
const DetailsSubform: FunctionComponent = () => {
94
  const intl = useIntl();
95
  return (
96
    <div data-c-container="medium">
97
      <div data-c-grid="gutter(all, 1) middle">
98
        <FastField
99
          id="personal-title"
100
          name="title"
101
          type="text"
102
          grid="base(1of1)"
103
          component={TextInput}
104
          required
105
          label={intl.formatMessage(messages.titleLabel)}
106
          placeholder={intl.formatMessage(messages.titlePlaceholder)}
107
        />
108
        <FastField
109
          id="personal-description"
110
          type="text"
111
          name="description"
112
          component={TextAreaInput}
113
          required
114
          grid="tl(1of1)"
115
          label={intl.formatMessage(messages.descriptionLabel)}
116
          placeholder={intl.formatMessage(messages.descriptionPlaceholder)}
117
          wordLimit={DESCRIPTION_WORD_LIMIT}
118
        />
119
        <div data-c-input="checkbox(group)" data-c-grid-item="base(1of1)">
120
          <label>{intl.formatMessage(messages.isShareableLabel)}</label>
121
          <FastField
122
            id="personal-isShareable"
123
            name="isShareable"
124
            component={CheckboxInput}
125
            grid="base(1of1)"
126
            label={intl.formatMessage(messages.isShareableInlineLabel)}
127
            checkboxGroup
128
          />
129
        </div>
130
        <FastField
131
          id="personal-startDate"
132
          name="startDate"
133
          component={DateInput}
134
          required
135
          grid="base(1of1)"
136
          label={intl.formatMessage(messages.startDateLabel)}
137
          placeholder={intl.formatMessage(messages.datePlaceholder)}
138
        />
139
        <Field
140
          id="personal-isActive"
141
          name="isActive"
142
          component={CheckboxInput}
143
          grid="tl(1of2)"
144
          label={intl.formatMessage(messages.isActiveLabel)}
145
        />
146
        <Field
147
          id="personal-endDate"
148
          name="endDate"
149
          component={DateInput}
150
          grid="base(1of2)"
151
          label={intl.formatMessage(messages.endDateLabel)}
152
          placeholder={intl.formatMessage(messages.datePlaceholder)}
153
        />
154
      </div>
155
    </div>
156
  );
157
};
158
159
export interface PersonalDetailsFormValues {
160
  title: string;
161
  description: string;
162
  isShareable: boolean;
163
  startDate: string;
164
  isActive: boolean;
165
  endDate: string;
166
}
167
168
type PersonalExperienceFormValues = SkillFormValues &
169
  EducationFormValues &
170
  PersonalDetailsFormValues;
171
export interface PersonalExperienceSubmitData {
172
  experiencePersonal: ExperiencePersonal;
173
  savedRequiredSkills: Skill[];
174
  savedOptionalSkills: Skill[];
175
}
176
177
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
178
export const validationShape = (intl: IntlShape) => {
179
  const requiredMsg = intl.formatMessage(validationMessages.required);
180
  const conditionalRequiredMsg = intl.formatMessage(
181
    validationMessages.endDateRequiredIfNotOngoing,
182
  );
183
  const inPastMsg = intl.formatMessage(validationMessages.dateMustBePast);
184
  const afterStartDateMsg = intl.formatMessage(
185
    validationMessages.endDateAfterStart,
186
  );
187
  const tooLong = intl.formatMessage(validationMessages.tooLong);
188
  return {
189
    title: Yup.string().required(requiredMsg),
190
    description: Yup.string()
191
      .required(requiredMsg)
192
      .test(
193
        "under-word-limit",
194
        tooLong,
195
        (value: string) => countNumberOfWords(value) <= DESCRIPTION_WORD_LIMIT,
196
      ),
197
    isShareable: Yup.boolean(),
198
    startDate: Yup.date().required(requiredMsg).max(new Date(), inPastMsg),
199
    isActive: Yup.boolean(),
200
    endDate: Yup.date().when("isActive", {
201
      is: false,
202
      then: Yup.date()
203
        .required(conditionalRequiredMsg)
204
        .min(Yup.ref("startDate"), afterStartDateMsg),
205
      otherwise: Yup.date().min(Yup.ref("startDate"), afterStartDateMsg),
206
    }),
207
  };
208
};
209
210
const experienceToDetails = (
211
  experiencePersonal: ExperiencePersonal,
212
): PersonalDetailsFormValues => {
213
  return {
214
    title: experiencePersonal.title,
215
    description: experiencePersonal.description,
216
    isShareable: experiencePersonal.is_shareable,
217
    startDate: toInputDateString(experiencePersonal.start_date),
218
    isActive: experiencePersonal.is_active,
219
    endDate: experiencePersonal.end_date
220
      ? toInputDateString(experiencePersonal.end_date)
221
      : "",
222
  };
223
};
224
225
const dataToFormValues = (
226
  data: PersonalExperienceSubmitData,
227
  locale: Locales,
228
): PersonalExperienceFormValues => {
229
  const { experiencePersonal, savedRequiredSkills, savedOptionalSkills } = data;
230
  const skillToName = (skill: Skill): string =>
231
    localizeFieldNonNull(locale, skill, "name");
232
  return {
233
    ...experienceToDetails(data.experiencePersonal),
234
    requiredSkills: savedRequiredSkills.map(skillToName),
235
    optionalSkills: savedOptionalSkills.map(skillToName),
236
    useAsEducationRequirement: experiencePersonal.is_education_requirement,
237
  };
238
};
239
240
const detailsToExperience = (
241
  formValues: PersonalDetailsFormValues,
242
  originalExperience: ExperiencePersonal,
243
): ExperiencePersonal => {
244
  return {
245
    ...originalExperience,
246
    title: formValues.title,
247
    description: formValues.description,
248
    is_shareable: formValues.isShareable,
249
    start_date: fromInputDateString(formValues.startDate),
250
    is_active: formValues.isActive,
251
    end_date: formValues.endDate
252
      ? fromInputDateString(formValues.endDate)
253
      : null,
254
  };
255
};
256
257
const formValuesToData = (
258
  formValues: PersonalExperienceFormValues,
259
  originalExperience: ExperiencePersonal,
260
  locale: Locales,
261
  skills: Skill[],
262
): PersonalExperienceSubmitData => {
263
  const nameToSkill = (name: string): Skill | null =>
264
    matchValueToModel(locale, "name", name, skills);
265
  return {
266
    experiencePersonal: {
267
      ...detailsToExperience(formValues, originalExperience),
268
      is_education_requirement: formValues.useAsEducationRequirement,
269
    },
270
    savedRequiredSkills: formValues.requiredSkills
271
      .map(nameToSkill)
272
      .filter(notEmpty),
273
    savedOptionalSkills: formValues.optionalSkills
274
      .map(nameToSkill)
275
      .filter(notEmpty),
276
  };
277
};
278
279
const newPersonalExperience = (
280
  experienceableId: number,
281
  experienceableType: ExperiencePersonal["experienceable_type"],
282
): ExperiencePersonal => ({
283
  id: 0,
284
  title: "",
285
  description: "",
286
  is_shareable: false,
287
  is_active: false,
288
  start_date: new Date(),
289
  end_date: null,
290
  is_education_requirement: false,
291
  experienceable_id: experienceableId,
292
  experienceable_type: experienceableType,
293
  type: "experience_personal",
294
});
295
296
interface ProfilePersonalModalProps {
297
  modalId: string;
298
  experiencePersonal: ExperiencePersonal | null;
299
  experienceableId: number;
300
  experienceableType: ExperiencePersonal["experienceable_type"];
301
  parentElement: Element | null;
302
  visible: boolean;
303
  onModalCancel: () => void;
304
  onModalConfirm: (data: ExperiencePersonal) => Promise<void>;
305
}
306
307
export const ProfilePersonalModal: FunctionComponent<ProfilePersonalModalProps> = ({
308
  modalId,
309
  experiencePersonal,
310
  experienceableId,
311
  experienceableType,
312
  parentElement,
313
  visible,
314
  onModalCancel,
315
  onModalConfirm,
316
}) => {
317
  const intl = useIntl();
318
319
  const originalExperience =
320
    experiencePersonal ??
321
    newPersonalExperience(experienceableId, experienceableType);
322
323
  const initialFormValues = experienceToDetails(originalExperience);
324
325
  const validationSchema = Yup.object().shape({
326
    ...validationShape(intl),
327
  });
328
329
  return (
330
    <Modal
331
      id={modalId}
332
      parentElement={parentElement}
333
      visible={visible}
334
      onModalCancel={onModalCancel}
335
      onModalConfirm={onModalCancel}
336
      className="application-experience-dialog"
337
    >
338
      <ExperienceModalHeader
339
        title={intl.formatMessage(messages.modalTitle)}
340
        iconClass="fa-mountain"
341
      />
342
      <Formik
343
        enableReinitialize
344
        initialValues={initialFormValues}
345
        onSubmit={async (values, actions): Promise<void> => {
346
          await onModalConfirm(detailsToExperience(values, originalExperience));
347
          actions.setSubmitting(false);
348
          actions.resetForm();
349
        }}
350
        validationSchema={validationSchema}
351
      >
352
        {(formikProps): React.ReactElement => (
353
          <Form>
354
            <Modal.Body>
355
              <ExperienceDetailsIntro
356
                description={intl.formatMessage(messages.modalDescription)}
357
              />
358
              <DetailsSubform />
359
            </Modal.Body>
360
            <ExperienceModalFooter buttonsDisabled={formikProps.isSubmitting} />
361
          </Form>
362
        )}
363
      </Formik>
364
    </Modal>
365
  );
366
};
367
368
interface PersonalExperienceModalProps {
369
  modalId: string;
370
  experiencePersonal: ExperiencePersonal | null;
371
  jobId: number;
372
  jobClassification: string;
373
  jobEducationRequirements: string | null;
374
  requiredSkills: Skill[];
375
  savedRequiredSkills: Skill[];
376
  optionalSkills: Skill[];
377
  savedOptionalSkills: Skill[];
378
  experienceableId: number;
379
  experienceableType: ExperiencePersonal["experienceable_type"];
380
  parentElement: Element | null;
381
  visible: boolean;
382
  onModalCancel: () => void;
383
  onModalConfirm: (data: PersonalExperienceSubmitData) => Promise<void>;
384
}
385
386
export const PersonalExperienceModal: React.FC<PersonalExperienceModalProps> = ({
387
  modalId,
388
  experiencePersonal,
389
  jobId,
390
  jobClassification,
391
  jobEducationRequirements,
392
  requiredSkills,
393
  savedRequiredSkills,
394
  optionalSkills,
395
  savedOptionalSkills,
396
  experienceableId,
397
  experienceableType,
398
  parentElement,
399
  visible,
400
  onModalCancel,
401
  onModalConfirm,
402
}) => {
403
  const intl = useIntl();
404
  const locale = getLocale(intl.locale);
405
406
  const originalExperience =
407
    experiencePersonal ??
408
    newPersonalExperience(experienceableId, experienceableType);
409
410
  const skillToName = (skill: Skill): string =>
411
    localizeFieldNonNull(locale, skill, "name");
412
413
  const initialFormValues = dataToFormValues(
414
    {
415
      experiencePersonal: originalExperience,
416
      savedRequiredSkills,
417
      savedOptionalSkills,
418
    },
419
    locale,
420
  );
421
422
  const validationSchema = Yup.object().shape({
423
    ...skillValidationShape,
424
    ...educationValidationShape,
425
    ...validationShape(intl),
426
  });
427
428
  return (
429
    <Modal
430
      id={modalId}
431
      parentElement={parentElement}
432
      visible={visible}
433
      onModalCancel={onModalCancel}
434
      onModalConfirm={onModalCancel}
435
      className="application-experience-dialog"
436
    >
437
      <ExperienceModalHeader
438
        title={intl.formatMessage(messages.modalTitle)}
439
        iconClass="fa-mountain"
440
      />
441
      <Formik
442
        enableReinitialize
443
        initialValues={initialFormValues}
444
        onSubmit={async (values, actions): Promise<void> => {
445
          await onModalConfirm(
446
            formValuesToData(values, originalExperience, locale, [
447
              ...requiredSkills,
448
              ...optionalSkills,
449
            ]),
450
          );
451
          actions.setSubmitting(false);
452
          actions.resetForm();
453
        }}
454
        validationSchema={validationSchema}
455
      >
456
        {(formikProps): React.ReactElement => (
457
          <Form>
458
            <Modal.Body>
459
              <ExperienceDetailsIntro
460
                description={intl.formatMessage(messages.modalDescription)}
461
              />
462
              <DetailsSubform />
463
              <SkillSubform
464
                keyPrefix="personal"
465
                jobId={jobId}
466
                jobRequiredSkills={requiredSkills.map(skillToName)}
467
                jobOptionalSkills={optionalSkills.map(skillToName)}
468
              />
469
              <EducationSubform
470
                keyPrefix="personal"
471
                jobClassification={jobClassification}
472
                jobEducationRequirements={jobEducationRequirements}
473
              />
474
            </Modal.Body>
475
            <ExperienceModalFooter buttonsDisabled={formikProps.isSubmitting} />
476
          </Form>
477
        )}
478
      </Formik>
479
    </Modal>
480
  );
481
};
482
483
export default PersonalExperienceModal;
484