Passed
Push — feature/settings-2fa ( 31e075...1e717f )
by Grant
19:37 queued 06:19
created

resources/assets/js/components/JobBuilder/Details/JobDetails.tsx   A

Complexity

Total Complexity 22
Complexity/F 0

Size

Lines of Code 1185
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 942
dl 0
loc 1185
rs 9.658
c 0
b 0
f 0
wmc 22
mnd 22
bc 22
fnc 0
bpm 0
cpm 0
noi 0
1
/* eslint-disable jsx-a11y/label-has-associated-control, camelcase, @typescript-eslint/camelcase */
2
import React, { useState, useRef } from "react";
3
import {
4
  injectIntl,
5
  WrappedComponentProps,
6
  FormattedMessage,
7
  MessageDescriptor,
8
  IntlShape,
9
} from "react-intl";
10
import { Formik, Form, Field } from "formik";
11
import nprogress from "nprogress";
12
import * as Yup from "yup";
13
import { connect } from "react-redux";
14
import RadioGroup from "../../Form/RadioGroup";
15
import TextInput from "../../Form/TextInput";
16
import NumberInput from "../../Form/NumberInput";
17
import SelectInput from "../../Form/SelectInput";
18
import JobPreview from "../../JobPreview";
19
import Modal from "../../Modal";
20
import { RootState } from "../../../store/store";
21
import { getJob as selectJob } from "../../../store/Job/jobSelector";
22
import { Job } from "../../../models/types";
23
import { DispatchType } from "../../../configureStore";
24
import { updateJob, createJob } from "../../../store/Job/jobActions";
25
import { validationMessages } from "../../Form/Messages";
26
import RadioInput from "../../Form/RadioInput";
27
import {
28
  LanguageRequirementId,
29
  SecurityClearanceId,
30
  ProvinceId,
31
  FrequencyId,
32
  TravelRequirementId,
33
  OvertimeRequirementId,
34
  ClassificationId,
35
} from "../../../models/lookupConstants";
36
import { emptyJob } from "../../../models/jobUtil";
37
import {
38
  securityClearance,
39
  languageRequirement,
40
  provinceName,
41
  frequencyName,
42
  travelRequirementDescription,
43
  overtimeRequirementDescription,
44
} from "../../../models/localizedConstants";
45
import ContextBlockItem from "../../ContextBlock/ContextBlockItem";
46
import CopyToClipboardButton from "../../CopyToClipboardButton";
47
import TextAreaInput from "../../Form/TextAreaInput";
48
import {
49
  formMessages,
50
  classificationOptionMessages,
51
  educationMessages,
52
} from "./JobDetailsMessages";
53
import { hasKey } from "../../../helpers/queries";
54
55
interface JobDetailsProps {
56
  // Optional Job to prepopulate form values from.
57
  job: Job | null;
58
  // Function to run after successful form validation.
59
  // It must return true if the submission was successful, false otherwise.
60
  handleSubmit: (values: Job) => Promise<boolean>;
61
  // The function to run when user clicks Prev Page
62
  handleReturn: () => void;
63
  // Function to run when modal cancel is clicked.
64
  handleModalCancel: () => void;
65
  // Function to run when modal confirm is clicked.
66
  handleModalConfirm: () => void;
67
  jobIsComplete: boolean;
68
  handleSkipToReview: () => Promise<void>;
69
}
70
71
type RemoteWorkType = "remoteWorkNone" | "remoteWorkCanada" | "remoteWorkWorld";
72
73
const remoteWorkMessages = {
74
  remoteWorkWorld: formMessages.remoteWorkWorldLabel,
75
  remoteWorkCanada: formMessages.remoteWorkCanadaLabel,
76
  remoteWorkNone: formMessages.remoteWorkNoneLabel,
77
};
78
79
type TeleworkOptionType =
80
  | "teleworkNever"
81
  | "teleworkOccasionally"
82
  | "teleworkSometimes"
83
  | "teleworkFrequently"
84
  | "teleworkAlways";
85
86
const teleworkMessages: {
87
  [key in TeleworkOptionType]: MessageDescriptor;
88
} = {
89
  teleworkNever: frequencyName(FrequencyId.never),
90
  teleworkOccasionally: frequencyName(FrequencyId.rarely),
91
  teleworkSometimes: frequencyName(FrequencyId.sometimes),
92
  teleworkFrequently: frequencyName(FrequencyId.often),
93
  teleworkAlways: frequencyName(FrequencyId.always),
94
};
95
96
const teleworkFrequencies: TeleworkOptionType[] = Object.keys(
97
  teleworkMessages,
98
) as TeleworkOptionType[];
99
100
type FlexHourOptionType =
101
  | "flexHoursNever"
102
  | "flexHoursOccasionally"
103
  | "flexHoursSometimes"
104
  | "flexHoursFrequently"
105
  | "flexHoursAlways";
106
107
const flexHourMessages: {
108
  [key in FlexHourOptionType]: MessageDescriptor;
109
} = {
110
  flexHoursNever: frequencyName(FrequencyId.never),
111
  flexHoursOccasionally: frequencyName(FrequencyId.sometimes),
112
  flexHoursFrequently: frequencyName(FrequencyId.rarely),
113
  flexHoursSometimes: frequencyName(FrequencyId.often),
114
  flexHoursAlways: frequencyName(FrequencyId.always),
115
};
116
const flexHourFrequencies: FlexHourOptionType[] = Object.keys(
117
  flexHourMessages,
118
) as FlexHourOptionType[];
119
120
type TravelOptionType =
121
  | "travelFrequently"
