Passed
Push — task/skill-skillcategory-relat... ( 8f6df1...d50b49 )
by Chris
04:37 queued 10s
created

resources/assets/js/components/Application/ExperienceModals/CommunityExperienceModal.tsx   A

Complexity

Total Complexity 6
Complexity/F 0

Size

Lines of Code 473
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 6
eloc 379
mnd 6
bc 6
fnc 0
dl 0
loc 473
rs 10
bpm 0
cpm 0
noi 0
c 0
b 0
f 0
1
/* eslint-disable @typescript-eslint/camelcase */
2
import React, { FunctionComponent } from "react";
3
import { FastField, Field, Formik, Form } from "formik";
4
import { defineMessages, useIntl, IntlShape } from "react-intl";
5
import * as Yup from "yup";
6
import {
7
  EducationFormValues,
8
  EducationSubform,
9
  validationShape as educationValidationShape,
10
} from "./EducationSubform";
11
import TextInput from "../../Form/TextInput";
12
import CheckboxInput from "../../Form/CheckboxInput";
13
import { validationMessages } from "../../Form/Messages";
14
import SkillSubform, {
15
  SkillFormValues,
16
  validationShape as skillValidationShape,
17
} from "./SkillSubform";
18
import { Skill, ExperienceCommunity } from "../../../models/types";
19
import {
20
  ExperienceModalHeader,
21
  ExperienceDetailsIntro,
22
  ExperienceModalFooter,
23
} from "./ExperienceModalCommon";
24
import Modal from "../../Modal";
25
import DateInput from "../../Form/DateInput";
26
import { toInputDateString, fromInputDateString } from "../../../helpers/dates";
27
import {
28
  Locales,
29
  localizeFieldNonNull,
30
  getLocale,
31
  matchValueToModel,
32
} from "../../../helpers/localize";
33
import { notEmpty } from "../../../helpers/queries";
34
35
export const messages = defineMessages({
36
  modalTitle: {
37
    id: "application.communityExperienceModal.modalTitle",
38
    defaultMessage: "Add Community Experience",
39
  },
40
  modalDescription: {
41
    id: "application.communityExperienceModal.modalDescription",
42
    defaultMessage:
43
      "Gained experience by being part of or giving back to a community? People learn skills from a wide range of experiences like volunteering or being part of non-profit organizations, indigenous communities, or virtual collaborations. (Here’s an opportunity to share the skills that your community has helped you develop.)",
44
  },
45
  titleLabel: {
46
    id: "application.communityExperienceModal.titleLabel",
47
    defaultMessage: "My Role / Job Title",
48
  },
49
  titlePlaceholder: {
50
    id: "application.communityExperienceModal.titlePlaceholder",
51
    defaultMessage: "e.g. Front-end Development",
52
  },
53
  groupLabel: {
54
    id: "application.communityExperienceModal.groupLabel",
55
    defaultMessage: "Group / Organization / Community",
56
  },
57
  groupPlaceholder: {
58
    id: "application.communityExperienceModal.groupPlaceholder",
59
    defaultMessage: "e.g. Government of Canada",
60
  },
61
  projectLabel: {
62
    id: "application.communityExperienceModal.projectLabel",
63
    defaultMessage: "Project / Product Name",
64
  },
65
  projectPlaceholder: {
66
    id: "application.communityExperienceModal.projectPlaceholder",
67
    defaultMessage: "e.g. Talent Cloud",
68
  },
69
  startDateLabel: {
70
    id: "application.communityExperienceModal.startDateLabel",
71
    defaultMessage: "Select a Start Date",
72
  },
73
  datePlaceholder: {
74
    id: "application.communityExperienceModal.datePlaceholder",
75
    defaultMessage: "yyyy-mm-dd",
76
  },
77
  isActiveLabel: {
78
    id: "application.communityExperienceModal.isActiveLabel",
79
    defaultMessage: "This experience is still ongoing, or...",
80
    description: "Label for checkbox that indicates work is still ongoing.",
81
  },
82
  endDateLabel: {
83
    id: "application.communityExperienceModal.endDateLabel",
84
    defaultMessage: "Select an End Date",
85
  },
86
});
87
88
export interface CommunityDetailsFormValues {
89
  title: string;
90
  group: string;
91
  project: string;
92
  startDate: string;
93
  isActive: boolean;
94
  endDate: string;
95
}
96
97
type CommunityExperienceFormValues = SkillFormValues &
98
  EducationFormValues &
