Passed
Push — feature/add-2fa-support ( 23e818...85b1f3 )
by Chris
24:19 queued 11:16
created

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

Complexity

Total Complexity 15
Complexity/F 0

Size

Lines of Code 282
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 230
dl 0
loc 282
rs 10
c 0
b 0
f 0
wmc 15
mnd 15
bc 15
fnc 0
bpm 0
cpm 0
noi 0
1
/* eslint-disable @typescript-eslint/camelcase */
2
import React from "react";
3
import { connect } from "react-redux";
4
import { defineMessages, WrappedComponentProps, injectIntl } from "react-intl";
5
import {
6
  RatingGuideAnswer as RatingGuideAnswerModel,
7
  Skill,
8
  Criteria,
9
} from "../../models/types";
10
import Select, { SelectOption } from "../Select";
11
import UpdatingTextArea from "../UpdatingTextArea";
12
import { getId, hasKey } from "../../helpers/queries";
13
import { RootState } from "../../store/store";
14
import { DispatchType } from "../../configureStore";
15
import {
16
  editTempRatingGuideAnswer,
17
  editRatingGuideAnswer,
18
  updateRatingGuideAnswer,
19
  storeNewRatingGuideAnswer,
20
  deleteTempRatingGuideAnswer,
21
  deleteRatingGuideAnswer,
22
} from "../../store/RatingGuideAnswer/ratingGuideAnswerActions";
23
import {
24
  ratingGuideAnswerIsEdited,
25
  ratingGuideAnswerIsUpdating,
26
  getRatingGuideAnswerById,
27
  getTempRatingGuideAnswerById,
28
  tempRatingGuideAnswerIsSaving,
29
} from "../../store/RatingGuideAnswer/ratingGuideAnswerSelectors";
30
import { getCriteriaById } from "../../store/Job/jobSelector";
31
import {
32
  getCriteriaToSkills,
33
  getCachedCriteriaUnansweredForQuestion,
34
} from "../../store/Job/jobSelectorComplex";
35
36
interface RatingGuideAnswerProps {
37
  answer: RatingGuideAnswerModel | null;
38
  unansweredCriteria: Criteria[];
39
  answerCriterion: Criteria | null;
40
  criteriaIdToSkill: { [id: number]: Skill | null };
41
  temp?: boolean;
42
  isUpdating: boolean;
43
  isEdited: boolean;
44
  editAnswer: (newAnswer: RatingGuideAnswerModel) => void;
45
  updateAnswer: (updatedAnswer: RatingGuideAnswerModel) => void;
46
  deleteAnswer: (id: number) => void;
47
}
48
49
const messages = defineMessages({
50
  selectLabel: {
51
    id: "ratingGuideAnswer.selectLabel",
52
    defaultMessage: "Select a Skill",
53
    description:
54
      "Label for the dropdown for selecting the skill this rating guide answer is used to assess.",
55
  },
56
  nullSelection: {
57
    id: "ratingGuideAnswer.nullSelection",
58
    defaultMessage: "Select a Skill...",
59
    description:
60
      "Null selection for the dropdown for selecting a skill this rating guide answer is used to assess.",
61
  },
62
  inputLabel: {
63
    id: "ratingGuideAnswer.answerLabel",
64
    defaultMessage: "Acceptable Passing Answer / Required to demonstrate",
65
    description: "Label for the rating guide answer input.",
66
  },
67
  inputPlaceholder: {
68
    id: "ratingGuideAnswer.answerPlaceholder",
69
    defaultMessage:
70
      "Write the expected answer to pass the applicant on this skill...",
71
    description: "Placeholder text for the rating guide answer.",
72
  },
73
});
74
75
const getAvailableCriteria = (
76
  availableCriteria: Criteria[],
77
  answerCriterion: Criteria | null,
78
): Criteria[] => {
79
  const availableCriteriaIds = availableCriteria.map(getId);
80
  // If this answer has a selected criteria, it should be considered available
81
  if (
82
    answerCriterion === null ||
83
    availableCriteriaIds.includes(answerCriterion.id)
84
  ) {
85
    return availableCriteria;
86
  }
87
  return [...availableCriteria, answerCriterion];
88
};
89
90
const RatingGuideAnswer: React.FunctionComponent<
91
  RatingGuideAnswerProps & WrappedComponentProps
