diff --git a/src/goals/MergeDuplicates/MergeDupsStep/MergeDragDrop/DropWord.tsx b/src/goals/MergeDuplicates/MergeDupsStep/MergeDragDrop/DropWord.tsx index 4fd9e7c98b..07aeddae21 100644 --- a/src/goals/MergeDuplicates/MergeDupsStep/MergeDragDrop/DropWord.tsx +++ b/src/goals/MergeDuplicates/MergeDupsStep/MergeDragDrop/DropWord.tsx @@ -43,7 +43,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 ( @@ -69,7 +69,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) => ( @@ -94,7 +99,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/MergeDupsTreeTypes.ts b/src/goals/MergeDuplicates/MergeDupsTreeTypes.ts index 9d86a37c2c..f333343797 100644 --- a/src/goals/MergeDuplicates/MergeDupsTreeTypes.ts +++ b/src/goals/MergeDuplicates/MergeDupsTreeTypes.ts @@ -55,12 +55,12 @@ export function newMergeTreeWord( } export function convertSenseToMergeTreeSense( - sense?: Sense, + sense: Sense, srcWordId = "", order = 0 ): MergeTreeSense { return { - ...(sense ?? newSense()), + ...sense, srcWordId, order, protected: sense?.accessibility === Status.Protected, diff --git a/src/goals/MergeDuplicates/Redux/MergeDupsActions.ts b/src/goals/MergeDuplicates/Redux/MergeDupsActions.ts index 31b8b06706..88a697a4ce 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 { Word } from "api/models"; import * as backend from "backend"; import { addCompletedMergeToGoal, @@ -15,216 +9,94 @@ import { import { defaultSidebar, MergeTreeReference, - MergeTreeSense, Sidebar, } from "goals/MergeDuplicates/MergeDupsTreeTypes"; import { MergeDups, MergeStepData, ReviewDeferredDups, - newMergeWords, } from "goals/MergeDuplicates/MergeDupsTypes"; import { - ClearTreeMergeAction, - CombineSenseMergeAction, - DeleteSenseMergeAction, - FlagWord, - MergeTreeActionTypes, - MergeTreeState, - MoveDuplicateMergeAction, - MoveSenseMergeAction, - OrderDuplicateMergeAction, - OrderSenseMergeAction, - SetDataMergeAction, - SetSidebarMergeAction, - SetVernacularMergeAction, + clearMergeWordsAction, + clearTreeAction, + combineSenseAction, + deleteSenseAction, + flagWordAction, + getMergeWordsAction, + moveDuplicateAction, + moveSenseAction, + orderDuplicateAction, + orderSenseAction, + setDataAction, + setSidebarAction, + setVernacularAction, +} from "goals/MergeDuplicates/Redux/MergeDupsReducer"; +import { + CombineSenseMergePayload, + FlagWordPayload, + 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 clearMergeWords(): Action { + return clearMergeWordsAction(); } -export function combineSense( - src: MergeTreeReference, - dest: MergeTreeReference -): CombineSenseMergeAction { - return { type: MergeTreeActionTypes.COMBINE_SENSE, payload: { src, dest } }; +export function clearTree(): Action { + return clearTreeAction(); } -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 getMergeWords(): Action { + return getMergeWordsAction(); +} + +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 setData(words: Word[]): PayloadAction { + return setDataAction(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 -// Given a wordId, constructs from the state the corresponding MergeWords. -// Returns the MergeWords, or undefined if the parent and child are identical. -function getMergeWords( - wordId: string, - mergeTree: MergeTreeState -): MergeWords | undefined { - // Find and build MergeSourceWord[]. - const word = mergeTree.tree.words[wordId]; - if (word) { - const data = mergeTree.data; - - // List of all non-deleted senses. - const nonDeleted = Object.values(mergeTree.tree.words).flatMap((w) => - Object.values(w.sensesGuids).flatMap((s) => s) - ); - - // Create list of all senses and add merge type tags slit by src word. - const senses: Hash = {}; - - // Build senses array. - for (const senseGuids of Object.values(word.sensesGuids)) { - for (const guid of senseGuids) { - const senseData = data.senses[guid]; - const wordId = senseData.srcWordId; - - if (!senses[wordId]) { - const dbWord = data.words[wordId]; - - // Add each sense into senses as separate or deleted. - senses[wordId] = []; - for (const sense of dbWord.senses) { - senses[wordId].push({ - ...sense, - srcWordId: wordId, - order: senses[wordId].length, - accessibility: nonDeleted.includes(sense.guid) - ? Status.Separate - : Status.Deleted, - protected: sense.accessibility === Status.Protected, - }); - } - } - } - } - - // Set sense and duplicate senses. - Object.values(word.sensesGuids).forEach((guids) => { - const sensesToCombine = guids - .map((g) => data.senses[g]) - .map((s) => senses[s.srcWordId][s.order]); - combineIntoFirstSense(sensesToCombine); - }); - - // Clean order of senses in each src word to reflect backend order. - Object.values(senses).forEach((wordSenses) => { - wordSenses = wordSenses.sort((a, b) => a.order - b.order); - senses[wordSenses[0].srcWordId] = wordSenses; - }); - - // Don't return empty merges: when the only child is the parent word - // and has the same number of senses as parent (all Active/Protected) - // and has the same flag. - if (Object.values(senses).length === 1) { - const onlyChild = Object.values(senses)[0]; - if ( - onlyChild[0].srcWordId === wordId && - onlyChild.length === data.words[wordId].senses.length && - !onlyChild.find( - (s) => ![Status.Active, Status.Protected].includes(s.accessibility) - ) && - compareFlags(word.flag, data.words[wordId].flag) === 0 - ) { - return undefined; - } - } - - // Construct parent and children. - const parent: Word = { ...data.words[wordId], senses: [], flag: word.flag }; - if (!parent.vernacular) { - parent.vernacular = word.vern; - } - const children: MergeSourceWord[] = Object.values(senses).map((sList) => { - sList.forEach((sense) => { - if ([Status.Active, Status.Protected].includes(sense.accessibility)) { - parent.senses.push({ - guid: sense.guid, - definitions: sense.definitions, - glosses: sense.glosses, - semanticDomains: sense.semanticDomains, - accessibility: sense.accessibility, - grammaticalInfo: sense.grammaticalInfo, - }); - } - }); - const getAudio = !sList.find((s) => s.accessibility === Status.Separate); - return { srcWordId: sList[0].srcWordId, getAudio }; - }); - - return newMergeWords(parent, children); - } -} - export function deferMerge() { return async (_: StoreStateDispatch, getState: () => StoreState) => { const mergeTree = getState().mergeDuplicateGoal; @@ -239,27 +111,11 @@ export function mergeAll() { // Add to blacklist. await backend.blacklistAdd(Object.keys(mergeTree.data.words)); - // Handle words with all senses deleted. - const possibleWords = Object.values(mergeTree.data.words); - const nonDeletedSenses = Object.values(mergeTree.tree.words).flatMap((w) => - Object.values(w.sensesGuids).flatMap((s) => s) - ); - const deletedWords = possibleWords.filter( - (w) => - !w.senses.map((s) => s.guid).find((g) => nonDeletedSenses.includes(g)) - ); - const mergeWordsArray = deletedWords.map((w) => - newMergeWords(w, [{ srcWordId: w.id, getAudio: false }], true) - ); - // Merge words. - const wordIds = Object.keys(mergeTree.tree.words); - wordIds.forEach((id) => { - const wordsToMerge = getMergeWords(id, mergeTree); - if (wordsToMerge) { - mergeWordsArray.push(wordsToMerge); - } - }); + dispatch(getMergeWords()); + + const mergeWordsArray = [...getState().mergeDuplicateGoal.mergeWords]; + dispatch(clearMergeWords()); if (mergeWordsArray.length) { const parentIds = await backend.mergeWords(mergeWordsArray); const childIds = [ @@ -268,10 +124,8 @@ export function mergeAll() { ), ]; const completedMerge = { childIds, parentIds }; - if (getState().goalsState.currentGoal) { - dispatch(addCompletedMergeToGoal(completedMerge)); - await dispatch(asyncUpdateGoal()); - } + dispatch(addCompletedMergeToGoal(completedMerge)); + await dispatch(asyncUpdateGoal()); } }; } @@ -283,59 +137,7 @@ export function dispatchMergeStepData(goal: MergeDups | ReviewDeferredDups) { const stepData = goal.steps[goal.currentStep] as MergeStepData; if (stepData) { const stepWords = stepData.words ?? []; - dispatch(setWordData(stepWords)); + dispatch(setData(stepWords)); } }; } - -/** 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..9334e402db 100644 --- a/src/goals/MergeDuplicates/Redux/MergeDupsReducer.ts +++ b/src/goals/MergeDuplicates/Redux/MergeDupsReducer.ts @@ -1,76 +1,76 @@ +import { createSlice } from "@reduxjs/toolkit"; import { v4 } from "uuid"; -import { Word } from "api/models"; +import { + GramCatGroup, + MergeSourceWord, + MergeWords, + Status, + Word, +} from "api/models"; import { convertSenseToMergeTreeSense, convertWordToMergeTreeWord, defaultSidebar, defaultTree, - MergeTree, + MergeData, MergeTreeSense, MergeTreeWord, newMergeTreeWord, } from "goals/MergeDuplicates/MergeDupsTreeTypes"; -import { - MergeTreeAction, - MergeTreeActionTypes, - MergeTreeState, -} from "goals/MergeDuplicates/Redux/MergeDupsReduxTypes"; -import { StoreAction, StoreActionTypes } from "rootActions"; +import { newMergeWords } from "goals/MergeDuplicates/MergeDupsTypes"; +import { MergeTreeState } from "goals/MergeDuplicates/Redux/MergeDupsReduxTypes"; +import { StoreActionTypes } from "rootActions"; import { Hash } from "types/hash"; +import { compareFlags } from "utilities/wordUtilities"; const defaultData = { words: {}, senses: {} }; export const defaultState: MergeTreeState = { data: defaultData, tree: defaultTree, + mergeWords: [], }; -export const mergeDupStepReducer = ( - state: MergeTreeState = defaultState, //createStore() calls each reducer with undefined state - action: MergeTreeAction | StoreAction -): MergeTreeState => { - switch (action.type) { - case MergeTreeActionTypes.CLEAR_TREE: { +const mergeDuplicatesSlice = createSlice({ + name: "mergeDupStepReducer", + initialState: defaultState, + reducers: { + clearMergeWordsAction: (state) => { + state.mergeWords = []; + }, + clearTreeAction: () => { return defaultState; - } - - case MergeTreeActionTypes.COMBINE_SENSE: { + }, + 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: { - const srcRef = action.payload.src; + words[destRef.wordId].sensesGuids[destRef.mergeSenseId].push( + ...destGuids + ); + state.tree.words = words; + } + }, + deleteSenseAction: (state, action) => { + const srcRef = action.payload; 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,216 +85,365 @@ export const mergeDupStepReducer = ( delete words[srcWordId]; } - let sidebar = tree.sidebar; + const sidebar = state.tree.sidebar; + // If the sense is being deleted from the words column + // and the sense is also shown in the sidebar, + // then reset the sidebar. if ( sidebar.wordId === srcRef.wordId && sidebar.mergeSenseId === srcRef.mergeSenseId && srcRef.order === undefined ) { - sidebar = defaultSidebar; + state.tree.sidebar = defaultSidebar; } - - return { ...state, tree: { ...state.tree, words, sidebar } }; - } - - case MergeTreeActionTypes.FLAG_WORD: { - const words: Hash = JSON.parse( - JSON.stringify(state.tree.words) + }, + flagWordAction: (state, action) => { + state.tree.words[action.payload.wordId].flag = action.payload.flag; + }, + getMergeWordsAction: (state) => { + // Handle words with all senses deleted. + const possibleWords = Object.values(state.data.words); + // List of all non-deleted senses. + const nonDeletedSenses = Object.values(state.tree.words).flatMap((w) => + Object.values(w.sensesGuids).flatMap((s) => s) ); - words[action.payload.wordId].flag = action.payload.flag; - return { ...state, tree: { ...state.tree, words } }; - } - - case MergeTreeActionTypes.MOVE_DUPLICATE: { - const srcRef = action.payload.ref; - const destWordId = action.payload.destWordId; - const words: Hash = JSON.parse( - JSON.stringify(state.tree.words) + const deletedWords = possibleWords.filter( + (w) => + !w.senses.map((s) => s.guid).find((g) => nonDeletedSenses.includes(g)) + ); + state.mergeWords = deletedWords.map((w) => + newMergeWords(w, [{ srcWordId: w.id, getAudio: false }], true) ); - const srcWordId = srcRef.wordId; - let mergeSenseId = srcRef.mergeSenseId; - - // Get guid of sense being restored from the sidebar. - if (srcRef.order === undefined) { - return state; + for (const wordId in state.tree.words) { + const mergeWord = state.tree.words[wordId]; + const mergeSenses = buildSenses( + mergeWord.sensesGuids, + state.data, + nonDeletedSenses + ); + const mergeWords = createMergeWords( + wordId, + mergeWord, + mergeSenses, + state.data.words[wordId] + ); + if (mergeWords) { + state.mergeWords.push(mergeWords); + } } - const srcGuids = words[srcWordId].sensesGuids[mergeSenseId]; - const guid = srcGuids.splice(srcRef.order, 1)[0]; + }, + moveSenseAction: (state, action) => { + const srcWordId = action.payload.ref.wordId; + const destWordId = action.payload.destWordId; + const srcOrder = action.payload.ref.order; + if (srcOrder === undefined && srcWordId !== destWordId) { + const mergeSenseId = action.payload.ref.mergeSenseId; + + 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; + } + words[destWordId] = newMergeTreeWord(); + } - // Check if dropping the sense into a new word. - if (words[destWordId] === undefined) { - 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; - if (srcGuids.length === 0) { - // If there are no guids left, this is a full move. - if (srcWordId === destWordId) { - return state; - } + // Cleanup the srcWord. delete words[srcWordId].sensesGuids[mergeSenseId]; if (!Object.keys(words[srcWordId].sensesGuids).length) { delete words[srcWordId]; } - } else { - // Otherwise, create a new sense in the destWord. - mergeSenseId = v4(); } + }, + moveDuplicateAction: (state, action) => { + const srcRef = action.payload.ref; + // Verify that the ref.order field is defined + if (srcRef.order !== undefined) { + const destWordId = action.payload.destWordId; + const words = state.tree.words; - // Update the destWord. - const sensesPairs = Object.entries(words[destWordId].sensesGuids); - sensesPairs.splice(action.payload.destOrder, 0, [mergeSenseId, [guid]]); - 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; + const srcWordId = srcRef.wordId; + let mergeSenseId = srcRef.mergeSenseId; - if (srcWordId === destWordId) { - return state; - } - const words: Hash = JSON.parse( - JSON.stringify(state.tree.words) - ); + // Get guid of sense being restored from the sidebar. + const srcGuids = words[srcWordId].sensesGuids[mergeSenseId]; + const guid = srcGuids.splice(srcRef.order, 1)[0]; - // Check if dropping the sense into a new word. - if (words[destWordId] === undefined) { - if (Object.keys(words[srcWordId].sensesGuids).length === 1) { - return state; + // Check if dropping the sense into a new word. + if (words[destWordId] === undefined) { + words[destWordId] = newMergeTreeWord(); } - 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; + if (srcGuids.length === 0) { + // If there are no guids left, this is a full move. + if (srcWordId === destWordId) { + return; + } + delete words[srcWordId].sensesGuids[mergeSenseId]; + if (!Object.keys(words[srcWordId].sensesGuids).length) { + delete words[srcWordId]; + } + } else { + // Otherwise, create a new sense in the destWord. + mergeSenseId = v4(); + } - // Cleanup the srcWord. - delete words[srcWordId].sensesGuids[mergeSenseId]; - if (!Object.keys(words[srcWordId].sensesGuids).length) { - delete words[srcWordId]; + // Update the destWord. + const sensesPairs = Object.entries(words[destWordId].sensesGuids); + sensesPairs.splice(action.payload.destOrder, 0, [mergeSenseId, [guid]]); + const newSensesGuids: Hash = {}; + sensesPairs.forEach(([key, value]) => (newSensesGuids[key] = value)); + words[destWordId].sensesGuids = newSensesGuids; } - - return { ...state, tree: { ...state.tree, words } }; - } - - case MergeTreeActionTypes.ORDER_DUPLICATE: { + }, + orderDuplicateAction: (state, action) => { const ref = action.payload.ref; const oldOrder = ref.order; const newOrder = action.payload.order; // Ensure the reorder is valid. - if (oldOrder === undefined || oldOrder === newOrder) { - return state; - } - - // Move the guid. - const oldSensesGuids = state.tree.words[ref.wordId].sensesGuids; - const guids = [...oldSensesGuids[ref.mergeSenseId]]; - 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; + if (oldOrder !== undefined && oldOrder !== newOrder) { + // Move the guid. + const oldSensesGuids = state.tree.words[ref.wordId].sensesGuids; + const guids = [...oldSensesGuids[ref.mergeSenseId]]; + const guid = guids.splice(oldOrder, 1)[0]; + guids.splice(newOrder, 0, guid); - return { ...state, tree: { ...state.tree, words } }; - } + const sensesGuids = { ...oldSensesGuids }; + sensesGuids[ref.mergeSenseId] = guids; - 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.ref.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; // Ensure the move is valid. - if (oldOrder === -1 || newOrder === undefined || oldOrder === newOrder) { - return state; - } - - // Move the sense pair to its new place. - const pair = sensePairs.splice(oldOrder, 1)[0]; - sensePairs.splice(newOrder, 0, pair); + if (oldOrder !== -1 && newOrder !== undefined && oldOrder !== newOrder) { + // Move the sense pair to its new place. + const pair = sensePairs.splice(oldOrder, 1)[0]; + sensePairs.splice(newOrder, 0, pair); + + // Rebuild the Hash. + word.sensesGuids = {}; + for (const [key, value] of sensePairs) { + word.sensesGuids[key] = value; + } - // Rebuild the Hash. - word.sensesGuids = {}; - for (const [key, value] of sensePairs) { - word.sensesGuids[key] = value; + state.tree.words[action.payload.ref.wordId] = word; } - - const words = { ...state.tree.words }; - words[action.payload.wordId] = word; - - return { ...state, tree: { ...state.tree, words } }; - } - - case MergeTreeActionTypes.SET_DATA: { + }, + setSidebarAction: (state, action) => { + state.tree.sidebar = action.payload; + }, + setDataAction: (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 } }; + state.tree.words = wordsTree; + state.data = { senses, words }; + state.mergeWords = []; + } + }, + setVernacularAction: (state, action) => { + state.tree.words[action.payload.wordId].vern = action.payload.vern; + }, + }, + extraReducers: (builder) => + builder.addCase(StoreActionTypes.RESET, () => defaultState), +}); + +// Helper Functions + +/** Create hash of senses keyed by id of src word. */ +function buildSenses( + sensesGuids: Hash, + data: MergeData, + nonDeletedSenses: string[] +): Hash { + const senses: Hash = {}; + for (const senseGuids of Object.values(sensesGuids)) { + for (const guid of senseGuids) { + const senseData = data.senses[guid]; + const wordId = senseData.srcWordId; + + if (!senses[wordId]) { + const dbWord = data.words[wordId]; + + // Add each sense into senses as separate or deleted. + senses[wordId] = []; + for (const sense of dbWord.senses) { + senses[wordId].push({ + ...sense, + srcWordId: wordId, + order: senses[wordId].length, + accessibility: nonDeletedSenses.includes(sense.guid) + ? Status.Separate + : Status.Deleted, + protected: sense.accessibility === Status.Protected, + }); + } + } } + } - 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 } }; + // Set sense and duplicate senses. + Object.values(sensesGuids).forEach((guids) => { + const sensesToCombine = guids + .map((g) => data.senses[g]) + .map((s) => senses[s.srcWordId][s.order]); + combineIntoFirstSense(sensesToCombine); + }); + + // Clean order of senses in each src word to reflect backend order. + Object.values(senses).forEach((wordSenses) => { + wordSenses = wordSenses.sort((a, b) => a.order - b.order); + senses[wordSenses[0].srcWordId] = wordSenses; + }); + + return senses; +} + +function createMergeWords( + wordId: string, + mergeWord: MergeTreeWord, + mergeSenses: Hash, + word: Word +): MergeWords | undefined { + // Don't return empty merges: when the only child is the parent word + // and has the same number of senses as parent (all Active/Protected) + // and has the same flag. + if (Object.values(mergeSenses).length === 1) { + const onlyChild = Object.values(mergeSenses)[0]; + if ( + onlyChild[0].srcWordId === wordId && + onlyChild.length === word.senses.length && + !onlyChild.find( + (s) => ![Status.Active, Status.Protected].includes(s.accessibility) + ) && + compareFlags(mergeWord.flag, word.flag) === 0 + ) { + return; } + } - case StoreActionTypes.RESET: { - return defaultState; + // Construct parent and children. + const parent: Word = { + ...word, + senses: [], + flag: mergeWord.flag, + }; + if (!parent.vernacular) { + parent.vernacular = mergeWord.vern; + } + const children: MergeSourceWord[] = Object.values(mergeSenses).map( + (sList) => { + sList.forEach((sense) => { + if ([Status.Active, Status.Protected].includes(sense.accessibility)) { + parent.senses.push({ + guid: sense.guid, + definitions: sense.definitions, + glosses: sense.glosses, + semanticDomains: sense.semanticDomains, + accessibility: sense.accessibility, + grammaticalInfo: sense.grammaticalInfo, + }); + } + }); + const getAudio = !sList.find((s) => s.accessibility === Status.Separate); + return { srcWordId: sList[0].srcWordId, getAudio }; } - - default: { - return state; + ); + + return newMergeWords(parent, children); +} + +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. + 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 }); + } + }); + }); +} + +export const { + clearMergeWordsAction, + clearTreeAction, + combineSenseAction, + deleteSenseAction, + flagWordAction, + getMergeWordsAction, + moveDuplicateAction, + moveSenseAction, + orderDuplicateAction, + orderSenseAction, + setDataAction, + setSidebarAction, + setVernacularAction, +} = mergeDuplicatesSlice.actions; + +export default mergeDuplicatesSlice.reducer; diff --git a/src/goals/MergeDuplicates/Redux/MergeDupsReduxTypes.ts b/src/goals/MergeDuplicates/Redux/MergeDupsReduxTypes.ts index b679ec586c..9f526b59cb 100644 --- a/src/goals/MergeDuplicates/Redux/MergeDupsReduxTypes.ts +++ b/src/goals/MergeDuplicates/Redux/MergeDupsReduxTypes.ts @@ -1,98 +1,38 @@ -import { Flag, Word } from "api/models"; +import { Flag, MergeWords } 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 { data: MergeData; tree: MergeTree; + mergeWords: MergeWords[]; } -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/goals/MergeDuplicates/Redux/tests/MergeDupsActions.test.tsx b/src/goals/MergeDuplicates/Redux/tests/MergeDupsActions.test.tsx index e026ecf36e..c5fcccc0b2 100644 --- a/src/goals/MergeDuplicates/Redux/tests/MergeDupsActions.test.tsx +++ b/src/goals/MergeDuplicates/Redux/tests/MergeDupsActions.test.tsx @@ -1,42 +1,23 @@ -import configureMockStore from "redux-mock-store"; -import thunk from "redux-thunk"; - -import { GramCatGroup, MergeWords, Sense, Status, Word } from "api/models"; +import { MergeWords, Sense, Status, Word } from "api/models"; +import { defaultState } from "components/App/DefaultState"; import { defaultTree, MergeData, MergeTree, - MergeTreeReference, - MergeTreeSense, newMergeTreeSense, newMergeTreeWord, } from "goals/MergeDuplicates/MergeDupsTreeTypes"; import { MergeDups, newMergeWords } from "goals/MergeDuplicates/MergeDupsTypes"; import { - combineIntoFirstSense, + deferMerge, dispatchMergeStepData, mergeAll, - mergeDefinitionIntoSense, - moveSense, - orderSense, + setData, } from "goals/MergeDuplicates/Redux/MergeDupsActions"; -import { - MergeTreeAction, - MergeTreeActionTypes, - 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"; +import { setupStore } from "store"; +import { GoalType } from "types/goals"; +import { multiSenseWord, newFlag, newWord } from "types/word"; // Used when the guids don't matter. function wordAnyGuids(vern: string, senses: Sense[], id: string): Word { @@ -48,11 +29,13 @@ function wordAnyGuids(vern: string, senses: Sense[], id: string): Word { }; } +const mockGraylistAdd = jest.fn(); const mockMergeWords = jest.fn(); jest.mock("backend", () => ({ blacklistAdd: jest.fn(), getWord: jest.fn(), + graylistAdd: () => mockGraylistAdd(), mergeWords: (mergeWordsArray: MergeWords[]) => mockMergeWords(mergeWordsArray), })); @@ -60,12 +43,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(), @@ -73,7 +53,12 @@ const mockStoreState: { history: [mockGoal], previousGoalType: GoalType.Default, }, - mergeDuplicateGoal: { data: {} as MergeData, tree: {} as MergeTree }, + mergeDuplicateGoal: { + data: {} as MergeData, + tree: {} as MergeTree, + mergeWords: [], + }, + _persist: { version: 1, rehydrated: false }, }; const vernA = "AAA"; @@ -112,11 +97,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, - mergeDuplicateGoal: { data, tree }, + const store = setupStore({ + ...preloadedState, + mergeDuplicateGoal: { data, tree, mergeWords: [] }, }); - await mockStore.dispatch(mergeAll()); + await store.dispatch(mergeAll()); expect(mockMergeWords).not.toHaveBeenCalled(); }); @@ -126,11 +111,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, - mergeDuplicateGoal: { data, tree }, + const store = setupStore({ + ...preloadedState, + mergeDuplicateGoal: { data, tree, mergeWords: [] }, }); - await mockStore.dispatch(mergeAll()); + await store.dispatch(mergeAll()); expect(mockMergeWords).toHaveBeenCalledTimes(1); const parentA = wordAnyGuids(vernA, [senses["S1"], senses["S2"]], idA); @@ -151,11 +136,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, - mergeDuplicateGoal: { data, tree }, + const store = setupStore({ + ...preloadedState, + mergeDuplicateGoal: { data, tree, mergeWords: [] }, }); - await mockStore.dispatch(mergeAll()); + await store.dispatch(mergeAll()); expect(mockMergeWords).toHaveBeenCalledTimes(1); const parentA = wordAnyGuids( @@ -180,11 +165,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, - mergeDuplicateGoal: { data, tree }, + const store = setupStore({ + ...preloadedState, + mergeDuplicateGoal: { data, tree, mergeWords: [] }, }); - await mockStore.dispatch(mergeAll()); + await store.dispatch(mergeAll()); expect(mockMergeWords).toHaveBeenCalledTimes(1); @@ -199,11 +184,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, - mergeDuplicateGoal: { data, tree }, + const store = setupStore({ + ...preloadedState, + mergeDuplicateGoal: { data, tree, mergeWords: [] }, }); - await mockStore.dispatch(mergeAll()); + await store.dispatch(mergeAll()); expect(mockMergeWords).toHaveBeenCalledTimes(1); const parent = wordAnyGuids(vernA, [senses["S1"]], idA); @@ -216,11 +201,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, - mergeDuplicateGoal: { data, tree }, + const store = setupStore({ + ...preloadedState, + mergeDuplicateGoal: { data, tree, mergeWords: [] }, }); - await mockStore.dispatch(mergeAll()); + await store.dispatch(mergeAll()); expect(mockMergeWords).toHaveBeenCalledTimes(1); const child = { srcWordId: idB, getAudio: false }; @@ -234,11 +219,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, - mergeDuplicateGoal: { data, tree }, + const store = setupStore({ + ...preloadedState, + mergeDuplicateGoal: { data, tree, mergeWords: [] }, }); - await mockStore.dispatch(mergeAll()); + await store.dispatch(mergeAll()); expect(mockMergeWords).toHaveBeenCalledTimes(1); @@ -251,158 +236,29 @@ describe("MergeDupActions", () => { }); describe("dispatchMergeStepData", () => { - it("creates an action to add MergeDups data", async () => { + it("creates an action to add MergeDups data", () => { 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]); - }); - }); - - describe("moveSense", () => { - const wordId = "mockWordId"; - const mergeSenseId = "mockSenseId"; - - 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); - }); - - 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 store = setupStore(); + store.dispatch(dispatchMergeStepData(goal)); + const setDataAction = setData(goalDataMock.plannedWords[0]); + expect(setDataAction.type).toEqual("mergeDupStepReducer/setDataAction"); }); }); - describe("orderSense", () => { - const wordId = "mockWordId"; - const mergeSenseId = "mockSenseId"; - const mockOrder = 0; - - 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); - }); - - 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); - }); - }); - - 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", () => { - const s4 = [newSense(), newSense(), newSense(), newSense()].map( - (s) => s as MergeTreeSense - ); - combineIntoFirstSense(s4); - expect(s4[0].accessibility).not.toBe(Status.Duplicate); - expect( - s4.filter((s) => s.accessibility === Status.Duplicate) - ).toHaveLength(s4.length - 1); - }); - - it("gives the first sense the earliest part of speech found in all senses", () => { - const s3 = [newSense(), newSense(), newSense()].map( - (s) => s as MergeTreeSense - ); - const gramInfo = { - catGroup: GramCatGroup.Verb, - grammaticalCategory: "vt", - }; - s3[1].grammaticalInfo = { ...gramInfo }; - s3[2].grammaticalInfo = { - catGroup: GramCatGroup.Preverb, - grammaticalCategory: "prev", - }; - combineIntoFirstSense(s3); - expect(s3[0].grammaticalInfo).toEqual(gramInfo); - - // Ensure the first sense's grammaticalInfo doesn't get overwritten. - s3[1].grammaticalInfo = newGrammaticalInfo(); - combineIntoFirstSense(s3); - expect(s3[0].grammaticalInfo).toEqual(gramInfo); - }); - - it("adds domains to first sense from other senses", () => { - const s3 = [newSense(), newSense(), newSense()].map( - (s) => s as MergeTreeSense - ); - s3[1].semanticDomains = [ - newSemanticDomain("1", "uno"), - newSemanticDomain("2", "dos"), - ]; - s3[2].semanticDomains = [newSemanticDomain("3", "three")]; - combineIntoFirstSense(s3); - expect(s3[0].semanticDomains).toHaveLength(3); - }); - - it("doesn't adds domains it already has", () => { - const s2 = [newSense(), newSense()].map((s) => s as MergeTreeSense); - s2[0].semanticDomains = [newSemanticDomain("1", "one")]; - s2[1].semanticDomains = [ - newSemanticDomain("1", "uno"), - newSemanticDomain("2", "dos"), - ]; - combineIntoFirstSense(s2); - expect(s2[0].semanticDomains).toHaveLength(2); + describe("deferMerge", () => { + it("add merge to graylist", () => { + const WA = newMergeTreeWord(vernA, { ID1: [S1], ID2: [S2] }); + WA.flag = newFlag("New flag"); + const WB = newMergeTreeWord(vernB, { ID1: [S3], ID2: [S4] }); + const tree: MergeTree = { ...defaultTree, words: { WA, WB } }; + const store = setupStore({ + ...preloadedState, + mergeDuplicateGoal: { data, tree, mergeWords: [] }, + }); + store.dispatch(deferMerge()); + expect(mockGraylistAdd).toHaveBeenCalledTimes(1); }); }); }); diff --git a/src/goals/MergeDuplicates/Redux/tests/MergeDupsDataMock.ts b/src/goals/MergeDuplicates/Redux/tests/MergeDupsDataMock.ts index c4786a8cef..0f70b78534 100644 --- a/src/goals/MergeDuplicates/Redux/tests/MergeDupsDataMock.ts +++ b/src/goals/MergeDuplicates/Redux/tests/MergeDupsDataMock.ts @@ -1,6 +1,14 @@ -import { Word } from "api/models"; +import type { PreloadedState } from "@reduxjs/toolkit"; +import { Definition, SemanticDomain, Word } from "api/models"; +import { defaultState } from "components/App/DefaultState"; +import { + convertSenseToMergeTreeSense, + convertWordToMergeTreeWord, + newMergeTreeWord, +} from "goals/MergeDuplicates/MergeDupsTreeTypes"; import { MergeDupsData } from "goals/MergeDuplicates/MergeDupsTypes"; -import { simpleWord } from "types/word"; +import { RootState } from "store"; +import { newSense, newWord, simpleWord } from "types/word"; const wordsArrayMock = (): Word[] => [ // Each simpleWord() has a randomly generated id @@ -26,3 +34,271 @@ export const goalDataMock: MergeDupsData = { wordsArrayMock(), ], }; + +// Words/Senses to be used for a preloaded mergeDuplicateGoal state +// in the unit tests for MergeDuplicates Actions/Reducer +const semDomSocial: SemanticDomain = { + guid: "00000000-0000-0000-0000-000000000000", + name: "Social behavior", + id: "4", + lang: "", +}; + +const semDomLanguage: SemanticDomain = { + guid: "00000000-0000-0000-0000-000000000000", + name: "Language and thought", + id: "3", + lang: "", +}; + +const definitionBah = { language: "en", text: "defBah" }; +const definitionBag = { language: "en", text: "defBag" }; +const definitionBagBah = { language: "en", text: "defBag;defBah" }; + +const senseBag = { + ...newSense("bag"), + guid: "guid-sense-bag", + semanticDomains: [semDomLanguage], + definitions: [definitionBag], +}; +const senseBah = { + ...newSense("bah"), + guid: "guid-sense-bah", + semanticDomains: [semDomSocial], + definitions: [definitionBah], +}; +const senseBar = { + ...newSense("bar"), + guid: "guid-sense-bar", + semanticDomains: [semDomLanguage], +}; +const senseBaz = { ...newSense("baz"), guid: "guid-sense-baz" }; + +const wordFoo1 = { ...newWord("foo"), id: "wordId-foo1", senses: [senseBah] }; +const wordFoo2 = { + ...newWord("foo"), + id: "wordId-foo2", + senses: [senseBar, senseBaz], +}; + +// Preloaded values for store when testing the MergeDups Goal +const persistedDefaultState: PreloadedState = { + ...defaultState, + _persist: { version: 1, rehydrated: false }, +}; + +export type ExpectedScenarioResult = { + // wordId for the parent word + parent: string; + // sense guids in the parent word + senses: string[]; + // semantic domain ids in the parent word + semDoms: string[]; + // definitions in the merged sense + defs: Definition[][]; + // child source word ids + children: string[]; +}; + +export type GetMergeWordsScenario = { + initialState: () => PreloadedState; + expectedResult: ExpectedScenarioResult[]; +}; + +// Scenario: +// Word1: +// vern: foo +// senses: bah +// +// Word2: +// vern: foo +// senses: bar, baz +// +// Sense "bah" is dragged to "Word2" as an additional sense +export const mergeTwoWordsScenario: GetMergeWordsScenario = { + initialState: () => { + return { + ...persistedDefaultState, + mergeDuplicateGoal: { + data: { + senses: { + "guid-sense-bah": convertSenseToMergeTreeSense( + senseBah, + wordFoo1.id, + 0 + ), + "guid-sense-bar": convertSenseToMergeTreeSense( + senseBar, + wordFoo2.id, + 0 + ), + "guid-sense-baz": convertSenseToMergeTreeSense( + senseBaz, + wordFoo2.id, + 1 + ), + }, + words: { + "wordId-foo1": wordFoo1, + "wordId-foo2": wordFoo2, + }, + }, + tree: { + sidebar: { + senses: [], + wordId: "", + mergeSenseId: "", + }, + words: { + "wordId-foo2": convertWordToMergeTreeWord({ + ...wordFoo2, + senses: [senseBar, senseBaz, senseBah], + }), + }, + }, + mergeWords: [], + }, + }; + }, + expectedResult: [ + { + parent: "wordId-foo2", + senses: ["guid-sense-bah", "guid-sense-bar", "guid-sense-baz"], + semDoms: ["3", "4"], + defs: [[], [], [definitionBah]], + children: ["wordId-foo1", "wordId-foo2"], + }, + ], +}; + +// Scenario: +// Word1: +// vern: foo +// senses: bah +// +// Word2: +// vern: foo +// senses: bar, baz +// +// Sense "bah" is dragged to Word2 and merged with sense "bar" +export const mergeTwoSensesScenario: GetMergeWordsScenario = { + initialState: () => { + return { + ...persistedDefaultState, + mergeDuplicateGoal: { + data: { + senses: { + "guid-sense-bah": convertSenseToMergeTreeSense( + senseBah, + wordFoo1.id, + 0 + ), + "guid-sense-bar": convertSenseToMergeTreeSense( + senseBar, + wordFoo2.id, + 0 + ), + "guid-sense-baz": convertSenseToMergeTreeSense( + senseBaz, + wordFoo2.id, + 1 + ), + }, + words: { + "wordId-foo1": wordFoo1, + "wordId-foo2": wordFoo2, + }, + }, + tree: { + sidebar: { + senses: [], + wordId: "", + mergeSenseId: "", + }, + words: { + "wordId-foo2": newMergeTreeWord(wordFoo2.vernacular, { + word2_senseA: [senseBar.guid], + word2_senseB: [senseBaz.guid, senseBah.guid], + }), + }, + }, + mergeWords: [], + }, + }; + }, + expectedResult: [ + { + parent: "wordId-foo2", + senses: ["guid-sense-bar", "guid-sense-baz"], + semDoms: ["3", "4"], + defs: [[], [definitionBah]], + children: ["wordId-foo1", "wordId-foo2"], + }, + ], +}; + +// Scenario: +// Word1: +// vern: foo +// senses: bah +// +// Word2: +// vern: foo +// senses: bar, bag +// +// Sense "bah" is dragged to Word2 and merged with sense "bag" +export const mergeTwoDefinitionsScenario: GetMergeWordsScenario = { + initialState: () => { + return { + ...persistedDefaultState, + mergeDuplicateGoal: { + data: { + senses: { + "guid-sense-bah": convertSenseToMergeTreeSense( + senseBah, + wordFoo1.id, + 0 + ), + "guid-sense-bar": convertSenseToMergeTreeSense( + senseBar, + wordFoo2.id, + 0 + ), + "guid-sense-bag": convertSenseToMergeTreeSense( + senseBag, + wordFoo2.id, + 1 + ), + }, + words: { + "wordId-foo1": wordFoo1, + "wordId-foo2": { ...wordFoo2, senses: [senseBar, senseBag] }, + }, + }, + tree: { + sidebar: { + senses: [], + wordId: "", + mergeSenseId: "", + }, + words: { + "wordId-foo2": newMergeTreeWord(wordFoo2.vernacular, { + word2_senseA: [senseBar.guid], + word2_senseB: [senseBag.guid, senseBah.guid], + }), + }, + }, + mergeWords: [], + }, + }; + }, + expectedResult: [ + { + parent: "wordId-foo2", + senses: ["guid-sense-bag", "guid-sense-bar"], + semDoms: ["3", "3", "4"], + defs: [[], [definitionBagBah]], + children: ["wordId-foo1", "wordId-foo2"], + }, + ], +}; diff --git a/src/goals/MergeDuplicates/Redux/tests/MergeDupsReducer.test.tsx b/src/goals/MergeDuplicates/Redux/tests/MergeDupsReducer.test.tsx index d0ce755111..8147bfee19 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,17 +7,27 @@ import { MergeTreeWord, newMergeTreeWord, } from "goals/MergeDuplicates/MergeDupsTreeTypes"; -import * as Actions from "goals/MergeDuplicates/Redux/MergeDupsActions"; import { + clearTree, + combineSense, + deleteSense, + flagWord, + getMergeWords, + moveSense, + orderSense, + setData, +} from "goals/MergeDuplicates/Redux/MergeDupsActions"; +import mergeDupStepReducer, { defaultState, - mergeDupStepReducer, } from "goals/MergeDuplicates/Redux/MergeDupsReducer"; +import { MergeTreeState } from "goals/MergeDuplicates/Redux/MergeDupsReduxTypes"; import { - MergeTreeAction, - MergeTreeActionTypes, - MergeTreeState, -} from "goals/MergeDuplicates/Redux/MergeDupsReduxTypes"; + mergeTwoDefinitionsScenario, + mergeTwoSensesScenario, + mergeTwoWordsScenario, +} from "goals/MergeDuplicates/Redux/tests/MergeDupsDataMock"; import { StoreAction, StoreActionTypes } from "rootActions"; +import { setupStore } from "store"; import { Hash } from "types/hash"; import { newFlag, testWordList } from "types/word"; @@ -37,13 +49,7 @@ beforeEach(() => { mockUuid.v4.mockImplementation(getMockUuid); }); -describe("MergeDupReducer", () => { - // a state with no duplicate senses - const initState = mergeDupStepReducer( - undefined, - Actions.setWordData(testWordList()) - ); - +describe("MergeDupsReducer", () => { // helper functions for working with a tree const getRefByGuid = ( guid: string, @@ -62,8 +68,12 @@ describe("MergeDupReducer", () => { }; test("clearTree", () => { - const newState = mergeDupStepReducer(initState, Actions.clearTree()); - expect(JSON.stringify(newState)).toEqual(JSON.stringify(defaultState)); + const store = setupStore(); + store.dispatch(setData(testWordList())); + store.dispatch(clearTree()); + expect(JSON.stringify(store.getState().mergeDuplicateGoal)).toEqual( + JSON.stringify(defaultState) + ); }); function testTreeWords(): Hash { @@ -89,9 +99,10 @@ describe("MergeDupReducer", () => { sidebar: defaultSidebar, words: testTreeWords(), }, + mergeWords: [], }; function checkTreeWords( - action: MergeTreeAction, + action: Action | PayloadAction, expected: Hash ): void { const result = mergeDupStepReducer(mockState, action).tree.words; @@ -115,7 +126,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 = { @@ -142,7 +153,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 = { @@ -169,7 +180,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]; @@ -192,7 +203,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 = { @@ -215,7 +226,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 = { @@ -241,7 +252,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]; @@ -262,7 +273,7 @@ describe("MergeDupReducer", () => { mergeSenseId: `${wordId}_senseA`, }; - const testAction = Actions.deleteSense(testRef); + const testAction = deleteSense(testRef); const expectedWords = testTreeWords(); delete expectedWords[wordId].sensesGuids[testRef.mergeSenseId]; @@ -277,7 +288,7 @@ describe("MergeDupReducer", () => { mergeSenseId: `${wordId}_senseB`, }; - const testAction = Actions.deleteSense(testRef); + const testAction = deleteSense(testRef); const expectedWords = testTreeWords(); delete expectedWords[wordId].sensesGuids[testRef.mergeSenseId]; @@ -292,7 +303,7 @@ describe("MergeDupReducer", () => { mergeSenseId: `${wordId}_senseA`, }; - const testAction = Actions.deleteSense(testRef); + const testAction = deleteSense(testRef); const expectedWords = testTreeWords(); delete expectedWords[wordId]; @@ -308,7 +319,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"] }; @@ -324,7 +335,7 @@ describe("MergeDupReducer", () => { order: 0, }; - const testAction = Actions.deleteSense(srcRef); + const testAction = deleteSense(srcRef); const expectedWords = testTreeWords(); delete expectedWords[srcWordId]; @@ -337,7 +348,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; @@ -346,6 +357,59 @@ describe("MergeDupReducer", () => { }); }); + describe("getMergeWords", () => { + it("sense moved from one word to another", () => { + const store = setupStore(mergeTwoWordsScenario.initialState()); + store.dispatch(getMergeWords()); + const mergeArray = store.getState().mergeDuplicateGoal.mergeWords; + const expectedResult = mergeTwoWordsScenario.expectedResult; + expect(mergeArray.length).toEqual(1); + expect(mergeArray[0].parent.id).toEqual(expectedResult[0].parent); + const senses = mergeArray[0].parent.senses.map((s) => s.guid).sort(); + expect(senses).toEqual(expectedResult[0].senses); + const semDoms = mergeArray[0].parent.senses + .flatMap((s) => s.semanticDomains.map((d) => d.id)) + .sort(); + expect(semDoms).toEqual(expectedResult[0].semDoms); + const defs = mergeArray[0].parent.senses.map((s) => s.definitions); + expect(defs).toEqual(expectedResult[0].defs); + }); + + it("sense from one word combined with sense in another", () => { + const store = setupStore(mergeTwoSensesScenario.initialState()); + store.dispatch(getMergeWords()); + const mergeArray = store.getState().mergeDuplicateGoal.mergeWords; + const expectedResult = mergeTwoSensesScenario.expectedResult; + expect(mergeArray.length).toEqual(1); + expect(mergeArray[0].parent.id).toEqual(expectedResult[0].parent); + const senses = mergeArray[0].parent.senses.map((s) => s.guid).sort(); + expect(senses).toEqual(expectedResult[0].senses); + const semDoms = mergeArray[0].parent.senses + .flatMap((s) => s.semanticDomains.map((d) => d.id)) + .sort(); + expect(semDoms).toEqual(expectedResult[0].semDoms); + const defs = mergeArray[0].parent.senses.map((s) => s.definitions); + expect(defs).toEqual(expectedResult[0].defs); + }); + + it("combine senses with definitions", () => { + const store = setupStore(mergeTwoDefinitionsScenario.initialState()); + store.dispatch(getMergeWords()); + const mergeArray = store.getState().mergeDuplicateGoal.mergeWords; + const expectedResult = mergeTwoDefinitionsScenario.expectedResult; + expect(mergeArray.length).toEqual(1); + expect(mergeArray[0].parent.id).toEqual(expectedResult[0].parent); + const senses = mergeArray[0].parent.senses.map((s) => s.guid).sort(); + expect(senses).toEqual(expectedResult[0].senses); + const semDoms = mergeArray[0].parent.senses + .flatMap((s) => s.semanticDomains.map((d) => d.id)) + .sort(); + expect(semDoms).toEqual(expectedResult[0].semDoms); + const defs = mergeArray[0].parent.senses.map((s) => s.definitions); + expect(defs).toEqual(expectedResult[0].defs); + }); + }); + describe("moveSense", () => { it("moves a sense out from sidebar to same word", () => { const wordId = "word2"; @@ -358,7 +422,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"] }; @@ -382,7 +450,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 = { @@ -405,7 +477,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 = { @@ -428,7 +504,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 = { @@ -451,8 +531,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("mergeDupStepReducer/moveSenseAction"); const expectedWords = testTreeWords(); delete expectedWords[srcWordId]; @@ -471,7 +555,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] = [ @@ -487,7 +571,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 = { @@ -511,10 +595,7 @@ describe("MergeDupReducer", () => { test("setWordData", () => { const wordList = testWordList(); - const treeState = mergeDupStepReducer( - undefined, - Actions.setWordData(wordList) - ); + const treeState = mergeDupStepReducer(undefined, setData(wordList)); // check if data has all words present for (const word of wordList) { const srcWordId = word.id; diff --git a/src/rootReducer.ts b/src/rootReducer.ts index a617d965fc..34dde802f9 100644 --- a/src/rootReducer.ts +++ b/src/rootReducer.ts @@ -7,7 +7,7 @@ import exportProjectReducer from "components/ProjectExport/Redux/ExportProjectRe 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";