Passed
Push — task/copy-profile-data-to-new-... ( 2f8460...653b18 )
by Grant
04:05
created

experienceReducer.ts ➔ isAward   A

Complexity

Conditions 1

Size

Total Lines 2
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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