Skip to content

Commit

Permalink
Port TreeView to use redux-toolkit (#2774)
Browse files Browse the repository at this point in the history
  • Loading branch information
imnasnainaec authored Nov 8, 2023
1 parent 6b800c0 commit 1c1db26
Show file tree
Hide file tree
Showing 10 changed files with 135 additions and 209 deletions.
11 changes: 4 additions & 7 deletions src/components/DataEntry/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,7 @@ import DataEntryTable from "components/DataEntry/DataEntryTable";
import ExistingDataTable from "components/DataEntry/ExistingDataTable";
import { filterWordsByDomain } from "components/DataEntry/utilities";
import TreeView from "components/TreeView";
import {
closeTreeAction,
openTreeAction,
} from "components/TreeView/Redux/TreeViewActions";
import { closeTree, openTree } from "components/TreeView/Redux/TreeViewActions";
import { StoreState } from "types";
import { useAppDispatch, useAppSelector } from "types/hooks";
import { newSemanticDomain } from "types/semanticDomain";
Expand Down Expand Up @@ -73,7 +70,7 @@ export default function DataEntry(): ReactElement {

// On first render, open tree.
useLayoutEffect(() => {
dispatch(openTreeAction());
dispatch(openTree());
}, [dispatch]);

// When window width changes, check if there's space for the sidebar.
Expand All @@ -99,7 +96,7 @@ export default function DataEntry(): ReactElement {
setDomainWords(
filterWordsByDomain(await getFrontierWords(), id, analysisLang)
);
dispatch(closeTreeAction());
dispatch(closeTree());
}, [analysisLang, dispatch, id]);

return (
Expand All @@ -118,7 +115,7 @@ export default function DataEntry(): ReactElement {
hasDrawerButton={isSmallScreen && domainWords.length > 0}
hideQuestions={() => setQuestionsVisible(false)}
isTreeOpen={open}
openTree={() => dispatch(openTreeAction())}
openTree={() => dispatch(openTree())}
semanticDomain={currentDomain}
showExistingData={() => setDrawerOpen(true)}
updateHeight={updateHeight}
Expand Down
11 changes: 4 additions & 7 deletions src/components/DataEntry/tests/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,8 @@ import DataEntry, {
treeViewDialogId,
} from "components/DataEntry";
import { defaultState as currentProjectState } from "components/Project/ProjectReduxTypes";
import { openTreeAction } from "components/TreeView/Redux/TreeViewActions";
import {
TreeViewAction,
TreeViewState,
} from "components/TreeView/Redux/TreeViewReduxTypes";
import { openTree } from "components/TreeView/Redux/TreeViewActions";
import { TreeViewState } from "components/TreeView/Redux/TreeViewReduxTypes";
import { newSemanticDomainTreeNode } from "types/semanticDomain";
import * as useWindowSize from "utilities/useWindowSize";

Expand All @@ -39,7 +36,7 @@ jest.mock("types/hooks", () => {
};
});

const mockDispatch = jest.fn((action: TreeViewAction) => action);
const mockDispatch = jest.fn((action: any) => action);
const mockDomain = newSemanticDomainTreeNode("mockId", "mockName", "mockLang");
const mockGetSemanticDomainFull = jest.fn();
const mockStore = createMockStore();
Expand Down Expand Up @@ -72,7 +69,7 @@ describe("DataEntry", () => {

it("dispatches to open the tree", async () => {
await renderDataEntry({ currentDomain: mockDomain });
expect(mockDispatch).toHaveBeenCalledWith(openTreeAction());
expect(mockDispatch).toHaveBeenCalledWith(openTree());
});

it("fetches domain", async () => {
Expand Down
4 changes: 2 additions & 2 deletions src/components/ProjectScreen/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ReactElement, useEffect } from "react";
import { clearCurrentProject } from "components/Project/ProjectActions";
import ChooseProject from "components/ProjectScreen/ChooseProject";
import CreateProject from "components/ProjectScreen/CreateProject";
import { resetTreeAction } from "components/TreeView/Redux/TreeViewActions";
import { resetTree } from "components/TreeView/Redux/TreeViewActions";
import { useAppDispatch } from "types/hooks";

/** Where users create a project or choose an existing one */
Expand All @@ -13,7 +13,7 @@ export default function ProjectScreen(): ReactElement {
/* Disable Data Entry, Data Cleanup, Project Settings until a project is selected or created. */
useEffect(() => {
dispatch(clearCurrentProject());
dispatch(resetTreeAction());
dispatch(resetTree());
}, [dispatch]);

return (
Expand Down
67 changes: 32 additions & 35 deletions src/components/TreeView/Redux/TreeViewActions.ts
Original file line number Diff line number Diff line change
@@ -1,60 +1,57 @@
import { Action, PayloadAction } from "@reduxjs/toolkit";

import { SemanticDomain, SemanticDomainTreeNode } from "api/models";
import { getSemanticDomainTreeNode } from "backend";
import {
TreeActionType,
TreeViewAction,
} from "components/TreeView/Redux/TreeViewReduxTypes";
resetTreeAction,
setCurrentDomainAction,
setDomainLanguageAction,
setTreeOpenAction,
} from "components/TreeView/Redux/TreeViewReducer";
import { StoreState } from "types";
import { StoreStateDispatch } from "types/Redux/actions";

export function closeTreeAction(): TreeViewAction {
return { type: TreeActionType.CLOSE_TREE };
// Action Creation Functions

export function closeTree(): PayloadAction {
return setTreeOpenAction(false);
}

export function openTreeAction(): TreeViewAction {
return { type: TreeActionType.OPEN_TREE };
export function openTree(): PayloadAction {
return setTreeOpenAction(true);
}

export function setDomainAction(
domain: SemanticDomainTreeNode
): TreeViewAction {
return { type: TreeActionType.SET_CURRENT_DOMAIN, domain };
export function resetTree(): Action {
return resetTreeAction();
}

export function setDomainLanguageAction(language: string): TreeViewAction {
return { type: TreeActionType.SET_DOMAIN_LANGUAGE, language };
export function setCurrentDomain(
domain: SemanticDomainTreeNode
): PayloadAction {
return setCurrentDomainAction(domain);
}

export function resetTreeAction(): TreeViewAction {
return { type: TreeActionType.RESET_TREE };
export function setDomainLanguage(language: string): PayloadAction {
return setDomainLanguageAction(language);
}

// Dispatch Functions

export function traverseTree(domain: SemanticDomain) {
return async (dispatch: StoreStateDispatch) => {
if (domain) {
await getSemanticDomainTreeNode(domain.id, domain.lang).then(
(response) => {
if (response) {
dispatch(setDomainAction(response));
}
}
);
}
};
}

export function updateTreeLanguage(language: string) {
return (dispatch: StoreStateDispatch) => {
if (language) {
dispatch(setDomainLanguageAction(language));
if (domain.id) {
const dom = await getSemanticDomainTreeNode(domain.id, domain.lang);
if (dom) {
dispatch(setCurrentDomain(dom));
}
}
};
}

export function initTreeDomain(language = "") {
export function initTreeDomain(lang = "") {
return async (dispatch: StoreStateDispatch, getState: () => StoreState) => {
const currentDomain = getState().treeViewState.currentDomain;
currentDomain.lang = language;
await dispatch(traverseTree(currentDomain));
await dispatch(
traverseTree({ ...getState().treeViewState.currentDomain, lang })
);
};
}
70 changes: 32 additions & 38 deletions src/components/TreeView/Redux/TreeViewReducer.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,33 @@
import {
TreeViewAction,
TreeActionType,
TreeViewState,
defaultState,
} from "components/TreeView/Redux/TreeViewReduxTypes";
import { StoreAction, StoreActionTypes } from "rootActions";
import { createSlice } from "@reduxjs/toolkit";

export const treeViewReducer = (
state: TreeViewState = defaultState,
action: StoreAction | TreeViewAction
): TreeViewState => {
switch (action.type) {
case TreeActionType.CLOSE_TREE:
return { ...state, open: false };
case TreeActionType.OPEN_TREE:
return { ...state, open: true };
case TreeActionType.RESET_TREE:
return defaultState;
case TreeActionType.SET_DOMAIN_LANGUAGE:
if (!action.language) {
throw new Error("Cannot set domain language to undefined.");
}
return {
...state,
currentDomain: { ...state.currentDomain, lang: action.language },
language: action.language,
};
case TreeActionType.SET_CURRENT_DOMAIN:
if (!action.domain) {
throw new Error("Cannot set the current domain to undefined.");
}
return { ...state, currentDomain: action.domain };
case StoreActionTypes.RESET:
return defaultState;
default:
return state;
}
};
import { defaultState } from "components/TreeView/Redux/TreeViewReduxTypes";
import { StoreActionTypes } from "rootActions";

const treeViewSlice = createSlice({
name: "treeViewState",
initialState: defaultState,
reducers: {
resetTreeAction: () => defaultState,
setCurrentDomainAction: (state, action) => {
state.currentDomain = action.payload;
},
setDomainLanguageAction: (state, action) => {
state.currentDomain.lang = action.payload;
state.language = action.payload;
},
setTreeOpenAction: (state, action) => {
state.open = action.payload;
},
},
extraReducers: (builder) =>
builder.addCase(StoreActionTypes.RESET, () => defaultState),
});

export const {
resetTreeAction,
setCurrentDomainAction,
setDomainLanguageAction,
setTreeOpenAction,
} = treeViewSlice.actions;

export default treeViewSlice.reducer;
14 changes: 0 additions & 14 deletions src/components/TreeView/Redux/TreeViewReduxTypes.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,6 @@
import { SemanticDomainTreeNode } from "api/models";
import { newSemanticDomainTreeNode } from "types/semanticDomain";

export enum TreeActionType {
CLOSE_TREE = "CLOSE_TREE",
OPEN_TREE = "OPEN_TREE",
RESET_TREE = "RESET_TREE",
SET_DOMAIN_LANGUAGE = "SET_DOMAIN_LANGUAGE",
SET_CURRENT_DOMAIN = "SET_CURRENT_DOMAIN",
}

export interface TreeViewAction {
type: TreeActionType;
domain?: SemanticDomainTreeNode;
language?: string;
}

export interface TreeViewState {
currentDomain: SemanticDomainTreeNode;
language: string;
Expand Down
99 changes: 58 additions & 41 deletions src/components/TreeView/Redux/tests/TreeViewActions.test.tsx
Original file line number Diff line number Diff line change
@@ -1,61 +1,78 @@
import configureMockStore from "redux-mock-store";
import thunk from "redux-thunk";
import { PreloadedState } from "redux";

import { defaultState } from "components/App/DefaultState";
import {
setDomainLanguageAction,
initTreeDomain,
setDomainLanguage,
traverseTree,
} from "components/TreeView/Redux/TreeViewActions";
import { RootState, setupStore } from "store";
import {
defaultState,
TreeActionType,
} from "components/TreeView/Redux/TreeViewReduxTypes";
import { newSemanticDomainTreeNode } from "types/semanticDomain";
newSemanticDomain,
newSemanticDomainTreeNode,
} from "types/semanticDomain";

jest.mock("backend", () => ({
getSemanticDomainTreeNode: (id: string, lang: string) =>
mockGetSemDomTreeNode(id, lang),
getSemanticDomainTreeNode: (...args: any[]) => mockGetSemDomTreeNode(...args),
}));

const mockGetSemDomTreeNode = jest.fn();

// Mock the track and identify methods of segment analytics.
global.analytics = { identify: jest.fn(), track: jest.fn() } as any;

const createMockStore = configureMockStore([thunk]);
const mockState = defaultState;

describe("TraverseTreeAction", () => {
it("SetDomainLanguage returns correct action", async () => {
const language = "lang";
const action = {
type: TreeActionType.SET_DOMAIN_LANGUAGE,
language,
};
const mockStore = createMockStore(mockState);
await mockStore.dispatch<any>(setDomainLanguageAction("lang"));
expect(mockStore.getActions()).toEqual([action]);
});
const mockId = "id";
const mockLang = "lang";

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

it("TraverseTreeAction dispatches on successful", async () => {
const mockDomainReturned = newSemanticDomainTreeNode("id", "name");
mockGetSemDomTreeNode.mockResolvedValue(mockDomainReturned);
const domain = { id: "id", name: "name", guid: "", lang: "" };
const action = {
type: TreeActionType.SET_CURRENT_DOMAIN,
domain: mockDomainReturned,
};
const mockStore = createMockStore(mockState);

await mockStore.dispatch<any>(traverseTree(domain));
expect(mockStore.getActions()).toEqual([action]);
describe("TreeViewActions", () => {
describe("setDomainLanguage", () => {
it("correctly affects state", async () => {
const store = setupStore();
store.dispatch(setDomainLanguage(mockLang));
const { currentDomain, language } = store.getState().treeViewState;
expect(currentDomain.lang).toEqual(mockLang);
expect(language).toEqual(mockLang);
});
});

it("TraverseTreeAction does not dispatch on null return", async () => {
mockGetSemDomTreeNode.mockResolvedValue(undefined);
const domain = { id: "id", name: "name", guid: "", lang: "" };
const mockStore = createMockStore(mockState);
describe("traverseTree", () => {
it("dispatches on successful", async () => {
const store = setupStore();
const dom = newSemanticDomain(mockId);
mockGetSemDomTreeNode.mockResolvedValue(dom);
await store.dispatch(traverseTree(dom));
const { currentDomain } = store.getState().treeViewState;
expect(currentDomain.id).toEqual(mockId);
});

it("does not dispatch on undefined", async () => {
const store = setupStore();
mockGetSemDomTreeNode.mockResolvedValue(undefined);
await store.dispatch(traverseTree(newSemanticDomain(mockId)));
const { currentDomain } = store.getState().treeViewState;
expect(currentDomain.id).not.toEqual(mockId);
});
});

await mockStore.dispatch<any>(traverseTree(domain));
expect(mockStore.getActions()).toEqual([]);
describe("initTreeDomain", () => {
it("changes domain lang but not id", async () => {
const nonDefaultState = {
currentDomain: newSemanticDomainTreeNode(mockId),
language: "",
open: true,
};
const store = setupStore({
...persistedDefaultState,
treeViewState: nonDefaultState,
});
await store.dispatch(initTreeDomain(mockLang));
expect(mockGetSemDomTreeNode).toBeCalledWith(mockId, mockLang);
});
});
});
Loading

0 comments on commit 1c1db26

Please sign in to comment.