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 CharInv goal to use redux-toolkit #2749

Merged
merged 10 commits into from
Nov 8, 2023
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"thecombine",
"upsert",
"venv",
"verns",
"wordlist",
"wordlists"
],
Expand Down
8 changes: 4 additions & 4 deletions src/components/App/DefaultState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,28 @@ import { defaultState as currentProjectState } from "components/Project/ProjectR
import { defaultState as exportProjectState } from "components/ProjectExport/Redux/ExportProjectReduxTypes";
import { defaultState as pronunciationsState } from "components/Pronunciations/Redux/PronunciationsReduxTypes";
import { defaultState as treeViewState } from "components/TreeView/Redux/TreeViewReduxTypes";
import { defaultState as characterInventoryState } from "goals/CharacterInventory/Redux/CharacterInventoryReducer";
import { defaultState as characterInventoryState } from "goals/CharacterInventory/Redux/CharacterInventoryReduxTypes";
import { defaultState as mergeDuplicateGoal } from "goals/MergeDuplicates/Redux/MergeDupsReducer";
import { defaultState as reviewEntriesState } from "goals/ReviewEntries/ReviewEntriesComponent/Redux/ReviewEntriesReduxTypes";
import { defaultState as analyticsState } from "types/Redux/analyticsReduxTypes";

export const defaultState = {
//login
//login and signup
loginState: { ...loginState },

//project
currentProjectState: { ...currentProjectState },
exportProjectState: { ...exportProjectState },

//data entry and review entries
//data entry and review entries goal
treeViewState: { ...treeViewState },
reviewEntriesState: { ...reviewEntriesState },
pronunciationsState: { ...pronunciationsState },

//goal timeline and current goal
goalsState: { ...goalTimelineState },

//merge duplicates goal
//merge duplicates goal and review deferred duplicates goal
mergeDuplicateGoal: { ...mergeDuplicateGoal },

//character inventory goal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
buttonIdSubmit,
} from "goals/CharacterInventory/CharInv/CharacterDetail/FindAndReplace";
import CharacterReplaceDialog from "goals/CharacterInventory/CharInv/CharacterDetail/FindAndReplace/CharacterReplaceDialog";
import { defaultState } from "goals/CharacterInventory/Redux/CharacterInventoryReducer";
import { defaultState } from "goals/CharacterInventory/Redux/CharacterInventoryReduxTypes";
import { StoreState } from "types";

// Dialog uses portals, which are not supported in react-test-renderer.
Expand Down
5 changes: 3 additions & 2 deletions src/goals/CharacterInventory/CharInv/CharacterList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ export default function CharacterList(): ReactElement {
};

useEffect(() => {
setOrderedChars(sortBy(allChars, sortOrder));
}, [allChars, setOrderedChars, sortOrder]);
// Spread allChars to not mutate the Redux state.
setOrderedChars(sortBy([...allChars], sortOrder));
}, [allChars, sortOrder]);

