Passed
Push — dev ( 1db6e3...07e9a4 )
by
unknown
07:30
created

resources/assets/js/store/RatingGuideAnswer/ratingGuideAnswerReducer.ts   A

Complexity

Total Complexity 6
Complexity/F 6

Size

Lines of Code 317
Function Count 1

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 6
eloc 248
mnd 5
bc 5
fnc 1
dl 0
loc 317
rs 10
bpm 5
cpm 6
noi 0
c 0
b 0
f 0

1 Function

Rating   Name   Duplication   Size   Complexity  
A ratingGuideAnswerReducer.ts ➔ hasIdenticalItem 0 6 4
1
import isEqual from "lodash/isEqual";
2
import { RatingGuideAnswer } from "../../models/types";
3
import {
4
  AssessmentPlanAction,
5
  FETCH_ASSESSMENT_PLAN_STARTED,
6
  FETCH_ASSESSMENT_PLAN_SUCCEEEDED,
7
  FETCH_ASSESSMENT_PLAN_FAILED,
8
} from "../AssessmentPlan/assessmentPlanActions";
9
import {
10
  RatingGuideAnswerAction,
11
  CREATE_TEMP_RATING_GUIDE_ANSWER,
12
  DELETE_RATING_GUIDE_ANSWER_FAILED,
13
  DELETE_RATING_GUIDE_ANSWER_STARTED,
14
  DELETE_RATING_GUIDE_ANSWER_SUCCEEDED,
15
  DELETE_TEMP_RATING_GUIDE_ANSWER,
16
  EDIT_RATING_GUIDE_ANSWER,
17
  EDIT_TEMP_RATING_GUIDE_ANSWER,
18
  STORE_NEW_RATING_GUIDE_ANSWER_FAILED,
19
  STORE_NEW_RATING_GUIDE_ANSWER_STARTED,
20
  STORE_NEW_RATING_GUIDE_ANSWER_SUCCEEDED,
21
  UPDATE_RATING_GUIDE_ANSWER_FAILED,
22
  UPDATE_RATING_GUIDE_ANSWER_STARTED,
23
  UPDATE_RATING_GUIDE_ANSWER_SUCCEEDED,
24
} from "./ratingGuideAnswerActions";
25
import {
26
  getId,
27
  mapToObject,
28
  deleteProperty,
29
  hasKey,
30
} from "../../helpers/queries";
31
32
export interface RatingGuideAnswerState {
33
  ratingGuideAnswers: {
34
    // Stores rating guide answers that are synced with the server
35
    [id: number]: RatingGuideAnswer;
36
  };
37
  editedRatingGuideAnswers: {
38
    // For storing rating guide answers that have been edited locally
39
    [id: number]: RatingGuideAnswer;
40
  };
41
  tempRatingGuideAnswers: {
42
    // For storing local rating guide answers that have never been saved to server
43
    [id: number]: RatingGuideAnswer;
44
  };
45
  tempRatingGuideAnswerSaving: {
46
    // Tracks whether a RatingGuideAnswer is currently being saved to server
47
    [id: number]: boolean;
48
  };
49
  ratingGuideAnswerUpdates: {
50
    // Tracks the number of pending updates
51
    [id: number]: number;
52
  };
53
  ratingGuideAnswerDeletes: {
54
    // Tracks the number of pending delete requests
55
    [id: number]: number;
56
  };
57
}
58
59
export const initState = (): RatingGuideAnswerState => ({
60
  ratingGuideAnswers: {},
61
  editedRatingGuideAnswers: {},
62
  tempRatingGuideAnswers: {},
63
  tempRatingGuideAnswerSaving: {},
64
  ratingGuideAnswerUpdates: {},
65
  ratingGuideAnswerDeletes: {},
66
});
67
68
/**
69
 * Return editedAssessments, with assessment removed if it is present and identical.
70
 * This is useful for not deleting a temp state when the first of several queued async updates completes.
71
 *
72
 */
