Skip to content

Commit

Permalink
Port ReviewEntries to use redux-toolkit (#2800)
Browse files Browse the repository at this point in the history
  • Loading branch information
imnasnainaec authored and jmgrady committed Mar 6, 2024
1 parent 3b56ee7 commit 26a0414
Show file tree
Hide file tree
Showing 13 changed files with 232 additions and 235 deletions.
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

0 comments on commit 26a0414

Please sign in to comment.