return (
<>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
import { Provider } from "react-redux";
import renderer from "react-test-renderer";
import { ReactTestRenderer, act, create } from "react-test-renderer";
import configureMockStore from "redux-mock-store";

import "tests/reactI18nextMock";

import CharacterList from "goals/CharacterInventory/CharInv/CharacterList";
import CharacterCard from "goals/CharacterInventory/CharInv/CharacterList/CharacterCard";
import { defaultState } from "goals/CharacterInventory/Redux/CharacterInventoryReducer";
import { newCharacterSetEntry } from "goals/CharacterInventory/Redux/CharacterInventoryReduxTypes";
import {
defaultState,
newCharacterSetEntry,
} from "goals/CharacterInventory/Redux/CharacterInventoryReduxTypes";

const characterSet = ["q", "w", "e", "r", "t", "y"].map(newCharacterSetEntry);
const mockStore = configureMockStore()({
characterInventoryState: { ...defaultState, characterSet },
});

let testRenderer: renderer.ReactTestRenderer;
let testRenderer: ReactTestRenderer;

beforeEach(async () => {
await renderer.act(async () => {
testRenderer = renderer.create(
await act(async () => {
testRenderer = create(
<Provider store={mockStore}>
<CharacterList />
</Provider>
Expand Down
4 changes: 2 additions & 2 deletions src/goals/CharacterInventory/CharInv/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import CharacterSetHeader from "goals/CharacterInventory/CharInv/CharacterSetHea
import {
exit,
loadCharInvData,
resetInState,
resetCharInv,
setSelectedCharacter,
uploadInventory,
} from "goals/CharacterInventory/Redux/CharacterInventoryActions";
Expand Down Expand Up @@ -54,7 +54,7 @@ export default function CharacterInventory(): ReactElement {
dispatch(loadCharInvData());

// Call when component unmounts.
() => dispatch(resetInState());
() => dispatch(resetCharInv());
}, [dispatch]);

const save = async (): Promise<void> => {
Expand Down
4 changes: 2 additions & 2 deletions src/goals/CharacterInventory/CharInv/tests/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import CharInv, {
dialogButtonIdYes,
dialogIdCancel,
} from "goals/CharacterInventory/CharInv";
import { defaultState as characterInventoryState } from "goals/CharacterInventory/Redux/CharacterInventoryReducer";
import { defaultState as characterInventoryState } from "goals/CharacterInventory/Redux/CharacterInventoryReduxTypes";

// Replace Dialog with something that doesn't create portals,
// because react-test-renderer does not support portals.
Expand All @@ -27,7 +27,7 @@ jest.mock("goals/CharacterInventory/CharInv/CharacterDetail", () => "div");
jest.mock("goals/CharacterInventory/Redux/CharacterInventoryActions", () => ({
exit: () => mockExit(),
loadCharInvData: () => mockLoadCharInvData(),
resetInState: () => jest.fn(),
reset: () => jest.fn(),
setSelectedCharacter: () => mockSetSelectedCharacter(),
uploadInventory: () => mockUploadInventory(),
}));
Expand Down
162 changes: 62 additions & 100 deletions src/goals/CharacterInventory/Redux/CharacterInventoryActions.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Action, PayloadAction } from "@reduxjs/toolkit";

import { Project } from "api/models";
import { getFrontierWords } from "backend";
import router from "browserRouter";
Expand All @@ -10,84 +12,59 @@ import {
CharacterStatus,
CharacterChange,
} from "goals/CharacterInventory/CharacterInventoryTypes";
import {
addRejectedCharacterAction,
addValidCharacterAction,
resetCharInvAction,
setAllWordsAction,
setCharacterSetAction,
setRejectedCharactersAction,
setSelectedCharacterAction,
setValidCharactersAction,
} from "goals/CharacterInventory/Redux/CharacterInventoryReducer";
import {
CharacterInventoryState,
CharacterSetEntry,
CharacterInventoryAction,
CharacterInventoryType,
getCharacterStatus,
} from "goals/CharacterInventory/Redux/CharacterInventoryReduxTypes";
import { StoreState } from "types";
import { StoreStateDispatch } from "types/Redux/actions";
import { Path } from "types/path";

// Action Creators
// Action Creation Functions

export function addToValidCharacters(
chars: string[]
): CharacterInventoryAction {
return {
type: CharacterInventoryType.ADD_TO_VALID_CHARACTERS,
payload: chars,
};
export function addRejectedCharacter(char: string): PayloadAction {
return addRejectedCharacterAction(char);
}

export function addToRejectedCharacters(
chars: string[]
): CharacterInventoryAction {
return {
type: CharacterInventoryType.ADD_TO_REJECTED_CHARACTERS,
payload: chars,
};
export function addValidCharacter(char: string): PayloadAction {
return addValidCharacterAction(char);
}

export function setValidCharacters(chars: string[]): CharacterInventoryAction {
return {
type: CharacterInventoryType.SET_VALID_CHARACTERS,
payload: chars,
};
export function resetCharInv(): Action {
return resetCharInvAction();
}

export function setRejectedCharacters(
chars: string[]
): CharacterInventoryAction {
return {
type: CharacterInventoryType.SET_REJECTED_CHARACTERS,
payload: chars,
};
export function setAllWords(words: string[]): PayloadAction {
return setAllWordsAction(words);
}

export function setAllWords(words: string[]): CharacterInventoryAction {
return {
type: CharacterInventoryType.SET_ALL_WORDS,
payload: words,
};
export function setCharacterSet(
characterSet: CharacterSetEntry[]
): PayloadAction {
return setCharacterSetAction(characterSet);
}

export function setSelectedCharacter(
character: string
): CharacterInventoryAction {
return {
type: CharacterInventoryType.SET_SELECTED_CHARACTER,
payload: [character],
};
export function setRejectedCharacters(chars: string[]): PayloadAction {
return setRejectedCharactersAction(chars);
}

export function setCharacterSet(
characterSet: CharacterSetEntry[]
): CharacterInventoryAction {
return {
type: CharacterInventoryType.SET_CHARACTER_SET,
payload: [],
characterSet,
};
export function setSelectedCharacter(character: string): PayloadAction {
return setSelectedCharacterAction(character);
}

export function resetInState(): CharacterInventoryAction {
return {
type: CharacterInventoryType.RESET,
payload: [],
};
export function setValidCharacters(chars: string[]): PayloadAction {
return setValidCharactersAction(chars);
}

// Dispatch Functions
Expand All @@ -96,10 +73,10 @@ export function setCharacterStatus(character: string, status: CharacterStatus) {
return (dispatch: StoreStateDispatch, getState: () => StoreState) => {
switch (status) {
case CharacterStatus.Accepted:
dispatch(addToValidCharacters([character]));
dispatch(addValidCharacter(character));
break;
case CharacterStatus.Rejected:
dispatch(addToRejectedCharacters([character]));
dispatch(addRejectedCharacter(character));
break;
case CharacterStatus.Undecided:
const state = getState().characterInventoryState;
Expand All @@ -120,17 +97,23 @@ export function setCharacterStatus(character: string, status: CharacterStatus) {
};
}

// Sends the character inventory to the server.
/** Sends the in-state character inventory to the server. */
export function uploadInventory() {
return async (dispatch: StoreStateDispatch, getState: () => StoreState) => {
const state = getState();
const changes = getChangesFromState(state);
const charInvState = getState().characterInventoryState;
const project = getState().currentProjectState.project;
const changes = getChanges(project, charInvState);
if (!changes.length) {
exit();
return;
}
const updatedProject = updateCurrentProject(state);
await dispatch(asyncUpdateCurrentProject(updatedProject));
await dispatch(
asyncUpdateCurrentProject({
...project,
rejectedCharacters: charInvState.rejectedCharacters,
validCharacters: charInvState.validCharacters,
})
);
dispatch(addCharInvChangesToGoal(changes));
await dispatch(asyncUpdateGoal());
exit();
Expand All @@ -146,28 +129,17 @@ export function fetchWords() {

export function getAllCharacters() {
return async (dispatch: StoreStateDispatch, getState: () => StoreState) => {
const state = getState();
const words = await getFrontierWords();
const charactersWithDuplicates: string[] = [];
words.forEach((word) => charactersWithDuplicates.push(...word.vernacular));
const characters = [...new Set(charactersWithDuplicates)];

const characterSet: CharacterSetEntry[] = [];
characters.forEach((letter) => {
characterSet.push({
character: letter,
occurrences: countCharacterOccurrences(
letter,
words.map((word) => word.vernacular)
),
status: getCharacterStatus(
letter,
state.currentProjectState.project.validCharacters,
state.currentProjectState.project.rejectedCharacters
),
});
});
dispatch(setCharacterSet(characterSet));
const allWords = getState().characterInventoryState.allWords;
const characters = new Set<string>();
allWords.forEach((w) => [...w].forEach((c) => characters.add(c)));
const { rejectedCharacters, validCharacters } =
getState().currentProjectState.project;
const entries: CharacterSetEntry[] = [...characters].map((c) => ({
character: c,
occurrences: countOccurrences(c, allWords),
status: getCharacterStatus(c, validCharacters, rejectedCharacters),
}));
dispatch(setCharacterSet(entries));
};
}

Expand All @@ -188,7 +160,10 @@ export function exit(): void {
router.navigate(Path.Goals);
}

function countCharacterOccurrences(char: string, words: string[]): number {
function countOccurrences(char: string, words: string[]): number {
if (char.length !== 1) {
console.error(`countOccurrences expects length 1 char, but got: ${char}`);
}
let count = 0;
for (const word of words) {
for (const letter of word) {
Expand All @@ -200,19 +175,13 @@ function countCharacterOccurrences(char: string, words: string[]): number {
return count;
}

function getChangesFromState(state: StoreState): CharacterChange[] {
const proj = state.currentProjectState.project;
const charInvState = state.characterInventoryState;
return getChanges(proj, charInvState);
}

export function getChanges(
proj: Project,
project: Project,
charInvState: CharacterInventoryState
): CharacterChange[] {
const oldAcc = proj.validCharacters;
const oldAcc = project.validCharacters;
const newAcc = charInvState.validCharacters;
const oldRej = proj.rejectedCharacters;
const oldRej = project.rejectedCharacters;
const newRej = charInvState.rejectedCharacters;
const allCharacters = [
...new Set([...oldAcc, ...newAcc, ...oldRej, ...newRej]),
Expand Down Expand Up @@ -261,10 +230,3 @@ function getChange(
}
return undefined;
}

function updateCurrentProject(state: StoreState): Project {
const project = state.currentProjectState.project;
project.validCharacters = state.characterInventoryState.validCharacters;
project.rejectedCharacters = state.characterInventoryState.rejectedCharacters;
return project;
}
Loading
Loading