Passed
Push — feature/experience-form-modals ( 7402c2...37703b )
by Tristan
03:49
created

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

Complexity

Total Complexity 5
Complexity/F 0

Size

Lines of Code 372
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 5
eloc 299
mnd 5
bc 5
fnc 0
dl 0
loc 372
bpm 0
cpm 0
noi 0
c 0
b 0
f 0
rs 10
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 { ExperienceWork, Skill } 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
} from "../../../helpers/localize";
32
import ExperienceWorkAccordion from "../Experience/ExperienceWorkAccordion";
33
import { notEmpty } from "../../../helpers/queries";
34
35
interface WorkExperienceModalProps {
36
  modalId: string;
37
  experienceWork: ExperienceWork | null;
38
  jobId: number;
39
  requiredSkills: Skill[];
40
  savedRequiredSkills: Skill[];
41
  optionalSkills: Skill[];
42
  savedOptionalSkills: Skill[];
43
  experienceRequirments: EducationSubformProps;
44
  useAsEducationRequirement: boolean;
45
  parentElement: Element | null;
46
  visible: boolean;
47
  onModalCancel: () => void;
48
  onModalConfirm: (data: WorkExperienceSubmitData) => Promise<void>;
49
}
50
51
const messages = defineMessages({
52
  modalTitle: {
53
    id: "workExperienceModal.modalTitle",
54
    defaultMessage: "Add Work Experience",
55
  },
56
  modalDescription: {
57
    id: "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: "workExperienceModal.jobTitleLabel",
63
    defaultMessage: "My Role/Job Title",
64
  },
65
  jobTitlePlaceholder: {
66
    id: "workExperienceModal.jobTitlePlaceholder",
67
    defaultMessage: "e.g. Front-end Development",
68
  },
69
  orgNameLabel: {
70
    id: "workExperienceModal.orgNameLabel",
71
    defaultMessage: "Organization/Company",
72
  },
73
  orgNamePlaceholder: {
74
    id: "workExperienceModal.orgNamePlaceholder",
75
    defaultMessage: "e.g. Government of Canada",
76
  },
77
  groupLabel: {
78
    id: "workExperienceModal.groupLabel",
79
    defaultMessage: "Team, Group, or Division",
80
  },
81
  groupPlaceholder: {
82
    id: "workExperienceModal.groupPlaceholder",
83
    defaultMessage: "e.g. Talent Cloud",
84
  },
85
  startDateLabel: {
86
    id: "workExperienceModal.startDateLabel",
87
    defaultMessage: "Select a Start Date",
88
  },
89
  datePlaceholder: {
90
    id: "workExperienceModal.datePlaceholder",
91
    defaultMessage: "yyyy-mm-dd",
92
  },
93
  isActiveLabel: {
94
    id: "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: "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
interface WorkExperienceSubmitData {
117
  experienceWork: ExperienceWork;
118
  savedRequiredSkills: Skill[];
119
  savedOptionalSkills: Skill[];
120
  useAsEducationRequirement: boolean;
121
}
122
123
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
124
export const validationShape = (intl: IntlShape) => {
125
  const requiredMsg = intl.formatMessage(validationMessages.required);
126
  const conditionalRequiredMsg = intl.formatMessage(
127
    validationMessages.endDateRequiredIfNotOngoing,
128
  );
129
  const inPastMsg = intl.formatMessage(validationMessages.dateMustBePast);
130
  const afterStartDateMsg = intl.formatMessage(
131
    validationMessages.endDateAfterStart,
132
  );
133
  return {
134
    title: Yup.string().required(requiredMsg),
135
    organization: Yup.string().required(requiredMsg),
136
    group: Yup.string().required(requiredMsg),
137
    startDate: Yup.date().required(requiredMsg).max(new Date(), inPastMsg),
138
    isActive: Yup.boolean(),
139
    endDate: Yup.date().when("isActive", {
140
      is: false,
141
      then: Yup.date()
142
        .required(conditionalRequiredMsg)
143
        .min(Yup.ref("startDate"), afterStartDateMsg),
144
      otherwise: Yup.date().min(Yup.ref("startDate"), afterStartDateMsg),
145
    }),
146
  };
147
};
148
149
const dataToFormValues = (
150
  data: WorkExperienceSubmitData,
151
  locale: Locales,
152
): WorkExperienceFormValues => {
153
  const {
154
    experienceWork,
155
    savedRequiredSkills,
156
    savedOptionalSkills,
157
    useAsEducationRequirement,
158
  } = data;
159
  const skillToName = (skill: Skill): string =>
160
    localizeFieldNonNull(locale, skill, "name");
161
  return {
162
    requiredSkills: savedRequiredSkills.map(skillToName),
163
    optionalSkills: savedOptionalSkills.map(skillToName),
164
    useAsEducationRequirement,
165
    title: experienceWork.title,
166
    organization: experienceWork.organization,
167
    group: experienceWork.group,
168
    startDate: toInputDateString(experienceWork.start_date),
169
    isActive: experienceWork.is_active,
170
    endDate: experienceWork.end_date
171
      ? toInputDateString(experienceWork.end_date)
172
      : "",
173
  };
174
};
175
176
/* eslint-disable @typescript-eslint/camelcase */
177
const formValuesToData = (
178
  formValues: WorkExperienceFormValues,
179
  originalExperience: ExperienceWork,
180
  locale: Locales,
181
  skills: Skill[],
182
): WorkExperienceSubmitData => {
183
  const nameToSkill = (name: string): Skill | null => {
184
    const matchingSkills = skills.filter(
185
      (skill) => localizeFieldNonNull(locale, skill, "name") === name,
186
    );
187
    return matchingSkills.length > 0 ? matchingSkills[0] : null;
188
  };
189
  return {
190
    experienceWork: {
191
      ...originalExperience,
192
      title: formValues.title,
193
      organization: formValues.organization,
194
      group: formValues.group,
195
      start_date: fromInputDateString(formValues.startDate),
196
      is_active: formValues.isActive,
197
      end_date: formValues.endDate
198
        ? fromInputDateString(formValues.endDate)
199
        : null,
200
    },
201
    useAsEducationRequirement: formValues.useAsEducationRequirement,
202
    savedRequiredSkills: formValues.requiredSkills
203
      .map(nameToSkill)
204
      .filter(notEmpty),
205
    savedOptionalSkills: formValues.optionalSkills
206
      .map(nameToSkill)
207
      .filter(notEmpty),
208
  };
209
};
210
211
const newExperienceWork = (): ExperienceWork => ({
212
  id: 0,
213
  title: "",
214
  organization: "",
215
  group: "",
216
  is_active: false,
217
  start_date: new Date(),
218
  end_date: null,
219
});
220
/* eslint-enable @typescript-eslint/camelcase */
221
222
export const WorkExperienceModal: React.FC<WorkExperienceModalProps> = ({
223
  modalId,
224
  experienceWork,
225
  jobId,
226
  requiredSkills,
227
  savedRequiredSkills,
228
  optionalSkills,
229
  savedOptionalSkills,
230
  experienceRequirments,
231
  useAsEducationRequirement,
232
  parentElement,
233
  visible,
234
  onModalCancel,
235
  onModalConfirm,
236
}) => {
237
  const intl = useIntl();
238
  const locale = getLocale(intl.locale);
239
240
  const originalExperience = experienceWork ?? newExperienceWork();
241
242
  const skillToName = (skill: Skill): string =>
243
    localizeFieldNonNull(locale, skill, "name");
244
245
  const initialFormValues = dataToFormValues(
246
    {
247
      experienceWork: originalExperience,
248
      savedRequiredSkills,
249
      savedOptionalSkills,
250
      useAsEducationRequirement,
251
    },
252
    locale,
253
  );
254
255
  const validationSchema = Yup.object().shape({
256
    ...skillValidationShape,
257
    ...educationValidationShape,
258
    ...validationShape(intl),
259
  });
260
261
  const detailsSubform = (
262
    <div data-c-container="medium">
263
      <div data-c-grid="gutter(all, 1) middle">
264
        <FastField
265
          id="title"
266
          type="text"
267
          name="title"
268
          component={TextInput}
269
          required
270
          grid="base(1of1)"
271
          label={intl.formatMessage(messages.jobTitleLabel)}
272
          placeholder={intl.formatMessage(messages.jobTitlePlaceholder)}
273
        />
274
        <FastField
275
          id="organization"
276
          type="text"
277
          name="organization"
278
          component={TextInput}
279
          required
280
          grid="base(1of2)"
281
          label={intl.formatMessage(messages.orgNameLabel)}
282
          placeholder={intl.formatMessage(messages.orgNamePlaceholder)}
283
        />
284
        <FastField
285
          id="group"
286
          type="text"
287
          name="group"
288
          component={TextInput}
289
          required
290
          grid="base(1of2)"
291
          label={intl.formatMessage(messages.groupLabel)}
292
          placeholder={intl.formatMessage(messages.groupPlaceholder)}
293
        />
294
        <FastField
295
          id="startDate"
296
          name="startDate"
297
          component={DateInput}
298
          required
299
          grid="base(1of1)"
300
          label={intl.formatMessage(messages.startDateLabel)}
301
          placeholder={intl.formatMessage(messages.datePlaceholder)}
302
        />
303
        <Field
304
          id="isActive"
305
          name="isActive"
306
          component={CheckboxInput}
307
          grid="tl(1of2)"
308
          label={intl.formatMessage(messages.isActiveLabel)}
309
        />
310
        <FastField
311
          id="endDate"
312
          name="endDate"
313
          component={DateInput}
314
          grid="base(1of2)"
315
          label={intl.formatMessage(messages.endDateLabel)}
316
          placeholder={intl.formatMessage(messages.datePlaceholder)}
317
        />
318
      </div>
319
    </div>
320
  );
321
322
  return (
323
    <Modal
324
      id={modalId}
325
      parentElement={parentElement}
326
      visible={visible}
327
      onModalCancel={onModalCancel}
328
      onModalConfirm={onModalCancel}
329
      className="application-experience-dialog"
330
    >
331
      <ExperienceModalHeader
332
        title={intl.formatMessage(messages.modalTitle)}
333
        iconClass="fa-briefcase"
334
      />
335
      <Formik
336
        enableReinitialize
337
        initialValues={initialFormValues}
338
        onSubmit={async (values, actions): Promise<void> => {
339
          await onModalConfirm(
340
            formValuesToData(values, originalExperience, locale, [
341
              ...requiredSkills,
342
              ...optionalSkills,
343
            ]),
344
          );
345
          actions.setSubmitting(false);
346
        }}
347
        validationSchema={validationSchema}
348
      >
349
        {(formikProps): React.ReactElement => (
350
          <Form>
351
            <Modal.Body>
352
              <ExperienceDetailsIntro
353
                description={intl.formatMessage(messages.modalDescription)}
354
              />
355
              {detailsSubform}
356
              <SkillSubform
357
                jobId={jobId}
358
                jobRequiredSkills={requiredSkills.map(skillToName)}
359
                jobOptionalSkills={optionalSkills.map(skillToName)}
360
              />
361
              <EducationSubform {...experienceRequirments} />
362
            </Modal.Body>
363
            <ExperienceModalFooter buttonsDisabled={formikProps.isSubmitting} />
364
          </Form>
365
        )}
366
      </Formik>
367
    </Modal>
368
  );
369
};
370
371
export default WorkExperienceModal;
372