Passed
Push — dev ( 889e9a...e64021 )
by
unknown
04:18
created

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

Complexity

Total Complexity 4
Complexity/F 0

Size

Lines of Code 376
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

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