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