Passed
Push — feature/application-translatio... ( 2578af...96b410 )
by Tristan
06:21
created

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

Complexity

Total Complexity 6
Complexity/F 6

Size

Lines of Code 318
Function Count 1

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

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

1 Function

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