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 TreeView to use redux-toolkit #2774

Merged
merged 5 commits into from
Nov 8, 2023
Merged
Show file tree
Hide file tree
Changes from 3 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
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";
resetAction,
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 resetAction();
}

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: {
resetAction: () => 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 {
resetAction,
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