Passed
Push — feature/redux-experience-skill ( 70f135 )
by Tristan
06:23
created

experienceReducer.ts ➔ setExperienceSkills   B

Complexity

Conditions 3

Size

Total Lines 50
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 38
dl 0
loc 50
rs 8.968
c 0
b 0
f 0
cc 3
1
import { combineReducers } from "redux";
2
import {
3
  ExperienceWork,
4
  ExperienceEducation,
5
  ExperienceCommunity,
6
  ExperienceAward,
7
  ExperiencePersonal,
8
  Experience,
9
  ExperienceSkill,
10
} from "../../models/types";
11
import {
12
  ExperienceAction,
13
  FETCH_EXPERIENCE_BY_APPLICANT_SUCCEEDED,
14
  FETCH_EXPERIENCE_BY_APPLICATION_SUCCEEDED,
15
  CREATE_EXPERIENCE_SUCCEEDED,
16
  FETCH_EXPERIENCE_BY_APPLICANT_STARTED,
17
  FETCH_EXPERIENCE_BY_APPLICANT_FAILED,
18
  FETCH_EXPERIENCE_BY_APPLICATION_STARTED,
19
  FETCH_EXPERIENCE_BY_APPLICATION_FAILED,
20
  UPDATE_EXPERIENCE_STARTED,
21
  UPDATE_EXPERIENCE_SUCCEEDED,
22
  UPDATE_EXPERIENCE_FAILED,
23
  DELETE_EXPERIENCE_STARTED,
24
  DELETE_EXPERIENCE_SUCCEEDED,
25
  DELETE_EXPERIENCE_FAILED,
26
  CREATE_EXPERIENCE_SKILL_SUCCEEDED,
27
  UPDATE_EXPERIENCE_SKILL_SUCCEEDED,
28
  DELETE_EXPERIENCE_SKILL_SUCCEEDED,
29
  UPDATE_EXPERIENCE_SKILL_STARTED,
30
  DELETE_EXPERIENCE_SKILL_STARTED,
31
  UPDATE_EXPERIENCE_SKILL_FAILED,
32
  DELETE_EXPERIENCE_SKILL_FAILED,
33
} from "./experienceActions";
34
import {
35
  mapToObject,
36
  getId,
37
  uniq,
38
  deleteProperty,
39
  mapObjectValues,
40
} from "../../helpers/queries";
41
42
export interface ExperienceSection<T> {
43
  byId: {
44
    [id: number]: T;
45
  };
46
  idsByApplicant: {
47
    [applicantId: number]: number[];
48
  };
49
  idsByApplication: {
50
    [applicationId: number]: number[];
51
  };
52
}
53
54
export interface EntityState {
55
  work: ExperienceSection<ExperienceWork>;
56
  education: ExperienceSection<ExperienceEducation>;
57
  community: ExperienceSection<ExperienceCommunity>;
58
  award: ExperienceSection<ExperienceAward>;
59
  personal: ExperienceSection<ExperiencePersonal>;
60
  experienceSkills: {
61
    byId: { [id: number]: ExperienceSkill };
62
    idsByWork: { [workId: number]: number[] };
63
    idsByEducation: { [educationId: number]: number[] };
64
    idsByCommunity: { [communityId: number]: number[] };
65
    idsByAward: { [awardId: number]: number[] };
66
    idsByPersonal: { [personalId: number]: number[] };
67
  };
68
}
69
70
export interface UiState {
71
  updatingByApplicant: {
72
    [id: number]: boolean;
73
  };
74
  updatingByApplication: {
75
    [id: number]: boolean;
76
  };
77
  updatingByTypeAndId: {
78
    work: {
79
      [id: number]: boolean;
80
    };
81
    education: {
82
      [id: number]: boolean;
83
    };
84
    community: {
85
      [id: number]: boolean;
86
    };
87
    award: {
88
      [id: number]: boolean;
89
    };
90
    personal: {
91
      [id: number]: boolean;
92
    };
93
  };
94
  updatingExperienceSkill: {
95
    [id: number]: boolean;
96
  };
97
}
98
99
export interface ExperienceState {
100
  entities: EntityState;
101
  ui: UiState;
102
}
103
104
export const initEntities = (): EntityState => ({
105
  work: {
106
    byId: {},
107
    idsByApplicant: {},
108
    idsByApplication: {},
109
  },
110
  education: {
111
    byId: {},
112
    idsByApplicant: {},
113
    idsByApplication: {},
114
  },
115
  community: {
116
    byId: {},
117
    idsByApplicant: {},
118
    idsByApplication: {},
119
  },
120
  award: {
121
    byId: {},
122
    idsByApplicant: {},
123
    idsByApplication: {},
124
  },
125
  personal: {
126
    byId: {},
127
    idsByApplicant: {},
128
    idsByApplication: {},
129
  },
130
  experienceSkills: {
131
    byId: {},
132
    idsByWork: {},
133
    idsByEducation: {},
134
    idsByCommunity: {},
135
    idsByAward: {},
136
    idsByPersonal: {},
137
  },
138
});
139
140
export const initUi = (): UiState => ({
141
  updatingByApplicant: {},
142
  updatingByApplication: {},
143
  updatingByTypeAndId: {
144
    work: {},
145
    education: {},
146
    community: {},
147
    award: {},
148
    personal: {},
149
  },
150
  updatingExperienceSkill: {},
151
});
152
153
export const initExperienceState = (): ExperienceState => ({
154
  entities: initEntities(),
155
  ui: initUi(),
156
});
157
158
function isWork(experience: Experience): experience is ExperienceWork {
159
  return experience.type === "experience_work";
160
}
161
function isEducation(
162
  experience: Experience,
163
): experience is ExperienceEducation {
164
  return experience.type === "experience_education";
165
}
166
function isCommunity(
167
  experience: Experience,
168
): experience is ExperienceCommunity {
169
  return experience.type === "experience_community";
170
}
171
function isAward(experience: Experience): experience is ExperienceAward {
172
  return experience.type === "experience_award";
173
}
174
function isPersonal(experience: Experience): experience is ExperiencePersonal {
175
  return experience.type === "experience_personal";
176
}
177
178
type EntityType = "work" | "education" | "community" | "award" | "personal";
179
180
const experienceTypeGuards = {
181
  work: isWork,
182
  education: isEducation,
183
  community: isCommunity,
184
  award: isAward,
185
  personal: isPersonal,
186
};
187
188
function massageType(experienceType: Experience["type"]): EntityType {
189
  /* eslint-disable @typescript-eslint/camelcase */
190
  const mapping: { [key in Experience["type"]]: EntityType } = {
191
    experience_work: "work",
192
    experience_education: "education",
193
    experience_community: "community",
194
    experience_award: "award",
195
    experience_personal: "personal",
196
  };
197
  /* eslint-enable @typescript-eslint/camelcase */
198
  return mapping[experienceType];
199
}
200
201
function fetchExperienceByApplication<T extends EntityType>(
202
  state: EntityState,
203
  action: ExperienceAction,
204
  type: T,
205
): EntityState[T] {
206
  const subState = state[type];
207
  if (action.type !== FETCH_EXPERIENCE_BY_APPLICATION_SUCCEEDED) {
208
    return subState;
209
  }
210
  const typeFilter = experienceTypeGuards[type];
211
  const experiences = action.payload
212
    .map((response) => response.experience)
213
    .filter(typeFilter);
214
  return {
215
    ...subState,
216
    byId: {
217
      ...subState.byId,
218
      ...mapToObject(experiences, getId),
219
    },
220
    idsByApplication: {
221
      ...subState.idsByApplicant,
222
      [action.meta.applicationId]: experiences.map(getId),
223
    },
224
  };
225
}
226
function fetchExperienceByApplicant<T extends EntityType>(
227
  state: EntityState,
228
  action: ExperienceAction,
229
  type: T,
230
): EntityState[T] {
231
  const subState = state[type];
232
  if (action.type !== FETCH_EXPERIENCE_BY_APPLICANT_SUCCEEDED) {
233
    return subState;
234
  }
235
  const typeFilter = experienceTypeGuards[type];
236
  const experiences = action.payload
237
    .map((response) => response.experience)
238
    .filter(typeFilter);
239
  return {
240
    ...subState,
241
    byId: {
242
      ...subState.byId,
243
      ...mapToObject(experiences, getId),
244
    },
245
    idsByApplicant: {
246
      ...subState.idsByApplicant,
247
      [action.meta.applicantId]: experiences.map(getId),
248
    },
249
  };
250
}
251
252
function setExperience<T extends EntityType>(
253
  state: EntityState,
254
  action: ExperienceAction,
255
  type: T,
256
): EntityState[T] {
257
  const subState = state[type];
258
  if (
259
    (action.type !== CREATE_EXPERIENCE_SUCCEEDED &&
260
      action.type !== UPDATE_EXPERIENCE_SUCCEEDED) ||
261
    massageType(action.meta.type) !== type
262
  ) {
263
    return subState;
264
  }
265
  const { experience } = action.payload;
266
  const ownerId = experience.experienceable_id;
267
  const idsByApplicant =
268
    experience.experienceable_type === "applicant"
269
      ? {
270
          ...subState.idsByApplicant,
271
          [ownerId]: uniq([
272
            ...(subState.idsByApplicant[ownerId] ?? []),
273
            experience.id,
274
          ]),
275
        }
276
      : subState.idsByApplicant;
277
  const idsByApplication =
278
    experience.experienceable_type === "application"
279
      ? {
280
          ...subState.idsByApplication,
281
          [ownerId]: uniq([
282
            ...(subState.idsByApplication[ownerId] ?? []),
283
            experience.id,
284
          ]),
285
        }
286
      : subState.idsByApplication;
287
288
  return {
289
    ...subState,
290
    byId: {
291
      ...subState.byId,
292
      [action.payload.experience.id]: action.payload,
293
    },
294
    idsByApplicant,
295
    idsByApplication,
296
  };
297
}
298
299
function deleteExperience<T extends EntityType>(
300
  state: EntityState,
301
  action: ExperienceAction,
302
  type: T,
303
): EntityState[T] {
304
  const subState = state[type];
305
  if (
306
    action.type !== DELETE_EXPERIENCE_SUCCEEDED ||
307
    massageType(action.meta.type) !== type
308
  ) {
309
    return subState;
310
  }
311
  const dropId = (ids: number[]): number[] =>
312
    ids.filter((id) => id !== action.meta.id);
313
  return {
314
    ...subState,
315
    byId: deleteProperty(subState.byId, action.meta.id),
316
    idsByApplicant: mapObjectValues(subState.idsByApplicant, dropId),
317
    idsByApplication: mapObjectValues(subState.idsByApplication, dropId),
318
  };
319
}
320
321
function setExperienceSkills(
322
  state: EntityState,
323
  experienceSkills: ExperienceSkill[],
324
): EntityState["experienceSkills"] {
325
  const newExpSkills = mapToObject(experienceSkills, getId);
326
  const workSkills = experienceSkills.filter(
327
    (expSkill) => expSkill.experience_type === "experience_work",
328
  );
329
  const educationSkills = experienceSkills.filter(
330
    (expSkill) => expSkill.experience_type === "experience_education",
331
  );
332
  const communitySkills = experienceSkills.filter(
333
    (expSkill) => expSkill.experience_type === "experience_community",
334
  );
335
  const awardSkills = experienceSkills.filter(
336
    (expSkill) => expSkill.experience_type === "experience_award",
337
  );
338
  const personalSkills = experienceSkills.filter(
339
    (expSkill) => expSkill.experience_type === "experience_personal",
340
  );
341
342
  interface ExpToSkillIds {
343
    [expId: number]: number[];
344
  }
345
  const reducer = (
346
    acc: ExpToSkillIds,
347
    expSkill: ExperienceSkill,
348
  ): ExpToSkillIds => {
349
    const prevIds = acc[expSkill.experience_id] ?? [];
350
    return {
351
      ...acc,
352
      [expSkill.experience_id]: [expSkill.id, ...prevIds],
353
    };
354
  };
355
  return {
356
    byId: { ...state.experienceSkills, ...newExpSkills },
357
    idsByWork: workSkills.reduce(reducer, state.experienceSkills.idsByWork),
358
    idsByEducation: educationSkills.reduce(
359
      reducer,
360
      state.experienceSkills.idsByEducation,
361
    ),
362
    idsByCommunity: communitySkills.reduce(
363
      reducer,
364
      state.experienceSkills.idsByCommunity,
365
    ),
366
    idsByAward: awardSkills.reduce(reducer, state.experienceSkills.idsByAward),
367
    idsByPersonal: personalSkills.reduce(
368
      reducer,
369
      state.experienceSkills.idsByPersonal,
370
    ),
371
  };
372
}
373
374
/* eslint-disable @typescript-eslint/camelcase */
375
const experienceSkillKeys = {
376
  experience_work: "idsByWork",
377
  experience_education: "idsByEducation",
378
  experience_community: "idsByCommunity",
379
  experience_award: "idsByAward",
380
  experience_personal: "idsByPersonal",
381
};
382
/* eslint-enable @typescript-eslint/camelcase */
383
384
function deleteExpSkillsForExperience(
385
  state: EntityState,
386
  experienceId: number,
387
  experienceType: Experience["type"],
388
): EntityState["experienceSkills"] {
389
  const experienceKey = experienceSkillKeys[experienceType];
390
  const expSkillIds: number[] =
391
    state.experienceSkills[experienceKey][experienceId] ?? [];
392
  return {
393
    ...state.experienceSkills,
394
    [experienceKey]: deleteProperty(
395
      state.experienceSkills[experienceKey],
396
      experienceId,
397
    ),
398
    byId: expSkillIds.reduce(
399
      (byId, deleteId) => deleteProperty(byId, deleteId),
400
      state.experienceSkills.byId,
401
    ),
402
  };
403
}
404
405
function deleteExperienceSkill(
406
  state: EntityState,
407
  experienceSkillId: number,
408
  experienceId: number,
409
  experienceType: ExperienceSkill["experience_type"],
410
) {
411
  const experienceKey = experienceSkillKeys[experienceType];
412
  return {
413
    ...state.experienceSkills,
414
    [experienceKey]: {
415
      ...state.experienceSkills[experienceKey],
416
      [experienceId]: state.experienceSkills[experienceKey][
417
        experienceId
418
      ].filter((id) => id !== experienceSkillId),
419
    },
420
    byId: deleteProperty(state.experienceSkills.byId, experienceSkillId),
421
  };
422
}
423
424
export const entitiesReducer = (
425
  state = initEntities(),
426
  action: ExperienceAction,
427
): EntityState => {
428
  switch (action.type) {
429
    case FETCH_EXPERIENCE_BY_APPLICANT_SUCCEEDED:
430
      return {
431
        ...state,
432
        work: fetchExperienceByApplicant(state, action, "work"),
433
        education: fetchExperienceByApplicant(state, action, "education"),
434
        community: fetchExperienceByApplicant(state, action, "community"),
435
        award: fetchExperienceByApplicant(state, action, "award"),
436
        personal: fetchExperienceByApplicant(state, action, "personal"),
437
        experienceSkills: setExperienceSkills(
438
          state,
439
          action.payload.map((response) => response.experienceSkills).flat(),
440
        ),
441
      };
442
    case FETCH_EXPERIENCE_BY_APPLICATION_SUCCEEDED:
443
      return {
444
        ...state,
445
        work: fetchExperienceByApplication(state, action, "work"),
446
        education: fetchExperienceByApplication(state, action, "education"),
447
        community: fetchExperienceByApplication(state, action, "community"),
448
        award: fetchExperienceByApplication(state, action, "award"),
449
        personal: fetchExperienceByApplication(state, action, "personal"),
450
        experienceSkills: setExperienceSkills(
451
          state,
452
          action.payload.map((response) => response.experienceSkills).flat(),
453
        ),
454
      };
455
    case CREATE_EXPERIENCE_SUCCEEDED:
456
    case UPDATE_EXPERIENCE_SUCCEEDED:
457
      return {
458
        ...state,
459
        [massageType(action.meta.type)]: setExperience(
460
          state,
461
          action,
462
          massageType(action.meta.type),
463
        ),
464
      };
465
    case DELETE_EXPERIENCE_SUCCEEDED:
466
      return {
467
        ...state,
468
        [massageType(action.meta.type)]: deleteExperience(
469
          state,
470
          action,
471
          massageType(action.meta.type),
472
        ),
473
        experienceSkills: deleteExpSkillsForExperience(
474
          state,
475
          action.meta.id,
476
          action.meta.type,
477
        ),
478
      };
479
    case CREATE_EXPERIENCE_SKILL_SUCCEEDED:
480
    case UPDATE_EXPERIENCE_SKILL_SUCCEEDED:
481
      return {
482
        ...state,
483
        experienceSkills: setExperienceSkills(state, [action.payload]),
484
      };
485
    case DELETE_EXPERIENCE_SKILL_SUCCEEDED:
486
      return {
487
        ...state,
488
        experienceSkills: deleteExperienceSkill(
489
          state,
490
          action.meta.id,
491
          action.meta.experienceId,
492
          action.meta.experienceType,
493
        ),
494
      };
495
    default:
496
      return state;
497
  }
498
};
499
500
export const uiReducer = (
501
  state = initUi(),
502
  action: ExperienceAction,
503
): UiState => {
504
  switch (action.type) {
505
    case FETCH_EXPERIENCE_BY_APPLICANT_STARTED:
506
      return {
507
        ...state,
508
        updatingByApplicant: {
509
          ...state.updatingByApplicant,
510
          [action.meta.applicantId]: true,
511
        },
512
      };
513
    case FETCH_EXPERIENCE_BY_APPLICANT_SUCCEEDED:
514
    case FETCH_EXPERIENCE_BY_APPLICANT_FAILED:
515
      return {
516
        ...state,
517
        updatingByApplicant: {
518
          ...state.updatingByApplicant,
519
          [action.meta.applicantId]: false,
520
        },
521
      };
522
    case FETCH_EXPERIENCE_BY_APPLICATION_STARTED:
523
      return {
524
        ...state,
525
        updatingByApplication: {
526
          ...state.updatingByApplication,
527
          [action.meta.applicationId]: true,
528
        },
529
      };
530
    case FETCH_EXPERIENCE_BY_APPLICATION_SUCCEEDED:
531
    case FETCH_EXPERIENCE_BY_APPLICATION_FAILED:
532
      return {
533
        ...state,
534
        updatingByApplication: {
535
          ...state.updatingByApplication,
536
          [action.meta.applicationId]: false,
537
        },
538
      };
539
    case UPDATE_EXPERIENCE_STARTED:
540
    case DELETE_EXPERIENCE_STARTED:
541
      return {
542
        ...state,
543
        updatingByTypeAndId: {
544
          ...state.updatingByTypeAndId,
545
          [massageType(action.meta.type)]: {
546
            ...state.updatingByTypeAndId[massageType(action.meta.type)],
547
            [action.meta.id]: true,
548
          },
549
        },
550
      };
551
    case UPDATE_EXPERIENCE_SUCCEEDED:
552
    case DELETE_EXPERIENCE_SUCCEEDED:
553
    case UPDATE_EXPERIENCE_FAILED:
554
    case DELETE_EXPERIENCE_FAILED:
555
      return {
556
        ...state,
557
        updatingByTypeAndId: {
558
          ...state.updatingByTypeAndId,
559
          [massageType(action.meta.type)]: {
560
            ...state.updatingByTypeAndId[massageType(action.meta.type)],
561
            [action.meta.id]: false,
562
          },
563
        },
564
      };
565
    case UPDATE_EXPERIENCE_SKILL_STARTED:
566
    case DELETE_EXPERIENCE_SKILL_STARTED:
567
      return {
568
        ...state,
569
        updatingExperienceSkill: {
570
          ...state.updatingExperienceSkill,
571
          [action.meta.id]: true,
572
        },
573
      };
574
    case UPDATE_EXPERIENCE_SKILL_SUCCEEDED:
575
    case UPDATE_EXPERIENCE_SKILL_FAILED:
576
    case DELETE_EXPERIENCE_SKILL_SUCCEEDED:
577
    case DELETE_EXPERIENCE_SKILL_FAILED:
578
      return {
579
        ...state,
580
        updatingExperienceSkill: {
581
          ...state.updatingExperienceSkill,
582
          [action.meta.id]: false,
583
        },
584
      };
585
    default:
586
      return state;
587
  }
588
};
589
590
export const experienceReducer = combineReducers({
591
  entities: entitiesReducer,
592
  ui: uiReducer,
593
});
594
595
export default experienceReducer;
596