Passed
Push — dev ( 0c0952...d8fc19 )
by Yonathan
06:55 queued 10s
created

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

Complexity

Total Complexity 4
Complexity/F 0

Size

Lines of Code 393
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 4
eloc 321
mnd 4
bc 4
fnc 0
dl 0
loc 393
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
  EducationFormValues,
7
  EducationSubform,
8
  validationShape as educationValidationShape,
9
} from "./EducationSubform";
10
import TextInput from "../../Form/TextInput";
11
import CheckboxInput from "../../Form/CheckboxInput";
12
import { validationMessages } from "../../Form/Messages";
13
import SkillSubform, {
14
  SkillFormValues,
15
  validationShape as skillValidationShape,
16
} from "./SkillSubform";
17
import { Skill, ExperiencePersonal } from "../../../models/types";
18
import {
19
  ExperienceModalHeader,
20
  ExperienceDetailsIntro,
21
  ExperienceModalFooter,
22
} from "./ExperienceModalCommon";
23
import Modal from "../../Modal";
24
import DateInput from "../../Form/DateInput";
25
import { toInputDateString, fromInputDateString } from "../../../helpers/dates";
26
import {
27
  Locales,
28
  localizeFieldNonNull,
29
  getLocale,
30
  matchValueToModel,
31
} from "../../../helpers/localize";
32
import { notEmpty } from "../../../helpers/queries";
33
import TextAreaInput from "../../Form/TextAreaInput";
34
import { countNumberOfWords } from "../../WordCounter/helpers";
35
36
interface PersonalExperienceModalProps {
37
  modalId: string;
38
  experiencePersonal: ExperiencePersonal | null;
39
  jobId: number;
40
  jobClassification: string;
41
  jobEducationRequirements: string | null;
42
  requiredSkills: Skill[];
43
  savedRequiredSkills: Skill[];
44
  optionalSkills: Skill[];
45
  savedOptionalSkills: Skill[];
46
  experienceableId: number;
47
  experienceableType: ExperiencePersonal["experienceable_type"];
48
  parentElement: Element | null;
49
  visible: boolean;
50
  onModalCancel: () => void;
51
  onModalConfirm: (data: PersonalExperienceSubmitData) => Promise<void>;
52
}
53
54
export const messages = defineMessages({
55
  modalTitle: {
56
    id: "application.personalExperienceModal.modalTitle",
57
    defaultMessage: "Add Personal Experience",
58
  },
59
  modalDescription: {
60
    id: "application.personalExperienceModal.modalDescription",
61
    defaultMessage:
62
      "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.)",
63
  },
64
  titleLabel: {
65
    id: "application.personalExperienceModal.titleLabel",
66
    defaultMessage: "Give this experience a title:",
67
  },
68
  titlePlaceholder: {
69
    id: "application.personalExperienceModal.titlePlaceholder",
70
    defaultMessage: "e.g. My Parenting Experience",
71
  },
72
  descriptionLabel: {
73
    id: "application.personalExperienceModal.descriptionLabel",
74
    defaultMessage: "Describe the project or activity:",
75
  },
76
  descriptionPlaceholder: {
77
    id: "application.personalExperienceModal.descriptionPlaceholder",
78
    defaultMessage: "e.g. I have extensive experience in...",
79
  },
80
  isShareableLabel: {
81
    id: "application.personalExperienceModal.isShareableLabel",
82
    defaultMessage: "Sharing Consent",
83
  },
84
  isShareableInlineLabel: {
85
    id: "application.personalExperienceModal.isShareableInlineLabel",
86
    defaultMessage:
87
      "This information is not sensitive in nature and I am comfortable sharing it with the staff managing this job application.",
88
  },
89
  startDateLabel: {
90
    id: "application.personalExperienceModal.startDateLabel",
91
    defaultMessage: "Select a Start Date",
92
  },
93
  datePlaceholder: {
94
    id: "application.personalExperienceModal.datePlaceholder",
95
    defaultMessage: "yyyy-mm-dd",
96
  },
97
  isActiveLabel: {
98
    id: "application.personalExperienceModal.isActiveLabel",
99
    defaultMessage: "This experience is still ongoing, or...",
100
    description: "Label for checkbox that indicates work is still ongoing.",
101
  },
102
  endDateLabel: {
103
    id: "application.personalExperienceModal.endDateLabel",
104
    defaultMessage: "Select an End Date",
105
  },
106
});
107
108
export interface PersonalDetailsFormValues {
109
  title: string;
110
  description: string;
111
  isShareable: boolean;
112
  startDate: string;
113
  isActive: boolean;
114
  endDate: string;
115
}
116
117
type PersonalExperienceFormValues = SkillFormValues &
118
  EducationFormValues &
119
  PersonalDetailsFormValues;
