Passed
Push — task/ci-browser-test-actions ( 7c3083...4796e2 )
by
unknown
03:54
created

resources/assets/js/store/Assessment/assessmentReducer.ts   A

Complexity

Total Complexity 6
Complexity/F 6

Size

Lines of Code 309
Function Count 1

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

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