Passed
Push — dev ( e21187...4e4b2b )
by
unknown
04:43
created

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

Complexity

Total Complexity 6
Complexity/F 0

Size

Lines of Code 468
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 6
eloc 376
mnd 6
bc 6
fnc 0
dl 0
loc 468
rs 10
bpm 0
cpm 0
noi 0
c 0
b 0
f 0
1
import React, { FunctionComponent } 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
export const messages = defineMessages({
35
  modalTitle: {
36
    id: "application.workExperienceModal.modalTitle",
37
    defaultMessage: "Add Work Experience",
38
  },
39
  modalDescription: {
40
    id: "application.workExperienceModal.modalDescription",
41
    defaultMessage:
42
      '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".)',
43
  },
44
  jobTitleLabel: {
45
    id: "application.workExperienceModal.jobTitleLabel",
46
    defaultMessage: "My Role/Job Title",
47
  },
48
  jobTitlePlaceholder: {
49
    id: "application.workExperienceModal.jobTitlePlaceholder",
50
    defaultMessage: "e.g. Front-end Development",
51
  },
52
  orgNameLabel: {
53
    id: "application.workExperienceModal.orgNameLabel",
54
    defaultMessage: "Organization/Company",
55
  },
56
  orgNamePlaceholder: {
57
    id: "application.workExperienceModal.orgNamePlaceholder",
58
    defaultMessage: "e.g. Government of Canada",
59
  },
60
  groupLabel: {
61
    id: "application.workExperienceModal.groupLabel",
62
    defaultMessage: "Team, Group, or Division",
63
  },
64
  groupPlaceholder: {
65
    id: "application.workExperienceModal.groupPlaceholder",
66
    defaultMessage: "e.g. Talent Cloud",
67
  },
68
  startDateLabel: {
69
    id: "application.workExperienceModal.startDateLabel",
70
    defaultMessage: "Select a Start Date",
71
  },
72
  datePlaceholder: {
73
    id: "application.workExperienceModal.datePlaceholder",
74
    defaultMessage: "yyyy-mm-dd",
75
  },
76
  isActiveLabel: {
77
    id: "application.workExperienceModal.isActiveLabel",
78
    defaultMessage: "This experience is still ongoing, or...",
79
    description: "Label for checkbox that indicates work is still ongoing.",
80
  },
81
  endDateLabel: {
82
    id: "application.workExperienceModal.endDateLabel",
83
    defaultMessage: "Select an End Date",
84
  },
85
});
86
87
export interface WorkDetailsFormValues {
88
  title: string;
89
  organization: string;
90
  group: string;
91
  startDate: string;
92
  isActive: boolean;
93
  endDate: string;
94
}
95
96
type WorkExperienceFormValues = SkillFormValues &
97
  EducationFormValues &
98
  WorkDetailsFormValues;
