Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Port Project to use redux-toolkit #2754

Merged
merged 4 commits into from
Nov 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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