122
  | "travelOpportunitiesAvailable"
123
  | "travelNoneRequired";
124
125
const travelMessages: {
126
  [key in TravelOptionType]: MessageDescriptor;
127
} = {
128
  travelFrequently: travelRequirementDescription(
129
    TravelRequirementId.frequently,
130
  ),
131
  travelOpportunitiesAvailable: travelRequirementDescription(
132
    TravelRequirementId.available,
133
  ),
134
  travelNoneRequired: travelRequirementDescription(TravelRequirementId.none),
135
};
136
const travelRequirements: TravelOptionType[] = Object.keys(
137
  travelMessages,
138
) as TravelOptionType[];
139
140
type OvertimeOptionType =
141
  | "overtimeFrequently"
142
  | "overtimeOpportunitiesAvailable"
143
  | "overtimeNoneRequired";
144
145
const overtimeMessages: {
146
  [key in OvertimeOptionType]: MessageDescriptor;
147
} = {
148
  overtimeFrequently: overtimeRequirementDescription(
149
    OvertimeRequirementId.frequently,
150
  ),
151
  overtimeOpportunitiesAvailable: overtimeRequirementDescription(
152
    OvertimeRequirementId.available,
153
  ),
154
  overtimeNoneRequired: overtimeRequirementDescription(
155
    OvertimeRequirementId.none,
156
  ),
157
};
158
const overtimeRequirements: OvertimeOptionType[] = Object.keys(
159
  overtimeMessages,
160
) as OvertimeOptionType[];
161
162
interface DetailsFormValues {
163
  title: string;
164
  termLength: number | "";
165
  classification: string;
166
  level: number | "";
167
  educationRequirements: string;
168
  securityLevel: number | "";
169
  language: number | "";
170
  city: string;
171
  province: number | "";
172
  remoteWork: RemoteWorkType;
173
  telework: TeleworkOptionType;
174
  flexHours: FlexHourOptionType;
175
  travel: TravelOptionType;
176
  overtime: OvertimeOptionType;
177
}
178
179
const isClassificationSet = (values: DetailsFormValues): boolean => {
180
  return values.classification.length > 0 && values.level !== "";
181
};
182
183
const getEducationMsgForClassification = (
184
  classification: string,
185
  intl: IntlShape,
186
): string => {
187
  return hasKey(educationMessages, classification)
188
    ? intl.formatMessage(educationMessages[classification])
189
    : "";
190
};
191
192
const jobToValues = (
193
  job: Job | null,
194
  locale: string,
195
  intl: IntlShape,
196
): DetailsFormValues => {
197
  const values: DetailsFormValues = job
198
    ? {
199
        title: job[locale].title ? String(job[locale].title) : "", // TODO: use utility method
200
        termLength: job.term_qty || "",
201
        classification: job.classification_code || "",
202
        level: job.classification_level || "",
203
        educationRequirements: job[locale].education || "",
204
        securityLevel: job.security_clearance_id || "",
205
        language: job.language_requirement_id || "",
206
        city: job[locale].city || "",
207
        province: job.province_id || "",
208
        remoteWork: job.remote_work_allowed
209
          ? "remoteWorkCanada"
210
          : "remoteWorkNone",
211
        // frequency ids range from 1-5
212
        telework: job.telework_allowed_frequency_id
213
          ? teleworkFrequencies[job.telework_allowed_frequency_id - 1]
214
          : "teleworkFrequently",
215
        flexHours: job.flexible_hours_frequency_id
216
          ? flexHourFrequencies[job.flexible_hours_frequency_id - 1]
217
          : "flexHoursFrequently",
218
        travel: job.travel_requirement_id
219
          ? travelRequirements[job.travel_requirement_id - 1]
220
          : "travelFrequently",
221
        overtime: job.overtime_requirement_id
222
          ? overtimeRequirements[job.overtime_requirement_id - 1]
223
          : "overtimeFrequently",
224
      }
225
    : {
226
        title: "",
227
        termLength: "",
228
        classification: "",
229
        level: "",
230
        educationRequirements: "",
231
        securityLevel: "",
232
        language: "",
233
        city: "",
234
        province: "",
235
        remoteWork: "remoteWorkCanada",
236
        telework: "teleworkFrequently",
237
        flexHours: "flexHoursFrequently",
238
        travel: "travelFrequently",
239
        overtime: "overtimeFrequently",
240
      };
241
  // If the job has the standard education requirements saved, no need to fill the custom textbox
242
  if (
243
    values.classification &&
244
    values.educationRequirements ===
245
      getEducationMsgForClassification(values.classification, intl)
246
  ) {
247
    return {
248
      ...values,
249
      educationRequirements: "",
250
    };
251
  }
252
  return values;
253
};
254
255
const updateJobWithValues = (
256
  initialJob: Job,
257
  locale: "en" | "fr",
258
  {
259
    title,
260
    termLength,
261
    classification,
262
    level,
263
    educationRequirements,
264
    securityLevel,
265
    language,
266
    city,
267
    province,
268
    remoteWork,
269
    telework,
270
    flexHours,
271
    travel,
272
    overtime,
273
  }: DetailsFormValues,
274
): Job => ({
275
  ...initialJob,
276
  term_qty: termLength || null,
277
  classification_code: classification,
278
  classification_level: level || null,
279
  security_clearance_id: securityLevel || null,
280
  language_requirement_id: language || null,
281
  province_id: province || null,
282
  remote_work_allowed: remoteWork !== "remoteWorkNone",
283
  telework_allowed_frequency_id: teleworkFrequencies.indexOf(telework) + 1,
284
  flexible_hours_frequency_id: flexHourFrequencies.indexOf(flexHours) + 1,
285
  travel_requirement_id: travelRequirements.indexOf(travel) + 1,
286
  overtime_requirement_id: overtimeRequirements.indexOf(overtime) + 1,
287
  [locale]: {
288
    ...initialJob[locale],
289
    title,
290
    city,
291
    education: educationRequirements,
292
  },
293
});
294
295
const JobDetails: React.FunctionComponent<
296
  JobDetailsProps & WrappedComponentProps
