Passed
Push — feature/experience-step-functi... ( 5abe99...215b5c )
by Tristan
08:04 queued 03:37
created

resources/assets/js/components/Application/ExperienceModals/WorkExperienceModal.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 302
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
  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 { ExperienceWork, Skill } 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
34
interface WorkExperienceModalProps {
35
  modalId: string;
36
  experienceWork: ExperienceWork | null;
37
  jobId: number;
38
  jobClassification: string;
39
  requiredSkills: Skill[];
40
  savedRequiredSkills: Skill[];
41
  optionalSkills: Skill[];
42
  savedOptionalSkills: Skill[];
43
  experienceableId: number;
44
  experienceableType: ExperienceWork["experienceable_type"];
45
  parentElement: Element | null;
46
  visible: boolean;
47
  onModalCancel: () => void;
48
  onModalConfirm: (data: WorkExperienceSubmitData) => Promise<void>;
49
}
50
51
export const messages = defineMessages({
52
  modalTitle: {
53
    id: "application.workExperienceModal.modalTitle",
54
    defaultMessage: "Add Work Experience",
55
  },
56
  modalDescription: {
57
    id: "application.workExperienceModal.modalDescription",
58
    defaultMessage:
59
      'Did work? Share your experiences gained from full-time positions, part-time positions, self-employment, fellowships or internships.  (Did some volunteering? Share this as a "Community Experience".)',
60
  },
61
  jobTitleLabel: {
62
    id: "application.workExperienceModal.jobTitleLabel",
63
    defaultMessage: "My Role/Job Title",
64
  },
65
  jobTitlePlaceholder: {
66
    id: "application.workExperienceModal.jobTitlePlaceholder",
67
    defaultMessage: "e.g. Front-end Development",
68
  },
69
  orgNameLabel: {
70
    id: "application.workExperienceModal.orgNameLabel",
71
    defaultMessage: "Organization/Company",
72
  },
73
  orgNamePlaceholder: {
74
    id: "application.workExperienceModal.orgNamePlaceholder",
75
    defaultMessage: "e.g. Government of Canada",
76
  },
77
  groupLabel: {
78
    id: "application.workExperienceModal.groupLabel",
79
    defaultMessage: "Team, Group, or Division",
80
  },
81
  groupPlaceholder: {
82
    id: "application.workExperienceModal.groupPlaceholder",
83
    defaultMessage: "e.g. Talent Cloud",
84
  },
85
  startDateLabel: {
86
    id: "application.workExperienceModal.startDateLabel",
87
    defaultMessage: "Select a Start Date",
88
  },
89
  datePlaceholder: {
90
    id: "application.workExperienceModal.datePlaceholder",
91
    defaultMessage: "yyyy-mm-dd",
92
  },
93
  isActiveLabel: {
94
    id: "application.workExperienceModal.isActiveLabel",
95
    defaultMessage: "This experience is still ongoing, or...",
96
    description: "Label for checkbox that indicates work is still ongoing.",
97
  },
98
  endDateLabel: {
99
    id: "application.workExperienceModal.endDateLabel",
100
    defaultMessage: "Select an End Date",
101
  },
102
});
103
104
export interface WorkDetailsFormValues {
105
  title: string;
106
  organization: string;
107
  group: string;
108
  startDate: string;
109
  isActive: boolean;
110
  endDate: string;
111
}
112
113
type WorkExperienceFormValues = SkillFormValues &
114
  EducationFormValues &
115
  WorkDetailsFormValues;
