Passed
Push — feature/experience-form-modals ( ac040e...10a0f1 )
by Tristan
03:59
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 298
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, Field, 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 CheckboxInput from "../../Form/CheckboxInput";
13
import { validationMessages } from "../../Form/Messages";
14
import SkillSubform, {
15
  SkillFormValues,
16
  validationShape as skillValidationShape,
17
} from "./SkillSubform";
18
import { Skill, ExperienceAward } 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, castStringToNum } from "../../../helpers/queries";
34
import { localizedFieldNonNull } from "../../../models/app";
35
import SelectInput from "../../Form/SelectInput";
36
37
interface AwardRecipientType {
38
  id: number;
39
  name: localizedFieldNonNull;
40
}
41
42
interface AwardRecognitionType {
43
  id: number;
44
  name: localizedFieldNonNull;
45
}
46
47
interface AwardExperienceModalProps {
48
  modalId: string;
49
  experienceAward: ExperienceAward | null;
50
  recipientTypes: AwardRecipientType[];
51
  recognitionTypes: AwardRecognitionType[];
52
  jobId: number;
53
  requiredSkills: Skill[];
54
  savedRequiredSkills: Skill[];
55
  optionalSkills: Skill[];
56
  savedOptionalSkills: Skill[];
57
  experienceRequirments: EducationSubformProps;
58
  useAsEducationRequirement: boolean;
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 | null;
121
  issuedBy: string;
122
  recognitionTypeId: number | null;
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
  useAsEducationRequirement: boolean;
134
}
135
136
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
137
export const validationShape = (intl: IntlShape) => {
138
  const requiredMsg = intl.formatMessage(validationMessages.required);
139
  const inPastMsg = intl.formatMessage(validationMessages.dateMustBePast);
140
  return {
141
    title: Yup.string().required(requiredMsg),
142
    recipientTypeId: Yup.number().required(requiredMsg),
143
    issuedBy: Yup.string().required(requiredMsg),
144
    recognitionTypeId: Yup.number().required(requiredMsg),
145
    awardDate: Yup.date().required(requiredMsg).max(new Date(), inPastMsg),
146
  };
147
};
148
149
const dataToFormValues = (
150
  data: AwardExperienceSubmitData,
151
  locale: Locales,
152
  creatingNew: boolean,
153
): AwardExperienceFormValues => {
154
  const {
155
    experienceAward,
156
    savedRequiredSkills,
157
    savedOptionalSkills,
158
    useAsEducationRequirement,
159
  } = data;
160
  const skillToName = (skill: Skill): string =>
161
    localizeFieldNonNull(locale, skill, "name");
162
  return {
163
    requiredSkills: savedRequiredSkills.map(skillToName),
164
    optionalSkills: savedOptionalSkills.map(skillToName),
165
    useAsEducationRequirement,
166
    title: experienceAward.title,
167
    recipientTypeId: creatingNew
168
      ? null
169
      : experienceAward.award_recipient_type_id,
170
    issuedBy: experienceAward.issued_by,
171
    recognitionTypeId: creatingNew
172
      ? null
173
      : experienceAward.award_recognition_type_id,
174
    awardedDate: toInputDateString(experienceAward.awarded_date),
175
  };
176
};
177
178
/* eslint-disable @typescript-eslint/camelcase */
179
const formValuesToData = (
180
  formValues: AwardExperienceFormValues,
181
  originalExperience: ExperienceAward,
182
  locale: Locales,
183
  skills: Skill[],
184
): AwardExperienceSubmitData => {
185
  const nameToSkill = (name: string): Skill | null =>
186
    matchValueToModel(locale, "name", name, skills);
187
  return {
188
    experienceAward: {
189
      ...originalExperience,
190
      title: formValues.title,
191
      award_recipient_type_id: formValues.recipientTypeId
192
        ? Number(formValues.recipientTypeId)
193
        : 1,
194
      issued_by: formValues.issuedBy,
195
      award_recognition_type_id: formValues.recognitionTypeId
196
        ? Number(formValues.recognitionTypeId)
197
        : 1,
198
      awarded_date: fromInputDateString(formValues.awardedDate),
199
    },
200
    useAsEducationRequirement: formValues.useAsEducationRequirement,
201
    savedRequiredSkills: formValues.requiredSkills
202
      .map(nameToSkill)
203
      .filter(notEmpty),
204
    savedOptionalSkills: formValues.optionalSkills
205
      .map(nameToSkill)
206
      .filter(notEmpty),
207
  };
208
};
209
210
const newExperienceAward = (): ExperienceAward => ({
211
  id: 0,
212
  title: "",
213
  award_recipient_type_id: 0,
214
  issued_by: "",
215
  award_recognition_type_id: 0,
216
  awarded_date: new Date(),
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
  requiredSkills,
227
  savedRequiredSkills,
228
  optionalSkills,
229
  savedOptionalSkills,
230
  experienceRequirments,
231
  useAsEducationRequirement,
232
  parentElement,
233
  visible,
234
  onModalCancel,
235
  onModalConfirm,
236
}) => {
237
  const intl = useIntl();
238
  const locale = getLocale(intl.locale);
239
240
  const originalExperience = experienceAward ?? newExperienceAward();
241
242
  const skillToName = (skill: Skill): string =>
243
    localizeFieldNonNull(locale, skill, "name");
244
245
  const initialFormValues = dataToFormValues(
246
    {
247
      experienceAward: originalExperience,
248
      savedRequiredSkills,
249
      savedOptionalSkills,
250
      useAsEducationRequirement,
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