Passed
Push — feature/h2-experience-modals ( 13356e )
by Tristan
06:18
created

resources/assets/js/components/ApplicantProfile/Experience/ProfileExperience.tsx   B

Complexity

Total Complexity 43
Complexity/F 0

Size

Lines of Code 468
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 43
eloc 391
mnd 43
bc 43
fnc 0
dl 0
loc 468
rs 8.96
bpm 0
cpm 0
noi 0
c 0
b 0
f 0

How to fix   Complexity   

Complexity

Complex classes like resources/assets/js/components/ApplicantProfile/Experience/ProfileExperience.tsx often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
/* eslint-disable camelcase */
2
import React, { useEffect } from "react";
3
import { FormattedMessage, useIntl } from "react-intl";
4
import {
5
  Experience,
6
  Skill,
7
  ExperienceSkill,
8
  ExperienceEducation,
9
  ExperienceWork,
10
  ExperienceCommunity,
11
  ExperiencePersonal,
12
  ExperienceAward,
13
} from "../../../models/types";
14
import {
15
  modalButtonProps,
16
  ModalButton,
17
  ModalButtonH2,
18
} from "../../Application/Experience/Experience";
19
import { mapToObject, getId } from "../../../helpers/queries";
20
import { experienceMessages } from "../../Application/applicationMessages";
21
import { toggleAccordion } from "../../../helpers/forms";
22
import { useUrlHash } from "../../../helpers/router";
23
24
import { getExperienceSkillsOfExperience } from "../../Application/helpers";
25
import { ProfileEducationAccordion } from "../../Application/ExperienceAccordions/ExperienceEducationAccordion";
26
import { ProfileWorkAccordion } from "../../Application/ExperienceAccordions/ExperienceWorkAccordion";
27
import { ProfileCommunityAccordion } from "../../Application/ExperienceAccordions/ExperienceCommunityAccordion";
28
import { ProfilePersonalAccordion } from "../../Application/ExperienceAccordions/ExperiencePersonalAccordion";
29
import { ProfileAwardAccordion } from "../../Application/ExperienceAccordions/ExperienceAwardAccordion";
30
import ProfileEducationModal from "./EducationExperienceProfileModal";
31
import ProfileWorkModal from "./WorkExperienceProfileModal";
32
import ProfileCommunityModal from "./CommunityExperienceProfileModal";
33
import ProfilePersonalModal from "./PersonalExperienceProfileModal";
34
import ProfileAwardModal from "./AwardExperienceProfileModal";
35
import {
36
  FormEducationStatus,
37
  FormEducationType,
38
} from "../../Application/ExperienceModals/EducationExperienceModal";
39
import {
40
  FormAwardRecipientType,
41
  FormAwardRecognitionType,
42
} from "../../Application/ExperienceModals/AwardExperienceModal";
43
import { ExperienceSubmitData } from "./ProfileExperienceCommon";
44
import { getApplicantSkillsUrl } from "../../../helpers/routes";
45
import { getLocale } from "../../../helpers/localize";
46
47
const profileExperienceAccordion = (
48
  experience: Experience,
49
  relevantSkills: ExperienceSkill[],
50
  skillsById: { [id: number]: Skill },
51
  handleEdit: () => void,
52
  handleDelete: () => Promise<void>,
53
): React.ReactElement | null => {
54
  switch (experience.type) {
55
    case "experience_education":
56
      return (
57
        <ProfileEducationAccordion
58
          key={`${experience.id}-${experience.type}`}
59
          experience={experience}
60
          handleDelete={handleDelete}
61
          handleEdit={handleEdit}
62
          relevantSkills={relevantSkills}
63
          skillsById={skillsById}
64
        />
65
      );
66
    case "experience_work":
67
      return (
68
        <ProfileWorkAccordion
69
          key={`${experience.id}-${experience.type}`}
70
          experience={experience}
71
          handleDelete={handleDelete}
72
          handleEdit={handleEdit}
73
          relevantSkills={relevantSkills}
74
          skillsById={skillsById}
75
        />
76
      );
77
    case "experience_community":
78
      return (
79
        <ProfileCommunityAccordion
80
          key={`${experience.id}-${experience.type}`}
81
          experience={experience}
82
          handleDelete={handleDelete}
83
          handleEdit={handleEdit}
84
          relevantSkills={relevantSkills}
85
          skillsById={skillsById}
86
        />
87
      );
88
    case "experience_personal":
89
      return (
90
        <ProfilePersonalAccordion
91
          key={`${experience.id}-${experience.type}`}
92
          experience={experience}
93
          handleDelete={handleDelete}
94
          handleEdit={handleEdit}
95
          relevantSkills={relevantSkills}
96
          skillsById={skillsById}
97
        />
98
      );
99
    case "experience_award":
100
      return (
101
        <ProfileAwardAccordion
102
          key={`${experience.id}-${experience.type}`}
103
          experience={experience}
104
          handleDelete={handleDelete}
105
          handleEdit={handleEdit}
106
          relevantSkills={relevantSkills}
107
          skillsById={skillsById}
108
        />
109
      );
110
    default:
111
      return null;
112
  }
113
};
114
115
const NoSkillsNotification: React.FC<{ applicantId: number }> = ({
116
  applicantId,
117
}) => {
118
  const intl = useIntl();
119
  const locale = getLocale(intl.locale);
120
  return (
121
    <div
122
      data-c-alert="warning"
123
      data-c-radius="rounded"
124
      role="alert"
125
      data-c-margin="bottom(1)"
126
      // data-c-grid="middle"
127
    >
128
      <div data-c-padding="half" data-c-grid="middle">
129
        <div
130
          data-c-grid-item="base(1of1) pl(1of8) tl(1of12)"
131
          data-c-align="center"
132
        >
133
          <i
134
            aria-hidden="true"
135
            className="fa fa-exclamation-circle"
136
            data-c-padding="right(.5)"
137
            data-c-font-size="h2"
138
            data-c-margin="tb(.5)"
139
          />
140
        </div>
141
        <div data-c-grid-item="base(1of1) pl(7of8) tl(11of12)">
142
          <p>
143
            <FormattedMessage
144
              id="profile.experience.noSkills"
145
              defaultMessage="<b>No skills:</b> It seems you have not added any skills yet. You can create experiences now, but this section works best when you have some skills on your profile. <a>Click here to add skills.</a>"
146
              description="Alert that appears when there are no skills yet attached to profile."
147
              values={{
148
                b: (value) => <span data-c-font-weight="bold">{value}</span>,
149
                a: (...chunks): React.ReactElement => (
150
                  <a href={getApplicantSkillsUrl(locale, applicantId)}>
151
                    {chunks}
152
                  </a>
153
                ),
154
              }}
155
            />
156
          </p>
157
        </div>
158
      </div>
159
    </div>
160
  );
161
};
162
export interface ProfileExperienceProps {
163
  applicantId: number;
164
  experiences: Experience[];
165
  educationStatuses: FormEducationStatus[];
166
  educationTypes: FormEducationType[];
167
  experienceSkills: ExperienceSkill[];
168
  userSkills: Skill[];
169
  recipientTypes: FormAwardRecipientType[];
170
  recognitionTypes: FormAwardRecognitionType[];
171
  handleCreateExperience: (
172
    data: ExperienceSubmitData<Experience>,
173
  ) => Promise<void>;
174
  handleUpdateExperience: (
175
    data: ExperienceSubmitData<Experience>,
176
  ) => Promise<void>;
177
  handleDeleteExperience: (
178
    id: number,
179
    type: Experience["type"],
180
  ) => Promise<void>;
181
}
182
183
export const ProfileExperience: React.FC<ProfileExperienceProps> = ({
184
  applicantId,
185
  experiences,
186
  educationStatuses,
187
  educationTypes,
188
  experienceSkills,
189
  userSkills,
190
  handleCreateExperience,
191
  handleUpdateExperience,
192
  handleDeleteExperience,
193
  recipientTypes,
194
  recognitionTypes,
195
}) => {
196
  const intl = useIntl();
197
198
  const [isModalVisible, setIsModalVisible] = React.useState<{
199
    id: Experience["type"] | "";
200
    visible: boolean;
201
  }>({
202
    id: "",
203
    visible: false,
204
  });
205
206
  const [experienceData, setExperienceData] = React.useState<Experience | null>(
207
    null,
208
  );
209
210
  const modalButtons = modalButtonProps(intl);
211
212
  const openModal = (id: Experience["type"]): void => {
213
    setIsModalVisible({ id, visible: true });
214
  };
215
216
  const closeModal = (): void => {
217
    setExperienceData(null);
218
    setIsModalVisible({ id: "", visible: false });
219
  };
220
221
  const updateExperience = (data: ExperienceSubmitData<Experience>) =>
222
    handleUpdateExperience(data).then(closeModal);
223
  const createExperience = (data: ExperienceSubmitData<Experience>) =>
224
    handleCreateExperience(data).then(closeModal);
225
226
  const editExperience = (experience: Experience): void => {
227
    setExperienceData(experience);
228
    setIsModalVisible({ id: experience.type, visible: true });
229
  };
230
231
  const deleteExperience = (experience: Experience): Promise<void> =>
232
    handleDeleteExperience(experience.id, experience.type).then(closeModal);
233
234
  const skillsById = mapToObject(userSkills, getId);
235
236
  const modalRoot = document.getElementById("modal-root");
237
238
  // Open modal for editing if URI has a hash.
239
  const uriHash = useUrlHash();
240
  let experienceType: string;
241
  let experienceId: number;
242
243
  if (uriHash) {
244
    const uriHashFragments = uriHash.substring(1).split("_");
245
    // Get experience type from first two fragments of URI hash.
246
    experienceType = `${uriHashFragments[0]}_${uriHashFragments[1]}`;
247
    // Get experience id from third fragment of URI hash.
248
    experienceId = Number(uriHashFragments[2]);
249
  }
250
251
  const experienceCurrent: Experience | undefined = experiences.find(
252
    (experience) =>
253
      experience.type === experienceType && experience.id === experienceId,
254
  );
255
256
  useEffect(() => {
257
    if (uriHash && experienceCurrent) {
258
      toggleAccordion(uriHash.substring(1));
259
      // Open edit experience modal.
260
      editExperience(experienceCurrent);
261
    }
262
    // eslint-disable-next-line react-hooks/exhaustive-deps
263
  }, []); // useEffect should only run on mount and unmount.
264
265
  return (
266
    <>
267
      {userSkills.length === 0 && (
268
        <NoSkillsNotification applicantId={applicantId} />
269
      )}
270
      <div>
271
        <h2 data-c-heading="h2" data-c-margin="bottom(1)">
272
          {intl.formatMessage(experienceMessages.heading)}
273
        </h2>
274
        <p data-c-margin="bottom(1)">
275
          <FormattedMessage
276
            id="profile.experience.preamble"
277
            defaultMessage="Use the buttons below to add experiences you want to share with the manager. Experiences you have added in the past also appear below, and you can edit them to link them to skills required for this job when necessary."
278
            description="First section of text on the experience step of the Application Timeline."
279
          />
280
        </p>
281
        {/* Experience Modal Buttons */}
282
        <div data-c-grid="gutter(all, 1)">
283
          {Object.values(modalButtons).map((buttonProps) => {
284
            const { id, title, icon } = buttonProps;
285
            // TODO: Use one type of button for everything.
286
            return id === "experience_work" ? (
287
              <ModalButtonH2
288
                key={id}
289
                id={id}
290
                title={title}
291
                icon={icon}
292
                openModal={openModal}
293
              />
294
            ) : (
295
              <ModalButton
296
                key={id}
297
                id={id}
298
                title={title}
299
                icon={icon}
300
                openModal={openModal}
301
              />
302
            );
303
          })}
304
        </div>
305
        {/* Experience Accordion List */}
306
        {experiences && experiences.length > 0 ? (
307
          <div className="experience-list" data-c-margin="top(2)">
308
            <div data-c-accordion-group>
309
              {experiences.map((experience) => {
310
                const relevantSkills: ExperienceSkill[] = getExperienceSkillsOfExperience(
311
                  experienceSkills,
312
                  experience,
313
                );
314
                const handleEdit = () => editExperience(experience);
315
                const handleDelete = () => deleteExperience(experience);
316
                const errorAccordion = () => (
317
                  <div
318
                    data-c-background="gray(10)"
319
                    data-c-radius="rounded"
320
                    data-c-border="all(thin, solid, gray)"
321
                    data-c-margin="top(1)"
322
                    data-c-padding="all(1)"
323
                  >
324
                    <div data-c-align="base(center)">
325
                      <p data-c-color="stop">
326
                        {intl.formatMessage(
327
                          experienceMessages.errorRenderingExperience,
328
                        )}
329
                      </p>
330
                    </div>
331
                  </div>
332
                );
333
                return (
334
                  profileExperienceAccordion(
335
                    experience,
336
                    relevantSkills,
337
                    skillsById,
338
                    handleEdit,
339
                    handleDelete,
340
                  ) ?? errorAccordion()
341
                );
342
              })}
343
            </div>
344
          </div>
345
        ) : (
346
          <div
347
            data-c-background="gray(10)"
348
            data-c-radius="rounded"
349
            data-c-border="all(thin, solid, gray)"
350
            data-c-margin="top(2)"
351
            data-c-padding="all(1)"
352
          >
353
            <div data-c-align="base(center)">
354
              <p data-c-color="gray">
355
                <FormattedMessage
356
                  id="profile.experience.noExperiences"
357
                  defaultMessage="Looks like you don't have any experience added yet. Use the buttons above to add experience. Don't forget that experience will always be saved to your profile so that you can use it on future applications!"
358
                  description="Message displayed when application has no experiences."
359
                />
360
              </p>
361
            </div>
362
          </div>
363
        )}
364
      </div>
365
      <div data-c-dialog-overlay={isModalVisible.visible ? "active" : ""} />
366
      <ProfileEducationModal
367
        educationStatuses={educationStatuses}
368
        educationTypes={educationTypes}
369
        experienceEducation={experienceData as ExperienceEducation}
370
        experienceableId={experienceData?.experienceable_id ?? 0}
371
        experienceableType={
372
          experienceData?.experienceable_type ?? "application"
373
        }
374
        userSkills={userSkills}
375
        experienceSkills={experienceSkills}
376
        modalId={modalButtons.education.id}
377
        onModalCancel={closeModal}
378
        onModalConfirm={
379
          experienceData === null ? createExperience : updateExperience
380
        }
381
        parentElement={modalRoot}
382
        visible={
383
          isModalVisible.visible &&
384
          isModalVisible.id === modalButtons.education.id
385
        }
386
      />
387
      <ProfileWorkModal
388
        experienceWork={experienceData as ExperienceWork}
389
        experienceableId={experienceData?.experienceable_id ?? 0}
390
        experienceableType={
391
          experienceData?.experienceable_type ?? "application"
392
        }
393
        userSkills={userSkills}
394
        experienceSkills={experienceSkills}
395
        modalId={modalButtons.work.id}
396
        onModalCancel={closeModal}
397
        onModalConfirm={
398
          experienceData === null ? createExperience : updateExperience
399
        }
400
        parentElement={modalRoot}
401
        visible={
402
          isModalVisible.visible && isModalVisible.id === modalButtons.work.id
403
        }
404
      />
405
      <ProfileCommunityModal
406
        experienceCommunity={experienceData as ExperienceCommunity}
407
        experienceableId={experienceData?.experienceable_id ?? 0}
408
        experienceableType={
409
          experienceData?.experienceable_type ?? "application"
410
        }
411
        userSkills={userSkills}
412
        experienceSkills={experienceSkills}
413
        modalId={modalButtons.community.id}
414
        onModalCancel={closeModal}
415
        onModalConfirm={
416
          experienceData === null ? createExperience : updateExperience
417
        }
418
        parentElement={modalRoot}
419
        visible={
420
          isModalVisible.visible &&
421
          isModalVisible.id === modalButtons.community.id
422
        }
423
      />
424
      <ProfilePersonalModal
425
        experiencePersonal={experienceData as ExperiencePersonal}
426
        experienceableId={experienceData?.experienceable_id ?? 0}
427
        experienceableType={
428
          experienceData?.experienceable_type ?? "application"
429
        }
430
        userSkills={userSkills}
431
        experienceSkills={experienceSkills}
432
        modalId={modalButtons.personal.id}
433
        onModalCancel={closeModal}
434
        onModalConfirm={
435
          experienceData === null ? createExperience : updateExperience
436
        }
437
        parentElement={modalRoot}
438
        visible={
439
          isModalVisible.visible &&
440
          isModalVisible.id === modalButtons.personal.id
441
        }
442
      />
443
      <ProfileAwardModal
444
        experienceAward={experienceData as ExperienceAward}
445
        experienceableId={experienceData?.experienceable_id ?? 0}
446
        experienceableType={
447
          experienceData?.experienceable_type ?? "application"
448
        }
449
        userSkills={userSkills}
450
        experienceSkills={experienceSkills}
451
        modalId={modalButtons.award.id}
452
        onModalCancel={closeModal}
453
        onModalConfirm={
454
          experienceData === null ? createExperience : updateExperience
455
        }
456
        parentElement={modalRoot}
457
        recipientTypes={recipientTypes}
458
        recognitionTypes={recognitionTypes}
459
        visible={
460
          isModalVisible.visible && isModalVisible.id === modalButtons.award.id
461
        }
462
      />
463
    </>
464
  );
465
};
466
467
export default ProfileExperience;
468