73
const deleteEditedIfIdentical = (
74
  editedRatingGuideAnswers: { [id: number]: RatingGuideAnswer },
75
  ratingGuideAnswer: RatingGuideAnswer,
76
): { [id: number]: RatingGuideAnswer } => {
77
  const { id } = ratingGuideAnswer;
78
  if (
79
    hasKey(editedRatingGuideAnswers, id) &&
80
    isEqual(editedRatingGuideAnswers[id], ratingGuideAnswer)
81
  ) {
82
    return deleteProperty(editedRatingGuideAnswers, id);
83
  }
84
  return editedRatingGuideAnswers;
85
};
86
87
const incrementUpdates = (
88
  updates: { [id: number]: number },
89
  id: number,
90
): { [id: number]: number } => {
91
  const oldVal = hasKey(updates, id) ? updates[id] : 0;
92
  return {
93
    ...updates,
94
    [id]: oldVal + 1,
95
  };
96
};
97
98
const decrementUpdates = (
99
  updates: { [id: number]: number },
100
  id: number,
101
): { [id: number]: number } => {
102
  const oldVal = hasKey(updates, id) ? updates[id] : 0;
103
  const newVal = Math.max(oldVal - 1, 0); // update count cannot be less than 0.
104
  return {
105
    ...updates,
106
    [id]: newVal,
107
  };
108
};
109
110
function hasIdenticalItem<T extends { id: number }>(
111
  items: { [id: number]: T },
112
  item: T,
113
): boolean {
114
  return hasKey(items, item.id) && isEqual(items[item.id], item);
115
}
116
117
const addTempRatingGuideAnswer = (
118
  state: RatingGuideAnswerState,
119
  ratingGuideQuestionId: number,
120
  criterionId: number | null,
121
  expectedAnswer: string | null,
122
): RatingGuideAnswerState => {
123
  const currentIds = Object.values(state.tempRatingGuideAnswers).map(getId);
124
  const newId = Math.max(...currentIds, 0) + 1;
125
  return {
126
    ...state,
127
    tempRatingGuideAnswers: {
128
      ...state.tempRatingGuideAnswers,
129
      [newId]: {
130
        id: newId,
131
        rating_guide_question_id: ratingGuideQuestionId,
132
        criterion_id: criterionId,
133
        expected_answer: expectedAnswer,
134
      },
135
    },
136
  };
137
};
138
139
export const ratingGuideAnswerReducer = (
140
  state = initState(),
141
  action: AssessmentPlanAction | RatingGuideAnswerAction,
142
): RatingGuideAnswerState => {
143
  switch (action.type) {
144
    case FETCH_ASSESSMENT_PLAN_STARTED:
145
      return state;
146
    case FETCH_ASSESSMENT_PLAN_SUCCEEEDED:
147
      return {
148
        ...state,
149
        ratingGuideAnswers: {
150
          ...state.ratingGuideAnswers,
151
          ...mapToObject(action.payload.ratingGuideAnswers, getId),
152
        },
153
      };
154
    case FETCH_ASSESSMENT_PLAN_FAILED:
155
      return state;
156
    case EDIT_RATING_GUIDE_ANSWER:
157
      return {
158
        ...state,
159
        editedRatingGuideAnswers: {
160
          ...state.editedRatingGuideAnswers,
161
          [action.payload.ratingGuideAnswer.id]:
162
            action.payload.ratingGuideAnswer,
163
        },
164
      };
165
    case UPDATE_RATING_GUIDE_ANSWER_STARTED:
166
      return {
167
        ...state,
168
        ratingGuideAnswerUpdates: incrementUpdates(
169
          state.ratingGuideAnswerUpdates,
170
          action.payload.ratingGuideAnswer.id,
171
        ),
172
      };
173
    case UPDATE_RATING_GUIDE_ANSWER_SUCCEEDED:
174
      return {
175
        ...state,
176
        ratingGuideAnswers: {
177
          ...state.ratingGuideAnswers,
178
          [action.payload.ratingGuideAnswer.id]:
179
            action.payload.ratingGuideAnswer,
180
        },
181
        editedRatingGuideAnswers: deleteEditedIfIdentical(
182
          state.editedRatingGuideAnswers,
183
          action.payload.ratingGuideAnswer,
184
        ),
185
        ratingGuideAnswerUpdates: decrementUpdates(
186
          state.ratingGuideAnswerUpdates,
187
          action.payload.ratingGuideAnswer.id,
188
        ),
189
      };
190
    case UPDATE_RATING_GUIDE_ANSWER_FAILED:
191
      // TODO: do something with error
192
      // TODO: should the temp state really be deleted?
193
      return {
194
        ...state,
195
        editedRatingGuideAnswers: deleteEditedIfIdentical(
196
          state.editedRatingGuideAnswers,
197
          action.meta,
198
        ),
199
        ratingGuideAnswerUpdates: decrementUpdates(
200
          state.ratingGuideAnswerUpdates,
201
          action.meta.id,
202
        ),
203
      };
204
    case DELETE_RATING_GUIDE_ANSWER_STARTED:
205
      return {
206
        ...state,
207
        ratingGuideAnswerDeletes: incrementUpdates(
208
          state.ratingGuideAnswerDeletes,
209
          action.payload.id,
210
        ),
211
      };
212
    case DELETE_RATING_GUIDE_ANSWER_SUCCEEDED:
213
      // TODO: should this delete both canonical and edited ratingGuideAnswers?
214
      // ...For now, I don't know of any situations where we wouldn't want both.
215
      return {
216
        ...state,
217
        ratingGuideAnswers: deleteProperty(
218
          state.ratingGuideAnswers,
219
          action.payload.id,
220
        ),
221
        editedRatingGuideAnswers: deleteProperty(
222
          state.editedRatingGuideAnswers,
223
          action.payload.id,
224
        ),
225
        ratingGuideAnswerDeletes: decrementUpdates(
226
          state.ratingGuideAnswerDeletes,
227
          action.payload.id,
228
        ),
229
      };
230
    case DELETE_RATING_GUIDE_ANSWER_FAILED:
231
      return {
232
        ...state,
233
        ratingGuideAnswerDeletes: decrementUpdates(
234
          state.ratingGuideAnswerDeletes,
235
          action.meta.id,
236
        ),
237
      };
238
    case CREATE_TEMP_RATING_GUIDE_ANSWER:
239
      return addTempRatingGuideAnswer(
240
        state,
241
        action.payload.ratingGuideQuestionId,
242
        action.payload.criterionId,
243
        action.payload.expectedAnswer,
244
      );
245
    case EDIT_TEMP_RATING_GUIDE_ANSWER:
246
      return {
247
        ...state,
248
        tempRatingGuideAnswers: {
249
          ...state.tempRatingGuideAnswers,
250
          [action.payload.ratingGuideAnswer.id]:
251
            action.payload.ratingGuideAnswer,
252
        },
253
      };
254
    case DELETE_TEMP_RATING_GUIDE_ANSWER:
255
      return {
256
        ...state,
257
        tempRatingGuideAnswers: deleteProperty(
258
          state.tempRatingGuideAnswers,
259
          action.payload.id,
260
        ),
261
      };
262
    case STORE_NEW_RATING_GUIDE_ANSWER_STARTED:
263
      return {
264
        ...state,
265
        tempRatingGuideAnswerSaving: {
266
          ...state.tempRatingGuideAnswerSaving,
267
          [action.payload.ratingGuideAnswer.id]: true,
268
        },
269
      };
270
    case STORE_NEW_RATING_GUIDE_ANSWER_SUCCEEDED:
271
      return {
272
        ...state,
273
        ratingGuideAnswers: {
274
          ...state.ratingGuideAnswers,
275
          [action.payload.ratingGuideAnswer.id]:
276
            action.payload.ratingGuideAnswer,
277
        },
278
        tempRatingGuideAnswerSaving: deleteProperty(
279
          state.tempRatingGuideAnswerSaving,
280
          action.payload.oldRatingGuideAnswer.id,
281
        ),
282
        // If temp ratingGuideAnswer differs from saved, move it to edited (with updated id)
283
        // If temp ratingGuideAnswer is equal to new saved, simply remove it from temp.
284
        editedRatingGuideAnswers: hasIdenticalItem(
285
          state.tempRatingGuideAnswers,
286
          action.payload.oldRatingGuideAnswer,
287
        )
288
          ? state.editedRatingGuideAnswers
289
          : {
290
              ...state.editedRatingGuideAnswers,
291
              [action.payload.ratingGuideAnswer.id]: {
292
                ...state.tempRatingGuideAnswers[
293
                  action.payload.oldRatingGuideAnswer.id
294
                ],
295
                id: action.payload.ratingGuideAnswer.id,
296
              },
297
            },
298
        tempRatingGuideAnswers: deleteProperty(
299
          state.tempRatingGuideAnswers,
300
          action.payload.oldRatingGuideAnswer.id,
301
        ),
302
      };
303
    case STORE_NEW_RATING_GUIDE_ANSWER_FAILED:
304
      return {
305
        ...state,
306
        tempRatingGuideAnswerSaving: {
307
          ...state.tempRatingGuideAnswerSaving,
308
          [action.meta.id]: false,
309
        },
310
      };
311
    default:
312
      return state;
313
  }
314
};
315
316
export default ratingGuideAnswerReducer;
317