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

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

Complexity

Total Complexity 6
Complexity/F 0

Size

Lines of Code 377
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

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