Passed
Push — dev ( dabde6...94b2f2 )
by Tristan
06:43 queued 19s
created

resources/assets/js/store/RatingGuideQuestion/ratingGuideQuestionReducer.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
import isEqual from "lodash/isEqual";
2
import { RatingGuideQuestion } 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
  RatingGuideQuestionAction,
11
  CREATE_TEMP_RATING_GUIDE_QUESTION,
12
  DELETE_RATING_GUIDE_QUESTION_FAILED,
13
  DELETE_RATING_GUIDE_QUESTION_STARTED,
14
  DELETE_RATING_GUIDE_QUESTION_SUCCEEDED,
15
  DELETE_TEMP_RATING_GUIDE_QUESTION,
16
  EDIT_RATING_GUIDE_QUESTION,
17
  EDIT_TEMP_RATING_GUIDE_QUESTION,
18
  STORE_NEW_RATING_GUIDE_QUESTION_FAILED,
19
  STORE_NEW_RATING_GUIDE_QUESTION_STARTED,
20
  STORE_NEW_RATING_GUIDE_QUESTION_SUCCEEDED,
21
  UPDATE_RATING_GUIDE_QUESTION_FAILED,
22
  UPDATE_RATING_GUIDE_QUESTION_STARTED,
23
  UPDATE_RATING_GUIDE_QUESTION_SUCCEEDED,
