Passed
Push — task/intl-plural-rules-4.0.0-b... ( 51b60a )
by Yonathan
12:36 queued 05:20
created

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

Complexity

Total Complexity 8
Complexity/F 0

Size

Lines of Code 491
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

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