Passed
Push — feature/experience-form-modals ( ac040e...10a0f1 )
by Tristan
03:59
created

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

Complexity

Total Complexity 4
Complexity/F 0

Size

Lines of Code 370
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

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