Passed
Push — dev ( 215397...f312c9 )
by Tristan
06:19 queued 16s
created

resources/assets/js/components/ApplicantProfile/ProfileExperience.tsx   A

Complexity

Total Complexity 9
Complexity/F 0

Size

Lines of Code 349
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 9
eloc 278
mnd 9
bc 9
fnc 0
dl 0
loc 349
rs 10
bpm 0
cpm 0
noi 0
c 0
b 0
f 0
1
import React, { FunctionComponent, useState } from "react";
2
import { FormattedMessage, useIntl } from "react-intl";
3
import * as Yup from "yup";
4
import { Field, Form, Formik } from "formik";
5
import {
6
  Criteria,
7
  Experience,
8
  Skill,
9
  ExperienceSkill,
10
} from "../../models/types";
11
import { MyExperience } from "../Application/Experience/Experience";
12
import {
13
  AwardRecipientType,
14
  AwardRecognitionType,
15
} from "../Application/ExperienceModals/AwardExperienceModal";
16
import {
17
  EducationStatus,
18
  EducationType,
19
} from "../Application/ExperienceModals/EducationExperienceModal";
20
import { find, hasKey, mapToObject, getId } from "../../helpers/queries";
21
import Modal from "../Modal";
22
import AlertWhenUnsaved from "../Form/AlertWhenUnsaved";
23
import TextAreaInput from "../Form/TextAreaInput";
24
import { skillMessages } from "../Application/applicationMessages";
25
import { validationMessages } from "../Form/Messages";
26
import { JUSTIFICATION_WORD_LIMIT } from "../Application/Skills/Skills";
27
import { countNumberOfWords } from "../WordCounter/helpers";
28
import WordCounter from "../WordCounter/WordCounter";
29
import displayMessages from "../Application/Skills/skillsMessages";
30
import {
31
  getExperienceHeading,
32
  getExperienceJustificationLabel,
33
  getExperienceSubheading,
34
} from "../../models/localizedConstants";
35
import { getLocale, localizeFieldNonNull } from "../../helpers/localize";
36
import { getExperienceOfExperienceSkill } from "../Application/helpers";
37
38
const SkillExperienceModal: FunctionComponent<{
39
  experienceSkill: ExperienceSkill | null;
40
  experiences: Experience[];
41
  skillsById: { [id: number]: Skill };
42
  handleCancel: () => void;
43
  handleConfirm: (data: ExperienceSkill) => Promise<void>;
44
  handleDelete: () => Promise<void>;
45
}> = ({
46
  experienceSkill,
47
  handleCancel,
48
  handleConfirm,
49
  handleDelete,
50
  skillsById,
51
  experiences,
52
}) => {
53
  const intl = useIntl();
54
  const locale = getLocale(intl.locale);
55
56
  const [isDeleting, setIsDeleting] = useState(false);
57
58
  const initialValues = {
59
    justification: experienceSkill?.justification ?? "",
60
  };
61
62
  const experienceSkillSchema = Yup.object().shape({
63
    justification: Yup.string()
64
      .test(
65
        "wordCount",
66
        intl.formatMessage(validationMessages.overMaxWords, {
67
          numberOfWords: JUSTIFICATION_WORD_LIMIT,
68
        }),
69
        (value: string) =>
70
          countNumberOfWords(value) <= JUSTIFICATION_WORD_LIMIT,
71
      )
72
      .required(intl.formatMessage(validationMessages.required)),
73
  });
74
75
  const experience =
76
    experienceSkill !== null
77
      ? getExperienceOfExperienceSkill(experienceSkill, experiences)
78
      : null;
79
  let textareaLabel = "";
80
  let heading = "";
81
  let subheading = "";
82
83
  if (
84
    experienceSkill !== null &&
85
    experience !== null &&
86
    hasKey(skillsById, experienceSkill.skill_id)
87
  ) {
88
    const skill = skillsById[experienceSkill.skill_id];
89
    const skillName = localizeFieldNonNull(locale, skill, "name");
90
    textareaLabel = getExperienceJustificationLabel(
91
      experience,
92
      intl,
93
      skillName,
94
    );
95
    heading = getExperienceHeading(experience, intl);
96
    subheading = getExperienceSubheading(experience, intl);
97
  }
98
99
  return (
100
    <Modal
101
      id="profile-experience-skill-modal"
102
      parentElement={document.getElementById("modal-root")}
103
      visible={experienceSkill !== null}
104
      onModalConfirm={handleCancel}
105
      onModalCancel={handleCancel}
106
    >
107
      <div
108
        className="dialog-header"
109
        data-c-background="c1(100)"
110
        data-c-border="bottom(thin, solid, black)"
111
        data-c-padding="tb(1)"
112
      >
113
        <div data-c-container="medium">
114
          <h5
115
            data-c-colour="white"
116
            data-c-font-size="h3"
117
            data-c-font-weight="bold"
118
            data-c-dialog-focus
119
          >
120
            {heading}
121
          </h5>
122
          <p
123
            data-c-margin="top(quarter)"
124
            data-c-colour="white"
125
            data-c-font-size="small"
126
          >
127
            {subheading}
128
          </p>
129
        </div>
130
      </div>
131
      {experienceSkill !== null && (
132
        <Formik
133
          initialValues={initialValues}
134
          validationSchema={experienceSkillSchema}
135
          onSubmit={(values, { setSubmitting, resetForm }): void => {
136
            handleConfirm({
137
              ...experienceSkill,
138
              justification: values.justification,
139
            })
140
              .then(() => {
141
                setSubmitting(false);
142
                resetForm();
143
              })
144
              .catch(() => {
145
                // If there is an error, don't reset the form, allowing user to retry.
146
                setSubmitting(false);
147
              });
148
          }}
149
        >
150
          {({ dirty, isSubmitting, resetForm }): React.ReactElement => (
151
            <Form>
152
              <AlertWhenUnsaved />
153
              <hr data-c-hr="thin(gray)" data-c-margin="bottom(1)" />
154
              <div data-c-padding="lr(1)">
155
                <Field
156
                  id="experience-skill-textarea"
157
                  name="justification"
158
                  label={textareaLabel}
159
                  component={TextAreaInput}
160
                  placeholder={intl.formatMessage(
161
                    skillMessages.experienceSkillPlaceholder,
162
                  )}
163
                  required
164
                />
165
              </div>
166
              <div data-c-padding="all(1)">
167
                <div data-c-grid="gutter(all, 1) middle">
168
                  <div
169
                    data-c-grid-item="tp(1of2)"
170
                    data-c-align="base(center) tp(left)"
171
                  >
172
                    <button
173
                      data-c-button="outline(c1)"
174
                      data-c-radius="rounded"
175
                      data-c-margin="right(1)"
176
                      type="button"
177
                      onClick={handleCancel}
178
                      disabled={isSubmitting || isDeleting}
179
                    >
180
                      <span>
181
                        <FormattedMessage
182
                          id="profileExperience.skillExperienceModal.cancel"
183
                          defaultMessage="Cancel"
184
                          description="Cancel button text"
185
                        />
186
                      </span>
187
                    </button>
188
                    <button
189
                      data-c-button="outline(stop)"
190
                      data-c-radius="rounded"
191
                      type="button"
192
                      onClick={() => {
193
                        setIsDeleting(true);
194
                        handleDelete()
195
                          .then(() => {
196
                            setIsDeleting(false);
197
                            resetForm();
198
                          })
199
                          .catch(() => {
200
                            setIsDeleting(false);
201
                          });
202
                      }}
203
                      disabled={isSubmitting || isDeleting}
204
                    >
205
                      <span>
206
                        <FormattedMessage
207
                          id="profileExperience.skillExperienceModal.delete"
208
                          defaultMessage="Delete"
209
                          description="Delete button text"
210
                        />
211
                      </span>
212
                    </button>
213
                  </div>
214
                  <div
215
                    data-c-grid-item="tp(1of2)"
216
                    data-c-align="base(center) tp(right)"
217
                  >
218
                    <WordCounter
219
                      elementId="experience-skill-textarea"
220
                      maxWords={JUSTIFICATION_WORD_LIMIT}
221
                      minWords={0}
222
                      absoluteValue
223
                      dataAttributes={{ "data-c-margin": "right(1)" }}
224
                      underMaxMessage={intl.formatMessage(
225
                        displayMessages.wordCountUnderMax,
226
                      )}
227
                      overMaxMessage={intl.formatMessage(
228
                        displayMessages.wordCountOverMax,
229
                      )}
230
                    />
231
                    <button
232
                      data-c-button="solid(c1)"
233
                      data-c-radius="rounded"
234
                      type="submit"
235
                      disabled={!dirty || isSubmitting || isDeleting}
236
                    >
237
                      <span>
238
                        {dirty
239
                          ? intl.formatMessage(displayMessages.save)
240
                          : intl.formatMessage(displayMessages.saved)}
241
                      </span>
242
                    </button>
243
                  </div>
244
                </div>
245
              </div>
246
            </Form>
247
          )}
248
        </Formik>
249
      )}
250
    </Modal>
251
  );
252
};
253
254
export interface ProfileExperienceProps {
255
  experiences: Experience[];
256
  educationStatuses: EducationStatus[];
257
  educationTypes: EducationType[];
258
  experienceSkills: ExperienceSkill[];
259
  criteria: Criteria[];
260
  skills: Skill[];
261
  jobId: number;
262
  jobClassificationId: number | null;
263
  jobEducationRequirements: string | null;
264
  recipientTypes: AwardRecipientType[];
265
  recognitionTypes: AwardRecognitionType[];
266
  handleSubmitExperience: (data: Experience) => Promise<void>;
267
  handleDeleteExperience: (
268
    id: number,
269
    type: Experience["type"],
270
  ) => Promise<void>;
271
  handleUpdateExperienceSkill: (expSkill: ExperienceSkill) => Promise<void>;
272
  handleDeleteExperienceSkill: (id: number) => Promise<void>;
273
}
274
275
export const ProfileExperience: React.FC<ProfileExperienceProps> = ({
276
  experiences,
277
  educationStatuses,
278
  educationTypes,
279
  experienceSkills,
280
  criteria,
281
  skills,
282
  handleSubmitExperience,
283
  handleDeleteExperience,
284
  handleUpdateExperienceSkill,
285
  handleDeleteExperienceSkill,
286
  jobId,
287
  jobClassificationId,
288
  jobEducationRequirements,
289
  recipientTypes,
290
  recognitionTypes,
291
}) => {
292
  const [editedExperienceSkillId, setEditedExperienceSkillId] = useState<
293
    number | null
294
  >(null);
295
  const editedExpSkill =
296
    editedExperienceSkillId !== null
297
      ? find(experienceSkills, editedExperienceSkillId)
298
      : null;
299
300
  const skillsById = mapToObject(skills, getId);
301
302
  return (
303
    <>
304
      <MyExperience
305
        experiences={experiences}
306
        educationStatuses={educationStatuses}
307
        educationTypes={educationTypes}
308
        experienceSkills={experienceSkills}
309
        criteria={criteria}
310
        skills={skills}
311
        jobId={jobId}
312
        jobClassificationId={jobClassificationId}
313
        jobEducationRequirements={jobEducationRequirements}
314
        recipientTypes={recipientTypes}
315
        recognitionTypes={recognitionTypes}
316
        handleSubmitExperience={handleSubmitExperience}
317
        handleDeleteExperience={handleDeleteExperience}
318
        handleEditSkill={setEditedExperienceSkillId}
319
        context="profile"
320
      />
321
      <div
322
        data-c-dialog-overlay={editedExperienceSkillId !== null ? "active" : ""}
323
      />
324
      <SkillExperienceModal
325
        experienceSkill={editedExpSkill}
326
        handleCancel={() => setEditedExperienceSkillId(null)}
327
        handleConfirm={async (expSkill): Promise<void> => {
328
          return handleUpdateExperienceSkill(expSkill).then(() => {
329
            setEditedExperienceSkillId(null);
330
          });
331
        }}
332
        handleDelete={async (): Promise<void> => {
333
          if (editedExperienceSkillId !== null) {
334
            return handleDeleteExperienceSkill(editedExperienceSkillId).then(
335
              () => {
336
                setEditedExperienceSkillId(null);
337
              },
338
            );
339
          }
340
        }}
341
        experiences={experiences}
342
        skillsById={skillsById}
343
      />
344
    </>
345
  );
346
};
347
348
export default ProfileExperience;
349