From 0a811cd0db1452c0dc6b73259eecfae186f3e226 Mon Sep 17 00:00:00 2001 From: Jim Grady Date: Thu, 5 Oct 2023 11:40:14 -0400 Subject: [PATCH 01/36] Move changes to state to reducer --- .../MergeDupsStep/MergeDragDrop/DropWord.tsx | 11 +- .../MergeDupsStep/MergeDragDrop/index.tsx | 20 +- .../MergeDuplicates/Redux/MergeDupsActions.ts | 180 +++------ .../MergeDuplicates/Redux/MergeDupsReducer.ts | 346 +++++++++--------- .../Redux/MergeDupsReduxTypes.ts | 99 +---- src/rootReducer.ts | 2 +- 6 files changed, 267 insertions(+), 391 deletions(-) diff --git a/src/goals/MergeDuplicates/MergeDupsStep/MergeDragDrop/DropWord.tsx b/src/goals/MergeDuplicates/MergeDupsStep/MergeDragDrop/DropWord.tsx index c892a28ea7..6598baf2e8 100644 --- a/src/goals/MergeDuplicates/MergeDupsStep/MergeDragDrop/DropWord.tsx +++ b/src/goals/MergeDuplicates/MergeDupsStep/MergeDragDrop/DropWord.tsx @@ -42,7 +42,7 @@ export default function DropWord(props: DropWordProps): ReactElement { // reset vern if not in vern list if (treeWord && !verns.includes(treeWord.vern)) { - dispatch(setVern(props.wordId, verns[0] || "")); + dispatch(setVern({ wordId: props.wordId, vern: verns[0] || "" })); } return ( @@ -68,7 +68,12 @@ export default function DropWord(props: DropWordProps): ReactElement { variant="standard" value={treeWord.vern} onChange={(e) => - dispatch(setVern(props.wordId, e.target.value as string)) + dispatch( + setVern({ + wordId: props.wordId, + vern: e.target.value as string, + }) + ) } > {verns.map((vern) => ( @@ -91,7 +96,7 @@ export default function DropWord(props: DropWordProps): ReactElement { { - dispatch(flagWord(props.wordId, newFlag)); + dispatch(flagWord({ wordId: props.wordId, flag: newFlag })); }} buttonId={`word-${props.wordId}-flag`} /> diff --git a/src/goals/MergeDuplicates/MergeDupsStep/MergeDragDrop/index.tsx b/src/goals/MergeDuplicates/MergeDupsStep/MergeDragDrop/index.tsx index 3889aa5ac1..be213702a9 100644 --- a/src/goals/MergeDuplicates/MergeDupsStep/MergeDragDrop/index.tsx +++ b/src/goals/MergeDuplicates/MergeDupsStep/MergeDragDrop/index.tsx @@ -55,7 +55,13 @@ export default function MergeDragDrop(): ReactElement { // Case 2a: Cannot merge a protected sense into another sense. if (sourceId !== res.combine.droppableId) { // The target sense is in a different word, so move instead of combine. - dispatch(moveSense(senseRef, res.combine.droppableId, 0)); + dispatch( + moveSense({ + ref: senseRef, + destWordId: res.combine.droppableId, + destOrder: 0, + }) + ); } return; } @@ -66,7 +72,7 @@ export default function MergeDragDrop(): ReactElement { // Case 2b: If the target is a sidebar sub-sense, it cannot receive a combine. return; } - dispatch(combineSense(senseRef, combineRef)); + dispatch(combineSense({ src: senseRef, dest: combineRef })); } else if (res.destination) { const destId = res.destination.droppableId; // Case 3: The sense was dropped in a droppable. @@ -77,7 +83,13 @@ export default function MergeDragDrop(): ReactElement { return; } // Move the sense to the dest MergeWord. - dispatch(moveSense(senseRef, destId, res.destination.index)); + dispatch( + moveSense({ + ref: senseRef, + destWordId: destId, + destOrder: res.destination.index, + }) + ); } else { // Case 3b: The source & dest droppables are the same, so we reorder, not move. const order = res.destination.index; @@ -90,7 +102,7 @@ export default function MergeDragDrop(): ReactElement { // If the sense wasn't moved or was moved within the sidebar above a protected sense, do nothing. return; } - dispatch(orderSense(senseRef, order)); + dispatch(orderSense({ ref: senseRef, order: order })); } } } diff --git a/src/goals/MergeDuplicates/Redux/MergeDupsActions.ts b/src/goals/MergeDuplicates/Redux/MergeDupsActions.ts index 775b52f229..8a1b29ae45 100644 --- a/src/goals/MergeDuplicates/Redux/MergeDupsActions.ts +++ b/src/goals/MergeDuplicates/Redux/MergeDupsActions.ts @@ -1,12 +1,6 @@ -import { - Definition, - Flag, - GramCatGroup, - MergeSourceWord, - MergeWords, - Status, - Word, -} from "api/models"; +import { Action, PayloadAction } from "@reduxjs/toolkit"; + +import { MergeSourceWord, MergeWords, Status, Word } from "api/models"; import * as backend from "backend"; import { addCompletedMergeToGoal, @@ -24,100 +18,80 @@ import { newMergeWords, } from "goals/MergeDuplicates/MergeDupsTypes"; import { - ClearTreeMergeAction, - CombineSenseMergeAction, - DeleteSenseMergeAction, - FlagWord, - MergeTreeActionTypes, + clearTreeAction, + combineIntoFirstSenseAction, + combineSenseAction, + deleteSenseAction, + flagWordAction, + moveDuplicateAction, + moveSenseAction, + orderDuplicateAction, + orderSenseAction, + setSidebarAction, + setWordDataAction, + setVernacularAction, +} from "goals/MergeDuplicates/Redux/MergeDupsReducer"; +import { + CombineSenseMergePayload, + FlagWordPayload, MergeTreeState, - MoveDuplicateMergeAction, - MoveSenseMergeAction, - OrderDuplicateMergeAction, - OrderSenseMergeAction, - SetDataMergeAction, - SetSidebarMergeAction, - SetVernacularMergeAction, + MoveSensePayload, + OrderSensePayload, + SetVernacularPayload, } from "goals/MergeDuplicates/Redux/MergeDupsReduxTypes"; import { StoreState } from "types"; import { StoreStateDispatch } from "types/Redux/actions"; import { Hash } from "types/hash"; import { compareFlags } from "utilities/wordUtilities"; -// Action Creators +// Action Creation Functions -export function clearTree(): ClearTreeMergeAction { - return { type: MergeTreeActionTypes.CLEAR_TREE }; +export function clearTree(): Action { + return clearTreeAction(); } -export function combineSense( - src: MergeTreeReference, - dest: MergeTreeReference -): CombineSenseMergeAction { - return { type: MergeTreeActionTypes.COMBINE_SENSE, payload: { src, dest } }; +export function combineIntoFirstSense(senses: MergeTreeSense[]): PayloadAction { + return combineIntoFirstSenseAction(senses); } -export function deleteSense(src: MergeTreeReference): DeleteSenseMergeAction { - return { type: MergeTreeActionTypes.DELETE_SENSE, payload: { src } }; +export function combineSense(payload: CombineSenseMergePayload): PayloadAction { + return combineSenseAction(payload); } -export function flagWord(wordId: string, flag: Flag): FlagWord { - return { type: MergeTreeActionTypes.FLAG_WORD, payload: { wordId, flag } }; +export function deleteSense(payload: MergeTreeReference): PayloadAction { + return deleteSenseAction(payload); } -export function moveSense( - ref: MergeTreeReference, - destWordId: string, - destOrder: number -): MoveDuplicateMergeAction | MoveSenseMergeAction { - if (ref.order === undefined) { - return { - type: MergeTreeActionTypes.MOVE_SENSE, - payload: { ...ref, destWordId, destOrder }, - }; +export function flagWord(payload: FlagWordPayload): PayloadAction { + return flagWordAction(payload); +} + +export function moveSense(payload: MoveSensePayload): PayloadAction { + if (payload.ref.order === undefined) { + return moveSenseAction(payload); + } else { + return moveDuplicateAction(payload); } - // If ref.order is defined, the sense is being moved out of the sidebar. - return { - type: MergeTreeActionTypes.MOVE_DUPLICATE, - payload: { ref, destWordId, destOrder }, - }; } -export function orderSense( - ref: MergeTreeReference, - order: number -): OrderDuplicateMergeAction | OrderSenseMergeAction { - if (ref.order === undefined) { - return { - type: MergeTreeActionTypes.ORDER_SENSE, - payload: { ...ref, order }, - }; +export function orderSense(payload: OrderSensePayload): PayloadAction { + if (payload.ref.order === undefined) { + return orderSenseAction(payload); + } else { + return orderDuplicateAction(payload); } - // If ref.order is defined, the sense is being ordered within the sidebar. - return { - type: MergeTreeActionTypes.ORDER_DUPLICATE, - payload: { ref, order }, - }; } -export function setSidebar(sidebar?: Sidebar): SetSidebarMergeAction { - return { - type: MergeTreeActionTypes.SET_SIDEBAR, - payload: sidebar ?? defaultSidebar, - }; +export function setSidebar(sidebar?: Sidebar): PayloadAction { + return setSidebarAction(sidebar ?? defaultSidebar); } -export function setWordData(words: Word[]): SetDataMergeAction { - return { type: MergeTreeActionTypes.SET_DATA, payload: words }; +export function setWordData(words: Word[]): PayloadAction { + return setWordDataAction(words); } -export function setVern( - wordId: string, - vern: string -): SetVernacularMergeAction { - return { - type: MergeTreeActionTypes.SET_VERNACULAR, - payload: { wordId, vern }, - }; +export function setVern(payload: SetVernacularPayload): PayloadAction { + return setVernacularAction(payload); } // Dispatch Functions @@ -286,55 +260,3 @@ export async function fetchMergeDupsData( ): Promise { return await backend.getDuplicates(maxInList, maxLists); } - -/** Modifies the mutable input sense list. */ -export function combineIntoFirstSense(senses: MergeTreeSense[]): void { - // Set the first sense to be merged as Active/Protected. - // This was the top sense when the sidebar was opened. - const mainSense = senses[0]; - mainSense.accessibility = mainSense.protected - ? Status.Protected - : Status.Active; - - // Merge the rest as duplicates. - // These were senses dropped into another sense. - senses.slice(1).forEach((dupSense) => { - dupSense.accessibility = Status.Duplicate; - // Put the duplicate's definitions in the main sense. - dupSense.definitions.forEach((def) => - mergeDefinitionIntoSense(mainSense, def) - ); - // Use the duplicate's part of speech if not specified in the main sense. - if (mainSense.grammaticalInfo.catGroup === GramCatGroup.Unspecified) { - mainSense.grammaticalInfo = { ...dupSense.grammaticalInfo }; - } - // Put the duplicate's domains in the main sense. - dupSense.semanticDomains.forEach((dom) => { - if (!mainSense.semanticDomains.find((d) => d.id === dom.id)) { - mainSense.semanticDomains.push({ ...dom }); - } - }); - }); -} - -/** Modifies the mutable input sense. */ -export function mergeDefinitionIntoSense( - sense: MergeTreeSense, - def: Definition, - sep = ";" -): void { - if (!def.text.length) { - return; - } - const defIndex = sense.definitions.findIndex( - (d) => d.language === def.language - ); - if (defIndex === -1) { - sense.definitions.push({ ...def }); - } else { - const oldText = sense.definitions[defIndex].text; - if (!oldText.split(sep).includes(def.text)) { - sense.definitions[defIndex].text = `${oldText}${sep}${def.text}`; - } - } -} diff --git a/src/goals/MergeDuplicates/Redux/MergeDupsReducer.ts b/src/goals/MergeDuplicates/Redux/MergeDupsReducer.ts index 90974b1d9d..b52fb4636d 100644 --- a/src/goals/MergeDuplicates/Redux/MergeDupsReducer.ts +++ b/src/goals/MergeDuplicates/Redux/MergeDupsReducer.ts @@ -1,22 +1,18 @@ +import { createSlice } from "@reduxjs/toolkit"; import { v4 } from "uuid"; -import { Word } from "api/models"; +import { GramCatGroup, Status, Word } from "api/models"; import { convertSenseToMergeTreeSense, convertWordToMergeTreeWord, defaultSidebar, defaultTree, - MergeTree, MergeTreeSense, MergeTreeWord, newMergeTreeWord, } from "goals/MergeDuplicates/MergeDupsTreeTypes"; -import { - MergeTreeAction, - MergeTreeActionTypes, - MergeTreeState, -} from "goals/MergeDuplicates/Redux/MergeDupsReduxTypes"; -import { StoreAction, StoreActionTypes } from "rootActions"; +import { MergeTreeState } from "goals/MergeDuplicates/Redux/MergeDupsReduxTypes"; +import { StoreActionTypes } from "rootActions"; import { Hash } from "types/hash"; const defaultData = { words: {}, senses: {} }; @@ -25,52 +21,88 @@ export const defaultState: MergeTreeState = { tree: defaultTree, }; -export const mergeDupStepReducer = ( - state: MergeTreeState = defaultState, //createStore() calls each reducer with undefined state - action: MergeTreeAction | StoreAction -): MergeTreeState => { - switch (action.type) { - case MergeTreeActionTypes.CLEAR_TREE: { - return defaultState; - } - - case MergeTreeActionTypes.COMBINE_SENSE: { +const mergeDupStepSlice = createSlice({ + name: "mergeDupStepReducer", + initialState: defaultState, + reducers: { + clearTreeAction: (state) => { + state = defaultState; + }, + combineIntoFirstSenseAction: (state, action) => { + // Set the first sense to be merged as Active/Protected. + // This was the top sense when the sidebar was opened. + const senses: MergeTreeSense[] = action.payload; + const mainSense = senses[0]; + mainSense.accessibility = mainSense.protected + ? Status.Protected + : Status.Active; + + // Merge the rest as duplicates. + // These were senses dropped into another sense. + senses.slice(1).forEach((dupSense) => { + dupSense.accessibility = Status.Duplicate; + // Put the duplicate's definitions in the main sense. + const sep = ";"; + dupSense.definitions.forEach((def) => { + if (def.text.length) { + const defIndex = mainSense.definitions.findIndex( + (d) => d.language === def.language + ); + if (defIndex === -1) { + mainSense.definitions.push({ ...def }); + } else { + const oldText = mainSense.definitions[defIndex].text; + if (!oldText.split(sep).includes(def.text)) { + mainSense.definitions[ + defIndex + ].text = `${oldText}${sep}${def.text}`; + } + } + } + }); + // Use the duplicate's part of speech if not specified in the main sense. + if (mainSense.grammaticalInfo.catGroup === GramCatGroup.Unspecified) { + mainSense.grammaticalInfo = { ...dupSense.grammaticalInfo }; + } + // Put the duplicate's domains in the main sense. + dupSense.semanticDomains.forEach((dom) => { + if (!mainSense.semanticDomains.find((d) => d.id === dom.id)) { + mainSense.semanticDomains.push({ ...dom }); + } + }); + }); + state.tree.sidebar.senses = senses; + }, + combineSenseAction: (state, action) => { const srcRef = action.payload.src; const destRef = action.payload.dest; // Ignore dropping a sense (or one of its sub-senses) into itself. - if (srcRef.mergeSenseId === destRef.mergeSenseId) { - return state; - } - - const words: Hash = JSON.parse( - JSON.stringify(state.tree.words) - ); - const srcWordId = srcRef.wordId; - const srcGuids = words[srcWordId].sensesGuids[srcRef.mergeSenseId]; - const destGuids: string[] = []; - if (srcRef.order === undefined || srcGuids.length === 1) { - destGuids.push(...srcGuids); - delete words[srcWordId].sensesGuids[srcRef.mergeSenseId]; - if (!Object.keys(words[srcWordId].sensesGuids).length) { - delete words[srcWordId]; + if (srcRef.mergeSenseId !== destRef.mergeSenseId) { + const words = state.tree.words; + const srcWordId = srcRef.wordId; + const srcGuids = words[srcWordId].sensesGuids[srcRef.mergeSenseId]; + const destGuids: string[] = []; + if (srcRef.order === undefined || srcGuids.length === 1) { + destGuids.push(...srcGuids); + delete words[srcWordId].sensesGuids[srcRef.mergeSenseId]; + if (!Object.keys(words[srcWordId].sensesGuids).length) { + delete words[srcWordId]; + } + } else { + destGuids.push(srcGuids.splice(srcRef.order, 1)[0]); } - } else { - destGuids.push(srcGuids.splice(srcRef.order, 1)[0]); - } - words[destRef.wordId].sensesGuids[destRef.mergeSenseId].push( - ...destGuids - ); - - return { ...state, tree: { ...state.tree, words } }; - } - - case MergeTreeActionTypes.DELETE_SENSE: { + words[destRef.wordId].sensesGuids[destRef.mergeSenseId].push( + ...destGuids + ); + state.tree.words = words; + } + }, + deleteSenseAction: (state, action) => { const srcRef = action.payload.src; const srcWordId = srcRef.wordId; - const tree: MergeTree = JSON.parse(JSON.stringify(state.tree)); - const words = tree.words; + const words = state.tree.words; const sensesGuids = words[srcWordId].sensesGuids; if (srcRef.order !== undefined) { @@ -85,40 +117,66 @@ export const mergeDupStepReducer = ( delete words[srcWordId]; } - let sidebar = tree.sidebar; + const sidebar = state.tree.sidebar; if ( sidebar.wordId === srcRef.wordId && sidebar.mergeSenseId === srcRef.mergeSenseId && srcRef.order === undefined ) { - sidebar = defaultSidebar; + state.tree.sidebar = defaultSidebar; } + }, + flagWordAction: (state, action) => { + state.tree.words[action.payload.wordId].flag = action.payload.flag; + }, + moveSenseAction: (state, action) => { + if (action.payload.ref.order === undefined) { + // MOVE_SENSE, + const srcWordId = action.payload.ref.wordId; + const mergeSenseId = action.payload.ref.mergeSenseId; + const destWordId = action.payload.destWordId; - return { ...state, tree: { ...state.tree, words, sidebar } }; - } + if (srcWordId === destWordId) { + return; + } + const words = state.tree.words; + + // Check if dropping the sense into a new word. + if (words[destWordId] === undefined) { + if (Object.keys(words[srcWordId].sensesGuids).length === 1) { + return state; + } + words[destWordId] = newMergeTreeWord(); + } - case MergeTreeActionTypes.FLAG_WORD: { - const words: Hash = JSON.parse( - JSON.stringify(state.tree.words) - ); - words[action.payload.wordId].flag = action.payload.flag; - return { ...state, tree: { ...state.tree, words } }; - } + // Update the destWord. + const guids = words[srcWordId].sensesGuids[mergeSenseId]; + const sensesPairs = Object.entries(words[destWordId].sensesGuids); + sensesPairs.splice(action.payload.destOrder, 0, [mergeSenseId, guids]); + const newSensesGuids: Hash = {}; + sensesPairs.forEach(([key, value]) => (newSensesGuids[key] = value)); + words[destWordId].sensesGuids = newSensesGuids; - case MergeTreeActionTypes.MOVE_DUPLICATE: { + // Cleanup the srcWord. + delete words[srcWordId].sensesGuids[mergeSenseId]; + if (!Object.keys(words[srcWordId].sensesGuids).length) { + delete words[srcWordId]; + } + } + }, + moveDuplicateAction: (state, action) => { const srcRef = action.payload.ref; + // Verify that the ref.order field is defined + if (srcRef.order === undefined) { + return; + } const destWordId = action.payload.destWordId; - const words: Hash = JSON.parse( - JSON.stringify(state.tree.words) - ); + const words = state.tree.words; const srcWordId = srcRef.wordId; let mergeSenseId = srcRef.mergeSenseId; // Get guid of sense being restored from the sidebar. - if (srcRef.order === undefined) { - return state; - } const srcGuids = words[srcWordId].sensesGuids[mergeSenseId]; const guid = srcGuids.splice(srcRef.order, 1)[0]; @@ -130,7 +188,7 @@ export const mergeDupStepReducer = ( if (srcGuids.length === 0) { // If there are no guids left, this is a full move. if (srcWordId === destWordId) { - return state; + return; } delete words[srcWordId].sensesGuids[mergeSenseId]; if (!Object.keys(words[srcWordId].sensesGuids).length) { @@ -147,48 +205,8 @@ export const mergeDupStepReducer = ( const newSensesGuids: Hash = {}; sensesPairs.forEach(([key, value]) => (newSensesGuids[key] = value)); words[destWordId].sensesGuids = newSensesGuids; - - return { ...state, tree: { ...state.tree, words } }; - } - - case MergeTreeActionTypes.MOVE_SENSE: { - const srcWordId = action.payload.wordId; - const mergeSenseId = action.payload.mergeSenseId; - const destWordId = action.payload.destWordId; - - if (srcWordId === destWordId) { - return state; - } - const words: Hash = JSON.parse( - JSON.stringify(state.tree.words) - ); - - // Check if dropping the sense into a new word. - if (words[destWordId] === undefined) { - if (Object.keys(words[srcWordId].sensesGuids).length === 1) { - return state; - } - words[destWordId] = newMergeTreeWord(); - } - - // Update the destWord. - const guids = [...words[srcWordId].sensesGuids[mergeSenseId]]; - const sensesPairs = Object.entries(words[destWordId].sensesGuids); - sensesPairs.splice(action.payload.destOrder, 0, [mergeSenseId, guids]); - const newSensesGuids: Hash = {}; - sensesPairs.forEach(([key, value]) => (newSensesGuids[key] = value)); - words[destWordId].sensesGuids = newSensesGuids; - - // Cleanup the srcWord. - delete words[srcWordId].sensesGuids[mergeSenseId]; - if (!Object.keys(words[srcWordId].sensesGuids).length) { - delete words[srcWordId]; - } - - return { ...state, tree: { ...state.tree, words } }; - } - - case MergeTreeActionTypes.ORDER_DUPLICATE: { + }, + orderDuplicateAction: (state, action) => { const ref = action.payload.ref; const oldOrder = ref.order; @@ -196,7 +214,7 @@ export const mergeDupStepReducer = ( // Ensure the reorder is valid. if (oldOrder === undefined || oldOrder === newOrder) { - return state; + return; } // Move the guid. @@ -205,25 +223,13 @@ export const mergeDupStepReducer = ( const guid = guids.splice(oldOrder, 1)[0]; guids.splice(newOrder, 0, guid); - // const sensesGuids = { ...oldSensesGuids }; sensesGuids[ref.mergeSenseId] = guids; - const word: MergeTreeWord = { - ...state.tree.words[ref.wordId], - sensesGuids, - }; - - const words = { ...state.tree.words }; - words[ref.wordId] = word; - - return { ...state, tree: { ...state.tree, words } }; - } - - case MergeTreeActionTypes.ORDER_SENSE: { - const word: MergeTreeWord = JSON.parse( - JSON.stringify(state.tree.words[action.payload.wordId]) - ); + state.tree.words[ref.wordId].sensesGuids = sensesGuids; + }, + orderSenseAction: (state, action) => { + const word = state.tree.words[action.payload.wordId]; // Convert the Hash to an array to expose the order. const sensePairs = Object.entries(word.sensesGuids); @@ -234,7 +240,7 @@ export const mergeDupStepReducer = ( // Ensure the move is valid. if (oldOrder === -1 || newOrder === undefined || oldOrder === newOrder) { - return state; + return; } // Move the sense pair to its new place. @@ -247,54 +253,46 @@ export const mergeDupStepReducer = ( word.sensesGuids[key] = value; } - const words = { ...state.tree.words }; - words[action.payload.wordId] = word; - - return { ...state, tree: { ...state.tree, words } }; - } - - case MergeTreeActionTypes.SET_DATA: { + state.tree.words[action.payload.wordId] = word; + }, + setSidebarAction: (state, action) => {}, + setWordDataAction: (state, action) => { if (action.payload.length === 0) { - return defaultState; - } - const words: Hash = {}; - const senses: Hash = {}; - const wordsTree: Hash = {}; - action.payload.forEach((word) => { - words[word.id] = JSON.parse(JSON.stringify(word)); - word.senses.forEach((s, order) => { - senses[s.guid] = convertSenseToMergeTreeSense(s, word.id, order); + state = defaultState; + } else { + const words: Hash = {}; + const senses: Hash = {}; + const wordsTree: Hash = {}; + action.payload.forEach((word: Word) => { + words[word.id] = JSON.parse(JSON.stringify(word)); + word.senses.forEach((s, order) => { + senses[s.guid] = convertSenseToMergeTreeSense(s, word.id, order); + }); + wordsTree[word.id] = convertWordToMergeTreeWord(word); }); - wordsTree[word.id] = convertWordToMergeTreeWord(word); - }); - return { - ...state, - tree: { ...state.tree, words: wordsTree }, - data: { senses, words }, - }; - } - - case MergeTreeActionTypes.SET_SIDEBAR: { - const sidebar = action.payload; - return { ...state, tree: { ...state.tree, sidebar } }; - } - - case MergeTreeActionTypes.SET_VERNACULAR: { - const word = { ...state.tree.words[action.payload.wordId] }; - word.vern = action.payload.vern; - - const words = { ...state.tree.words }; - words[action.payload.wordId] = word; - - return { ...state, tree: { ...state.tree, words } }; - } - - case StoreActionTypes.RESET: { - return defaultState; - } - - default: { - return state; - } - } -}; + state.tree.words = wordsTree; + state.data = { senses, words }; + } + }, + setVernacularAction: (state, action) => {}, + }, + extraReducers: (builder) => + builder.addCase(StoreActionTypes.RESET, () => defaultState), +}); + +export const { + clearTreeAction, + combineIntoFirstSenseAction, + combineSenseAction, + deleteSenseAction, + flagWordAction, + moveDuplicateAction, + moveSenseAction, + orderDuplicateAction, + orderSenseAction, + setSidebarAction, + setWordDataAction, + setVernacularAction, +} = mergeDupStepSlice.actions; + +export default mergeDupStepSlice.reducer; diff --git a/src/goals/MergeDuplicates/Redux/MergeDupsReduxTypes.ts b/src/goals/MergeDuplicates/Redux/MergeDupsReduxTypes.ts index b679ec586c..3087a79ed3 100644 --- a/src/goals/MergeDuplicates/Redux/MergeDupsReduxTypes.ts +++ b/src/goals/MergeDuplicates/Redux/MergeDupsReduxTypes.ts @@ -1,23 +1,18 @@ -import { Flag, Word } from "api/models"; +import { Flag } from "api/models"; import { MergeData, MergeTree, MergeTreeReference, - Sidebar, } from "goals/MergeDuplicates/MergeDupsTreeTypes"; -export enum MergeTreeActionTypes { - CLEAR_TREE = "CLEAR_TREE", - COMBINE_SENSE = "COMBINE_SENSE", - DELETE_SENSE = "DELETE_SENSE", - FLAG_WORD = "FLAG_WORD", - MOVE_DUPLICATE = "MOVE_DUPLICATE", - MOVE_SENSE = "MOVE_SENSE", - ORDER_DUPLICATE = "ORDER_DUPLICATE", - ORDER_SENSE = "ORDER_SENSE", - SET_DATA = "SET_DATA", - SET_SIDEBAR = "SET_SIDEBAR", - SET_VERNACULAR = "SET_VERNACULAR", +export interface CombineSenseMergePayload { + src: MergeTreeReference; + dest: MergeTreeReference; +} + +export interface FlagWordPayload { + wordId: string; + flag: Flag; } export interface MergeTreeState { @@ -25,74 +20,18 @@ export interface MergeTreeState { tree: MergeTree; } -export interface ClearTreeMergeAction { - type: MergeTreeActionTypes.CLEAR_TREE; -} - -export interface CombineSenseMergeAction { - type: MergeTreeActionTypes.COMBINE_SENSE; - payload: { src: MergeTreeReference; dest: MergeTreeReference }; -} - -export interface DeleteSenseMergeAction { - type: MergeTreeActionTypes.DELETE_SENSE; - payload: { src: MergeTreeReference }; -} - -export interface FlagWord { - type: MergeTreeActionTypes.FLAG_WORD; - payload: { wordId: string; flag: Flag }; -} - -export interface MoveDuplicateMergeAction { - type: MergeTreeActionTypes.MOVE_DUPLICATE; - payload: { ref: MergeTreeReference; destWordId: string; destOrder: number }; +export interface MoveSensePayload { + ref: MergeTreeReference; + destWordId: string; + destOrder: number; } -export interface MoveSenseMergeAction { - type: MergeTreeActionTypes.MOVE_SENSE; - payload: { - wordId: string; - mergeSenseId: string; - destWordId: string; - destOrder: number; - }; +export interface OrderSensePayload { + ref: MergeTreeReference; + order: number; } -export interface OrderDuplicateMergeAction { - type: MergeTreeActionTypes.ORDER_DUPLICATE; - payload: { ref: MergeTreeReference; order: number }; +export interface SetVernacularPayload { + wordId: string; + vern: string; } - -export interface OrderSenseMergeAction { - type: MergeTreeActionTypes.ORDER_SENSE; - payload: MergeTreeReference; -} - -export interface SetDataMergeAction { - type: MergeTreeActionTypes.SET_DATA; - payload: Word[]; -} - -export interface SetSidebarMergeAction { - type: MergeTreeActionTypes.SET_SIDEBAR; - payload: Sidebar; -} - -export interface SetVernacularMergeAction { - type: MergeTreeActionTypes.SET_VERNACULAR; - payload: { wordId: string; vern: string }; -} - -export type MergeTreeAction = - | ClearTreeMergeAction - | CombineSenseMergeAction - | DeleteSenseMergeAction - | FlagWord - | MoveDuplicateMergeAction - | MoveSenseMergeAction - | OrderDuplicateMergeAction - | OrderSenseMergeAction - | SetDataMergeAction - | SetSidebarMergeAction - | SetVernacularMergeAction; diff --git a/src/rootReducer.ts b/src/rootReducer.ts index dabea8c886..5926c3ed82 100644 --- a/src/rootReducer.ts +++ b/src/rootReducer.ts @@ -8,7 +8,7 @@ import { createProjectReducer } from "components/ProjectScreen/CreateProject/Red import { pronunciationsReducer } from "components/Pronunciations/Redux/PronunciationsReducer"; import { treeViewReducer } from "components/TreeView/Redux/TreeViewReducer"; import { characterInventoryReducer } from "goals/CharacterInventory/Redux/CharacterInventoryReducer"; -import { mergeDupStepReducer } from "goals/MergeDuplicates/Redux/MergeDupsReducer"; +import mergeDupStepReducer from "goals/MergeDuplicates/Redux/MergeDupsReducer"; import { reviewEntriesReducer } from "goals/ReviewEntries/ReviewEntriesComponent/Redux/ReviewEntriesReducer"; import { StoreState } from "types"; import { analyticsReducer } from "types/Redux/analytics"; From 078a7260b9ae53fde52eed429df94163253fe68c Mon Sep 17 00:00:00 2001 From: Jim Grady Date: Mon, 9 Oct 2023 09:32:05 -0400 Subject: [PATCH 02/36] Complete update of MergeDups actions and reducer --- .../MergeDuplicates/Redux/MergeDupsActions.ts | 60 ------------------- .../MergeDuplicates/Redux/MergeDupsReducer.ts | 19 ++++-- 2 files changed, 13 insertions(+), 66 deletions(-) diff --git a/src/goals/MergeDuplicates/Redux/MergeDupsActions.ts b/src/goals/MergeDuplicates/Redux/MergeDupsActions.ts index 61c935569e..2aa645ea83 100644 --- a/src/goals/MergeDuplicates/Redux/MergeDupsActions.ts +++ b/src/goals/MergeDuplicates/Redux/MergeDupsActions.ts @@ -1,14 +1,6 @@ import { Action, PayloadAction } from "@reduxjs/toolkit"; -import { - Flag, - GramCatGroup, - MergeSourceWord, - MergeWords, import { MergeSourceWord, MergeWords, Status, Word } from "api/models"; - Status, - Word, -} from "api/models"; import * as backend from "backend"; import { addCompletedMergeToGoal, @@ -276,55 +268,3 @@ export async function fetchMergeDupsData( ): Promise { return await backend.getDuplicates(maxInList, maxLists); } - -/** Modifies the mutable input sense list. */ -export function combineIntoFirstSense(senses: MergeTreeSense[]): void { - // Set the first sense to be merged as Active/Protected. - // This was the top sense when the sidebar was opened. - const mainSense = senses[0]; - mainSense.accessibility = mainSense.protected - ? Status.Protected - : Status.Active; - - // Merge the rest as duplicates. - // These were senses dropped into another sense. - senses.slice(1).forEach((dupSense) => { - dupSense.accessibility = Status.Duplicate; - // Put the duplicate's definitions in the main sense. - dupSense.definitions.forEach((def) => - mergeDefinitionIntoSense(mainSense, def) - ); - // Use the duplicate's part of speech if not specified in the main sense. - if (mainSense.grammaticalInfo.catGroup === GramCatGroup.Unspecified) { - mainSense.grammaticalInfo = { ...dupSense.grammaticalInfo }; - } - // Put the duplicate's domains in the main sense. - dupSense.semanticDomains.forEach((dom) => { - if (!mainSense.semanticDomains.find((d) => d.id === dom.id)) { - mainSense.semanticDomains.push({ ...dom }); - } - }); - }); -} - -/** Modifies the mutable input sense. */ -export function mergeDefinitionIntoSense( - sense: MergeTreeSense, - def: Definition, - sep = ";" -): void { - if (!def.text.length) { - return; - } - const defIndex = sense.definitions.findIndex( - (d) => d.language === def.language - ); - if (defIndex === -1) { - sense.definitions.push({ ...def }); - } else { - const oldText = sense.definitions[defIndex].text; - if (!oldText.split(sep).includes(def.text)) { - sense.definitions[defIndex].text = `${oldText}${sep}${def.text}`; - } - } -} diff --git a/src/goals/MergeDuplicates/Redux/MergeDupsReducer.ts b/src/goals/MergeDuplicates/Redux/MergeDupsReducer.ts index b52fb4636d..22984be253 100644 --- a/src/goals/MergeDuplicates/Redux/MergeDupsReducer.ts +++ b/src/goals/MergeDuplicates/Redux/MergeDupsReducer.ts @@ -100,7 +100,7 @@ const mergeDupStepSlice = createSlice({ } }, deleteSenseAction: (state, action) => { - const srcRef = action.payload.src; + const srcRef = action.payload; const srcWordId = srcRef.wordId; const words = state.tree.words; @@ -229,12 +229,15 @@ const mergeDupStepSlice = createSlice({ state.tree.words[ref.wordId].sensesGuids = sensesGuids; }, orderSenseAction: (state, action) => { - const word = state.tree.words[action.payload.wordId]; + const word = state.tree.words[action.payload.ref.wordId]; + // const word: MergeTreeWord = JSON.parse( + // JSON.stringify(state.tree.words[action.payload.wordId]) + // ); // Convert the Hash to an array to expose the order. const sensePairs = Object.entries(word.sensesGuids); - const mergeSenseId = action.payload.mergeSenseId; + const mergeSenseId = action.payload.ref.mergeSenseId; const oldOrder = sensePairs.findIndex((p) => p[0] === mergeSenseId); const newOrder = action.payload.order; @@ -253,9 +256,11 @@ const mergeDupStepSlice = createSlice({ word.sensesGuids[key] = value; } - state.tree.words[action.payload.wordId] = word; + state.tree.words[action.payload.ref.wordId] = word; + }, + setSidebarAction: (state, action) => { + state.tree.sidebar = action.payload; }, - setSidebarAction: (state, action) => {}, setWordDataAction: (state, action) => { if (action.payload.length === 0) { state = defaultState; @@ -274,7 +279,9 @@ const mergeDupStepSlice = createSlice({ state.data = { senses, words }; } }, - setVernacularAction: (state, action) => {}, + setVernacularAction: (state, action) => { + state.tree.words[action.payload.wordId].vern = action.payload.vern; + }, }, extraReducers: (builder) => builder.addCase(StoreActionTypes.RESET, () => defaultState), From e405255f1d7e2fd46baca6f92e7ac284eab117b0 Mon Sep 17 00:00:00 2001 From: Jim Grady Date: Wed, 11 Oct 2023 10:52:40 -0400 Subject: [PATCH 03/36] WIP: Update merge dups tests --- .../MergeDuplicates/Redux/MergeDupsReducer.ts | 2 +- .../Redux/tests/MergeDups.test.tsx | 21 +++ .../Redux/tests/MergeDupsActions.test.tsx | 129 +++++++++--------- .../Redux/tests/MergeDupsReducer.test.tsx | 6 +- 4 files changed, 90 insertions(+), 68 deletions(-) create mode 100644 src/goals/MergeDuplicates/Redux/tests/MergeDups.test.tsx diff --git a/src/goals/MergeDuplicates/Redux/MergeDupsReducer.ts b/src/goals/MergeDuplicates/Redux/MergeDupsReducer.ts index 22984be253..98d10cd5ac 100644 --- a/src/goals/MergeDuplicates/Redux/MergeDupsReducer.ts +++ b/src/goals/MergeDuplicates/Redux/MergeDupsReducer.ts @@ -144,7 +144,7 @@ const mergeDupStepSlice = createSlice({ // Check if dropping the sense into a new word. if (words[destWordId] === undefined) { if (Object.keys(words[srcWordId].sensesGuids).length === 1) { - return state; + return; } words[destWordId] = newMergeTreeWord(); } diff --git a/src/goals/MergeDuplicates/Redux/tests/MergeDups.test.tsx b/src/goals/MergeDuplicates/Redux/tests/MergeDups.test.tsx new file mode 100644 index 0000000000..e04c9a86f4 --- /dev/null +++ b/src/goals/MergeDuplicates/Redux/tests/MergeDups.test.tsx @@ -0,0 +1,21 @@ +import "@testing-library/jest-dom"; +import { act, cleanup } from "@testing-library/react"; + +import MergeDupsStep from "goals/MergeDuplicates/MergeDupsStep"; +import { setupStore } from "store"; +import { renderWithProviders } from "utilities/testUtilities"; + +beforeEach(() => { + jest.clearAllMocks(); +}); + +afterEach(cleanup); + +describe("Render MergeDups", () => { + it("Renders MergeDups with no errors", async () => { + const store= setupStore(); + await act(async () => { + renderWithProviders(, {store: store}); + }); + }); +}); diff --git a/src/goals/MergeDuplicates/Redux/tests/MergeDupsActions.test.tsx b/src/goals/MergeDuplicates/Redux/tests/MergeDupsActions.test.tsx index e026ecf36e..89886ed41c 100644 --- a/src/goals/MergeDuplicates/Redux/tests/MergeDupsActions.test.tsx +++ b/src/goals/MergeDuplicates/Redux/tests/MergeDupsActions.test.tsx @@ -16,27 +16,23 @@ import { combineIntoFirstSense, dispatchMergeStepData, mergeAll, - mergeDefinitionIntoSense, moveSense, orderSense, + setWordData, } from "goals/MergeDuplicates/Redux/MergeDupsActions"; -import { - MergeTreeAction, - MergeTreeActionTypes, - MergeTreeState, -} from "goals/MergeDuplicates/Redux/MergeDupsReduxTypes"; +import { MergeTreeState } from "goals/MergeDuplicates/Redux/MergeDupsReduxTypes"; import { goalDataMock } from "goals/MergeDuplicates/Redux/tests/MergeDupsDataMock"; +import { setupStore } from "store"; import { GoalsState, GoalType } from "types/goals"; import { newSemanticDomain } from "types/semanticDomain"; import { multiSenseWord, - newDefinition, newFlag, newGrammaticalInfo, newSense, newWord, } from "types/word"; -import { Bcp47Code } from "types/writingSystem"; +// import { Bcp47Code } from "types/writingSystem"; // Used when the guids don't matter. function wordAnyGuids(vern: string, senses: Sense[], id: string): Word { @@ -252,33 +248,36 @@ describe("MergeDupActions", () => { describe("dispatchMergeStepData", () => { it("creates an action to add MergeDups data", async () => { + const store = setupStore(); const goal = new MergeDups(); goal.steps = [{ words: [...goalDataMock.plannedWords[0]] }]; - const mockStore = createMockStore(); - await mockStore.dispatch(dispatchMergeStepData(goal)); - const setWordData: MergeTreeAction = { - type: MergeTreeActionTypes.SET_DATA, - payload: [...goalDataMock.plannedWords[0]], - }; - expect(mockStore.getActions()).toEqual([setWordData]); + await store.dispatch(dispatchMergeStepData(goal)); + store.dispatch(setWordData([...goalDataMock.plannedWords[0]])); + expect(store.getActions()).toEqual([setWordData]); }); }); describe("moveSense", () => { const wordId = "mockWordId"; const mergeSenseId = "mockSenseId"; + const store = setupStore(); it("creates a MOVE_SENSE action when going from word to word", () => { const mockRef: MergeTreeReference = { wordId, mergeSenseId }; - const resultAction = moveSense(mockRef, wordId, -1); - expect(resultAction.type).toEqual(MergeTreeActionTypes.MOVE_SENSE); + store.dispatch( + moveSense({ ref: mockRef, destWordId: wordId, destOrder: -1 }) + ); + const destWordRef = store.getState().mergeDuplicateGoal.tree.words[wordId]; + // expect(destWordRef.).toEqual(MergeTreeActionTypes.MOVE_SENSE); }); it("creates a MOVE_DUPLICATE action when going from sidebar to word", () => { const mockRef: MergeTreeReference = { wordId, mergeSenseId, order: 0 }; - const resultAction = moveSense(mockRef, wordId, -1); - expect(resultAction.type).toEqual(MergeTreeActionTypes.MOVE_DUPLICATE); + store.dispatch( + moveSense({ ref: mockRef, destWordId: wordId, destOrder: -1 }) + ); + // expect(resultAction.type).toEqual(MergeTreeActionTypes.MOVE_DUPLICATE); }); }); @@ -300,52 +299,52 @@ describe("MergeDupActions", () => { }); }); - describe("mergeDefinitionIntoSense", () => { - const defAEn = newDefinition("a", Bcp47Code.En); - const defAFr = newDefinition("a", Bcp47Code.Fr); - const defBEn = newDefinition("b", Bcp47Code.En); - let sense: MergeTreeSense; - - beforeEach(() => { - sense = newSense() as MergeTreeSense; - }); - - it("ignores definitions with empty text", () => { - mergeDefinitionIntoSense(sense, newDefinition()); - expect(sense.definitions).toHaveLength(0); - mergeDefinitionIntoSense(sense, newDefinition("", Bcp47Code.En)); - expect(sense.definitions).toHaveLength(0); - }); - - it("adds definitions with new languages", () => { - mergeDefinitionIntoSense(sense, defAEn); - expect(sense.definitions).toHaveLength(1); - mergeDefinitionIntoSense(sense, defAFr); - expect(sense.definitions).toHaveLength(2); - }); - - it("only adds definitions with new text", () => { - sense.definitions.push({ ...defAEn }, { ...defAFr }); - - mergeDefinitionIntoSense(sense, defAFr); - expect(sense.definitions).toHaveLength(2); - expect( - sense.definitions.find((d) => d.language === Bcp47Code.Fr)!.text - ).toEqual(defAFr.text); - - const twoEnTexts = `${defAEn.text};${defBEn.text}`; - mergeDefinitionIntoSense(sense, defBEn); - expect(sense.definitions).toHaveLength(2); - expect( - sense.definitions.find((d) => d.language === Bcp47Code.En)!.text - ).toEqual(twoEnTexts); - mergeDefinitionIntoSense(sense, defAEn); - expect(sense.definitions).toHaveLength(2); - expect( - sense.definitions.find((d) => d.language === Bcp47Code.En)!.text - ).toEqual(twoEnTexts); - }); - }); + // describe("mergeDefinitionIntoSense", () => { + // const defAEn = newDefinition("a", Bcp47Code.En); + // const defAFr = newDefinition("a", Bcp47Code.Fr); + // const defBEn = newDefinition("b", Bcp47Code.En); + // let sense: MergeTreeSense; + + // beforeEach(() => { + // sense = newSense() as MergeTreeSense; + // }); + + // it("ignores definitions with empty text", () => { + // mergeDefinitionIntoSense(sense, newDefinition()); + // expect(sense.definitions).toHaveLength(0); + // mergeDefinitionIntoSense(sense, newDefinition("", Bcp47Code.En)); + // expect(sense.definitions).toHaveLength(0); + // }); + + // it("adds definitions with new languages", () => { + // mergeDefinitionIntoSense(sense, defAEn); + // expect(sense.definitions).toHaveLength(1); + // mergeDefinitionIntoSense(sense, defAFr); + // expect(sense.definitions).toHaveLength(2); + // }); + + // it("only adds definitions with new text", () => { + // sense.definitions.push({ ...defAEn }, { ...defAFr }); + + // mergeDefinitionIntoSense(sense, defAFr); + // expect(sense.definitions).toHaveLength(2); + // expect( + // sense.definitions.find((d) => d.language === Bcp47Code.Fr)!.text + // ).toEqual(defAFr.text); + + // const twoEnTexts = `${defAEn.text};${defBEn.text}`; + // mergeDefinitionIntoSense(sense, defBEn); + // expect(sense.definitions).toHaveLength(2); + // expect( + // sense.definitions.find((d) => d.language === Bcp47Code.En)!.text + // ).toEqual(twoEnTexts); + // mergeDefinitionIntoSense(sense, defAEn); + // expect(sense.definitions).toHaveLength(2); + // expect( + // sense.definitions.find((d) => d.language === Bcp47Code.En)!.text + // ).toEqual(twoEnTexts); + // }); + // }); describe("combineIntoFirstSense", () => { it("sets all but the first sense to duplicate status", () => { diff --git a/src/goals/MergeDuplicates/Redux/tests/MergeDupsReducer.test.tsx b/src/goals/MergeDuplicates/Redux/tests/MergeDupsReducer.test.tsx index 67ad92c75d..ad29686248 100644 --- a/src/goals/MergeDuplicates/Redux/tests/MergeDupsReducer.test.tsx +++ b/src/goals/MergeDuplicates/Redux/tests/MergeDupsReducer.test.tsx @@ -6,9 +6,8 @@ import { newMergeTreeWord, } from "goals/MergeDuplicates/MergeDupsTreeTypes"; import * as Actions from "goals/MergeDuplicates/Redux/MergeDupsActions"; -import { +import mergeDupStepReducer, { defaultState, - mergeDupStepReducer, } from "goals/MergeDuplicates/Redux/MergeDupsReducer"; import { MergeTreeAction, @@ -34,8 +33,11 @@ function getMockUuid(increment = true): string { beforeEach(() => { mockUuid.v4.mockImplementation(getMockUuid); + jest.clearAllMocks(); }); +afterEach(cleanup); + describe("MergeDupReducer", () => { // a state with no duplicate senses const initState = mergeDupStepReducer( From 98100c4a9a9a1c2f60e199e1f2378f7e94cb7deb Mon Sep 17 00:00:00 2001 From: Jim Grady Date: Thu, 12 Oct 2023 10:19:29 -0400 Subject: [PATCH 04/36] Clear type errors --- .../Redux/tests/MergeDupsActions.test.tsx | 35 +++---- .../Redux/tests/MergeDupsReducer.test.tsx | 96 +++++++++++-------- 2 files changed, 75 insertions(+), 56 deletions(-) diff --git a/src/goals/MergeDuplicates/Redux/tests/MergeDupsActions.test.tsx b/src/goals/MergeDuplicates/Redux/tests/MergeDupsActions.test.tsx index 89886ed41c..caf09d88ec 100644 --- a/src/goals/MergeDuplicates/Redux/tests/MergeDupsActions.test.tsx +++ b/src/goals/MergeDuplicates/Redux/tests/MergeDupsActions.test.tsx @@ -20,19 +20,23 @@ import { orderSense, setWordData, } from "goals/MergeDuplicates/Redux/MergeDupsActions"; -import { MergeTreeState } from "goals/MergeDuplicates/Redux/MergeDupsReduxTypes"; +import { + MergeTreeAction, + MergeTreeActionTypes, + MergeTreeState, +} from "goals/MergeDuplicates/Redux/MergeDupsReduxTypes"; import { goalDataMock } from "goals/MergeDuplicates/Redux/tests/MergeDupsDataMock"; -import { setupStore } from "store"; import { GoalsState, GoalType } from "types/goals"; import { newSemanticDomain } from "types/semanticDomain"; import { multiSenseWord, + newDefinition, newFlag, newGrammaticalInfo, newSense, newWord, } from "types/word"; -// import { Bcp47Code } from "types/writingSystem"; +import { Bcp47Code } from "types/writingSystem"; // Used when the guids don't matter. function wordAnyGuids(vern: string, senses: Sense[], id: string): Word { @@ -248,36 +252,33 @@ describe("MergeDupActions", () => { describe("dispatchMergeStepData", () => { it("creates an action to add MergeDups data", async () => { - const store = setupStore(); const goal = new MergeDups(); goal.steps = [{ words: [...goalDataMock.plannedWords[0]] }]; - await store.dispatch(dispatchMergeStepData(goal)); - store.dispatch(setWordData([...goalDataMock.plannedWords[0]])); - expect(store.getActions()).toEqual([setWordData]); + const mockStore = createMockStore(); + await mockStore.dispatch(dispatchMergeStepData(goal)); + const setWordData: MergeTreeAction = { + type: MergeTreeActionTypes.SET_DATA, + payload: [...goalDataMock.plannedWords[0]], + }; + expect(mockStore.getActions()).toEqual([setWordData]); }); }); describe("moveSense", () => { const wordId = "mockWordId"; const mergeSenseId = "mockSenseId"; - const store = setupStore(); it("creates a MOVE_SENSE action when going from word to word", () => { const mockRef: MergeTreeReference = { wordId, mergeSenseId }; - store.dispatch( - moveSense({ ref: mockRef, destWordId: wordId, destOrder: -1 }) - ); - const destWordRef = store.getState().mergeDuplicateGoal.tree.words[wordId]; - // expect(destWordRef.).toEqual(MergeTreeActionTypes.MOVE_SENSE); + const resultAction = moveSense(mockRef, wordId, -1); + expect(resultAction.type).toEqual(MergeTreeActionTypes.MOVE_SENSE); }); it("creates a MOVE_DUPLICATE action when going from sidebar to word", () => { const mockRef: MergeTreeReference = { wordId, mergeSenseId, order: 0 }; - store.dispatch( - moveSense({ ref: mockRef, destWordId: wordId, destOrder: -1 }) - ); - // expect(resultAction.type).toEqual(MergeTreeActionTypes.MOVE_DUPLICATE); + const resultAction = moveSense(mockRef, wordId, -1); + expect(resultAction.type).toEqual(MergeTreeActionTypes.MOVE_DUPLICATE); }); }); diff --git a/src/goals/MergeDuplicates/Redux/tests/MergeDupsReducer.test.tsx b/src/goals/MergeDuplicates/Redux/tests/MergeDupsReducer.test.tsx index ad29686248..4dbbd45322 100644 --- a/src/goals/MergeDuplicates/Redux/tests/MergeDupsReducer.test.tsx +++ b/src/goals/MergeDuplicates/Redux/tests/MergeDupsReducer.test.tsx @@ -1,3 +1,5 @@ +import { Action, PayloadAction } from "@reduxjs/toolkit"; + import { convertSenseToMergeTreeSense, defaultSidebar, @@ -5,15 +7,20 @@ import { MergeTreeWord, newMergeTreeWord, } from "goals/MergeDuplicates/MergeDupsTreeTypes"; -import * as Actions from "goals/MergeDuplicates/Redux/MergeDupsActions"; +import { + clearTree, + combineSense, + deleteSense, + flagWord, + moveSense, + orderSense, + setWordData, +} from "goals/MergeDuplicates/Redux/MergeDupsActions"; import mergeDupStepReducer, { + moveSenseAction, defaultState, } from "goals/MergeDuplicates/Redux/MergeDupsReducer"; -import { - MergeTreeAction, - MergeTreeActionTypes, - MergeTreeState, -} from "goals/MergeDuplicates/Redux/MergeDupsReduxTypes"; +import { MergeTreeState } from "goals/MergeDuplicates/Redux/MergeDupsReduxTypes"; import { StoreAction, StoreActionTypes } from "rootActions"; import { Hash } from "types/hash"; import { newFlag, testWordList } from "types/word"; @@ -33,17 +40,11 @@ function getMockUuid(increment = true): string { beforeEach(() => { mockUuid.v4.mockImplementation(getMockUuid); - jest.clearAllMocks(); }); -afterEach(cleanup); - describe("MergeDupReducer", () => { // a state with no duplicate senses - const initState = mergeDupStepReducer( - undefined, - Actions.setWordData(testWordList()) - ); + const initState = mergeDupStepReducer(undefined, setWordData(testWordList())); // helper functions for working with a tree const getRefByGuid = ( @@ -63,7 +64,7 @@ describe("MergeDupReducer", () => { }; test("clearTree", () => { - const newState = mergeDupStepReducer(initState, Actions.clearTree()); + const newState = mergeDupStepReducer(initState, clearTree()); expect(JSON.stringify(newState)).toEqual(JSON.stringify(defaultState)); }); @@ -92,7 +93,7 @@ describe("MergeDupReducer", () => { }, }; function checkTreeWords( - action: MergeTreeAction, + action: Action | PayloadAction, expected: Hash ) { const result = mergeDupStepReducer(mockState, action).tree.words; @@ -116,7 +117,7 @@ describe("MergeDupReducer", () => { mergeSenseId: `${destWordId}_senseA`, }; - const testAction = Actions.combineSense(srcRef, destRef); + const testAction = combineSense({ src: srcRef, dest: destRef }); const expectedWords = testTreeWords(); expectedWords[srcWordId].sensesGuids = { @@ -143,7 +144,7 @@ describe("MergeDupReducer", () => { mergeSenseId: `${destWordId}_senseA`, }; - const testAction = Actions.combineSense(srcRef, destRef); + const testAction = combineSense({ src: srcRef, dest: destRef }); const expectedWords = testTreeWords(); expectedWords[destWordId].sensesGuids = { @@ -170,7 +171,7 @@ describe("MergeDupReducer", () => { mergeSenseId: `${destWordId}_senseA`, }; - const testAction = Actions.combineSense(srcRef, destRef); + const testAction = combineSense({ src: srcRef, dest: destRef }); const expectedWords = testTreeWords(); delete expectedWords[srcWordId]; @@ -193,7 +194,7 @@ describe("MergeDupReducer", () => { mergeSenseId: `${wordId}_senseA`, }; - const testAction = Actions.combineSense(srcRef, destRef); + const testAction = combineSense({ src: srcRef, dest: destRef }); const expectedWords = testTreeWords(); expectedWords[wordId].sensesGuids = { @@ -216,7 +217,7 @@ describe("MergeDupReducer", () => { mergeSenseId: `${destWordId}_senseA`, }; - const testAction = Actions.combineSense(srcRef, destRef); + const testAction = combineSense({ src: srcRef, dest: destRef }); const expectedWords = testTreeWords(); expectedWords[srcWordId].sensesGuids = { @@ -242,7 +243,7 @@ describe("MergeDupReducer", () => { mergeSenseId: `${destWordId}_senseA`, }; - const testAction = Actions.combineSense(srcRef, destRef); + const testAction = combineSense({ src: srcRef, dest: destRef }); const expectedWords = testTreeWords(); delete expectedWords[srcWordId]; @@ -263,7 +264,7 @@ describe("MergeDupReducer", () => { mergeSenseId: `${wordId}_senseA`, }; - const testAction = Actions.deleteSense(testRef); + const testAction = deleteSense(testRef); const expectedWords = testTreeWords(); delete expectedWords[wordId].sensesGuids[testRef.mergeSenseId]; @@ -278,7 +279,7 @@ describe("MergeDupReducer", () => { mergeSenseId: `${wordId}_senseB`, }; - const testAction = Actions.deleteSense(testRef); + const testAction = deleteSense(testRef); const expectedWords = testTreeWords(); delete expectedWords[wordId].sensesGuids[testRef.mergeSenseId]; @@ -293,7 +294,7 @@ describe("MergeDupReducer", () => { mergeSenseId: `${wordId}_senseA`, }; - const testAction = Actions.deleteSense(testRef); + const testAction = deleteSense(testRef); const expectedWords = testTreeWords(); delete expectedWords[wordId]; @@ -309,7 +310,7 @@ describe("MergeDupReducer", () => { order: 0, }; - const testAction = Actions.deleteSense(testRef); + const testAction = deleteSense(testRef); const expectedWords = testTreeWords(); expectedWords[wordId].sensesGuids = { word2_senseA: ["word2_senseA_1"] }; @@ -325,7 +326,7 @@ describe("MergeDupReducer", () => { order: 0, }; - const testAction = Actions.deleteSense(srcRef); + const testAction = deleteSense(srcRef); const expectedWords = testTreeWords(); delete expectedWords[srcWordId]; @@ -338,7 +339,7 @@ describe("MergeDupReducer", () => { it("adds a flag to a word", () => { const wordId = "word1"; const testFlag = newFlag("flagged"); - const testAction = Actions.flagWord(wordId, testFlag); + const testAction = flagWord({ wordId: wordId, flag: testFlag }); const expectedWords = testTreeWords(); expectedWords[wordId].flag = testFlag; @@ -359,7 +360,11 @@ describe("MergeDupReducer", () => { // Intercept the uuid that will be assigned. const nextGuid = getMockUuid(false); - const testAction = Actions.moveSense(testRef, wordId, 1); + const testAction = moveSense({ + ref: testRef, + destWordId: wordId, + destOrder: 1, + }); const expectedWords = testTreeWords(); expectedWords[wordId].sensesGuids = { word2_senseA: ["word2_senseA_1"] }; @@ -383,7 +388,11 @@ describe("MergeDupReducer", () => { // Intercept the uuid that will be assigned. const nextGuid = getMockUuid(false); - const testAction = Actions.moveSense(testRef, destWordId, 2); + const testAction = moveSense({ + ref: testRef, + destWordId: destWordId, + destOrder: 2, + }); const expectedWords = testTreeWords(); expectedWords[srcWordId].sensesGuids = { @@ -406,7 +415,11 @@ describe("MergeDupReducer", () => { const destWordId = "word1"; - const testAction = Actions.moveSense(testRef, destWordId, 1); + const testAction = moveSense({ + ref: testRef, + destWordId: destWordId, + destOrder: 1, + }); const expectedWords = testTreeWords(); expectedWords[srcWordId].sensesGuids = { @@ -429,7 +442,11 @@ describe("MergeDupReducer", () => { const destWordId = "word1"; - const testAction = Actions.moveSense(testRef, destWordId, 1); + const testAction = moveSense({ + ref: testRef, + destWordId: destWordId, + destOrder: 1, + }); const expectedWords = testTreeWords(); expectedWords[srcWordId].sensesGuids = { @@ -452,8 +469,12 @@ describe("MergeDupReducer", () => { const destWordId = "word2"; - const testAction = Actions.moveSense(testRef, destWordId, 1); - expect(testAction.type).toEqual(MergeTreeActionTypes.MOVE_SENSE); + const testAction = moveSense({ + ref: testRef, + destWordId: destWordId, + destOrder: 1, + }); + expect(testAction.type).toEqual(moveSenseAction); const expectedWords = testTreeWords(); delete expectedWords[srcWordId]; @@ -472,7 +493,7 @@ describe("MergeDupReducer", () => { const mergeSenseId = `${wordId}_senseA`; const testRef: MergeTreeReference = { wordId, mergeSenseId, order: 0 }; - const testAction = Actions.orderSense(testRef, 1); + const testAction = orderSense({ ref: testRef, order: 1 }); const expectedWords = testTreeWords(); expectedWords[wordId].sensesGuids[mergeSenseId] = [ @@ -488,7 +509,7 @@ describe("MergeDupReducer", () => { const mergeSenseId = `${wordId}_senseA`; const testRef: MergeTreeReference = { wordId, mergeSenseId }; - const testAction = Actions.orderSense(testRef, 1); + const testAction = orderSense({ ref: testRef, order: 1 }); const expectedWords = testTreeWords(); expectedWords[wordId].sensesGuids = { @@ -512,10 +533,7 @@ describe("MergeDupReducer", () => { test("setWordData", () => { const wordList = testWordList(); - const treeState = mergeDupStepReducer( - undefined, - Actions.setWordData(wordList) - ); + const treeState = mergeDupStepReducer(undefined, setWordData(wordList)); // check if data has all words present for (const word of wordList) { const srcWordId = word.id; From e46901adaa8c9338de24bbc1657ed3efd0a80d2c Mon Sep 17 00:00:00 2001 From: Jim Grady Date: Thu, 12 Oct 2023 11:07:23 -0400 Subject: [PATCH 05/36] Fix test errors --- .../Redux/tests/MergeDups.test.tsx | 4 +- .../Redux/tests/MergeDupsActions.test.tsx | 43 +++++++++++-------- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/goals/MergeDuplicates/Redux/tests/MergeDups.test.tsx b/src/goals/MergeDuplicates/Redux/tests/MergeDups.test.tsx index e04c9a86f4..c5d7e1f19a 100644 --- a/src/goals/MergeDuplicates/Redux/tests/MergeDups.test.tsx +++ b/src/goals/MergeDuplicates/Redux/tests/MergeDups.test.tsx @@ -13,9 +13,9 @@ afterEach(cleanup); describe("Render MergeDups", () => { it("Renders MergeDups with no errors", async () => { - const store= setupStore(); + const store = setupStore(); await act(async () => { - renderWithProviders(, {store: store}); + renderWithProviders(, { store: store }); }); }); }); diff --git a/src/goals/MergeDuplicates/Redux/tests/MergeDupsActions.test.tsx b/src/goals/MergeDuplicates/Redux/tests/MergeDupsActions.test.tsx index caf09d88ec..0f4f44234c 100644 --- a/src/goals/MergeDuplicates/Redux/tests/MergeDupsActions.test.tsx +++ b/src/goals/MergeDuplicates/Redux/tests/MergeDupsActions.test.tsx @@ -21,22 +21,22 @@ import { setWordData, } from "goals/MergeDuplicates/Redux/MergeDupsActions"; import { - MergeTreeAction, - MergeTreeActionTypes, - MergeTreeState, -} from "goals/MergeDuplicates/Redux/MergeDupsReduxTypes"; + moveDuplicateAction, + moveSenseAction, + orderDuplicateAction, + orderSenseAction, +} from "goals/MergeDuplicates/Redux/MergeDupsReducer"; +import { MergeTreeState } from "goals/MergeDuplicates/Redux/MergeDupsReduxTypes"; import { goalDataMock } from "goals/MergeDuplicates/Redux/tests/MergeDupsDataMock"; import { GoalsState, GoalType } from "types/goals"; import { newSemanticDomain } from "types/semanticDomain"; import { multiSenseWord, - newDefinition, newFlag, newGrammaticalInfo, newSense, newWord, } from "types/word"; -import { Bcp47Code } from "types/writingSystem"; // Used when the guids don't matter. function wordAnyGuids(vern: string, senses: Sense[], id: string): Word { @@ -257,11 +257,8 @@ describe("MergeDupActions", () => { const mockStore = createMockStore(); await mockStore.dispatch(dispatchMergeStepData(goal)); - const setWordData: MergeTreeAction = { - type: MergeTreeActionTypes.SET_DATA, - payload: [...goalDataMock.plannedWords[0]], - }; - expect(mockStore.getActions()).toEqual([setWordData]); + const testAction = setWordData(goalDataMock.plannedWords[0]); + expect(mockStore.getActions()).toEqual([testAction]); }); }); @@ -271,14 +268,22 @@ describe("MergeDupActions", () => { it("creates a MOVE_SENSE action when going from word to word", () => { const mockRef: MergeTreeReference = { wordId, mergeSenseId }; - const resultAction = moveSense(mockRef, wordId, -1); - expect(resultAction.type).toEqual(MergeTreeActionTypes.MOVE_SENSE); + const resultAction = moveSense({ + ref: mockRef, + destWordId: wordId, + destOrder: -1, + }); + expect(resultAction.type).toEqual(moveSenseAction); }); it("creates a MOVE_DUPLICATE action when going from sidebar to word", () => { const mockRef: MergeTreeReference = { wordId, mergeSenseId, order: 0 }; - const resultAction = moveSense(mockRef, wordId, -1); - expect(resultAction.type).toEqual(MergeTreeActionTypes.MOVE_DUPLICATE); + const resultAction = moveSense({ + ref: mockRef, + destWordId: wordId, + destOrder: -1, + }); + expect(resultAction.type).toEqual(moveDuplicateAction); }); }); @@ -289,14 +294,14 @@ describe("MergeDupActions", () => { it("creates an ORDER_SENSE action when moving within a word", () => { const mockRef: MergeTreeReference = { wordId, mergeSenseId }; - const resultAction = orderSense(mockRef, mockOrder); - expect(resultAction.type).toEqual(MergeTreeActionTypes.ORDER_SENSE); + const resultAction = orderSense({ ref: mockRef, order: mockOrder }); + expect(resultAction.type).toEqual(orderSenseAction); }); it("creates an ORDER_DUPLICATE action when moving within the sidebar", () => { const mockRef: MergeTreeReference = { wordId, mergeSenseId, order: 0 }; - const resultAction = orderSense(mockRef, mockOrder); - expect(resultAction.type).toEqual(MergeTreeActionTypes.ORDER_DUPLICATE); + const resultAction = orderSense({ ref: mockRef, order: mockOrder }); + expect(resultAction.type).toEqual(orderDuplicateAction); }); }); From 1d42d5d33f9262285d0a8022ef71114c4f1ad51e Mon Sep 17 00:00:00 2001 From: Jim Grady Date: Thu, 12 Oct 2023 11:38:16 -0400 Subject: [PATCH 06/36] Fix lint warning --- src/goals/MergeDuplicates/Redux/MergeDupsReducer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/goals/MergeDuplicates/Redux/MergeDupsReducer.ts b/src/goals/MergeDuplicates/Redux/MergeDupsReducer.ts index 98d10cd5ac..4fc4591bb1 100644 --- a/src/goals/MergeDuplicates/Redux/MergeDupsReducer.ts +++ b/src/goals/MergeDuplicates/Redux/MergeDupsReducer.ts @@ -25,8 +25,8 @@ const mergeDupStepSlice = createSlice({ name: "mergeDupStepReducer", initialState: defaultState, reducers: { - clearTreeAction: (state) => { - state = defaultState; + clearTreeAction: () => { + return defaultState; }, combineIntoFirstSenseAction: (state, action) => { // Set the first sense to be merged as Active/Protected. From 448a45e82d162fbc6edee332d0761dd2a90e16f2 Mon Sep 17 00:00:00 2001 From: Jim Grady Date: Thu, 12 Oct 2023 14:01:56 -0400 Subject: [PATCH 07/36] Fix action creation tests --- .../Redux/tests/MergeDupsActions.test.tsx | 18 ++++++++---------- .../Redux/tests/MergeDupsReducer.test.tsx | 3 +-- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/goals/MergeDuplicates/Redux/tests/MergeDupsActions.test.tsx b/src/goals/MergeDuplicates/Redux/tests/MergeDupsActions.test.tsx index 0f4f44234c..956cb690ce 100644 --- a/src/goals/MergeDuplicates/Redux/tests/MergeDupsActions.test.tsx +++ b/src/goals/MergeDuplicates/Redux/tests/MergeDupsActions.test.tsx @@ -20,12 +20,6 @@ import { orderSense, setWordData, } from "goals/MergeDuplicates/Redux/MergeDupsActions"; -import { - moveDuplicateAction, - moveSenseAction, - orderDuplicateAction, - orderSenseAction, -} from "goals/MergeDuplicates/Redux/MergeDupsReducer"; import { MergeTreeState } from "goals/MergeDuplicates/Redux/MergeDupsReduxTypes"; import { goalDataMock } from "goals/MergeDuplicates/Redux/tests/MergeDupsDataMock"; import { GoalsState, GoalType } from "types/goals"; @@ -273,7 +267,7 @@ describe("MergeDupActions", () => { destWordId: wordId, destOrder: -1, }); - expect(resultAction.type).toEqual(moveSenseAction); + expect(resultAction.type).toEqual("mergeDupStepReducer/moveSenseAction"); }); it("creates a MOVE_DUPLICATE action when going from sidebar to word", () => { @@ -283,7 +277,9 @@ describe("MergeDupActions", () => { destWordId: wordId, destOrder: -1, }); - expect(resultAction.type).toEqual(moveDuplicateAction); + expect(resultAction.type).toEqual( + "mergeDupStepReducer/moveDuplicateAction" + ); }); }); @@ -295,13 +291,15 @@ describe("MergeDupActions", () => { it("creates an ORDER_SENSE action when moving within a word", () => { const mockRef: MergeTreeReference = { wordId, mergeSenseId }; const resultAction = orderSense({ ref: mockRef, order: mockOrder }); - expect(resultAction.type).toEqual(orderSenseAction); + expect(resultAction.type).toEqual("mergeDupStepReducer/orderSenseAction"); }); it("creates an ORDER_DUPLICATE action when moving within the sidebar", () => { const mockRef: MergeTreeReference = { wordId, mergeSenseId, order: 0 }; const resultAction = orderSense({ ref: mockRef, order: mockOrder }); - expect(resultAction.type).toEqual(orderDuplicateAction); + expect(resultAction.type).toEqual( + "mergeDupStepReducer/orderDuplicateAction" + ); }); }); diff --git a/src/goals/MergeDuplicates/Redux/tests/MergeDupsReducer.test.tsx b/src/goals/MergeDuplicates/Redux/tests/MergeDupsReducer.test.tsx index 49ba08f619..045c885848 100644 --- a/src/goals/MergeDuplicates/Redux/tests/MergeDupsReducer.test.tsx +++ b/src/goals/MergeDuplicates/Redux/tests/MergeDupsReducer.test.tsx @@ -17,7 +17,6 @@ import { setWordData, } from "goals/MergeDuplicates/Redux/MergeDupsActions"; import mergeDupStepReducer, { - moveSenseAction, defaultState, } from "goals/MergeDuplicates/Redux/MergeDupsReducer"; import { MergeTreeState } from "goals/MergeDuplicates/Redux/MergeDupsReduxTypes"; @@ -475,7 +474,7 @@ describe("MergeDupReducer", () => { destWordId: destWordId, destOrder: 1, }); - expect(testAction.type).toEqual(moveSenseAction); + expect(testAction.type).toEqual("mergeDupStepReducer/moveSenseAction"); const expectedWords = testTreeWords(); delete expectedWords[srcWordId]; From 7beffcf8ff0a982252404c365092133416f79c65 Mon Sep 17 00:00:00 2001 From: Jim Grady Date: Wed, 18 Oct 2023 09:12:29 -0400 Subject: [PATCH 08/36] Update MergeDupsActions.test.tsx --- .../Redux/tests/MergeDups.test.tsx | 21 ------ .../Redux/tests/MergeDupsActions.test.tsx | 64 +++++++++---------- 2 files changed, 30 insertions(+), 55 deletions(-) delete mode 100644 src/goals/MergeDuplicates/Redux/tests/MergeDups.test.tsx diff --git a/src/goals/MergeDuplicates/Redux/tests/MergeDups.test.tsx b/src/goals/MergeDuplicates/Redux/tests/MergeDups.test.tsx deleted file mode 100644 index c5d7e1f19a..0000000000 --- a/src/goals/MergeDuplicates/Redux/tests/MergeDups.test.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import "@testing-library/jest-dom"; -import { act, cleanup } from "@testing-library/react"; - -import MergeDupsStep from "goals/MergeDuplicates/MergeDupsStep"; -import { setupStore } from "store"; -import { renderWithProviders } from "utilities/testUtilities"; - -beforeEach(() => { - jest.clearAllMocks(); -}); - -afterEach(cleanup); - -describe("Render MergeDups", () => { - it("Renders MergeDups with no errors", async () => { - const store = setupStore(); - await act(async () => { - renderWithProviders(, { store: store }); - }); - }); -}); diff --git a/src/goals/MergeDuplicates/Redux/tests/MergeDupsActions.test.tsx b/src/goals/MergeDuplicates/Redux/tests/MergeDupsActions.test.tsx index 956cb690ce..b76e01c991 100644 --- a/src/goals/MergeDuplicates/Redux/tests/MergeDupsActions.test.tsx +++ b/src/goals/MergeDuplicates/Redux/tests/MergeDupsActions.test.tsx @@ -1,7 +1,5 @@ -import configureMockStore from "redux-mock-store"; -import thunk from "redux-thunk"; - import { GramCatGroup, MergeWords, Sense, Status, Word } from "api/models"; +import { defaultState } from "components/App/DefaultState"; import { defaultTree, MergeData, @@ -20,9 +18,9 @@ import { orderSense, setWordData, } from "goals/MergeDuplicates/Redux/MergeDupsActions"; -import { MergeTreeState } from "goals/MergeDuplicates/Redux/MergeDupsReduxTypes"; import { goalDataMock } from "goals/MergeDuplicates/Redux/tests/MergeDupsDataMock"; -import { GoalsState, GoalType } from "types/goals"; +import { setupStore } from "store"; +import { GoalType } from "types/goals"; import { newSemanticDomain } from "types/semanticDomain"; import { multiSenseWord, @@ -54,12 +52,9 @@ jest.mock("backend", () => ({ const mockGoal = new MergeDups(); mockGoal.data = goalDataMock; mockGoal.steps = [{ words: [] }, { words: [] }]; -const createMockStore = configureMockStore([thunk]); -const mockStoreState: { - goalsState: GoalsState; - mergeDuplicateGoal: MergeTreeState; -} = { +const preloadedState = { + ...defaultState, goalsState: { allGoalTypes: [], currentGoal: new MergeDups(), @@ -68,6 +63,7 @@ const mockStoreState: { previousGoalType: GoalType.Default, }, mergeDuplicateGoal: { data: {} as MergeData, tree: {} as MergeTree }, + _persist: { version: 1, rehydrated: false }, }; const vernA = "AAA"; @@ -106,11 +102,11 @@ describe("MergeDupActions", () => { const WA = newMergeTreeWord(vernA, { ID1: [S1], ID2: [S2] }); const WB = newMergeTreeWord(vernB, { ID1: [S3], ID2: [S4] }); const tree: MergeTree = { ...defaultTree, words: { WA, WB } }; - const mockStore = createMockStore({ - ...mockStoreState, + const store = setupStore({ + ...preloadedState, mergeDuplicateGoal: { data, tree }, }); - await mockStore.dispatch(mergeAll()); + await store.dispatch(mergeAll()); expect(mockMergeWords).not.toHaveBeenCalled(); }); @@ -120,11 +116,11 @@ describe("MergeDupActions", () => { const WA = newMergeTreeWord(vernA, { ID1: [S1, S3], ID2: [S2] }); const WB = newMergeTreeWord(vernB, { ID1: [S4] }); const tree: MergeTree = { ...defaultTree, words: { WA, WB } }; - const mockStore = createMockStore({ - ...mockStoreState, + const store = setupStore({ + ...preloadedState, mergeDuplicateGoal: { data, tree }, }); - await mockStore.dispatch(mergeAll()); + await store.dispatch(mergeAll()); expect(mockMergeWords).toHaveBeenCalledTimes(1); const parentA = wordAnyGuids(vernA, [senses["S1"], senses["S2"]], idA); @@ -145,11 +141,11 @@ describe("MergeDupActions", () => { const WA = newMergeTreeWord(vernA, { ID1: [S1], ID2: [S2], ID3: [S3] }); const WB = newMergeTreeWord(vernB, { ID1: [S4] }); const tree: MergeTree = { ...defaultTree, words: { WA, WB } }; - const mockStore = createMockStore({ - ...mockStoreState, + const store = setupStore({ + ...preloadedState, mergeDuplicateGoal: { data, tree }, }); - await mockStore.dispatch(mergeAll()); + await store.dispatch(mergeAll()); expect(mockMergeWords).toHaveBeenCalledTimes(1); const parentA = wordAnyGuids( @@ -174,11 +170,11 @@ describe("MergeDupActions", () => { const WA = newMergeTreeWord(vernA, { ID1: [S1, S2] }); const WB = newMergeTreeWord(vernB, { ID1: [S3], ID2: [S4] }); const tree: MergeTree = { ...defaultTree, words: { WA, WB } }; - const mockStore = createMockStore({ - ...mockStoreState, + const store = setupStore({ + ...preloadedState, mergeDuplicateGoal: { data, tree }, }); - await mockStore.dispatch(mergeAll()); + await store.dispatch(mergeAll()); expect(mockMergeWords).toHaveBeenCalledTimes(1); @@ -193,11 +189,11 @@ describe("MergeDupActions", () => { const WA = newMergeTreeWord(vernA, { ID1: [S1] }); const WB = newMergeTreeWord(vernB, { ID1: [S3], ID2: [S4] }); const tree: MergeTree = { ...defaultTree, words: { WA, WB } }; - const mockStore = createMockStore({ - ...mockStoreState, + const store = setupStore({ + ...preloadedState, mergeDuplicateGoal: { data, tree }, }); - await mockStore.dispatch(mergeAll()); + await store.dispatch(mergeAll()); expect(mockMergeWords).toHaveBeenCalledTimes(1); const parent = wordAnyGuids(vernA, [senses["S1"]], idA); @@ -210,11 +206,11 @@ describe("MergeDupActions", () => { it("delete all senses from a word", async () => { const WA = newMergeTreeWord(vernA, { ID1: [S1], ID2: [S2] }); const tree: MergeTree = { ...defaultTree, words: { WA } }; - const mockStore = createMockStore({ - ...mockStoreState, + const store = setupStore({ + ...preloadedState, mergeDuplicateGoal: { data, tree }, }); - await mockStore.dispatch(mergeAll()); + await store.dispatch(mergeAll()); expect(mockMergeWords).toHaveBeenCalledTimes(1); const child = { srcWordId: idB, getAudio: false }; @@ -228,11 +224,11 @@ describe("MergeDupActions", () => { WA.flag = newFlag("New flag"); const WB = newMergeTreeWord(vernB, { ID1: [S3], ID2: [S4] }); const tree: MergeTree = { ...defaultTree, words: { WA, WB } }; - const mockStore = createMockStore({ - ...mockStoreState, + const store = setupStore({ + ...preloadedState, mergeDuplicateGoal: { data, tree }, }); - await mockStore.dispatch(mergeAll()); + await store.dispatch(mergeAll()); expect(mockMergeWords).toHaveBeenCalledTimes(1); @@ -249,10 +245,10 @@ describe("MergeDupActions", () => { const goal = new MergeDups(); goal.steps = [{ words: [...goalDataMock.plannedWords[0]] }]; - const mockStore = createMockStore(); - await mockStore.dispatch(dispatchMergeStepData(goal)); + const store = setupStore(); + await store.dispatch(dispatchMergeStepData(goal)); const testAction = setWordData(goalDataMock.plannedWords[0]); - expect(mockStore.getActions()).toEqual([testAction]); + // expect(store.getActions()).toEqual([testAction]); }); }); From 79b46a3dc17ca92281273022cdc39ed4209a93c1 Mon Sep 17 00:00:00 2001 From: "D. Ror" Date: Thu, 12 Oct 2023 12:56:09 -0400 Subject: [PATCH 09/36] [Typescript] Require function return type (#2685) --- package.json | 6 +++ src/backend/index.ts | 3 +- src/backend/tests/localStorage.test.tsx | 2 +- .../AnnouncementBanner/AnnouncementBanner.tsx | 5 +- src/components/App/AppLoggedIn.tsx | 2 +- src/components/App/SignalRHub.tsx | 10 +++- .../AppBar/tests/AppBarComponent.test.tsx | 2 +- .../AppBar/tests/ProjectButtons.test.tsx | 10 ++-- src/components/AppBar/tests/UserMenu.test.tsx | 24 +++++----- src/components/Buttons/FileInputButton.tsx | 10 ++-- src/components/Buttons/PartOfSpeechButton.tsx | 2 +- .../EntryCellComponents/DeleteEntry.tsx | 10 ++-- .../EntryCellComponents/EntryNote.tsx | 8 ++-- .../tests/EntryNote.test.tsx | 25 ++++++---- .../DataEntryTable/NewEntry/SenseDialog.tsx | 2 +- .../DataEntryTable/NewEntry/VernDialog.tsx | 2 +- .../DataEntryTable/tests/RecentEntry.test.tsx | 43 +++++++++-------- .../DataEntryTable/tests/index.test.tsx | 2 +- src/components/Dialogs/ButtonConfirmation.tsx | 2 +- .../Dialogs/DeleteEditTextDialog.tsx | 13 +++-- src/components/Dialogs/EditTextDialog.tsx | 11 +++-- .../GoalTimeline/Redux/GoalActions.ts | 2 +- src/components/GoalTimeline/index.tsx | 5 +- .../GoalTimeline/tests/GoalRedux.test.tsx | 4 +- .../Login/LoginPage/LoginComponent.tsx | 10 ++-- .../LoginPage/tests/LoginComponent.test.tsx | 2 +- .../Login/SignUpPage/SignUpComponent.tsx | 12 ++--- src/components/Login/SignUpPage/index.ts | 1 + .../SignUpPage/tests/SignUpComponent.test.tsx | 2 +- .../Login/tests/LoginActions.test.tsx | 2 +- src/components/PageNotFound/component.tsx | 8 ++-- .../ProjectExport/DownloadButton.tsx | 4 +- src/components/ProjectExport/ExportButton.tsx | 5 +- .../tests/CreateProjectComponent.test.tsx | 47 ++++++++++++------- .../ProjectSettings/ProjectArchive.tsx | 21 ++++----- .../ProjectSettings/ProjectSelect.tsx | 2 +- src/components/ProjectSettings/index.tsx | 2 +- .../tests/ProjectSelect.test.tsx | 10 ++-- .../ProjectSettings/tests/index.test.tsx | 7 +-- .../tests/PronunciationsBackend.test.tsx | 12 +++-- .../ProjectUsersButtonWithConfirmation.tsx | 6 +-- .../SiteSettings/ProjectManagement/index.tsx | 2 +- .../Statistics/DomainStatistics.tsx | 2 +- .../Statistics/LineChartComponent.tsx | 8 ++-- .../ProgressBar/LinearProgressBar.tsx | 5 +- .../ProgressBar/ProgressBarComponent.tsx | 6 +-- src/components/Statistics/Statistics.tsx | 6 +-- src/components/Statistics/UserStatistics.tsx | 2 +- .../tests/DomainStatistics.test.tsx | 10 ++-- .../Statistics/tests/Statistics.test.tsx | 10 ++-- .../Statistics/tests/UserStatistics.test.tsx | 10 ++-- .../TreeDepiction/TreeDepictionTypes.ts | 4 +- .../TreeView/TreeDepiction/index.tsx | 4 +- .../tests/DomainTileButton.test.tsx | 20 ++++---- .../TreeDepiction/tests/index.test.tsx | 16 +++---- src/components/TreeView/TreeSearch.tsx | 8 ++-- src/components/TreeView/index.tsx | 2 +- .../TreeView/tests/TreeNavigator.test.tsx | 2 +- .../TreeView/tests/TreeSearch.test.tsx | 4 +- src/components/UserSettings/AvatarUpload.tsx | 14 ++++-- .../CharInv/CharacterDetail/CharacterInfo.tsx | 7 +-- .../CharacterStatusControl.tsx | 3 +- .../FindAndReplace/CharacterReplaceDialog.tsx | 8 ++-- .../CharInv/CharacterDetail/index.tsx | 5 +- .../CharInv/CharacterList/CharacterCard.tsx | 3 +- .../CharInv/CharacterList/index.tsx | 4 +- .../CharacterInventory/CharInv/index.tsx | 4 +- .../CharInv/tests/index.test.tsx | 28 +++++------ .../Redux/CharacterInventoryActions.ts | 4 +- src/goals/DefaultGoal/DisplayProgress.tsx | 3 +- .../tests/DisplayProgress.test.tsx | 16 +++---- .../Redux/tests/MergeDupsDataMock.ts | 3 +- .../Redux/tests/MergeDupsReducer.test.tsx | 2 +- src/goals/MergeDuplicates/index.tsx | 4 +- src/goals/ReviewDeferredDuplicates/index.tsx | 4 +- .../ReviewEntriesComponent/CellColumns.tsx | 4 +- .../CellComponents/PronunciationsCell.tsx | 10 ++-- .../tests/PronunciationsCell.test.tsx | 20 ++++---- .../Redux/ReviewEntriesActions.ts | 10 +++- .../ReviewEntriesTable.tsx | 2 +- .../tests/ReviewEntriesActions.test.tsx | 8 ++-- .../tests/index.test.tsx | 12 ++--- src/store.ts | 1 + src/utilities/fontComponents.tsx | 2 +- src/utilities/fontCssUtilities.ts | 4 +- src/utilities/spellChecker.ts | 2 +- src/utilities/testUtilities.tsx | 1 + src/utilities/tests/dictionaryLoader.test.ts | 5 +- src/utilities/tests/utilities.test.ts | 2 +- src/utilities/useWindowSize.tsx | 2 +- 90 files changed, 380 insertions(+), 296 deletions(-) diff --git a/package.json b/package.json index 948c50c354..79f643dde1 100644 --- a/package.json +++ b/package.json @@ -142,6 +142,12 @@ "*.dic.js" ], "rules": { + "@typescript-eslint/explicit-function-return-type": [ + "warn", + { + "allowExpressions": true + } + ], "@typescript-eslint/no-empty-interface": "warn", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-inferrable-types": "warn", diff --git a/src/backend/index.ts b/src/backend/index.ts index d81f53ae64..c3266860a5 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -109,11 +109,12 @@ const wordApi = new Api.WordApi(config, BASE_PATH, axiosInstance); // Backend controllers receiving a file via a "[FromForm] FileUpload fileUpload" param // have the internal fields expanded by openapi-generator as params in our Api. +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type function fileUpload(file: File) { return { file, filePath: "", name: "" }; } -function defaultOptions() { +function defaultOptions(): object { return { headers: authHeader() }; } diff --git a/src/backend/tests/localStorage.test.tsx b/src/backend/tests/localStorage.test.tsx index 6bfe8e2f75..31b02a86b1 100644 --- a/src/backend/tests/localStorage.test.tsx +++ b/src/backend/tests/localStorage.test.tsx @@ -32,7 +32,7 @@ afterAll(() => { } }); -function expectAllEmpty() { +function expectAllEmpty(): void { expect(LocalStorage.getAvatar()).toEqual(""); expect(LocalStorage.getClosedBanner()).toEqual(""); expect(LocalStorage.getCurrentUser()).toEqual(undefined); diff --git a/src/components/AnnouncementBanner/AnnouncementBanner.tsx b/src/components/AnnouncementBanner/AnnouncementBanner.tsx index 4a6d838af8..5db0fffa96 100644 --- a/src/components/AnnouncementBanner/AnnouncementBanner.tsx +++ b/src/components/AnnouncementBanner/AnnouncementBanner.tsx @@ -3,6 +3,7 @@ import { Box, IconButton, Toolbar, Typography } from "@mui/material"; import { CSSProperties, Fragment, + ReactElement, useCallback, useEffect, useState, @@ -17,7 +18,7 @@ import { useAppSelector } from "types/hooks"; import { Path } from "types/path"; import theme, { themeColors } from "types/theme"; -export default function AnnouncementBanner() { +export default function AnnouncementBanner(): ReactElement { const [banner, setBanner] = useState(""); const [margins, setMargins] = useState({}); @@ -40,7 +41,7 @@ export default function AnnouncementBanner() { }); }, [loc, calculateMargins]); - function closeBanner() { + function closeBanner(): void { setClosedBanner(banner); setBanner(""); } diff --git a/src/components/App/AppLoggedIn.tsx b/src/components/App/AppLoggedIn.tsx index 9b3ceee085..713866723c 100644 --- a/src/components/App/AppLoggedIn.tsx +++ b/src/components/App/AppLoggedIn.tsx @@ -57,7 +57,7 @@ export default function AppWithBar(): ReactElement { } }, [proj]); - const overrideThemeFont = (theme: Theme) => + const overrideThemeFont = (theme: Theme): Theme => styleOverrides ? createTheme({ ...theme, diff --git a/src/components/App/SignalRHub.tsx b/src/components/App/SignalRHub.tsx index 7b829f8c5d..0606481cda 100644 --- a/src/components/App/SignalRHub.tsx +++ b/src/components/App/SignalRHub.tsx @@ -3,7 +3,13 @@ import { HubConnectionBuilder, HubConnectionState, } from "@microsoft/signalr"; -import { Fragment, useCallback, useEffect, useState } from "react"; +import { + Fragment, + ReactElement, + useCallback, + useEffect, + useState, +} from "react"; import { baseURL } from "backend"; import { getUserId } from "backend/localStorage"; @@ -16,7 +22,7 @@ import { StoreState } from "types"; import { useAppDispatch, useAppSelector } from "types/hooks"; /** A central hub for monitoring export status on SignalR */ -export default function SignalRHub() { +export default function SignalRHub(): ReactElement { const exportState = useAppSelector( (state: StoreState) => state.exportProjectState ); diff --git a/src/components/AppBar/tests/AppBarComponent.test.tsx b/src/components/AppBar/tests/AppBarComponent.test.tsx index b58fc64f49..b1f9796539 100644 --- a/src/components/AppBar/tests/AppBarComponent.test.tsx +++ b/src/components/AppBar/tests/AppBarComponent.test.tsx @@ -18,7 +18,7 @@ jest.mock("backend", () => ({ const mockStore = configureMockStore()(defaultState); -function setMockFunctions() { +function setMockFunctions(): void { mockGetUser.mockResolvedValue(mockUser); } diff --git a/src/components/AppBar/tests/ProjectButtons.test.tsx b/src/components/AppBar/tests/ProjectButtons.test.tsx index 186e422b3b..ed6060897c 100644 --- a/src/components/AppBar/tests/ProjectButtons.test.tsx +++ b/src/components/AppBar/tests/ProjectButtons.test.tsx @@ -1,6 +1,6 @@ import { Button } from "@mui/material"; 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"; @@ -30,15 +30,15 @@ const mockProjectId = "proj-id"; const mockProjectRoles: { [key: string]: string } = {}; mockProjectRoles[mockProjectId] = "non-empty-string"; -let testRenderer: renderer.ReactTestRenderer; +let testRenderer: ReactTestRenderer; const mockStore = configureMockStore()({ currentProjectState: { project: { name: "" } }, }); -const renderProjectButtons = async (path = Path.Root) => { - await renderer.act(async () => { - testRenderer = renderer.create( +const renderProjectButtons = async (path = Path.Root): Promise => { + await act(async () => { + testRenderer = create( diff --git a/src/components/AppBar/tests/UserMenu.test.tsx b/src/components/AppBar/tests/UserMenu.test.tsx index 5a9c8189bf..3a34f8f562 100644 --- a/src/components/AppBar/tests/UserMenu.test.tsx +++ b/src/components/AppBar/tests/UserMenu.test.tsx @@ -1,6 +1,6 @@ import { Button, MenuItem } from "@mui/material"; import { Provider } from "react-redux"; -import renderer from "react-test-renderer"; +import { act, create, ReactTestRenderer } from "react-test-renderer"; import configureMockStore from "redux-mock-store"; import "tests/reactI18nextMock"; @@ -21,7 +21,7 @@ jest.mock("react-router-dom", () => ({ useNavigate: jest.fn(), })); -let testRenderer: renderer.ReactTestRenderer; +let testRenderer: ReactTestRenderer; const mockStore = configureMockStore()(); @@ -30,7 +30,7 @@ const mockGetUserId = jest.fn(); const mockUser = newUser(); const mockUserId = "mockUserId"; -function setMockFunctions() { +function setMockFunctions(): void { mockGetUser.mockResolvedValue(mockUser); mockGetUserId.mockReturnValue(mockUserId); } @@ -41,9 +41,9 @@ beforeEach(() => { }); describe("UserMenu", () => { - it("renders without crashing", () => { - renderer.act(() => { - testRenderer = renderer.create( + it("renders without crashing", async () => { + await act(async () => { + testRenderer = create( @@ -59,18 +59,18 @@ describe("UserMenu", () => { expect(await getIsAdmin()).toBeTruthy(); }); - it("UserMenuList has one more item for admins (Site Settings)", () => { - renderMenuList(); + it("UserMenuList has one more item for admins (Site Settings)", async () => { + await renderMenuList(); const normalMenuItems = testRenderer.root.findAllByType(MenuItem).length; - renderMenuList(true); + await renderMenuList(true); const adminMenuItems = testRenderer.root.findAllByType(MenuItem).length; expect(adminMenuItems).toBe(normalMenuItems + 1); }); }); -function renderMenuList(isAdmin = false) { - renderer.act(() => { - testRenderer = renderer.create( +async function renderMenuList(isAdmin = false): Promise { + await act(async () => { + testRenderer = create( diff --git a/src/components/Buttons/FileInputButton.tsx b/src/components/Buttons/FileInputButton.tsx index 3ca32fe9f3..75e75ed863 100644 --- a/src/components/Buttons/FileInputButton.tsx +++ b/src/components/Buttons/FileInputButton.tsx @@ -1,17 +1,17 @@ import { Button } from "@mui/material"; import { ButtonProps } from "@mui/material/Button"; -import React, { ReactElement } from "react"; +import { ReactElement, ReactNode } from "react"; interface BrowseProps { updateFile: (file: File) => void; accept?: string; - children?: React.ReactNode; + children?: ReactNode; buttonProps?: ButtonProps; } // This button links to a set of functions export default function FileInputButton(props: BrowseProps): ReactElement { - function updateFirstFile(files: FileList) { + function updateFirstFile(files: FileList): void { const file = files[0]; if (file) { props.updateFile(file); @@ -19,7 +19,7 @@ export default function FileInputButton(props: BrowseProps): ReactElement { } return ( - + <> {/* The actual file input element is hidden... */} - + ); } diff --git a/src/components/Buttons/PartOfSpeechButton.tsx b/src/components/Buttons/PartOfSpeechButton.tsx index 23feb1158b..05db4aa1fb 100644 --- a/src/components/Buttons/PartOfSpeechButton.tsx +++ b/src/components/Buttons/PartOfSpeechButton.tsx @@ -32,7 +32,7 @@ export default function PartOfSpeech(props: PartOfSpeechProps): ReactElement { catGroupText ); - const CatGroupButton = () => ( + const CatGroupButton = (): ReactElement => ( } diff --git a/src/components/DataEntry/DataEntryTable/EntryCellComponents/DeleteEntry.tsx b/src/components/DataEntry/DataEntryTable/EntryCellComponents/DeleteEntry.tsx index f69dba7441..5e6377311c 100644 --- a/src/components/DataEntry/DataEntryTable/EntryCellComponents/DeleteEntry.tsx +++ b/src/components/DataEntry/DataEntryTable/EntryCellComponents/DeleteEntry.tsx @@ -1,6 +1,6 @@ import { Delete } from "@mui/icons-material"; import { IconButton, Tooltip } from "@mui/material"; -import React, { useState } from "react"; +import { ReactElement, useState } from "react"; import { useTranslation } from "react-i18next"; import { CancelConfirmDialog } from "components/Dialogs"; @@ -18,11 +18,11 @@ interface DeleteEntryProps { /** * A delete button */ -export default function DeleteEntry(props: DeleteEntryProps) { +export default function DeleteEntry(props: DeleteEntryProps): ReactElement { const [open, setOpen] = useState(false); const { t } = useTranslation(); - function handleClick() { + function handleClick(): void { if (props.confirmId) { setOpen(true); } else { @@ -31,7 +31,7 @@ export default function DeleteEntry(props: DeleteEntryProps) { } return ( - + <> - + ); } diff --git a/src/components/DataEntry/DataEntryTable/EntryCellComponents/EntryNote.tsx b/src/components/DataEntry/DataEntryTable/EntryCellComponents/EntryNote.tsx index c80f1c2290..142a48e4d1 100644 --- a/src/components/DataEntry/DataEntryTable/EntryCellComponents/EntryNote.tsx +++ b/src/components/DataEntry/DataEntryTable/EntryCellComponents/EntryNote.tsx @@ -1,6 +1,6 @@ import { AddComment, Comment } from "@mui/icons-material"; import { IconButton, Tooltip } from "@mui/material"; -import React, { useState } from "react"; +import { ReactElement, useState } from "react"; import { useTranslation } from "react-i18next"; import { EditTextDialog } from "components/Dialogs"; @@ -14,12 +14,12 @@ interface EntryNoteProps { /** * A note adding/editing button */ -export default function EntryNote(props: EntryNoteProps) { +export default function EntryNote(props: EntryNoteProps): ReactElement { const [noteOpen, setNoteOpen] = useState(false); const { t } = useTranslation(); return ( - + <> - + ); } diff --git a/src/components/DataEntry/DataEntryTable/EntryCellComponents/tests/EntryNote.test.tsx b/src/components/DataEntry/DataEntryTable/EntryCellComponents/tests/EntryNote.test.tsx index bfe5e16b09..6f885f46a4 100644 --- a/src/components/DataEntry/DataEntryTable/EntryCellComponents/tests/EntryNote.test.tsx +++ b/src/components/DataEntry/DataEntryTable/EntryCellComponents/tests/EntryNote.test.tsx @@ -1,5 +1,10 @@ import { AddComment, Comment } from "@mui/icons-material"; -import renderer from "react-test-renderer"; +import { + ReactTestInstance, + ReactTestRenderer, + act, + create, +} from "react-test-renderer"; import "tests/reactI18nextMock"; @@ -7,12 +12,12 @@ import EntryNote from "components/DataEntry/DataEntryTable/EntryCellComponents/E const mockText = "Test text"; -let testMaster: renderer.ReactTestRenderer; -let testHandle: renderer.ReactTestInstance; +let testMaster: ReactTestRenderer; +let testHandle: ReactTestInstance; -function renderWithText(text: string) { - renderer.act(() => { - testMaster = renderer.create( +async function renderWithText(text: string): Promise { + await act(async () => { + testMaster = create( ); }); @@ -20,14 +25,14 @@ function renderWithText(text: string) { } describe("DeleteEntry", () => { - it("renders without note", () => { - renderWithText(""); + it("renders without note", async () => { + await renderWithText(""); expect(testHandle.findAllByType(AddComment).length).toBe(1); expect(testHandle.findAllByType(Comment).length).toBe(0); }); - it("renders with note", () => { - renderWithText(mockText); + it("renders with note", async () => { + await renderWithText(mockText); expect(testHandle.findAllByType(AddComment).length).toBe(0); expect(testHandle.findAllByType(Comment).length).toBe(1); }); diff --git a/src/components/DataEntry/DataEntryTable/NewEntry/SenseDialog.tsx b/src/components/DataEntry/DataEntryTable/NewEntry/SenseDialog.tsx index 19cbb1c910..11132c97f4 100644 --- a/src/components/DataEntry/DataEntryTable/NewEntry/SenseDialog.tsx +++ b/src/components/DataEntry/DataEntryTable/NewEntry/SenseDialog.tsx @@ -54,7 +54,7 @@ interface SenseListProps { analysisLang: string; } -export function SenseList(props: SenseListProps) { +export function SenseList(props: SenseListProps): ReactElement { const { t } = useTranslation(); const hasPartsOfSpeech = !!props.selectedWord.senses.find( diff --git a/src/components/DataEntry/DataEntryTable/NewEntry/VernDialog.tsx b/src/components/DataEntry/DataEntryTable/NewEntry/VernDialog.tsx index 82e08ae0d8..77e96dc084 100644 --- a/src/components/DataEntry/DataEntryTable/NewEntry/VernDialog.tsx +++ b/src/components/DataEntry/DataEntryTable/NewEntry/VernDialog.tsx @@ -54,7 +54,7 @@ interface VernListProps { analysisLang?: string; } -export function VernList(props: VernListProps) { +export function VernList(props: VernListProps): ReactElement { const { t } = useTranslation(); const hasPartsOfSpeech = !!props.vernacularWords.find((w) => diff --git a/src/components/DataEntry/DataEntryTable/tests/RecentEntry.test.tsx b/src/components/DataEntry/DataEntryTable/tests/RecentEntry.test.tsx index 0cd97a629a..fc8525608b 100644 --- a/src/components/DataEntry/DataEntryTable/tests/RecentEntry.test.tsx +++ b/src/components/DataEntry/DataEntryTable/tests/RecentEntry.test.tsx @@ -1,6 +1,11 @@ import { StyledEngineProvider, ThemeProvider } from "@mui/material/styles"; import { Provider } from "react-redux"; -import renderer from "react-test-renderer"; +import { + ReactTestInstance, + ReactTestRenderer, + act, + create, +} from "react-test-renderer"; import configureMockStore from "redux-mock-store"; import "tests/reactI18nextMock"; @@ -38,12 +43,12 @@ const mockUpdateGloss = jest.fn(); const mockUpdateNote = jest.fn(); const mockUpdateVern = jest.fn(); -let testMaster: renderer.ReactTestRenderer; -let testHandle: renderer.ReactTestInstance; +let testMaster: ReactTestRenderer; +let testHandle: ReactTestInstance; -function renderWithWord(word: Word) { - renderer.act(() => { - testMaster = renderer.create( +async function renderWithWord(word: Word): Promise { + await act(async () => { + testMaster = create( @@ -75,14 +80,14 @@ beforeEach(() => { describe("ExistingEntry", () => { describe("component", () => { - it("renders recorder and no players", () => { - renderWithWord(mockWord); + it("renders recorder and no players", async () => { + await renderWithWord(mockWord); expect(testHandle.findAllByType(AudioPlayer).length).toEqual(0); expect(testHandle.findAllByType(AudioRecorder).length).toEqual(1); }); - it("renders recorder and 3 players", () => { - renderWithWord({ ...mockWord, audio: ["a.wav", "b.wav", "c.wav"] }); + it("renders recorder and 3 players", async () => { + await renderWithWord({ ...mockWord, audio: ["a.wav", "b.wav", "c.wav"] }); expect(testHandle.findAllByType(AudioPlayer).length).toEqual(3); expect(testHandle.findAllByType(AudioRecorder).length).toEqual(1); }); @@ -90,10 +95,10 @@ describe("ExistingEntry", () => { describe("vernacular", () => { it("updates if changed", async () => { - renderWithWord(mockWord); + await renderWithWord(mockWord); testHandle = testHandle.findByType(VernWithSuggestions); - async function updateVernAndBlur(text: string) { - await renderer.act(async () => { + async function updateVernAndBlur(text: string): Promise { + await act(async () => { await testHandle.props.updateVernField(text); await testHandle.props.onBlur(); }); @@ -108,10 +113,10 @@ describe("ExistingEntry", () => { describe("gloss", () => { it("updates if changed", async () => { - renderWithWord(mockWord); + await renderWithWord(mockWord); testHandle = testHandle.findByType(GlossWithSuggestions); - async function updateGlossAndBlur(text: string) { - await renderer.act(async () => { + async function updateGlossAndBlur(text: string): Promise { + await act(async () => { await testHandle.props.updateGlossField(text); await testHandle.props.onBlur(); }); @@ -125,10 +130,10 @@ describe("ExistingEntry", () => { }); describe("note", () => { - it("updates text", () => { - renderWithWord(mockWord); + it("updates text", async () => { + await renderWithWord(mockWord); testHandle = testHandle.findByType(EntryNote).findByType(EditTextDialog); - renderer.act(() => { + await act(async () => { testHandle.props.updateText(mockText); }); expect(mockUpdateNote).toBeCalledWith(mockText); diff --git a/src/components/DataEntry/DataEntryTable/tests/index.test.tsx b/src/components/DataEntry/DataEntryTable/tests/index.test.tsx index c10380ebb4..e8e0e70342 100644 --- a/src/components/DataEntry/DataEntryTable/tests/index.test.tsx +++ b/src/components/DataEntry/DataEntryTable/tests/index.test.tsx @@ -387,7 +387,7 @@ describe("DataEntryTable", () => { const vern = "vern"; const glossDef = "gloss"; const noteText = "note"; - act(() => { + await act(async () => { testHandle.props.setNewVern(vern); testHandle.props.setNewGloss(glossDef); testHandle.props.setNewNote(noteText); diff --git a/src/components/Dialogs/ButtonConfirmation.tsx b/src/components/Dialogs/ButtonConfirmation.tsx index 0c3f0d798d..c348b05ef5 100644 --- a/src/components/Dialogs/ButtonConfirmation.tsx +++ b/src/components/Dialogs/ButtonConfirmation.tsx @@ -30,7 +30,7 @@ export default function ButtonConfirmation( const [loading, setLoading] = useState(false); const { t } = useTranslation(); - async function onConfirm() { + async function onConfirm(): Promise { setLoading(true); await props.onConfirm(); setLoading(false); diff --git a/src/components/Dialogs/DeleteEditTextDialog.tsx b/src/components/Dialogs/DeleteEditTextDialog.tsx index afb2faf7ec..afbc9c8a87 100644 --- a/src/components/Dialogs/DeleteEditTextDialog.tsx +++ b/src/components/Dialogs/DeleteEditTextDialog.tsx @@ -40,12 +40,12 @@ export default function DeleteEditTextDialog( const [text, setText] = useState(props.text); const { t } = useTranslation(); - function onCancel() { + function onCancel(): void { setText(props.text); props.close(); } - function onDelete() { + function onDelete(): void { setText(props.text); if (props.onDelete) { props.onDelete(); @@ -53,20 +53,23 @@ export default function DeleteEditTextDialog( props.close(); } - async function onSave() { + async function onSave(): Promise { setLoading(true); await props.updateText(text); setLoading(false); props.close(); } - function escapeClose(_: any, reason: "backdropClick" | "escapeKeyDown") { + function escapeClose( + _: any, + reason: "backdropClick" | "escapeKeyDown" + ): void { if (reason === "escapeKeyDown") { onCancel(); } } - function confirmIfEnter(event: React.KeyboardEvent) { + function confirmIfEnter(event: React.KeyboardEvent): void { if (event.key === Key.Enter) { onSave(); } diff --git a/src/components/Dialogs/EditTextDialog.tsx b/src/components/Dialogs/EditTextDialog.tsx index b891c283fc..976a3c4d88 100644 --- a/src/components/Dialogs/EditTextDialog.tsx +++ b/src/components/Dialogs/EditTextDialog.tsx @@ -35,25 +35,28 @@ export default function EditTextDialog( const [text, setText] = useState(props.text); const { t } = useTranslation(); - async function onConfirm() { + async function onConfirm(): Promise { props.close(); if (text !== props.text) { await props.updateText(text); } } - function onCancel() { + function onCancel(): void { setText(props.text); props.close(); } - function escapeClose(_: any, reason: "backdropClick" | "escapeKeyDown") { + function escapeClose( + _: any, + reason: "backdropClick" | "escapeKeyDown" + ): void { if (reason === "escapeKeyDown") { props.close(); } } - function confirmIfEnter(event: React.KeyboardEvent) { + function confirmIfEnter(event: React.KeyboardEvent): void { if (event.key === Key.Enter) { onConfirm(); } diff --git a/src/components/GoalTimeline/Redux/GoalActions.ts b/src/components/GoalTimeline/Redux/GoalActions.ts index dce3840252..66db597cc0 100644 --- a/src/components/GoalTimeline/Redux/GoalActions.ts +++ b/src/components/GoalTimeline/Redux/GoalActions.ts @@ -216,7 +216,7 @@ export async function loadGoalData(goalType: GoalType): Promise { } } -async function saveCurrentStep(goal: Goal) { +async function saveCurrentStep(goal: Goal): Promise { const userEditId = getUserEditId(); if (userEditId) { const step = goal.steps[goal.currentStep]; diff --git a/src/components/GoalTimeline/index.tsx b/src/components/GoalTimeline/index.tsx index 93c6074935..b354192fe7 100644 --- a/src/components/GoalTimeline/index.tsx +++ b/src/components/GoalTimeline/index.tsx @@ -68,7 +68,8 @@ export default function GoalTimeline(): ReactElement { (state: StoreState) => state.goalsState ); - const chooseGoal = (goal: Goal) => dispatch(asyncAddGoal(goal)); + const chooseGoal = (goal: Goal): Promise => + dispatch(asyncAddGoal(goal)); const [availableGoalTypes, setAvailableGoalTypes] = useState([]); const [suggestedGoalTypes, setSuggestedGoalTypes] = useState([]); @@ -86,7 +87,7 @@ export default function GoalTimeline(): ReactElement { dispatch(asyncGetUserEdits()); setLoaded(true); } - const updateHasGraylist = async () => + const updateHasGraylist = async (): Promise => setHasGraylist( await getGraylistEntries(1).then((res) => res.length !== 0) ); diff --git a/src/components/GoalTimeline/tests/GoalRedux.test.tsx b/src/components/GoalTimeline/tests/GoalRedux.test.tsx index 5af9ca0bc0..b6d1d03ff1 100644 --- a/src/components/GoalTimeline/tests/GoalRedux.test.tsx +++ b/src/components/GoalTimeline/tests/GoalRedux.test.tsx @@ -61,7 +61,7 @@ const mockGetUser = jest.fn(); const mockGetUserEditById = jest.fn(); const mockNavigate = jest.fn(); const mockUpdateUser = jest.fn(); -function setMockFunctions() { +function setMockFunctions(): void { mockAddGoalToUserEdit.mockResolvedValue(0); mockAddStepToGoal.mockResolvedValue(0); mockCreateUserEdit.mockResolvedValue(mockUser); @@ -108,7 +108,7 @@ const mockUser = newUser("First Last", "username"); mockUser.id = mockUserId; mockUser.workedProjects[mockProjectId] = mockUserEditId; -function setupLocalStorage() { +function setupLocalStorage(): void { LocalStorage.setCurrentUser(mockUser); LocalStorage.setProjectId(mockProjectId); } diff --git a/src/components/Login/LoginPage/LoginComponent.tsx b/src/components/Login/LoginPage/LoginComponent.tsx index f9b02ef6be..3291d02cff 100644 --- a/src/components/Login/LoginPage/LoginComponent.tsx +++ b/src/components/Login/LoginPage/LoginComponent.tsx @@ -9,7 +9,7 @@ import { TextField, Typography, } from "@mui/material"; -import { Component } from "react"; +import { Component, ReactElement } from "react"; import { withTranslation, WithTranslation } from "react-i18next"; import { BannerType } from "api/models"; @@ -71,7 +71,7 @@ export class Login extends Component { }; } - componentDidMount() { + componentDidMount(): void { this.props.reset(); getBannerText(BannerType.Login).then((loginBanner) => this.setState({ loginBanner }) @@ -84,13 +84,13 @@ export class Login extends Component { HTMLTextAreaElement | HTMLInputElement | HTMLSelectElement >, field: K - ) { + ): void { const value = e.target.value; this.setState({ [field]: value } as Pick); } - login(e: React.FormEvent) { + login(e: React.FormEvent): void { e.preventDefault(); const username: string = this.state.username.trim(); @@ -105,7 +105,7 @@ export class Login extends Component { } } - render() { + render(): ReactElement { return ( diff --git a/src/components/Login/LoginPage/tests/LoginComponent.test.tsx b/src/components/Login/LoginPage/tests/LoginComponent.test.tsx index 2fbfd09627..45ee12c976 100644 --- a/src/components/Login/LoginPage/tests/LoginComponent.test.tsx +++ b/src/components/Login/LoginPage/tests/LoginComponent.test.tsx @@ -60,7 +60,7 @@ function testLogin( password: string, goodUsername: boolean, goodPassword: boolean -) { +): void { loginHandle.instance.setState({ username, password }); loginHandle.instance.login(MOCK_EVENT); expect(loginHandle.instance.state.error).toEqual({ diff --git a/src/components/Login/SignUpPage/SignUpComponent.tsx b/src/components/Login/SignUpPage/SignUpComponent.tsx index 5a7903f5ab..d06f888c6e 100644 --- a/src/components/Login/SignUpPage/SignUpComponent.tsx +++ b/src/components/Login/SignUpPage/SignUpComponent.tsx @@ -7,7 +7,7 @@ import { TextField, Typography, } from "@mui/material"; -import { Component } from "react"; +import { Component, ReactElement } from "react"; import { withTranslation, WithTranslation } from "react-i18next"; import router from "browserRouter"; @@ -86,7 +86,7 @@ export class SignUp extends Component { }; } - componentDidMount() { + componentDidMount(): void { const search = window.location.search; const email = new URLSearchParams(search).get("email"); if (email) { @@ -101,7 +101,7 @@ export class SignUp extends Component { HTMLTextAreaElement | HTMLInputElement | HTMLSelectElement >, field: K - ) { + ): void { const value = e.target.value; this.setState({ [field]: value } as Pick); this.setState((prevState) => ({ @@ -109,7 +109,7 @@ export class SignUp extends Component { })); } - async checkUsername() { + async checkUsername(): Promise { if (!meetsUsernameRequirements(this.state.username)) { this.setState((prevState) => ({ error: { ...prevState.error, username: true }, @@ -117,7 +117,7 @@ export class SignUp extends Component { } } - async signUp(e: React.FormEvent) { + async signUp(e: React.FormEvent): Promise { e.preventDefault(); const name = this.state.name.trim(); const username = this.state.username.trim(); @@ -146,7 +146,7 @@ export class SignUp extends Component { } } - render() { + render(): ReactElement { return ( diff --git a/src/components/Login/SignUpPage/index.ts b/src/components/Login/SignUpPage/index.ts index 598a46ac06..f3bbe065e6 100644 --- a/src/components/Login/SignUpPage/index.ts +++ b/src/components/Login/SignUpPage/index.ts @@ -16,6 +16,7 @@ function mapStateToProps(state: StoreState): SignUpStateProps { }; } +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type function mapDispatchToProps(dispatch: StoreStateDispatch) { return { signUp: ( diff --git a/src/components/Login/SignUpPage/tests/SignUpComponent.test.tsx b/src/components/Login/SignUpPage/tests/SignUpComponent.test.tsx index cb092f7566..84df8b4651 100644 --- a/src/components/Login/SignUpPage/tests/SignUpComponent.test.tsx +++ b/src/components/Login/SignUpPage/tests/SignUpComponent.test.tsx @@ -98,7 +98,7 @@ async function testSignUp( error_email: boolean, error_password: boolean, error_confirmPassword: boolean -) { +): Promise { signUpHandle.instance.setState({ name, username, diff --git a/src/components/Login/tests/LoginActions.test.tsx b/src/components/Login/tests/LoginActions.test.tsx index 0c53288cac..587f4762a2 100644 --- a/src/components/Login/tests/LoginActions.test.tsx +++ b/src/components/Login/tests/LoginActions.test.tsx @@ -175,7 +175,7 @@ describe("LoginAction", () => { function testActionCreatorAgainst( LoginAction: (name: string) => UserAction, type: LoginType -) { +): void { expect(LoginAction(mockUser.username)).toEqual({ type: type, payload: { username: mockUser.username }, diff --git a/src/components/PageNotFound/component.tsx b/src/components/PageNotFound/component.tsx index 12b178ac1f..b09d68ff75 100644 --- a/src/components/PageNotFound/component.tsx +++ b/src/components/PageNotFound/component.tsx @@ -1,5 +1,5 @@ import { Typography } from "@mui/material"; -import React from "react"; +import { ReactElement } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; @@ -10,12 +10,12 @@ import { Path } from "types/path"; * A custom 404 page that should be displayed anytime the user tries to navigate * to a nonexistent route. */ -export default function PageNotFound() { +export default function PageNotFound(): ReactElement { const { t } = useTranslation(); const navigate = useNavigate(); return ( - + <> {t("generic.404Title")} @@ -30,6 +30,6 @@ export default function PageNotFound() { {t("generic.404Text")} - + ); } diff --git a/src/components/ProjectExport/DownloadButton.tsx b/src/components/ProjectExport/DownloadButton.tsx index d7e4ae27ba..ce844d3a90 100644 --- a/src/components/ProjectExport/DownloadButton.tsx +++ b/src/components/ProjectExport/DownloadButton.tsx @@ -21,7 +21,7 @@ import { useAppDispatch, useAppSelector } from "types/hooks"; import { themeColors } from "types/theme"; import { getNowDateTimeString } from "utilities/utilities"; -function makeExportName(projectName: string) { +function makeExportName(projectName: string): string { return `${projectName}_${getNowDateTimeString()}.zip`; } @@ -104,7 +104,7 @@ export default function DownloadButton( } } - function iconColor() { + function iconColor(): `#${string}` { return exportState.status === ExportStatus.Failure ? themeColors.error : props.colorSecondary diff --git a/src/components/ProjectExport/ExportButton.tsx b/src/components/ProjectExport/ExportButton.tsx index edfd0ab8d8..4b17e033f0 100644 --- a/src/components/ProjectExport/ExportButton.tsx +++ b/src/components/ProjectExport/ExportButton.tsx @@ -1,5 +1,6 @@ import { ButtonProps } from "@mui/material/Button"; import { enqueueSnackbar } from "notistack"; +import { ReactElement } from "react"; import { useTranslation } from "react-i18next"; import { isFrontierNonempty } from "backend"; @@ -15,11 +16,11 @@ interface ExportButtonProps { } /** A button for exporting project to Lift file */ -export default function ExportButton(props: ExportButtonProps) { +export default function ExportButton(props: ExportButtonProps): ReactElement { const dispatch = useAppDispatch(); const { t } = useTranslation(); - async function exportProj() { + async function exportProj(): Promise { await isFrontierNonempty(props.projectId).then(async (isNonempty) => { if (isNonempty) { await dispatch(asyncExportProject(props.projectId)); diff --git a/src/components/ProjectScreen/CreateProject/tests/CreateProjectComponent.test.tsx b/src/components/ProjectScreen/CreateProject/tests/CreateProjectComponent.test.tsx index bec78a8c57..b39d3c42d2 100644 --- a/src/components/ProjectScreen/CreateProject/tests/CreateProjectComponent.test.tsx +++ b/src/components/ProjectScreen/CreateProject/tests/CreateProjectComponent.test.tsx @@ -1,6 +1,11 @@ import { LanguagePicker } from "mui-language-picker"; import { Provider } from "react-redux"; -import renderer from "react-test-renderer"; +import { + ReactTestInstance, + ReactTestRenderer, + act, + create, +} from "react-test-renderer"; import configureMockStore from "redux-mock-store"; import "tests/reactI18nextMock"; @@ -30,18 +35,22 @@ const mockState = { }; const mockStore = createMockStore(mockState); -const mockEvent = (value = "") => ({ - preventDefault: jest.fn(), +const mockChangeEvent = ( + value: string +): { target: Partial } => ({ target: { value }, }); +const mockSubmitEvent = (): Partial> => ({ + preventDefault: jest.fn(), +}); -let projectMaster: renderer.ReactTestRenderer; -let projectHandle: renderer.ReactTestInstance; +let projectMaster: ReactTestRenderer; +let projectHandle: ReactTestInstance; 4; beforeAll(async () => { - await renderer.act(async () => { - projectMaster = renderer.create( + await act(async () => { + projectMaster = create( @@ -59,22 +68,26 @@ describe("CreateProject", () => { const nameField = projectHandle.findByProps({ id: fieldIdName }); expect(nameField.props.error).toBeFalsy(); - await renderer.act(async () => { - projectHandle.findByProps({ id: formId }).props.onSubmit(mockEvent()); + await act(async () => { + projectHandle + .findByProps({ id: formId }) + .props.onSubmit(mockSubmitEvent()); }); expect(nameField.props.error).toBeTruthy(); }); it("errors on taken name", async () => { const nameField = projectHandle.findByProps({ id: fieldIdName }); - await renderer.act(async () => { - nameField.props.onChange(mockEvent("non-empty")); + await act(async () => { + nameField.props.onChange(mockChangeEvent("non-empty-value")); }); expect(nameField.props.error).toBeFalsy(); mockProjectDuplicateCheck.mockResolvedValueOnce(true); - await renderer.act(async () => { - projectHandle.findByProps({ id: formId }).props.onSubmit(mockEvent()); + await act(async () => { + projectHandle + .findByProps({ id: formId }) + .props.onSubmit(mockSubmitEvent()); }); expect(nameField.props.error).toBeTruthy(); }); @@ -85,7 +98,7 @@ describe("CreateProject", () => { const langPickers = projectHandle.findAllByType(LanguagePicker); expect(langPickers).toHaveLength(2); - await renderer.act(async () => { + await act(async () => { langPickers[0].props.setCode("non-empty"); }); expect(button.props.disabled).toBeFalsy(); @@ -97,7 +110,7 @@ describe("CreateProject", () => { // File with no writing systems only disables analysis lang picker. mockUploadLiftAndGetWritingSystems.mockResolvedValueOnce([]); - await renderer.act(async () => { + await act(async () => { button.props.updateFile(new File([], "")); }); expect(projectHandle.findAllByType(LanguagePicker)).toHaveLength(1); @@ -106,7 +119,7 @@ describe("CreateProject", () => { mockUploadLiftAndGetWritingSystems.mockResolvedValueOnce([ newWritingSystem(), ]); - await renderer.act(async () => { + await act(async () => { button.props.updateFile(new File([], "oneLang")); }); expect(projectHandle.findAllByType(LanguagePicker)).toHaveLength(0); @@ -116,7 +129,7 @@ describe("CreateProject", () => { const button = projectHandle.findByType(FileInputButton); const langs = [newWritingSystem("redLang"), newWritingSystem("blueLang")]; mockUploadLiftAndGetWritingSystems.mockResolvedValueOnce(langs); - await renderer.act(async () => { + await act(async () => { button.props.updateFile(new File([], "twoLang")); }); const vernSelect = projectHandle.findByProps({ id: selectIdVern }); diff --git a/src/components/ProjectSettings/ProjectArchive.tsx b/src/components/ProjectSettings/ProjectArchive.tsx index 4dc16374a3..b52b461676 100644 --- a/src/components/ProjectSettings/ProjectArchive.tsx +++ b/src/components/ProjectSettings/ProjectArchive.tsx @@ -1,6 +1,6 @@ import { Button } from "@mui/material"; import { ButtonProps } from "@mui/material/Button"; -import { useState } from "react"; +import { ReactElement, useState } from "react"; import { useTranslation } from "react-i18next"; import { archiveProject, restoreProject } from "backend"; @@ -17,25 +17,20 @@ interface ProjectArchiveProps extends ButtonProps { /** * Button for archiving/restoring project (changing isActive) */ -export default function ProjectArchive(props: ProjectArchiveProps) { +export default function ProjectArchive( + props: ProjectArchiveProps +): ReactElement { const [open, setOpen] = useState(false); const { t } = useTranslation(); - async function updateProj() { + async function updateProj(): Promise { if (props.archive) { await archiveProject(props.projectId); } else { await restoreProject(props.projectId); } - handleClose(); - await props.updateParent(); - } - - function handleOpen() { - setOpen(true); - } - function handleClose() { setOpen(false); + await props.updateParent(); } return ( @@ -43,7 +38,7 @@ export default function ProjectArchive(props: ProjectArchiveProps) {