1
|
|
|
/* eslint-disable camelcase */ |
2
|
|
|
import React, { useState, useEffect, useRef } from "react"; |
3
|
|
|
import { |
4
|
|
|
FormattedMessage, |
5
|
|
|
useIntl, |
6
|
|
|
IntlShape, |
7
|
|
|
defineMessage, |
8
|
|
|
} from "react-intl"; |
9
|
|
|
import { |
10
|
|
|
Skill, |
11
|
|
|
ExperienceEducation, |
12
|
|
|
ExperienceWork, |
13
|
|
|
ExperienceCommunity, |
14
|
|
|
Experience, |
15
|
|
|
ExperiencePersonal, |
16
|
|
|
ExperienceAward, |
17
|
|
|
ExperienceSkill, |
18
|
|
|
Criteria, |
19
|
|
|
} from "../../../models/types"; |
20
|
|
|
import { localizeFieldNonNull, getLocale } from "../../../helpers/localize"; |
21
|
|
|
import { |
22
|
|
|
SkillTypeId, |
23
|
|
|
CriteriaTypeId, |
24
|
|
|
getKeyByValue, |
25
|
|
|
ClassificationId, |
26
|
|
|
// ClassificationId, |
27
|
|
|
} from "../../../models/lookupConstants"; |
28
|
|
|
import EducationExperienceModal, { |
29
|
|
|
messages as educationMessages, |
30
|
|
|
EducationType, |
31
|
|
|
EducationStatus, |
32
|
|
|
EducationExperienceSubmitData, |
33
|
|
|
} from "../ExperienceModals/EducationExperienceModal"; |
34
|
|
|
|
35
|
|
|
import WorkExperienceModal, { |
36
|
|
|
messages as workMessages, |
37
|
|
|
WorkExperienceSubmitData, |
38
|
|
|
} from "../ExperienceModals/WorkExperienceModal"; |
39
|
|
|
import CommunityExperienceModal, { |
40
|
|
|
messages as communityMessages, |
41
|
|
|
CommunityExperienceSubmitData, |
42
|
|
|
} from "../ExperienceModals/CommunityExperienceModal"; |
43
|
|
|
import PersonalExperienceModal, { |
44
|
|
|
messages as personalMessages, |
45
|
|
|
PersonalExperienceSubmitData, |
46
|
|
|
} from "../ExperienceModals/PersonalExperienceModal"; |
47
|
|
|
import AwardExperienceModal, { |
48
|
|
|
messages as awardMessages, |
49
|
|
|
AwardRecipientType, |
50
|
|
|
AwardRecognitionType, |
51
|
|
|
AwardExperienceSubmitData, |
52
|
|
|
} from "../ExperienceModals/AwardExperienceModal"; |
53
|
|
|
import { ExperienceEducationAccordion } from "../ExperienceAccordions/ExperienceEducationAccordion"; |
54
|
|
|
import { ExperienceWorkAccordion } from "../ExperienceAccordions/ExperienceWorkAccordion"; |
55
|
|
|
import { ExperienceCommunityAccordion } from "../ExperienceAccordions/ExperienceCommunityAccordion"; |
56
|
|
|
import { ExperiencePersonalAccordion } from "../ExperienceAccordions/ExperiencePersonalAccordion"; |
57
|
|
|
import { ExperienceAwardAccordion } from "../ExperienceAccordions/ExperienceAwardAccordion"; |
58
|
|
|
import { |
59
|
|
|
getSkillOfCriteria, |
60
|
|
|
getSkillsOfExperience, |
61
|
|
|
getDisconnectedRequiredSkills, |
62
|
|
|
} from "../helpers"; |
63
|
|
|
import { navigationMessages, experienceMessages } from "../applicationMessages"; |
64
|
|
|
import { notEmpty, removeDuplicatesById } from "../../../helpers/queries"; |
65
|
|
|
import { RootState } from "../../../store/store"; |
66
|
|
|
import { focusOnElement } from "../../../helpers/forms"; |
67
|
|
|
|
68
|
|
|
export function modalButtonProps( |
69
|
|
|
intl: IntlShape, |
70
|
|
|
): { [key: string]: { id: Experience["type"]; title: string; icon: string } } { |
71
|
|
|
return { |
72
|
|
|
education: { |
73
|
|
|
id: "experience_education", |
74
|
|
|
title: intl.formatMessage(educationMessages.modalTitle), |
75
|
|
|
icon: "fas fa-book", |
76
|
|
|
}, |
77
|
|
|
work: { |
78
|
|
|
id: "experience_work", |
79
|
|
|
title: intl.formatMessage(workMessages.modalTitle), |
80
|
|
|
icon: "fas fa-briefcase", |
81
|
|
|
}, |
82
|
|
|
community: { |
83
|
|
|
id: "experience_community", |
84
|
|
|
title: intl.formatMessage(communityMessages.modalTitle), |
85
|
|
|
icon: "fas fa-people-carry", |
86
|
|
|
}, |
87
|
|
|
personal: { |
88
|
|
|
id: "experience_personal", |
89
|
|
|
title: intl.formatMessage(personalMessages.modalTitle), |
90
|
|
|
icon: "fas fa-mountain", |
91
|
|
|
}, |
92
|
|
|
award: { |
93
|
|
|
id: "experience_award", |
94
|
|
|
title: intl.formatMessage(awardMessages.modalTitle), |
95
|
|
|
icon: "fas fa-trophy", |
96
|
|
|
}, |
97
|
|
|
}; |
98
|
|
|
} |
99
|
|
|
|
100
|
|
|
export const ModalButton: React.FunctionComponent<{ |
101
|
|
|
id: Experience["type"]; |
102
|
|
|
title: string; |
103
|
|
|
icon: string; |
104
|
|
|
openModal: (id: Experience["type"]) => void; |
105
|
|
|
}> = ({ id, title, icon, openModal }) => { |
106
|
|
|
return ( |
107
|
|
|
<div key={id} data-c-grid-item="base(1of2) tp(1of3) tl(1of5)"> |
108
|
|
|
<button |
109
|
|
|
className="application-experience-trigger" |
110
|
|
|
data-c-card |
111
|
|
|
data-c-background="c1(100)" |
112
|
|
|
data-c-radius="rounded" |
113
|
|
|
title={title} |
114
|
|
|
data-c-dialog-id={id} |
115
|
|
|
data-c-dialog-action="open" |
116
|
|
|
type="button" |
117
|
|
|
onClick={(): void => openModal(id)} |
118
|
|
|
> |
119
|
|
|
<i className={icon} aria-hidden="true" /> |
120
|
|
|
<span data-c-font-size="regular" data-c-font-weight="bold"> |
121
|
|
|
{title} |
122
|
|
|
</span> |
123
|
|
|
</button> |
124
|
|
|
</div> |
125
|
|
|
); |
126
|
|
|
}; |
127
|
|
|
|
128
|
|
|
const applicationExperienceAccordion = ( |
129
|
|
|
experience: Experience, |
130
|
|
|
irrelevantSkillCount: number, |
131
|
|
|
relevantSkills: ExperienceSkill[], |
132
|
|
|
skills: Skill[], |
133
|
|
|
handleEdit: () => void, |
134
|
|
|
handleDelete: () => Promise<void>, |
135
|
|
|
): React.ReactElement | null => { |
136
|
|
|
switch (experience.type) { |
137
|
|
|
case "experience_education": |
138
|
|
|
return ( |
139
|
|
|
<ExperienceEducationAccordion |
140
|
|
|
key={`${experience.id}-${experience.type}`} |
141
|
|
|
experience={experience} |
142
|
|
|
handleDelete={handleDelete} |
143
|
|
|
handleEdit={handleEdit} |
144
|
|
|
irrelevantSkillCount={irrelevantSkillCount} |
145
|
|
|
relevantSkills={relevantSkills} |
146
|
|
|
skills={skills} |
147
|
|
|
showButtons |
148
|
|
|
showSkillDetails |
149
|
|
|
/> |
150
|
|
|
); |
151
|
|
|
case "experience_work": |
152
|
|
|
return ( |
153
|
|
|
<ExperienceWorkAccordion |
154
|
|
|
key={`${experience.id}-${experience.type}`} |
155
|
|
|
experience={experience} |
156
|
|
|
handleDelete={handleDelete} |
157
|
|
|
handleEdit={handleEdit} |
158
|
|
|
irrelevantSkillCount={irrelevantSkillCount} |
159
|
|
|
relevantSkills={relevantSkills} |
160
|
|
|
skills={skills} |
161
|
|
|
showButtons |
162
|
|
|
showSkillDetails |
163
|
|
|
/> |
164
|
|
|
); |
165
|
|
|
case "experience_community": |
166
|
|
|
return ( |
167
|
|
|
<ExperienceCommunityAccordion |
168
|
|
|
key={`${experience.id}-${experience.type}`} |
169
|
|
|
experience={experience} |
170
|
|
|
handleDelete={handleDelete} |
171
|
|
|
handleEdit={handleEdit} |
172
|
|
|
irrelevantSkillCount={irrelevantSkillCount} |
173
|
|
|
relevantSkills={relevantSkills} |
174
|
|
|
skills={skills} |
175
|
|
|
showButtons |
176
|
|
|
showSkillDetails |
177
|
|
|
/> |
178
|
|
|
); |
179
|
|
|
case "experience_personal": |
180
|
|
|
return ( |
181
|
|
|
<ExperiencePersonalAccordion |
182
|
|
|
key={`${experience.id}-${experience.type}`} |
183
|
|
|
experience={experience} |
184
|
|
|
handleEdit={handleEdit} |
185
|
|
|
handleDelete={handleDelete} |
186
|
|
|
irrelevantSkillCount={irrelevantSkillCount} |
187
|
|
|
relevantSkills={relevantSkills} |
188
|
|
|
skills={skills} |
189
|
|
|
showButtons |
190
|
|
|
showSkillDetails |
191
|
|
|
/> |
192
|
|
|
); |
193
|
|
|
case "experience_award": |
194
|
|
|
return ( |
195
|
|
|
<ExperienceAwardAccordion |
196
|
|
|
key={`${experience.id}-${experience.type}`} |
197
|
|
|
experience={experience} |
198
|
|
|
handleDelete={handleDelete} |
199
|
|
|
handleEdit={handleEdit} |
200
|
|
|
irrelevantSkillCount={irrelevantSkillCount} |
201
|
|
|
relevantSkills={relevantSkills} |
202
|
|
|
skills={skills} |
203
|
|
|
showButtons |
204
|
|
|
showSkillDetails |
205
|
|
|
/> |
206
|
|
|
); |
207
|
|
|
default: |
208
|
|
|
return null; |
209
|
|
|
} |
210
|
|
|
}; |
211
|
|
|
|
212
|
|
|
export type ExperienceSubmitData = |
213
|
|
|
| EducationExperienceSubmitData |
214
|
|
|
| WorkExperienceSubmitData |
215
|
|
|
| CommunityExperienceSubmitData |
216
|
|
|
| PersonalExperienceSubmitData |
217
|
|
|
| AwardExperienceSubmitData; |
218
|
|
|
|
219
|
|
|
interface ExperienceProps { |
220
|
|
|
assetSkills: Skill[]; |
221
|
|
|
disconnectedRequiredSkills: Skill[]; |
222
|
|
|
educationStatuses: EducationStatus[]; |
223
|
|
|
educationTypes: EducationType[]; |
224
|
|
|
hasError?: boolean; |
225
|
|
|
essentialSkills: Skill[]; |
226
|
|
|
experiences: Experience[]; |
227
|
|
|
experienceSkills: ExperienceSkill[]; |
228
|
|
|
jobId: number; |
229
|
|
|
jobClassificationId: number | null; |
230
|
|
|
jobEducationRequirements: string | null; |
231
|
|
|
recipientTypes: AwardRecipientType[]; |
232
|
|
|
recognitionTypes: AwardRecognitionType[]; |
233
|
|
|
skills: Skill[]; |
234
|
|
|
handleDeleteExperience: ( |
235
|
|
|
id: number, |
236
|
|
|
type: Experience["type"], |
237
|
|
|
) => Promise<void>; |
238
|
|
|
handleSubmitExperience: (data: ExperienceSubmitData) => Promise<void>; |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
export const MyExperience: React.FunctionComponent<ExperienceProps> = ({ |
242
|
|
|
assetSkills, |
243
|
|
|
disconnectedRequiredSkills, |
244
|
|
|
hasError, |
245
|
|
|
educationStatuses, |
246
|
|
|
educationTypes, |
247
|
|
|
essentialSkills, |
248
|
|
|
experiences, |
249
|
|
|
experienceSkills, |
250
|
|
|
jobId, |
251
|
|
|
jobClassificationId, |
252
|
|
|
jobEducationRequirements, |
253
|
|
|
recipientTypes, |
254
|
|
|
recognitionTypes, |
255
|
|
|
skills, |
256
|
|
|
handleSubmitExperience, |
257
|
|
|
handleDeleteExperience, |
258
|
|
|
}) => { |
259
|
|
|
const intl = useIntl(); |
260
|
|
|
const locale = getLocale(intl.locale); |
261
|
|
|
|
262
|
|
|
const jobClassification = |
263
|
|
|
jobClassificationId !== null |
264
|
|
|
? getKeyByValue(ClassificationId, jobClassificationId) |
265
|
|
|
: ""; |
266
|
|
|
|
267
|
|
|
const [experienceData, setExperienceData] = useState< |
268
|
|
|
| (Experience & { |
269
|
|
|
savedOptionalSkills: Skill[]; |
270
|
|
|
savedRequiredSkills: Skill[]; |
271
|
|
|
}) |
272
|
|
|
| null |
273
|
|
|
>(null); |
274
|
|
|
|
275
|
|
|
const [isModalVisible, setIsModalVisible] = useState<{ |
276
|
|
|
id: Experience["type"] | ""; |
277
|
|
|
visible: boolean; |
278
|
|
|
}>({ |
279
|
|
|
id: "", |
280
|
|
|
visible: false, |
281
|
|
|
}); |
282
|
|
|
|
283
|
|
|
const openModal = (id: Experience["type"]): void => { |
284
|
|
|
setIsModalVisible({ id, visible: true }); |
285
|
|
|
}; |
286
|
|
|
|
287
|
|
|
const closeModal = (): void => { |
288
|
|
|
setExperienceData(null); |
289
|
|
|
setIsModalVisible({ id: "", visible: false }); |
290
|
|
|
}; |
291
|
|
|
|
292
|
|
|
const submitExperience = (data) => |
293
|
|
|
handleSubmitExperience(data).then(closeModal); |
294
|
|
|
|
295
|
|
|
const editExperience = ( |
296
|
|
|
experience: Experience, |
297
|
|
|
savedOptionalSkills: Skill[], |
298
|
|
|
savedRequiredSkills: Skill[], |
299
|
|
|
): void => { |
300
|
|
|
setExperienceData({ |
301
|
|
|
...experience, |
302
|
|
|
savedOptionalSkills, |
303
|
|
|
savedRequiredSkills, |
304
|
|
|
}); |
305
|
|
|
setIsModalVisible({ id: experience.type, visible: true }); |
306
|
|
|
}; |
307
|
|
|
|
308
|
|
|
const deleteExperience = (experience: Experience): Promise<void> => |
309
|
|
|
handleDeleteExperience(experience.id, experience.type).then(closeModal); |
310
|
|
|
|
311
|
|
|
const softSkills = removeDuplicatesById( |
312
|
|
|
[...assetSkills, ...essentialSkills].filter( |
313
|
|
|
(skill) => skill.skill_type_id === SkillTypeId.Soft, |
314
|
|
|
), |
315
|
|
|
); |
316
|
|
|
|
317
|
|
|
const modalButtons = modalButtonProps(intl); |
318
|
|
|
const modalRoot = document.getElementById("modal-root"); |
319
|
|
|
|
320
|
|
|
return ( |
321
|
|
|
<> |
322
|
|
|
<div data-c-container="medium"> |
323
|
|
|
<h2 data-c-heading="h2" data-c-margin="top(3) bottom(1)"> |
324
|
|
|
{intl.formatMessage(experienceMessages.heading)} |
325
|
|
|
</h2> |
326
|
|
|
<p data-c-margin="bottom(1)"> |
327
|
|
|
<FormattedMessage |
328
|
|
|
id="application.experience.preamble" |
329
|
|
|
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." |
330
|
|
|
description="First section of text on the experience step of the Application Timeline." |
331
|
|
|
/> |
332
|
|
|
</p> |
333
|
|
|
|
334
|
|
|
<div data-c-grid="gutter(all, 1)"> |
335
|
|
|
{essentialSkills.length > 0 && ( |
336
|
|
|
<div data-c-grid-item="tl(1of2)"> |
337
|
|
|
<p data-c-margin="bottom(.5)"> |
338
|
|
|
<FormattedMessage |
339
|
|
|
id="application.experience.essentialSkillsListIntro" |
340
|
|
|
description="Text before the list of essential skills on the experience step of the Application Timeline." |
341
|
|
|
defaultMessage="This job <span>requires</span> the following skills:" |
342
|
|
|
values={{ |
343
|
|
|
span: (chunks): React.ReactElement => ( |
344
|
|
|
<span data-c-font-weight="bold" data-c-color="c2"> |
345
|
|
|
{chunks} |
346
|
|
|
</span> |
347
|
|
|
), |
348
|
|
|
}} |
349
|
|
|
/> |
350
|
|
|
</p> |
351
|
|
|
<ul data-c-margin="bottom(1)"> |
352
|
|
|
{essentialSkills.map((skill) => ( |
353
|
|
|
<li key={skill.id}> |
354
|
|
|
{localizeFieldNonNull(locale, skill, "name")} |
355
|
|
|
</li> |
356
|
|
|
))} |
357
|
|
|
</ul> |
358
|
|
|
</div> |
359
|
|
|
)} |
360
|
|
|
{assetSkills.length > 0 && ( |
361
|
|
|
<div data-c-grid-item="tl(1of2)"> |
362
|
|
|
<p data-c-margin="bottom(.5)"> |
363
|
|
|
<FormattedMessage |
364
|
|
|
id="application.experience.assetSkillsListIntro" |
365
|
|
|
defaultMessage="These skills are beneficial, but not required:" |
366
|
|
|
description="Text before the list of asset skills on the experience step of the Application Timeline." |
367
|
|
|
/> |
368
|
|
|
</p> |
369
|
|
|
<ul data-c-margin="bottom(1)"> |
370
|
|
|
{assetSkills.map((skill) => ( |
371
|
|
|
<li key={skill.id}> |
372
|
|
|
{localizeFieldNonNull(locale, skill, "name")} |
373
|
|
|
</li> |
374
|
|
|
))} |
375
|
|
|
</ul> |
376
|
|
|
</div> |
377
|
|
|
)} |
378
|
|
|
</div> |
379
|
|
|
<p data-c-color="gray" data-c-margin="bottom(2)"> |
380
|
|
|
<FormattedMessage |
381
|
|
|
id="application.experience.softSkillsList" |
382
|
|
|
defaultMessage="Don't forget, {skill} will be evaluated later in the hiring process." |
383
|
|
|
description="List of soft skills that will be evaluated later." |
384
|
|
|
values={{ |
385
|
|
|
skill: ( |
386
|
|
|
<> |
387
|
|
|
{softSkills.map((skill, index) => { |
388
|
|
|
const and = " and "; |
389
|
|
|
const lastElement = index === softSkills.length - 1; |
390
|
|
|
return ( |
391
|
|
|
<React.Fragment key={skill.id}> |
392
|
|
|
{lastElement && softSkills.length > 1 && and} |
393
|
|
|
<span key={skill.id} data-c-font-weight="bold"> |
394
|
|
|
{localizeFieldNonNull(locale, skill, "name")} |
395
|
|
|
</span> |
396
|
|
|
{!lastElement && softSkills.length > 2 && ", "} |
397
|
|
|
</React.Fragment> |
398
|
|
|
); |
399
|
|
|
})} |
400
|
|
|
</> |
401
|
|
|
), |
402
|
|
|
}} |
403
|
|
|
/> |
404
|
|
|
</p> |
405
|
|
|
{hasError && ( |
406
|
|
|
<div |
407
|
|
|
// This alert message needs the tabindex attribute for it to be focusable. |
408
|
|
|
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex |
409
|
|
|
tabIndex={0} |
410
|
|
|
data-c-alert="error" |
411
|
|
|
data-c-radius="rounded" |
412
|
|
|
role="alert" |
413
|
|
|
data-c-margin="bottom(1)" |
414
|
|
|
id="experience-step-form-error" |
415
|
|
|
style={{ display: "block" }} |
416
|
|
|
> |
417
|
|
|
<div data-c-padding="all(.5)"> |
418
|
|
|
<p data-c-alignment="base(left)"> |
419
|
|
|
<FormattedMessage |
420
|
|
|
id="application.experience.errorMessage" |
421
|
|
|
defaultMessage="To continue, please connect the following required skill(s) to an experience:" |
422
|
|
|
description="Error message displayed to user when experience step validation fails" |
423
|
|
|
/> |
424
|
|
|
{` `} |
425
|
|
|
{disconnectedRequiredSkills.map((skill, index) => { |
426
|
|
|
const and = " and "; |
427
|
|
|
const lastElement = |
428
|
|
|
index === disconnectedRequiredSkills.length - 1; |
429
|
|
|
return ( |
430
|
|
|
<React.Fragment key={skill.id}> |
431
|
|
|
{lastElement && |
432
|
|
|
disconnectedRequiredSkills.length > 1 && |
433
|
|
|
and} |
434
|
|
|
<span key={skill.id} data-c-font-weight="bold"> |
435
|
|
|
{localizeFieldNonNull(locale, skill, "name")} |
436
|
|
|
</span> |
437
|
|
|
{!lastElement && |
438
|
|
|
disconnectedRequiredSkills.length > 2 && |
439
|
|
|
", "} |
440
|
|
|
</React.Fragment> |
441
|
|
|
); |
442
|
|
|
})} |
443
|
|
|
</p> |
444
|
|
|
</div> |
445
|
|
|
</div> |
446
|
|
|
)} |
447
|
|
|
{/* Experience Modal Buttons */} |
448
|
|
|
<div data-c-grid="gutter(all, 1)"> |
449
|
|
|
{Object.values(modalButtons).map((buttonProps) => { |
450
|
|
|
const { id, title, icon } = buttonProps; |
451
|
|
|
return ( |
452
|
|
|
<ModalButton |
453
|
|
|
key={id} |
454
|
|
|
id={id} |
455
|
|
|
title={title} |
456
|
|
|
icon={icon} |
457
|
|
|
openModal={openModal} |
458
|
|
|
/> |
459
|
|
|
); |
460
|
|
|
})} |
461
|
|
|
</div> |
462
|
|
|
{/* Experience Accordion List */} |
463
|
|
|
{experiences && experiences.length > 0 ? ( |
464
|
|
|
<div className="experience-list" data-c-margin="top(2)"> |
465
|
|
|
<div data-c-accordion-group> |
466
|
|
|
{experiences.map((experience) => { |
467
|
|
|
const savedOptionalSkills = getSkillsOfExperience( |
468
|
|
|
experienceSkills, |
469
|
|
|
experience, |
470
|
|
|
assetSkills, |
471
|
|
|
); |
472
|
|
|
const savedRequiredSkills = getSkillsOfExperience( |
473
|
|
|
experienceSkills, |
474
|
|
|
experience, |
475
|
|
|
essentialSkills, |
476
|
|
|
); |
477
|
|
|
const relevantSkills: ExperienceSkill[] = savedRequiredSkills |
478
|
|
|
.map((skill) => { |
479
|
|
|
return experienceSkills.find( |
480
|
|
|
({ experience_id, experience_type, skill_id }) => |
481
|
|
|
experience_id === experience.id && |
482
|
|
|
skill_id === skill.id && |
483
|
|
|
experience_type === experience.type, |
484
|
|
|
); |
485
|
|
|
}) |
486
|
|
|
.filter(notEmpty); |
487
|
|
|
|
488
|
|
|
const handleEdit = () => |
489
|
|
|
editExperience( |
490
|
|
|
experience, |
491
|
|
|
savedOptionalSkills, |
492
|
|
|
savedRequiredSkills, |
493
|
|
|
); |
494
|
|
|
const handleDelete = () => deleteExperience(experience); |
495
|
|
|
|
496
|
|
|
const errorAccordion = () => ( |
497
|
|
|
<div |
498
|
|
|
data-c-background="gray(10)" |
499
|
|
|
data-c-radius="rounded" |
500
|
|
|
data-c-border="all(thin, solid, gray)" |
501
|
|
|
data-c-margin="top(1)" |
502
|
|
|
data-c-padding="all(1)" |
503
|
|
|
> |
504
|
|
|
<div data-c-align="base(center)"> |
505
|
|
|
<p data-c-color="stop"> |
506
|
|
|
{intl.formatMessage( |
507
|
|
|
experienceMessages.errorRenderingExperience, |
508
|
|
|
)} |
509
|
|
|
</p> |
510
|
|
|
</div> |
511
|
|
|
</div> |
512
|
|
|
); |
513
|
|
|
|
514
|
|
|
// Number of skills attached to Experience but are not part of the jobs skill criteria. |
515
|
|
|
const irrelevantSkillCount = |
516
|
|
|
experienceSkills.filter( |
517
|
|
|
(experienceSkill) => |
518
|
|
|
experienceSkill.experience_id === experience.id && |
519
|
|
|
experienceSkill.experience_type === experience.type, |
520
|
|
|
).length - |
521
|
|
|
(savedOptionalSkills.length + savedRequiredSkills.length); |
522
|
|
|
|
523
|
|
|
return ( |
524
|
|
|
applicationExperienceAccordion( |
525
|
|
|
experience, |
526
|
|
|
irrelevantSkillCount, |
527
|
|
|
relevantSkills, |
528
|
|
|
skills, |
529
|
|
|
handleEdit, |
530
|
|
|
handleDelete, |
531
|
|
|
) ?? errorAccordion() |
532
|
|
|
); |
533
|
|
|
})} |
534
|
|
|
</div> |
535
|
|
|
</div> |
536
|
|
|
) : ( |
537
|
|
|
<div |
538
|
|
|
data-c-background="gray(10)" |
539
|
|
|
data-c-radius="rounded" |
540
|
|
|
data-c-border="all(thin, solid, gray)" |
541
|
|
|
data-c-margin="top(2)" |
542
|
|
|
data-c-padding="all(1)" |
543
|
|
|
> |
544
|
|
|
<div data-c-align="base(center)"> |
545
|
|
|
<p data-c-color="gray"> |
546
|
|
|
<FormattedMessage |
547
|
|
|
id="application.experience.noExperiences" |
548
|
|
|
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!" |
549
|
|
|
description="Message displayed when application has no experiences." |
550
|
|
|
/> |
551
|
|
|
</p> |
552
|
|
|
</div> |
553
|
|
|
</div> |
554
|
|
|
)} |
555
|
|
|
{disconnectedRequiredSkills && disconnectedRequiredSkills.length > 0 && ( |
556
|
|
|
<p data-c-color="stop" data-c-margin="top(2)"> |
557
|
|
|
<FormattedMessage |
558
|
|
|
id="application.experience.unconnectedSkills" |
559
|
|
|
defaultMessage="The following required skill(s) are not connected to your experience:" |
560
|
|
|
description="Message showing list of required skills that are not connected to a experience." |
561
|
|
|
/>{" "} |
562
|
|
|
{disconnectedRequiredSkills.map((skill) => ( |
563
|
|
|
<React.Fragment key={skill.id}> |
564
|
|
|
<span |
565
|
|
|
data-c-tag="stop" |
566
|
|
|
data-c-radius="pill" |
567
|
|
|
data-c-font-size="small" |
568
|
|
|
> |
569
|
|
|
{localizeFieldNonNull(locale, skill, "name")} |
570
|
|
|
</span>{" "} |
571
|
|
|
</React.Fragment> |
572
|
|
|
))} |
573
|
|
|
</p> |
574
|
|
|
)} |
575
|
|
|
</div> |
576
|
|
|
|
577
|
|
|
<div data-c-dialog-overlay={isModalVisible.visible ? "active" : ""} /> |
578
|
|
|
<EducationExperienceModal |
579
|
|
|
educationStatuses={educationStatuses} |
580
|
|
|
educationTypes={educationTypes} |
581
|
|
|
experienceEducation={experienceData as ExperienceEducation} |
582
|
|
|
experienceableId={experienceData?.experienceable_id ?? 0} |
583
|
|
|
experienceableType={ |
584
|
|
|
experienceData?.experienceable_type ?? "application" |
585
|
|
|
} |
586
|
|
|
jobId={jobId} |
587
|
|
|
jobClassification={jobClassification} |
588
|
|
|
jobEducationRequirements={jobEducationRequirements} |
589
|
|
|
modalId={modalButtons.education.id} |
590
|
|
|
onModalCancel={closeModal} |
591
|
|
|
onModalConfirm={submitExperience} |
592
|
|
|
optionalSkills={assetSkills} |
593
|
|
|
parentElement={modalRoot} |
594
|
|
|
requiredSkills={essentialSkills} |
595
|
|
|
savedOptionalSkills={experienceData?.savedOptionalSkills ?? []} |
596
|
|
|
savedRequiredSkills={experienceData?.savedRequiredSkills ?? []} |
597
|
|
|
visible={ |
598
|
|
|
isModalVisible.visible && |
599
|
|
|
isModalVisible.id === modalButtons.education.id |
600
|
|
|
} |
601
|
|
|
/> |
602
|
|
|
<WorkExperienceModal |
603
|
|
|
experienceWork={experienceData as ExperienceWork} |
604
|
|
|
experienceableId={experienceData?.experienceable_id ?? 0} |
605
|
|
|
experienceableType={ |
606
|
|
|
experienceData?.experienceable_type ?? "application" |
607
|
|
|
} |
608
|
|
|
jobId={jobId} |
609
|
|
|
jobClassification={jobClassification} |
610
|
|
|
jobEducationRequirements={jobEducationRequirements} |
611
|
|
|
modalId={modalButtons.work.id} |
612
|
|
|
onModalCancel={closeModal} |
613
|
|
|
onModalConfirm={submitExperience} |
614
|
|
|
optionalSkills={assetSkills} |
615
|
|
|
parentElement={modalRoot} |
616
|
|
|
requiredSkills={essentialSkills} |
617
|
|
|
savedOptionalSkills={experienceData?.savedOptionalSkills ?? []} |
618
|
|
|
savedRequiredSkills={experienceData?.savedRequiredSkills ?? []} |
619
|
|
|
visible={ |
620
|
|
|
isModalVisible.visible && isModalVisible.id === modalButtons.work.id |
621
|
|
|
} |
622
|
|
|
/> |
623
|
|
|
<CommunityExperienceModal |
624
|
|
|
experienceCommunity={experienceData as ExperienceCommunity} |
625
|
|
|
experienceableId={experienceData?.experienceable_id ?? 0} |
626
|
|
|
experienceableType={ |
627
|
|
|
experienceData?.experienceable_type ?? "application" |
628
|
|
|
} |
629
|
|
|
jobId={jobId} |
630
|
|
|
jobClassification={jobClassification} |
631
|
|
|
jobEducationRequirements={jobEducationRequirements} |
632
|
|
|
modalId={modalButtons.community.id} |
633
|
|
|
onModalCancel={closeModal} |
634
|
|
|
onModalConfirm={submitExperience} |
635
|
|
|
optionalSkills={assetSkills} |
636
|
|
|
parentElement={modalRoot} |
637
|
|
|
requiredSkills={essentialSkills} |
638
|
|
|
savedOptionalSkills={experienceData?.savedOptionalSkills ?? []} |
639
|
|
|
savedRequiredSkills={experienceData?.savedRequiredSkills ?? []} |
640
|
|
|
visible={ |
641
|
|
|
isModalVisible.visible && |
642
|
|
|
isModalVisible.id === modalButtons.community.id |
643
|
|
|
} |
644
|
|
|
/> |
645
|
|
|
<PersonalExperienceModal |
646
|
|
|
experiencePersonal={experienceData as ExperiencePersonal} |
647
|
|
|
experienceableId={experienceData?.experienceable_id ?? 0} |
648
|
|
|
experienceableType={ |
649
|
|
|
experienceData?.experienceable_type ?? "application" |
650
|
|
|
} |
651
|
|
|
jobId={jobId} |
652
|
|
|
jobClassification={jobClassification} |
653
|
|
|
jobEducationRequirements={jobEducationRequirements} |
654
|
|
|
modalId={modalButtons.personal.id} |
655
|
|
|
onModalCancel={closeModal} |
656
|
|
|
onModalConfirm={submitExperience} |
657
|
|
|
optionalSkills={assetSkills} |
658
|
|
|
parentElement={modalRoot} |
659
|
|
|
requiredSkills={essentialSkills} |
660
|
|
|
savedOptionalSkills={experienceData?.savedOptionalSkills ?? []} |
661
|
|
|
savedRequiredSkills={experienceData?.savedRequiredSkills ?? []} |
662
|
|
|
visible={ |
663
|
|
|
isModalVisible.visible && |
664
|
|
|
isModalVisible.id === modalButtons.personal.id |
665
|
|
|
} |
666
|
|
|
/> |
667
|
|
|
<AwardExperienceModal |
668
|
|
|
experienceAward={experienceData as ExperienceAward} |
669
|
|
|
experienceableId={experienceData?.experienceable_id ?? 0} |
670
|
|
|
experienceableType={ |
671
|
|
|
experienceData?.experienceable_type ?? "application" |
672
|
|
|
} |
673
|
|
|
jobId={jobId} |
674
|
|
|
jobClassification={jobClassification} |
675
|
|
|
jobEducationRequirements={jobEducationRequirements} |
676
|
|
|
modalId={modalButtons.award.id} |
677
|
|
|
onModalCancel={closeModal} |
678
|
|
|
onModalConfirm={submitExperience} |
679
|
|
|
optionalSkills={assetSkills} |
680
|
|
|
parentElement={modalRoot} |
681
|
|
|
recipientTypes={recipientTypes} |
682
|
|
|
recognitionTypes={recognitionTypes} |
683
|
|
|
requiredSkills={essentialSkills} |
684
|
|
|
savedOptionalSkills={experienceData?.savedOptionalSkills ?? []} |
685
|
|
|
savedRequiredSkills={experienceData?.savedRequiredSkills ?? []} |
686
|
|
|
visible={ |
687
|
|
|
isModalVisible.visible && isModalVisible.id === modalButtons.award.id |
688
|
|
|
} |
689
|
|
|
/> |
690
|
|
|
</> |
691
|
|
|
); |
692
|
|
|
}; |
693
|
|
|
|
694
|
|
|
interface ExperienceStepProps { |
695
|
|
|
experiences: Experience[]; |
696
|
|
|
educationStatuses: EducationStatus[]; |
697
|
|
|
educationTypes: EducationType[]; |
698
|
|
|
experienceSkills: ExperienceSkill[]; |
699
|
|
|
criteria: Criteria[]; |
700
|
|
|
skills: Skill[]; |
701
|
|
|
jobId: number; |
702
|
|
|
jobClassificationId: number | null; |
703
|
|
|
jobEducationRequirements: string | null; |
704
|
|
|
recipientTypes: AwardRecipientType[]; |
705
|
|
|
recognitionTypes: AwardRecognitionType[]; |
706
|
|
|
handleDeleteExperience: ( |
707
|
|
|
id: number, |
708
|
|
|
type: Experience["type"], |
709
|
|
|
) => Promise<void>; |
710
|
|
|
handleSubmitExperience: (data: ExperienceSubmitData) => Promise<void>; |
711
|
|
|
handleContinue: () => void; |
712
|
|
|
handleQuit: () => void; |
713
|
|
|
handleReturn: () => void; |
714
|
|
|
} |
715
|
|
|
|
716
|
|
|
export const ExperienceStep: React.FunctionComponent<ExperienceStepProps> = ({ |
717
|
|
|
experiences, |
718
|
|
|
educationStatuses, |
719
|
|
|
educationTypes, |
720
|
|
|
experienceSkills, |
721
|
|
|
criteria, |
722
|
|
|
skills, |
723
|
|
|
handleSubmitExperience, |
724
|
|
|
handleDeleteExperience, |
725
|
|
|
jobId, |
726
|
|
|
jobClassificationId, |
727
|
|
|
jobEducationRequirements, |
728
|
|
|
recipientTypes, |
729
|
|
|
recognitionTypes, |
730
|
|
|
handleContinue, |
731
|
|
|
handleQuit, |
732
|
|
|
handleReturn, |
733
|
|
|
}) => { |
734
|
|
|
const intl = useIntl(); |
735
|
|
|
const [hasError, setHasError] = useState(false); |
736
|
|
|
|
737
|
|
|
// Hack solution for experience step validation error message: focusOnElement is called in the onClick method (line 830) before the element is added to the dom. Therefore, the useEffect hook is needed for the first focus, after hasError triggers re-render. |
738
|
|
|
useEffect(() => { |
739
|
|
|
if (hasError) { |
740
|
|
|
focusOnElement("experience-step-form-error"); |
741
|
|
|
} |
742
|
|
|
}, [hasError]); |
743
|
|
|
|
744
|
|
|
const filteredSkills = criteria.reduce( |
745
|
|
|
(result, criterion): { essential: Skill[]; asset: Skill[] } => { |
746
|
|
|
const skillOfCriterion = getSkillOfCriteria(criterion, skills); |
747
|
|
|
if (skillOfCriterion) { |
748
|
|
|
if (criterion.criteria_type_id === CriteriaTypeId.Essential) { |
749
|
|
|
result.essential.push(skillOfCriterion); |
750
|
|
|
} |
751
|
|
|
if (criterion.criteria_type_id === CriteriaTypeId.Asset) { |
752
|
|
|
result.asset.push(skillOfCriterion); |
753
|
|
|
} |
754
|
|
|
} |
755
|
|
|
return result; |
756
|
|
|
}, |
757
|
|
|
{ essential: [], asset: [] } as { essential: Skill[]; asset: Skill[] }, |
758
|
|
|
); |
759
|
|
|
|
760
|
|
|
const essentialSkills = removeDuplicatesById(filteredSkills.essential); |
761
|
|
|
const assetSkills = removeDuplicatesById(filteredSkills.asset); |
762
|
|
|
|
763
|
|
|
const disconnectedRequiredSkills = getDisconnectedRequiredSkills( |
764
|
|
|
experiences, |
765
|
|
|
experienceSkills, |
766
|
|
|
essentialSkills, |
767
|
|
|
); |
768
|
|
|
|
769
|
|
|
return ( |
770
|
|
|
<> |
771
|
|
|
<MyExperience |
772
|
|
|
assetSkills={assetSkills} |
773
|
|
|
disconnectedRequiredSkills={disconnectedRequiredSkills} |
774
|
|
|
hasError={hasError} |
775
|
|
|
experiences={experiences} |
776
|
|
|
educationStatuses={educationStatuses} |
777
|
|
|
educationTypes={educationTypes} |
778
|
|
|
experienceSkills={experienceSkills} |
779
|
|
|
essentialSkills={essentialSkills} |
780
|
|
|
skills={skills} |
781
|
|
|
jobId={jobId} |
782
|
|
|
jobClassificationId={jobClassificationId} |
783
|
|
|
jobEducationRequirements={jobEducationRequirements} |
784
|
|
|
recipientTypes={recipientTypes} |
785
|
|
|
recognitionTypes={recognitionTypes} |
786
|
|
|
handleSubmitExperience={handleSubmitExperience} |
787
|
|
|
handleDeleteExperience={handleDeleteExperience} |
788
|
|
|
/> |
789
|
|
|
<div data-c-container="medium" data-c-padding="tb(2)"> |
790
|
|
|
<hr data-c-hr="thin(c1)" data-c-margin="bottom(2)" /> |
791
|
|
|
<div data-c-grid="gutter"> |
792
|
|
|
<div |
793
|
|
|
data-c-alignment="base(centre) tp(left)" |
794
|
|
|
data-c-grid-item="tp(1of2)" |
795
|
|
|
> |
796
|
|
|
<button |
797
|
|
|
data-c-button="outline(c2)" |
798
|
|
|
data-c-radius="rounded" |
799
|
|
|
type="button" |
800
|
|
|
onClick={(): void => handleReturn()} |
801
|
|
|
> |
802
|
|
|
{intl.formatMessage(navigationMessages.return)} |
803
|
|
|
</button> |
804
|
|
|
</div> |
805
|
|
|
<div |
806
|
|
|
data-c-alignment="base(centre) tp(right)" |
807
|
|
|
data-c-grid-item="tp(1of2)" |
808
|
|
|
> |
809
|
|
|
<button |
810
|
|
|
data-c-button="outline(c2)" |
811
|
|
|
data-c-radius="rounded" |
812
|
|
|
type="button" |
813
|
|
|
onClick={(): void => handleQuit()} |
814
|
|
|
> |
815
|
|
|
{intl.formatMessage(navigationMessages.quit)} |
816
|
|
|
</button> |
817
|
|
|
<button |
818
|
|
|
data-c-button="solid(c1)" |
819
|
|
|
data-c-radius="rounded" |
820
|
|
|
data-c-margin="left(1)" |
821
|
|
|
type="button" |
822
|
|
|
onClick={(): void => { |
823
|
|
|
// If all required skills have been connected to an experience, then continue. |
824
|
|
|
// Else, show error alert. |
825
|
|
|
if (disconnectedRequiredSkills.length === 0) { |
826
|
|
|
handleContinue(); |
827
|
|
|
} else { |
828
|
|
|
setHasError(true); |
829
|
|
|
focusOnElement("experience-step-form-error"); |
830
|
|
|
} |
831
|
|
|
}} |
832
|
|
|
> |
833
|
|
|
{intl.formatMessage(navigationMessages.continue)} |
834
|
|
|
</button> |
835
|
|
|
</div> |
836
|
|
|
</div> |
837
|
|
|
</div> |
838
|
|
|
</> |
839
|
|
|
); |
840
|
|
|
}; |
841
|
|
|
|
842
|
|
|
export default ExperienceStep; |
843
|
|
|
|