Passed
Push — dev ( 5fa166...b05899 )
by Yonathan
05:27 queued 11s
created

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

Complexity

Total Complexity 4
Complexity/F 0

Size

Lines of Code 397
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 4
eloc 324
c 0
b 0
f 0
dl 0
loc 397
rs 10
mnd 4
bc 4
fnc 0
bpm 0
cpm 0
noi 0
1
import React, { FunctionComponent } 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
  ExperienceSubmitData,
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
101
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
102
export const communityValidationShape = (intl: IntlShape) => {
103
  const requiredMsg = intl.formatMessage(validationMessages.required);
104
  const conditionalRequiredMsg = intl.formatMessage(
105
    validationMessages.endDateRequiredIfNotOngoing,
106
  );
107
  const inPastMsg = intl.formatMessage(validationMessages.dateMustBePast);
108
  const afterStartDateMsg = intl.formatMessage(
109
    validationMessages.endDateAfterStart,
110
  );
111
  return {
112
    title: Yup.string().required(requiredMsg),
113
    group: Yup.string().required(requiredMsg),
114
    project: Yup.string().required(requiredMsg),
115
    startDate: Yup.date().required(requiredMsg).max(new Date(), inPastMsg),
116
    isActive: Yup.boolean(),
117
    endDate: Yup.date().when("isActive", {
118
      is: false,
119
      then: Yup.date()
120
        .required(conditionalRequiredMsg)
121
        .min(Yup.ref("startDate"), afterStartDateMsg),
122
      otherwise: Yup.date().min(Yup.ref("startDate"), afterStartDateMsg),
123
    }),
124
  };
125
};
126
127
export const experienceToDetails = (
128
  experienceCommunity: ExperienceCommunity,
129
): CommunityDetailsFormValues => {
130
  return {
131
    title: experienceCommunity.title,
132
    group: experienceCommunity.group,
133
    project: experienceCommunity.project,
134
    startDate: toInputDateString(experienceCommunity.start_date),
135
    isActive: experienceCommunity.is_active,
136
    endDate: experienceCommunity.end_date
137
      ? toInputDateString(experienceCommunity.end_date)
138
      : "",
139
  };
140
};
141
142
const dataToFormValues = (
143
  data: ExperienceSubmitData<ExperienceCommunity>,
144
  locale: Locales,
145
): CommunityExperienceFormValues => {
146
  const { experience, savedRequiredSkills, savedOptionalSkills } = data;
147
  const skillToName = (skill: Skill): string =>
148
    localizeFieldNonNull(locale, skill, "name");
149
  return {
150
    ...experienceToDetails(data.experience),
151
    requiredSkills: savedRequiredSkills.map(skillToName),
152
    optionalSkills: savedOptionalSkills.map(skillToName),
153
    useAsEducationRequirement: experience.is_education_requirement,
154
  };
155
};
156
157
export const detailsToExperience = (
158
  formValues: CommunityDetailsFormValues,
159
  originalExperience: ExperienceCommunity,
160
): ExperienceCommunity => {
161
  return {
162
    ...originalExperience,
163
    title: formValues.title,
164
    group: formValues.group,
165
    project: formValues.project,
166
    start_date: fromInputDateString(formValues.startDate),
167
    is_active: formValues.isActive,
168
    end_date: formValues.endDate
169
      ? fromInputDateString(formValues.endDate)
170
      : null,
171
  };
172
};
173
174
const formValuesToData = (
175
  formValues: CommunityExperienceFormValues,
176
  originalExperience: ExperienceCommunity,
177
  locale: Locales,
178
  skills: Skill[],
179
): ExperienceSubmitData<ExperienceCommunity> => {
180
  const nameToSkill = (name: string): Skill | null =>
181
    matchValueToModel(locale, "name", name, skills);
182
  return {
183
    experience: {
184
      ...detailsToExperience(formValues, originalExperience),
185
      is_education_requirement: formValues.useAsEducationRequirement,
186
    },
187
    savedRequiredSkills: formValues.requiredSkills
188
      .map(nameToSkill)
189
      .filter(notEmpty),
190
    savedOptionalSkills: formValues.optionalSkills
191
      .map(nameToSkill)
192
      .filter(notEmpty),
193
  };
194
};
195
196
export const newCommunityExperience = (
197
  experienceableId: number,
198
  experienceableType: ExperienceCommunity["experienceable_type"],
199
): ExperienceCommunity => ({
200
  id: 0,
201
  title: "",
202
  group: "",
203
  project: "",
204
  is_active: false,
205
  start_date: new Date(),
206
  end_date: null,
207
  is_education_requirement: false,
208
  experienceable_id: experienceableId,
209
  experienceable_type: experienceableType,
210
  type: "experience_community",
211
});
212
213
export const CommunityDetailsSubform: FunctionComponent = () => {
214
  const intl = useIntl();
215
  return (
216
    <div data-c-container="medium">
217
      <div data-c-grid="gutter(all, 1) middle">
218
        <FastField
219
          id="community-title"
220
          name="title"
221
          type="text"
222
          grid="base(1of1)"
223
          component={TextInput}
224
          required
225
          label={intl.formatMessage(messages.titleLabel)}
226
          placeholder={intl.formatMessage(messages.titlePlaceholder)}
227
        />
228
        <FastField
229
          id="community-group"
230
          type="text"
231
          name="group"
232
          component={TextInput}
233
          required
234
          grid="tl(1of2)"
235
          label={intl.formatMessage(messages.groupLabel)}
236
          placeholder={intl.formatMessage(messages.groupPlaceholder)}
237
        />
238
        <FastField
239
          id="community-project"
240
          type="text"
241
          name="project"
242
          component={TextInput}
243
          required
244
          grid="tl(1of2)"
245
          label={intl.formatMessage(messages.projectLabel)}
246
          placeholder={intl.formatMessage(messages.projectPlaceholder)}
247
        />
248
        <FastField
249
          id="community-startDate"
250
          name="startDate"
251
          component={DateInput}
252
          required
253
          grid="base(1of1)"
254
          label={intl.formatMessage(messages.startDateLabel)}
255
          placeholder={intl.formatMessage(messages.datePlaceholder)}
256
        />
257
        <Field
258
          id="community-isActive"
259
          name="isActive"
260
          component={CheckboxInput}
261
          grid="tl(1of2)"
262
          label={intl.formatMessage(messages.isActiveLabel)}
263
        />
264
        <Field
265
          id="community-endDate"
266
          name="endDate"
267
          component={DateInput}
268
          grid="base(1of2)"
269
          label={intl.formatMessage(messages.endDateLabel)}
270
          placeholder={intl.formatMessage(messages.datePlaceholder)}
271
        />
272
      </div>
273
    </div>
274
  );
275
};
276
277
interface CommunityExperienceModalProps {
278
  modalId: string;
279
  experienceCommunity: ExperienceCommunity | null;
280
  jobId: number;
281
  classificationEducationRequirements: string | null;
282
  jobEducationRequirements: string | null;
283
  requiredSkills: Skill[];
284
  savedRequiredSkills: Skill[];
285
  optionalSkills: Skill[];
286
  savedOptionalSkills: Skill[];
287
  experienceableId: number;
288
  experienceableType: ExperienceCommunity["experienceable_type"];
289
  parentElement: Element | null;
290
  visible: boolean;
291
  onModalCancel: () => void;
292
  onModalConfirm: (
293
    data: ExperienceSubmitData<ExperienceCommunity>,
294
  ) => Promise<void>;
295
}
296
297
export const CommunityExperienceModal: React.FC<CommunityExperienceModalProps> = ({
298
  modalId,
299
  experienceCommunity,
300
  jobId,
301
  classificationEducationRequirements,
302
  jobEducationRequirements,
303
  requiredSkills,
304
  savedRequiredSkills,
305
  optionalSkills,
306
  savedOptionalSkills,
307
  experienceableId,
308
  experienceableType,
309
  parentElement,
310
  visible,
311
  onModalCancel,
312
  onModalConfirm,
313
}) => {
314
  const intl = useIntl();
315
  const locale = getLocale(intl.locale);
316
317
  const originalExperience =
318
    experienceCommunity ??
319
    newCommunityExperience(experienceableId, experienceableType);
320
321
  const skillToName = (skill: Skill): string =>
322
    localizeFieldNonNull(locale, skill, "name");
323
324
  const initialFormValues = dataToFormValues(
325
    {
326
      experience: originalExperience,
327
      savedRequiredSkills,
328
      savedOptionalSkills,
329
    },
330
    locale,
331
  );
332
333
  const validationSchema = Yup.object().shape({
334
    ...skillValidationShape,
335
    ...educationValidationShape,
336
    ...communityValidationShape(intl),
337
  });
338
339
  return (
340
    <Modal
341
      id={modalId}
342
      parentElement={parentElement}
343
      visible={visible}
344
      onModalCancel={onModalCancel}
345
      onModalConfirm={onModalCancel}
346
      className="application-experience-dialog"
347
    >
348
      <ExperienceModalHeader
349
        title={intl.formatMessage(messages.modalTitle)}
350
        iconClass="fa-people-carry"
351
      />
352
      <Formik
353
        enableReinitialize
354
        initialValues={initialFormValues}
355
        onSubmit={async (values, actions): Promise<void> => {
356
          await onModalConfirm(
357
            formValuesToData(values, originalExperience, locale, [
358
              ...requiredSkills,
359
              ...optionalSkills,
360
            ]),
361
          );
362
          actions.setSubmitting(false);
363
          actions.resetForm();
364
        }}
365
        validationSchema={validationSchema}
366
      >
367
        {(formikProps): React.ReactElement => (
368
          <Form>
369
            <Modal.Body>
370
              <ExperienceDetailsIntro
371
                description={intl.formatMessage(messages.modalDescription)}
372
              />
373
              <CommunityDetailsSubform />
374
              <SkillSubform
375
                keyPrefix="community"
376
                jobId={jobId}
377
                jobRequiredSkills={requiredSkills.map(skillToName)}
378
                jobOptionalSkills={optionalSkills.map(skillToName)}
379
              />
380
              <EducationSubform
381
                keyPrefix="community"
382
                classificationEducationRequirements={
383
                  classificationEducationRequirements
384
                }
385
                jobEducationRequirements={jobEducationRequirements}
386
              />
387
            </Modal.Body>
388
            <ExperienceModalFooter buttonsDisabled={formikProps.isSubmitting} />
389
          </Form>
390
        )}
391
      </Formik>
392
    </Modal>
393
  );
394
};
395
396
export default CommunityExperienceModal;
397