99
  CommunityDetailsFormValues;
100
export interface CommunityExperienceSubmitData {
101
  experienceCommunity: ExperienceCommunity;
102
  savedRequiredSkills: Skill[];
103
  savedOptionalSkills: Skill[];
104
}
105
106
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
107
export const validationShape = (intl: IntlShape) => {
108
  const requiredMsg = intl.formatMessage(validationMessages.required);
109
  const conditionalRequiredMsg = intl.formatMessage(
110
    validationMessages.endDateRequiredIfNotOngoing,
111
  );
112
  const inPastMsg = intl.formatMessage(validationMessages.dateMustBePast);
113
  const afterStartDateMsg = intl.formatMessage(
114
    validationMessages.endDateAfterStart,
115
  );
116
  return {
117
    title: Yup.string().required(requiredMsg),
118
    group: Yup.string().required(requiredMsg),
119
    project: Yup.string().required(requiredMsg),
120
    startDate: Yup.date().required(requiredMsg).max(new Date(), inPastMsg),
121
    isActive: Yup.boolean(),
122
    endDate: Yup.date().when("isActive", {
123
      is: false,
124
      then: Yup.date()
125
        .required(conditionalRequiredMsg)
126
        .min(Yup.ref("startDate"), afterStartDateMsg),
127
      otherwise: Yup.date().min(Yup.ref("startDate"), afterStartDateMsg),
128
    }),
129
  };
130
};
131
132
const experienceToDetails = (
133
  experienceCommunity: ExperienceCommunity,
134
): CommunityDetailsFormValues => {
135
  return {
136
    title: experienceCommunity.title,
137
    group: experienceCommunity.group,
138
    project: experienceCommunity.project,
139
    startDate: toInputDateString(experienceCommunity.start_date),
140
    isActive: experienceCommunity.is_active,
141
    endDate: experienceCommunity.end_date
142
      ? toInputDateString(experienceCommunity.end_date)
143
      : "",
144
  };
145
};
146
147
const dataToFormValues = (
148
  data: CommunityExperienceSubmitData,
149
  locale: Locales,
150
): CommunityExperienceFormValues => {
151
  const {
152
    experienceCommunity,
153
    savedRequiredSkills,
154
    savedOptionalSkills,
155
  } = data;
156
  const skillToName = (skill: Skill): string =>
157
    localizeFieldNonNull(locale, skill, "name");
158
  return {
159
    ...experienceToDetails(data.experienceCommunity),
160
    requiredSkills: savedRequiredSkills.map(skillToName),
161
    optionalSkills: savedOptionalSkills.map(skillToName),
162
    useAsEducationRequirement: experienceCommunity.is_education_requirement,
163
  };
164
};
165
166
const detailsToExperience = (
167
  formValues: CommunityDetailsFormValues,
168
  originalExperience: ExperienceCommunity,
169
): ExperienceCommunity => {
170
  return {
171
    ...originalExperience,
172
    title: formValues.title,
173
    group: formValues.group,
174
    project: formValues.project,
175
    start_date: fromInputDateString(formValues.startDate),
176
    is_active: formValues.isActive,
177
    end_date: formValues.endDate
178
      ? fromInputDateString(formValues.endDate)
179
      : null,
180
  };
181
};
182
183
const formValuesToData = (
184
  formValues: CommunityExperienceFormValues,
185
  originalExperience: ExperienceCommunity,
186
  locale: Locales,
187
  skills: Skill[],
188
): CommunityExperienceSubmitData => {
189
  const nameToSkill = (name: string): Skill | null =>
190
    matchValueToModel(locale, "name", name, skills);
191
  return {
192
    experienceCommunity: {
193
      ...detailsToExperience(formValues, originalExperience),
194
      is_education_requirement: formValues.useAsEducationRequirement,
195
    },
196
    savedRequiredSkills: formValues.requiredSkills
197
      .map(nameToSkill)
198
      .filter(notEmpty),
199
    savedOptionalSkills: formValues.optionalSkills
200
      .map(nameToSkill)
201
      .filter(notEmpty),
202
  };
203
};
204
205
const newCommunityExperience = (
206
  experienceableId: number,
207
  experienceableType: ExperienceCommunity["experienceable_type"],
208
): ExperienceCommunity => ({
209
  id: 0,
210
  title: "",
211
  group: "",
212
  project: "",
213
  is_active: false,
214
  start_date: new Date(),
215
  end_date: null,
216
  is_education_requirement: false,
217
  experienceable_id: experienceableId,
218
  experienceable_type: experienceableType,
219
  type: "experience_community",
220
});
221
222
const DetailsSubform: FunctionComponent = () => {
223
  const intl = useIntl();
224
  return (
225
    <div data-c-container="medium">
226
      <div data-c-grid="gutter(all, 1) middle">
227
        <FastField
228
          id="community-title"
229
          name="title"
230
          type="text"
231
          grid="base(1of1)"
232
          component={TextInput}
233
          required
234
          label={intl.formatMessage(messages.titleLabel)}
235
          placeholder={intl.formatMessage(messages.titlePlaceholder)}
236
        />
237
        <FastField
238
          id="community-group"
239
          type="text"
240
          name="group"
241
          component={TextInput}
242
          required
243
          grid="tl(1of2)"
244
          label={intl.formatMessage(messages.groupLabel)}
245
          placeholder={intl.formatMessage(messages.groupPlaceholder)}
246
        />
247
        <FastField
248
          id="community-project"
249
          type="text"
250
          name="project"
251
          component={TextInput}
252
          required
253
          grid="tl(1of2)"
254
          label={intl.formatMessage(messages.projectLabel)}
255
          placeholder={intl.formatMessage(messages.projectPlaceholder)}
256
        />
257
        <FastField
258
          id="community-startDate"
259
          name="startDate"
260
          component={DateInput}
261
          required
262
          grid="base(1of1)"
263
          label={intl.formatMessage(messages.startDateLabel)}
264
          placeholder={intl.formatMessage(messages.datePlaceholder)}
265
        />
266
        <Field
267
          id="community-isActive"
268
          name="isActive"
269
          component={CheckboxInput}
270
          grid="tl(1of2)"
271
          label={intl.formatMessage(messages.isActiveLabel)}
272
        />
273
        <Field
274
          id="community-endDate"
275
          name="endDate"
276
          component={DateInput}
277
          grid="base(1of2)"
278
          label={intl.formatMessage(messages.endDateLabel)}
279
          placeholder={intl.formatMessage(messages.datePlaceholder)}
280
        />
281
      </div>
282
    </div>
283
  );
284
};
285
286
interface ProfileCommunityModalProps {
287
  modalId: string;
288
  experienceCommunity: ExperienceCommunity | null;
289
  experienceableId: number;
290
  experienceableType: ExperienceCommunity["experienceable_type"];
291
  parentElement: Element | null;
292
  visible: boolean;
293
  onModalCancel: () => void;
294
  onModalConfirm: (data: ExperienceCommunity) => Promise<void>;
295
}
296
297
export const ProfileCommunityModal: FunctionComponent<ProfileCommunityModalProps> = ({
298
  modalId,
299
  experienceCommunity,
300
  experienceableId,
301
  experienceableType,
302
  parentElement,
303
  visible,
304
  onModalCancel,
305
  onModalConfirm,
306
}) => {
307
  const intl = useIntl();
308
309
  const originalExperience =
310
    experienceCommunity ??
311
    newCommunityExperience(experienceableId, experienceableType);
312
313
  const initialFormValues = experienceToDetails(originalExperience);
314
315
  const validationSchema = Yup.object().shape({
316
    ...validationShape(intl),
317
  });
318
319
  return (
320
    <Modal
321
      id={modalId}
322
      parentElement={parentElement}
323
      visible={visible}
324
      onModalCancel={onModalCancel}
325
      onModalConfirm={onModalCancel}
326
      className="application-experience-dialog"
327
    >
328
      <ExperienceModalHeader
329
        title={intl.formatMessage(messages.modalTitle)}
330
        iconClass="fa-people-carry"
331
      />
332
      <Formik
333
        enableReinitialize
334
        initialValues={initialFormValues}
335
        onSubmit={async (values, actions): Promise<void> => {
336
          await onModalConfirm(detailsToExperience(values, originalExperience));
337
          actions.setSubmitting(false);
338
          actions.resetForm();
339
        }}
340
        validationSchema={validationSchema}
341
      >
342
        {(formikProps): React.ReactElement => (
343
          <Form>
344
            <Modal.Body>
345
              <ExperienceDetailsIntro
346
                description={intl.formatMessage(messages.modalDescription)}
347
              />
348
              <DetailsSubform />
349
            </Modal.Body>
350
            <ExperienceModalFooter buttonsDisabled={formikProps.isSubmitting} />
351
          </Form>
352
        )}
353
      </Formik>
354
    </Modal>
355
  );
356
};
357
interface CommunityExperienceModalProps {
358
  modalId: string;
359
  experienceCommunity: ExperienceCommunity | null;
360
  jobId: number;
361
  jobClassification: string;
362
  jobEducationRequirements: string | null;
363
  requiredSkills: Skill[];
364
  savedRequiredSkills: Skill[];
365
  optionalSkills: Skill[];
366
  savedOptionalSkills: Skill[];
367
  experienceableId: number;
368
  experienceableType: ExperienceCommunity["experienceable_type"];
369
  parentElement: Element | null;
370
  visible: boolean;
371
  onModalCancel: () => void;
372
  onModalConfirm: (data: CommunityExperienceSubmitData) => Promise<void>;
373
}
374
375
export const CommunityExperienceModal: React.FC<CommunityExperienceModalProps> = ({
376
  modalId,
377
  experienceCommunity,
378
  jobId,
379
  jobClassification,
380
  jobEducationRequirements,
381
  requiredSkills,
382
  savedRequiredSkills,
383
  optionalSkills,
384
  savedOptionalSkills,
385
  experienceableId,
386
  experienceableType,
387
  parentElement,
388
  visible,
389
  onModalCancel,
390
  onModalConfirm,
391
}) => {
392
  const intl = useIntl();
393
  const locale = getLocale(intl.locale);
394
395
  const originalExperience =
396
    experienceCommunity ??
397
    newCommunityExperience(experienceableId, experienceableType);
398
399
  const skillToName = (skill: Skill): string =>
400
    localizeFieldNonNull(locale, skill, "name");
401
402
  const initialFormValues = dataToFormValues(
403
    {
404
      experienceCommunity: originalExperience,
405
      savedRequiredSkills,
406
      savedOptionalSkills,
407
    },
408
    locale,
409
  );
410
411
  const validationSchema = Yup.object().shape({
412
    ...skillValidationShape,
413
    ...educationValidationShape,
414
    ...validationShape(intl),
415
  });
416
417
  return (
418
    <Modal
419
      id={modalId}
420
      parentElement={parentElement}
421
      visible={visible}
422
      onModalCancel={onModalCancel}
423
      onModalConfirm={onModalCancel}
424
      className="application-experience-dialog"
425
    >
426
      <ExperienceModalHeader
427
        title={intl.formatMessage(messages.modalTitle)}
428
        iconClass="fa-people-carry"
429
      />
430
      <Formik
431
        enableReinitialize
432
        initialValues={initialFormValues}
433
        onSubmit={async (values, actions): Promise<void> => {
434
          await onModalConfirm(
435
            formValuesToData(values, originalExperience, locale, [
436
              ...requiredSkills,
437
              ...optionalSkills,
438
            ]),
439
          );
440
          actions.setSubmitting(false);
441
          actions.resetForm();
442
        }}
443
        validationSchema={validationSchema}
444
      >
445
        {(formikProps): React.ReactElement => (
446
          <Form>
447
            <Modal.Body>
448
              <ExperienceDetailsIntro
449
                description={intl.formatMessage(messages.modalDescription)}
450
              />
451
              <DetailsSubform />
452
              <SkillSubform
453
                keyPrefix="community"
454
                jobId={jobId}
455
                jobRequiredSkills={requiredSkills.map(skillToName)}
456
                jobOptionalSkills={optionalSkills.map(skillToName)}
457
              />
458
              <EducationSubform
459
                keyPrefix="community"
460
                jobClassification={jobClassification}
461
                jobEducationRequirements={jobEducationRequirements}
462
              />
463
            </Modal.Body>
464
            <ExperienceModalFooter buttonsDisabled={formikProps.isSubmitting} />
465
          </Form>
466
        )}
467
      </Formik>
468
    </Modal>
469
  );
470
};
471
472
export default CommunityExperienceModal;
473