Passed
Push — task/improve-storybook-build-p... ( ef2892 )
by Yonathan
06:06
created

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

Complexity

Total Complexity 21
Complexity/F 0

Size

Lines of Code 1133
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

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