116
export interface WorkExperienceSubmitData {
117
  experienceWork: ExperienceWork;
118
  savedRequiredSkills: Skill[];
119
  savedOptionalSkills: Skill[];
120
}
121
122
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
123
export const validationShape = (intl: IntlShape) => {
124
  const requiredMsg = intl.formatMessage(validationMessages.required);
125
  const conditionalRequiredMsg = intl.formatMessage(
126
    validationMessages.endDateRequiredIfNotOngoing,
127
  );
128
  const inPastMsg = intl.formatMessage(validationMessages.dateMustBePast);
129
  const afterStartDateMsg = intl.formatMessage(
130
    validationMessages.endDateAfterStart,
131
  );
132
  return {
133
    title: Yup.string().required(requiredMsg),
134
    organization: Yup.string().required(requiredMsg),
135
    group: Yup.string().required(requiredMsg),
136
    startDate: Yup.date().required(requiredMsg).max(new Date(), inPastMsg),
137
    isActive: Yup.boolean(),
138
    endDate: Yup.date().when("isActive", {
139
      is: false,
140
      then: Yup.date()
141
        .required(conditionalRequiredMsg)
142
        .min(Yup.ref("startDate"), afterStartDateMsg),
143
      otherwise: Yup.date().min(Yup.ref("startDate"), afterStartDateMsg),
144
    }),
145
  };
146
};
147
148
const dataToFormValues = (
149
  data: WorkExperienceSubmitData,
150
  locale: Locales,
151
): WorkExperienceFormValues => {
152
  const { experienceWork, savedRequiredSkills, savedOptionalSkills } = data;
153
  const skillToName = (skill: Skill): string =>
154
    localizeFieldNonNull(locale, skill, "name");
155
  return {
156
    requiredSkills: savedRequiredSkills.map(skillToName),
157
    optionalSkills: savedOptionalSkills.map(skillToName),
158
    useAsEducationRequirement: experienceWork.is_education_requirement,
159
    title: experienceWork.title,
160
    organization: experienceWork.organization,
161
    group: experienceWork.group,
162
    startDate: toInputDateString(experienceWork.start_date),
163
    isActive: experienceWork.is_active,
164
    endDate: experienceWork.end_date
165
      ? toInputDateString(experienceWork.end_date)
166
      : "",
167
  };
168
};
169
170
/* eslint-disable @typescript-eslint/camelcase */
171
const formValuesToData = (
172
  formValues: WorkExperienceFormValues,
173
  originalExperience: ExperienceWork,
174
  locale: Locales,
175
  skills: Skill[],
176
): WorkExperienceSubmitData => {
177
  const nameToSkill = (name: string): Skill | null =>
178
    matchValueToModel(locale, "name", name, skills);
179
  return {
180
    experienceWork: {
181
      ...originalExperience,
182
      title: formValues.title,
183
      organization: formValues.organization,
184
      group: formValues.group,
185
      start_date: fromInputDateString(formValues.startDate),
186
      is_active: formValues.isActive,
187
      end_date: formValues.endDate
188
        ? fromInputDateString(formValues.endDate)
189
        : null,
190
      is_education_requirement: formValues.useAsEducationRequirement,
191
    },
192
    savedRequiredSkills: formValues.requiredSkills
193
      .map(nameToSkill)
194
      .filter(notEmpty),
195
    savedOptionalSkills: formValues.optionalSkills
196
      .map(nameToSkill)
197
      .filter(notEmpty),
198
  };
199
};
200
201
const newExperienceWork = (
202
  experienceableId: number,
203
  experienceableType: ExperienceWork["experienceable_type"],
204
): ExperienceWork => ({
205
  id: 0,
206
  title: "",
207
  organization: "",
208
  group: "",
209
  is_active: false,
210
  start_date: new Date(),
211
  end_date: null,
212
  is_education_requirement: false,
213
  experienceable_id: experienceableId,
214
  experienceable_type: experienceableType,
215
  type: "experience_work",
216
});
217
/* eslint-enable @typescript-eslint/camelcase */
218
219
export const WorkExperienceModal: React.FC<WorkExperienceModalProps> = ({
220
  modalId,
221
  experienceWork,
222
  jobId,
223
  jobClassification,
224
  requiredSkills,
225
  savedRequiredSkills,
226
  optionalSkills,
227
  savedOptionalSkills,
228
  experienceableId,
229
  experienceableType,
230
  parentElement,
231
  visible,
232
  onModalCancel,
233
  onModalConfirm,
234
}) => {
235
  const intl = useIntl();
236
  const locale = getLocale(intl.locale);
237
238
  const originalExperience =
239
    experienceWork ?? newExperienceWork(experienceableId, experienceableType);
240
241
  const skillToName = (skill: Skill): string =>
242
    localizeFieldNonNull(locale, skill, "name");
243
244
  const initialFormValues = dataToFormValues(
245
    {
246
      experienceWork: originalExperience,
247
      savedRequiredSkills,
248
      savedOptionalSkills,
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
          type="text"
265
          name="title"
266
          component={TextInput}
267
          required
268
          grid="base(1of1)"
269
          label={intl.formatMessage(messages.jobTitleLabel)}
270
          placeholder={intl.formatMessage(messages.jobTitlePlaceholder)}
271
        />
272
        <FastField
273
          id="organization"
274
          type="text"
275
          name="organization"
276
          component={TextInput}
277
          required
278
          grid="base(1of2)"
279
          label={intl.formatMessage(messages.orgNameLabel)}
280
          placeholder={intl.formatMessage(messages.orgNamePlaceholder)}
281
        />
282
        <FastField
283
          id="group"
284
          type="text"
285
          name="group"
286
          component={TextInput}
287
          required
288
          grid="base(1of2)"
289
          label={intl.formatMessage(messages.groupLabel)}
290
          placeholder={intl.formatMessage(messages.groupPlaceholder)}
291
        />
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-briefcase"
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 jobClassification={jobClassification} />
360
            </Modal.Body>
361
            <ExperienceModalFooter buttonsDisabled={formikProps.isSubmitting} />
362
          </Form>
363
        )}
364
      </Formik>
365
    </Modal>
366
  );
367
};
368
369
export default WorkExperienceModal;
370