Passed
Push — feature/experience-step-functi... ( 5abe99...215b5c )
by Tristan
08:04 queued 03:37
created

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

Complexity

Total Complexity 4
Complexity/F 0

Size

Lines of Code 375
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 4
eloc 304
mnd 4
bc 4
fnc 0
dl 0
loc 375
rs 10
bpm 0
cpm 0
noi 0
c 0
b 0
f 0
1
import React from "react";
2
import { FastField, Field, Formik, Form } from "formik";
3
import { defineMessages, useIntl, IntlShape } from "react-intl";
4
import * as Yup from "yup";
5
import {
6
  EducationFormValues,
7
  EducationSubform,
8
  validationShape as educationValidationShape,
9
} from "./EducationSubform";
10
import TextInput from "../../Form/TextInput";
11
import CheckboxInput from "../../Form/CheckboxInput";
12
import { validationMessages } from "../../Form/Messages";
13
import SkillSubform, {
14
  SkillFormValues,
15
  validationShape as skillValidationShape,
16
} from "./SkillSubform";
17
import { Skill, ExperienceCommunity } from "../../../models/types";
18
import {
19
  ExperienceModalHeader,
20
  ExperienceDetailsIntro,
21
  ExperienceModalFooter,
22
} from "./ExperienceModalCommon";
23
import Modal from "../../Modal";
24
import DateInput from "../../Form/DateInput";
25
import { toInputDateString, fromInputDateString } from "../../../helpers/dates";
26
import {
27
  Locales,
28
  localizeFieldNonNull,
29
  getLocale,
30
  matchValueToModel,
31
} from "../../../helpers/localize";
32
import { notEmpty } from "../../../helpers/queries";
33
34
interface CommunityExperienceModalProps {
35
  modalId: string;
36
  experienceCommunity: ExperienceCommunity | null;
37
  jobId: number;
38
  jobClassification: string;
39
  requiredSkills: Skill[];
40
  savedRequiredSkills: Skill[];
41
  optionalSkills: Skill[];
42
  savedOptionalSkills: Skill[];
43
  experienceableId: number;
44
  experienceableType: ExperienceCommunity["experienceable_type"];
45
  parentElement: Element | null;
46
  visible: boolean;
47
  onModalCancel: () => void;
48
  onModalConfirm: (data: CommunityExperienceSubmitData) => Promise<void>;
49
}
50
51
export const messages = defineMessages({
52
  modalTitle: {
53
    id: "application.communityExperienceModal.modalTitle",
54
    defaultMessage: "Add Community Experience",
55
  },
56
  modalDescription: {
57
    id: "application.communityExperienceModal.modalDescription",
58
    defaultMessage:
59
      "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.)",
60
  },
61
  titleLabel: {
62
    id: "application.communityExperienceModal.titleLabel",
63
    defaultMessage: "My Role / Job Title",
64
  },
65
  titlePlaceholder: {
66
    id: "application.communityExperienceModal.titlePlaceholder",
67
    defaultMessage: "e.g. Front-end Development",
68
  },
69
  groupLabel: {
70
    id: "application.communityExperienceModal.groupLabel",
71
    defaultMessage: "Group / Organization / Community",
72
  },
73
  groupPlaceholder: {
74
    id: "application.communityExperienceModal.groupPlaceholder",
75
    defaultMessage: "e.g. Government of Canada",
76
  },
77
  projectLabel: {
78
    id: "application.communityExperienceModal.projectLabel",
79
    defaultMessage: "Project / Product Name",
80
  },
81
  projectPlaceholder: {
82
    id: "application.communityExperienceModal.projectPlaceholder",
83
    defaultMessage: "e.g. Talent Cloud",
84
  },
85
  startDateLabel: {
86
    id: "application.communityExperienceModal.startDateLabel",
87
    defaultMessage: "Select a Start Date",
88
  },
89
  datePlaceholder: {
90
    id: "application.communityExperienceModal.datePlaceholder",
91
    defaultMessage: "yyyy-mm-dd",
92
  },
93
  isActiveLabel: {
94
    id: "application.communityExperienceModal.isActiveLabel",
95
    defaultMessage: "This experience is still ongoing, or...",
96
    description: "Label for checkbox that indicates work is still ongoing.",
97
  },
98
  endDateLabel: {
99
    id: "application.communityExperienceModal.endDateLabel",
100
    defaultMessage: "Select an End Date",
101
  },
102
});
103
104
export interface CommunityDetailsFormValues {
105
  title: string;
106
  group: string;
107
  project: string;
108
  startDate: string;
109
  isActive: boolean;
110
  endDate: string;
111
}
112
113
type CommunityExperienceFormValues = SkillFormValues &
114
  EducationFormValues &