120
export interface PersonalExperienceSubmitData {
121
  experiencePersonal: ExperiencePersonal;
122
  savedRequiredSkills: Skill[];
123
  savedOptionalSkills: Skill[];
124
}
125
126
const DESCRIPTION_WORD_LIMIT = 100;
127
128
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
129
export const validationShape = (intl: IntlShape) => {
130
  const requiredMsg = intl.formatMessage(validationMessages.required);
131
  const conditionalRequiredMsg = intl.formatMessage(
132
    validationMessages.endDateRequiredIfNotOngoing,
133
  );
134
  const inPastMsg = intl.formatMessage(validationMessages.dateMustBePast);
135
  const afterStartDateMsg = intl.formatMessage(
136
    validationMessages.endDateAfterStart,
137
  );
138
  const tooLong = intl.formatMessage(validationMessages.tooLong);
139
  return {
140
    title: Yup.string().required(requiredMsg),
141
    description: Yup.string()
142
      .required(requiredMsg)
143
      .test(
144
        "under-word-limit",
145
        tooLong,
146
        (value: string) => countNumberOfWords(value) <= DESCRIPTION_WORD_LIMIT,
147
      ),
148
    isShareable: Yup.boolean(),
149
    startDate: Yup.date().required(requiredMsg).max(new Date(), inPastMsg),
150
    isActive: Yup.boolean(),
151
    endDate: Yup.date().when("isActive", {
152
      is: false,
153
      then: Yup.date()
154
        .required(conditionalRequiredMsg)
155
        .min(Yup.ref("startDate"), afterStartDateMsg),
156
      otherwise: Yup.date().min(Yup.ref("startDate"), afterStartDateMsg),
157
    }),
158
  };
159
};
160
161
const dataToFormValues = (
162
  data: PersonalExperienceSubmitData,
163
  locale: Locales,
164
): PersonalExperienceFormValues => {
165
  const { experiencePersonal, savedRequiredSkills, savedOptionalSkills } = data;
166
  const skillToName = (skill: Skill): string =>
167
    localizeFieldNonNull(locale, skill, "name");
168
  return {
169
    requiredSkills: savedRequiredSkills.map(skillToName),
170
    optionalSkills: savedOptionalSkills.map(skillToName),
171
    useAsEducationRequirement: experiencePersonal.is_education_requirement,
172
    title: experiencePersonal.title,
173
    description: experiencePersonal.description,
174
    isShareable: experiencePersonal.is_shareable,
175
    startDate: toInputDateString(experiencePersonal.start_date),
176
    isActive: experiencePersonal.is_active,
177
    endDate: experiencePersonal.end_date
178
      ? toInputDateString(experiencePersonal.end_date)
179
      : "",
180
  };
181
};
182
183
/* eslint-disable @typescript-eslint/camelcase */
184
const formValuesToData = (
185
  formValues: PersonalExperienceFormValues,
186
  originalExperience: ExperiencePersonal,
187
  locale: Locales,
188
  skills: Skill[],
189
): PersonalExperienceSubmitData => {
190
  const nameToSkill = (name: string): Skill | null =>
191
    matchValueToModel(locale, "name", name, skills);
192
  return {
193
    experiencePersonal: {
194
      ...originalExperience,
195
      title: formValues.title,
196
      description: formValues.description,
197
      is_shareable: formValues.isShareable,
198
      start_date: fromInputDateString(formValues.startDate),
199
      is_active: formValues.isActive,
200
      end_date: formValues.endDate
201
        ? fromInputDateString(formValues.endDate)
202
        : null,
203
      is_education_requirement: formValues.useAsEducationRequirement,
204
    },
205
    savedRequiredSkills: formValues.requiredSkills
206
      .map(nameToSkill)
207
      .filter(notEmpty),
208
    savedOptionalSkills: formValues.optionalSkills
209
      .map(nameToSkill)
210
      .filter(notEmpty),
211
  };
212
};
213
214
const newPersonalExperience = (
215
  experienceableId: number,
216
  experienceableType: ExperiencePersonal["experienceable_type"],
217
): ExperiencePersonal => ({
218
  id: 0,
219
  title: "",
220
  description: "",
221
  is_shareable: false,
222
  is_active: false,
223
  start_date: new Date(),
224
  end_date: null,
225
  is_education_requirement: false,
226
  experienceable_id: experienceableId,
227
  experienceable_type: experienceableType,
228
  type: "experience_personal",
229
});
230
/* eslint-enable @typescript-eslint/camelcase */
231
232
export const PersonalExperienceModal: React.FC<PersonalExperienceModalProps> = ({
233
  modalId,
234
  experiencePersonal,
235
  jobId,
236
  jobClassification,
237
  jobEducationRequirements,
238
  requiredSkills,
239
  savedRequiredSkills,
240
  optionalSkills,
241
  savedOptionalSkills,
242
  experienceableId,
243
  experienceableType,
244
  parentElement,
245
  visible,
246
  onModalCancel,
247
  onModalConfirm,
248
}) => {
249
  const intl = useIntl();
250
  const locale = getLocale(intl.locale);
251
252
  const originalExperience =
253
    experiencePersonal ??
254
    newPersonalExperience(experienceableId, experienceableType);
255
256
  const skillToName = (skill: Skill): string =>
257
    localizeFieldNonNull(locale, skill, "name");
258
259
  const initialFormValues = dataToFormValues(
260
    {
261
      experiencePersonal: originalExperience,
262
      savedRequiredSkills,
263
      savedOptionalSkills,
264
    },
265
    locale,
266
  );
267
268
  const validationSchema = Yup.object().shape({
269
    ...skillValidationShape,
270
    ...educationValidationShape,
271
    ...validationShape(intl),
272
  });
273
274
  const detailsSubform = (
275
    <div data-c-container="medium">
276
      <div data-c-grid="gutter(all, 1) middle">
277
        <FastField
278
          id="personal-title"
279
          name="title"
280
          type="text"
281
          grid="base(1of1)"
282
          component={TextInput}
283
          required
284
          label={intl.formatMessage(messages.titleLabel)}
285
          placeholder={intl.formatMessage(messages.titlePlaceholder)}
286
        />
287
        <FastField
288
          id="personal-description"
289
          type="text"
290
          name="description"
291
          component={TextAreaInput}
292
          required
293
          grid="tl(1of1)"
294
          label={intl.formatMessage(messages.descriptionLabel)}
295
          placeholder={intl.formatMessage(messages.descriptionPlaceholder)}
296
          wordLimit={DESCRIPTION_WORD_LIMIT}
297
        />
298
        <div data-c-input="checkbox(group)" data-c-grid-item="base(1of1)">
299
          <label>{intl.formatMessage(messages.isShareableLabel)}</label>
300
          <FastField
301
            id="personal-isShareable"
302
            name="isShareable"
303
            component={CheckboxInput}
304
            grid="base(1of1)"
305
            label={intl.formatMessage(messages.isShareableInlineLabel)}
306
            checkboxGroup
307
          />
308
        </div>
309
        <FastField
310
          id="personal-startDate"
311
          name="startDate"
312
          component={DateInput}
313
          required
314
          grid="base(1of1)"
315
          label={intl.formatMessage(messages.startDateLabel)}
316
          placeholder={intl.formatMessage(messages.datePlaceholder)}
317
        />
318
        <Field
319
          id="personal-isActive"
320
          name="isActive"
321
          component={CheckboxInput}
322
          grid="tl(1of2)"
323
          label={intl.formatMessage(messages.isActiveLabel)}
324
        />
325
        <Field
326
          id="personal-endDate"
327
          name="endDate"
328
          component={DateInput}
329
          grid="base(1of2)"
330
          label={intl.formatMessage(messages.endDateLabel)}
331
          placeholder={intl.formatMessage(messages.datePlaceholder)}
332
        />
333
      </div>
334
    </div>
335
  );
336
337
  return (
338
    <Modal
339
      id={modalId}
340
      parentElement={parentElement}
341
      visible={visible}
342
      onModalCancel={onModalCancel}
343
      onModalConfirm={onModalCancel}
344
      className="application-experience-dialog"
345
    >
346
      <ExperienceModalHeader
347
        title={intl.formatMessage(messages.modalTitle)}
348
        iconClass="fa-mountain"
349
      />
350
      <Formik
351
        enableReinitialize
352
        initialValues={initialFormValues}
353
        onSubmit={async (values, actions): Promise<void> => {
354
          await onModalConfirm(
355
            formValuesToData(values, originalExperience, locale, [
356
              ...requiredSkills,
357
              ...optionalSkills,
358
            ]),
359
          );
360
          actions.setSubmitting(false);
361
          actions.resetForm();
362
        }}
363
        validationSchema={validationSchema}
364
      >
365
        {(formikProps): React.ReactElement => (
366
          <Form>
367
            <Modal.Body>
368
              <ExperienceDetailsIntro
369
                description={intl.formatMessage(messages.modalDescription)}
370
              />
371
              {detailsSubform}
372
              <SkillSubform
373
                keyPrefix="personal"
374
                jobId={jobId}
375
                jobRequiredSkills={requiredSkills.map(skillToName)}
376
                jobOptionalSkills={optionalSkills.map(skillToName)}
377
              />
378
              <EducationSubform
379
                keyPrefix="personal"
380
                jobClassification={jobClassification}
381
                jobEducationRequirements={jobEducationRequirements}
382
              />
383
            </Modal.Body>
384
            <ExperienceModalFooter buttonsDisabled={formikProps.isSubmitting} />
385
          </Form>
386
        )}
387
      </Formik>
388
    </Modal>
389
  );
390
};
391
392
export default PersonalExperienceModal;
393