Passed
Push — feature/applicant-profile-skil... ( 78e4f0...588f1c )
by
unknown
05:50
created

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

Complexity

Total Complexity 6
Complexity/F 0

Size

Lines of Code 410
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

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