Passed
Push — feature/data-request-hooks ( 562d87...7ed4d3 )
by Tristan
06:17
created

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

Complexity

Total Complexity 15
Complexity/F 0

Size

Lines of Code 283
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

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