99
export interface WorkExperienceSubmitData {
100
  experienceWork: ExperienceWork;
101
  savedRequiredSkills: Skill[];
102
  savedOptionalSkills: Skill[];
103
}
104
105
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
106
export const validationShape = (intl: IntlShape) => {
107
  const requiredMsg = intl.formatMessage(validationMessages.required);
108
  const conditionalRequiredMsg = intl.formatMessage(
109
    validationMessages.endDateRequiredIfNotOngoing,
110
  );
111
  const inPastMsg = intl.formatMessage(validationMessages.dateMustBePast);
112
  const afterStartDateMsg = intl.formatMessage(
113
    validationMessages.endDateAfterStart,
114
  );
115
  return {
116
    title: Yup.string().required(requiredMsg),
117
    organization: Yup.string().required(requiredMsg),
118
    group: Yup.string().required(requiredMsg),
119
    startDate: Yup.date().required(requiredMsg).max(new Date(), inPastMsg),
120
    isActive: Yup.boolean(),
121
    endDate: Yup.date().when("isActive", {
122
      is: false,
123
      then: Yup.date()
124
        .required(conditionalRequiredMsg)
125
        .min(Yup.ref("startDate"), afterStartDateMsg),
126
      otherwise: Yup.date().min(Yup.ref("startDate"), afterStartDateMsg),
127
    }),
128
  };
129
};
130
131
const experienceToDetails = (
132
  experienceWork: ExperienceWork,
133
): WorkDetailsFormValues => {
134
  return {
135
    title: experienceWork.title,
136
    organization: experienceWork.organization,
137
    group: experienceWork.group,
138
    startDate: toInputDateString(experienceWork.start_date),
139
    isActive: experienceWork.is_active,
140
    endDate: experienceWork.end_date
141
      ? toInputDateString(experienceWork.end_date)
142
      : "",
143
  };
144
};
145
146
const dataToFormValues = (
147
  data: WorkExperienceSubmitData,
148
  locale: Locales,
149
): WorkExperienceFormValues => {
150
  const { experienceWork, savedRequiredSkills, savedOptionalSkills } = data;
151
  const skillToName = (skill: Skill): string =>
152
    localizeFieldNonNull(locale, skill, "name");
153
  return {
154
    ...experienceToDetails(data.experienceWork),
155
    requiredSkills: savedRequiredSkills.map(skillToName),
156
    optionalSkills: savedOptionalSkills.map(skillToName),
157
    useAsEducationRequirement: experienceWork.is_education_requirement,
158
  };
159
};
160
161
const detailsToExperience = (
162
  formValues: WorkDetailsFormValues,
163
  originalExperience: ExperienceWork,
164
): ExperienceWork => {
165
  return {
166
    ...originalExperience,
167
    title: formValues.title,
168
    organization: formValues.organization,
169
    group: formValues.group,
170
    start_date: fromInputDateString(formValues.startDate),
171
    is_active: formValues.isActive,
172
    end_date: formValues.endDate
173
      ? fromInputDateString(formValues.endDate)
174
      : null,
175
  };
176
};
177
178
const formValuesToData = (
179
  formValues: WorkExperienceFormValues,
180
  originalExperience: ExperienceWork,
181
  locale: Locales,
182
  skills: Skill[],
183
): WorkExperienceSubmitData => {
184
  const nameToSkill = (name: string): Skill | null =>
185
    matchValueToModel(locale, "name", name, skills);
186
  return {
187
    experienceWork: {
188
      ...detailsToExperience(formValues, originalExperience),
189
      is_education_requirement: formValues.useAsEducationRequirement,
190
    },
191
    savedRequiredSkills: formValues.requiredSkills
192
      .map(nameToSkill)
193
      .filter(notEmpty),
194
    savedOptionalSkills: formValues.optionalSkills
195
      .map(nameToSkill)
196
      .filter(notEmpty),
197
  };
198
};
199
200
const newExperienceWork = (
201
  experienceableId: number,
202
  experienceableType: ExperienceWork["experienceable_type"],
203
): ExperienceWork => ({
204
  id: 0,
205
  title: "",
206
  organization: "",
207
  group: "",
208
  is_active: false,
209
  start_date: new Date(),
210
  end_date: null,
211
  is_education_requirement: false,
212
  experienceable_id: experienceableId,
213
  experienceable_type: experienceableType,
214
  type: "experience_work",
215
});
216
217
const DetailsSubform: FunctionComponent = () => {
218
  const intl = useIntl();
219
  return (
220
    <div data-c-container="medium">
221
      <div data-c-grid="gutter(all, 1) middle">
222
        <FastField
223
          id="work-title"
224
          type="text"
225
          name="title"
226
          component={TextInput}
227
          required
228
          grid="base(1of1)"
229
          label={intl.formatMessage(messages.jobTitleLabel)}
230
          placeholder={intl.formatMessage(messages.jobTitlePlaceholder)}
231
        />
232
        <FastField
233
          id="work-organization"
234
          type="text"
235
          name="organization"
236
          component={TextInput}
237
          required
238
          grid="base(1of2)"
239
          label={intl.formatMessage(messages.orgNameLabel)}
240
          placeholder={intl.formatMessage(messages.orgNamePlaceholder)}
241
        />
242
        <FastField
243
          id="work-group"
244
          type="text"
245
          name="group"
246
          component={TextInput}
247
          required
248
          grid="base(1of2)"
249
          label={intl.formatMessage(messages.groupLabel)}
250
          placeholder={intl.formatMessage(messages.groupPlaceholder)}
251
        />
252
        <FastField
253
          id="work-startDate"
254
          name="startDate"
255
          component={DateInput}
256
          required
257
          grid="base(1of1)"
258
          label={intl.formatMessage(messages.startDateLabel)}
259
          placeholder={intl.formatMessage(messages.datePlaceholder)}
260
        />
261
        <Field
262
          id="work-isActive"
263
          name="isActive"
264
          component={CheckboxInput}
265
          grid="tl(1of2)"
266
          label={intl.formatMessage(messages.isActiveLabel)}
267
        />
268
        <Field
269
          id="work-endDate"
270
          name="endDate"
271
          component={DateInput}
272
          grid="base(1of2)"
273
          label={intl.formatMessage(messages.endDateLabel)}
274
          placeholder={intl.formatMessage(messages.datePlaceholder)}
275
        />
276
      </div>
277
    </div>
278
  );
279
};
280
281
interface ProfileWorkModalProps {
282
  modalId: string;
283
  experienceWork: ExperienceWork | null;
284
  experienceableId: number;
285
  experienceableType: ExperienceWork["experienceable_type"];
286
  parentElement: Element | null;
287
  visible: boolean;
288
  onModalCancel: () => void;
289
  onModalConfirm: (data: ExperienceWork) => Promise<void>;
290
}
291
292
export const ProfileWorkModal: FunctionComponent<ProfileWorkModalProps> = ({
293
  modalId,
294
  experienceWork,
295
  experienceableId,
296
  experienceableType,
297
  parentElement,
298
  visible,
299
  onModalCancel,
300
  onModalConfirm,
301
}) => {
302
  const intl = useIntl();
303
304
  const originalExperience =
305
    experienceWork ?? newExperienceWork(experienceableId, experienceableType);
306
307
  const initialFormValues = experienceToDetails(originalExperience);
308
309
  const validationSchema = Yup.object().shape({
310
    ...validationShape(intl),
311
  });
312
313
  return (
314
    <Modal
315
      id={modalId}
316
      parentElement={parentElement}
317
      visible={visible}
318
      onModalCancel={onModalCancel}
319
      onModalConfirm={onModalCancel}
320
      className="application-experience-dialog"
321
    >
322
      <ExperienceModalHeader
323
        title={intl.formatMessage(messages.modalTitle)}
324
        iconClass="fa-briefcase"
325
      />
326
      <Formik
327
        enableReinitialize
328
        initialValues={initialFormValues}
329
        onSubmit={async (values, actions): Promise<void> => {
330
          await onModalConfirm(detailsToExperience(values, originalExperience));
331
          actions.setSubmitting(false);
332
          actions.resetForm();
333
        }}
334
        validationSchema={validationSchema}
335
      >
336
        {(formikProps): React.ReactElement => (
337
          <Form>
338
            <Modal.Body>
339
              <ExperienceDetailsIntro
340
                description={intl.formatMessage(messages.modalDescription)}
341
              />
342
              <DetailsSubform />
343
            </Modal.Body>
344
            <ExperienceModalFooter buttonsDisabled={formikProps.isSubmitting} />
345
          </Form>
346
        )}
347
      </Formik>
348
    </Modal>
349
  );
350
};
351
interface WorkExperienceModalProps {
352
  modalId: string;
353
  experienceWork: ExperienceWork | null;
354
  jobId: number;
355
  classificationEducationRequirements: string | null;
356
  jobEducationRequirements: string | null;
357
  requiredSkills: Skill[];
358
  savedRequiredSkills: Skill[];
359
  optionalSkills: Skill[];
360
  savedOptionalSkills: Skill[];
361
  experienceableId: number;
362
  experienceableType: ExperienceWork["experienceable_type"];
363
  parentElement: Element | null;
364
  visible: boolean;
365
  onModalCancel: () => void;
366
  onModalConfirm: (data: WorkExperienceSubmitData) => Promise<void>;
367
}
368
369
export const WorkExperienceModal: React.FC<WorkExperienceModalProps> = ({
370
  modalId,
371
  experienceWork,
372
  jobId,
373
  classificationEducationRequirements,
374
  jobEducationRequirements,
375
  requiredSkills,
376
  savedRequiredSkills,
377
  optionalSkills,
378
  savedOptionalSkills,
379
  experienceableId,
380
  experienceableType,
381
  parentElement,
382
  visible,
383
  onModalCancel,
384
  onModalConfirm,
385
}) => {
386
  const intl = useIntl();
387
  const locale = getLocale(intl.locale);
388
389
  const originalExperience =
390
    experienceWork ?? newExperienceWork(experienceableId, experienceableType);
391
392
  const skillToName = (skill: Skill): string =>
393
    localizeFieldNonNull(locale, skill, "name");
394
395
  const initialFormValues = dataToFormValues(
396
    {
397
      experienceWork: originalExperience,
398
      savedRequiredSkills,
399
      savedOptionalSkills,
400
    },
401
    locale,
402
  );
403
404
  const validationSchema = Yup.object().shape({
405
    ...skillValidationShape,
406
    ...educationValidationShape,
407
    ...validationShape(intl),
408
  });
409
410
  return (
411
    <Modal
412
      id={modalId}
413
      parentElement={parentElement}
414
      visible={visible}
415
      onModalCancel={onModalCancel}
416
      onModalConfirm={onModalCancel}
417
      className="application-experience-dialog"
418
    >
419
      <ExperienceModalHeader
420
        title={intl.formatMessage(messages.modalTitle)}
421
        iconClass="fa-briefcase"
422
      />
423
      <Formik
424
        enableReinitialize
425
        initialValues={initialFormValues}
426
        onSubmit={async (values, actions): Promise<void> => {
427
          await onModalConfirm(
428
            formValuesToData(values, originalExperience, locale, [
429
              ...requiredSkills,
430
              ...optionalSkills,
431
            ]),
432
          );
433
          actions.setSubmitting(false);
434
          actions.resetForm();
435
        }}
436
        validationSchema={validationSchema}
437
      >
438
        {(formikProps): React.ReactElement => (
439
          <Form>
440
            <Modal.Body>
441
              <ExperienceDetailsIntro
442
                description={intl.formatMessage(messages.modalDescription)}
443
              />
444
              <DetailsSubform />
445
              <SkillSubform
446
                keyPrefix="work"
447
                jobId={jobId}
448
                jobRequiredSkills={requiredSkills.map(skillToName)}
449
                jobOptionalSkills={optionalSkills.map(skillToName)}
450
              />
451
              <EducationSubform
452
                keyPrefix="work"
453
                classificationEducationRequirements={
454
                  classificationEducationRequirements
455
                }
456
                jobEducationRequirements={jobEducationRequirements}
457
              />
458
            </Modal.Body>
459
            <ExperienceModalFooter buttonsDisabled={formikProps.isSubmitting} />
460
          </Form>
461
        )}
462
      </Formik>
463
    </Modal>
464
  );
465
};
466
467
export default WorkExperienceModal;
468