Passed
Push — task/hygrogen-react-components ( c486f0 )
by Yonathan
07:32
created

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

Complexity

Total Complexity 6
Complexity/F 0

Size

Lines of Code 472
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

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