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

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

Complexity

Total Complexity 4
Complexity/F 0

Size

Lines of Code 384
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 4
eloc 314
mnd 4
bc 4
fnc 0
dl 0
loc 384
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
});
229
/* eslint-enable @typescript-eslint/camelcase */
230
231
export const PersonalExperienceModal: React.FC<PersonalExperienceModalProps> = ({
232
  modalId,
233
  experiencePersonal,
234
  jobId,
235
  requiredSkills,
236
  savedRequiredSkills,
237
  optionalSkills,
238
  savedOptionalSkills,
239
  experienceRequirments,
240
  experienceableId,
241
  experienceableType,
242
  parentElement,
243
  visible,
244
  onModalCancel,
245
  onModalConfirm,
246
}) => {
247
  const intl = useIntl();
248
  const locale = getLocale(intl.locale);
249
250
  const originalExperience =
251
    experiencePersonal ??
252
    newPersonalExperience(experienceableId, experienceableType);
253
254
  const skillToName = (skill: Skill): string =>
255
    localizeFieldNonNull(locale, skill, "name");
256
257
  const initialFormValues = dataToFormValues(
258
    {
259
      experiencePersonal: originalExperience,
260
      savedRequiredSkills,
261
      savedOptionalSkills,
262
    },
263
    locale,
264
  );
265
266
  const validationSchema = Yup.object().shape({
267
    ...skillValidationShape,
268
    ...educationValidationShape,
269
    ...validationShape(intl),
270
  });
271
272
  const detailsSubform = (
273
    <div data-c-container="medium">
274
      <div data-c-grid="gutter(all, 1) middle">
275
        <FastField
276
          id="title"
277
          name="title"
278
          type="text"
279
          grid="base(1of1)"
280
          component={TextInput}
281
          required
282
          label={intl.formatMessage(messages.titleLabel)}
283
          placeholder={intl.formatMessage(messages.titlePlaceholder)}
284
        />
285
        <FastField
286
          id="description"
287
          type="text"
288
          name="description"
289
          component={TextAreaInput}
290
          required
291
          grid="tl(1of1)"
292
          label={intl.formatMessage(messages.descriptionLabel)}
293
          placeholder={intl.formatMessage(messages.descriptionPlaceholder)}
294
          wordLimit={DESCRIPTION_WORD_LIMIT}
295
        />
296
        <div data-c-input="checkbox(group)" data-c-grid-item="base(1of1)">
297
          <label>{intl.formatMessage(messages.isShareableLabel)}</label>
298
          <FastField
299
            id="isShareable"
300
            name="isShareable"
301
            component={CheckboxInput}
302
            grid="base(1of1)"
303
            label={intl.formatMessage(messages.isShareableInlineLabel)}
304
          />
305
        </div>
306
        <FastField
307
          id="startDate"
308
          name="startDate"
309
          component={DateInput}
310
          required
311
          grid="base(1of1)"
312
          label={intl.formatMessage(messages.startDateLabel)}
313
          placeholder={intl.formatMessage(messages.datePlaceholder)}
314
        />
315
        <Field
316
          id="isActive"
317
          name="isActive"
318
          component={CheckboxInput}
319
          grid="tl(1of2)"
320
          label={intl.formatMessage(messages.isActiveLabel)}
321
        />
322
        <Field
323
          id="endDate"
324
          name="endDate"
325
          component={DateInput}
326
          grid="base(1of2)"
327
          label={intl.formatMessage(messages.endDateLabel)}
328
          placeholder={intl.formatMessage(messages.datePlaceholder)}
329
        />
330
      </div>
331
    </div>
332
  );
333
334
  return (
335
    <Modal
336
      id={modalId}
337
      parentElement={parentElement}
338
      visible={visible}
339
      onModalCancel={onModalCancel}
340
      onModalConfirm={onModalCancel}
341
      className="application-experience-dialog"
342
    >
343
      <ExperienceModalHeader
344
        title={intl.formatMessage(messages.modalTitle)}
345
        iconClass="fa-mountain"
346
      />
347
      <Formik
348
        enableReinitialize
349
        initialValues={initialFormValues}
350
        onSubmit={async (values, actions): Promise<void> => {
351
          await onModalConfirm(
352
            formValuesToData(values, originalExperience, locale, [
353
              ...requiredSkills,
354
              ...optionalSkills,
355
            ]),
356
          );
357
          actions.setSubmitting(false);
358
        }}
359
        validationSchema={validationSchema}
360
      >
361
        {(formikProps): React.ReactElement => (
362
          <Form>
363
            <Modal.Body>
364
              <ExperienceDetailsIntro
365
                description={intl.formatMessage(messages.modalDescription)}
366
              />
367
              {detailsSubform}
368
              <SkillSubform
369
                jobId={jobId}
370
                jobRequiredSkills={requiredSkills.map(skillToName)}
371
                jobOptionalSkills={optionalSkills.map(skillToName)}
372
              />
373
              <EducationSubform {...experienceRequirments} />
374
            </Modal.Body>
375
            <ExperienceModalFooter buttonsDisabled={formikProps.isSubmitting} />
376
          </Form>
377
        )}
378
      </Formik>
379
    </Modal>
380
  );
381
};
382
383
export default PersonalExperienceModal;
384