297
> = ({
298
  job,
299
  handleSubmit,
300
  handleReturn,
301
  handleModalCancel,
302
  handleModalConfirm,
303
  jobIsComplete,
304
  handleSkipToReview,
305
  intl,
306
}: JobDetailsProps & WrappedComponentProps): React.ReactElement => {
307
  const [isModalVisible, setIsModalVisible] = useState(false);
308
309
  const modalParentRef = useRef<HTMLDivElement>(null);
310
  const { locale } = intl;
311
  if (locale !== "en" && locale !== "fr") {
312
    throw Error("Unexpected intl.locale"); // TODO: Deal with this more elegantly.
313
  }
314
  const initialValues: DetailsFormValues = jobToValues(
315
    job || null,
316
    locale,
317
    intl,
318
  );
319
320
  const remoteWorkPossibleValues: RemoteWorkType[] = [
321
    "remoteWorkNone",
322
    "remoteWorkCanada",
323
    "remoteWorkWorld",
324
  ];
325
326
  const jobSchema = Yup.object().shape({
327
    title: Yup.string()
328
      .min(2, intl.formatMessage(validationMessages.tooShort))
329
      .max(50, intl.formatMessage(validationMessages.tooLong))
330
      .required(intl.formatMessage(validationMessages.required)),
331
    termLength: Yup.number()
332
      .min(1, intl.formatMessage(validationMessages.tooShort))
333
      .max(36, intl.formatMessage(validationMessages.tooLong))
334
      .required(intl.formatMessage(validationMessages.required)),
335
    classification: Yup.mixed()
336
      .oneOf(
337
        Object.keys(ClassificationId),
338
        intl.formatMessage(validationMessages.invalidSelection),
339
      )
340
      .required(intl.formatMessage(validationMessages.required)),
341
    level: Yup.number()
342
      .min(1, intl.formatMessage(validationMessages.invalidSelection))
343
      .max(9, intl.formatMessage(validationMessages.invalidSelection))
344
      .required(intl.formatMessage(validationMessages.required)),
345
    educationRequirements: Yup.string(),
346
    securityLevel: Yup.number()
347
      .oneOf(
348
        Object.values(SecurityClearanceId),
349
        intl.formatMessage(validationMessages.invalidSelection),
350
      )
351
      .required(intl.formatMessage(validationMessages.required)),
352
    language: Yup.number()
353
      .oneOf(
354
        Object.values(LanguageRequirementId),
355
        intl.formatMessage(validationMessages.invalidSelection),
356
      )
357
      .required(intl.formatMessage(validationMessages.required)),
358
    city: Yup.string()
359
      .min(3, intl.formatMessage(validationMessages.tooShort))
360
      .max(50, intl.formatMessage(validationMessages.tooLong))
361
      .required(intl.formatMessage(validationMessages.required)),
362
    province: Yup.number()
363
      .oneOf(
364
        Object.values(ProvinceId),
365
        intl.formatMessage(validationMessages.invalidSelection),
366
      )
367
      .required(intl.formatMessage(validationMessages.required)),
368
    remoteWork: Yup.mixed()
369
      .oneOf(
370
        remoteWorkPossibleValues,
371
        intl.formatMessage(validationMessages.invalidSelection),
372
      )
373
      .required(intl.formatMessage(validationMessages.required)),
374
    telework: Yup.mixed()
375
      .oneOf(
376
        teleworkFrequencies,
377
        intl.formatMessage(validationMessages.invalidSelection),
378
      )
379
      .required(intl.formatMessage(validationMessages.required)),
380
    flexHours: Yup.mixed()
381
      .oneOf(
382
        flexHourFrequencies,
383
        intl.formatMessage(validationMessages.invalidSelection),
384
      )
385
      .required(intl.formatMessage(validationMessages.required)),
386
    travel: Yup.mixed()
387
      .oneOf(
388
        travelRequirements,
389
        intl.formatMessage(validationMessages.invalidSelection),
390
      )
391
      .required(intl.formatMessage(validationMessages.required)),
392
    overtime: Yup.mixed()
393
      .oneOf(
394
        overtimeRequirements,
395
        intl.formatMessage(validationMessages.invalidSelection),
396
      )
397
      .required(intl.formatMessage(validationMessages.required)),
398
  });
399
400
  const handleEducationRequirements = (values: DetailsFormValues): string => {
401
    return values.educationRequirements.length > 0
402
      ? values.educationRequirements
403
      : getEducationMsgForClassification(values.classification, intl);
404
  };
405
406
  const updateValuesAndReturn = (values: DetailsFormValues): void => {
407
    nprogress.start();
408
    // The following only triggers after validations pass
409
    const educationRequirements = handleEducationRequirements(values);
410
    const modifiedValues: DetailsFormValues = {
411
      ...values,
412
      educationRequirements,
413
    };
414
    handleSubmit(
415
      updateJobWithValues(job || emptyJob(), locale, modifiedValues),
416
    ).then((isSuccessful: boolean): void => {
417
      if (isSuccessful) {
418
        nprogress.done();
419
        handleReturn();
420
      }
421
    });
422
  };
423
424
  return (
425
    <section>
426
      <div
427
        data-c-container="form"
428
        data-c-padding="top(triple) bottom(triple)"
429
        ref={modalParentRef}
430
      >
431
        <h3
432
          data-c-font-size="h3"
433
          data-c-font-weight="bold"
434
          data-c-margin="bottom(double)"
435
        >
436
          <FormattedMessage
437
            id="jobBuilder.details.heading"
438
            defaultMessage="Job Details"
439
            description="Job Details page heading"
440
          />
441
        </h3>
442
        <Formik
443
          enableReinitialize
444
          initialValues={initialValues}
445
          validationSchema={jobSchema}
446
          onSubmit={(values, actions): void => {
447
            // The following only triggers after validations pass
448
            const educationRequirements: string = handleEducationRequirements(
449
              values,
450
            );
451
            const detailsFormValues: DetailsFormValues = {
452
              ...values,
453
              educationRequirements,
454
            };
455
456
            nprogress.start();
457
            handleSubmit(
458
              updateJobWithValues(job || emptyJob(), locale, detailsFormValues),
459
            )
460
              .then((isSuccessful: boolean): void => {
461
                if (isSuccessful) {
462
                  nprogress.done();
463
                  setIsModalVisible(true);
464
                }
465
              })
466
              .finally((): void => {
467
                actions.setSubmitting(false); // Required by Formik to finish the submission cycle
468
              });
469
          }}
470
          render={({
471
            errors,
472
            touched,
473
            isSubmitting,
474
            values,
475
          }): React.ReactElement => (
476
            <section>
477
              <Form
478
                id="job-information"
479
                data-c-container="form"
480
                data-c-grid="gutter"
481
              >
482
                <Field
483
                  type="text"
484
                  name="title"
485
                  component={TextInput}
486
                  required
487
                  grid="tl(1of2)"
488
                  id="builder02JobTitle"
489
                  label={intl.formatMessage(formMessages.titleLabel)}
490
                  placeholder={intl.formatMessage(
491
                    formMessages.titlePlaceholder,
492
                  )}
493
                />
494
                <Field
495
                  type="number"
496
                  name="termLength"
497
                  component={NumberInput}
498
                  placeholder={intl.formatMessage(
499
                    formMessages.termLengthPlaceholder,
500
                  )}
501
                  min={1}
502
                  max={36}
503
                  required
504
                  grid="tl(1of2)"
505
                  id="builder02TermLength"
506
                  label={intl.formatMessage(formMessages.termLengthLabel)}
507
                />
508
                <Field
509
                  name="classification"
510
                  id="builder02Classification"
511
                  label={intl.formatMessage(formMessages.classificationLabel)}
512
                  grid="tl(1of2)"
513
                  component={SelectInput}
514
                  required
515
                  nullSelection={intl.formatMessage(
516
                    formMessages.classificationNullSelection,
517
                  )}
518
                  options={[
519
                    {
520
                      value: "AS",
521
                      label: intl.formatMessage(
522
                        classificationOptionMessages.AdministrativeServices,
523
                      ),
524
                    },
525
                    {
526
                      value: "BI",
527
                      label: intl.formatMessage(
528
                        classificationOptionMessages.BiologicalSciences,
529
                      ),
530
                    },
531
                    {
532
                      value: "CO",
533
                      label: intl.formatMessage(
534
                        classificationOptionMessages.Commerce,
535
                      ),
536
                    },
537
                    {
538
                      value: "CR",
539
                      label: intl.formatMessage(
540
                        classificationOptionMessages.ClericalRegulatory,
541
                      ),
542
                    },
543
                    {
544
                      value: "CS",
545
                      label: intl.formatMessage(
546
                        classificationOptionMessages.ComputerSystems,
547
                      ),
548
                    },
549
                    {
550
                      value: "EC",
551
                      label: intl.formatMessage(
552
                        classificationOptionMessages.EconomicsSocialSciences,
553
                      ),
554
                    },
555
                    {
556
                      value: "EX",
557
                      label: intl.formatMessage(
558
                        classificationOptionMessages.Executive,
559
                      ),
560
                    },
561
                    {
562
                      value: "FO",
563
                      label: intl.formatMessage(
564
                        classificationOptionMessages.Forestry,
565
                      ),
566
                    },
567
                    {
568
                      value: "IS",
569
                      label: intl.formatMessage(
570
                        classificationOptionMessages.InformationServices,
571
                      ),
572
                    },
573
                    {
574
                      value: "PC",
575
                      label: intl.formatMessage(
576
                        classificationOptionMessages.PhysicalSciences,
577
                      ),
578
                    },
579
                    {
580
                      value: "PE",
581
                      label: intl.formatMessage(
582
                        classificationOptionMessages.PersonnelAdministration,
583
                      ),
584
                    },
585
                    {
586
                      value: "PM",
587
                      label: intl.formatMessage(
588
                        classificationOptionMessages.ProgrammeAdministration,
589
                      ),
590
                    },
591
                    {
592
                      value: "AD",
593
                      label: intl.formatMessage(
594
                        classificationOptionMessages.AdministrativeServices2,
595
                      ),
596
                    },
597
                  ]}
598
                />
599
                <Field
600
                  name="level"
601
                  id="builder02Level"
602
                  component={SelectInput}
603
                  required
604
                  label={intl.formatMessage(formMessages.levelLabel)}
605
                  grid="tl(1of2)"
606
                  nullSelection={intl.formatMessage(
607
                    formMessages.levelNullSelection,
608
                  )}
609
                  options={[
610
                    { value: 1, label: "1" },
611
                    { value: 2, label: "2" },
612
                    { value: 3, label: "3" },
613
                    { value: 4, label: "4" },
614
                    { value: 5, label: "5" },
615
                    { value: 6, label: "6" },
616
                    { value: 7, label: "7" },
617
                    { value: 8, label: "8" },
618
                    { value: 9, label: "9" },
619
                  ]}
620
                />
621
                <div data-c-grid-item="base(1of1)">
622
                  {!isClassificationSet(values) ? (
623
                    <p
624
                      data-c-font-weight="bold"
625
                      data-c-margin="bottom(normal)"
626
                      data-c-colour="grey"
627
                      data-c-border="all(thin, solid, grey)"
628
                      data-c-background="white(100)"
629
                      data-c-padding="all(normal)"
630
                      data-c-alignment="base(center)"
631
                    >
632
                      <FormattedMessage
633
                        id="jobBuilder.details.SelectClassAndLvlMessage"
634
                        defaultMessage="Please select a classification and level before preparing the education requirements."
635
                        description="Message displayed after classification and level select boxes."
636
                      />
637
                    </p>
638
                  ) : (
639
                    <>
640
                      <p
641
                        data-c-font-weight="bold"
642
                        data-c-margin="bottom(normal)"
643
                      >
644
                        <FormattedMessage
645
                          id="jobBuilder.details.educationRequirementHeader"
646
                          defaultMessage="Based on the classification level you selected, this standard paragraph will appear on the job poster."
647
                          description="Header message displayed for the Education requirement section."
648
                        />
649
                      </p>
650
                      <div>
651
                        <ContextBlockItem
652
                          wrapperMargin="bottom(normal)"
653
                          subtext={getEducationMsgForClassification(
654
                            values.classification,
655
                            intl,
656
                          )}
657
                        />
658
                      </div>
659
660
                      <div className="job-builder-education-customization active">
661
                        <p data-c-margin="bottom(normal)">
662
                          <FormattedMessage
663
                            id="jobBuilder.details.educationRequirementCopyAndPaste"
664
                            defaultMessage="If you want to customize this paragraph, copy and paste it into the textbox below."
665
                            description="Footer message displayed for the Education requirement section."
666
                          />
667
                        </p>
668
                        <p
669
                          data-c-font-weight="bold"
670
                          data-c-margin="bottom(normal)"
671
                        >
672
                          <FormattedMessage
673
                            id="jobBuilder.details.educationRequirementReviewChanges"
674
                            defaultMessage="Your HR advisor will review your changes."
675
                            description="Footer message displayed for the Education requirement section."
676
                          />
677
                        </p>
678
                        <div
679
                          data-c-alignment="base(centre)"
680
                          data-c-margin="top(normal) bottom(half)"
681
                        >
682
                          <CopyToClipboardButton
683
                            actionText={
684
                              <FormattedMessage
685
                                id="button.copyToClipboard"
686
                                defaultMessage="Copy to Clipboard"
687
                                description="Button to copy text to clipboard."
688
                              />
689
                            }
690
                            postActionText={
691
                              <FormattedMessage
692
                                id="button.copied"
693
                                defaultMessage="Copied!"
694
                                description="Confirmation for Button to copy text to clipboard."
695
                              />
696
                            }
697
                            textToCopy={getEducationMsgForClassification(
698
                              values.classification,
699
                              intl,
700
                            )}
701
                          />
702
                        </div>
703
                        <Field
704
                          type="textarea"
705
                          id="education_requirements"
706
                          name="educationRequirements"
707
                          label={intl.formatMessage(
708
                            formMessages.educationRequirementsLabel,
709
                          )}
710
                          placeholder={intl.formatMessage(
711
                            formMessages.educationRequirementPlaceholder,
712
                          )}
713
                          component={TextAreaInput}
714
                          grid="base(1of1)"
715
                        />
716
                      </div>
717
                    </>
718
                  )}
719
                </div>
720
                <Field
721
                  name="securityLevel"
722
                  id="builder02SecurityLevel"
723
                  component={SelectInput}
724
                  required
725
                  grid="tl(1of2)"
726
                  label={intl.formatMessage(formMessages.securityLevelLabel)}
727
                  nullSelection={intl.formatMessage(
728
                    formMessages.securityLevelNullSelection,
729
                  )}
730
                  options={Object.values(SecurityClearanceId).map(
731
                    (id: number): { value: number; label: string } => ({
732
                      value: id,
733
                      label: intl.formatMessage(securityClearance(id)),
734
                    }),
735
                  )}
736
                />
737
                <Field
738
                  name="language"
739
                  id="builder02Language"
740
                  component={SelectInput}
741
                  required
742
                  grid="tl(1of2)"
743
                  label={intl.formatMessage(formMessages.languageLabel)}
744
                  nullSelection={intl.formatMessage(
745
                    formMessages.languageNullSelection,
746
                  )}
747
                  options={Object.values(LanguageRequirementId).map(
748
                    (id: number): { value: number; label: string } => ({
749
                      value: id,
750
                      label: intl.formatMessage(languageRequirement(id)),
751
                    }),
752
                  )}
753
                />
754
                <Field
755
                  name="city"
756
                  type="text"
757
                  component={TextInput}
758
                  required
759
                  grid="tl(1of2)"
760
                  id="builder02City"
761
                  label={intl.formatMessage(formMessages.cityLabel)}
762
                  placeholder={intl.formatMessage(formMessages.cityPlaceholder)}
763
                />
764
                <Field
765
                  name="province"
766
                  id="builder02Province"
767
                  component={SelectInput}
768
                  required
769
                  grid="tl(1of2)"
770
                  label={intl.formatMessage(formMessages.provinceLabel)}
771
                  nullSelection={intl.formatMessage(
772
                    formMessages.provinceNullSelection,
773
                  )}
774
                  options={Object.values(ProvinceId).map((id: number): {
775
                    value: number;
776
                    label: string;
777
                  } => ({
778
                    value: id,
779
                    label: intl.formatMessage(provinceName(id)),
780
                  }))}
781
                />
782
                <p data-c-margin="bottom(normal)" data-c-font-weight="bold">
783
                  <FormattedMessage
784
                    id="jobBuilder.details.remoteWorkGroupHeader"
785
                    defaultMessage="Is remote work allowed?"
786
                    description="Header message displayed on the remote work group input."
787
                  />
788
                </p>
789
                <p data-c-margin="bottom(normal)">
790
                  <FormattedMessage
791
                    id="jobBuilder.details.remoteWorkGroupBody"
792
                    defaultMessage="Want the best talent in Canada? You increase your chances when you allow those in other parts of Canada to apply. Regional diversity also adds perspective to your team culture. Make sure to discuss this in advance with your HR Advisor."
793
                    description="Body message displayed on the remote work group input."
794
                  />
795
                </p>
796
                <RadioGroup
797
                  id="remoteWork"
798
                  label={intl.formatMessage(formMessages.remoteWorkGroupLabel)}
799
                  required
800
                  grid="base(1of1)"
801
                  error={errors.remoteWork}
802
                  touched={touched.remoteWork}
803
                  value={values.remoteWork}
804
                >
805
                  {Object.keys(remoteWorkMessages).map(
806
                    (key): React.ReactElement => {
807
                      return (
808
                        <Field
809
                          key={key}
810
                          name="remoteWork"
811
                          component={RadioInput}
812
                          id={key}
813
                          label={intl.formatMessage(remoteWorkMessages[key])}
814
                        />
815
                      );
816
                    },
817
                  )}
818
                </RadioGroup>
819
                <p data-c-margin="bottom(normal)" data-c-font-weight="bold">
820
                  <FormattedMessage
821
                    id="jobBuilder.details.teleworkGroupHeader"
822
                    defaultMessage="How often is telework allowed?"
823
                    description="Header message displayed on the telework group input."
824
                  />
825
                </p>
826
                <p data-c-margin="bottom(normal)">
827
                  <FormattedMessage
828
                    id="jobBuilder.details.teleworkGroupBody"
829
                    defaultMessage="Demonstrate that you trust your employees and you have a positive workplace culture. Allow telework as an option."
830
                    description="Body message displayed on the telework group input."
831
                  />
832
                </p>
833
                <RadioGroup
834
                  id="telework"
835
                  label={intl.formatMessage(formMessages.teleworkGroupLabel)}
836
                  required
837
                  grid="base(1of1)"
838
                  error={errors.telework}
839
                  touched={touched.telework}
840
                  value={values.telework}
841
                >
842
                  {Object.keys(teleworkMessages).map(
843
                    (key): React.ReactElement => {
844
                      return (
845
                        <Field
846
                          key={key}
847
                          name="telework"
848
                          component={RadioInput}
849
                          id={key}
850
                          label={intl.formatMessage(teleworkMessages[key])}
851
                        />
852
                      );
853
                    },
854
                  )}
855
                </RadioGroup>
856
                <p data-c-margin="bottom(normal)" data-c-font-weight="bold">
857
                  <FormattedMessage
858
                    id="jobBuilder.details.flexHoursGroupHeader"
859
                    defaultMessage="How often are flexible hours allowed?"
860
                    description="Header message displayed on the flex hours group input."
861
                  />
862
                </p>
863
                <p data-c-margin="bottom(normal)">
864
                  <FormattedMessage
865
                    id="jobBuilder.details.flexHoursGroupBody"
866
                    defaultMessage={`Want to support a more gender inclusive workplace?
867
                          Studies show allowing flex hours is a great way to improve opportunities for women and parents.`}
868
                    description="Body message displayed on the flex hours group input."
869
                  />
870
                </p>
871
                <RadioGroup
872
                  id="flexHours"
873
                  required
874
                  grid="base(1of1)"
875
                  label={intl.formatMessage(formMessages.flexHoursGroupLabel)}
876
                  error={errors.flexHours}
877
                  touched={touched.flexHours}
878
                  value={values.flexHours}
879
                >
880
                  {Object.keys(flexHourMessages).map(
881
                    (key): React.ReactElement => {
882
                      return (
883
                        <Field
884
                          key={key}
885
                          name="flexHours"
886
                          component={RadioInput}
887
                          id={key}
888
                          label={intl.formatMessage(flexHourMessages[key])}
889
                        />
890
                      );
891
                    },
892
                  )}
893
                </RadioGroup>
894
                <p data-c-margin="bottom(normal)" data-c-font-weight="bold">
895
                  <FormattedMessage
896
                    id="jobBuilder.details.travelGroupHeader"
897
                    defaultMessage="Is travel required?"
898
                    description="Header message displayed on the travel group input."
899
                  />
900
                </p>
901
                <RadioGroup
902
                  id="travel"
903
                  required
904
                  grid="base(1of1)"
905
                  label={intl.formatMessage(formMessages.travelGroupLabel)}
906
                  error={errors.travel}
907
                  touched={touched.travel}
908
                  value={values.travel}
909
                >
910
                  {Object.keys(travelMessages).map(
911
                    (key): React.ReactElement => {
912
                      return (
913
                        <Field
914
                          key={key}
915
                          name="travel"
916
                          component={RadioInput}
917
                          id={key}
918
                          label={intl.formatMessage(travelMessages[key])}
919
                        />
920
                      );
921
                    },
922
                  )}
923
                </RadioGroup>
924
                <p data-c-margin="bottom(normal)" data-c-font-weight="bold">
925
                  <FormattedMessage
926
                    id="jobBuilder.details.overtimeGroupHeader"
927
                    defaultMessage="Is overtime required?"
928
                    description="Header message displayed on the overtime group input."
929
                  />
930
                </p>
931
                <RadioGroup
932
                  id="overtime"
933
                  required
934
                  grid="base(1of1)"
935
                  label={intl.formatMessage(formMessages.overtimeGroupLabel)}
936
                  error={errors.overtime}
937
                  touched={touched.overtime}
938
                  value={values.overtime}
939
                >
940
                  {Object.keys(overtimeMessages).map(
941
                    (key): React.ReactElement => {
942
                      return (
943
                        <Field
944
                          key={key}
945
                          name="overtime"
946
                          component={RadioInput}
947
                          id={key}
948
                          label={intl.formatMessage(overtimeMessages[key])}
949
                        />
950
                      );
951
                    },
952
                  )}
953
                </RadioGroup>
954
                <div data-c-grid="gutter" data-c-grid-item="base(1of1)">
955
                  <div data-c-grid-item="base(1of1)">
956
                    <hr data-c-margin="top(normal) bottom(normal)" />
957
                  </div>
958
                  <div
959
                    data-c-alignment="base(centre) tp(left)"
960
                    data-c-grid-item="tp(1of2)"
961
                  >
962
                    <button
963
                      data-c-button="outline(c2)"
964
                      data-c-radius="rounded"
965
                      type="button"
966
                      disabled={isSubmitting}
967
                      onClick={(): void => {
968
                        updateValuesAndReturn(values);
969
                      }}
970
                    >
971
                      <FormattedMessage
972
                        id="jobBuilder.details.returnButtonLabel"
973
                        defaultMessage="Save & Return to Intro"
974
                        description="The text displayed on the Save & Return button of the Job Details form."
975
                      />
976
                    </button>
977
                  </div>
978
                  <div
979
                    data-c-alignment="base(centre) tp(right)"
980
                    data-c-grid-item="tp(1of2)"
981
                  >
982
                    <button
983
                      data-c-button="solid(c1)"
984
                      data-c-radius="rounded"
985
                      type="submit"
986
                      disabled={isSubmitting}
987
                    >
988
                      <FormattedMessage
989
                        id="jobBuilder.details.submitButtonLabel"
990
                        defaultMessage="Save & Preview"
991
                        description="The text displayed on the submit button for the Job Details form."
992
                      />
993
                    </button>
994
                  </div>
995
                </div>
996
              </Form>
997
              <Modal
998
                id="job-details-preview"
999
                parentElement={modalParentRef.current}
1000
                visible={isModalVisible}
1001
                onModalConfirm={(): void => {
1002
                  handleModalConfirm();
1003
                  setIsModalVisible(false);
1004
                }}
1005
                onModalCancel={(): void => {
1006
                  handleModalCancel();
1007
                  setIsModalVisible(false);
1008
                }}
1009
                onModalMiddle={(): void => {
1010
                  handleSkipToReview().finally((): void => {
1011
                    setIsModalVisible(false);
1012
                  });
1013
                }}
1014
              >
1015
                <Modal.Header>
1016
                  <div
1017
                    data-c-background="c1(100)"
1018
                    data-c-border="bottom(thin, solid, black)"
1019
                    data-c-padding="normal"
1020
                  >
1021
                    <h5
1022
                      data-c-colour="white"
1023
                      data-c-font-size="h4"
1024
                      id="job-details-preview-title"
1025
                    >
1026
                      <FormattedMessage
1027
                        id="jobBuilder.details.modalHeader"
1028
                        defaultMessage="You're off to a great start!"
1029
                        description="The text displayed in the header of the Job Details modal."
1030
                      />
1031
                    </h5>
1032
                  </div>
1033
                </Modal.Header>
1034
                <Modal.Body>
1035
                  <div
1036
                    data-c-border="bottom(thin, solid, black)"
1037
                    data-c-padding="normal"
1038
                    id="job-details-preview-description"
1039
                  >
1040
                    <p>
1041
                      <FormattedMessage
1042
                        id="jobBuilder.details.modalBody"
1043
                        defaultMessage="Here's a preview of the Job Information you just entered. Feel free to go back and edit things or move to the next step if you're happy with it."
1044
                        description="The text displayed in the body of the Job Details modal."
1045
                      />
1046
                    </p>
1047
                  </div>
1048
                  <div
1049
                    data-c-background="grey(20)"
1050
                    data-c-border="bottom(thin, solid, black)"
1051
                    data-c-padding="normal"
1052
                  >
1053
                    {/* TODO: Pull in the signed-in Manager's department */}
1054
                    <JobPreview
1055
                      title={values.title}
1056
                      department="Department"
1057
                      remoteWork={intl.formatMessage(
1058
                        remoteWorkMessages[values.remoteWork],
1059
                      )}
1060
                      language={
1061
                        typeof values.language === "string"
1062
                          ? ""
1063
                          : intl.formatMessage(
1064
                              languageRequirement(Number(values.language)),
1065
                            )
1066
                      }
1067
                      city={values.city}
1068
                      province={
1069
                        typeof values.province === "string"
1070
                          ? ""
1071
                          : intl.formatMessage(
1072
                              provinceName(Number(values.province)),
1073
                            )
1074
                      }
1075
                      education={
1076
                        values.educationRequirements.length > 0
1077
                          ? values.educationRequirements
1078
                          : getEducationMsgForClassification(
1079
                              values.classification,
1080
                              intl,
1081
                            )
1082
                      }
1083
                      termLength={
1084
                        typeof values.termLength === "string"
1085
                          ? null
1086
                          : Number(values.termLength)
1087
                      }
1088
                      telework={intl.formatMessage(
1089
                        teleworkMessages[values.telework],
1090
                      )}
1091
                      flexHours={intl.formatMessage(
1092
                        flexHourMessages[values.flexHours],
1093
                      )}
1094
                      securityLevel={
1095
                        typeof values.securityLevel === "string"
1096
                          ? ""
1097
                          : intl.formatMessage(
1098
                              securityClearance(Number(values.securityLevel)),
1099
                            )
1100
                      }
1101
                      classification={String(values.classification)}
1102
                      level={String(values.level)}
1103
                      travel={intl.formatMessage(travelMessages[values.travel])}
1104
                      overtime={intl.formatMessage(
1105
                        overtimeMessages[values.overtime],
1106
                      )}
1107
                    />
1108
                  </div>
1109
                </Modal.Body>
1110
                <Modal.Footer>
1111
                  <Modal.FooterCancelBtn>
1112
                    <FormattedMessage
1113
                      id="jobBuilder.details.modalCancelLabel"
1114
                      defaultMessage="Go Back"
1115
                      description="The text displayed on the cancel button of the Job Details modal."
1116
                    />
1117
                  </Modal.FooterCancelBtn>
1118
                  {jobIsComplete && (
1119
                    <Modal.FooterMiddleBtn>
1120
                      <FormattedMessage
1121
                        id="jobBuilder.details.modalMiddleLabel"
1122
                        defaultMessage="Skip to Review"
1123
                        description="The text displayed on the 'Skip to Review' button of the Job Details modal."
1124
                      />
1125
                    </Modal.FooterMiddleBtn>
1126
                  )}
1127
                  <Modal.FooterConfirmBtn>
1128
                    <FormattedMessage
1129
                      id="jobBuilder.details.modalConfirmLabel"
1130
                      defaultMessage="Next Step"
1131
                      description="The text displayed on the confirm button of the Job Details modal."
1132
                    />
1133
                  </Modal.FooterConfirmBtn>
1134
                </Modal.Footer>
1135
              </Modal>
1136
            </section>
1137
          )}
1138
        />
1139
      </div>
1140
      <div data-c-dialog-overlay={isModalVisible ? "active" : ""} />
1141
    </section>
1142
  );
1143
};
1144
1145
export const JobDetailsIntl = injectIntl(JobDetails);
1146
1147
interface JobDetailsContainerProps {
1148
  jobId: number | null;
1149
  handleModalCancel: () => void;
1150
  handleModalConfirm: () => void;
1151
}
1152
1153
const mapStateToProps = (
1154
  state: RootState,
1155
  ownProps: JobDetailsContainerProps,
1156
): {
1157
  job: Job | null;
1158
} => ({
1159
  job: ownProps.jobId ? selectJob(state, ownProps as { jobId: number }) : null,
1160
});
1161
1162
const mapDispatchToProps = (
1163
  dispatch: DispatchType,
1164
  ownProps: JobDetailsContainerProps,
1165
): {
1166
  handleSubmit: (newJob: Job) => Promise<boolean>;
1167
} => ({
1168
  handleSubmit: ownProps.jobId
1169
    ? async (newJob: Job): Promise<boolean> => {
1170
        const result = await dispatch(updateJob(newJob));
1171
        return !result.error;
1172
      }
1173
    : async (newJob: Job): Promise<boolean> => {
1174
        const result = await dispatch(createJob(newJob));
1175
        return !result.error;
1176
      },
1177
});
1178
1179
export const JobDetailsContainer = connect(
1180
  mapStateToProps,
1181
  mapDispatchToProps,
1182
)(injectIntl(JobDetails));
1183
1184
export default JobDetailsContainer;
1185