Passed
Push — feature/post-covid-application... ( 4340d4...92a3aa )
by Yonathan
08:31 queued 01:05
created

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

Complexity

Total Complexity 6
Complexity/F 0

Size

Lines of Code 376
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 6
eloc 301
mnd 6
bc 6
fnc 0
dl 0
loc 376
rs 10
bpm 0
cpm 0
noi 0
c 0
b 0
f 0
1
import React from "react";
2
import { FastField, 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 { validationMessages } from "../../Form/Messages";
13
import SkillSubform, {
14
  SkillFormValues,
15
  validationShape as skillValidationShape,
16
} from "./SkillSubform";
17
import { Skill, ExperienceAward } 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
import { localizedFieldNonNull } from "../../../models/app";
34
import SelectInput from "../../Form/SelectInput";
35
36
interface AwardRecipientType {
37
  id: number;
38
  name: localizedFieldNonNull;
39
}
40
41
interface AwardRecognitionType {
42
  id: number;
43
  name: localizedFieldNonNull;
44
}
45
46
interface AwardExperienceModalProps {
47
  modalId: string;
48
  experienceAward: ExperienceAward | null;
49
  recipientTypes: AwardRecipientType[];
50
  recognitionTypes: AwardRecognitionType[];
51
  jobId: number;
52
  requiredSkills: Skill[];
53
  savedRequiredSkills: Skill[];
54
  optionalSkills: Skill[];
55
  savedOptionalSkills: Skill[];
56
  experienceRequirments: EducationSubformProps;
57
  experienceableId: number;
58
  experienceableType: ExperienceAward["experienceable_type"];
59
  parentElement: Element | null;
60
  visible: boolean;
61
  onModalCancel: () => void;
62
  onModalConfirm: (data: AwardExperienceSubmitData) => Promise<void>;
63
}
64
65
const messages = defineMessages({
66
  modalTitle: {
67
    id: "awardExperienceModal.modalTitle",
68
    defaultMessage: "Add an Award",
69
  },
70
  modalDescription: {
71
    id: "awardExperienceModal.modalDescription",
72
    defaultMessage:
73
      "Did you get recognized for going above and beyond? There are many ways to get recognized, awards are just one of them. (Here’s an opportunity to share how you’ve been recognized.)",
74
  },
75
  titleLabel: {
76
    id: "awardExperienceModal.titleLabel",
77
    defaultMessage: "Award Title",
78
  },
79
  titlePlaceholder: {
80
    id: "awardExperienceModal.titlePlaceholder",
81
    defaultMessage: "e.g. My Award",
82
  },
83
  recipientTypeLabel: {
84
    id: "awardExperienceModal.recipientTypeLabel",
85
    defaultMessage: "Awarded to...",
86
  },
87
  recipientTypePlaceholder: {
88
    id: "awardExperienceModal.recipientTypePlaceholder",
89
    defaultMessage: "Select an option...",
90
    description: "Default selection in the Recipient Type dropdown menu.",
91
  },
92
  issuerLabel: {
93
    id: "awardExperienceModal.issuerLabel",
94
    defaultMessage: "Issuing Organization or Institution",
95
  },
96
  issuerPlaceholder: {
97
    id: "awardExperienceModal.issuerPlaceholder",
98
    defaultMessage: "e.g. Government of Canada",
99
  },
100
  recognitionTypeLabel: {
101
    id: "awardExperienceModal.recognitionTypeLabel",
102
    defaultMessage: "Scope of the Award",
103
  },
104
  recognitionTypePlaceholder: {
105
    id: "awardExperienceModal.recognitionTypePlaceholder",
106
    defaultMessage: "Select a scope...",
107
  },
108
  awardedDateLabel: {
109
    id: "awardExperienceModal.awardedDateLabel",
110
    defaultMessage: "Date Awarded",
111
  },
112
  datePlaceholder: {
113
    id: "awardExperienceModal.datePlaceholder",
114
    defaultMessage: "yyyy-mm-dd",
115
  },
116
});
117
118
export interface AwardDetailsFormValues {
119
  title: string;
120
  recipientTypeId: number | "";
121
  issuedBy: string;
122
  recognitionTypeId: number | "";
123
  awardedDate: string;
124
}
125
126
type AwardExperienceFormValues = SkillFormValues &
127
  EducationFormValues &
128
  AwardDetailsFormValues;
129
interface AwardExperienceSubmitData {
130
  experienceAward: ExperienceAward;
131
  savedRequiredSkills: Skill[];
132
  savedOptionalSkills: Skill[];
133
}
134
135
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
136
export const validationShape = (intl: IntlShape) => {
137
  const requiredMsg = intl.formatMessage(validationMessages.required);
138
  const inPastMsg = intl.formatMessage(validationMessages.dateMustBePast);
139
  return {
140
    title: Yup.string().required(requiredMsg),
141
    recipientTypeId: Yup.number().required(requiredMsg),
142
    issuedBy: Yup.string().required(requiredMsg),
143
    recognitionTypeId: Yup.number().required(requiredMsg),
144
    awardedDate: Yup.date().required(requiredMsg).max(new Date(), inPastMsg),
145
  };
146
};
147
148
const dataToFormValues = (
149
  data: AwardExperienceSubmitData,
150
  locale: Locales,
151
  creatingNew: boolean,
152
): AwardExperienceFormValues => {
153
  const { experienceAward, savedRequiredSkills, savedOptionalSkills } = data;
154
  const skillToName = (skill: Skill): string =>
155
    localizeFieldNonNull(locale, skill, "name");
156
  return {
157
    requiredSkills: savedRequiredSkills.map(skillToName),
158
    optionalSkills: savedOptionalSkills.map(skillToName),
159
    useAsEducationRequirement: experienceAward.is_education_requirement,
160
    title: experienceAward.title,
161
    recipientTypeId: creatingNew ? "" : experienceAward.award_recipient_type_id,
162
    issuedBy: experienceAward.issued_by,
163
    recognitionTypeId: creatingNew
164
      ? ""
165
      : experienceAward.award_recognition_type_id,
166
    awardedDate: toInputDateString(experienceAward.awarded_date),
167
  };
168
};
169
170
/* eslint-disable @typescript-eslint/camelcase */
171
const formValuesToData = (
172
  formValues: AwardExperienceFormValues,
173
  originalExperience: ExperienceAward,
174
  locale: Locales,
175
  skills: Skill[],
176
): AwardExperienceSubmitData => {
177
  const nameToSkill = (name: string): Skill | null =>
178
    matchValueToModel(locale, "name", name, skills);
179
  return {
180
    experienceAward: {
181
      ...originalExperience,
182
      title: formValues.title,
183
      award_recipient_type_id: formValues.recipientTypeId
184
        ? Number(formValues.recipientTypeId)
185
        : 1,
186
      issued_by: formValues.issuedBy,
187
      award_recognition_type_id: formValues.recognitionTypeId
188
        ? Number(formValues.recognitionTypeId)
189
        : 1,
190
      awarded_date: fromInputDateString(formValues.awardedDate),
191
      is_education_requirement: formValues.useAsEducationRequirement,
192
    },
193
    savedRequiredSkills: formValues.requiredSkills
194
      .map(nameToSkill)
195
      .filter(notEmpty),
196
    savedOptionalSkills: formValues.optionalSkills
197
      .map(nameToSkill)
198
      .filter(notEmpty),
199
  };
200
};
201
202
const newExperienceAward = (
203
  experienceableId: number,
204
  experienceableType: ExperienceAward["experienceable_type"],
205
): ExperienceAward => ({
206
  id: 0,
207
  title: "",
208
  award_recipient_type_id: 0,
209
  issued_by: "",
210
  award_recognition_type_id: 0,
211
  awarded_date: new Date(),
212
  is_education_requirement: false,
213
  experienceable_id: experienceableId,
214
  experienceable_type: experienceableType,
215
  type: "experience_award",
216
});
217
/* eslint-enable @typescript-eslint/camelcase */
218
219
export const AwardExperienceModal: React.FC<AwardExperienceModalProps> = ({
220
  modalId,
221
  experienceAward,
222
  recipientTypes,
223
  recognitionTypes,
224
  jobId,
225
  requiredSkills,
226
  savedRequiredSkills,
227
  optionalSkills,
228
  savedOptionalSkills,
229
  experienceRequirments,
230
  experienceableId,
231
  experienceableType,
232
  parentElement,
233
  visible,
234
  onModalCancel,
235
  onModalConfirm,
236
}) => {
237
  const intl = useIntl();
238
  const locale = getLocale(intl.locale);
239
240
  const originalExperience =
241
    experienceAward ?? newExperienceAward(experienceableId, experienceableType);
242
243
  const skillToName = (skill: Skill): string =>
244
    localizeFieldNonNull(locale, skill, "name");
245
246
  const initialFormValues = dataToFormValues(
247
    {
248
      experienceAward: originalExperience,
249
      savedRequiredSkills,
250
      savedOptionalSkills,
251
    },
252
    locale,
253
    experienceAward === null,
254
  );
255
256
  const validationSchema = Yup.object().shape({
257
    ...skillValidationShape,
258
    ...educationValidationShape,
259
    ...validationShape(intl),
260
  });
261
262
  const detailsSubform = (
263
    <div data-c-container="medium">
264
      <div data-c-grid="gutter(all, 1) middle">
265
        <FastField
266
          id="title"
267
          type="text"
268
          name="title"
269
          component={TextInput}
270
          required
271
          grid="base(1of1)"
272
          label={intl.formatMessage(messages.titleLabel)}
273
          placeholder={intl.formatMessage(messages.titlePlaceholder)}
274
        />
275
        <FastField
276
          id="recipientTypeId"
277
          name="recipientTypeId"
278
          label={intl.formatMessage(messages.recipientTypeLabel)}
279
          grid="tl(1of2)"
280
          component={SelectInput}
281
          required
282
          nullSelection={intl.formatMessage(messages.recipientTypePlaceholder)}
283
          options={recipientTypes.map((type) => ({
284
            value: type.id,
285
            label: localizeFieldNonNull(locale, type, "name"),
286
          }))}
287
        />
288
        <FastField
289
          id="issuedBy"
290
          type="text"
291
          name="issuedBy"
292
          component={TextInput}
293
          required
294
          grid="tl(1of2)"
295
          label={intl.formatMessage(messages.issuerLabel)}
296
          placeholder={intl.formatMessage(messages.issuerPlaceholder)}
297
        />
298
        <FastField
299
          id="recognitionTypeId"
300
          name="recognitionTypeId"
301
          label={intl.formatMessage(messages.recognitionTypeLabel)}
302
          grid="tl(1of2)"
303
          component={SelectInput}
304
          required
305
          nullSelection={intl.formatMessage(
306
            messages.recognitionTypePlaceholder,
307
          )}
308
          options={recognitionTypes.map((status) => ({
309
            value: status.id,
310
            label: localizeFieldNonNull(locale, status, "name"),
311
          }))}
312
        />
313
        <FastField
314
          id="awardedDate"
315
          name="awardedDate"
316
          component={DateInput}
317
          required
318
          grid="tl(1of2)"
319
          label={intl.formatMessage(messages.awardedDateLabel)}
320
          placeholder={intl.formatMessage(messages.datePlaceholder)}
321
        />
322
      </div>
323
    </div>
324
  );
325
326
  return (
327
    <Modal
328
      id={modalId}
329
      parentElement={parentElement}
330
      visible={visible}
331
      onModalCancel={onModalCancel}
332
      onModalConfirm={onModalCancel}
333
      className="application-experience-dialog"
334
    >
335
      <ExperienceModalHeader
336
        title={intl.formatMessage(messages.modalTitle)}
337
        iconClass="fa-book"
338
      />
339
      <Formik
340
        enableReinitialize
341
        initialValues={initialFormValues}
342
        onSubmit={async (values, actions): Promise<void> => {
343
          await onModalConfirm(
344
            formValuesToData(values, originalExperience, locale, [
345
              ...requiredSkills,
346
              ...optionalSkills,
347
            ]),
348
          );
349
          actions.setSubmitting(false);
350
        }}
351
        validationSchema={validationSchema}
352
      >
353
        {(formikProps): React.ReactElement => (
354
          <Form>
355
            <Modal.Body>
356
              <ExperienceDetailsIntro
357
                description={intl.formatMessage(messages.modalDescription)}
358
              />
359
              {detailsSubform}
360
              <SkillSubform
361
                jobId={jobId}
362
                jobRequiredSkills={requiredSkills.map(skillToName)}
363
                jobOptionalSkills={optionalSkills.map(skillToName)}
364
              />
365
              <EducationSubform {...experienceRequirments} />
366
            </Modal.Body>
367
            <ExperienceModalFooter buttonsDisabled={formikProps.isSubmitting} />
368
          </Form>
369
        )}
370
      </Formik>
371
    </Modal>
372
  );
373
};
374
375
export default AwardExperienceModal;
376