Skip to content

Commit

Permalink
[MergeDups] Refactor frontend logic and show audio count (#2893)
Browse files Browse the repository at this point in the history
  • Loading branch information
imnasnainaec authored Apr 19, 2024
1 parent e72e2eb commit 613bd07
Show file tree
Hide file tree
Showing 9 changed files with 221 additions and 145 deletions.
3 changes: 1 addition & 2 deletions Backend/Models/Sense.cs
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,6 @@ public enum Status
Active,
Deleted,
Duplicate,
Protected,
Separate
Protected
}
}
1 change: 0 additions & 1 deletion src/api/models/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,4 @@ export enum Status {
Deleted = "Deleted",
Duplicate = "Duplicate",
Protected = "Protected",
Separate = "Separate",
}
18 changes: 14 additions & 4 deletions src/goals/MergeDuplicates/MergeDupsStep/MergeDragDrop/DropWord.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,25 @@ import {
Select,
Typography,
} from "@mui/material";
import { ReactElement } from "react";
import { type ReactElement } from "react";
import { Droppable } from "react-beautiful-dnd";
import { useTranslation } from "react-i18next";

import { Flag, ProtectReason, ReasonType } from "api/models";
import { type Flag, type ProtectReason, ReasonType } from "api/models";
import {
FlagButton,
IconButtonWithTooltip,
NoteButton,
} from "components/Buttons";
import MultilineTooltipTitle from "components/MultilineTooltipTitle";
import { AudioSummary } from "components/WordCard";
import DragSense from "goals/MergeDuplicates/MergeDupsStep/MergeDragDrop/DragSense";
import { MergeTreeWord } from "goals/MergeDuplicates/MergeDupsTreeTypes";
import { type MergeTreeWord } from "goals/MergeDuplicates/MergeDupsTreeTypes";
import {
flagWord,
setVern,
} from "goals/MergeDuplicates/Redux/MergeDupsActions";
import { StoreState } from "types";
import { type StoreState } from "types";
import { useAppDispatch, useAppSelector } from "types/hooks";
import theme from "types/theme";
import { TypographyWithFont } from "utilities/fontComponents";
Expand Down Expand Up @@ -110,6 +111,9 @@ export function DropWordCardHeader(
const { senses, words } = useAppSelector(
(state: StoreState) => state.mergeDuplicateGoal.data
);
const { counts, moves } = useAppSelector(
(state: StoreState) => state.mergeDuplicateGoal.audio
);

const { t } = useTranslation();

Expand All @@ -126,6 +130,11 @@ export function DropWordCardHeader(
...new Set(guids.map((g) => words[senses[g].srcWordId].vernacular)),
];

// Compute how many audio pronunciations the word will have post-merge.
const otherIds = moves[props.wordId] ?? [];
const otherCount = otherIds.reduce((sum, id) => sum + counts[id], 0);
const audioCount = (treeWord?.audioCount ?? 0) + otherCount;

// Reset vern if not in vern list.
if (treeWord && !verns.includes(treeWord.vern)) {
dispatchSetVern(verns.length ? verns[0] : "");
Expand Down Expand Up @@ -231,6 +240,7 @@ export function DropWordCardHeader(
text={<MultilineTooltipTitle lines={tooltipTexts} />}
/>
)}
<AudioSummary count={audioCount} />
{treeWord.note.text ? <NoteButton noteText={treeWord.note.text} /> : null}
<FlagButton
buttonId={`word-${props.wordId}-flag`}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ jest.mock("backend", () => ({}));
jest.mock("goals/MergeDuplicates/Redux/MergeDupsActions", () => ({
setSidebar: (...args: any[]) => mockSetSidebar(...args),
}));
// Mock "i18n", else `Error: connect ECONNREFUSED ::1:80`
jest.mock("i18n", () => ({}));
jest.mock("types/hooks", () => {
return {
...jest.requireActual("types/hooks"),
Expand Down
3 changes: 3 additions & 0 deletions src/goals/MergeDuplicates/MergeDupsTreeTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export interface MergeTreeWord {
flag: Flag;
note: Note;
protected: boolean;
audioCount: number;
}

export function newMergeTreeSense(
Expand All @@ -63,6 +64,7 @@ export function newMergeTreeWord(
flag: newFlag(),
note: newNote(),
protected: false,
audioCount: 0,
};
}

Expand All @@ -87,6 +89,7 @@ export function convertWordToMergeTreeWord(word: Word): MergeTreeWord {
mergeTreeWord.flag = { ...word.flag };
mergeTreeWord.note = { ...word.note };
mergeTreeWord.protected = word.accessibility === Status.Protected;
mergeTreeWord.audioCount = word.audio.length;
return mergeTreeWord;
}

Expand Down
84 changes: 62 additions & 22 deletions src/goals/MergeDuplicates/Redux/MergeDupsReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,17 @@ import {
defaultTree,
newMergeTreeWord,
} from "goals/MergeDuplicates/MergeDupsTreeTypes";
import { newMergeWords } from "goals/MergeDuplicates/MergeDupsTypes";
import { defaultState } from "goals/MergeDuplicates/Redux/MergeDupsReduxTypes";
import {
buildSenses,
createMergeWords,
defaultAudio,
defaultState,
} from "goals/MergeDuplicates/Redux/MergeDupsReduxTypes";
import {
combineIntoFirstSense,
createMergeChildren,
createMergeParent,
gatherWordSenses,
getDeletedMergeWords,
isEmptyMerge,
} from "goals/MergeDuplicates/Redux/reducerUtilities";
import { StoreActionTypes } from "rootActions";
import { type Hash } from "types/hash";
Expand Down Expand Up @@ -80,6 +86,7 @@ const mergeDuplicatesSlice = createSlice({
deletedSenseGuids.push(...srcGuids);
delete sensesGuids[srcRef.mergeSenseId];
if (!Object.keys(sensesGuids).length) {
delete state.audio.moves[srcWordId];
delete words[srcWordId];
}

Expand All @@ -104,32 +111,51 @@ const mergeDuplicatesSlice = createSlice({
},

getMergeWordsAction: (state) => {
// Handle words with all senses deleted.
const possibleWords = Object.values(state.data.words);
const dataWords = Object.values(state.data.words);
const deletedSenseGuids = state.tree.deletedSenseGuids;
const deletedWords = possibleWords.filter((w) =>
w.senses.every((s) => deletedSenseGuids.includes(s.guid))
);
state.mergeWords = deletedWords.map((w) =>
newMergeWords(w, [{ srcWordId: w.id, getAudio: false }], true)

// First handle words with all senses deleted.
state.mergeWords = getDeletedMergeWords(dataWords, deletedSenseGuids);

// Then build the rest of the mergeWords.

// Gather all senses (accessibility will be updated as mergeWords are built).
const wordTreeSenses = gatherWordSenses(dataWords, deletedSenseGuids);
const allSenses = Object.values(wordTreeSenses).flatMap((mergeSenses) =>
mergeSenses.map((ms) => ms.sense)
);

// Build one merge word per column.
for (const wordId in state.tree.words) {
// Get from tree the basic info for this column.
const mergeWord = state.tree.words[wordId];
const mergeSenses = buildSenses(
mergeWord.sensesGuids,
state.data,
deletedSenseGuids

// Get from data all senses in this column.
const mergeSenses = Object.values(mergeWord.sensesGuids).map((guids) =>
guids.map((g) => state.data.senses[g])
);
const mergeWords = createMergeWords(
wordId,
mergeWord,

// Update those senses in the set of all senses.
mergeSenses.forEach((senses) => {
const sensesToUpdate = senses.map(
(s) => wordTreeSenses[s.srcWordId][s.order]
);
combineIntoFirstSense(sensesToUpdate);
});

// Check if nothing to merge.
const wordToUpdate = state.data.words[wordId];
if (isEmptyMerge(wordToUpdate, mergeWord)) {
continue;
}

// Create merge words.
const children = createMergeChildren(
mergeSenses,
state.data.words[wordId]
state.audio.moves[wordId]
);
if (mergeWords) {
state.mergeWords.push(mergeWords);
}
const parent = createMergeParent(wordToUpdate, mergeWord, allSenses);
state.mergeWords.push({ parent, children, deleteOnly: false });
}
},

Expand Down Expand Up @@ -161,6 +187,17 @@ const mergeDuplicatesSlice = createSlice({
// Cleanup the srcWord.
delete words[srcWordId].sensesGuids[mergeSenseId];
if (!Object.keys(words[srcWordId].sensesGuids).length) {
// If this was the word's last sense, move the audio...
const moves = state.audio.moves;
if (!Object.keys(moves).includes(destWordId)) {
moves[destWordId] = [];
}
moves[destWordId].push(srcWordId);
if (Object.keys(moves).includes(srcWordId)) {
moves[destWordId].push(...moves[srcWordId]);
delete moves[srcWordId];
}
// ...and delete the word from the tree
delete words[srcWordId];
}
}
Expand Down Expand Up @@ -254,15 +291,18 @@ const mergeDuplicatesSlice = createSlice({
const words: Hash<Word> = {};
const senses: Hash<MergeTreeSense> = {};
const wordsTree: Hash<MergeTreeWord> = {};
const counts: Hash<number> = {};
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);
counts[word.id] = word.audio.length;
});
state.data = { ...defaultData, senses, words };
state.tree = { ...defaultTree, words: wordsTree };
state.audio = { ...defaultAudio, counts };
state.mergeWords = [];
}
},
Expand Down
20 changes: 20 additions & 0 deletions src/goals/MergeDuplicates/Redux/MergeDupsReduxTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,38 @@ import {
defaultData,
defaultTree,
} from "goals/MergeDuplicates/MergeDupsTreeTypes";
import { type Hash } from "types/hash";

// Redux state

/** `.counts` is a dictionary of all audio counts of the words being merged:
* - key: id of a word in the set of potential duplicates
* - value: number of audio pronunciations on the word
*
* `.moves` is a dictionary of words receiving the audio of other words:
* - key: id of a word receiving audio
* - value: array of ids of words whose audio is being received */
export interface MergeAudio {
counts: Hash<number>;
moves: Hash<string[]>;
}

export const defaultAudio: MergeAudio = {
counts: {},
moves: {},
};

export interface MergeTreeState {
data: MergeData;
tree: MergeTree;
audio: MergeAudio;
mergeWords: MergeWords[];
}

export const defaultState: MergeTreeState = {
data: defaultData,
tree: defaultTree,
audio: defaultAudio,
mergeWords: [],
};

Expand Down
Loading

0 comments on commit 613bd07

Please sign in to comment.