Skip to content

Commit

Permalink
Port Project to use redux-toolkit (#2754)
Browse files Browse the repository at this point in the history
  • Loading branch information
imnasnainaec authored Nov 13, 2023
1 parent 2df7dbb commit c7aaa71
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 140 deletions.
47 changes: 26 additions & 21 deletions src/components/Project/ProjectActions.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,35 @@
import { Action, PayloadAction } from "@reduxjs/toolkit";

import { Project, User } from "api/models";
import { getAllProjectUsers, updateProject } from "backend";
import { setProjectId } from "backend/localStorage";
import {
ProjectAction,
ProjectActionType,
} from "components/Project/ProjectReduxTypes";
resetAction,
setProjectAction,
setUsersAction,
} from "components/Project/ProjectReducer";
import { StoreStateDispatch } from "types/Redux/actions";
import { newProject } from "types/project";

export function setCurrentProject(payload?: Project): ProjectAction {
return {
type: ProjectActionType.SET_CURRENT_PROJECT,
payload,
};
// Action Creation Functions

export function resetCurrentProject(): Action {
return resetAction();
}

export function setCurrentProjectUsers(payload?: User[]): ProjectAction {
return {
type: ProjectActionType.SET_CURRENT_PROJECT_USERS,
payload,
};
export function setCurrentProject(project?: Project): PayloadAction {
return setProjectAction(project ?? newProject());
}

export function clearCurrentProject() {
return (dispatch: StoreStateDispatch) => {
setProjectId();
dispatch(setCurrentProject());
dispatch(setCurrentProjectUsers());
export function setCurrentProjectUsers(users?: User[]): PayloadAction {
return setUsersAction(users ?? []);
}

// Dispatch Functions

export function asyncRefreshProjectUsers(projectId: string) {
return async (dispatch: StoreStateDispatch) => {
dispatch(setCurrentProjectUsers(await getAllProjectUsers(projectId)));
};
}

Expand All @@ -36,9 +40,10 @@ export function asyncUpdateCurrentProject(project: Project) {
};
}

export function asyncRefreshProjectUsers(projectId: string) {
return async (dispatch: StoreStateDispatch) => {
dispatch(setCurrentProjectUsers(await getAllProjectUsers(projectId)));
export function clearCurrentProject() {
return (dispatch: StoreStateDispatch) => {
setProjectId();
dispatch(resetCurrentProject());
};
}

Expand Down
51 changes: 26 additions & 25 deletions src/components/Project/ProjectReducer.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
import {
CurrentProjectState,
defaultState,
ProjectAction,
ProjectActionType,
} from "components/Project/ProjectReduxTypes";
import { StoreAction, StoreActionTypes } from "rootActions";
import { newProject } from "types/project";
import { createSlice } from "@reduxjs/toolkit";

export const projectReducer = (
state = defaultState,
action: ProjectAction | StoreAction
): CurrentProjectState => {
switch (action.type) {
case ProjectActionType.SET_CURRENT_PROJECT:
if (action.payload?.id === state.project.id) {
return { ...state, project: action.payload };
import { defaultState } from "components/Project/ProjectReduxTypes";
import { StoreActionTypes } from "rootActions";

const projectSlice = createSlice({
name: "currentProjectState",
initialState: defaultState,
reducers: {
resetAction: () => defaultState,
setProjectAction: (state, action) => {
if (state.project.id !== action.payload.id) {
state.users = [];
}
return { project: action.payload ?? newProject(), users: [] };
case ProjectActionType.SET_CURRENT_PROJECT_USERS:
return { ...state, users: action.payload ?? [] };
case StoreActionTypes.RESET:
return defaultState;
default:
return state;
}
};
state.project = action.payload;
},
setUsersAction: (state, action) => {
state.users = action.payload;
},
},
extraReducers: (builder) =>
builder.addCase(StoreActionTypes.RESET, () => defaultState),
});

export const { resetAction, setProjectAction, setUsersAction } =
projectSlice.actions;

export default projectSlice.reducer;
19 changes: 0 additions & 19 deletions src/components/Project/ProjectReduxTypes.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,6 @@
import { Project, User } from "api/models";
import { newProject } from "types/project";

export enum ProjectActionType {
SET_CURRENT_PROJECT = "SET_CURRENT_PROJECT",
SET_CURRENT_PROJECT_USERS = "SET_CURRENT_PROJECT_USERS",
}

export interface SetCurrentProjectAction {
type: ProjectActionType.SET_CURRENT_PROJECT;
payload?: Project;
}

export interface SetCurrentProjectUsersAction {
type: ProjectActionType.SET_CURRENT_PROJECT_USERS;
payload?: User[];
}

export type ProjectAction =
| SetCurrentProjectAction
| SetCurrentProjectUsersAction;

export interface CurrentProjectState {
project: Project;
users: User[];
Expand Down
104 changes: 104 additions & 0 deletions src/components/Project/tests/ProjectActions.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { PreloadedState } from "redux";

import { Project } from "api/models";
import { defaultState } from "components/App/DefaultState";
import {
asyncRefreshProjectUsers,
asyncUpdateCurrentProject,
clearCurrentProject,
setNewCurrentProject,
} from "components/Project/ProjectActions";
import { RootState, setupStore } from "store";
import { newProject } from "types/project";
import { newUser } from "types/user";

jest.mock("backend", () => ({
getAllProjectUsers: (...args: any[]) => mockGetAllProjectUsers(...args),
updateProject: (...args: any[]) => mockUpdateProject(...args),
}));

const mockGetAllProjectUsers = jest.fn();
const mockUpdateProject = jest.fn();
const mockProjId = "project-id";

// Preloaded values for store when testing
const persistedDefaultState: PreloadedState<RootState> = {
...defaultState,
_persist: { version: 1, rehydrated: false },
};

describe("ProjectActions", () => {
describe("asyncUpdateCurrentProject", () => {
it("updates the backend and correctly affects state for different id", async () => {
const proj: Project = { ...newProject(), id: mockProjId };
const store = setupStore({
...persistedDefaultState,
currentProjectState: { project: proj, users: [newUser()] },
});
const id = "new-id";
await store.dispatch(asyncUpdateCurrentProject({ ...proj, id }));
expect(mockUpdateProject).toBeCalledTimes(1);
const { project, users } = store.getState().currentProjectState;
expect(project.id).toEqual(id);
expect(users).toHaveLength(0);
});

it("updates the backend and correctly affects state for same id", async () => {
const proj: Project = { ...newProject(), id: mockProjId };
const store = setupStore({
...persistedDefaultState,
currentProjectState: { project: proj, users: [newUser()] },
});
const name = "new-name";
await store.dispatch(asyncUpdateCurrentProject({ ...proj, name }));
expect(mockUpdateProject).toBeCalledTimes(1);
const { project, users } = store.getState().currentProjectState;
expect(project.name).toEqual(name);
expect(users).toHaveLength(1);
});
});

describe("asyncRefreshProjectUsers", () => {
it("correctly affects state", async () => {
const proj: Project = { ...newProject(), id: mockProjId };
const store = setupStore({
...persistedDefaultState,
currentProjectState: { project: proj, users: [] },
});
const mockUsers = [newUser(), newUser(), newUser()];
mockGetAllProjectUsers.mockResolvedValueOnce(mockUsers);
await store.dispatch(asyncRefreshProjectUsers("mockProjId"));
const { project, users } = store.getState().currentProjectState;
expect(project.id).toEqual(mockProjId);
expect(users).toHaveLength(mockUsers.length);
});
});

describe("clearCurrentProject", () => {
it("correctly affects state", () => {
const nonDefaultState = {
project: { ...newProject(), id: "nonempty-string" },
users: [newUser()],
};
const store = setupStore({
...persistedDefaultState,
currentProjectState: nonDefaultState,
});
store.dispatch(clearCurrentProject());
const { project, users } = store.getState().currentProjectState;
expect(project.id).toEqual("");
expect(users).toHaveLength(0);
});
});

describe("setNewCurrentProject", () => {
it("correctly affects state and doesn't update the backend", () => {
const proj: Project = { ...newProject(), id: mockProjId };
const store = setupStore();
store.dispatch(setNewCurrentProject(proj));
expect(mockUpdateProject).not.toBeCalled();
const { project } = store.getState().currentProjectState;
expect(project.id).toEqual(mockProjId);
});
});
});
74 changes: 0 additions & 74 deletions src/components/Project/tests/ProjectReducer.test.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion src/rootReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { combineReducers, Reducer } from "redux";

import goalsReducer from "components/GoalTimeline/Redux/GoalReducer";
import loginReducer from "components/Login/Redux/LoginReducer";
import { projectReducer } from "components/Project/ProjectReducer";
import projectReducer from "components/Project/ProjectReducer";
import exportProjectReducer from "components/ProjectExport/Redux/ExportProjectReducer";
import pronunciationsReducer from "components/Pronunciations/Redux/PronunciationsReducer";
import treeViewReducer from "components/TreeView/Redux/TreeViewReducer";
Expand Down

0 comments on commit c7aaa71

Please sign in to comment.