Passed
Push — feature/account-deletion ( 08a9be...9b6c18 )
by Grant
06:20
created

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

Complexity

Total Complexity 4
Complexity/F 0

Size

Lines of Code 385
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 4
eloc 315
mnd 4
bc 4
fnc 0
dl 0
loc 385
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, 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
interface PersonalExperienceModalProps {
38
  modalId: string;
39
  experiencePersonal: ExperiencePersonal | null;
40
  jobId: number;
41
  requiredSkills: Skill[];
42
  savedRequiredSkills: Skill[];
43
  optionalSkills: Skill[];
44
  savedOptionalSkills: Skill[];
45
  experienceRequirments: EducationSubformProps;
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
const messages = defineMessages({
55
  modalTitle: {
56
    id: "personalExperienceModal.modalTitle",
57
    defaultMessage: "Add Personal Experience",
58
  },
59
  modalDescription: {
60
    id: "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: "personalExperienceModal.titleLabel",
66
    defaultMessage: "Give this experience a title:",
67
  },
68
  titlePlaceholder: {
69
    id: "personalExperienceModal.titlePlaceholder",
70
    defaultMessage: "e.g. My Parenting Experience",
71
  },
72
  descriptionLabel: {
73
    id: "personalExperienceModal.descriptionLabel",
74
    defaultMessage: "Describe the project or activity:",
75
  },
76
  descriptionPlaceholder: {
77
    id: "personalExperienceModal.descriptionPlaceholder",
78
    defaultMessage: "e.g. I have extensive experience in...",
79
  },
80
  isShareableLabel: {
81
    id: "personalExperienceModal.isShareableLabel",
82
    defaultMessage: "Sharing Consent",
83
  },
84
  isShareableInlineLabel: {
85
    id: "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: "personalExperienceModal.startDateLabel",
91
    defaultMessage: "Select a Start Date",
92
  },
93
  datePlaceholder: {
94
    id: "personalExperienceModal.datePlaceholder",
95
    defaultMessage: "yyyy-mm-dd",
96
  },
97
  isActiveLabel: {
98
    id: "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: "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
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
  requiredSkills,
237
  savedRequiredSkills,
238
  optionalSkills,
239
  savedOptionalSkills,
240
  experienceRequirments,
241
  experienceableId,
242
  experienceableType,
243
  parentElement,
244
  visible,
245
  onModalCancel,
246
  onModalConfirm,
247
}) => {
248
  const intl = useIntl();
249
  const locale = getLocale(intl.locale);
250
251
  const originalExperience =
252
    experiencePersonal ??
253
    newPersonalExperience(experienceableId, experienceableType);
254
255
  const skillToName = (skill: Skill): string =>
256
    localizeFieldNonNull(locale, skill, "name");
257
258
  const initialFormValues = dataToFormValues(
259
    {
260
      experiencePersonal: originalExperience,
261
      savedRequiredSkills,
262
      savedOptionalSkills,
263
    },
264
    locale,
265
  );
266
267
  const validationSchema = Yup.object().shape({
268
    ...skillValidationShape,
269
    ...educationValidationShape,
270
    ...validationShape(intl),
271
  });
272
273
  const detailsSubform = (
274
    <div data-c-container="medium">
275
      <div data-c-grid="gutter(all, 1) middle">
276
        <FastField
277
          id="title"
278
          name="title"
279
          type="text"
280
          grid="base(1of1)"
281
          component={TextInput}
282
          required
283
          label={intl.formatMessage(messages.titleLabel)}
284
          placeholder={intl.formatMessage(messages.titlePlaceholder)}
285
        />
286
        <FastField
287
          id="description"
288
          type="text"
289
          name="description"
290
          component={TextAreaInput}
291
          required
292
          grid="tl(1of1)"
293
          label={intl.formatMessage(messages.descriptionLabel)}
294
          placeholder={intl.formatMessage(messages.descriptionPlaceholder)}
295
          wordLimit={DESCRIPTION_WORD_LIMIT}
296
        />
297
        <div data-c-input="checkbox(group)" data-c-grid-item="base(1of1)">
298
          <label>{intl.formatMessage(messages.isShareableLabel)}</label>
299
          <FastField
300
            id="isShareable"
301
            name="isShareable"
302
            component={CheckboxInput}
303
            grid="base(1of1)"
304
            label={intl.formatMessage(messages.isShareableInlineLabel)}
305
          />
306
        </div>
307
        <FastField
308
          id="startDate"
309
          name="startDate"
310
          component={DateInput}
311
          required
312
          grid="base(1of1)"
313
          label={intl.formatMessage(messages.startDateLabel)}
314
          placeholder={intl.formatMessage(messages.datePlaceholder)}
315
        />
316
        <Field
317
          id="isActive"
318
          name="isActive"
319
          component={CheckboxInput}
320
          grid="tl(1of2)"
321
          label={intl.formatMessage(messages.isActiveLabel)}
322
        />
323
        <Field
324
          id="endDate"
325
          name="endDate"
326
          component={DateInput}
327
          grid="base(1of2)"
328
          label={intl.formatMessage(messages.endDateLabel)}
329
          placeholder={intl.formatMessage(messages.datePlaceholder)}
330
        />
331
      </div>
332
    </div>
333
  );
334
335
  return (
336
    <Modal
337
      id={modalId}
338
      parentElement={parentElement}
339
      visible={visible}
340
      onModalCancel={onModalCancel}
341
      onModalConfirm={onModalCancel}
342
      className="application-experience-dialog"
343
    >
344
      <ExperienceModalHeader
345
        title={intl.formatMessage(messages.modalTitle)}
346
        iconClass="fa-mountain"
347
      />
348
      <Formik
349
        enableReinitialize
350
        initialValues={initialFormValues}
351
        onSubmit={async (values, actions): Promise<void> => {
352
          await onModalConfirm(
353
            formValuesToData(values, originalExperience, locale, [
354
              ...requiredSkills,
355
              ...optionalSkills,
356
            ]),
357
          );
358
          actions.setSubmitting(false);
359
        }}
360
        validationSchema={validationSchema}
361
      >
362
        {(formikProps): React.ReactElement => (
363
          <Form>
364
            <Modal.Body>
365
              <ExperienceDetailsIntro
366
                description={intl.formatMessage(messages.modalDescription)}
367
              />
368
              {detailsSubform}
369
              <SkillSubform
370
                jobId={jobId}
371
                jobRequiredSkills={requiredSkills.map(skillToName)}
372
                jobOptionalSkills={optionalSkills.map(skillToName)}
373
              />
374
              <EducationSubform {...experienceRequirments} />
375
            </Modal.Body>
376
            <ExperienceModalFooter buttonsDisabled={formikProps.isSubmitting} />
377
          </Form>
378
        )}
379
      </Formik>
380
    </Modal>
381
  );
382
};
383
384
export default PersonalExperienceModal;
385