115
  CommunityDetailsFormValues;
116
export interface CommunityExperienceSubmitData {
117
  experienceCommunity: ExperienceCommunity;
118
  savedRequiredSkills: Skill[];
119
  savedOptionalSkills: Skill[];
120
}
121
122
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
123
export const validationShape = (intl: IntlShape) => {
124
  const requiredMsg = intl.formatMessage(validationMessages.required);
125
  const conditionalRequiredMsg = intl.formatMessage(
126
    validationMessages.endDateRequiredIfNotOngoing,
127
  );
128
  const inPastMsg = intl.formatMessage(validationMessages.dateMustBePast);
129
  const afterStartDateMsg = intl.formatMessage(
130
    validationMessages.endDateAfterStart,
131
  );
132
  return {
133
    title: Yup.string().required(requiredMsg),
134
    group: Yup.string().required(requiredMsg),
135
    project: Yup.string().required(requiredMsg),
136
    startDate: Yup.date().required(requiredMsg).max(new Date(), inPastMsg),
137
    isActive: Yup.boolean(),
138
    endDate: Yup.date().when("isActive", {
139
      is: false,
140
      then: Yup.date()
141
        .required(conditionalRequiredMsg)
142
        .min(Yup.ref("startDate"), afterStartDateMsg),
143
      otherwise: Yup.date().min(Yup.ref("startDate"), afterStartDateMsg),
144
    }),
145
  };
146
};
147
148
const dataToFormValues = (
149
  data: CommunityExperienceSubmitData,
150
  locale: Locales,
151
): CommunityExperienceFormValues => {
152
  const {
153
    experienceCommunity,
154
    savedRequiredSkills,
155
    savedOptionalSkills,
156
  } = data;
157
  const skillToName = (skill: Skill): string =>
158
    localizeFieldNonNull(locale, skill, "name");
159
  return {
160
    requiredSkills: savedRequiredSkills.map(skillToName),
161
    optionalSkills: savedOptionalSkills.map(skillToName),
162
    useAsEducationRequirement: experienceCommunity.is_education_requirement,
163
    title: experienceCommunity.title,
164
    group: experienceCommunity.group,
165
    project: experienceCommunity.project,
166
    startDate: toInputDateString(experienceCommunity.start_date),
167
    isActive: experienceCommunity.is_active,
168
    endDate: experienceCommunity.end_date
169
      ? toInputDateString(experienceCommunity.end_date)
170
      : "",
171
  };
172
};
173
174
/* eslint-disable @typescript-eslint/camelcase */
175
const formValuesToData = (
176
  formValues: CommunityExperienceFormValues,
177
  originalExperience: ExperienceCommunity,
178
  locale: Locales,
179
  skills: Skill[],
180
): CommunityExperienceSubmitData => {
181
  const nameToSkill = (name: string): Skill | null =>
182
    matchValueToModel(locale, "name", name, skills);
183
  return {
184
    experienceCommunity: {
185
      ...originalExperience,
186
      title: formValues.title,
187
      group: formValues.group,
188
      project: formValues.project,
189
      start_date: fromInputDateString(formValues.startDate),
190
      is_active: formValues.isActive,
191
      end_date: formValues.endDate
192
        ? fromInputDateString(formValues.endDate)
193
        : null,
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
/* eslint-enable @typescript-eslint/camelcase */
222
223
export const CommunityExperienceModal: React.FC<CommunityExperienceModalProps> = ({
224
  modalId,
225
  experienceCommunity,
226
  jobId,
227
  jobClassification,
228
  requiredSkills,
229
  savedRequiredSkills,
230
  optionalSkills,
231
  savedOptionalSkills,
232
  experienceableId,
233
  experienceableType,
234
  parentElement,
235
  visible,
236
  onModalCancel,
237
  onModalConfirm,
238
}) => {
239
  const intl = useIntl();
240
  const locale = getLocale(intl.locale);
241
242
  const originalExperience =
243
    experienceCommunity ??
244
    newCommunityExperience(experienceableId, experienceableType);
245
246
  const skillToName = (skill: Skill): string =>
247
    localizeFieldNonNull(locale, skill, "name");
248
249
  const initialFormValues = dataToFormValues(
250
    {
251
      experienceCommunity: originalExperience,
252
      savedRequiredSkills,
253
      savedOptionalSkills,
254
    },
255
    locale,
256
  );
257
258
  const validationSchema = Yup.object().shape({
259
    ...skillValidationShape,
260
    ...educationValidationShape,
261
    ...validationShape(intl),
262
  });
263
264
  const detailsSubform = (
265
    <div data-c-container="medium">
266
      <div data-c-grid="gutter(all, 1) middle">
267
        <FastField
268
          id="title"
269
          name="title"
270
          type="text"
271
          grid="base(1of1)"
272
          component={TextInput}
273
          required
274
          label={intl.formatMessage(messages.titleLabel)}
275
          placeholder={intl.formatMessage(messages.titlePlaceholder)}
276
        />
277
        <FastField
278
          id="group"
279
          type="text"
280
          name="group"
281
          component={TextInput}
282
          required
283
          grid="tl(1of2)"
284
          label={intl.formatMessage(messages.groupLabel)}
285
          placeholder={intl.formatMessage(messages.groupPlaceholder)}
286
        />
287
        <FastField
288
          id="project"
289
          type="text"
290
          name="project"
291
          component={TextInput}
292
          required
293
          grid="tl(1of2)"
294
          label={intl.formatMessage(messages.projectLabel)}
295
          placeholder={intl.formatMessage(messages.projectPlaceholder)}
296
        />
297
        <FastField
298
          id="startDate"
299
          name="startDate"
300
          component={DateInput}
301
          required
302
          grid="base(1of1)"
303
          label={intl.formatMessage(messages.startDateLabel)}
304
          placeholder={intl.formatMessage(messages.datePlaceholder)}
305
        />
306
        <Field
307
          id="isActive"
308
          name="isActive"
309
          component={CheckboxInput}
310
          grid="tl(1of2)"
311
          label={intl.formatMessage(messages.isActiveLabel)}
312
        />
313
        <Field
314
          id="endDate"
315
          name="endDate"
316
          component={DateInput}
317
          grid="base(1of2)"
318
          label={intl.formatMessage(messages.endDateLabel)}
319
          placeholder={intl.formatMessage(messages.datePlaceholder)}
320
        />
321
      </div>
322
    </div>
323
  );
324
325
  return (
326
    <Modal
327
      id={modalId}
328
      parentElement={parentElement}
329
      visible={visible}
330
      onModalCancel={onModalCancel}
331
      onModalConfirm={onModalCancel}
332
      className="application-experience-dialog"
333
    >
334
      <ExperienceModalHeader
335
        title={intl.formatMessage(messages.modalTitle)}
336
        iconClass="fa-people-carry"
337
      />
338
      <Formik
339
        enableReinitialize
340
        initialValues={initialFormValues}
341
        onSubmit={async (values, actions): Promise<void> => {
342
          await onModalConfirm(
343
            formValuesToData(values, originalExperience, locale, [
344
              ...requiredSkills,
345
              ...optionalSkills,
346
            ]),
347
          );
348
          actions.setSubmitting(false);
349
        }}
350
        validationSchema={validationSchema}
351
      >
352
        {(formikProps): React.ReactElement => (
353
          <Form>
354
            <Modal.Body>
355
              <ExperienceDetailsIntro
356
                description={intl.formatMessage(messages.modalDescription)}
357
              />
358
              {detailsSubform}
359
              <SkillSubform
360
                jobId={jobId}
361
                jobRequiredSkills={requiredSkills.map(skillToName)}
362
                jobOptionalSkills={optionalSkills.map(skillToName)}
363
              />
364
              <EducationSubform jobClassification={jobClassification} />
365
            </Modal.Body>
366
            <ExperienceModalFooter buttonsDisabled={formikProps.isSubmitting} />
367
          </Form>
368
        )}
369
      </Formik>
370
    </Modal>
371
  );
372
};
373
374
export default CommunityExperienceModal;
375