Passed
Push — feature/skills-experience-tran... ( cd7b7e...d704a5 )
by Tristan
05:22
created

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

Complexity

Total Complexity 42
Complexity/F 0

Size

Lines of Code 458
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

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