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 ReviewEntries to use redux-toolkit #2800

Merged
merged 11 commits into from
Dec 5, 2023
4 changes: 2 additions & 2 deletions src/goals/DefaultGoal/BaseGoalScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import PageNotFound from "components/PageNotFound/component";
import DisplayProgress from "goals/DefaultGoal/DisplayProgress";
import Loading from "goals/DefaultGoal/Loading";
import { clearTree } from "goals/MergeDuplicates/Redux/MergeDupsActions";
import { clearReviewEntriesState } from "goals/ReviewEntries/Redux/ReviewEntriesActions";
import { resetReviewEntries } from "goals/ReviewEntries/Redux/ReviewEntriesActions";
import { StoreState } from "types";
import { Goal, GoalStatus, GoalType } from "types/goals";
import { useAppDispatch, useAppSelector } from "types/hooks";
Expand Down Expand Up @@ -52,7 +52,7 @@ export function BaseGoalScreen(): ReactElement {
useEffect(() => {
return function cleanup(): void {
dispatch(setCurrentGoal());
dispatch(clearReviewEntriesState());
dispatch(resetReviewEntries());
dispatch(clearTree());
};
}, [dispatch]);
Expand Down
111 changes: 60 additions & 51 deletions src/goals/ReviewEntries/Redux/ReviewEntriesActions.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import { Sense } from "api/models";
import { Action, PayloadAction } from "@reduxjs/toolkit";

import { Sense, Word } from "api/models";
import * as backend from "backend";
import {
addEntryEditToGoal,
asyncUpdateGoal,
} from "components/GoalTimeline/Redux/GoalActions";
import { uploadFileFromUrl } from "components/Pronunciations/utilities";
import {
ReviewClearReviewEntriesState,
ReviewEntriesActionTypes,
ReviewSortBy,
ReviewUpdateWord,
ReviewUpdateWords,
} from "goals/ReviewEntries/Redux/ReviewEntriesReduxTypes";
deleteWordAction,
resetReviewEntriesAction,
setAllWordsAction,
setSortByAction,
updateWordAction,
} from "goals/ReviewEntries/Redux/ReviewEntriesReducer";
import {
ColumnId,
ReviewEntriesSense,
Expand All @@ -20,38 +22,45 @@ import {
import { StoreStateDispatch } from "types/Redux/actions";
import { newNote, newSense } from "types/word";

export function sortBy(columnId?: ColumnId): ReviewSortBy {
return {
type: ReviewEntriesActionTypes.SortBy,
sortBy: columnId,
};
// Action Creation Functions

export function deleteWord(wordId: string): Action {
return deleteWordAction(wordId);
}

export function updateAllWords(words: ReviewEntriesWord[]): ReviewUpdateWords {
return {
type: ReviewEntriesActionTypes.UpdateAllWords,
words,
};
export function resetReviewEntries(): Action {
return resetReviewEntriesAction();
}

export function setAllWords(words: Word[]): PayloadAction {
return setAllWordsAction(words);
}

export function setSortBy(columnId?: ColumnId): PayloadAction {
return setSortByAction(columnId);
}

function updateWord(oldId: string, updatedWord: ReviewEntriesWord) {
interface WordUpdate {
oldId: string;
updatedWord: Word;
}

export function updateWord(update: WordUpdate): PayloadAction {
return updateWordAction(update);
}

// Dispatch Functions

/** Updates a word and the current goal. */
function asyncUpdateWord(oldId: string, updatedWord: Word) {
return async (dispatch: StoreStateDispatch) => {
dispatch(addEntryEditToGoal({ newId: updatedWord.id, oldId }));
await dispatch(asyncUpdateGoal());
const update: ReviewUpdateWord = {
type: ReviewEntriesActionTypes.UpdateWord,
oldId,
updatedWord,
};
dispatch(update);
dispatch(updateWord({ oldId, updatedWord }));
};
}

export function clearReviewEntriesState(): ReviewClearReviewEntriesState {
return { type: ReviewEntriesActionTypes.ClearReviewEntriesState };
}

// Return the translation code for our error, or undefined if there is no error
/** Return the translation code for our error, or undefined if there is no error */
export function getSenseError(
sense: ReviewEntriesSense,
checkGlosses = true,
Expand All @@ -66,10 +75,10 @@ export function getSenseError(
return undefined;
}

// Returns a cleaned array of senses ready to be saved (none with .deleted=true):
// * If a sense is marked as deleted or is utterly blank, it is removed
// * If a sense lacks gloss, return error
// * If the user attempts to delete all senses, return old senses with deleted senses removed
/** Returns a cleaned array of senses ready to be saved (none with .deleted=true):
* - If a sense is marked as deleted or is utterly blank, it is removed
* - If a sense lacks gloss, return error
* - If the user attempts to delete all senses, return old senses with deleted senses removed */
function cleanSenses(
senses: ReviewEntriesSense[],
oldSenses: ReviewEntriesSense[]
Expand Down Expand Up @@ -111,10 +120,10 @@ function cleanSenses(
return oldSenses.filter((s) => !s.deleted);
}

// Clean the vernacular field of a word:
// * If all senses are deleted, reject
// * If there's no vernacular field, add in the vernacular of old field
// * If neither the word nor oldWord has a vernacular, reject
/** Clean the vernacular field of a word:
* - If all senses are deleted, reject
* - If there's no vernacular field, add in the vernacular of old field
* - If neither the word nor oldWord has a vernacular, reject */
function cleanWord(
word: ReviewEntriesWord,
oldWord: ReviewEntriesWord
Expand All @@ -132,7 +141,7 @@ function cleanWord(
return typeof senses === "string" ? senses : { ...word, vernacular, senses };
}

// Converts the ReviewEntriesWord into a Word to send to the backend
/** Converts the ReviewEntriesWord into a Word to send to the backend */
export function updateFrontierWord(
newData: ReviewEntriesWord,
oldData?: ReviewEntriesWord
Expand All @@ -145,6 +154,7 @@ export function updateFrontierWord(
if (typeof editSource === "string") {
return Promise.reject(editSource);
}
const oldId = editSource.id;

// Set aside audio changes for last.
const delAudio = oldData.audio.filter(
Expand All @@ -155,7 +165,7 @@ export function updateFrontierWord(
delete editSource.audioNew;

// Get the original word, for updating.
const editWord = await backend.getWord(editSource.id);
const editWord = await backend.getWord(oldId);

// Update the data.
editWord.vernacular = editSource.vernacular;
Expand All @@ -166,23 +176,22 @@ export function updateFrontierWord(
editWord.flag = { ...editSource.flag };

// Update the word in the backend, and retrieve the id.
editSource.id = (await backend.updateWord(editWord)).id;
let newId = (await backend.updateWord(editWord)).id;

// Add/remove audio.
for (const url of addAudio) {
editSource.id = await uploadFileFromUrl(editSource.id, url);
newId = await uploadFileFromUrl(newId, url);
}
for (const fileName of delAudio) {
editSource.id = await backend.deleteAudio(editSource.id, fileName);
newId = await backend.deleteAudio(newId, fileName);
}
editSource.audio = (await backend.getWord(editSource.id)).audio;

// Update the review entries word in the state.
await dispatch(updateWord(editWord.id, editSource));
// Update the word in the state.
await dispatch(asyncUpdateWord(oldId, await backend.getWord(newId)));
};
}

// Creates a Sense from a cleaned ReviewEntriesSense and array of old senses.
/** Creates a Sense from a cleaned ReviewEntriesSense and array of old senses. */
export function getSenseFromEditSense(
editSense: ReviewEntriesSense,
oldSenses: Sense[]
Expand All @@ -199,23 +208,23 @@ export function getSenseFromEditSense(
return sense;
}

// Performs specified backend Word-updating function, then makes state ReviewEntriesWord-updating dispatch
function refreshWord(
/** Performs specified backend Word-updating function, then makes state ReviewEntriesWord-updating dispatch */
function asyncRefreshWord(
oldWordId: string,
wordUpdater: (wordId: string) => Promise<string>
) {
return async (dispatch: StoreStateDispatch): Promise<void> => {
const newWordId = await wordUpdater(oldWordId);
const word = await backend.getWord(newWordId);
await dispatch(updateWord(oldWordId, new ReviewEntriesWord(word)));
await dispatch(asyncUpdateWord(oldWordId, word));
};
}

export function deleteAudio(
wordId: string,
fileName: string
): (dispatch: StoreStateDispatch) => Promise<void> {
return refreshWord(wordId, (wordId: string) =>
return asyncRefreshWord(wordId, (wordId: string) =>
backend.deleteAudio(wordId, fileName)
);
}
Expand All @@ -224,7 +233,7 @@ export function uploadAudio(
wordId: string,
audioFile: File
): (dispatch: StoreStateDispatch) => Promise<void> {
return refreshWord(wordId, (wordId: string) =>
return asyncRefreshWord(wordId, (wordId: string) =>
backend.uploadAudio(wordId, audioFile)
);
}
70 changes: 34 additions & 36 deletions src/goals/ReviewEntries/Redux/ReviewEntriesReducer.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,38 @@
import {
defaultState,
ReviewEntriesAction,
ReviewEntriesActionTypes,
ReviewEntriesState,
} from "goals/ReviewEntries/Redux/ReviewEntriesReduxTypes";
import { StoreAction, StoreActionTypes } from "rootActions";
import { createSlice } from "@reduxjs/toolkit";

export const reviewEntriesReducer = (
state: ReviewEntriesState = defaultState, //createStore() calls each reducer with undefined state
action: ReviewEntriesAction | StoreAction
): ReviewEntriesState => {
switch (action.type) {
case ReviewEntriesActionTypes.SortBy:
// Change which column is being sorted by
return { ...state, sortBy: action.sortBy };
import { defaultState } from "goals/ReviewEntries/Redux/ReviewEntriesReduxTypes";
import { StoreActionTypes } from "rootActions";

case ReviewEntriesActionTypes.UpdateAllWords:
// Update the local words
return { ...state, words: action.words };
const reviewEntriesSlice = createSlice({
name: "reviewEntriesState",
initialState: defaultState,
reducers: {
deleteWordAction: (state, action) => {
state.words = state.words.filter((w) => w.id !== action.payload);
},
resetReviewEntriesAction: () => defaultState,
setAllWordsAction: (state, action) => {
state.words = action.payload;
},
setSortByAction: (state, action) => {
state.sortBy = action.payload;
},
updateWordAction: (state, action) => {
state.words = state.words.map((w) =>
w.id === action.payload.oldId ? action.payload.updatedWord : w
);
},
},
extraReducers: (builder) =>
builder.addCase(StoreActionTypes.RESET, () => defaultState),
});

case ReviewEntriesActionTypes.UpdateWord:
// Update the word of specified id
return {
...state,
words: state.words.map((w) =>
w.id === action.oldId ? { ...action.updatedWord } : w
),
};
export const {
deleteWordAction,
resetReviewEntriesAction,
setAllWordsAction,
setSortByAction,
updateWordAction,
} = reviewEntriesSlice.actions;

case ReviewEntriesActionTypes.ClearReviewEntriesState:
return defaultState;

case StoreActionTypes.RESET:
return defaultState;

default:
return state;
}
};
export default reviewEntriesSlice.reducer;
45 changes: 3 additions & 42 deletions src/goals/ReviewEntries/Redux/ReviewEntriesReduxTypes.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,12 @@
import {
ColumnId,
ReviewEntriesWord,
} from "goals/ReviewEntries/ReviewEntriesTypes";

export enum ReviewEntriesActionTypes {
SortBy = "SORT_BY",
UpdateAllWords = "UPDATE_ALL_WORDS",
UpdateWord = "UPDATE_WORD",
ClearReviewEntriesState = "CLEAR_REVIEW_ENTRIES_STATE",
}

export interface ReviewSortBy {
type: ReviewEntriesActionTypes.SortBy;
sortBy?: ColumnId;
}

export interface ReviewUpdateWords {
type: ReviewEntriesActionTypes.UpdateAllWords;
words: ReviewEntriesWord[];
}

export interface ReviewUpdateWord {
type: ReviewEntriesActionTypes.UpdateWord;
oldId: string;
updatedWord: ReviewEntriesWord;
}

export interface ReviewClearReviewEntriesState {
type: ReviewEntriesActionTypes.ClearReviewEntriesState;
}

export type ReviewEntriesAction =
| ReviewSortBy
| ReviewUpdateWords
| ReviewUpdateWord
| ReviewClearReviewEntriesState;
import { Word } from "api/models";
import { ColumnId } from "goals/ReviewEntries/ReviewEntriesTypes";

export interface ReviewEntriesState {
words: ReviewEntriesWord[];
isRecording: boolean;
words: Word[];
sortBy?: ColumnId;
wordBeingRecorded?: string;
}

export const defaultState: ReviewEntriesState = {
words: [],
isRecording: false,
sortBy: undefined,
wordBeingRecorded: undefined,
};
Loading