Passed
Push — feature/experience-form-modals ( ac040e...10a0f1 )
by Tristan
03:59
created

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

Complexity

Total Complexity 4
Complexity/F 0

Size

Lines of Code 368
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 4
eloc 297
mnd 4
bc 4
fnc 0
dl 0
loc 368
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
  useAsEducationRequirement: boolean;
45
  parentElement: Element | null;
46
  visible: boolean;
47
  onModalCancel: () => void;
48
  onModalConfirm: (data: CommunityExperienceSubmitData) => Promise<void>;
49
}
50
51
const messages = defineMessages({
52
  modalTitle: {
53
    id: "communityExperienceModal.modalTitle",
54
    defaultMessage: "Add Community Experience",
55
  },
56
  modalDescription: {
57
    id: "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: "communityExperienceModal.titleLabel",
63
    defaultMessage: "My Role / Job Title",
64
  },
65
  titlePlaceholder: {
66
    id: "communityExperienceModal.titlePlaceholder",
67
    defaultMessage: "e.g. Front-end Development",
68
  },
69
  groupLabel: {
70
    id: "communityExperienceModal.groupLabel",
71
    defaultMessage: "Group / Organization / Community",
72
  },
73
  groupPlaceholder: {
74
    id: "communityExperienceModal.groupPlaceholder",
75
    defaultMessage: "e.g. Government of Canada",
76
  },
77
  projectLabel: {
78
    id: "communityExperienceModal.projectLabel",
79
    defaultMessage: "Project / Product Name",
80
  },
81
  projectPlaceholder: {
82
    id: "communityExperienceModal.projectPlaceholder",
83
    defaultMessage: "e.g. Talent Cloud",
84
  },
85
  startDateLabel: {
86
    id: "communityExperienceModal.startDateLabel",
87
    defaultMessage: "Select a Start Date",
88
  },
89
  datePlaceholder: {
90
    id: "communityExperienceModal.datePlaceholder",
91
    defaultMessage: "yyyy-mm-dd",
92
  },
93
  isActiveLabel: {
94
    id: "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: "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
interface CommunityExperienceSubmitData {
117
  experienceCommunity: ExperienceCommunity;
118
  savedRequiredSkills: Skill[];
119
  savedOptionalSkills: Skill[];
120
  useAsEducationRequirement: boolean;
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
    useAsEducationRequirement,
158
  } = data;
159
  const skillToName = (skill: Skill): string =>
160
    localizeFieldNonNull(locale, skill, "name");
161
  return {
162
    requiredSkills: savedRequiredSkills.map(skillToName),
163
    optionalSkills: savedOptionalSkills.map(skillToName),
164
    useAsEducationRequirement,
165
    title: experienceCommunity.title,
166
    group: experienceCommunity.group,
167
    project: experienceCommunity.project,
168
    startDate: toInputDateString(experienceCommunity.start_date),
169
    isActive: experienceCommunity.is_active,
170
    endDate: experienceCommunity.end_date
171
      ? toInputDateString(experienceCommunity.end_date)
172
      : "",
173
  };
174
};
175
176
/* eslint-disable @typescript-eslint/camelcase */
177
const formValuesToData = (
178
  formValues: CommunityExperienceFormValues,
179
  originalExperience: ExperienceCommunity,
180
  locale: Locales,
181
  skills: Skill[],
182
): CommunityExperienceSubmitData => {
183
  const nameToSkill = (name: string): Skill | null =>
184
    matchValueToModel(locale, "name", name, skills);
185
  return {
186
    experienceCommunity: {
187
      ...originalExperience,
188
      title: formValues.title,
189
      group: formValues.group,
190
      project: formValues.project,
191
      start_date: fromInputDateString(formValues.startDate),
192
      is_active: formValues.isActive,
193
      end_date: formValues.endDate
194
        ? fromInputDateString(formValues.endDate)
195
        : null,
196
    },
197
    useAsEducationRequirement: formValues.useAsEducationRequirement,
198
    savedRequiredSkills: formValues.requiredSkills
199
      .map(nameToSkill)
200
      .filter(notEmpty),
201
    savedOptionalSkills: formValues.optionalSkills
202
      .map(nameToSkill)
203
      .filter(notEmpty),
204
  };
205
};
206
207
const newCommunityExperience = (): ExperienceCommunity => ({
208
  id: 0,
209
  title: "",
210
  group: "",
211
  project: "",
212
  is_active: false,
213
  start_date: new Date(),
214
  end_date: null,
215
});
216
/* eslint-enable @typescript-eslint/camelcase */
217
218
export const CommunityExperienceModal: React.FC<CommunityExperienceModalProps> = ({
219
  modalId,
220
  experienceCommunity,
221
  jobId,
222
  requiredSkills,
223
  savedRequiredSkills,
224
  optionalSkills,
225
  savedOptionalSkills,
226
  experienceRequirments,
227
  useAsEducationRequirement,
228
  parentElement,
229
  visible,
230
  onModalCancel,
231
  onModalConfirm,
232
}) => {
233
  const intl = useIntl();
234
  const locale = getLocale(intl.locale);
235
236
  const originalExperience = experienceCommunity ?? newCommunityExperience();
237
238
  const skillToName = (skill: Skill): string =>
239
    localizeFieldNonNull(locale, skill, "name");
240
241
  const initialFormValues = dataToFormValues(
242
    {
243
      experienceCommunity: originalExperience,
244
      savedRequiredSkills,
245
      savedOptionalSkills,
246
      useAsEducationRequirement,
247
    },
248
    locale,
249
  );
250
251
  const validationSchema = Yup.object().shape({
252
    ...skillValidationShape,
253
    ...educationValidationShape,
254
    ...validationShape(intl),
255
  });
256
257
  const detailsSubform = (
258
    <div data-c-container="medium">
259
      <div data-c-grid="gutter(all, 1) middle">
260
        <FastField
261
          id="title"
262
          name="title"
263
          type="text"
264
          grid="base(1of1)"
265
          component={TextInput}
266
          required
267
          label={intl.formatMessage(messages.titleLabel)}
268
          placeholder={intl.formatMessage(messages.titlePlaceholder)}
269
        />
270
        <FastField
271
          id="group"
272
          type="text"
273
          name="group"
274
          component={TextInput}
275
          required
276
          grid="tl(1of2)"
277
          label={intl.formatMessage(messages.groupLabel)}
278
          placeholder={intl.formatMessage(messages.groupPlaceholder)}
279
        />
280
        <FastField
281
          id="project"
282
          type="text"
283
          name="project"
284
          component={TextInput}
285
          required
286
          grid="tl(1of2)"
287
          label={intl.formatMessage(messages.projectLabel)}
288
          placeholder={intl.formatMessage(messages.projectPlaceholder)}
289
        />
290
        <FastField
291
          id="startDate"
292
          name="startDate"
293
          component={DateInput}
294
          required
295
          grid="base(1of1)"
296
          label={intl.formatMessage(messages.startDateLabel)}
297
          placeholder={intl.formatMessage(messages.datePlaceholder)}
298
        />
299
        <Field
300
          id="isActive"
301
          name="isActive"
302
          component={CheckboxInput}
303
          grid="tl(1of2)"
304
          label={intl.formatMessage(messages.isActiveLabel)}
305
        />
306
        <Field
307
          id="endDate"
308
          name="endDate"
309
          component={DateInput}
310
          grid="base(1of2)"
311
          label={intl.formatMessage(messages.endDateLabel)}
312
          placeholder={intl.formatMessage(messages.datePlaceholder)}
313
        />
314
      </div>
315
    </div>
316
  );
317
318
  return (
319
    <Modal
320
      id={modalId}
321
      parentElement={parentElement}
322
      visible={visible}
323
      onModalCancel={onModalCancel}
324
      onModalConfirm={onModalCancel}
325
      className="application-experience-dialog"
326
    >
327
      <ExperienceModalHeader
328
        title={intl.formatMessage(messages.modalTitle)}
329
        iconClass="fa-people-carry"
330
      />
331
      <Formik
332
        enableReinitialize
333
        initialValues={initialFormValues}
334
        onSubmit={async (values, actions): Promise<void> => {
335
          await onModalConfirm(
336
            formValuesToData(values, originalExperience, locale, [
337
              ...requiredSkills,
338
              ...optionalSkills,
339
            ]),
340
          );
341
          actions.setSubmitting(false);
342
        }}
343
        validationSchema={validationSchema}
344
      >
345
        {(formikProps): React.ReactElement => (
346
          <Form>
347
            <Modal.Body>
348
              <ExperienceDetailsIntro
349
                description={intl.formatMessage(messages.modalDescription)}
350
              />
351
              {detailsSubform}
352
              <SkillSubform
353
                jobId={jobId}
354
                jobRequiredSkills={requiredSkills.map(skillToName)}
355
                jobOptionalSkills={optionalSkills.map(skillToName)}
356
              />
357
              <EducationSubform {...experienceRequirments} />
358
            </Modal.Body>
359
            <ExperienceModalFooter buttonsDisabled={formikProps.isSubmitting} />
360
          </Form>
361
        )}
362
      </Formik>
363
    </Modal>
364
  );
365
};
366
367
export default CommunityExperienceModal;
368