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 { getFocusableElements, 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: (triggerRef: React.RefObject<HTMLButtonElement>) => 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
|
|
|
triggerRef: React.RefObject<HTMLButtonElement> | null; |
201
|
|
|
}>({ |
202
|
|
|
id: "", |
203
|
|
|
visible: false, |
204
|
|
|
triggerRef: null, |
205
|
|
|
}); |
206
|
|
|
|
207
|
|
|
const [experienceData, setExperienceData] = React.useState<Experience | null>( |
208
|
|
|
null, |
209
|
|
|
); |
210
|
|
|
|
211
|
|
|
const modalButtons = modalButtonProps(intl); |
212
|
|
|
|
213
|
|
|
const openModal = ( |
214
|
|
|
id: Experience["type"], |
215
|
|
|
triggerRef: React.RefObject<HTMLButtonElement> | null, |
216
|
|
|
): void => { |
217
|
|
|
setIsModalVisible({ id, visible: true, triggerRef }); |
218
|
|
|
}; |
219
|
|
|
|
220
|
|
|
const closeModal = (): void => { |
221
|
|
|
setExperienceData(null); |
222
|
|
|
if (isModalVisible.triggerRef?.current) { |
223
|
|
|
isModalVisible.triggerRef.current.focus(); |
224
|
|
|
} else { |
225
|
|
|
const focusableElements = getFocusableElements(); |
226
|
|
|
if (focusableElements.length > 0) { |
227
|
|
|
focusableElements[0].focus(); |
228
|
|
|
} |
229
|
|
|
} |
230
|
|
|
setIsModalVisible({ id: "", visible: false, triggerRef: null }); |
231
|
|
|
}; |
232
|
|
|
|
233
|
|
|
const updateExperience = (data: ExperienceSubmitData<Experience>) => |
234
|
|
|
handleUpdateExperience(data).then(closeModal); |
235
|
|
|
const createExperience = (data: ExperienceSubmitData<Experience>) => |
236
|
|
|
handleCreateExperience(data).then(closeModal); |
237
|
|
|
|
238
|
|
|
const editExperience = ( |
239
|
|
|
experience: Experience, |
240
|
|
|
triggerRef: React.RefObject<HTMLButtonElement> | null, |
241
|
|
|
): void => { |
242
|
|
|
setExperienceData(experience); |
243
|
|
|
setIsModalVisible({ id: experience.type, visible: true, triggerRef }); |
244
|
|
|
}; |
245
|
|
|
|
246
|
|
|
const deleteExperience = (experience: Experience): Promise<void> => |
247
|
|
|
handleDeleteExperience(experience.id, experience.type).then(closeModal); |
248
|
|
|
|
249
|
|
|
const skillsById = mapToObject(userSkills, getId); |
250
|
|
|
|
251
|
|
|
const modalRoot = document.getElementById("modal-root"); |
252
|
|
|
|
253
|
|
|
// Open modal for editing if URI has a hash. |
254
|
|
|
const uriHash = useUrlHash(); |
255
|
|
|
let experienceType: string; |
256
|
|
|
let experienceId: number; |
257
|
|
|
|
258
|
|
|
if (uriHash) { |
259
|
|
|
const uriHashFragments = uriHash.substring(1).split("_"); |
260
|
|
|
// Get experience type from first two fragments of URI hash. |
261
|
|
|
experienceType = `${uriHashFragments[0]}_${uriHashFragments[1]}`; |
262
|
|
|
// Get experience id from third fragment of URI hash. |
263
|
|
|
experienceId = Number(uriHashFragments[2]); |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
const experienceCurrent: Experience | undefined = experiences.find( |
267
|
|
|
(experience) => |
268
|
|
|
experience.type === experienceType && experience.id === experienceId, |
269
|
|
|
); |
270
|
|
|
|
271
|
|
|
useEffect(() => { |
272
|
|
|
if (uriHash && experienceCurrent) { |
273
|
|
|
toggleAccordion(uriHash.substring(1)); |
274
|
|
|
// Open edit experience modal. |
275
|
|
|
editExperience(experienceCurrent, null); |
276
|
|
|
} |
277
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps |
278
|
|
|
}, []); // useEffect should only run on mount and unmount. |
279
|
|
|
|
280
|
|
|
return ( |
281
|
|
|
<> |
282
|
|
|
{userSkills.length === 0 && ( |
283
|
|
|
<NoSkillsNotification applicantId={applicantId} /> |
284
|
|
|
)} |
285
|
|
|
<div> |
286
|
|
|
<h2 data-c-heading="h2" data-c-margin="bottom(1)"> |
287
|
|
|
{intl.formatMessage(experienceMessages.heading)} |
288
|
|
|
</h2> |
289
|
|
|
<p data-c-margin="bottom(1)"> |
290
|
|
|
<FormattedMessage |
291
|
|
|
id="profile.experience.preamble" |
292
|
|
|
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." |
293
|
|
|
description="First section of text on the experience step of the Application Timeline." |
294
|
|
|
/> |
295
|
|
|
</p> |
296
|
|
|
{/* Experience Modal Buttons */} |
297
|
|
|
<div data-c-grid="gutter(all, 1)"> |
298
|
|
|
{Object.values(modalButtons).map((buttonProps) => { |
299
|
|
|
const { id, title, icon } = buttonProps; |
300
|
|
|
return ( |
301
|
|
|
<ModalButton |
302
|
|
|
id={id} |
303
|
|
|
key={id} |
304
|
|
|
title={title} |
305
|
|
|
icon={icon} |
306
|
|
|
openModal={openModal} |
307
|
|
|
/> |
308
|
|
|
); |
309
|
|
|
})} |
310
|
|
|
</div> |
311
|
|
|
{/* Experience Accordion List */} |
312
|
|
|
{experiences && experiences.length > 0 ? ( |
313
|
|
|
<div className="experience-list" data-c-margin="top(2)"> |
314
|
|
|
<div data-c-accordion-group> |
315
|
|
|
{experiences.map((experience) => { |
316
|
|
|
const relevantSkills: ExperienceSkill[] = getExperienceSkillsOfExperience( |
317
|
|
|
experienceSkills, |
318
|
|
|
experience, |
319
|
|
|
); |
320
|
|
|
const handleEdit = ( |
321
|
|
|
triggerRef: React.RefObject<HTMLButtonElement>, |
322
|
|
|
) => editExperience(experience, triggerRef); |
323
|
|
|
const handleDelete = () => deleteExperience(experience); |
324
|
|
|
const errorAccordion = () => ( |
325
|
|
|
<div |
326
|
|
|
data-c-background="gray(10)" |
327
|
|
|
data-c-radius="rounded" |
328
|
|
|
data-c-border="all(thin, solid, gray)" |
329
|
|
|
data-c-margin="top(1)" |
330
|
|
|
data-c-padding="all(1)" |
331
|
|
|
> |
332
|
|
|
<div data-c-align="base(center)"> |
333
|
|
|
<p data-c-color="stop"> |
334
|
|
|
{intl.formatMessage( |
335
|
|
|
experienceMessages.errorRenderingExperience, |
336
|
|
|
)} |
337
|
|
|
</p> |
338
|
|
|
</div> |
339
|
|
|
</div> |
340
|
|
|
); |
341
|
|
|
return ( |
342
|
|
|
profileExperienceAccordion( |
343
|
|
|
experience, |
344
|
|
|
relevantSkills, |
345
|
|
|
skillsById, |
346
|
|
|
handleEdit, |
347
|
|
|
handleDelete, |
348
|
|
|
) ?? errorAccordion() |
349
|
|
|
); |
350
|
|
|
})} |
351
|
|
|
</div> |
352
|
|
|
</div> |
353
|
|
|
) : ( |
354
|
|
|
<div |
355
|
|
|
data-c-background="gray(10)" |
356
|
|
|
data-c-radius="rounded" |
357
|
|
|
data-c-border="all(thin, solid, gray)" |
358
|
|
|
data-c-margin="top(2)" |
359
|
|
|
data-c-padding="all(1)" |
360
|
|
|
> |
361
|
|
|
<div data-c-align="base(center)"> |
362
|
|
|
<p data-c-color="gray"> |
363
|
|
|
<FormattedMessage |
364
|
|
|
id="profile.experience.noExperiences" |
365
|
|
|
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!" |
366
|
|
|
description="Message displayed when application has no experiences." |
367
|
|
|
/> |
368
|
|
|
</p> |
369
|
|
|
</div> |
370
|
|
|
</div> |
371
|
|
|
)} |
372
|
|
|
</div> |
373
|
|
|
<div data-c-dialog-overlay={isModalVisible.visible ? "active" : ""} /> |
374
|
|
|
<ProfileEducationModal |
375
|
|
|
educationStatuses={educationStatuses} |
376
|
|
|
educationTypes={educationTypes} |
377
|
|
|
experienceEducation={experienceData as ExperienceEducation} |
378
|
|
|
experienceableId={experienceData?.experienceable_id ?? 0} |
379
|
|
|
experienceableType={ |
380
|
|
|
experienceData?.experienceable_type ?? "application" |
381
|
|
|
} |
382
|
|
|
userSkills={userSkills} |
383
|
|
|
experienceSkills={experienceSkills} |
384
|
|
|
modalId={modalButtons.education.id} |
385
|
|
|
onModalCancel={closeModal} |
386
|
|
|
onModalConfirm={ |
387
|
|
|
experienceData === null ? createExperience : updateExperience |
388
|
|
|
} |
389
|
|
|
parentElement={modalRoot} |
390
|
|
|
visible={ |
391
|
|
|
isModalVisible.visible && |
392
|
|
|
isModalVisible.id === modalButtons.education.id |
393
|
|
|
} |
394
|
|
|
/> |
395
|
|
|
<ProfileWorkModal |
396
|
|
|
experienceWork={experienceData as ExperienceWork} |
397
|
|
|
experienceableId={experienceData?.experienceable_id ?? 0} |
398
|
|
|
experienceableType={ |
399
|
|
|
experienceData?.experienceable_type ?? "application" |
400
|
|
|
} |
401
|
|
|
userSkills={userSkills} |
402
|
|
|
experienceSkills={experienceSkills} |
403
|
|
|
modalId={modalButtons.work.id} |
404
|
|
|
onModalCancel={closeModal} |
405
|
|
|
onModalConfirm={ |
406
|
|
|
experienceData === null ? createExperience : updateExperience |
407
|
|
|
} |
408
|
|
|
parentElement={modalRoot} |
409
|
|
|
visible={ |
410
|
|
|
isModalVisible.visible && isModalVisible.id === modalButtons.work.id |
411
|
|
|
} |
412
|
|
|
/> |
413
|
|
|
<ProfileCommunityModal |
414
|
|
|
experienceCommunity={experienceData as ExperienceCommunity} |
415
|
|
|
experienceableId={experienceData?.experienceable_id ?? 0} |
416
|
|
|
experienceableType={ |
417
|
|
|
experienceData?.experienceable_type ?? "application" |
418
|
|
|
} |
419
|
|
|
userSkills={userSkills} |
420
|
|
|
experienceSkills={experienceSkills} |
421
|
|
|
modalId={modalButtons.community.id} |
422
|
|
|
onModalCancel={closeModal} |
423
|
|
|
onModalConfirm={ |
424
|
|
|
experienceData === null ? createExperience : updateExperience |
425
|
|
|
} |
426
|
|
|
parentElement={modalRoot} |
427
|
|
|
visible={ |
428
|
|
|
isModalVisible.visible && |
429
|
|
|
isModalVisible.id === modalButtons.community.id |
430
|
|
|
} |
431
|
|
|
/> |
432
|
|
|
<ProfilePersonalModal |
433
|
|
|
experiencePersonal={experienceData as ExperiencePersonal} |
434
|
|
|
experienceableId={experienceData?.experienceable_id ?? 0} |
435
|
|
|
experienceableType={ |
436
|
|
|
experienceData?.experienceable_type ?? "application" |
437
|
|
|
} |
438
|
|
|
userSkills={userSkills} |
439
|
|
|
experienceSkills={experienceSkills} |
440
|
|
|
modalId={modalButtons.personal.id} |
441
|
|
|
onModalCancel={closeModal} |
442
|
|
|
onModalConfirm={ |
443
|
|
|
experienceData === null ? createExperience : updateExperience |
444
|
|
|
} |
445
|
|
|
parentElement={modalRoot} |
446
|
|
|
visible={ |
447
|
|
|
isModalVisible.visible && |
448
|
|
|
isModalVisible.id === modalButtons.personal.id |
449
|
|
|
} |
450
|
|
|
/> |
451
|
|
|
<ProfileAwardModal |
452
|
|
|
experienceAward={experienceData as ExperienceAward} |
453
|
|
|
experienceableId={experienceData?.experienceable_id ?? 0} |
454
|
|
|
experienceableType={ |
455
|
|
|
experienceData?.experienceable_type ?? "application" |
456
|
|
|
} |
457
|
|
|
userSkills={userSkills} |
458
|
|
|
experienceSkills={experienceSkills} |
459
|
|
|
modalId={modalButtons.award.id} |
460
|
|
|
onModalCancel={closeModal} |
461
|
|
|
onModalConfirm={ |
462
|
|
|
experienceData === null ? createExperience : updateExperience |
463
|
|
|
} |
464
|
|
|
parentElement={modalRoot} |
465
|
|
|
recipientTypes={recipientTypes} |
466
|
|
|
recognitionTypes={recognitionTypes} |
467
|
|
|
visible={ |
468
|
|
|
isModalVisible.visible && isModalVisible.id === modalButtons.award.id |
469
|
|
|
} |
470
|
|
|
/> |
471
|
|
|
</> |
472
|
|
|
); |
473
|
|
|
}; |
474
|
|
|
|
475
|
|
|
export default ProfileExperience; |
476
|
|
|
|