92
> = ({
93
  answer,
94
  unansweredCriteria,
95
  answerCriterion,
96
  criteriaIdToSkill,
97
  isUpdating,
98
  editAnswer,
99
  updateAnswer,
100
  deleteAnswer,
101
  intl,
102
}): React.ReactElement | null => {
103
  if (answer === null) {
104
    return null;
105
  }
106
  const availableCriteria = getAvailableCriteria(
107
    unansweredCriteria,
108
    answerCriterion,
109
  );
110
  if (availableCriteria.length === 0) {
111
    return null;
112
  }
113
  const options = availableCriteria.map(
114
    (criterion): SelectOption => {
115
      const skill = hasKey<Skill | null>(criteriaIdToSkill, criterion.id)
116
        ? criteriaIdToSkill[criterion.id]
117
        : null;
118
      return {
119
        value: criterion.id,
120
        label: skill ? skill[intl.locale].name : "",
121
      };
122
    },
123
  );
124
  return (
125
    <div data-c-grid="gutter top">
126
      <div data-c-grid-item="base(1of1) tp(1of8)" data-c-alignment="center" />
127
      <div data-c-grid-item="base(1of1) tp(2of8)">
128
        <Select
129
          id={`ratingGuideSelectSkill_${answer.id}`}
130
          name="ratingGuideSelectSkill"
131
          label={intl.formatMessage(messages.selectLabel)}
132
          required
133
          options={options}
134
          onChange={(event): void =>
135
            updateAnswer({
136
              ...answer,
137
              criterion_id: event.target.value
138
                ? Number(event.target.value)
139
                : null,
140
            })
141
          }
142
          selected={answer.criterion_id}
143
          nullSelection={intl.formatMessage(messages.nullSelection)}
144
        />
145
      </div>
146
      <div data-c-grid-item="base(1of1) tp(4of8)">
147
        <UpdatingTextArea
148
          id={`ratingGuideAnswer${answer.id}`}
149
          name="ratingGuideAnswer"
150
          label={intl.formatMessage(messages.inputLabel)}
151
          required
152
          placeholder={intl.formatMessage(messages.inputPlaceholder)}
153
          value={answer.expected_answer || ""}
154
          updateDelay={500}
155
          onChange={(event: React.ChangeEvent<HTMLTextAreaElement>): void => {
156
            const newAnswer = String(event.target.value);
157
            editAnswer({
158
              ...answer,
159
              expected_answer: newAnswer,
160
            });
161
          }}
162
          handleSave={(): void => {
163
            updateAnswer(answer);
164
          }}
165
        />
166
      </div>
167
      <div data-c-alignment="center" data-c-grid-item="base(1of1) tp(1of8)">
168
        <button
169
          className="button-trash"
170
          type="button"
171
          onClick={(): void => deleteAnswer(answer.id)}
172
          disabled={isUpdating}
173
        >
174
          {isUpdating ? (
175
            <i className="fa fa-spinner fa-spin" />
176
          ) : (
177
            <i className="fa fa-trash" />
178
          )}
179
        </button>
180
      </div>
181
    </div>
182
  );
183
};
184
185
const getAnswer = (
186
  state: RootState,
187
  answerId: number,
188
  temp?: boolean,
189
): RatingGuideAnswerModel | null =>
190
  temp
191
    ? getTempRatingGuideAnswerById(state, { answerId })
192
    : getRatingGuideAnswerById(state, { answerId });
