Passed
Push — dev ( 08af1d...32f961 )
by
unknown
04:34
created

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

Complexity

Total Complexity 6
Complexity/F 0

Size

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