1
|
|
|
import React, { useState, useRef, useReducer } from "react"; |
2
|
|
|
import { FormattedMessage, defineMessages, useIntl } from "react-intl"; |
3
|
|
|
import nprogress from "nprogress"; |
4
|
|
|
import { Job, Skill, Criteria, JobPosterKeyTask } from "../../../models/types"; |
5
|
|
|
import Modal from "../../Modal"; |
6
|
|
|
import CriteriaForm from "./CriteriaForm"; |
7
|
|
|
import { mapToObject, getId, hasKey, notEmpty } from "../../../helpers/queries"; |
8
|
|
|
import { CriteriaTypeId } from "../../../models/lookupConstants"; |
9
|
|
|
import Select, { SelectOption } from "../../Select"; |
10
|
|
|
import { getSkillLevelName } from "../../../models/jobUtil"; |
11
|
|
|
import Criterion from "../Criterion"; |
12
|
|
|
import { |
13
|
|
|
localizeField, |
14
|
|
|
getLocale, |
15
|
|
|
localizeFieldNonNull, |
16
|
|
|
} from "../../../helpers/localize"; |
17
|
|
|
import { imageUrl } from "../../../helpers/routes"; |
18
|
|
|
|
19
|
|
|
interface JobSkillsProps { |
20
|
|
|
// The job being built |
21
|
|
|
job: Job; |
22
|
|
|
// This job's classification key (i.e. CS) |
23
|
|
|
classificationKey: string; |
24
|
|
|
// This job's key tasks |
25
|
|
|
keyTasks: JobPosterKeyTask[]; |
26
|
|
|
// Criteria already part of the job |
27
|
|
|
initialCriteria: Criteria[]; |
28
|
|
|
// The list of all possible skills |
29
|
|
|
skills: Skill[]; |
30
|
|
|
// The function to run when user clicks Save. Must return the updated list of criteria if successful. |
31
|
|
|
handleSubmit: (criteria: Criteria[]) => Promise<Criteria[]>; |
32
|
|
|
// The function to run when user clicks Prev Pag |
33
|
|
|
handleReturn: () => void; |
34
|
|
|
// The function to run when user clicks Next Page |
35
|
|
|
handleContinue: () => void; |
36
|
|
|
/** Whether the entire job is complete and valid for submission. */ |
37
|
|
|
jobIsComplete: boolean; |
38
|
|
|
/** Function that skips to final review. */ |
39
|
|
|
handleSkipToReview: () => Promise<void>; |
40
|
|
|
} |
41
|
|
|
|
42
|
|
|
const messages = defineMessages({ |
43
|
|
|
emailUs: { |
44
|
|
|
id: "jobBuilder.skills.emailLink", |
45
|
|
|
defaultMessage: "get in touch with us through email", |
46
|
|
|
description: "Text for an email link in a larger block of text", |
47
|
|
|
}, |
48
|
|
|
selectSkillLabel: { |
49
|
|
|
id: "jobBuilder.skills.selectSkillLabel", |
50
|
|
|
defaultMessage: "Please select a skill from our list", |
51
|
|
|
description: "Label for skill selection dropdown menu", |
52
|
|
|
}, |
53
|
|
|
selectSkillNull: { |
54
|
|
|
id: "jobBuilder.skills.selectSkillNull", |
55
|
|
|
defaultMessage: "Please select a skill", |
56
|
|
|
description: "Label for skill selection dropdown null/default state", |
57
|
|
|
}, |
58
|
|
|
}); |
59
|
|
|
|
60
|
|
|
const altMessages = defineMessages({ |
61
|
|
|
unhappyArrow: { |
62
|
|
|
id: "jobBuilder.skills.alt.unhappyArrow", |
63
|
|
|
defaultMessage: "Arrow icon highlighting the unhappy smiley icon.", |
64
|
|
|
description: "Alternative text describing unhappy arrow image", |
65
|
|
|
}, |
66
|
|
|
unhappySmiley: { |
67
|
|
|
id: "jobBuilder.skills.alt.unhappySmiley", |
68
|
|
|
defaultMessage: "Unhappy coloured smiley icon.", |
69
|
|
|
description: "Alternative text describing unhappy smiley image", |
70
|
|
|
}, |
71
|
|
|
unhappyGraySmiley: { |
72
|
|
|
id: "jobBuilder.skills.alt.unhappyGraySmiley", |
73
|
|
|
defaultMessage: "Unhappy grayscale smiley icon.", |
74
|
|
|
description: "Alternative text describing unhappy grayscale smiley image", |
75
|
|
|
}, |
76
|
|
|
neutralArrow: { |
77
|
|
|
id: "jobBuilder.skills.alt.neutralArrow", |
78
|
|
|
defaultMessage: "Arrow icon highlighting the neutral smiley icon.", |
79
|
|
|
description: "Alternative text describing neutral arrow image", |
80
|
|
|
}, |
81
|
|
|
neutralSmiley: { |
82
|
|
|
id: "jobBuilder.skills.alt.neutralSmiley", |
83
|
|
|
defaultMessage: "neutral coloured smiley icon.", |
84
|
|
|
description: "Alternative text describing neutral smiley image", |
85
|
|
|
}, |
86
|
|
|
neutralGraySmiley: { |
87
|
|
|
id: "jobBuilder.skills.alt.neutralGraySmiley", |
88
|
|
|
defaultMessage: "neutral grayscale smiley icon.", |
89
|
|
|
description: "Alternative text describing neutral grayscale smiley image", |
90
|
|
|
}, |
91
|
|
|
happyArrow: { |
92
|
|
|
id: "jobBuilder.skills.alt.happyArrow", |
93
|
|
|
defaultMessage: "Arrow icon highlighting the happy smiley icon.", |
94
|
|
|
description: "Alternative text describing happy arrow image", |
95
|
|
|
}, |
96
|
|
|
happySmiley: { |
97
|
|
|
id: "jobBuilder.skills.alt.happySmiley", |
98
|
|
|
defaultMessage: "happy coloured smiley icon.", |
99
|
|
|
description: "Alternative text describing happy smiley image", |
100
|
|
|
}, |
101
|
|
|
happyGraySmiley: { |
102
|
|
|
id: "jobBuilder.skills.alt.happyGraySmiley", |
103
|
|
|
defaultMessage: "happy grayscale smiley icon.", |
104
|
|
|
description: "Alternative text describing happy grayscale smiley image", |
105
|
|
|
}, |
106
|
|
|
}); |
107
|
|
|
// "Arrow highlighting the neutral smiley icon." |
108
|
|
|
// function arrayMove<T>(arr: T[], fromIndex: number, toIndex: number): T[] { |
109
|
|
|
// const arrCopy = [...arr]; |
110
|
|
|
// const element = arrCopy[fromIndex]; |
111
|
|
|
// arrCopy.splice(fromIndex, 1); |
112
|
|
|
// arrCopy.splice(toIndex, 0, element); |
113
|
|
|
// return arrCopy; |
114
|
|
|
// } |
115
|
|
|
|
116
|
|
|
// function moveUp<T>(arr: T[], fromIndex: number): T[] { |
117
|
|
|
// if (fromIndex <= 0) { |
118
|
|
|
// return arr; |
119
|
|
|
// } |
120
|
|
|
// return arrayMove(arr, fromIndex, fromIndex - 1); |
121
|
|
|
// } |
122
|
|
|
|
123
|
|
|
// function moveDown<T>(arr: T[], fromIndex: number): T[] { |
124
|
|
|
// if (fromIndex + 1 >= arr.length) { |
125
|
|
|
// return arr; |
126
|
|
|
// } |
127
|
|
|
// return arrayMove(arr, fromIndex, fromIndex + 1); |
128
|
|
|
// } |
129
|
|
|
|
130
|
|
|
type CriteriaAction = |
131
|
|
|
| { |
132
|
|
|
type: "add"; |
133
|
|
|
payload: Criteria; |
134
|
|
|
} |
135
|
|
|
| { |
136
|
|
|
type: "edit"; |
137
|
|
|
payload: Criteria; |
138
|
|
|
} |
139
|
|
|
| { |
140
|
|
|
type: "remove"; |
141
|
|
|
payload: Criteria; |
142
|
|
|
} |
143
|
|
|
| { |
144
|
|
|
type: "removeSkill"; |
145
|
|
|
payload: { |
146
|
|
|
skillId: number; |
147
|
|
|
}; |
148
|
|
|
} |
149
|
|
|
| { |
150
|
|
|
type: "replace"; |
151
|
|
|
payload: Criteria[]; |
152
|
|
|
}; |
153
|
|
|
const criteriaReducer = ( |
154
|
|
|
state: Criteria[], |
155
|
|
|
action: CriteriaAction, |
156
|
|
|
): Criteria[] => { |
157
|
|
|
switch (action.type) { |
158
|
|
|
case "add": |
159
|
|
|
return [ |
160
|
|
|
// When adding a criterion, make sure it isn't a duplicate |
161
|
|
|
// Always compare criteria using skill_id, to avoid duplicate skills. (And new criteria may not have unique ids yet.) |
162
|
|
|
...state.filter( |
163
|
|
|
(criterion): boolean => |
164
|
|
|
criterion.skill_id !== action.payload.skill_id, |
165
|
|
|
), |
166
|
|
|
action.payload, |
167
|
|
|
]; |
168
|
|
|
case "edit": |
169
|
|
|
// Replace the edited criterion with the one from the action payload. |
170
|
|
|
// This is different from "add" because it keeps the same ordering. |
171
|
|
|
return state.map( |
172
|
|
|
(criterion): Criteria => |
173
|
|
|
criterion.skill_id === action.payload.skill_id |
174
|
|
|
? action.payload |
175
|
|
|
: criterion, |
176
|
|
|
); |
177
|
|
|
case "remove": |
178
|
|
|
return state.filter( |
179
|
|
|
(criterion): boolean => criterion.skill_id !== action.payload.skill_id, |
180
|
|
|
); |
181
|
|
|
case "removeSkill": |
182
|
|
|
return state.filter( |
183
|
|
|
(criterion): boolean => criterion.skill_id !== action.payload.skillId, |
184
|
|
|
); |
185
|
|
|
case "replace": |
186
|
|
|
// Totally replace the saved list of criteria with the received payload |
187
|
|
|
return action.payload; |
188
|
|
|
|
189
|
|
|
default: |
190
|
|
|
return state; |
191
|
|
|
} |
192
|
|
|
}; |
193
|
|
|
|
194
|
|
|
export const skillAlreadySelected = ( |
195
|
|
|
selectedCriteria: Criteria[], |
196
|
|
|
skill: Skill, |
197
|
|
|
): boolean => |
198
|
|
|
selectedCriteria.find( |
199
|
|
|
(criterion): boolean => criterion.skill_id === skill.id, |
200
|
|
|
) !== undefined; |
201
|
|
|
|
202
|
|
|
export const JobSkills: React.FunctionComponent<JobSkillsProps> = ({ |
203
|
|
|
job, |
204
|
|
|
classificationKey, |
205
|
|
|
keyTasks, |
206
|
|
|
initialCriteria, |
207
|
|
|
skills, |
208
|
|
|
handleSubmit, |
209
|
|
|
handleReturn, |
210
|
|
|
handleContinue, |
211
|
|
|
jobIsComplete, |
212
|
|
|
handleSkipToReview, |
213
|
|
|
}): React.ReactElement => { |
214
|
|
|
const intl = useIntl(); |
215
|
|
|
const locale = getLocale(intl.locale); |
216
|
|
|
|
217
|
|
|
// The ideal number of skills for each category |
218
|
|
|
const minOccupational = 3; |
219
|
|
|
const maxOccupational = 5; |
220
|
|
|
const minCulture = 0; |
221
|
|
|
const maxCulture = 4; |
222
|
|
|
const minFuture = 0; |
223
|
|
|
const maxFuture = 2; |
224
|
|
|
|
225
|
|
|
// This is where the edited list of criteria is stored |
226
|
|
|
const [jobCriteria, criteriaDispatch] = useReducer( |
227
|
|
|
criteriaReducer, |
228
|
|
|
initialCriteria, |
229
|
|
|
); |
230
|
|
|
const skillCount: number = jobCriteria.length; |
231
|
|
|
const essentialCriteria: Criteria[] = jobCriteria.filter( |
232
|
|
|
(criteria): boolean => |
233
|
|
|
criteria.criteria_type_id === CriteriaTypeId.Essential, |
234
|
|
|
); |
235
|
|
|
const essentialCount: number = essentialCriteria.length; |
236
|
|
|
const assetCriteria: Criteria[] = jobCriteria.filter( |
237
|
|
|
(criteria): boolean => criteria.criteria_type_id === CriteriaTypeId.Asset, |
238
|
|
|
); |
239
|
|
|
const assetCount: number = assetCriteria.length; |
240
|
|
|
|
241
|
|
|
// Set this to true to show the Key Tasks modal |
242
|
|
|
const [tasksModalVisible, setTasksModalVisible] = useState(false); |
243
|
|
|
|
244
|
|
|
// When skillBeingAdded is not null, the modal to add a new skill will appear. |
245
|
|
|
const [skillBeingAdded, setSkillBeingAdded] = useState<Skill | null>(null); |
246
|
|
|
|
247
|
|
|
// Set to true if submit button is touched |
248
|
|
|
const [submitTouched, setSubmitTouched] = useState(false); |
249
|
|
|
|
250
|
|
|
// When criteriaBeingEdited is not null, the modal for editing that criterion will appear. |
251
|
|
|
const [ |
252
|
|
|
criteriaBeingEdited, |
253
|
|
|
setCriteriaBeingEdited, |
254
|
|
|
] = useState<Criteria | null>(null); |
255
|
|
|
|
256
|
|
|
const [isPreviewVisible, setIsPreviewVisible] = useState(false); |
257
|
|
|
|
258
|
|
|
// This should be true if ANY modal is visible. The modal overlay uses this. |
259
|
|
|
const isModalVisible = |
260
|
|
|
tasksModalVisible || |
261
|
|
|
skillBeingAdded !== null || |
262
|
|
|
criteriaBeingEdited !== null || |
263
|
|
|
isPreviewVisible; |
264
|
|
|
const modalParentRef = useRef<HTMLDivElement>(null); |
265
|
|
|
|
266
|
|
|
const tasksModalId = "job-builder-review-tasks"; |
267
|
|
|
const addModalId = "job-builder-add-skill"; |
268
|
|
|
const editModalId = "job-builder-edit-skill"; |
269
|
|
|
const previewModalId = "job-builder-preview-skills"; |
270
|
|
|
|
271
|
|
|
const countInRange = (min: number, max: number, count: number): boolean => { |
272
|
|
|
return count >= min && count <= max; |
273
|
|
|
}; |
274
|
|
|
|
275
|
|
|
const sortAlphabetically = (a: Skill, b: Skill): number => { |
276
|
|
|
const skillA: string = localizeFieldNonNull( |
277
|
|
|
locale, |
278
|
|
|
a, |
279
|
|
|
"name", |
280
|
|
|
).toUpperCase(); |
281
|
|
|
const skillB: string = localizeFieldNonNull( |
282
|
|
|
locale, |
283
|
|
|
b, |
284
|
|
|
"name", |
285
|
|
|
).toUpperCase(); |
286
|
|
|
|
287
|
|
|
return skillA.localeCompare(skillB, locale, { sensitivity: "base" }); |
288
|
|
|
}; |
289
|
|
|
|
290
|
|
|
// Map the skills into a dictionary for quicker access |
291
|
|
|
const skillsById = mapToObject(skills, getId); |
292
|
|
|
const getSkillOfCriteria = (criterion: Criteria): Skill | null => { |
293
|
|
|
return hasKey(skillsById, criterion.skill_id) |
294
|
|
|
? skillsById[criterion.skill_id] |
295
|
|
|
: null; |
296
|
|
|
}; |
297
|
|
|
|
298
|
|
|
const getClassifications = (skill: Skill): string[] => |
299
|
|
|
skill.classifications.map((classification): string => classification.key); |
300
|
|
|
const isOccupational = (skill: Skill): boolean => |
301
|
|
|
job.classification_id !== null && |
302
|
|
|
getClassifications(skill).includes(classificationKey); |
303
|
|
|
const occupationalSkills = skills |
304
|
|
|
.filter(isOccupational) |
305
|
|
|
.sort(sortAlphabetically); |
306
|
|
|
const occupationalCriteria = jobCriteria.filter((criterion): boolean => { |
307
|
|
|
const critSkill = getSkillOfCriteria(criterion); |
308
|
|
|
return critSkill !== null && isOccupational(critSkill); |
309
|
|
|
}); |
310
|
|
|
|
311
|
|
|
const isCulture = (skill: Skill): boolean => skill.is_culture_skill; |
312
|
|
|
const cultureSkills = skills.filter(isCulture).sort(sortAlphabetically); |
313
|
|
|
const cultureCriteria = jobCriteria.filter((criterion): boolean => { |
314
|
|
|
const skill = getSkillOfCriteria(criterion); |
315
|
|
|
return skill !== null && isCulture(skill); |
316
|
|
|
}); |
317
|
|
|
const isFuture = (skill: Skill): boolean => skill.is_future_skill; |
318
|
|
|
const futureSkills = skills.filter(isFuture).sort(sortAlphabetically); |
319
|
|
|
const futureCriteria = jobCriteria.filter((criterion): boolean => { |
320
|
|
|
const skill = getSkillOfCriteria(criterion); |
321
|
|
|
return skill !== null && isFuture(skill); |
322
|
|
|
}); |
323
|
|
|
|
324
|
|
|
// Optional skills are those that don't fit into the other three categories |
325
|
|
|
const isOptional = (skill: Skill): boolean => |
326
|
|
|
!isOccupational(skill) && !isCulture(skill) && !isFuture(skill); |
327
|
|
|
const otherSkills = skills.filter(isOptional).sort(sortAlphabetically); |
328
|
|
|
const selectedSkillIds = jobCriteria |
329
|
|
|
.map(getSkillOfCriteria) |
330
|
|
|
.filter(notEmpty) |
331
|
|
|
.map(getId); |
332
|
|
|
const selectedOtherSkills: Skill[] = otherSkills.filter((skill): boolean => |
333
|
|
|
selectedSkillIds.includes(skill.id), |
334
|
|
|
); |
335
|
|
|
const unselectedOtherSkills: Skill[] = otherSkills.filter( |
336
|
|
|
(skill): boolean => !selectedSkillIds.includes(skill.id), |
337
|
|
|
); |
338
|
|
|
|
339
|
|
|
const [isSaving, setIsSaving] = useState(false); |
340
|
|
|
|
341
|
|
|
const errorMessage = useRef<HTMLAnchorElement>(null); // React.createRef<HTMLAnchorElement>(); |
342
|
|
|
const focusOnError = (): void => { |
343
|
|
|
// eslint-disable-next-line no-unused-expressions |
344
|
|
|
errorMessage?.current?.focus(); |
345
|
|
|
}; |
346
|
|
|
|
347
|
|
|
const saveAndPreview = (): void => { |
348
|
|
|
setSubmitTouched(true); |
349
|
|
|
if (essentialCount > 0) { |
350
|
|
|
nprogress.start(); |
351
|
|
|
setIsSaving(true); |
352
|
|
|
handleSubmit(jobCriteria) |
353
|
|
|
.then((criteria: Criteria[]): void => { |
354
|
|
|
criteriaDispatch({ type: "replace", payload: criteria }); |
355
|
|
|
nprogress.done(); |
356
|
|
|
setIsPreviewVisible(true); |
357
|
|
|
setIsSaving(false); |
358
|
|
|
}) |
359
|
|
|
.catch((): void => { |
360
|
|
|
nprogress.done(); |
361
|
|
|
setIsSaving(false); |
362
|
|
|
}); |
363
|
|
|
} else { |
364
|
|
|
focusOnError(); |
365
|
|
|
} |
366
|
|
|
}; |
367
|
|
|
const saveAndReturn = (): void => { |
368
|
|
|
nprogress.start(); |
369
|
|
|
setIsSaving(true); |
370
|
|
|
handleSubmit(jobCriteria) |
371
|
|
|
.then((criteria: Criteria[]): void => { |
372
|
|
|
criteriaDispatch({ type: "replace", payload: criteria }); |
373
|
|
|
nprogress.done(); |
374
|
|
|
handleReturn(); |
375
|
|
|
setIsSaving(false); |
376
|
|
|
}) |
377
|
|
|
.catch((): void => { |
378
|
|
|
nprogress.done(); |
379
|
|
|
setIsSaving(false); |
380
|
|
|
}); |
381
|
|
|
}; |
382
|
|
|
|
383
|
|
|
const renderNullCriteriaRow = (): React.ReactElement => ( |
384
|
|
|
<div className="jpb-skill-null" data-c-grid="gutter middle"> |
385
|
|
|
{/** TODO: add these back in when implementing UP/DOWN buttons again */} |
386
|
|
|
{/* <div data-c-grid-item="base(2of10) tl(1of10)" data-c-align="base(centre)"> |
387
|
|
|
<button type="button" data-tc-move-up-trigger> |
388
|
|
|
<i className="fas fa-angle-up" /> |
389
|
|
|
</button> |
390
|
|
|
<button type="button" data-tc-move-down-trigger> |
391
|
|
|
<i className="fas fa-angle-down" /> |
392
|
|
|
</button> |
393
|
|
|
</div> */} |
394
|
|
|
<div data-c-grid-item="base(6of10) tl(7of10)"> |
395
|
|
|
<div data-c-grid="gutter"> |
396
|
|
|
<div data-c-grid-item="base(1of1) tl(2of3)"> |
397
|
|
|
{/* <span>0</span> |
398
|
|
|
<span |
399
|
|
|
data-c-background="grey(40)" |
400
|
|
|
data-c-font-size="small" |
401
|
|
|
data-c-margin="rl(half)" |
402
|
|
|
data-c-padding="tb(quarter) rl(half)" |
403
|
|
|
data-c-radius="rounded" |
404
|
|
|
data-c-colour="white" |
405
|
|
|
> |
406
|
|
|
<i className="fas fa-briefcase" /> |
407
|
|
|
</span> */} |
408
|
|
|
<span> |
409
|
|
|
<FormattedMessage |
410
|
|
|
id="jobBuilder.skills.addSkillBelow" |
411
|
|
|
defaultMessage="Add skills below to proceed." |
412
|
|
|
description="Placeholder skill title / instructions to add skill" |
413
|
|
|
/> |
414
|
|
|
</span> |
415
|
|
|
</div> |
416
|
|
|
<div data-c-grid-item="base(1of1) tl(1of3)"> |
417
|
|
|
<span |
418
|
|
|
data-c-colour="white" |
419
|
|
|
data-c-background="grey(40)" |
420
|
|
|
data-c-padding="tb(quarter) rl(half)" |
421
|
|
|
data-c-radius="rounded" |
422
|
|
|
data-c-font-size="small" |
423
|
|
|
> |
424
|
|
|
<FormattedMessage |
425
|
|
|
id="jobBuilder.skills.skillLevel" |
426
|
|
|
defaultMessage="Skill Level" |
427
|
|
|
description="Placeholder label" |
428
|
|
|
/> |
429
|
|
|
</span> |
430
|
|
|
</div> |
431
|
|
|
</div> |
432
|
|
|
</div> |
433
|
|
|
<div data-c-grid-item="base(3of10)"> |
434
|
|
|
<div data-c-grid="gutter"> |
435
|
|
|
<div |
436
|
|
|
data-c-grid-item="base(1of1) tl(1of2)" |
437
|
|
|
data-c-align="base(centre)" |
438
|
|
|
> |
439
|
|
|
<button type="button"> |
440
|
|
|
<i className="fas fa-edit" /> |
441
|
|
|
</button> |
442
|
|
|
</div> |
443
|
|
|
<div |
444
|
|
|
data-c-grid-item="base(1of1) tl(1of2)" |
445
|
|
|
data-c-align="base(centre)" |
446
|
|
|
> |
447
|
|
|
<button type="button"> |
448
|
|
|
<i className="fas fa-trash" /> |
449
|
|
|
</button> |
450
|
|
|
</div> |
451
|
|
|
</div> |
452
|
|
|
</div> |
453
|
|
|
</div> |
454
|
|
|
); |
455
|
|
|
|
456
|
|
|
const renderCriteriaRow = ( |
457
|
|
|
criterion: Criteria, |
458
|
|
|
index: number, |
459
|
|
|
): React.ReactElement | null => { |
460
|
|
|
const skill = getSkillOfCriteria(criterion); |
461
|
|
|
if (skill === null) { |
462
|
|
|
return null; |
463
|
|
|
} |
464
|
|
|
return ( |
465
|
|
|
<li |
466
|
|
|
key={skill.id} |
467
|
|
|
className={`jpb-skill ${isOccupational(skill) ? "occupational" : ""} ${ |
468
|
|
|
isCulture(skill) ? "cultural" : "" |
469
|
|
|
} ${isFuture(skill) ? "future" : ""} ${ |
470
|
|
|
isOptional(skill) ? "optional" : "" |
471
|
|
|
}`} |
472
|
|
|
data-tc-up-down-item |
473
|
|
|
> |
474
|
|
|
<div data-c-grid="gutter middle"> |
475
|
|
|
{/** Removing the up/down buttons for now */} |
476
|
|
|
{/** TODO: removing the buttons messes with the row height, get Josh's help to fix it */} |
477
|
|
|
{/* <div |
478
|
|
|
data-c-grid-item="base(2of10) tl(1of10)" |
479
|
|
|
data-c-align="base(centre)" |
480
|
|
|
> |
481
|
|
|
<button type="button" data-tc-move-up-trigger> |
482
|
|
|
<i className="fas fa-angle-up" /> |
483
|
|
|
</button> |
484
|
|
|
<button type="button" data-tc-move-down-trigger> |
485
|
|
|
<i className="fas fa-angle-down" /> |
486
|
|
|
</button> |
487
|
|
|
</div> */} |
488
|
|
|
<div data-c-grid-item="base(6of10) tl(7of10)"> |
489
|
|
|
<div data-c-grid="gutter"> |
490
|
|
|
<div data-c-grid-item="base(1of1) tl(2of3)"> |
491
|
|
|
<span data-c-margin="right(normal)">{index + 1}.</span> |
492
|
|
|
{/* This icon will automatically update based on the class you've specified above, on the jpb-skill. */} |
493
|
|
|
{/* <span |
494
|
|
|
className="jpb-skill-type" |
495
|
|
|
data-c-font-size="small" |
496
|
|
|
data-c-margin="rl(half)" |
497
|
|
|
data-c-padding="tb(quarter) rl(half)" |
498
|
|
|
data-c-radius="rounded" |
499
|
|
|
data-c-colour="white" |
500
|
|
|
title="This is an occupational skill." |
501
|
|
|
> |
502
|
|
|
<i className="fas fa-briefcase" /> |
503
|
|
|
<i className="fas fa-coffee" /> |
504
|
|
|
<i className="fas fa-certificate" /> |
505
|
|
|
<i className="fas fa-book" /> |
506
|
|
|
</span> */} |
507
|
|
|
{/* The skill name. */} |
508
|
|
|
<span>{localizeField(locale, skill, "name")}</span> |
509
|
|
|
</div> |
510
|
|
|
<div data-c-grid-item="base(1of1) tl(1of3)"> |
511
|
|
|
<span |
512
|
|
|
data-c-radius="pill" |
513
|
|
|
data-c-padding="tb(quarter) rl(half)" |
514
|
|
|
data-c-border="all(thin, solid, c1)" |
515
|
|
|
data-c-colour="c1" |
516
|
|
|
data-c-font-size="small" |
517
|
|
|
data-c-display="inline-block" |
518
|
|
|
data-c-alignment="center" |
519
|
|
|
> |
520
|
|
|
{intl.formatMessage(getSkillLevelName(criterion, skill))} |
521
|
|
|
</span> |
522
|
|
|
</div> |
523
|
|
|
</div> |
524
|
|
|
</div> |
525
|
|
|
<div data-c-grid-item="base(3of10)"> |
526
|
|
|
<div data-c-grid="gutter"> |
527
|
|
|
<div |
528
|
|
|
data-c-grid-item="base(1of1) tl(1of2)" |
529
|
|
|
data-c-align="base(centre)" |
530
|
|
|
> |
531
|
|
|
<button |
532
|
|
|
type="button" |
533
|
|
|
data-c-colour="c1" |
534
|
|
|
onClick={(): void => setCriteriaBeingEdited(criterion)} |
535
|
|
|
> |
536
|
|
|
<i className="fas fa-edit" /> |
537
|
|
|
</button> |
538
|
|
|
</div> |
539
|
|
|
<div |
540
|
|
|
data-c-grid-item="base(1of1) tl(1of2)" |
541
|
|
|
data-c-align="base(centre)" |
542
|
|
|
> |
543
|
|
|
<button |
544
|
|
|
type="button" |
545
|
|
|
data-c-colour="c1" |
546
|
|
|
data-c-hover-colour="stop" |
547
|
|
|
onClick={(): void => |
548
|
|
|
criteriaDispatch({ |
549
|
|
|
type: "remove", |
550
|
|
|
payload: criterion, |
551
|
|
|
}) |
552
|
|
|
} |
553
|
|
|
> |
554
|
|
|
<i className="fas fa-trash" /> |
555
|
|
|
</button> |
556
|
|
|
</div> |
557
|
|
|
</div> |
558
|
|
|
</div> |
559
|
|
|
</div> |
560
|
|
|
</li> |
561
|
|
|
); |
562
|
|
|
}; |
563
|
|
|
|
564
|
|
|
const renderSkillButton = (skill: Skill): React.ReactElement => { |
565
|
|
|
const alreadySelected = skillAlreadySelected(jobCriteria, skill); |
566
|
|
|
// Open the Add Skill modal, or remove it if its already added |
567
|
|
|
const handleClick = (): void => |
568
|
|
|
alreadySelected |
569
|
|
|
? criteriaDispatch({ |
570
|
|
|
type: "removeSkill", |
571
|
|
|
payload: { skillId: skill.id }, |
572
|
|
|
}) |
573
|
|
|
: setSkillBeingAdded(skill); |
574
|
|
|
return ( |
575
|
|
|
<li key={skill.id}> |
576
|
|
|
<button |
577
|
|
|
className={`jpb-skill-trigger ${alreadySelected ? "active" : ""}`} |
578
|
|
|
data-c-button="outline(c1)" |
579
|
|
|
data-c-radius="rounded" |
580
|
|
|
data-c-padding="all(half)" |
581
|
|
|
type="button" |
582
|
|
|
onClick={handleClick} |
583
|
|
|
> |
584
|
|
|
<span data-c-padding="right(half)"> |
585
|
|
|
<i className="fas fa-plus-circle" /> |
586
|
|
|
<i className="fas fa-minus-circle" /> |
587
|
|
|
</span> |
588
|
|
|
{localizeField(locale, skill, "name")} |
589
|
|
|
</button> |
590
|
|
|
</li> |
591
|
|
|
); |
592
|
|
|
}; |
593
|
|
|
|
594
|
|
|
const submitButton = ( |
595
|
|
|
<button |
596
|
|
|
data-c-button="solid(c2)" |
597
|
|
|
data-c-radius="rounded" |
598
|
|
|
type="button" |
599
|
|
|
disabled={isSaving} |
600
|
|
|
onClick={(): void => saveAndPreview()} |
601
|
|
|
> |
602
|
|
|
<FormattedMessage |
603
|
|
|
id="jobBuilder.skills.button.previewSkills" |
604
|
|
|
defaultMessage="Save & Preview Skills" |
605
|
|
|
description="Label of Button" |
606
|
|
|
/> |
607
|
|
|
</button> |
608
|
|
|
); |
609
|
|
|
const smileyArrowBadImg = imageUrl("icon-smiley-arrow-bad.svg"); |
610
|
|
|
const smileyArrowMediumImg = imageUrl("icon-smiley-arrow-medium.svg"); |
611
|
|
|
const smileyArrowGoodImg = imageUrl("icon-smiley-arrow-good.svg"); |
612
|
|
|
const smileyBadImg = imageUrl("icon-smiley-bad.svg"); |
613
|
|
|
const smileyBadGreyImg = imageUrl("icon-smiley-bad-grey.svg"); |
614
|
|
|
const smileyMediumImg = imageUrl("icon-smiley-medium.svg"); |
615
|
|
|
const smileyMediumGreyImg = imageUrl("icon-smiley-medium-grey.svg"); |
616
|
|
|
const smileyGoodImg = imageUrl("icon-smiley-good.svg"); |
617
|
|
|
const smileyGoodGreyImg = imageUrl("icon-smiley-good-grey.svg"); |
618
|
|
|
|
619
|
|
|
return ( |
620
|
|
|
<> |
621
|
|
|
<div |
622
|
|
|
data-c-container="form" |
623
|
|
|
data-c-padding="top(triple) bottom(triple)" |
624
|
|
|
ref={modalParentRef} |
625
|
|
|
> |
626
|
|
|
<h3 |
627
|
|
|
data-c-font-size="h3" |
628
|
|
|
data-c-font-weight="bold" |
629
|
|
|
data-c-margin="bottom(double)" |
630
|
|
|
> |
631
|
|
|
<FormattedMessage |
632
|
|
|
id="jobBuilder.skills.title" |
633
|
|
|
defaultMessage="Skills" |
634
|
|
|
description="section title" |
635
|
|
|
/> |
636
|
|
|
</h3> |
637
|
|
|
<p data-c-margin="bottom(triple)"> |
638
|
|
|
<FormattedMessage |
639
|
|
|
id="jobBuilder.skills.description" |
640
|
|
|
defaultMessage="This is where you'll select the criteria that are required to do this job effectively. Below are two bars that indicate a measurement of your current skill selection." |
641
|
|
|
description="section description under title" |
642
|
|
|
/> |
643
|
|
|
</p> |
644
|
|
|
<div |
645
|
|
|
data-c-margin="bottom(triple)" |
646
|
|
|
data-c-align="base(centre) tl(left)" |
647
|
|
|
> |
648
|
|
|
{/* We'll want this button to functionally be the exact same as the button at the bottom of the page, where it saves the data, and opens the preview modal. */} |
649
|
|
|
<button |
650
|
|
|
data-c-button="solid(c2)" |
651
|
|
|
data-c-radius="rounded" |
652
|
|
|
type="button" |
653
|
|
|
disabled={tasksModalVisible} |
654
|
|
|
onClick={(): void => setTasksModalVisible(true)} |
655
|
|
|
> |
656
|
|
|
<FormattedMessage |
657
|
|
|
id="jobBuilder.skills.button.keyTasks" |
658
|
|
|
defaultMessage="View Key Tasks" |
659
|
|
|
description="Button label" |
660
|
|
|
/> |
661
|
|
|
</button> |
662
|
|
|
</div> |
663
|
|
|
|
664
|
|
|
{/* Total Skills List */} |
665
|
|
|
<h4 |
666
|
|
|
data-c-colour="c2" |
667
|
|
|
data-c-font-size="h4" |
668
|
|
|
data-c-margin="bottom(normal)" |
669
|
|
|
> |
670
|
|
|
<FormattedMessage |
671
|
|
|
id="jobBuilder.skills.listTitle" |
672
|
|
|
defaultMessage="Your Skills List" |
673
|
|
|
description="List section title" |
674
|
|
|
/> |
675
|
|
|
</h4> |
676
|
|
|
<div data-c-grid="gutter top"> |
677
|
|
|
<div data-c-grid-item="base(1of1) tl(1of2)"> |
678
|
|
|
<div |
679
|
|
|
data-c-border="all(thin, solid, black)" |
680
|
|
|
data-c-radius="rounded" |
681
|
|
|
data-c-padding="normal" |
682
|
|
|
> |
683
|
|
|
<p data-c-font-weight="bold" data-c-margin="bottom(normal)"> |
684
|
|
|
<FormattedMessage |
685
|
|
|
id="jobBuilder.skills.statusSmiley.essentialTitle" |
686
|
|
|
defaultMessage="Number of Essential Skills" |
687
|
|
|
description="Title of skill status tracker" |
688
|
|
|
/> |
689
|
|
|
</p> |
690
|
|
|
{/* TODO: SmileyStatusIndicator can be extracted as its own component, since its already repeated within this page. */} |
691
|
|
|
{/* This is the new smiley status indicator component. It is reused twice on this page, once to indicate how many ESSENTIAL skills the user has selected, and a second time to indicate the TOTAL number of skills selected. The component functions the same way for both instances, but the ***scale is different***. There's a chance that the labels will be different too, so best to build it with that in mind. You can activate the appropriate smiley by assigning an "active" class to the relevant "jpb-skill-measure-item" element. See the UI in-browser for an example of what this looks like. */} |
692
|
|
|
<div |
693
|
|
|
data-c-grid="gutter" |
694
|
|
|
data-c-align="centre" |
695
|
|
|
data-c-padding="top(normal)" |
696
|
|
|
> |
697
|
|
|
<div |
698
|
|
|
className={`jpb-skill-measure-item bad ${ |
699
|
|
|
countInRange(0, 1, essentialCount) ? "active" : "" |
700
|
|
|
}`} |
701
|
|
|
data-c-grid-item="base(1of5)" |
702
|
|
|
> |
703
|
|
|
{/* This div appears in each step of the indicator, but we need the number inside the "span" to reflect the number of skills currently selected (within the context of the indicator, i.e. only show the number of essential skills selected in the essential indicator). */} |
704
|
|
|
<div> |
705
|
|
|
<img |
706
|
|
|
src={smileyArrowBadImg} |
707
|
|
|
alt={intl.formatMessage(altMessages.unhappyArrow)} |
708
|
|
|
/> |
709
|
|
|
<span |
710
|
|
|
data-c-font-weight="bold" |
711
|
|
|
data-c-colour="white" |
712
|
|
|
data-c-font-size="small" |
713
|
|
|
> |
714
|
|
|
{essentialCount} |
715
|
|
|
</span> |
716
|
|
|
</div> |
717
|
|
|
<img |
718
|
|
|
src={smileyBadImg} |
719
|
|
|
alt={intl.formatMessage(altMessages.unhappySmiley)} |
720
|
|
|
/> |
721
|
|
|
<img |
722
|
|
|
src={smileyBadGreyImg} |
723
|
|
|
alt={intl.formatMessage(altMessages.unhappyGraySmiley)} |
724
|
|
|
/> |
725
|
|
|
<p data-c-font-size="small" data-c-font-weight="bold"> |
726
|
|
|
<FormattedMessage |
727
|
|
|
id="jobBuilder.skills.statusSmiley.essential.tooFew" |
728
|
|
|
defaultMessage="Too Few" |
729
|
|
|
description="Description of quantity of skills" |
730
|
|
|
/> |
731
|
|
|
</p> |
732
|
|
|
<p data-c-font-size="small">0 - 1</p> |
733
|
|
|
</div> |
734
|
|
|
<div |
735
|
|
|
className={`jpb-skill-measure-item medium ${ |
736
|
|
|
countInRange(2, 3, essentialCount) ? "active" : "" |
737
|
|
|
}`} |
738
|
|
|
data-c-grid-item="base(1of5)" |
739
|
|
|
> |
740
|
|
|
<div> |
741
|
|
|
<img |
742
|
|
|
src={smileyArrowMediumImg} |
743
|
|
|
alt={intl.formatMessage(altMessages.neutralArrow)} |
744
|
|
|
/> |
745
|
|
|
<span |
746
|
|
|
data-c-font-weight="bold" |
747
|
|
|
data-c-colour="white" |
748
|
|
|
data-c-font-size="small" |
749
|
|
|
> |
750
|
|
|
{essentialCount} |
751
|
|
|
</span> |
752
|
|
|
</div> |
753
|
|
|
<img |
754
|
|
|
src={smileyMediumImg} |
755
|
|
|
alt={intl.formatMessage(altMessages.neutralSmiley)} |
756
|
|
|
/> |
757
|
|
|
<img |
758
|
|
|
src={smileyMediumGreyImg} |
759
|
|
|
alt={intl.formatMessage(altMessages.neutralGraySmiley)} |
760
|
|
|
/> |
761
|
|
|
<p data-c-font-size="small" data-c-font-weight="bold"> |
762
|
|
|
<FormattedMessage |
763
|
|
|
id="jobBuilder.skills.statusSmiley.essential.almost" |
764
|
|
|
defaultMessage="Almost" |
765
|
|
|
description="Description of quantity of skills" |
766
|
|
|
/> |
767
|
|
|
</p> |
768
|
|
|
<p data-c-font-size="small">2 - 3</p> |
769
|
|
|
</div> |
770
|
|
|
<div |
771
|
|
|
className={`jpb-skill-measure-item good ${ |
772
|
|
|
countInRange(4, 6, essentialCount) ? "active" : "" |
773
|
|
|
}`} |
774
|
|
|
data-c-grid-item="base(1of5)" |
775
|
|
|
> |
776
|
|
|
<div> |
777
|
|
|
<img |
778
|
|
|
src={smileyArrowGoodImg} |
779
|
|
|
alt={intl.formatMessage(altMessages.happyArrow)} |
780
|
|
|
/> |
781
|
|
|
<span |
782
|
|
|
data-c-font-weight="bold" |
783
|
|
|
data-c-colour="white" |
784
|
|
|
data-c-font-size="small" |
785
|
|
|
> |
786
|
|
|
{essentialCount} |
787
|
|
|
</span> |
788
|
|
|
</div> |
789
|
|
|
<img |
790
|
|
|
src={smileyGoodImg} |
791
|
|
|
alt={intl.formatMessage(altMessages.happySmiley)} |
792
|
|
|
/> |
793
|
|
|
<img |
794
|
|
|
src={smileyGoodGreyImg} |
795
|
|
|
alt={intl.formatMessage(altMessages.happyGraySmiley)} |
796
|
|
|
/> |
797
|
|
|
<p data-c-font-size="small" data-c-font-weight="bold"> |
798
|
|
|
<FormattedMessage |
799
|
|
|
id="jobBuilder.skills.statusSmiley.essential.awesome" |
800
|
|
|
defaultMessage="Awesome" |
801
|
|
|
description="Description of quantity of skills" |
802
|
|
|
/> |
803
|
|
|
</p> |
804
|
|
|
<p data-c-font-size="small">4 - 6</p> |
805
|
|
|
</div> |
806
|
|
|
<div |
807
|
|
|
className={`jpb-skill-measure-item medium ${ |
808
|
|
|
countInRange(7, 8, essentialCount) ? "active" : "" |
809
|
|
|
}`} |
810
|
|
|
data-c-grid-item="base(1of5)" |
811
|
|
|
> |
812
|
|
|
<div> |
813
|
|
|
<img |
814
|
|
|
src={smileyArrowMediumImg} |
815
|
|
|
alt={intl.formatMessage(altMessages.neutralArrow)} |
816
|
|
|
/> |
817
|
|
|
<span |
818
|
|
|
data-c-font-weight="bold" |
819
|
|
|
data-c-colour="white" |
820
|
|
|
data-c-font-size="small" |
821
|
|
|
> |
822
|
|
|
{essentialCount} |
823
|
|
|
</span> |
824
|
|
|
</div> |
825
|
|
|
<img |
826
|
|
|
src={smileyMediumImg} |
827
|
|
|
alt={intl.formatMessage(altMessages.neutralSmiley)} |
828
|
|
|
/> |
829
|
|
|
<img |
830
|
|
|
src={smileyMediumGreyImg} |
831
|
|
|
alt={intl.formatMessage(altMessages.neutralGraySmiley)} |
832
|
|
|
/> |
833
|
|
|
<p data-c-font-size="small" data-c-font-weight="bold"> |
834
|
|
|
<FormattedMessage |
835
|
|
|
id="jobBuilder.skills.statusSmiley.essential.acceptable" |
836
|
|
|
defaultMessage="Acceptable" |
837
|
|
|
description="Description of quantity of skills" |
838
|
|
|
/> |
839
|
|
|
</p> |
840
|
|
|
<p data-c-font-size="small">7 - 8</p> |
841
|
|
|
</div> |
842
|
|
|
<div |
843
|
|
|
className={`jpb-skill-measure-item bad ${ |
844
|
|
|
essentialCount >= 9 ? "active" : "" |
845
|
|
|
}`} |
846
|
|
|
data-c-grid-item="base(1of5)" |
847
|
|
|
> |
848
|
|
|
<div> |
849
|
|
|
{/* TODO: Alt Text Translations */} |
850
|
|
|
<img |
851
|
|
|
src={smileyArrowBadImg} |
852
|
|
|
alt={intl.formatMessage(altMessages.unhappyArrow)} |
853
|
|
|
/> |
854
|
|
|
<span |
855
|
|
|
data-c-font-weight="bold" |
856
|
|
|
data-c-colour="white" |
857
|
|
|
data-c-font-size="small" |
858
|
|
|
> |
859
|
|
|
{essentialCount} |
860
|
|
|
</span> |
861
|
|
|
</div> |
862
|
|
|
<img |
863
|
|
|
src={smileyBadImg} |
864
|
|
|
alt={intl.formatMessage(altMessages.unhappySmiley)} |
865
|
|
|
/> |
866
|
|
|
<img |
867
|
|
|
src={smileyBadGreyImg} |
868
|
|
|
alt={intl.formatMessage(altMessages.unhappyGraySmiley)} |
869
|
|
|
/> |
870
|
|
|
<p data-c-font-size="small" data-c-font-weight="bold"> |
871
|
|
|
<FormattedMessage |
872
|
|
|
id="jobBuilder.skills.statusSmiley.essential.tooMany" |
873
|
|
|
defaultMessage="Too Many" |
874
|
|
|
description="Description of quantity of skills" |
875
|
|
|
/> |
876
|
|
|
</p> |
877
|
|
|
<p data-c-font-size="small">9 +</p> |
878
|
|
|
</div> |
879
|
|
|
</div> |
880
|
|
|
</div> |
881
|
|
|
</div> |
882
|
|
|
<div data-c-grid-item="base(1of1) tl(1of2)"> |
883
|
|
|
<div |
884
|
|
|
data-c-border="all(thin, solid, black)" |
885
|
|
|
data-c-radius="rounded" |
886
|
|
|
data-c-padding="normal" |
887
|
|
|
> |
888
|
|
|
<p data-c-font-weight="bold" data-c-margin="bottom(normal)"> |
889
|
|
|
<FormattedMessage |
890
|
|
|
id="jobBuilder.skills.statusSmiley.title" |
891
|
|
|
defaultMessage="Total Number of Skills" |
892
|
|
|
description="Title of skill quantity evaluator" |
893
|
|
|
/> |
894
|
|
|
</p> |
895
|
|
|
{/* This is the second smiley indicator, used for total skills. Note the difference in the scale from the first. */} |
896
|
|
|
<div |
897
|
|
|
data-c-grid="gutter" |
898
|
|
|
data-c-align="centre" |
899
|
|
|
data-c-padding="top(normal)" |
900
|
|
|
> |
901
|
|
|
<div |
902
|
|
|
className={`jpb-skill-measure-item bad ${ |
903
|
|
|
countInRange(0, 3, skillCount) ? "active" : "" |
904
|
|
|
}`} |
905
|
|
|
data-c-grid-item="base(1of5)" |
906
|
|
|
> |
907
|
|
|
<div> |
908
|
|
|
<img |
909
|
|
|
src={smileyArrowBadImg} |
910
|
|
|
alt={intl.formatMessage(altMessages.unhappyArrow)} |
911
|
|
|
/> |
912
|
|
|
<span |
913
|
|
|
data-c-font-weight="bold" |
914
|
|
|
data-c-colour="white" |
915
|
|
|
data-c-font-size="small" |
916
|
|
|
> |
917
|
|
|
{skillCount} |
918
|
|
|
</span> |
919
|
|
|
</div> |
920
|
|
|
<img |
921
|
|
|
src={smileyBadImg} |
922
|
|
|
alt={intl.formatMessage(altMessages.unhappySmiley)} |
923
|
|
|
/> |
924
|
|
|
<img |
925
|
|
|
src={smileyBadGreyImg} |
926
|
|
|
alt={intl.formatMessage(altMessages.unhappyGraySmiley)} |
927
|
|
|
/> |
928
|
|
|
<p data-c-font-size="small" data-c-font-weight="bold"> |
929
|
|
|
<FormattedMessage |
930
|
|
|
id="jobBuilder.skills.statusSmiley.tooFew" |
931
|
|
|
defaultMessage="Too Few" |
932
|
|
|
description="Description of quantity of skills" |
933
|
|
|
/> |
934
|
|
|
</p> |
935
|
|
|
<p data-c-font-size="small">0 - 3</p> |
936
|
|
|
</div> |
937
|
|
|
<div |
938
|
|
|
className={`jpb-skill-measure-item medium ${ |
939
|
|
|
countInRange(4, 6, skillCount) ? "active" : "" |
940
|
|
|
}`} |
941
|
|
|
data-c-grid-item="base(1of5)" |
942
|
|
|
> |
943
|
|
|
<div> |
944
|
|
|
<img |
945
|
|
|
src={smileyArrowMediumImg} |
946
|
|
|
alt={intl.formatMessage(altMessages.neutralArrow)} |
947
|
|
|
/> |
948
|
|
|
<span |
949
|
|
|
data-c-font-weight="bold" |
950
|
|
|
data-c-colour="white" |
951
|
|
|
data-c-font-size="small" |
952
|
|
|
> |
953
|
|
|
{skillCount} |
954
|
|
|
</span> |
955
|
|
|
</div> |
956
|
|
|
<img |
957
|
|
|
src={smileyMediumImg} |
958
|
|
|
alt={intl.formatMessage(altMessages.neutralSmiley)} |
959
|
|
|
/> |
960
|
|
|
<img |
961
|
|
|
src={smileyMediumGreyImg} |
962
|
|
|
alt={intl.formatMessage(altMessages.neutralGraySmiley)} |
963
|
|
|
/> |
964
|
|
|
<p data-c-font-size="small" data-c-font-weight="bold"> |
965
|
|
|
<FormattedMessage |
966
|
|
|
id="jobBuilder.skills.statusSmiley.almost" |
967
|
|
|
defaultMessage="Almost" |
968
|
|
|
description="Description of quantity of skills" |
969
|
|
|
/> |
970
|
|
|
</p> |
971
|
|
|
<p data-c-font-size="small">4 - 6</p> |
972
|
|
|
</div> |
973
|
|
|
<div |
974
|
|
|
className={`jpb-skill-measure-item good ${ |
975
|
|
|
countInRange(7, 8, skillCount) ? "active" : "" |
976
|
|
|
}`} |
977
|
|
|
data-c-grid-item="base(1of5)" |
978
|
|
|
> |
979
|
|
|
<div> |
980
|
|
|
<img |
981
|
|
|
src={smileyArrowGoodImg} |
982
|
|
|
alt={intl.formatMessage(altMessages.happyArrow)} |
983
|
|
|
/> |
984
|
|
|
<span |
985
|
|
|
data-c-font-weight="bold" |
986
|
|
|
data-c-colour="white" |
987
|
|
|
data-c-font-size="small" |
988
|
|
|
> |
989
|
|
|
{skillCount} |
990
|
|
|
</span> |
991
|
|
|
</div> |
992
|
|
|
<img |
993
|
|
|
src={smileyGoodImg} |
994
|
|
|
alt={intl.formatMessage(altMessages.happySmiley)} |
995
|
|
|
/> |
996
|
|
|
<img |
997
|
|
|
src={smileyGoodGreyImg} |
998
|
|
|
alt={intl.formatMessage(altMessages.happyGraySmiley)} |
999
|
|
|
/> |
1000
|
|
|
<p data-c-font-size="small" data-c-font-weight="bold"> |
1001
|
|
|
<FormattedMessage |
1002
|
|
|
id="jobBuilder.skills.statusSmiley.awesome" |
1003
|
|
|
defaultMessage="Awesome" |
1004
|
|
|
description="Description of quantity of skills" |
1005
|
|
|
/> |
1006
|
|
|
</p> |
1007
|
|
|
<p data-c-font-size="small">7 - 8</p> |
1008
|
|
|
</div> |
1009
|
|
|
<div |
1010
|
|
|
className={`jpb-skill-measure-item medium ${ |
1011
|
|
|
countInRange(9, 10, skillCount) ? "active" : "" |
1012
|
|
|
}`} |
1013
|
|
|
data-c-grid-item="base(1of5)" |
1014
|
|
|
> |
1015
|
|
|
<div> |
1016
|
|
|
<img |
1017
|
|
|
src={smileyArrowMediumImg} |
1018
|
|
|
alt={intl.formatMessage(altMessages.neutralArrow)} |
1019
|
|
|
/> |
1020
|
|
|
<span |
1021
|
|
|
data-c-font-weight="bold" |
1022
|
|
|
data-c-colour="white" |
1023
|
|
|
data-c-font-size="small" |
1024
|
|
|
> |
1025
|
|
|
{skillCount} |
1026
|
|
|
</span> |
1027
|
|
|
</div> |
1028
|
|
|
<img |
1029
|
|
|
src={smileyMediumImg} |
1030
|
|
|
alt={intl.formatMessage(altMessages.neutralSmiley)} |
1031
|
|
|
/> |
1032
|
|
|
<img |
1033
|
|
|
src={smileyMediumGreyImg} |
1034
|
|
|
alt={intl.formatMessage(altMessages.neutralGraySmiley)} |
1035
|
|
|
/> |
1036
|
|
|
<p data-c-font-size="small" data-c-font-weight="bold"> |
1037
|
|
|
<FormattedMessage |
1038
|
|
|
id="jobBuilder.skills.statusSmiley.acceptable" |
1039
|
|
|
defaultMessage="Acceptable" |
1040
|
|
|
description="Description of quantity of skills" |
1041
|
|
|
/> |
1042
|
|
|
</p> |
1043
|
|
|
<p data-c-font-size="small">9 - 10</p> |
1044
|
|
|
</div> |
1045
|
|
|
<div |
1046
|
|
|
className={`jpb-skill-measure-item bad ${ |
1047
|
|
|
skillCount >= 11 ? "active" : "" |
1048
|
|
|
}`} |
1049
|
|
|
data-c-grid-item="base(1of5)" |
1050
|
|
|
> |
1051
|
|
|
<div> |
1052
|
|
|
<img |
1053
|
|
|
src={smileyArrowBadImg} |
1054
|
|
|
alt={intl.formatMessage(altMessages.unhappyArrow)} |
1055
|
|
|
/> |
1056
|
|
|
<span |
1057
|
|
|
data-c-font-weight="bold" |
1058
|
|
|
data-c-colour="white" |
1059
|
|
|
data-c-font-size="small" |
1060
|
|
|
> |
1061
|
|
|
{skillCount} |
1062
|
|
|
</span> |
1063
|
|
|
</div> |
1064
|
|
|
<img |
1065
|
|
|
src={smileyBadImg} |
1066
|
|
|
alt={intl.formatMessage(altMessages.unhappySmiley)} |
1067
|
|
|
/> |
1068
|
|
|
<img |
1069
|
|
|
src={smileyBadGreyImg} |
1070
|
|
|
alt={intl.formatMessage(altMessages.unhappyGraySmiley)} |
1071
|
|
|
/> |
1072
|
|
|
<p data-c-font-size="small" data-c-font-weight="bold"> |
1073
|
|
|
<FormattedMessage |
1074
|
|
|
id="jobBuilder.skills.statusSmiley.tooMany" |
1075
|
|
|
defaultMessage="Too Many" |
1076
|
|
|
description="Description of quantity of skills" |
1077
|
|
|
/> |
1078
|
|
|
</p> |
1079
|
|
|
<p data-c-font-size="small">11 +</p> |
1080
|
|
|
</div> |
1081
|
|
|
</div> |
1082
|
|
|
</div> |
1083
|
|
|
</div> |
1084
|
|
|
</div> |
1085
|
|
|
{/* This element is the skills list/management area for the user. From here they can see the skills they've added, modify the order, see the type (occupational [based on classification], cultural, future), see the level they've selected (only if the skill isn't an asset skill), edit the skill, and remove it. */} |
1086
|
|
|
<div |
1087
|
|
|
data-c-background="grey(10)" |
1088
|
|
|
data-c-radius="rounded" |
1089
|
|
|
data-c-padding="all(normal)" |
1090
|
|
|
data-c-margin="top(normal) bottom(normal)" |
1091
|
|
|
> |
1092
|
|
|
<p data-c-font-weight="bold" data-c-margin="bottom(normal)"> |
1093
|
|
|
<FormattedMessage |
1094
|
|
|
id="jobBuilder.skills.title.essentialSkills" |
1095
|
|
|
defaultMessage="Essential Skills" |
1096
|
|
|
description="Title of essential skills list" |
1097
|
|
|
/> |
1098
|
|
|
</p> |
1099
|
|
|
{/* This is the null state to be used when the user lands on the page for the first time. Be sure to include it in the assets list too! Note that it exists outside the skill-list div to avoid it being confused with the list of skills. */} |
1100
|
|
|
{/* Null state. */} |
1101
|
|
|
{essentialCount === 0 && renderNullCriteriaRow()} |
1102
|
|
|
<ol className="jpb-skill-list" data-tc-up-down-list> |
1103
|
|
|
{/* This is an individual skill. I've handled the up/down script and the modal trigger, but I'll leave managing the value of the skill's list number, the modal contents, and the deletion to you folks. I've also migrated the up/down script to a universal one. When it comes to the "jpb-skill", you'll need to add a class that specifies which TYPE of skill it is (occupational, cultural, future). This will handle interior colour/icon changes. */} |
1104
|
|
|
{essentialCriteria.map(renderCriteriaRow)} |
1105
|
|
|
</ol> |
1106
|
|
|
{/* Repeat what you have above for asset skills. The biggest thing to note here is that the level should be empty in this list, and when the user changes the level of an essential skill to asset, it should be moved down into this list (and vice versa). */} |
1107
|
|
|
<p |
1108
|
|
|
data-c-font-weight="bold" |
1109
|
|
|
data-c-margin="top(normal) bottom(normal)" |
1110
|
|
|
> |
1111
|
|
|
<FormattedMessage |
1112
|
|
|
id="jobBuilder.skills.title.assetSkills" |
1113
|
|
|
defaultMessage="Asset Skills" |
1114
|
|
|
description="Title of asset skills list" |
1115
|
|
|
/> |
1116
|
|
|
</p> |
1117
|
|
|
{/* Asset null state goes here. */} |
1118
|
|
|
{assetCount === 0 && renderNullCriteriaRow()} |
1119
|
|
|
<ol className="jpb-skill-list" data-tc-up-down-list> |
1120
|
|
|
{assetCriteria.map(renderCriteriaRow)} |
1121
|
|
|
</ol> |
1122
|
|
|
</div> |
1123
|
|
|
<div |
1124
|
|
|
data-c-margin="bottom(triple)" |
1125
|
|
|
data-c-align="base(centre) tl(right)" |
1126
|
|
|
> |
1127
|
|
|
{/* We'll want this button to functionally be the exact same as the button at the bottom of the page, where it saves the data, and opens the preview modal. */} |
1128
|
|
|
{submitButton} |
1129
|
|
|
</div> |
1130
|
|
|
{/* The 3 sections below are each functionally similar and can probably be united into one component. The biggest difference between the three is that "Cultural Skills" has a categorical breakdown between "Recommended Skills" and the rest of the category. These recommendations are based directly on the way the manager answered their work environment questions, but I'm not sure how the logic works, so you'll want to check in with Lauren/Jasmita on this. */} |
1131
|
|
|
<h4 |
1132
|
|
|
data-c-colour="c2" |
1133
|
|
|
data-c-font-size="h4" |
1134
|
|
|
data-c-margin="bottom(normal)" |
1135
|
|
|
> |
1136
|
|
|
<FormattedMessage |
1137
|
|
|
id="jobBuilder.skills.title.skillSelection" |
1138
|
|
|
defaultMessage="Skill Selection" |
1139
|
|
|
description="Title of skill selection section" |
1140
|
|
|
/> |
1141
|
|
|
</h4> |
1142
|
|
|
{/* Occupational Skills */} |
1143
|
|
|
{/* You can modify colour/icon using the category classes here again (occupational, cultural, future) on the "jpb-skill-category" element. */} |
1144
|
|
|
<div |
1145
|
|
|
id="jpb-occupational-skills" |
1146
|
|
|
className="jpb-skill-category occupational" |
1147
|
|
|
data-c-margin="bottom(normal)" |
1148
|
|
|
data-c-padding="normal" |
1149
|
|
|
data-c-radius="rounded" |
1150
|
|
|
data-c-background="grey(10)" |
1151
|
|
|
> |
1152
|
|
|
<div data-c-grid="gutter top"> |
1153
|
|
|
<div data-c-grid-item="tp(2of3) ds(3of4)"> |
1154
|
|
|
<h5 |
1155
|
|
|
className="jpb-skill-section-title" |
1156
|
|
|
data-c-font-size="h4" |
1157
|
|
|
data-c-margin="bottom(normal)" |
1158
|
|
|
> |
1159
|
|
|
{/* These icons will change automatically based on the class specified above. */} |
1160
|
|
|
<span |
1161
|
|
|
data-c-font-size="small" |
1162
|
|
|
data-c-margin="right(half)" |
1163
|
|
|
data-c-padding="tb(quarter) rl(half)" |
1164
|
|
|
data-c-radius="rounded" |
1165
|
|
|
data-c-colour="white" |
1166
|
|
|
> |
1167
|
|
|
<i className="fas fa-briefcase" /> |
1168
|
|
|
<i className="fas fa-coffee" /> |
1169
|
|
|
<i className="fas fa-certificate" /> |
1170
|
|
|
<i className="fas fa-book" /> |
1171
|
|
|
</span> |
1172
|
|
|
{/* Category Title */} |
1173
|
|
|
<FormattedMessage |
1174
|
|
|
id="jobBuilder.skills.title.occupationalSkills" |
1175
|
|
|
defaultMessage="Occupational Competencies" |
1176
|
|
|
description="Title of skills category" |
1177
|
|
|
/> |
1178
|
|
|
</h5> |
1179
|
|
|
{/* Category description - basically this outlines what the category means. */} |
1180
|
|
|
{/* <p> |
1181
|
|
|
// TODO: Add this message back in once we have copy. |
1182
|
|
|
<FormattedMessage |
1183
|
|
|
id="jobBuilder.skills.description.occupationalSkills" |
1184
|
|
|
defaultMessage="" |
1185
|
|
|
description="Description of a category of skills" |
1186
|
|
|
/> |
1187
|
|
|
</p> */} |
1188
|
|
|
</div> |
1189
|
|
|
<div |
1190
|
|
|
data-c-grid-item="tp(1of3) ds(1of4)" |
1191
|
|
|
data-c-align="base(centre) tp(right)" |
1192
|
|
|
> |
1193
|
|
|
{/* This target value changes depending on the category (occupational has 3 - 4, cultural and future have fewer) - you can see these values in their respective sections below. You can also add a "complete" class to this "jpb-skill-target" element to change the target icon to a checkmark to indicate to the user that they're within the range. Note that the other two categories (cultural and future) start their ranges at 0, so the "complete" class should be on those sections by default. */} |
1194
|
|
|
<div |
1195
|
|
|
className={`jpb-skill-target ${ |
1196
|
|
|
countInRange( |
1197
|
|
|
minOccupational, |
1198
|
|
|
maxOccupational, |
1199
|
|
|
occupationalCriteria.length, |
1200
|
|
|
) |
1201
|
|
|
? "complete" |
1202
|
|
|
: "" |
1203
|
|
|
}`} |
1204
|
|
|
> |
1205
|
|
|
<i data-c-colour="stop" className="fas fa-bullseye" /> |
1206
|
|
|
<i data-c-colour="go" className="fas fa-check" /> |
1207
|
|
|
<span> |
1208
|
|
|
<FormattedMessage |
1209
|
|
|
id="jobBuilder.skills.range.occupationalSkills" |
1210
|
|
|
defaultMessage="Aim for {minOccupational} - {maxOccupational} skills." |
1211
|
|
|
description="Ranage recommendation for occupational competencies in job poster" |
1212
|
|
|
values={{ minOccupational, maxOccupational }} |
1213
|
|
|
/> |
1214
|
|
|
</span> |
1215
|
|
|
</div> |
1216
|
|
|
</div> |
1217
|
|
|
{/* This is the list of skills. Clicking a skill button should trigger the "Edit skill" modal so that the user can edit the definition/level before adding it. If they DO add it, you can assign an "active" class to the respective button so indicate that it's selected. This will change it's colour and icon automatically. This is also the area where "Culture Skills" is split into the two categories - see the Culture Skills section below for what that looks like. */} |
1218
|
|
|
{(job.classification_id === undefined || |
1219
|
|
|
job.classification_id === null) && ( |
1220
|
|
|
<p data-c-font-weight="bold" data-c-grid-item="base(1of1)"> |
1221
|
|
|
<FormattedMessage |
1222
|
|
|
id="jobBuilder.skills.nullText.occupationalSkills" |
1223
|
|
|
defaultMessage="You must return to Step 1 and choose a Classification." |
1224
|
|
|
description="Placeholder text for occupational competencies list." |
1225
|
|
|
/> |
1226
|
|
|
</p> |
1227
|
|
|
)} |
1228
|
|
|
|
1229
|
|
|
<ul className="jpb-skill-cloud" data-c-grid-item="base(1of1)"> |
1230
|
|
|
{occupationalSkills.map(renderSkillButton)} |
1231
|
|
|
</ul> |
1232
|
|
|
</div> |
1233
|
|
|
</div> |
1234
|
|
|
{/* Cultural Skills */} |
1235
|
|
|
{/* This section is here so that you can see the categorical division of culture skills. */} |
1236
|
|
|
<div |
1237
|
|
|
className="jpb-skill-category cultural" |
1238
|
|
|
data-c-margin="bottom(normal)" |
1239
|
|
|
data-c-padding="normal" |
1240
|
|
|
data-c-radius="rounded" |
1241
|
|
|
data-c-background="grey(10)" |
1242
|
|
|
> |
1243
|
|
|
<div data-c-grid="gutter top"> |
1244
|
|
|
<div data-c-grid-item="tp(2of3) ds(3of4)"> |
1245
|
|
|
<h5 |
1246
|
|
|
className="jpb-skill-section-title" |
1247
|
|
|
data-c-font-size="h4" |
1248
|
|
|
data-c-margin="bottom(normal)" |
1249
|
|
|
> |
1250
|
|
|
<span |
1251
|
|
|
data-c-font-size="small" |
1252
|
|
|
data-c-margin="right(half)" |
1253
|
|
|
data-c-padding="tb(quarter) rl(half)" |
1254
|
|
|
data-c-radius="rounded" |
1255
|
|
|
data-c-colour="white" |
1256
|
|
|
> |
1257
|
|
|
<i className="fas fa-briefcase" /> |
1258
|
|
|
<i className="fas fa-coffee" /> |
1259
|
|
|
<i className="fas fa-certificate" /> |
1260
|
|
|
<i className="fas fa-book" /> |
1261
|
|
|
</span> |
1262
|
|
|
<FormattedMessage |
1263
|
|
|
id="jobBuilder.skills.title.culturalSkills" |
1264
|
|
|
defaultMessage="Behavioural Competencies" |
1265
|
|
|
description="Title of skills category" |
1266
|
|
|
/> |
1267
|
|
|
</h5> |
1268
|
|
|
{/* <p> |
1269
|
|
|
// TODO: Add this message back in once we have copy. |
1270
|
|
|
<FormattedMessage |
1271
|
|
|
id="jobBuilder.skills.description.culturalSkills" |
1272
|
|
|
defaultMessage="" |
1273
|
|
|
description="Description of a category of skills" |
1274
|
|
|
/> |
1275
|
|
|
</p> */} |
1276
|
|
|
</div> |
1277
|
|
|
<div |
1278
|
|
|
data-c-grid-item="tp(1of3) ds(1of4)" |
1279
|
|
|
data-c-align="base(centre) tp(right)" |
1280
|
|
|
> |
1281
|
|
|
<div |
1282
|
|
|
className={`jpb-skill-target ${ |
1283
|
|
|
countInRange(minCulture, maxCulture, cultureCriteria.length) |
1284
|
|
|
? "complete" |
1285
|
|
|
: "" |
1286
|
|
|
}`} |
1287
|
|
|
> |
1288
|
|
|
<i data-c-colour="stop" className="fas fa-bullseye" /> |
1289
|
|
|
<i data-c-colour="go" className="fas fa-check" /> |
1290
|
|
|
<span> |
1291
|
|
|
<FormattedMessage |
1292
|
|
|
id="jobBuilder.skills.range.culturalSkills" |
1293
|
|
|
defaultMessage="Aim for {minCulture} - {maxCulture} skills." |
1294
|
|
|
description="Range recommendation for behavioural competencies in job poster" |
1295
|
|
|
values={{ minCulture, maxCulture }} |
1296
|
|
|
/> |
1297
|
|
|
</span> |
1298
|
|
|
</div> |
1299
|
|
|
</div> |
1300
|
|
|
{/** Culture skills are intended to be split into two lists, Recommended and Remaining. Until the recommendation logic is nailed down, its just one. */} |
1301
|
|
|
<ul className="jpb-skill-cloud" data-c-grid-item="base(1of1)"> |
1302
|
|
|
{cultureSkills.map(renderSkillButton)} |
1303
|
|
|
</ul> |
1304
|
|
|
{/* So here's where culture skills get broken into categories. In theory this logic will be used down the road to break occupational skills into occupations (e.g. CS - UX Designer), but for now this the only instance where it happens. */} |
1305
|
|
|
{/* <ul className="jpb-skill-cloud" data-c-grid-item="base(1of1)"> |
1306
|
|
|
// Note that this "p" tag has a different margin value than the one in the "ul" below. |
1307
|
|
|
<p |
1308
|
|
|
data-c-font-weight="bold" |
1309
|
|
|
data-c-margin="top(half) bottom(normal)" |
1310
|
|
|
> |
1311
|
|
|
Recommended Skills: |
1312
|
|
|
</p> |
1313
|
|
|
// This is where the skill recommendations from Work Environment go. |
1314
|
|
|
<li> |
1315
|
|
|
<button |
1316
|
|
|
className="jpb-skill-trigger" |
1317
|
|
|
data-c-button="outline(c1)" |
1318
|
|
|
data-c-radius="rounded" |
1319
|
|
|
> |
1320
|
|
|
<i className="fas fa-plus-circle" /> |
1321
|
|
|
<i className="fas fa-minus-circle" /> |
1322
|
|
|
Skill Name |
1323
|
|
|
</button> |
1324
|
|
|
</li> |
1325
|
|
|
</ul> |
1326
|
|
|
<ul className="jpb-skill-cloud" data-c-grid-item="base(1of1)"> |
1327
|
|
|
<p |
1328
|
|
|
data-c-font-weight="bold" |
1329
|
|
|
data-c-margin="top(normal) bottom(normal)" |
1330
|
|
|
> |
1331
|
|
|
Remaining Skills: |
1332
|
|
|
</p> |
1333
|
|
|
// This is where the remaining culture skills go. Please make sure that the skills in the recommendation list above do not appear here. |
1334
|
|
|
<li> |
1335
|
|
|
<button |
1336
|
|
|
className="jpb-skill-trigger" |
1337
|
|
|
data-c-button="outline(c1)" |
1338
|
|
|
data-c-radius="rounded" |
1339
|
|
|
> |
1340
|
|
|
<i className="fas fa-plus-circle" /> |
1341
|
|
|
<i className="fas fa-minus-circle" /> |
1342
|
|
|
Skill Name |
1343
|
|
|
</button> |
1344
|
|
|
</li> |
1345
|
|
|
</ul> */} |
1346
|
|
|
</div> |
1347
|
|
|
</div> |
1348
|
|
|
{/* Future Skills */} |
1349
|
|
|
{/* This section is just here so you can see what it looks like with the future class. */} |
1350
|
|
|
<div |
1351
|
|
|
className="jpb-skill-category future" |
1352
|
|
|
data-c-margin="bottom(normal)" |
1353
|
|
|
data-c-padding="normal" |
1354
|
|
|
data-c-radius="rounded" |
1355
|
|
|
data-c-background="grey(10)" |
1356
|
|
|
> |
1357
|
|
|
<div data-c-grid="gutter top"> |
1358
|
|
|
<div data-c-grid-item="tp(2of3) ds(3of4)"> |
1359
|
|
|
<h5 |
1360
|
|
|
className="jpb-skill-section-title" |
1361
|
|
|
data-c-font-size="h4" |
1362
|
|
|
data-c-margin="bottom(normal)" |
1363
|
|
|
> |
1364
|
|
|
<span |
1365
|
|
|
data-c-font-size="small" |
1366
|
|
|
data-c-margin="right(half)" |
1367
|
|
|
data-c-padding="tb(quarter) rl(half)" |
1368
|
|
|
data-c-radius="rounded" |
1369
|
|
|
data-c-colour="white" |
1370
|
|
|
> |
1371
|
|
|
<i className="fas fa-briefcase" /> |
1372
|
|
|
<i className="fas fa-coffee" /> |
1373
|
|
|
<i className="fas fa-certificate" /> |
1374
|
|
|
<i className="fas fa-book" /> |
1375
|
|
|
</span> |
1376
|
|
|
<FormattedMessage |
1377
|
|
|
id="jobBuilder.skills.title.futureSkills" |
1378
|
|
|
defaultMessage="Public Service Competencies" |
1379
|
|
|
description="Title of skills category" |
1380
|
|
|
/> |
1381
|
|
|
</h5> |
1382
|
|
|
{/* <p> |
1383
|
|
|
// TODO: Add this message back in once we have copy. |
1384
|
|
|
<FormattedMessage |
1385
|
|
|
id="jobBuilder.skills.description.futureSkills" |
1386
|
|
|
defaultMessage="" |
1387
|
|
|
description="Description of a category of skills" |
1388
|
|
|
/> |
1389
|
|
|
</p> */} |
1390
|
|
|
</div> |
1391
|
|
|
<div |
1392
|
|
|
data-c-grid-item="tp(1of3) ds(1of4)" |
1393
|
|
|
data-c-align="base(centre) tp(right)" |
1394
|
|
|
> |
1395
|
|
|
<div |
1396
|
|
|
className={`jpb-skill-target ${ |
1397
|
|
|
countInRange(minFuture, maxFuture, futureCriteria.length) |
1398
|
|
|
? "complete" |
1399
|
|
|
: "" |
1400
|
|
|
}`} |
1401
|
|
|
> |
1402
|
|
|
<i data-c-colour="stop" className="fas fa-bullseye" /> |
1403
|
|
|
<i data-c-colour="go" className="fas fa-check" /> |
1404
|
|
|
<span> |
1405
|
|
|
<FormattedMessage |
1406
|
|
|
id="jobBuilder.skills.range.futureSkills" |
1407
|
|
|
defaultMessage="Aim for {minFuture} - {maxFuture} skills." |
1408
|
|
|
description="Ranage recommendation for public service competencies in job poster" |
1409
|
|
|
values={{ minFuture, maxFuture }} |
1410
|
|
|
/> |
1411
|
|
|
</span> |
1412
|
|
|
</div> |
1413
|
|
|
</div> |
1414
|
|
|
<ul className="jpb-skill-cloud" data-c-grid-item="base(1of1)"> |
1415
|
|
|
{futureSkills.map(renderSkillButton)} |
1416
|
|
|
</ul> |
1417
|
|
|
</div> |
1418
|
|
|
</div> |
1419
|
|
|
{/* This section is basically just text, but it prompts the manager to get in touch with us if they can't find the skill they're looking for. */} |
1420
|
|
|
{/* "Custom" Skills */} |
1421
|
|
|
<h5 data-c-font-weight="bold" data-c-margin="top(double) bottom(half)"> |
1422
|
|
|
<FormattedMessage |
1423
|
|
|
id="jobBuilder.skills.title.missingSkill" |
1424
|
|
|
defaultMessage="Can't find the skill you need?" |
1425
|
|
|
description="Title of instructions for missing skill" |
1426
|
|
|
/> |
1427
|
|
|
</h5> |
1428
|
|
|
<p data-c-margin="bottom(normal)"> |
1429
|
|
|
{/* TODO: Refactor for react-intl version 3, using new rich text xml tag syntax eg. |
1430
|
|
|
<FormattedMessage |
1431
|
|
|
id="jobBuilder.skills.instructions.missingSkills" |
1432
|
|
|
defaultMessage="Building a skills list is a huge endeavour, and it's not |
1433
|
|
|
surprising that Talent Cloud's list doesn't have the skill |
1434
|
|
|
you're looking for. To help us expand our skill list, please <link>get in touch with us through email</link>. Provide the skill's name, as well as a short description to |
1435
|
|
|
kick-off the discussion." |
1436
|
|
|
values={{ |
1437
|
|
|
link: msg => ( |
1438
|
|
|
<a href="mailto:[email protected]"> |
1439
|
|
|
{msg} |
1440
|
|
|
</a> |
1441
|
|
|
), |
1442
|
|
|
}} |
1443
|
|
|
/> */} |
1444
|
|
|
<FormattedMessage |
1445
|
|
|
id="jobBuilder.skills.instructions.missingSkills" |
1446
|
|
|
defaultMessage="Building a skills list is a huge endeavour, and it's not surprising that Talent Cloud's list doesn't have the skill you're looking for. To help us expand our skill list, please {link}. Provide the skill's name, as well as a short description to kick-off the discussion." |
1447
|
|
|
values={{ |
1448
|
|
|
link: ( |
1449
|
|
|
<a href="mailto:[email protected]"> |
1450
|
|
|
{intl.formatMessage(messages.emailUs)} |
1451
|
|
|
</a> |
1452
|
|
|
), |
1453
|
|
|
}} |
1454
|
|
|
/> |
1455
|
|
|
</p> |
1456
|
|
|
<div |
1457
|
|
|
className="jpb-skill-category optional" |
1458
|
|
|
data-c-margin="bottom(normal)" |
1459
|
|
|
data-c-padding="normal" |
1460
|
|
|
data-c-radius="rounded" |
1461
|
|
|
data-c-background="grey(10)" |
1462
|
|
|
> |
1463
|
|
|
<div data-c-grid="gutter top"> |
1464
|
|
|
<div data-c-grid-item="base(1of1)"> |
1465
|
|
|
{/** TODO: Fix the layout of the skill cloud */} |
1466
|
|
|
<h5 className="jpb-skill-section-title" data-c-font-size="h4"> |
1467
|
|
|
<span |
1468
|
|
|
data-c-font-size="small" |
1469
|
|
|
data-c-margin="right(half)" |
1470
|
|
|
data-c-padding="tb(quarter) rl(half)" |
1471
|
|
|
data-c-radius="rounded" |
1472
|
|
|
data-c-colour="white" |
1473
|
|
|
> |
1474
|
|
|
<i className="fas fa-briefcase" /> |
1475
|
|
|
<i className="fas fa-coffee" /> |
1476
|
|
|
<i className="fas fa-certificate" /> |
1477
|
|
|
<i className="fas fa-book" /> |
1478
|
|
|
</span> |
1479
|
|
|
<FormattedMessage |
1480
|
|
|
id="jobBuilder.skills.title.otherSkills" |
1481
|
|
|
defaultMessage="Other Skills" |
1482
|
|
|
description="Title of other skills section" |
1483
|
|
|
/> |
1484
|
|
|
</h5> |
1485
|
|
|
</div> |
1486
|
|
|
<div data-c-grid-item="base(1of1)"> |
1487
|
|
|
<Select |
1488
|
|
|
id="jpb-all-skills-select" |
1489
|
|
|
name="jpbAllSkillsSelect" |
1490
|
|
|
label={intl.formatMessage(messages.selectSkillLabel)} |
1491
|
|
|
selected={null} |
1492
|
|
|
nullSelection={intl.formatMessage(messages.selectSkillNull)} |
1493
|
|
|
options={unselectedOtherSkills.map( |
1494
|
|
|
(skill): SelectOption => ({ |
1495
|
|
|
value: skill.id, |
1496
|
|
|
label: localizeFieldNonNull(locale, skill, "name"), |
1497
|
|
|
}), |
1498
|
|
|
)} |
1499
|
|
|
onChange={(event): void => { |
1500
|
|
|
const skillId = Number(event.target.value); |
1501
|
|
|
if (hasKey(skillsById, skillId)) { |
1502
|
|
|
const skill = skillsById[skillId]; |
1503
|
|
|
setSkillBeingAdded(skill); |
1504
|
|
|
} |
1505
|
|
|
}} |
1506
|
|
|
/> |
1507
|
|
|
</div> |
1508
|
|
|
<ul className="jpb-skill-cloud" data-c-grid-item="base(1of1)"> |
1509
|
|
|
{/** TODO: Get this null state text hiding/showing. */} |
1510
|
|
|
{selectedOtherSkills.length === 0 && ( |
1511
|
|
|
<p> |
1512
|
|
|
<FormattedMessage |
1513
|
|
|
id="jobBuilder.skills.placeholder.otherSkills" |
1514
|
|
|
defaultMessage="There are no extra skills added." |
1515
|
|
|
description="Placeholder when there are no other skills" |
1516
|
|
|
/> |
1517
|
|
|
</p> |
1518
|
|
|
)} |
1519
|
|
|
{selectedOtherSkills.map(renderSkillButton)} |
1520
|
|
|
</ul> |
1521
|
|
|
</div> |
1522
|
|
|
</div> |
1523
|
|
|
<div data-c-grid="gutter"> |
1524
|
|
|
<div data-c-grid-item="base(1of1)"> |
1525
|
|
|
<hr data-c-margin="top(normal) bottom(normal)" /> |
1526
|
|
|
</div> |
1527
|
|
|
<div |
1528
|
|
|
data-c-alignment="base(centre) tp(left)" |
1529
|
|
|
data-c-grid-item="tp(1of2)" |
1530
|
|
|
> |
1531
|
|
|
<button |
1532
|
|
|
data-c-button="outline(c2)" |
1533
|
|
|
data-c-radius="rounded" |
1534
|
|
|
type="button" |
1535
|
|
|
disabled={isSaving} |
1536
|
|
|
onClick={(): void => saveAndReturn()} |
1537
|
|
|
> |
1538
|
|
|
<FormattedMessage |
1539
|
|
|
id="jobBuilder.skills.button.returnToTasks" |
1540
|
|
|
defaultMessage="Save & Return to Tasks" |
1541
|
|
|
description="Button Label" |
1542
|
|
|
/> |
1543
|
|
|
</button> |
1544
|
|
|
</div> |
1545
|
|
|
<div |
1546
|
|
|
data-c-alignment="base(centre) tp(right)" |
1547
|
|
|
data-c-grid-item="tp(1of2)" |
1548
|
|
|
> |
1549
|
|
|
{/* Modal trigger, same as last step. */} |
1550
|
|
|
{submitButton} |
1551
|
|
|
|
1552
|
|
|
<div |
1553
|
|
|
role="alert" |
1554
|
|
|
data-c-alert="error" |
1555
|
|
|
data-c-radius="rounded" |
1556
|
|
|
data-c-margin="top(normal)" |
1557
|
|
|
data-c-padding="all(half)" |
1558
|
|
|
data-c-visibility={ |
1559
|
|
|
essentialCount === 0 && submitTouched ? "visible" : "invisible" |
1560
|
|
|
} |
1561
|
|
|
style={{ |
1562
|
|
|
display: `inline-block`, |
1563
|
|
|
}} |
1564
|
|
|
> |
1565
|
|
|
<a |
1566
|
|
|
href="#jpb-occupational-skills" |
1567
|
|
|
tabIndex={0} |
1568
|
|
|
ref={errorMessage} |
1569
|
|
|
> |
1570
|
|
|
<FormattedMessage |
1571
|
|
|
id="jobBuilder.skills.essentialSkillRequiredError" |
1572
|
|
|
defaultMessage="At least one 'Essential Skill' is required." |
1573
|
|
|
description="Label of Button" |
1574
|
|
|
/> |
1575
|
|
|
</a> |
1576
|
|
|
</div> |
1577
|
|
|
</div> |
1578
|
|
|
</div> |
1579
|
|
|
</div> |
1580
|
|
|
<div data-c-dialog-overlay={isModalVisible ? "active" : ""} /> |
1581
|
|
|
{/** This modal simply displays key tasks. */} |
1582
|
|
|
<Modal |
1583
|
|
|
id={tasksModalId} |
1584
|
|
|
parentElement={modalParentRef.current} |
1585
|
|
|
visible={tasksModalVisible} |
1586
|
|
|
onModalCancel={(): void => setTasksModalVisible(false)} |
1587
|
|
|
onModalConfirm={(): void => setTasksModalVisible(false)} |
1588
|
|
|
> |
1589
|
|
|
<Modal.Header> |
1590
|
|
|
<div |
1591
|
|
|
data-c-background="c1(100)" |
1592
|
|
|
data-c-border="bottom(thin, solid, black)" |
1593
|
|
|
data-c-padding="normal" |
1594
|
|
|
> |
1595
|
|
|
<h5 |
1596
|
|
|
data-c-colour="white" |
1597
|
|
|
data-c-font-size="h4" |
1598
|
|
|
id={`${tasksModalId}-title`} |
1599
|
|
|
> |
1600
|
|
|
<FormattedMessage |
1601
|
|
|
id="jobBuilder.skills.title.keyTasks" |
1602
|
|
|
defaultMessage="Key Tasks" |
1603
|
|
|
description="Title of Key Tasks Section" |
1604
|
|
|
/> |
1605
|
|
|
</h5> |
1606
|
|
|
</div> |
1607
|
|
|
</Modal.Header> |
1608
|
|
|
<Modal.Body> |
1609
|
|
|
<div data-c-border="bottom(thin, solid, black)"> |
1610
|
|
|
<div |
1611
|
|
|
data-c-border="bottom(thin, solid, black)" |
1612
|
|
|
data-c-padding="normal" |
1613
|
|
|
id={`${tasksModalId}-description`} |
1614
|
|
|
> |
1615
|
|
|
<ul> |
1616
|
|
|
{keyTasks.map( |
1617
|
|
|
(task): React.ReactElement => ( |
1618
|
|
|
<li key={task.id}> |
1619
|
|
|
{localizeField(locale, task, "description")} |
1620
|
|
|
</li> |
1621
|
|
|
), |
1622
|
|
|
)} |
1623
|
|
|
</ul> |
1624
|
|
|
</div> |
1625
|
|
|
</div> |
1626
|
|
|
</Modal.Body> |
1627
|
|
|
<Modal.Footer> |
1628
|
|
|
<Modal.FooterCancelBtn> |
1629
|
|
|
<FormattedMessage |
1630
|
|
|
id="jobBuilder.skills.tasksModalCancelLabel" |
1631
|
|
|
defaultMessage="Back to Skills" |
1632
|
|
|
description="The text displayed on the cancel button of the Key Tasks modal on the Job Builder Skills step." |
1633
|
|
|
/> |
1634
|
|
|
</Modal.FooterCancelBtn> |
1635
|
|
|
</Modal.Footer> |
1636
|
|
|
</Modal> |
1637
|
|
|
{/** This modal is for adding brand new skills */} |
1638
|
|
|
<Modal |
1639
|
|
|
id={addModalId} |
1640
|
|
|
parentElement={modalParentRef.current} |
1641
|
|
|
visible={skillBeingAdded !== null} |
1642
|
|
|
onModalCancel={(): void => { |
1643
|
|
|
setSkillBeingAdded(null); |
1644
|
|
|
}} |
1645
|
|
|
onModalConfirm={(): void => { |
1646
|
|
|
setSkillBeingAdded(null); |
1647
|
|
|
}} |
1648
|
|
|
> |
1649
|
|
|
<Modal.Header> |
1650
|
|
|
<div |
1651
|
|
|
data-c-background="c1(100)" |
1652
|
|
|
data-c-border="bottom(thin, solid, black)" |
1653
|
|
|
data-c-padding="normal" |
1654
|
|
|
> |
1655
|
|
|
<h5 |
1656
|
|
|
data-c-colour="white" |
1657
|
|
|
data-c-font-size="h4" |
1658
|
|
|
id={`${addModalId}-title`} |
1659
|
|
|
> |
1660
|
|
|
<FormattedMessage |
1661
|
|
|
id="jobBuilder.skills.title.addASkill" |
1662
|
|
|
defaultMessage="Add a skill" |
1663
|
|
|
description="Title of Add a skill Section" |
1664
|
|
|
/> |
1665
|
|
|
</h5> |
1666
|
|
|
</div> |
1667
|
|
|
</Modal.Header> |
1668
|
|
|
<Modal.Body> |
1669
|
|
|
{skillBeingAdded !== null && ( |
1670
|
|
|
<CriteriaForm |
1671
|
|
|
jobPosterId={job.id} |
1672
|
|
|
skill={skillBeingAdded} |
1673
|
|
|
handleCancel={(): void => { |
1674
|
|
|
setSkillBeingAdded(null); |
1675
|
|
|
}} |
1676
|
|
|
handleSubmit={(criteria: Criteria): void => { |
1677
|
|
|
criteriaDispatch({ type: "add", payload: criteria }); |
1678
|
|
|
setSkillBeingAdded(null); |
1679
|
|
|
}} |
1680
|
|
|
/> |
1681
|
|
|
)} |
1682
|
|
|
</Modal.Body> |
1683
|
|
|
</Modal> |
1684
|
|
|
{/** This modal is for editing already added skills */} |
1685
|
|
|
<Modal |
1686
|
|
|
id={editModalId} |
1687
|
|
|
parentElement={modalParentRef.current} |
1688
|
|
|
visible={criteriaBeingEdited !== null} |
1689
|
|
|
onModalCancel={(): void => { |
1690
|
|
|
setCriteriaBeingEdited(null); |
1691
|
|
|
}} |
1692
|
|
|
onModalConfirm={(): void => { |
1693
|
|
|
setCriteriaBeingEdited(null); |
1694
|
|
|
}} |
1695
|
|
|
> |
1696
|
|
|
<Modal.Header> |
1697
|
|
|
<div |
1698
|
|
|
data-c-background="c1(100)" |
1699
|
|
|
data-c-border="bottom(thin, solid, black)" |
1700
|
|
|
data-c-padding="normal" |
1701
|
|
|
> |
1702
|
|
|
<h5 |
1703
|
|
|
data-c-colour="white" |
1704
|
|
|
data-c-font-size="h4" |
1705
|
|
|
id={`${editModalId}-title`} |
1706
|
|
|
> |
1707
|
|
|
<FormattedMessage |
1708
|
|
|
id="jobBuilder.skills.title.editSkill" |
1709
|
|
|
defaultMessage="Edit skill" |
1710
|
|
|
description="Title of Edit skill Modal" |
1711
|
|
|
/> |
1712
|
|
|
</h5> |
1713
|
|
|
</div> |
1714
|
|
|
</Modal.Header> |
1715
|
|
|
<Modal.Body> |
1716
|
|
|
{criteriaBeingEdited !== null && |
1717
|
|
|
getSkillOfCriteria(criteriaBeingEdited) !== null && ( |
1718
|
|
|
<CriteriaForm |
1719
|
|
|
jobPosterId={job.id} |
1720
|
|
|
criteria={criteriaBeingEdited} |
1721
|
|
|
skill={getSkillOfCriteria(criteriaBeingEdited) as Skill} // The cast is okay here (but still not ideal) because of the !== null check a few lines up |
1722
|
|
|
handleCancel={(): void => { |
1723
|
|
|
setCriteriaBeingEdited(null); |
1724
|
|
|
}} |
1725
|
|
|
handleSubmit={(criteria: Criteria): void => { |
1726
|
|
|
criteriaDispatch({ type: "edit", payload: criteria }); |
1727
|
|
|
setCriteriaBeingEdited(null); |
1728
|
|
|
}} |
1729
|
|
|
/> |
1730
|
|
|
)} |
1731
|
|
|
</Modal.Body> |
1732
|
|
|
</Modal> |
1733
|
|
|
{/** This modal is the preview */} |
1734
|
|
|
<Modal |
1735
|
|
|
id={previewModalId} |
1736
|
|
|
parentElement={modalParentRef.current} |
1737
|
|
|
visible={isPreviewVisible} |
1738
|
|
|
onModalCancel={(): void => setIsPreviewVisible(false)} |
1739
|
|
|
onModalConfirm={(): void => handleContinue()} |
1740
|
|
|
onModalMiddle={(): void => { |
1741
|
|
|
handleSkipToReview(); |
1742
|
|
|
}} |
1743
|
|
|
> |
1744
|
|
|
<Modal.Header> |
1745
|
|
|
<div |
1746
|
|
|
data-c-background="c1(100)" |
1747
|
|
|
data-c-border="bottom(thin, solid, black)" |
1748
|
|
|
data-c-padding="normal" |
1749
|
|
|
> |
1750
|
|
|
<h5 |
1751
|
|
|
data-c-colour="white" |
1752
|
|
|
data-c-font-size="h4" |
1753
|
|
|
id={`${previewModalId}-title`} |
1754
|
|
|
> |
1755
|
|
|
<FormattedMessage |
1756
|
|
|
id="jobBuilder.skills.title.keepItUp" |
1757
|
|
|
defaultMessage="Keep it up!" |
1758
|
|
|
description="Title of Keep it up! Modal" |
1759
|
|
|
/> |
1760
|
|
|
</h5> |
1761
|
|
|
</div> |
1762
|
|
|
</Modal.Header> |
1763
|
|
|
<Modal.Body> |
1764
|
|
|
<div data-c-border="bottom(thin, solid, black)"> |
1765
|
|
|
<div |
1766
|
|
|
data-c-border="bottom(thin, solid, black)" |
1767
|
|
|
data-c-padding="normal" |
1768
|
|
|
id={`${previewModalId}-description`} |
1769
|
|
|
> |
1770
|
|
|
<p> |
1771
|
|
|
<FormattedMessage |
1772
|
|
|
id="jobBuilder.skills.description.keepItUp" |
1773
|
|
|
defaultMessage="Here's a preview of the Skills you just entered. Feel free to go back and edit things or move to the next step if you're happy with it." |
1774
|
|
|
description="Body text of Keep it up! Modal" |
1775
|
|
|
/> |
1776
|
|
|
</p> |
1777
|
|
|
</div> |
1778
|
|
|
|
1779
|
|
|
<div data-c-background="grey(20)" data-c-padding="normal"> |
1780
|
|
|
<div |
1781
|
|
|
className="manager-job-card" |
1782
|
|
|
data-c-background="white(100)" |
1783
|
|
|
data-c-padding="normal" |
1784
|
|
|
data-c-radius="rounded" |
1785
|
|
|
> |
1786
|
|
|
<h4 |
1787
|
|
|
data-c-border="bottom(thin, solid, black)" |
1788
|
|
|
data-c-font-size="h4" |
1789
|
|
|
data-c-font-weight="600" |
1790
|
|
|
data-c-margin="bottom(normal)" |
1791
|
|
|
data-c-padding="bottom(normal)" |
1792
|
|
|
> |
1793
|
|
|
<FormattedMessage |
1794
|
|
|
id="jobBuilder.skills.title.needsToHave" |
1795
|
|
|
defaultMessage="Skills the Employee Needs to Have" |
1796
|
|
|
description="Section Header in Modal" |
1797
|
|
|
/> |
1798
|
|
|
</h4> |
1799
|
|
|
{essentialCriteria.length === 0 ? ( |
1800
|
|
|
<p> |
1801
|
|
|
<FormattedMessage |
1802
|
|
|
id="jobBuilder.skills.nullState" |
1803
|
|
|
defaultMessage="You haven't added any skills yet." |
1804
|
|
|
description="The text displayed in the skills modal when you haven't added any skills." |
1805
|
|
|
/> |
1806
|
|
|
</p> |
1807
|
|
|
) : ( |
1808
|
|
|
essentialCriteria.map( |
1809
|
|
|
(criterion): React.ReactElement | null => { |
1810
|
|
|
const skill = getSkillOfCriteria(criterion); |
1811
|
|
|
if (skill === null) { |
1812
|
|
|
return null; |
1813
|
|
|
} |
1814
|
|
|
return ( |
1815
|
|
|
<Criterion |
1816
|
|
|
criterion={criterion} |
1817
|
|
|
skill={skill} |
1818
|
|
|
key={skill.id} |
1819
|
|
|
/> |
1820
|
|
|
); |
1821
|
|
|
}, |
1822
|
|
|
) |
1823
|
|
|
)} |
1824
|
|
|
<h4 |
1825
|
|
|
data-c-border="bottom(thin, solid, black)" |
1826
|
|
|
data-c-font-size="h4" |
1827
|
|
|
data-c-font-weight="600" |
1828
|
|
|
data-c-margin="top(double) bottom(normal)" |
1829
|
|
|
data-c-padding="bottom(normal)" |
1830
|
|
|
> |
1831
|
|
|
<FormattedMessage |
1832
|
|
|
id="jobBuilder.skills.title.niceToHave" |
1833
|
|
|
defaultMessage="Skills That Would Be Nice For the Employee to Have" |
1834
|
|
|
description="Section Header in Modal" |
1835
|
|
|
/> |
1836
|
|
|
</h4> |
1837
|
|
|
{assetCriteria.length === 0 ? ( |
1838
|
|
|
<p> |
1839
|
|
|
<FormattedMessage |
1840
|
|
|
id="jobBuilder.skills.nullState" |
1841
|
|
|
defaultMessage="You haven't added any skills yet." |
1842
|
|
|
description="The text displayed in the skills modal when you haven't added any skills." |
1843
|
|
|
/> |
1844
|
|
|
</p> |
1845
|
|
|
) : ( |
1846
|
|
|
assetCriteria.map((criterion): React.ReactElement | null => { |
1847
|
|
|
const skill = getSkillOfCriteria(criterion); |
1848
|
|
|
if (skill === null) { |
1849
|
|
|
return null; |
1850
|
|
|
} |
1851
|
|
|
return ( |
1852
|
|
|
<Criterion |
1853
|
|
|
criterion={criterion} |
1854
|
|
|
skill={skill} |
1855
|
|
|
key={skill.id} |
1856
|
|
|
/> |
1857
|
|
|
); |
1858
|
|
|
}) |
1859
|
|
|
)} |
1860
|
|
|
</div> |
1861
|
|
|
</div> |
1862
|
|
|
</div> |
1863
|
|
|
</Modal.Body> |
1864
|
|
|
<Modal.Footer> |
1865
|
|
|
<Modal.FooterCancelBtn> |
1866
|
|
|
<FormattedMessage |
1867
|
|
|
id="jobBuilder.skills.previewModalCancelLabel" |
1868
|
|
|
defaultMessage="Go Back" |
1869
|
|
|
description="The text displayed on the cancel button of the Job Builder Skills Preview modal." |
1870
|
|
|
/> |
1871
|
|
|
</Modal.FooterCancelBtn> |
1872
|
|
|
{jobIsComplete && ( |
1873
|
|
|
<Modal.FooterMiddleBtn> |
1874
|
|
|
<FormattedMessage |
1875
|
|
|
id="jobBuilder.skills.previewModalMiddleLabel" |
1876
|
|
|
defaultMessage="Skip to Review" |
1877
|
|
|
description="The text displayed on the 'Skip to Review' button of the Job Builder Skills Preview modal." |
1878
|
|
|
/> |
1879
|
|
|
</Modal.FooterMiddleBtn> |
1880
|
|
|
)} |
1881
|
|
|
<Modal.FooterConfirmBtn> |
1882
|
|
|
<FormattedMessage |
1883
|
|
|
id="jobBuilder.skills.previewModalConfirmLabel" |
1884
|
|
|
defaultMessage="Next Step" |
1885
|
|
|
description="The text displayed on the confirm button of the Job Builder Skills Preview modal." |
1886
|
|
|
/> |
1887
|
|
|
</Modal.FooterConfirmBtn> |
1888
|
|
|
</Modal.Footer> |
1889
|
|
|
</Modal> |
1890
|
|
|
</> |
1891
|
|
|
); |
1892
|
|
|
}; |
1893
|
|
|
|
1894
|
|
|
export default JobSkills; |
1895
|
|
|
|