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