24
} from "./ratingGuideQuestionActions";
25
import {
26
  getId,
27
  mapToObject,
28
  deleteProperty,
29
  hasKey,
30
} from "../../helpers/queries";
31
32
export interface RatingGuideQuestionState {
33
  ratingGuideQuestions: {
34
    // Stores rating guide answers that are synced with the server
35
    [id: number]: RatingGuideQuestion;
36
  };
37
  editedRatingGuideQuestions: {
38
    // For storing rating guide answers that have been edited locally
39
    [id: number]: RatingGuideQuestion;
40
  };
41
  tempRatingGuideQuestions: {
42
    // For storing local rating guide answers that have never been saved to server
43
    [id: number]: RatingGuideQuestion;
44
  };
45
  tempRatingGuideQuestionSaving: {
46
    // Tracks whether a RatingGuideQuestion is currently being saved to server
47
    [id: number]: boolean;
48
  };
49
  ratingGuideQuestionUpdates: {
50
    // Tracks the number of pending updates
51
    [id: number]: number;
52
  };
53
  ratingGuideQuestionDeletes: {
54
    // Tracks the number of pending delete requests
55
    [id: number]: number;
56
  };
57
}
58
59
export const initState = (): RatingGuideQuestionState => ({
60
  ratingGuideQuestions: {},
61
  editedRatingGuideQuestions: {},
62
  tempRatingGuideQuestions: {},
63
  tempRatingGuideQuestionSaving: {},
64
  ratingGuideQuestionUpdates: {},
65
  ratingGuideQuestionDeletes: {},
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
  editedRatingGuideQuestions: { [id: number]: RatingGuideQuestion },
75
  ratingGuideQuestion: RatingGuideQuestion,
76
): { [id: number]: RatingGuideQuestion } => {
77
  const { id } = ratingGuideQuestion;
78
  if (
79
    hasKey(editedRatingGuideQuestions, id) &&
80
    isEqual(editedRatingGuideQuestions[id], ratingGuideQuestion)
81
  ) {
82
    return deleteProperty(editedRatingGuideQuestions, id);
83
  }
84
  return editedRatingGuideQuestions;
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 addTempRatingGuideQuestion = (
118
  state: RatingGuideQuestionState,
119
  jobPosterId: number,
120
  assessmentTypeId: number,
121
  newQuestion: string | null,
122
): RatingGuideQuestionState => {
123
  const currentIds = Object.values(state.tempRatingGuideQuestions).map(getId);
124
  const newId = Math.max(...currentIds, 0) + 1;
125
  return {
126
    ...state,
127
    tempRatingGuideQuestions: {
128
      ...state.tempRatingGuideQuestions,
129
      [newId]: {
130
        id: newId,
131
        job_poster_id: jobPosterId,
132
        assessment_type_id: assessmentTypeId,
133
        question: newQuestion,
134
      },
135
    },
136
  };
137
};
138
139
export const ratingGuideQuestionReducer = (
140
  state = initState(),
141
  action: AssessmentPlanAction | RatingGuideQuestionAction,
142
): RatingGuideQuestionState => {
143
  switch (action.type) {
144
    case FETCH_ASSESSMENT_PLAN_STARTED:
145
      return state;
146
    case FETCH_ASSESSMENT_PLAN_SUCCEEEDED:
147
      return {
148
        ...state,
149
        ratingGuideQuestions: {
150
          ...state.ratingGuideQuestions,
151
          ...mapToObject(action.payload.ratingGuideQuestions, getId),
152
        },
153
      };
154
    case FETCH_ASSESSMENT_PLAN_FAILED:
155
      return state;
156
    case EDIT_RATING_GUIDE_QUESTION:
157
      return {
158
        ...state,
159
        editedRatingGuideQuestions: {
160
          ...state.editedRatingGuideQuestions,
161
          [action.payload.ratingGuideQuestion.id]:
162
            action.payload.ratingGuideQuestion,
163
        },
164
      };
165
    case UPDATE_RATING_GUIDE_QUESTION_STARTED:
166
      return {
167
        ...state,
168
        ratingGuideQuestionUpdates: incrementUpdates(
169
          state.ratingGuideQuestionUpdates,
170
          action.payload.ratingGuideQuestion.id,
171
        ),
172
      };
173
    case UPDATE_RATING_GUIDE_QUESTION_SUCCEEDED:
174
      return {
175
        ...state,
176
        ratingGuideQuestions: {
177
          ...state.ratingGuideQuestions,
178
          [action.payload.ratingGuideQuestion.id]:
179
            action.payload.ratingGuideQuestion,
180
        },
181
        editedRatingGuideQuestions: deleteEditedIfIdentical(
182
          state.editedRatingGuideQuestions,
183
          action.payload.ratingGuideQuestion,
184
        ),
185
        ratingGuideQuestionUpdates: decrementUpdates(
186
          state.ratingGuideQuestionUpdates,
187
          action.payload.ratingGuideQuestion.id,
188
        ),
189
      };
190
    case UPDATE_RATING_GUIDE_QUESTION_FAILED:
191
      // TODO: do something with error
192
      // TODO: should the temp state really be deleted?
193
      return {
194
        ...state,
195
        editedRatingGuideQuestions: deleteEditedIfIdentical(
196
          state.editedRatingGuideQuestions,
197
          action.meta,
198
        ),
199
        ratingGuideQuestionUpdates: decrementUpdates(
200
          state.ratingGuideQuestionUpdates,
201
          action.meta.id,
202
        ),
203
      };
204
    case DELETE_RATING_GUIDE_QUESTION_STARTED:
205
      return {
206
        ...state,
207
        ratingGuideQuestionDeletes: incrementUpdates(
208
          state.ratingGuideQuestionDeletes,
209
          action.payload.id,
210
        ),
211
      };
212
    case DELETE_RATING_GUIDE_QUESTION_SUCCEEDED:
213
      // TODO: should this delete both canonical and edited ratingGuideQuestions?
214
      // ...For now, I don't know of any situations where we wouldn't want both.
215
      return {
216
        ...state,
217
        ratingGuideQuestions: deleteProperty(
218
          state.ratingGuideQuestions,
219
          action.payload.id,
220
        ),
221
        editedRatingGuideQuestions: deleteProperty(
222
          state.editedRatingGuideQuestions,
223
          action.payload.id,
224
        ),
225
        ratingGuideQuestionDeletes: decrementUpdates(
226
          state.ratingGuideQuestionDeletes,
227
          action.payload.id,
228
        ),
229
      };
230
    case DELETE_RATING_GUIDE_QUESTION_FAILED:
231
      return {
232
        ...state,
233
        ratingGuideQuestionDeletes: decrementUpdates(
234
          state.ratingGuideQuestionDeletes,
235
          action.meta.id,
236
        ),
237
      };
238
    case CREATE_TEMP_RATING_GUIDE_QUESTION:
239
      return addTempRatingGuideQuestion(
240
        state,
241
        action.payload.jobPosterId,
242
        action.payload.assessmentTypeId,
243
        action.payload.question,
244
      );
245
    case EDIT_TEMP_RATING_GUIDE_QUESTION:
246
      return {
247
        ...state,
248
        tempRatingGuideQuestions: {
249
          ...state.tempRatingGuideQuestions,
250
          [action.payload.ratingGuideQuestion.id]:
251
            action.payload.ratingGuideQuestion,
252
        },
253
      };
254
    case DELETE_TEMP_RATING_GUIDE_QUESTION:
255
      return {
256
        ...state,
257
        tempRatingGuideQuestions: deleteProperty(
258
          state.tempRatingGuideQuestions,
259
          action.payload.id,
260
        ),
261
      };
262
    case STORE_NEW_RATING_GUIDE_QUESTION_STARTED:
263
      return {
264
        ...state,
265
        tempRatingGuideQuestionSaving: {
266
          ...state.tempRatingGuideQuestionSaving,
267
          [action.payload.ratingGuideQuestion.id]: true,
268
        },
269
      };
270
    case STORE_NEW_RATING_GUIDE_QUESTION_SUCCEEDED:
271
      return {
272
        ...state,
273
        ratingGuideQuestions: {
274
          ...state.ratingGuideQuestions,
275
          [action.payload.ratingGuideQuestion.id]:
276
            action.payload.ratingGuideQuestion,
277
        },
278
        tempRatingGuideQuestionSaving: deleteProperty(
279
          state.tempRatingGuideQuestionSaving,
280
          action.payload.oldRatingGuideQuestion.id,
281
        ),
282
        // If temp ratingGuideQuestion differs from saved, move it to edited (with updated id)
283
        // If temp ratingGuideQuestion is equal to new saved, simply remove it from temp.
284
        editedRatingGuideQuestions: hasIdenticalItem(
285
          state.tempRatingGuideQuestions,
286
          action.payload.oldRatingGuideQuestion,
287
        )
288
          ? state.editedRatingGuideQuestions
289
          : {
290
              ...state.editedRatingGuideQuestions,
291
              [action.payload.ratingGuideQuestion.id]: {
292
                ...state.tempRatingGuideQuestions[
293
                  action.payload.oldRatingGuideQuestion.id
294
                ],
295
                id: action.payload.ratingGuideQuestion.id,
296
              },
297
            },
298
        tempRatingGuideQuestions: deleteProperty(
299
          state.tempRatingGuideQuestions,
300
          action.payload.oldRatingGuideQuestion.id,
301
        ),
302
      };
303
    case STORE_NEW_RATING_GUIDE_QUESTION_FAILED:
304
      return {
305
        ...state,
306
        tempRatingGuideQuestionSaving: {
307
          ...state.tempRatingGuideQuestionSaving,
308
          [action.meta.id]: false,
309
        },
310
      };
311
    default:
312
      return state;
313
  }
314
};
315
316
export default ratingGuideQuestionReducer;
317