Passed
Push — task/manager-application-revie... ( 376fbc...a77584 )
by Tristan
04:25
created

resources/assets/js/components/AssessmentPlan/AssessmentPlanSkill.tsx   A

Complexity

Total Complexity 8
Complexity/F 0

Size

Lines of Code 385
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 306
dl 0
loc 385
rs 10
c 0
b 0
f 0
wmc 8
mnd 8
bc 8
fnc 0
bpm 0
cpm 0
noi 0
1
import React, { useEffect } from "react";
2
import {
3
  injectIntl,
4
  WrappedComponentProps,
5
  defineMessages,
6
  FormattedMessage,
7
} from "react-intl";
8
import { connect } from "react-redux";
9
import {
10
  skillLevelDescription as SkillLevelDescriptionMessage,
11
  skillLevelName,
12
  assessmentType,
13
} from "../../models/localizedConstants";
14
import Select, { SelectOption } from "../Select";
15
import { AssessmentTypeId, enumToIds } from "../../models/lookupConstants";
16
import {
17
  Criteria,
18
  Assessment,
19
  TempAssessment,
20
  Skill,
21
} from "../../models/types";
22
import { RootState } from "../../store/store";
23
import {
24
  getTempAssessmentsByCriterion,
25
  tempAssessmentsAreSavingByCriterion,
26
  getCachedAssessmentsByCriterion,
27
  getCachedAssessmentsAreEditedByCriteria,
28
  getCachedAssessmentsAreUpdatingByCriteria,
29
} from "../../store/Assessment/assessmentSelector";
30
import { DispatchType } from "../../configureStore";
31
import {
32
  updateAssessment as updateAssessmentAction,
33
  editAssessment as editAssessmentAction,
34
  editTempAssessment as editTempAssessmentAction,
35
  deleteTempAssessment as deleteTempAssessmentAction,
36
  createTempAssessment,
37
  storeNewAssessment,
38
  deleteAssessment,
39
} from "../../store/Assessment/assessmentActions";
40
import { getCriteriaById } from "../../store/Job/jobSelector";
41
import { getSkillById } from "../../store/Skill/skillSelector";
42
import { notEmpty } from "../../helpers/queries";
43
import { getLocale, localizeField } from "../../helpers/localize";
44
45
interface AssessmentPlanSkillProps {
46
  criterion: Criteria | null;
47
  skill: Skill | null;
48
  assessments: Assessment[];
49
  assessmentsEdited: { [id: number]: boolean };
50
  assessmentsUpdating: { [id: number]: boolean };
51
  tempAssessments: TempAssessment[];
52
  tempAssessmentsSaving: { [id: number]: boolean };
53
  createAssessment: () => void;
54
  editAssessment: (newAssessment: Assessment) => void;
55
  updateAssessment: (newAssessment: Assessment) => void;
56
  removeAssessment: (assessmentId: number) => void;
57
  editTempAssessment: (newAssessment: TempAssessment) => void;
58
  saveTempAssessment: (assessment: Assessment) => void;
59
  removeTempAssessment: (id: number) => void;
60
}
61
62
const localizations = defineMessages({
63
  selectAssessmentNull: {
64
    id: "assessmentPlan.selectAssessment.null",
65
    defaultMessage: "Select an Assessment",
66
    description:
67
      "Default select element before an assessment type has been chosen",
68
  },
69
  selectAssessmentLabel: {
70
    id: "assessmentPlan.selectAssessment.label",
71
    defaultMessage: "Select an Assessment",
72
    description: "Label for Assessment Type select element.",
73
  },
74
});
75
76
export const AssessmentPlanSkill: React.FunctionComponent<AssessmentPlanSkillProps &
77
  WrappedComponentProps> = ({
78
  criterion,
79
  skill,
80
  assessments,
81
  assessmentsEdited,
82
  assessmentsUpdating,
83
  tempAssessments,
84
  tempAssessmentsSaving,
85
  createAssessment,
86
  editAssessment,
87
  updateAssessment,
88
  removeAssessment,
89
  editTempAssessment,
90
  saveTempAssessment,
91
  removeTempAssessment,
92
  intl,
93
}: AssessmentPlanSkillProps &
94
  WrappedComponentProps): React.ReactElement | null => {
95
  const locale = getLocale(intl.locale);
96
  useEffect((): void => {
97
    if (criterion === null || skill === null) {
98
      return;
99
    }
100
    assessments.forEach(
101
      (assessment): void => {
102
        // If assessment has been edited, and is not currently being updated, start an update.
103
        if (
104
          assessmentsEdited[assessment.id] &&
105
          !assessmentsUpdating[assessment.id]
106
        ) {
107
          updateAssessment(assessment);
108
        }
109
      },
110
      [assessments, assessmentsEdited, assessmentsUpdating],
111
    );
112
  });
113
  useEffect((): void => {
114
    if (criterion === null || skill === null) {
115
      return;
116
    }
117
    tempAssessments.forEach((temp): void => {
118
      // If any temp assessments exist, we want to save them as soon as they're valid
119
      if (!tempAssessmentsSaving[temp.id] && temp.assessment_type_id !== null) {
120
        saveTempAssessment(temp as Assessment); // TODO: remove TempAssessment type, just use Assessment everywhere
121
      }
122
    });
123
  }, [
124
    criterion,
125
    saveTempAssessment,
126
    skill,
127
    tempAssessments,
128
    tempAssessmentsSaving,
129
  ]);
130
131
  if (criterion === null || skill === null) {
132
    return null;
133
  }
134
135
  const skillLevel = intl.formatMessage(
136
    skillLevelName(criterion.skill_level_id, skill.skill_type_id),
137
  );
138
  const skillLevelDescription = intl.formatMessage(
139
    SkillLevelDescriptionMessage(criterion.skill_level_id, skill.skill_type_id),
140
  );
141
  const skillDescription = localizeField(locale, criterion, "description")
142
    ? localizeField(locale, criterion, "description")
143
    : localizeField(locale, skill, "description");
144
  const assessmentTypeOptions = enumToIds(AssessmentTypeId).map(
145
    (typeId): SelectOption => {
146
      return {
147
        value: typeId,
148
        label: intl.formatMessage(assessmentType(typeId)),
149
      };
150
    },
151
  );
152
  const selectAssessmentNull = intl.formatMessage(
153
    localizations.selectAssessmentNull,
154
  );
155
  const selectAssessmentLabel = intl.formatMessage(
156
    localizations.selectAssessmentLabel,
157
  );
158
159
  const selectedAssessmentTypes: number[] = [
160
    ...assessments.map((assessment): number => assessment.assessment_type_id),
161
    ...tempAssessments
162
      .map((temp): number | null => temp.assessment_type_id)
163
      .filter(notEmpty),
164
  ];
165
  const SelectBlock: React.FunctionComponent<{
166
    assessment: Assessment | TempAssessment;
167
    isUpdating: boolean;
168
    onChange: (newAssessment: Assessment | TempAssessment) => void;
169
    onDelete: (id: number) => void;
170
  }> = ({ assessment, isUpdating, onChange, onDelete }): React.ReactElement => {
171
    const options = assessmentTypeOptions.filter((option): boolean => {
172
      // Ensure we can't select an option already selected in a sibling selector
173
      return (
174
        option.value === assessment.assessment_type_id ||
175
        !selectedAssessmentTypes.includes(Number(option.value))
176
      );
177
    });
178
    return (
179
      <div data-c-grid="middle">
180
        <div data-c-grid-item="base(2of3) tl(4of5)">
181
          <Select
182
            id={`assessmentSelect_${criterion.id}_${assessment.id}`}
183
            name="assessmentTypeId"
184
            label={selectAssessmentLabel}
185
            required
186
            options={options}
187
            onChange={(event: React.ChangeEvent<HTMLSelectElement>): void => {
188
              const selectedType = Number(event.target.value);
189
              onChange({
190
                ...assessment,
191
                // eslint-disable-next-line @typescript-eslint/camelcase
192
                assessment_type_id: selectedType,
193
              });
194
            }}
195
            selected={assessment.assessment_type_id}
196
            nullSelection={selectAssessmentNull}
197
          />
198
        </div>
199
        <div
200
          data-c-alignment="base(center)"
201
          data-c-grid-item="base(1of3) tl(1of5)"
202
        >
203
          <button
204
            className="button-trash"
205
            type="button"
206
            onClick={(): void => {
207
              onDelete(assessment.id);
208
            }}
209
            disabled={isUpdating}
210
          >
211
            {isUpdating ? (
212
              <i className="fa fa-spinner fa-spin" />
213
            ) : (
214
              <i className="fa fa-trash" />
215
            )}
216
          </button>
217
        </div>
218
      </div>
219
    );
220
  };
221
222
  return (
223
    <div
224
      data-c-border="top(thin, solid, black)"
225
      data-c-margin="top(normal) bottom(normal)"
226
    >
227
      <div data-c-grid="gutter top">
228
        <div data-c-grid-item="base(1of1)">
229
          <h5 data-c-font-size="h4" data-c-margin="top(normal)">
230
            <FormattedMessage
231
              id="assessmentPlan.criteriaTitle"
232
              defaultMessage="{skillName} - {skillLevel}"
233
              description="Title of a skill section in the Assessment Plan Builder."
234
              values={{
235
                skillName: localizeField(locale, skill, "name"),
236
                skillLevel,
237
              }}
238
            />
239
          </h5>
240
        </div>
241
        <div data-c-grid-item="tl(2of7)">
242
          <span data-c-font-weight="bold" data-c-margin="bottom(half)">
243
            <FormattedMessage
244
              id="assessmentPlan.skillDescriptionLabel"
245
              defaultMessage="Description"
246
              description="Label for the text that describes a skill criterion."
247
            />
248
          </span>
249
          <p data-c-font-size="small">{skillDescription}</p>
250
        </div>
251
        <div data-c-grid-item="tl(2of7)">
252
          <span data-c-font-weight="bold" data-c-margin="bottom(half)">
253
            <FormattedMessage
254
              id="assessmentPlan.skillLevelDescriptionLabel"
255
              defaultMessage="Skill Level Selected"
256
              description="Label for the text that describes a Skill Level."
257
            />
258
          </span>
259
          <p data-c-font-size="small">{skillLevelDescription}</p>
260
        </div>
261
        <div data-c-grid-item="tl(3of7)">
262
          <div data-c-grid>
263
            <div data-c-grid-item="base(1of2)">
264
              <span data-c-font-weight="bold" data-c-margin="bottom(half)">
265
                <FormattedMessage
266
                  id="assessmentPlan.assessmentTypesLabel"
267
                  defaultMessage="Assessment Types"
268
                  description="Label for section where you choose assessment types for a criterion."
269
                />
270
              </span>
271
            </div>
272
            <div data-c-alignment="base(right)" data-c-grid-item="base(1of2)">
273
              <button
274
                className="button-link"
275
                type="button"
276
                onClick={(): void => createAssessment()}
277
              >
278
                <FormattedMessage
279
                  id="assessmentPlan.addAssessmentButton"
280
                  defaultMessage="Add an Assessment"
281
                  description="Text for the button that adds a new assessment for a criterion."
282
                />
283
              </button>
284
            </div>
285
          </div>
286
          {assessments.map(
287
            (assessment): React.ReactElement => (
288
              <SelectBlock
289
                key={`assessmentPlanSkillSelectorAssessment${assessment.id}`}
290
                assessment={assessment}
291
                isUpdating={assessmentsUpdating[assessment.id]}
292
                onChange={editAssessment}
293
                onDelete={removeAssessment}
294
              />
295
            ),
296
          )}
297
          {tempAssessments.map(
298
            (tempAssessment): React.ReactElement => (
299
              <SelectBlock
300
                key={`assessmentPlanSkillSelectorTempAssessment${tempAssessment.id}`}
301
                assessment={tempAssessment}
302
                isUpdating={tempAssessmentsSaving[tempAssessment.id]}
303
                onChange={editTempAssessment}
304
                onDelete={removeTempAssessment}
305
              />
306
            ),
307
          )}
308
        </div>
309
      </div>
310
    </div>
311
  );
312
};
313
314
interface AssessmentPlanSkillContainerProps {
315
  criterionId: number;
316
}
317
318
const mapStateToProps = (
319
  state: RootState,
320
  ownProps: AssessmentPlanSkillContainerProps,
321
): {
322
  criterion: Criteria | null;
323
  skill: Skill | null;
324
  assessments: Assessment[];
325
  assessmentsEdited: { [id: number]: boolean };
326
  assessmentsUpdating: { [id: number]: boolean };
327
  tempAssessments: TempAssessment[];
328
  tempAssessmentsSaving: { [id: number]: boolean };
329
} => {
330
  const criterion = getCriteriaById(state, ownProps);
331
  return {
332
    criterion,
333
    skill: criterion ? getSkillById(state, criterion.skill_id) : null,
334
    assessments: getCachedAssessmentsByCriterion(state, ownProps),
335
    assessmentsEdited: getCachedAssessmentsAreEditedByCriteria(state, ownProps),
336
    assessmentsUpdating: getCachedAssessmentsAreUpdatingByCriteria(
337
      state,
338
      ownProps,
339
    ),
340
    tempAssessments: getTempAssessmentsByCriterion(state, ownProps),
341
    tempAssessmentsSaving: tempAssessmentsAreSavingByCriterion(state, ownProps),
342
  };
343
};
344
345
const mapDispatchToProps = (
346
  dispatch: DispatchType,
347
  ownProps: AssessmentPlanSkillContainerProps,
348
): {
349
  createAssessment: () => void;
350
  editAssessment: (newAssessment: Assessment) => void;
351
  updateAssessment: (newAssessment: Assessment) => void;
352
  removeAssessment: (assessmentId: number) => void;
353
  editTempAssessment: (newAssessment: TempAssessment) => void;
354
  saveTempAssessment: (assessment: Assessment) => void;
355
  removeTempAssessment: (id: number) => void;
356
} => ({
357
  createAssessment: (): void => {
358
    dispatch(createTempAssessment(ownProps.criterionId, null));
359
  },
360
  editAssessment: (assessment: Assessment): void => {
361
    dispatch(editAssessmentAction(assessment));
362
  },
363
  updateAssessment: (assessment: Assessment): void =>
364
    dispatch(updateAssessmentAction(assessment)),
365
  removeAssessment: (assessmentId: number): void => {
366
    dispatch(deleteAssessment(assessmentId));
367
  },
368
  editTempAssessment: (assessment: TempAssessment): void => {
369
    dispatch(editTempAssessmentAction(assessment));
370
  },
371
  removeTempAssessment: (id: number): void => {
372
    dispatch(deleteTempAssessmentAction(id));
373
  },
374
  saveTempAssessment: (assessment: Assessment): void => {
375
    dispatch(storeNewAssessment(assessment));
376
  },
377
});
378
379
const AssessmentPlanSkillContainer = connect(
380
  mapStateToProps,
381
  mapDispatchToProps,
382
)(injectIntl(AssessmentPlanSkill));
383
384
export default AssessmentPlanSkillContainer;
385