193
194
interface RatingGuideAnswerContainerProps {
195
  answerId: number;
196
  temp?: boolean;
197
}
198
199
const emptyCriteria: Criteria[] = [];
200
201
const mapStateToProps = (
202
  state: RootState,
203
  ownProps: RatingGuideAnswerContainerProps,
204
): {
205
  answer: RatingGuideAnswerModel | null;
206
  unansweredCriteria: Criteria[];
207
  answerCriterion: Criteria | null;
208
  criteriaIdToSkill: { [id: number]: Skill | null };
209
  temp?: boolean;
210
  isUpdating: boolean;
211
  isEdited: boolean;
212
} => {
213
  const answer = getAnswer(state, ownProps.answerId, ownProps.temp);
214
  return {
215
    answer,
216
    unansweredCriteria: answer
217
      ? getCachedCriteriaUnansweredForQuestion(state, {
218
          questionId: answer.rating_guide_question_id,
219
          isTempQuestion: false,
220
        })
221
      : emptyCriteria,
222
    answerCriterion:
223
      answer && answer.criterion_id
224
        ? getCriteriaById(state, { criterionId: answer.criterion_id })
225
        : null,
226
    criteriaIdToSkill: getCriteriaToSkills(state),
227
    isEdited: ratingGuideAnswerIsEdited(state, ownProps),
228
    isUpdating: ownProps.temp
229
      ? tempRatingGuideAnswerIsSaving(state, ownProps.answerId)
230
      : ratingGuideAnswerIsUpdating(state, ownProps.answerId),
231
  };
232
};
233
234
const mapDispatchToProps = (dispatch: DispatchType, ownProps): any => ({
235
  editAnswer: ownProps.temp
236
    ? (ratingGuideAnswer: RatingGuideAnswerModel): void => {
237
        dispatch(editTempRatingGuideAnswer(ratingGuideAnswer));
238
      }
239
    : (ratingGuideAnswer: RatingGuideAnswerModel): void => {
240
        dispatch(editRatingGuideAnswer(ratingGuideAnswer));
241
      },
242
  updateAnswer: ownProps.temp
243
    ? (ratingGuideAnswer: RatingGuideAnswerModel): void => {
244
        // We must also edit the local temp answer, because it will be checked
245
        // against the updated version when the store request succeeds.
246
        dispatch(editTempRatingGuideAnswer(ratingGuideAnswer));
247
        dispatch(storeNewRatingGuideAnswer(ratingGuideAnswer));
248
      }
249
    : (ratingGuideAnswer: RatingGuideAnswerModel): void =>
250
        dispatch(updateRatingGuideAnswer(ratingGuideAnswer)),
251
  deleteAnswer: ownProps.temp
252
    ? (id: number): void => {
253
        dispatch(deleteTempRatingGuideAnswer(id));
254
      }
255
    : (ratingGuideAnswerId: number): void => {
256
        dispatch(deleteRatingGuideAnswer(ratingGuideAnswerId));
257
      },
258
  // This is only possibly used by mergeProps
259
  editTempAnswer: (ratingGuideAnswer: RatingGuideAnswerModel): void => {
260
    dispatch(editTempRatingGuideAnswer(ratingGuideAnswer));
261
  },
262
});
263
264
const mergeProps = (stateProps, dispatchProps, ownProps) => ({
265
  ...ownProps,
266
  ...stateProps,
267
  ...dispatchProps,
268
  // If this is a currently saving temp answer, ensure we don't launch another store request
269
  updateAnswer:
270
    ownProps.temp && stateProps.isUpdating
271
      ? dispatchProps.editTempAnswer
272
      : dispatchProps.updateAnswer,
273
});
274
275
const RatingGuideAnswerContainer = connect(
276
  mapStateToProps,
277
  mapDispatchToProps,
278
  mergeProps,
279
)(injectIntl(RatingGuideAnswer));
280
281
export default RatingGuideAnswerContainer;
282