From a25f160eb93ad3a1bed89cdf6ed7784e85eaf1e7 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Wed, 25 Oct 2023 11:12:33 -0400 Subject: [PATCH 01/11] Add Redux for completed ReviewEntries edits --- .../GoalTimeline/Redux/GoalActions.ts | 6 ++++++ .../GoalTimeline/Redux/GoalReducer.ts | 20 +++++++++++++++++++ src/goals/ReviewEntries/ReviewEntries.ts | 9 +++++++++ .../Redux/ReviewEntriesActions.ts | 1 + src/types/goals.ts | 3 ++- 5 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/components/GoalTimeline/Redux/GoalActions.ts b/src/components/GoalTimeline/Redux/GoalActions.ts index 66db597cc0..e583c63cb6 100644 --- a/src/components/GoalTimeline/Redux/GoalActions.ts +++ b/src/components/GoalTimeline/Redux/GoalActions.ts @@ -8,6 +8,7 @@ import router from "browserRouter"; import { addCharInvChangesToGoalAction, addCompletedMergeToGoalAction, + addEntryEditToGoalAction, incrementGoalStepAction, loadUserEditsAction, setCurrentGoalAction, @@ -17,6 +18,7 @@ import { } from "components/GoalTimeline/Redux/GoalReducer"; import { CharacterChange } from "goals/CharacterInventory/CharacterInventoryTypes"; import { dispatchMergeStepData } from "goals/MergeDuplicates/Redux/MergeDupsActions"; +import { EntryEdit } from "goals/ReviewEntries/ReviewEntries"; import { StoreState } from "types"; import { StoreStateDispatch } from "types/Redux/actions"; import { Goal, GoalStatus, GoalType } from "types/goals"; @@ -31,6 +33,10 @@ export function addCharInvChangesToGoal( return addCharInvChangesToGoalAction(charChanges); } +export function addEntryEditToGoal(entryEdit: EntryEdit): PayloadAction { + return addEntryEditToGoalAction(entryEdit); +} + export function addCompletedMergeToGoal(changes: MergeUndoIds): PayloadAction { return addCompletedMergeToGoalAction(changes); } diff --git a/src/components/GoalTimeline/Redux/GoalReducer.ts b/src/components/GoalTimeline/Redux/GoalReducer.ts index 2632f2f400..44915b3725 100644 --- a/src/components/GoalTimeline/Redux/GoalReducer.ts +++ b/src/components/GoalTimeline/Redux/GoalReducer.ts @@ -5,6 +5,7 @@ import { MergeDupsData, MergesCompleted, } from "goals/MergeDuplicates/MergeDupsTypes"; +import { EntriesEdited, EntryEdit } from "goals/ReviewEntries/ReviewEntries"; import { StoreActionTypes } from "rootActions"; import { GoalType } from "types/goals"; @@ -30,6 +31,24 @@ const goalSlice = createSlice({ state.currentGoal.changes = changes; } }, + addEntryEditToGoalAction: (state, action) => { + if (state.currentGoal.goalType === GoalType.ReviewEntries) { + const changes = { ...state.currentGoal.changes } as EntriesEdited; + if (!changes.entryEdits) { + changes.entryEdits = []; + } + const newEdit = action.payload as EntryEdit; + const oldEdit = changes.entryEdits.find( + (e) => e.newId === newEdit.oldId + ); + if (oldEdit) { + oldEdit.newId = newEdit.newId; + } else { + changes.entryEdits.push(newEdit); + } + state.currentGoal.changes = changes; + } + }, incrementGoalStepAction: (state) => { if (state.currentGoal.currentStep + 1 < state.currentGoal.numSteps) { state.currentGoal.currentStep++; @@ -81,6 +100,7 @@ const goalSlice = createSlice({ export const { addCharInvChangesToGoalAction, addCompletedMergeToGoalAction, + addEntryEditToGoalAction, incrementGoalStepAction, loadUserEditsAction, setCurrentGoalAction, diff --git a/src/goals/ReviewEntries/ReviewEntries.ts b/src/goals/ReviewEntries/ReviewEntries.ts index efd3a26cf2..d51fefbfdd 100644 --- a/src/goals/ReviewEntries/ReviewEntries.ts +++ b/src/goals/ReviewEntries/ReviewEntries.ts @@ -5,3 +5,12 @@ export class ReviewEntries extends Goal { super(GoalType.ReviewEntries, GoalName.ReviewEntries); } } + +export type EntryEdit = { + newId: string; + oldId: string; +}; + +export interface EntriesEdited { + entryEdits: EntryEdit[]; +} diff --git a/src/goals/ReviewEntries/ReviewEntriesComponent/Redux/ReviewEntriesActions.ts b/src/goals/ReviewEntries/ReviewEntriesComponent/Redux/ReviewEntriesActions.ts index e13dc1f781..cee09a7570 100644 --- a/src/goals/ReviewEntries/ReviewEntriesComponent/Redux/ReviewEntriesActions.ts +++ b/src/goals/ReviewEntries/ReviewEntriesComponent/Redux/ReviewEntriesActions.ts @@ -30,6 +30,7 @@ export function updateAllWords(words: ReviewEntriesWord[]): ReviewUpdateWords { }; } +// TODO: also dispatch(addEntryEditToGoal({newId: updatedWord.id, oldId})) function updateWord( oldId: string, updatedWord: ReviewEntriesWord diff --git a/src/types/goals.ts b/src/types/goals.ts index 0a7e995780..8935d38af6 100644 --- a/src/types/goals.ts +++ b/src/types/goals.ts @@ -11,12 +11,13 @@ import { MergeStepData, MergesCompleted, } from "goals/MergeDuplicates/MergeDupsTypes"; +import { EntriesEdited } from "goals/ReviewEntries/ReviewEntries"; import { newUser } from "types/user"; export type GoalData = CharInvData | MergeDupsData; // Record is the recommended type for an empty object. export type GoalStep = CharInvStepData | MergeStepData | Record; -export type GoalChanges = CharInvChanges | MergesCompleted; +export type GoalChanges = CharInvChanges | EntriesEdited | MergesCompleted; export interface GoalProps { goal?: Goal; From 05ec274dbc9766328736bcdbba40f00bdf7726ea Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Wed, 25 Oct 2023 11:19:45 -0400 Subject: [PATCH 02/11] Add goal dispatch to ReviewEntries update dispatch --- .../Redux/ReviewEntriesActions.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/goals/ReviewEntries/ReviewEntriesComponent/Redux/ReviewEntriesActions.ts b/src/goals/ReviewEntries/ReviewEntriesComponent/Redux/ReviewEntriesActions.ts index cee09a7570..9e002166f1 100644 --- a/src/goals/ReviewEntries/ReviewEntriesComponent/Redux/ReviewEntriesActions.ts +++ b/src/goals/ReviewEntries/ReviewEntriesComponent/Redux/ReviewEntriesActions.ts @@ -1,5 +1,6 @@ import { Sense } from "api/models"; import * as backend from "backend"; +import { addEntryEditToGoal } from "components/GoalTimeline/Redux/GoalActions"; import { uploadFileFromUrl } from "components/Pronunciations/utilities"; import { ReviewClearReviewEntriesState, @@ -30,15 +31,15 @@ export function updateAllWords(words: ReviewEntriesWord[]): ReviewUpdateWords { }; } -// TODO: also dispatch(addEntryEditToGoal({newId: updatedWord.id, oldId})) -function updateWord( - oldId: string, - updatedWord: ReviewEntriesWord -): ReviewUpdateWord { - return { - type: ReviewEntriesActionTypes.UpdateWord, - oldId, - updatedWord, +function updateWord(oldId: string, updatedWord: ReviewEntriesWord) { + return (dispatch: StoreStateDispatch) => { + dispatch(addEntryEditToGoal({ newId: updatedWord.id, oldId })); + const update: ReviewUpdateWord = { + type: ReviewEntriesActionTypes.UpdateWord, + oldId, + updatedWord, + }; + dispatch(update); }; } From d13fa64e41fcc69e02b95013e8a0e4a17c66b50f Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Mon, 6 Nov 2023 15:26:53 -0500 Subject: [PATCH 03/11] Create prototype ReviewEntriesCompleted; Reorganize goals/ReviewEntries/ --- public/locales/en/translation.json | 5 + src/components/App/DefaultState.ts | 2 +- .../DataEntryTable/NewEntry/SenseDialog.tsx | 4 +- .../DataEntryTable/NewEntry/VernDialog.tsx | 4 +- .../NewEntry/tests/SenseDialog.test.tsx | 2 +- .../NewEntry/tests/VernDialog.test.tsx | 2 +- .../GoalTimeline/Redux/GoalActions.ts | 2 +- .../GoalTimeline/Redux/GoalReducer.ts | 5 +- src/goals/DefaultGoal/BaseGoalScreen.tsx | 18 +-- .../Redux/ReviewEntriesActions.ts | 4 +- .../Redux/ReviewEntriesReducer.ts | 2 +- .../Redux/ReviewEntriesReduxTypes.ts | 2 +- src/goals/ReviewEntries/ReviewEntries.ts | 16 --- .../ReviewEntries/ReviewEntriesCompleted.tsx | 117 ++++++++++++++++++ .../CellComponents/index.ts | 23 ---- .../CellColumns.tsx | 4 +- .../CellComponents/AlignedList.tsx | 0 .../CellComponents/DefinitionCell.tsx | 4 +- .../CellComponents/DeleteCell.tsx | 4 +- .../CellComponents/DomainCell.tsx | 4 +- .../CellComponents/FlagCell.tsx | 2 +- .../CellComponents/GlossCell.tsx | 4 +- .../CellComponents/NoteCell.tsx | 2 +- .../CellComponents/PartOfSpeechCell.tsx | 4 +- .../CellComponents/PronunciationsCell.tsx | 2 +- .../CellComponents/SenseCell.tsx | 6 +- .../CellComponents/VernacularCell.tsx | 2 +- .../CellComponents/index.ts | 23 ++++ .../CellComponents/tests/AlignedList.test.tsx | 2 +- .../tests/DefinitionCell.test.tsx | 4 +- .../CellComponents/tests/DeleteCell.test.tsx | 6 +- .../CellComponents/tests/DomainCell.test.tsx | 6 +- .../CellComponents/tests/FlagCell.test.tsx | 4 +- .../CellComponents/tests/GlossCell.test.tsx | 4 +- .../CellComponents/tests/NoteCell.test.tsx | 4 +- .../tests/PartOfSpeechCell.test.tsx | 4 +- .../tests/PronunciationsCell.test.tsx | 13 +- .../CellComponents/tests/SenseCell.test.tsx | 4 +- .../tests/VernacularCell.test.tsx | 4 +- .../icons.tsx | 0 .../index.tsx} | 6 +- .../tests/CellColumns.test.tsx | 4 +- .../ReviewEntriesTypes.ts | 16 +++ .../{ReviewEntriesComponent => }/index.tsx | 29 +++-- .../tests/ReviewEntriesActions.test.tsx | 4 +- .../tests/ReviewEntriesReducer.test.tsx | 8 +- .../tests/WordsMock.ts | 2 +- .../tests/index.test.tsx | 23 ++-- src/rootReducer.ts | 2 +- src/types/goals.ts | 2 +- src/types/index.ts | 2 +- src/utilities/goalUtilities.ts | 2 +- 52 files changed, 276 insertions(+), 148 deletions(-) rename src/goals/ReviewEntries/{ReviewEntriesComponent => }/Redux/ReviewEntriesActions.ts (97%) rename src/goals/ReviewEntries/{ReviewEntriesComponent => }/Redux/ReviewEntriesReducer.ts (92%) rename src/goals/ReviewEntries/{ReviewEntriesComponent => }/Redux/ReviewEntriesReduxTypes.ts (93%) delete mode 100644 src/goals/ReviewEntries/ReviewEntries.ts create mode 100644 src/goals/ReviewEntries/ReviewEntriesCompleted.tsx delete mode 100644 src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/index.ts rename src/goals/ReviewEntries/{ReviewEntriesComponent => ReviewEntriesTable}/CellColumns.tsx (99%) rename src/goals/ReviewEntries/{ReviewEntriesComponent => ReviewEntriesTable}/CellComponents/AlignedList.tsx (100%) rename src/goals/ReviewEntries/{ReviewEntriesComponent => ReviewEntriesTable}/CellComponents/DefinitionCell.tsx (97%) rename src/goals/ReviewEntries/{ReviewEntriesComponent => ReviewEntriesTable}/CellComponents/DeleteCell.tsx (93%) rename src/goals/ReviewEntries/{ReviewEntriesComponent => ReviewEntriesTable}/CellComponents/DomainCell.tsx (97%) rename src/goals/ReviewEntries/{ReviewEntriesComponent => ReviewEntriesTable}/CellComponents/FlagCell.tsx (95%) rename src/goals/ReviewEntries/{ReviewEntriesComponent => ReviewEntriesTable}/CellComponents/GlossCell.tsx (96%) rename src/goals/ReviewEntries/{ReviewEntriesComponent => ReviewEntriesTable}/CellComponents/NoteCell.tsx (95%) rename src/goals/ReviewEntries/{ReviewEntriesComponent => ReviewEntriesTable}/CellComponents/PartOfSpeechCell.tsx (84%) rename src/goals/ReviewEntries/{ReviewEntriesComponent => ReviewEntriesTable}/CellComponents/PronunciationsCell.tsx (95%) rename src/goals/ReviewEntries/{ReviewEntriesComponent => ReviewEntriesTable}/CellComponents/SenseCell.tsx (90%) rename src/goals/ReviewEntries/{ReviewEntriesComponent => ReviewEntriesTable}/CellComponents/VernacularCell.tsx (96%) create mode 100644 src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/index.ts rename src/goals/ReviewEntries/{ReviewEntriesComponent => ReviewEntriesTable}/CellComponents/tests/AlignedList.test.tsx (77%) rename src/goals/ReviewEntries/{ReviewEntriesComponent => ReviewEntriesTable}/CellComponents/tests/DefinitionCell.test.tsx (85%) rename src/goals/ReviewEntries/{ReviewEntriesComponent => ReviewEntriesTable}/CellComponents/tests/DeleteCell.test.tsx (69%) rename src/goals/ReviewEntries/{ReviewEntriesComponent => ReviewEntriesTable}/CellComponents/tests/DomainCell.test.tsx (79%) rename src/goals/ReviewEntries/{ReviewEntriesComponent => ReviewEntriesTable}/CellComponents/tests/FlagCell.test.tsx (72%) rename src/goals/ReviewEntries/{ReviewEntriesComponent => ReviewEntriesTable}/CellComponents/tests/GlossCell.test.tsx (85%) rename src/goals/ReviewEntries/{ReviewEntriesComponent => ReviewEntriesTable}/CellComponents/tests/NoteCell.test.tsx (70%) rename src/goals/ReviewEntries/{ReviewEntriesComponent => ReviewEntriesTable}/CellComponents/tests/PartOfSpeechCell.test.tsx (59%) rename src/goals/ReviewEntries/{ReviewEntriesComponent => ReviewEntriesTable}/CellComponents/tests/PronunciationsCell.test.tsx (94%) rename src/goals/ReviewEntries/{ReviewEntriesComponent => ReviewEntriesTable}/CellComponents/tests/SenseCell.test.tsx (67%) rename src/goals/ReviewEntries/{ReviewEntriesComponent => ReviewEntriesTable}/CellComponents/tests/VernacularCell.test.tsx (78%) rename src/goals/ReviewEntries/{ReviewEntriesComponent => ReviewEntriesTable}/icons.tsx (100%) rename src/goals/ReviewEntries/{ReviewEntriesComponent/ReviewEntriesTable.tsx => ReviewEntriesTable/index.tsx} (96%) rename src/goals/ReviewEntries/{ReviewEntriesComponent => ReviewEntriesTable}/tests/CellColumns.test.tsx (98%) rename src/goals/ReviewEntries/{ReviewEntriesComponent => }/ReviewEntriesTypes.ts (89%) rename src/goals/ReviewEntries/{ReviewEntriesComponent => }/index.tsx (52%) rename src/goals/ReviewEntries/{ReviewEntriesComponent => }/tests/ReviewEntriesActions.test.tsx (98%) rename src/goals/ReviewEntries/{ReviewEntriesComponent => }/tests/ReviewEntriesReducer.test.tsx (84%) rename src/goals/ReviewEntries/{ReviewEntriesComponent => }/tests/WordsMock.ts (96%) rename src/goals/ReviewEntries/{ReviewEntriesComponent => }/tests/index.test.tsx (84%) diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 74e2a26202..392a527094 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -320,6 +320,11 @@ "toolbar": { "search": "Search" } + }, + "undo": { + "undo": "Undo Edit", + "undoDialog": "Undo this edit?", + "undoDisabled": "Undo Unavailable" } }, "charInventory": { diff --git a/src/components/App/DefaultState.ts b/src/components/App/DefaultState.ts index 5e981553ad..339b3d4295 100644 --- a/src/components/App/DefaultState.ts +++ b/src/components/App/DefaultState.ts @@ -6,7 +6,7 @@ import { defaultState as pronunciationsState } from "components/Pronunciations/R import { defaultState as treeViewState } from "components/TreeView/Redux/TreeViewReduxTypes"; import { defaultState as characterInventoryState } from "goals/CharacterInventory/Redux/CharacterInventoryReducer"; import { defaultState as mergeDuplicateGoal } from "goals/MergeDuplicates/Redux/MergeDupsReducer"; -import { defaultState as reviewEntriesState } from "goals/ReviewEntries/ReviewEntriesComponent/Redux/ReviewEntriesReduxTypes"; +import { defaultState as reviewEntriesState } from "goals/ReviewEntries/Redux/ReviewEntriesReduxTypes"; import { defaultState as analyticsState } from "types/Redux/analyticsReduxTypes"; export const defaultState = { diff --git a/src/components/DataEntry/DataEntryTable/NewEntry/SenseDialog.tsx b/src/components/DataEntry/DataEntryTable/NewEntry/SenseDialog.tsx index 11132c97f4..67255d1904 100644 --- a/src/components/DataEntry/DataEntryTable/NewEntry/SenseDialog.tsx +++ b/src/components/DataEntry/DataEntryTable/NewEntry/SenseDialog.tsx @@ -14,8 +14,8 @@ import StyledMenuItem from "components/DataEntry/DataEntryTable/NewEntry/StyledM import { DomainCell, PartOfSpeechCell, -} from "goals/ReviewEntries/ReviewEntriesComponent/CellComponents"; -import { ReviewEntriesWord } from "goals/ReviewEntries/ReviewEntriesComponent/ReviewEntriesTypes"; +} from "goals/ReviewEntries/ReviewEntriesTable/CellComponents"; +import { ReviewEntriesWord } from "goals/ReviewEntries/ReviewEntriesTypes"; import { firstGlossText } from "utilities/wordUtilities"; interface SenseDialogProps { diff --git a/src/components/DataEntry/DataEntryTable/NewEntry/VernDialog.tsx b/src/components/DataEntry/DataEntryTable/NewEntry/VernDialog.tsx index 77e96dc084..bb977365aa 100644 --- a/src/components/DataEntry/DataEntryTable/NewEntry/VernDialog.tsx +++ b/src/components/DataEntry/DataEntryTable/NewEntry/VernDialog.tsx @@ -15,8 +15,8 @@ import { DomainCell, GlossCell, PartOfSpeechCell, -} from "goals/ReviewEntries/ReviewEntriesComponent/CellComponents"; -import { ReviewEntriesWord } from "goals/ReviewEntries/ReviewEntriesComponent/ReviewEntriesTypes"; +} from "goals/ReviewEntries/ReviewEntriesTable/CellComponents"; +import { ReviewEntriesWord } from "goals/ReviewEntries/ReviewEntriesTypes"; interface vernDialogProps { vernacularWords: Word[]; diff --git a/src/components/DataEntry/DataEntryTable/NewEntry/tests/SenseDialog.test.tsx b/src/components/DataEntry/DataEntryTable/NewEntry/tests/SenseDialog.test.tsx index 88e6cc85b0..4451a9fc20 100644 --- a/src/components/DataEntry/DataEntryTable/NewEntry/tests/SenseDialog.test.tsx +++ b/src/components/DataEntry/DataEntryTable/NewEntry/tests/SenseDialog.test.tsx @@ -16,7 +16,7 @@ import { defaultWritingSystem } from "types/writingSystem"; // MUI: Unable to set focus to a MenuItem whose component has not been rendered. jest.mock("@mui/material/MenuItem", () => "div"); -jest.mock("goals/ReviewEntries/ReviewEntriesComponent/CellComponents", () => ({ +jest.mock("goals/ReviewEntries/ReviewEntriesTable/CellComponents", () => ({ DomainCell: () =>
, PartOfSpeechCell: () =>
, })); diff --git a/src/components/DataEntry/DataEntryTable/NewEntry/tests/VernDialog.test.tsx b/src/components/DataEntry/DataEntryTable/NewEntry/tests/VernDialog.test.tsx index d52584e437..51eb3cb09c 100644 --- a/src/components/DataEntry/DataEntryTable/NewEntry/tests/VernDialog.test.tsx +++ b/src/components/DataEntry/DataEntryTable/NewEntry/tests/VernDialog.test.tsx @@ -16,7 +16,7 @@ import { defaultWritingSystem } from "types/writingSystem"; // MUI: Unable to set focus to a MenuItem whose component has not been rendered. jest.mock("@mui/material/MenuItem", () => "div"); -jest.mock("goals/ReviewEntries/ReviewEntriesComponent/CellComponents", () => ({ +jest.mock("goals/ReviewEntries/ReviewEntriesTable/CellComponents", () => ({ DomainCell: () =>
, GlossCell: () =>
, PartOfSpeechCell: () =>
, diff --git a/src/components/GoalTimeline/Redux/GoalActions.ts b/src/components/GoalTimeline/Redux/GoalActions.ts index e583c63cb6..5d9cc898e4 100644 --- a/src/components/GoalTimeline/Redux/GoalActions.ts +++ b/src/components/GoalTimeline/Redux/GoalActions.ts @@ -18,7 +18,7 @@ import { } from "components/GoalTimeline/Redux/GoalReducer"; import { CharacterChange } from "goals/CharacterInventory/CharacterInventoryTypes"; import { dispatchMergeStepData } from "goals/MergeDuplicates/Redux/MergeDupsActions"; -import { EntryEdit } from "goals/ReviewEntries/ReviewEntries"; +import { EntryEdit } from "goals/ReviewEntries/ReviewEntriesTypes"; import { StoreState } from "types"; import { StoreStateDispatch } from "types/Redux/actions"; import { Goal, GoalStatus, GoalType } from "types/goals"; diff --git a/src/components/GoalTimeline/Redux/GoalReducer.ts b/src/components/GoalTimeline/Redux/GoalReducer.ts index 44915b3725..a54b8b3141 100644 --- a/src/components/GoalTimeline/Redux/GoalReducer.ts +++ b/src/components/GoalTimeline/Redux/GoalReducer.ts @@ -5,7 +5,10 @@ import { MergeDupsData, MergesCompleted, } from "goals/MergeDuplicates/MergeDupsTypes"; -import { EntriesEdited, EntryEdit } from "goals/ReviewEntries/ReviewEntries"; +import { + EntriesEdited, + EntryEdit, +} from "goals/ReviewEntries/ReviewEntriesTypes"; import { StoreActionTypes } from "rootActions"; import { GoalType } from "types/goals"; diff --git a/src/goals/DefaultGoal/BaseGoalScreen.tsx b/src/goals/DefaultGoal/BaseGoalScreen.tsx index 06dd04668b..61338138be 100644 --- a/src/goals/DefaultGoal/BaseGoalScreen.tsx +++ b/src/goals/DefaultGoal/BaseGoalScreen.tsx @@ -1,22 +1,22 @@ import loadable from "@loadable/component"; -import React, { ReactElement, useEffect } from "react"; +import { ReactElement, useEffect } from "react"; import { setCurrentGoal } from "components/GoalTimeline/Redux/GoalActions"; import PageNotFound from "components/PageNotFound/component"; import DisplayProgress from "goals/DefaultGoal/DisplayProgress"; import Loading from "goals/DefaultGoal/Loading"; import { clearTree } from "goals/MergeDuplicates/Redux/MergeDupsActions"; -import ReviewDeferredDuplicates from "goals/ReviewDeferredDuplicates"; -import { clearReviewEntriesState } from "goals/ReviewEntries/ReviewEntriesComponent/Redux/ReviewEntriesActions"; +import { clearReviewEntriesState } from "goals/ReviewEntries/Redux/ReviewEntriesActions"; import { StoreState } from "types"; import { Goal, GoalStatus, GoalType } from "types/goals"; import { useAppDispatch, useAppSelector } from "types/hooks"; const CharacterInventory = loadable(() => import("goals/CharacterInventory")); const MergeDup = loadable(() => import("goals/MergeDuplicates")); -const ReviewEntriesComponent = loadable( - () => import("goals/ReviewEntries/ReviewEntriesComponent") +const ReviewDeferredDups = loadable( + () => import("goals/ReviewDeferredDuplicates") ); +const ReviewEntries = loadable(() => import("goals/ReviewEntries")); function displayComponent(goal: Goal): ReactElement { const isCompleted = goal.status === GoalStatus.Completed; @@ -26,9 +26,9 @@ function displayComponent(goal: Goal): ReactElement { case GoalType.MergeDups: return ; case GoalType.ReviewDeferredDups: - return ; + return ; case GoalType.ReviewEntries: - return ; + return ; default: return ; } @@ -58,9 +58,9 @@ export function BaseGoalScreen(): ReactElement { }, [dispatch]); return ( - + <> {goal.status !== GoalStatus.Completed && } {displayComponent(goal)} - + ); } diff --git a/src/goals/ReviewEntries/ReviewEntriesComponent/Redux/ReviewEntriesActions.ts b/src/goals/ReviewEntries/Redux/ReviewEntriesActions.ts similarity index 97% rename from src/goals/ReviewEntries/ReviewEntriesComponent/Redux/ReviewEntriesActions.ts rename to src/goals/ReviewEntries/Redux/ReviewEntriesActions.ts index 9e002166f1..089cc8e6a1 100644 --- a/src/goals/ReviewEntries/ReviewEntriesComponent/Redux/ReviewEntriesActions.ts +++ b/src/goals/ReviewEntries/Redux/ReviewEntriesActions.ts @@ -8,12 +8,12 @@ import { ReviewSortBy, ReviewUpdateWord, ReviewUpdateWords, -} from "goals/ReviewEntries/ReviewEntriesComponent/Redux/ReviewEntriesReduxTypes"; +} from "goals/ReviewEntries/Redux/ReviewEntriesReduxTypes"; import { ColumnId, ReviewEntriesSense, ReviewEntriesWord, -} from "goals/ReviewEntries/ReviewEntriesComponent/ReviewEntriesTypes"; +} from "goals/ReviewEntries/ReviewEntriesTypes"; import { StoreStateDispatch } from "types/Redux/actions"; import { newNote, newSense } from "types/word"; diff --git a/src/goals/ReviewEntries/ReviewEntriesComponent/Redux/ReviewEntriesReducer.ts b/src/goals/ReviewEntries/Redux/ReviewEntriesReducer.ts similarity index 92% rename from src/goals/ReviewEntries/ReviewEntriesComponent/Redux/ReviewEntriesReducer.ts rename to src/goals/ReviewEntries/Redux/ReviewEntriesReducer.ts index 12c11c3aa1..bd0d75ff49 100644 --- a/src/goals/ReviewEntries/ReviewEntriesComponent/Redux/ReviewEntriesReducer.ts +++ b/src/goals/ReviewEntries/Redux/ReviewEntriesReducer.ts @@ -3,7 +3,7 @@ import { ReviewEntriesAction, ReviewEntriesActionTypes, ReviewEntriesState, -} from "goals/ReviewEntries/ReviewEntriesComponent/Redux/ReviewEntriesReduxTypes"; +} from "goals/ReviewEntries/Redux/ReviewEntriesReduxTypes"; import { StoreAction, StoreActionTypes } from "rootActions"; export const reviewEntriesReducer = ( diff --git a/src/goals/ReviewEntries/ReviewEntriesComponent/Redux/ReviewEntriesReduxTypes.ts b/src/goals/ReviewEntries/Redux/ReviewEntriesReduxTypes.ts similarity index 93% rename from src/goals/ReviewEntries/ReviewEntriesComponent/Redux/ReviewEntriesReduxTypes.ts rename to src/goals/ReviewEntries/Redux/ReviewEntriesReduxTypes.ts index d9d2113574..8df1d02619 100644 --- a/src/goals/ReviewEntries/ReviewEntriesComponent/Redux/ReviewEntriesReduxTypes.ts +++ b/src/goals/ReviewEntries/Redux/ReviewEntriesReduxTypes.ts @@ -1,7 +1,7 @@ import { ColumnId, ReviewEntriesWord, -} from "goals/ReviewEntries/ReviewEntriesComponent/ReviewEntriesTypes"; +} from "goals/ReviewEntries/ReviewEntriesTypes"; export enum ReviewEntriesActionTypes { SortBy = "SORT_BY", diff --git a/src/goals/ReviewEntries/ReviewEntries.ts b/src/goals/ReviewEntries/ReviewEntries.ts deleted file mode 100644 index d51fefbfdd..0000000000 --- a/src/goals/ReviewEntries/ReviewEntries.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Goal, GoalName, GoalType } from "types/goals"; - -export class ReviewEntries extends Goal { - constructor() { - super(GoalType.ReviewEntries, GoalName.ReviewEntries); - } -} - -export type EntryEdit = { - newId: string; - oldId: string; -}; - -export interface EntriesEdited { - entryEdits: EntryEdit[]; -} diff --git a/src/goals/ReviewEntries/ReviewEntriesCompleted.tsx b/src/goals/ReviewEntries/ReviewEntriesCompleted.tsx new file mode 100644 index 0000000000..e179150888 --- /dev/null +++ b/src/goals/ReviewEntries/ReviewEntriesCompleted.tsx @@ -0,0 +1,117 @@ +import { ArrowRightAlt } from "@mui/icons-material"; +import { Button, Grid, Typography } from "@mui/material"; +import { ReactElement, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useSelector } from "react-redux"; + +import { getFrontierWords, getWord, updateWord } from "backend"; +import { CancelConfirmDialog } from "components/Dialogs"; +import { + EntriesEdited, + EntryEdit, +} from "goals/ReviewEntries/ReviewEntriesTypes"; +import { StoreState } from "types"; +import theme from "types/theme"; + +export default function ReviewEntriesCompleted(): ReactElement { + const changes = useSelector( + (state: StoreState) => state.goalsState.currentGoal.changes as EntriesEdited + ); + const { t } = useTranslation(); + + return ( + <> + + {t("reviewEntries.title")} + + + {t("reviewEntries.completed.number")} + {changes.entryEdits?.length ?? 0} + + {changes.entryEdits?.map((e) => )} + + ); +} + +function EditedEntry(props: { edit: EntryEdit }): ReactElement { + return ( + + {props.edit.oldId} + + + + {props.edit.newId} + + + ); +} + +interface UndoButtonProps { + edit: EntryEdit; + textId: string; + dialogId: string; + disabledId: string; +} + +function UndoButton(props: UndoButtonProps): ReactElement { + const [isUndoEnabled, setUndoEnabled] = useState(false); + const [undoDialogOpen, setUndoDialogOpen] = useState(false); + const { t } = useTranslation(); + + useEffect(() => { + function checkFrontier(): void { + getFrontierWords().then((words) => + setUndoEnabled(words.findIndex((w) => w.id === props.edit.newId) !== -1) + ); + } + checkFrontier(); + }); + + return isUndoEnabled ? ( + + <> + + setUndoDialogOpen(false)} + handleConfirm={() => + undoEdit(props.edit).then(() => setUndoDialogOpen(false)) + } + buttonIdCancel="edit-undo-cancel" + buttonIdConfirm="edit-undo-confirm" + /> + + + ) : ( + + <> + + + + ); +} + +const undoEdit = async (edit: EntryEdit): Promise => { + const oldWord = await getWord(edit.oldId); + await updateWord({ ...oldWord, id: edit.newId }); +}; diff --git a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/index.ts b/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/index.ts deleted file mode 100644 index 930c959a6a..0000000000 --- a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import DefinitionCell from "goals/ReviewEntries/ReviewEntriesComponent/CellComponents/DefinitionCell"; -import DeleteCell from "goals/ReviewEntries/ReviewEntriesComponent/CellComponents/DeleteCell"; -import DomainCell from "goals/ReviewEntries/ReviewEntriesComponent/CellComponents/DomainCell"; -import FlagCell from "goals/ReviewEntries/ReviewEntriesComponent/CellComponents/FlagCell"; -import GlossCell from "goals/ReviewEntries/ReviewEntriesComponent/CellComponents/GlossCell"; -import NoteCell from "goals/ReviewEntries/ReviewEntriesComponent/CellComponents/NoteCell"; -import PartOfSpeechCell from "goals/ReviewEntries/ReviewEntriesComponent/CellComponents/PartOfSpeechCell"; -import PronunciationsCell from "goals/ReviewEntries/ReviewEntriesComponent/CellComponents/PronunciationsCell"; -import SenseCell from "goals/ReviewEntries/ReviewEntriesComponent/CellComponents/SenseCell"; -import VernacularCell from "goals/ReviewEntries/ReviewEntriesComponent/CellComponents/VernacularCell"; - -export { - DefinitionCell, - DeleteCell, - DomainCell, - FlagCell, - GlossCell, - NoteCell, - PartOfSpeechCell, - PronunciationsCell, - SenseCell, - VernacularCell, -}; diff --git a/src/goals/ReviewEntries/ReviewEntriesComponent/CellColumns.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/CellColumns.tsx similarity index 99% rename from src/goals/ReviewEntries/ReviewEntriesComponent/CellColumns.tsx rename to src/goals/ReviewEntries/ReviewEntriesTable/CellColumns.tsx index daa3b40afa..868e88cd48 100644 --- a/src/goals/ReviewEntries/ReviewEntriesComponent/CellColumns.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/CellColumns.tsx @@ -14,13 +14,13 @@ import { PronunciationsCell, SenseCell, VernacularCell, -} from "goals/ReviewEntries/ReviewEntriesComponent/CellComponents"; +} from "goals/ReviewEntries/ReviewEntriesTable/CellComponents"; import { ColumnId, ReviewEntriesSense, ReviewEntriesWord, ReviewEntriesWordField, -} from "goals/ReviewEntries/ReviewEntriesComponent/ReviewEntriesTypes"; +} from "goals/ReviewEntries/ReviewEntriesTypes"; import { compareFlags } from "utilities/wordUtilities"; export class ColumnTitle { diff --git a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/AlignedList.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/AlignedList.tsx similarity index 100% rename from src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/AlignedList.tsx rename to src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/AlignedList.tsx diff --git a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/DefinitionCell.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/DefinitionCell.tsx similarity index 97% rename from src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/DefinitionCell.tsx rename to src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/DefinitionCell.tsx index 3add6d25de..dbff3b9328 100644 --- a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/DefinitionCell.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/DefinitionCell.tsx @@ -5,10 +5,10 @@ import { useSelector } from "react-redux"; import { Definition, WritingSystem } from "api/models"; import Overlay from "components/Overlay"; -import { FieldParameterStandard } from "goals/ReviewEntries/ReviewEntriesComponent/CellColumns"; +import { FieldParameterStandard } from "goals/ReviewEntries/ReviewEntriesTable/CellColumns"; import AlignedList, { SPACER, -} from "goals/ReviewEntries/ReviewEntriesComponent/CellComponents/AlignedList"; +} from "goals/ReviewEntries/ReviewEntriesTable/CellComponents/AlignedList"; import { StoreState } from "types"; import { newDefinition } from "types/word"; import { diff --git a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/DeleteCell.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/DeleteCell.tsx similarity index 93% rename from src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/DeleteCell.tsx rename to src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/DeleteCell.tsx index b69f7e4320..eeb88118bf 100644 --- a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/DeleteCell.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/DeleteCell.tsx @@ -5,8 +5,8 @@ import { useTranslation } from "react-i18next"; import { deleteFrontierWord as deleteFromBackend } from "backend"; import { CancelConfirmDialog } from "components/Dialogs"; -import { updateAllWords } from "goals/ReviewEntries/ReviewEntriesComponent/Redux/ReviewEntriesActions"; -import { ReviewEntriesWord } from "goals/ReviewEntries/ReviewEntriesComponent/ReviewEntriesTypes"; +import { updateAllWords } from "goals/ReviewEntries/Redux/ReviewEntriesActions"; +import { ReviewEntriesWord } from "goals/ReviewEntries/ReviewEntriesTypes"; import { StoreState } from "types"; import { useAppDispatch, useAppSelector } from "types/hooks"; diff --git a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/DomainCell.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/DomainCell.tsx similarity index 97% rename from src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/DomainCell.tsx rename to src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/DomainCell.tsx index 9610ed113c..4379fba2b1 100644 --- a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/DomainCell.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/DomainCell.tsx @@ -11,12 +11,12 @@ import Overlay from "components/Overlay"; import TreeView from "components/TreeView"; import AlignedList, { SPACER, -} from "goals/ReviewEntries/ReviewEntriesComponent/CellComponents/AlignedList"; +} from "goals/ReviewEntries/ReviewEntriesTable/CellComponents/AlignedList"; import { ColumnId, ReviewEntriesSense, ReviewEntriesWord, -} from "goals/ReviewEntries/ReviewEntriesComponent/ReviewEntriesTypes"; +} from "goals/ReviewEntries/ReviewEntriesTypes"; import { StoreState } from "types"; import { newSemanticDomainForMongoDB } from "types/semanticDomain"; import { themeColors } from "types/theme"; diff --git a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/FlagCell.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/FlagCell.tsx similarity index 95% rename from src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/FlagCell.tsx rename to src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/FlagCell.tsx index b93d65af6a..5b907d05fa 100644 --- a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/FlagCell.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/FlagCell.tsx @@ -2,7 +2,7 @@ import { ReactElement } from "react"; import { Flag } from "api/models"; import FlagButton from "components/Buttons/FlagButton"; -import { FieldParameterStandard } from "goals/ReviewEntries/ReviewEntriesComponent/CellColumns"; +import { FieldParameterStandard } from "goals/ReviewEntries/ReviewEntriesTable/CellColumns"; interface FlagCellProps extends FieldParameterStandard { editable?: boolean; diff --git a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/GlossCell.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/GlossCell.tsx similarity index 96% rename from src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/GlossCell.tsx rename to src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/GlossCell.tsx index b46b6a2111..3889f4736f 100644 --- a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/GlossCell.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/GlossCell.tsx @@ -5,10 +5,10 @@ import { useSelector } from "react-redux"; import { Gloss, WritingSystem } from "api/models"; import Overlay from "components/Overlay"; -import { FieldParameterStandard } from "goals/ReviewEntries/ReviewEntriesComponent/CellColumns"; +import { FieldParameterStandard } from "goals/ReviewEntries/ReviewEntriesTable/CellColumns"; import AlignedList, { SPACER, -} from "goals/ReviewEntries/ReviewEntriesComponent/CellComponents/AlignedList"; +} from "goals/ReviewEntries/ReviewEntriesTable/CellComponents/AlignedList"; import { StoreState } from "types"; import { newGloss } from "types/word"; import { diff --git a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/NoteCell.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/NoteCell.tsx similarity index 95% rename from src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/NoteCell.tsx rename to src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/NoteCell.tsx index 7a3ad5c809..5146fe1da2 100644 --- a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/NoteCell.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/NoteCell.tsx @@ -2,7 +2,7 @@ import { TextField } from "@mui/material"; import { ReactElement } from "react"; import { useTranslation } from "react-i18next"; -import { FieldParameterStandard } from "goals/ReviewEntries/ReviewEntriesComponent/CellColumns"; +import { FieldParameterStandard } from "goals/ReviewEntries/ReviewEntriesTable/CellColumns"; export default function NoteCell(props: FieldParameterStandard): ReactElement { const { t } = useTranslation(); diff --git a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/PartOfSpeechCell.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/PartOfSpeechCell.tsx similarity index 84% rename from src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/PartOfSpeechCell.tsx rename to src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/PartOfSpeechCell.tsx index c3f3a982cf..803c5aff94 100644 --- a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/PartOfSpeechCell.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/PartOfSpeechCell.tsx @@ -2,8 +2,8 @@ import { Grid } from "@mui/material"; import { ReactElement } from "react"; import { PartOfSpeechButton } from "components/Buttons"; -import AlignedList from "goals/ReviewEntries/ReviewEntriesComponent/CellComponents/AlignedList"; -import { ReviewEntriesWord } from "goals/ReviewEntries/ReviewEntriesComponent/ReviewEntriesTypes"; +import AlignedList from "goals/ReviewEntries/ReviewEntriesTable/CellComponents/AlignedList"; +import { ReviewEntriesWord } from "goals/ReviewEntries/ReviewEntriesTypes"; interface PartOfSpeechCellProps { rowData: ReviewEntriesWord; diff --git a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/PronunciationsCell.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/PronunciationsCell.tsx similarity index 95% rename from src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/PronunciationsCell.tsx rename to src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/PronunciationsCell.tsx index 4ae4d14d4a..001727e8fa 100644 --- a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/PronunciationsCell.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/PronunciationsCell.tsx @@ -5,7 +5,7 @@ import PronunciationsFrontend from "components/Pronunciations/PronunciationsFron import { deleteAudio, uploadAudio, -} from "goals/ReviewEntries/ReviewEntriesComponent/Redux/ReviewEntriesActions"; +} from "goals/ReviewEntries/Redux/ReviewEntriesActions"; import { useAppDispatch } from "types/hooks"; interface PronunciationsCellProps { diff --git a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/SenseCell.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/SenseCell.tsx similarity index 90% rename from src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/SenseCell.tsx rename to src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/SenseCell.tsx index 135dd68ebd..b3b9fe5f24 100644 --- a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/SenseCell.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/SenseCell.tsx @@ -3,9 +3,9 @@ import { Chip, IconButton, Tooltip } from "@mui/material"; import { ReactElement } from "react"; import { useTranslation } from "react-i18next"; -import { FieldParameterStandard } from "goals/ReviewEntries/ReviewEntriesComponent/CellColumns"; -import AlignedList from "goals/ReviewEntries/ReviewEntriesComponent/CellComponents/AlignedList"; -import { ReviewEntriesSense } from "goals/ReviewEntries/ReviewEntriesComponent/ReviewEntriesTypes"; +import { FieldParameterStandard } from "goals/ReviewEntries/ReviewEntriesTable/CellColumns"; +import AlignedList from "goals/ReviewEntries/ReviewEntriesTable/CellComponents/AlignedList"; +import { ReviewEntriesSense } from "goals/ReviewEntries/ReviewEntriesTypes"; interface SenseCellProps extends FieldParameterStandard { delete: (deleteIndex: string) => void; diff --git a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/VernacularCell.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/VernacularCell.tsx similarity index 96% rename from src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/VernacularCell.tsx rename to src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/VernacularCell.tsx index ca8cf63348..c59706c91d 100644 --- a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/VernacularCell.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/VernacularCell.tsx @@ -1,7 +1,7 @@ import { ReactElement } from "react"; import { useTranslation } from "react-i18next"; -import { FieldParameterStandard } from "goals/ReviewEntries/ReviewEntriesComponent/CellColumns"; +import { FieldParameterStandard } from "goals/ReviewEntries/ReviewEntriesTable/CellColumns"; import { TextFieldWithFont } from "utilities/fontComponents"; interface VernacularCellProps extends FieldParameterStandard { diff --git a/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/index.ts b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/index.ts new file mode 100644 index 0000000000..ad76e1318e --- /dev/null +++ b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/index.ts @@ -0,0 +1,23 @@ +import DefinitionCell from "goals/ReviewEntries/ReviewEntriesTable/CellComponents/DefinitionCell"; +import DeleteCell from "goals/ReviewEntries/ReviewEntriesTable/CellComponents/DeleteCell"; +import DomainCell from "goals/ReviewEntries/ReviewEntriesTable/CellComponents/DomainCell"; +import FlagCell from "goals/ReviewEntries/ReviewEntriesTable/CellComponents/FlagCell"; +import GlossCell from "goals/ReviewEntries/ReviewEntriesTable/CellComponents/GlossCell"; +import NoteCell from "goals/ReviewEntries/ReviewEntriesTable/CellComponents/NoteCell"; +import PartOfSpeechCell from "goals/ReviewEntries/ReviewEntriesTable/CellComponents/PartOfSpeechCell"; +import PronunciationsCell from "goals/ReviewEntries/ReviewEntriesTable/CellComponents/PronunciationsCell"; +import SenseCell from "goals/ReviewEntries/ReviewEntriesTable/CellComponents/SenseCell"; +import VernacularCell from "goals/ReviewEntries/ReviewEntriesTable/CellComponents/VernacularCell"; + +export { + DefinitionCell, + DeleteCell, + DomainCell, + FlagCell, + GlossCell, + NoteCell, + PartOfSpeechCell, + PronunciationsCell, + SenseCell, + VernacularCell, +}; diff --git a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/tests/AlignedList.test.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/tests/AlignedList.test.tsx similarity index 77% rename from src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/tests/AlignedList.test.tsx rename to src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/tests/AlignedList.test.tsx index f808edea63..bfc9a168d7 100644 --- a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/tests/AlignedList.test.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/tests/AlignedList.test.tsx @@ -1,6 +1,6 @@ import renderer from "react-test-renderer"; -import AlignedList from "goals/ReviewEntries/ReviewEntriesComponent/CellComponents/AlignedList"; +import AlignedList from "goals/ReviewEntries/ReviewEntriesTable/CellComponents/AlignedList"; describe("AlignedList", () => { it("renders without crashing", () => { diff --git a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/tests/DefinitionCell.test.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/tests/DefinitionCell.test.tsx similarity index 85% rename from src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/tests/DefinitionCell.test.tsx rename to src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/tests/DefinitionCell.test.tsx index e5218c7f56..db4afeac00 100644 --- a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/tests/DefinitionCell.test.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/tests/DefinitionCell.test.tsx @@ -4,8 +4,8 @@ import configureMockStore from "redux-mock-store"; import "tests/reactI18nextMock"; -import DefinitionCell from "goals/ReviewEntries/ReviewEntriesComponent/CellComponents/DefinitionCell"; -import mockWords from "goals/ReviewEntries/ReviewEntriesComponent/tests/WordsMock"; +import DefinitionCell from "goals/ReviewEntries/ReviewEntriesTable/CellComponents/DefinitionCell"; +import mockWords from "goals/ReviewEntries/tests/WordsMock"; import { defaultWritingSystem } from "types/writingSystem"; // The multiline Input, TextField cause problems in the mock environment. diff --git a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/tests/DeleteCell.test.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/tests/DeleteCell.test.tsx similarity index 69% rename from src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/tests/DeleteCell.test.tsx rename to src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/tests/DeleteCell.test.tsx index 9824eb6309..2cc3b02281 100644 --- a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/tests/DeleteCell.test.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/tests/DeleteCell.test.tsx @@ -4,9 +4,9 @@ import configureMockStore from "redux-mock-store"; import "tests/reactI18nextMock"; -import DeleteCell from "goals/ReviewEntries/ReviewEntriesComponent/CellComponents/DeleteCell"; -import { defaultState as reviewEntriesState } from "goals/ReviewEntries/ReviewEntriesComponent/Redux/ReviewEntriesReduxTypes"; -import mockWords from "goals/ReviewEntries/ReviewEntriesComponent/tests/WordsMock"; +import { defaultState as reviewEntriesState } from "goals/ReviewEntries/Redux/ReviewEntriesReduxTypes"; +import DeleteCell from "goals/ReviewEntries/ReviewEntriesTable/CellComponents/DeleteCell"; +import mockWords from "goals/ReviewEntries/tests/WordsMock"; const mockStore = configureMockStore()({ reviewEntriesState }); const mockWord = mockWords()[0]; diff --git a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/tests/DomainCell.test.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/tests/DomainCell.test.tsx similarity index 79% rename from src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/tests/DomainCell.test.tsx rename to src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/tests/DomainCell.test.tsx index c071e37c67..b7ff54c197 100644 --- a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/tests/DomainCell.test.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/tests/DomainCell.test.tsx @@ -6,9 +6,9 @@ import configureMockStore from "redux-mock-store"; import "tests/reactI18nextMock"; import { defaultState as treeViewState } from "components/TreeView/Redux/TreeViewReduxTypes"; -import DomainCell from "goals/ReviewEntries/ReviewEntriesComponent/CellComponents/DomainCell"; -import { ColumnId } from "goals/ReviewEntries/ReviewEntriesComponent/ReviewEntriesTypes"; -import mockWords from "goals/ReviewEntries/ReviewEntriesComponent/tests/WordsMock"; +import DomainCell from "goals/ReviewEntries/ReviewEntriesTable/CellComponents/DomainCell"; +import { ColumnId } from "goals/ReviewEntries/ReviewEntriesTypes"; +import mockWords from "goals/ReviewEntries/tests/WordsMock"; jest.mock("components/TreeView", () => "div"); diff --git a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/tests/FlagCell.test.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/tests/FlagCell.test.tsx similarity index 72% rename from src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/tests/FlagCell.test.tsx rename to src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/tests/FlagCell.test.tsx index 5663d9dc4d..69a52cd1d0 100644 --- a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/tests/FlagCell.test.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/tests/FlagCell.test.tsx @@ -2,8 +2,8 @@ import renderer from "react-test-renderer"; import "tests/reactI18nextMock"; -import FlagCell from "goals/ReviewEntries/ReviewEntriesComponent/CellComponents/FlagCell"; -import mockWords from "goals/ReviewEntries/ReviewEntriesComponent/tests/WordsMock"; +import FlagCell from "goals/ReviewEntries/ReviewEntriesTable/CellComponents/FlagCell"; +import mockWords from "goals/ReviewEntries/tests/WordsMock"; const mockWord = mockWords()[1]; diff --git a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/tests/GlossCell.test.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/tests/GlossCell.test.tsx similarity index 85% rename from src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/tests/GlossCell.test.tsx rename to src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/tests/GlossCell.test.tsx index c537d6393d..71ffd82aa6 100644 --- a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/tests/GlossCell.test.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/tests/GlossCell.test.tsx @@ -4,8 +4,8 @@ import configureMockStore from "redux-mock-store"; import "tests/reactI18nextMock"; -import GlossCell from "goals/ReviewEntries/ReviewEntriesComponent/CellComponents/GlossCell"; -import mockWords from "goals/ReviewEntries/ReviewEntriesComponent/tests/WordsMock"; +import GlossCell from "goals/ReviewEntries/ReviewEntriesTable/CellComponents/GlossCell"; +import mockWords from "goals/ReviewEntries/tests/WordsMock"; import { defaultWritingSystem } from "types/writingSystem"; // The multiline Input, TextField cause problems in the mock environment. diff --git a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/tests/NoteCell.test.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/tests/NoteCell.test.tsx similarity index 70% rename from src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/tests/NoteCell.test.tsx rename to src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/tests/NoteCell.test.tsx index 1a64a8e1ec..1eb657bc9f 100644 --- a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/tests/NoteCell.test.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/tests/NoteCell.test.tsx @@ -2,8 +2,8 @@ import renderer from "react-test-renderer"; import "tests/reactI18nextMock"; -import NoteCell from "goals/ReviewEntries/ReviewEntriesComponent/CellComponents/NoteCell"; -import mockWords from "goals/ReviewEntries/ReviewEntriesComponent/tests/WordsMock"; +import NoteCell from "goals/ReviewEntries/ReviewEntriesTable/CellComponents/NoteCell"; +import mockWords from "goals/ReviewEntries/tests/WordsMock"; // The multiline TextField causes problems in the mock environment. jest.mock("@mui/material/TextField", () => "div"); diff --git a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/tests/PartOfSpeechCell.test.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/tests/PartOfSpeechCell.test.tsx similarity index 59% rename from src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/tests/PartOfSpeechCell.test.tsx rename to src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/tests/PartOfSpeechCell.test.tsx index 7eaf3d0b76..e267907195 100644 --- a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/tests/PartOfSpeechCell.test.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/tests/PartOfSpeechCell.test.tsx @@ -2,8 +2,8 @@ import renderer from "react-test-renderer"; import "tests/reactI18nextMock"; -import PartOfSpeechCell from "goals/ReviewEntries/ReviewEntriesComponent/CellComponents/PartOfSpeechCell"; -import mockWords from "goals/ReviewEntries/ReviewEntriesComponent/tests/WordsMock"; +import PartOfSpeechCell from "goals/ReviewEntries/ReviewEntriesTable/CellComponents/PartOfSpeechCell"; +import mockWords from "goals/ReviewEntries/tests/WordsMock"; const mockWord = mockWords()[1]; diff --git a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/tests/PronunciationsCell.test.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/tests/PronunciationsCell.test.tsx similarity index 94% rename from src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/tests/PronunciationsCell.test.tsx rename to src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/tests/PronunciationsCell.test.tsx index e5cb3a1352..a5d4eb955d 100644 --- a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/tests/PronunciationsCell.test.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/tests/PronunciationsCell.test.tsx @@ -8,7 +8,7 @@ import "tests/reactI18nextMock"; import AudioPlayer from "components/Pronunciations/AudioPlayer"; import AudioRecorder from "components/Pronunciations/AudioRecorder"; import { defaultState as pronunciationsState } from "components/Pronunciations/Redux/PronunciationsReduxTypes"; -import PronunciationsCell from "goals/ReviewEntries/ReviewEntriesComponent/CellComponents/PronunciationsCell"; +import PronunciationsCell from "goals/ReviewEntries/ReviewEntriesTable/CellComponents/PronunciationsCell"; import theme from "types/theme"; // Mock the audio components @@ -18,13 +18,10 @@ jest jest.mock("components/Pronunciations/Recorder"); // Mock the store interactions -jest.mock( - "goals/ReviewEntries/ReviewEntriesComponent/Redux/ReviewEntriesActions", - () => ({ - deleteAudio: (...args: any[]) => mockDeleteAudio(...args), - uploadAudio: (...args: any[]) => mockUploadAudio(...args), - }) -); +jest.mock("goals/ReviewEntries/Redux/ReviewEntriesActions", () => ({ + deleteAudio: (...args: any[]) => mockDeleteAudio(...args), + uploadAudio: (...args: any[]) => mockUploadAudio(...args), +})); jest.mock("types/hooks", () => { return { ...jest.requireActual("types/hooks"), diff --git a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/tests/SenseCell.test.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/tests/SenseCell.test.tsx similarity index 67% rename from src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/tests/SenseCell.test.tsx rename to src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/tests/SenseCell.test.tsx index eff956e6f2..1244b93cc3 100644 --- a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/tests/SenseCell.test.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/tests/SenseCell.test.tsx @@ -2,8 +2,8 @@ import renderer from "react-test-renderer"; import "tests/reactI18nextMock"; -import SenseCell from "goals/ReviewEntries/ReviewEntriesComponent/CellComponents/SenseCell"; -import mockWords from "goals/ReviewEntries/ReviewEntriesComponent/tests/WordsMock"; +import SenseCell from "goals/ReviewEntries/ReviewEntriesTable/CellComponents/SenseCell"; +import mockWords from "goals/ReviewEntries/tests/WordsMock"; const mockWord = mockWords()[1]; diff --git a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/tests/VernacularCell.test.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/tests/VernacularCell.test.tsx similarity index 78% rename from src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/tests/VernacularCell.test.tsx rename to src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/tests/VernacularCell.test.tsx index 200844b6a9..71d0237853 100644 --- a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/tests/VernacularCell.test.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/CellComponents/tests/VernacularCell.test.tsx @@ -2,8 +2,8 @@ import renderer from "react-test-renderer"; import "tests/reactI18nextMock"; -import VernacularCell from "goals/ReviewEntries/ReviewEntriesComponent/CellComponents/VernacularCell"; -import mockWords from "goals/ReviewEntries/ReviewEntriesComponent/tests/WordsMock"; +import VernacularCell from "goals/ReviewEntries/ReviewEntriesTable/CellComponents/VernacularCell"; +import mockWords from "goals/ReviewEntries/tests/WordsMock"; // The multiline TextField causes problems in the mock environment. jest.mock("@mui/material/TextField", () => "div"); diff --git a/src/goals/ReviewEntries/ReviewEntriesComponent/icons.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/icons.tsx similarity index 100% rename from src/goals/ReviewEntries/ReviewEntriesComponent/icons.tsx rename to src/goals/ReviewEntries/ReviewEntriesTable/icons.tsx diff --git a/src/goals/ReviewEntries/ReviewEntriesComponent/ReviewEntriesTable.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/index.tsx similarity index 96% rename from src/goals/ReviewEntries/ReviewEntriesComponent/ReviewEntriesTable.tsx rename to src/goals/ReviewEntries/ReviewEntriesTable/index.tsx index b1f4cb76a1..ff4579f341 100644 --- a/src/goals/ReviewEntries/ReviewEntriesComponent/ReviewEntriesTable.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/index.tsx @@ -5,12 +5,12 @@ import React, { ReactElement, createRef, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; -import columns from "goals/ReviewEntries/ReviewEntriesComponent/CellColumns"; +import columns from "goals/ReviewEntries/ReviewEntriesTable/CellColumns"; +import tableIcons from "goals/ReviewEntries/ReviewEntriesTable/icons"; import { ColumnId, ReviewEntriesWord, -} from "goals/ReviewEntries/ReviewEntriesComponent/ReviewEntriesTypes"; -import tableIcons from "goals/ReviewEntries/ReviewEntriesComponent/icons"; +} from "goals/ReviewEntries/ReviewEntriesTypes"; import { StoreState } from "types"; interface ReviewEntriesTableProps { diff --git a/src/goals/ReviewEntries/ReviewEntriesComponent/tests/CellColumns.test.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/tests/CellColumns.test.tsx similarity index 98% rename from src/goals/ReviewEntries/ReviewEntriesComponent/tests/CellColumns.test.tsx rename to src/goals/ReviewEntries/ReviewEntriesTable/tests/CellColumns.test.tsx index bd7133e6e6..aff52ef85e 100644 --- a/src/goals/ReviewEntries/ReviewEntriesComponent/tests/CellColumns.test.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/tests/CellColumns.test.tsx @@ -3,11 +3,11 @@ import "tests/reactI18nextMock"; import { GramCatGroup, GrammaticalInfo } from "api/models"; import columns, { ColumnTitle, -} from "goals/ReviewEntries/ReviewEntriesComponent/CellColumns"; +} from "goals/ReviewEntries/ReviewEntriesTable/CellColumns"; import { ReviewEntriesSense, ReviewEntriesWord, -} from "goals/ReviewEntries/ReviewEntriesComponent/ReviewEntriesTypes"; +} from "goals/ReviewEntries/ReviewEntriesTypes"; import { newSemanticDomain } from "types/semanticDomain"; import { newDefinition, newFlag, newGloss } from "types/word"; import { Bcp47Code } from "types/writingSystem"; diff --git a/src/goals/ReviewEntries/ReviewEntriesComponent/ReviewEntriesTypes.ts b/src/goals/ReviewEntries/ReviewEntriesTypes.ts similarity index 89% rename from src/goals/ReviewEntries/ReviewEntriesComponent/ReviewEntriesTypes.ts rename to src/goals/ReviewEntries/ReviewEntriesTypes.ts index 5af81ddfe8..0b06f81886 100644 --- a/src/goals/ReviewEntries/ReviewEntriesComponent/ReviewEntriesTypes.ts +++ b/src/goals/ReviewEntries/ReviewEntriesTypes.ts @@ -8,6 +8,7 @@ import { Status, Word, } from "api/models"; +import { Goal, GoalName, GoalType } from "types/goals"; import { newSense, newWord } from "types/word"; import { cleanDefinitions, cleanGlosses } from "utilities/wordUtilities"; @@ -23,6 +24,21 @@ export enum ColumnId { Flag, } +export class ReviewEntries extends Goal { + constructor() { + super(GoalType.ReviewEntries, GoalName.ReviewEntries); + } +} + +export type EntryEdit = { + newId: string; + oldId: string; +}; + +export interface EntriesEdited { + entryEdits: EntryEdit[]; +} + // These must match the ReviewEntriesWord fields for use in ReviewEntriesTable export enum ReviewEntriesWordField { Id = "id", diff --git a/src/goals/ReviewEntries/ReviewEntriesComponent/index.tsx b/src/goals/ReviewEntries/index.tsx similarity index 52% rename from src/goals/ReviewEntries/ReviewEntriesComponent/index.tsx rename to src/goals/ReviewEntries/index.tsx index d43d0bcb34..407aebcab0 100644 --- a/src/goals/ReviewEntries/ReviewEntriesComponent/index.tsx +++ b/src/goals/ReviewEntries/index.tsx @@ -5,26 +5,35 @@ import { sortBy, updateAllWords, updateFrontierWord, -} from "goals/ReviewEntries/ReviewEntriesComponent/Redux/ReviewEntriesActions"; -import ReviewEntriesTable from "goals/ReviewEntries/ReviewEntriesComponent/ReviewEntriesTable"; +} from "goals/ReviewEntries/Redux/ReviewEntriesActions"; +import ReviewEntriesCompleted from "goals/ReviewEntries/ReviewEntriesCompleted"; +import ReviewEntriesTable from "goals/ReviewEntries/ReviewEntriesTable"; import { ColumnId, ReviewEntriesWord, -} from "goals/ReviewEntries/ReviewEntriesComponent/ReviewEntriesTypes"; +} from "goals/ReviewEntries/ReviewEntriesTypes"; import { useAppDispatch } from "types/hooks"; -export default function ReviewEntriesComponent(): ReactElement { +interface ReviewEntriesProps { + completed: boolean; +} + +export default function ReviewEntries(props: ReviewEntriesProps): ReactElement { const dispatch = useAppDispatch(); const [loaded, setLoaded] = useState(false); useEffect(() => { - getFrontierWords().then((frontier) => { - dispatch(updateAllWords(frontier.map((w) => new ReviewEntriesWord(w)))); - setLoaded(true); - }); - }, [dispatch]); + if (!props.completed) { + getFrontierWords().then((frontier) => { + dispatch(updateAllWords(frontier.map((w) => new ReviewEntriesWord(w)))); + setLoaded(true); + }); + } + }, [dispatch, props]); - return loaded ? ( + return props.completed ? ( + + ) : loaded ? ( dispatch(updateFrontierWord(newData, oldData)) diff --git a/src/goals/ReviewEntries/ReviewEntriesComponent/tests/ReviewEntriesActions.test.tsx b/src/goals/ReviewEntries/tests/ReviewEntriesActions.test.tsx similarity index 98% rename from src/goals/ReviewEntries/ReviewEntriesComponent/tests/ReviewEntriesActions.test.tsx rename to src/goals/ReviewEntries/tests/ReviewEntriesActions.test.tsx index 67cda6fb0c..bf62285273 100644 --- a/src/goals/ReviewEntries/ReviewEntriesComponent/tests/ReviewEntriesActions.test.tsx +++ b/src/goals/ReviewEntries/tests/ReviewEntriesActions.test.tsx @@ -6,11 +6,11 @@ import { getSenseError, getSenseFromEditSense, updateFrontierWord, -} from "goals/ReviewEntries/ReviewEntriesComponent/Redux/ReviewEntriesActions"; +} from "goals/ReviewEntries/Redux/ReviewEntriesActions"; import { ReviewEntriesSense, ReviewEntriesWord, -} from "goals/ReviewEntries/ReviewEntriesComponent/ReviewEntriesTypes"; +} from "goals/ReviewEntries/ReviewEntriesTypes"; import { newSemanticDomain } from "types/semanticDomain"; import { newFlag, newGloss, newNote, newSense, newWord } from "types/word"; import { Bcp47Code } from "types/writingSystem"; diff --git a/src/goals/ReviewEntries/ReviewEntriesComponent/tests/ReviewEntriesReducer.test.tsx b/src/goals/ReviewEntries/tests/ReviewEntriesReducer.test.tsx similarity index 84% rename from src/goals/ReviewEntries/ReviewEntriesComponent/tests/ReviewEntriesReducer.test.tsx rename to src/goals/ReviewEntries/tests/ReviewEntriesReducer.test.tsx index 4b3ca74e49..b9873b3331 100644 --- a/src/goals/ReviewEntries/ReviewEntriesComponent/tests/ReviewEntriesReducer.test.tsx +++ b/src/goals/ReviewEntries/tests/ReviewEntriesReducer.test.tsx @@ -1,13 +1,13 @@ -import { reviewEntriesReducer } from "goals/ReviewEntries/ReviewEntriesComponent/Redux/ReviewEntriesReducer"; +import { reviewEntriesReducer } from "goals/ReviewEntries/Redux/ReviewEntriesReducer"; import { defaultState, ReviewEntriesActionTypes, -} from "goals/ReviewEntries/ReviewEntriesComponent/Redux/ReviewEntriesReduxTypes"; +} from "goals/ReviewEntries/Redux/ReviewEntriesReduxTypes"; import { ReviewEntriesSense, ReviewEntriesWord, -} from "goals/ReviewEntries/ReviewEntriesComponent/ReviewEntriesTypes"; -import mockWords from "goals/ReviewEntries/ReviewEntriesComponent/tests/WordsMock"; +} from "goals/ReviewEntries/ReviewEntriesTypes"; +import mockWords from "goals/ReviewEntries/tests/WordsMock"; import { newSemanticDomain } from "types/semanticDomain"; import { Bcp47Code } from "types/writingSystem"; diff --git a/src/goals/ReviewEntries/ReviewEntriesComponent/tests/WordsMock.ts b/src/goals/ReviewEntries/tests/WordsMock.ts similarity index 96% rename from src/goals/ReviewEntries/ReviewEntriesComponent/tests/WordsMock.ts rename to src/goals/ReviewEntries/tests/WordsMock.ts index c2a70a592c..01a2bfbca9 100644 --- a/src/goals/ReviewEntries/ReviewEntriesComponent/tests/WordsMock.ts +++ b/src/goals/ReviewEntries/tests/WordsMock.ts @@ -2,7 +2,7 @@ import { GramCatGroup, Sense, Word } from "api/models"; import { ReviewEntriesSense, ReviewEntriesWord, -} from "goals/ReviewEntries/ReviewEntriesComponent/ReviewEntriesTypes"; +} from "goals/ReviewEntries/ReviewEntriesTypes"; import { newSemanticDomain } from "types/semanticDomain"; import { newDefinition, diff --git a/src/goals/ReviewEntries/ReviewEntriesComponent/tests/index.test.tsx b/src/goals/ReviewEntries/tests/index.test.tsx similarity index 84% rename from src/goals/ReviewEntries/ReviewEntriesComponent/tests/index.test.tsx rename to src/goals/ReviewEntries/tests/index.test.tsx index 298cf1ae63..7590959b91 100644 --- a/src/goals/ReviewEntries/ReviewEntriesComponent/tests/index.test.tsx +++ b/src/goals/ReviewEntries/tests/index.test.tsx @@ -5,12 +5,10 @@ import configureMockStore from "redux-mock-store"; import "tests/reactI18nextMock"; -import ReviewEntriesComponent from "goals/ReviewEntries/ReviewEntriesComponent"; -import * as actions from "goals/ReviewEntries/ReviewEntriesComponent/Redux/ReviewEntriesActions"; -import { ReviewEntriesWord } from "goals/ReviewEntries/ReviewEntriesComponent/ReviewEntriesTypes"; -import mockWords, { - mockCreateWord, -} from "goals/ReviewEntries/ReviewEntriesComponent/tests/WordsMock"; +import ReviewEntries from "goals/ReviewEntries"; +import * as actions from "goals/ReviewEntries/Redux/ReviewEntriesActions"; +import { ReviewEntriesWord } from "goals/ReviewEntries/ReviewEntriesTypes"; +import mockWords, { mockCreateWord } from "goals/ReviewEntries/tests/WordsMock"; import { defaultWritingSystem } from "types/writingSystem"; const mockGetFrontierWords = jest.fn(); @@ -42,6 +40,9 @@ jest.mock("backend", () => ({ // Mock the node module used by AudioRecorder. jest.mock("components/Pronunciations/Recorder"); jest.mock("components/TreeView", () => "div"); +jest.mock("components/GoalTimeline/Redux/GoalActions", () => ({ + addEntryEditToGoal: () => jest.fn(), +})); jest.mock("types/hooks", () => ({ useAppDispatch: () => jest.fn(), })); @@ -61,11 +62,7 @@ const state = { reviewEntriesState: { words: mockReviewEntryWords }, treeViewState: { open: false, - currentDomain: { - name: "domain", - id: "number", - subdomains: [], - }, + currentDomain: { id: "number", name: "domain", subdomains: [] }, }, }; const mockStore = configureMockStore()(state); @@ -90,13 +87,13 @@ beforeEach(async () => { await act(async () => { create( - + ); }); }); -describe("ReviewEntriesComponent", () => { +describe("ReviewEntries", () => { it("Initializes correctly", () => { expect(updateAllWordsSpy).toHaveBeenCalled(); const wordIds = updateAllWordsSpy.mock.calls[0][0].map( diff --git a/src/rootReducer.ts b/src/rootReducer.ts index 34dde802f9..bd22360ec1 100644 --- a/src/rootReducer.ts +++ b/src/rootReducer.ts @@ -8,7 +8,7 @@ import { pronunciationsReducer } from "components/Pronunciations/Redux/Pronuncia import { treeViewReducer } from "components/TreeView/Redux/TreeViewReducer"; import { characterInventoryReducer } from "goals/CharacterInventory/Redux/CharacterInventoryReducer"; import mergeDupStepReducer from "goals/MergeDuplicates/Redux/MergeDupsReducer"; -import { reviewEntriesReducer } from "goals/ReviewEntries/ReviewEntriesComponent/Redux/ReviewEntriesReducer"; +import { reviewEntriesReducer } from "goals/ReviewEntries/Redux/ReviewEntriesReducer"; import { StoreState } from "types"; import { analyticsReducer } from "types/Redux/analytics"; diff --git a/src/types/goals.ts b/src/types/goals.ts index 8935d38af6..c2981dec8d 100644 --- a/src/types/goals.ts +++ b/src/types/goals.ts @@ -11,7 +11,7 @@ import { MergeStepData, MergesCompleted, } from "goals/MergeDuplicates/MergeDupsTypes"; -import { EntriesEdited } from "goals/ReviewEntries/ReviewEntries"; +import { EntriesEdited } from "goals/ReviewEntries/ReviewEntriesTypes"; import { newUser } from "types/user"; export type GoalData = CharInvData | MergeDupsData; diff --git a/src/types/index.ts b/src/types/index.ts index 114069a2de..024d8c07b1 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -5,7 +5,7 @@ import { PronunciationsState } from "components/Pronunciations/Redux/Pronunciati import { TreeViewState } from "components/TreeView/Redux/TreeViewReduxTypes"; import { CharacterInventoryState } from "goals/CharacterInventory/Redux/CharacterInventoryReduxTypes"; import { MergeTreeState } from "goals/MergeDuplicates/Redux/MergeDupsReduxTypes"; -import { ReviewEntriesState } from "goals/ReviewEntries/ReviewEntriesComponent/Redux/ReviewEntriesReduxTypes"; +import { ReviewEntriesState } from "goals/ReviewEntries/Redux/ReviewEntriesReduxTypes"; import { AnalyticsState } from "types/Redux/analyticsReduxTypes"; import { GoalsState } from "types/goals"; diff --git a/src/utilities/goalUtilities.ts b/src/utilities/goalUtilities.ts index 21f1be80ca..e5e3abf8b2 100644 --- a/src/utilities/goalUtilities.ts +++ b/src/utilities/goalUtilities.ts @@ -6,7 +6,7 @@ import { MergeDups, ReviewDeferredDups, } from "goals/MergeDuplicates/MergeDupsTypes"; -import { ReviewEntries } from "goals/ReviewEntries/ReviewEntries"; +import { ReviewEntries } from "goals/ReviewEntries/ReviewEntriesTypes"; import { SpellCheckGloss } from "goals/SpellCheckGloss/SpellCheckGloss"; import { ValidateChars } from "goals/ValidateChars/ValidateChars"; import { ValidateStrWords } from "goals/ValidateStrWords/ValidateStrWords"; From 8e0c4ec822db7502d13509a8a399bdfb45c79b7a Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Wed, 25 Oct 2023 17:34:55 -0400 Subject: [PATCH 04/11] Clean up --- public/locales/en/translation.json | 3 + src/components/GoalTimeline/GoalList.tsx | 7 +- .../Redux/ReviewEntriesActions.ts | 12 ++- .../tests/ReviewEntriesActions.test.tsx | 5 +- .../Redux/tests/ReviewEntriesReducer.test.tsx | 51 +++++++++++++ .../ReviewEntries/ReviewEntriesCompleted.tsx | 24 ++++-- src/goals/ReviewEntries/ReviewEntriesTypes.ts | 34 ++++++++- .../tests/ReviewEntriesReducer.test.tsx | 76 ------------------- src/goals/ReviewEntries/tests/WordsMock.ts | 32 +------- src/goals/ReviewEntries/tests/index.test.tsx | 17 +++-- 10 files changed, 133 insertions(+), 128 deletions(-) rename src/goals/ReviewEntries/{ => Redux}/tests/ReviewEntriesActions.test.tsx (98%) create mode 100644 src/goals/ReviewEntries/Redux/tests/ReviewEntriesReducer.test.tsx delete mode 100644 src/goals/ReviewEntries/tests/ReviewEntriesReducer.test.tsx diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 392a527094..8817cd415b 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -321,6 +321,9 @@ "search": "Search" } }, + "completed": { + "number": "Number of entries edited: " + }, "undo": { "undo": "Undo Edit", "undoDialog": "Undo this edit?", diff --git a/src/components/GoalTimeline/GoalList.tsx b/src/components/GoalTimeline/GoalList.tsx index d576bab38b..6817aef2a5 100644 --- a/src/components/GoalTimeline/GoalList.tsx +++ b/src/components/GoalTimeline/GoalList.tsx @@ -12,6 +12,8 @@ import { CharInvChangesGoalList } from "goals/CharacterInventory/CharInvComplete import { CharInvChanges } from "goals/CharacterInventory/CharacterInventoryTypes"; import { MergesCount } from "goals/MergeDuplicates/MergeDupsCompleted"; import { MergesCompleted } from "goals/MergeDuplicates/MergeDupsTypes"; +import { EditsCount } from "goals/ReviewEntries/ReviewEntriesCompleted"; +import { EntriesEdited } from "goals/ReviewEntries/ReviewEntriesTypes"; import { Goal, GoalStatus, GoalType } from "types/goals"; type Orientation = "horizontal" | "vertical"; @@ -120,7 +122,8 @@ function GoalTile(props: GoalTileProps): ReactElement { (goal.status === GoalStatus.Completed && goal.goalType !== GoalType.CreateCharInv && goal.goalType !== GoalType.MergeDups && - goal.goalType !== GoalType.ReviewDeferredDups) + goal.goalType !== GoalType.ReviewDeferredDups && + goal.goalType !== GoalType.ReviewEntries) } data-testid="goal-button" > @@ -161,6 +164,8 @@ function getCompletedGoalInfo(goal: Goal): ReactElement { case GoalType.MergeDups: case GoalType.ReviewDeferredDups: return MergesCount(goal.changes as MergesCompleted); + case GoalType.ReviewEntries: + return EditsCount(goal.changes as EntriesEdited); default: return ; } diff --git a/src/goals/ReviewEntries/Redux/ReviewEntriesActions.ts b/src/goals/ReviewEntries/Redux/ReviewEntriesActions.ts index 089cc8e6a1..dee559c3df 100644 --- a/src/goals/ReviewEntries/Redux/ReviewEntriesActions.ts +++ b/src/goals/ReviewEntries/Redux/ReviewEntriesActions.ts @@ -1,6 +1,9 @@ import { Sense } from "api/models"; import * as backend from "backend"; -import { addEntryEditToGoal } from "components/GoalTimeline/Redux/GoalActions"; +import { + addEntryEditToGoal, + asyncUpdateGoal, +} from "components/GoalTimeline/Redux/GoalActions"; import { uploadFileFromUrl } from "components/Pronunciations/utilities"; import { ReviewClearReviewEntriesState, @@ -32,8 +35,9 @@ export function updateAllWords(words: ReviewEntriesWord[]): ReviewUpdateWords { } function updateWord(oldId: string, updatedWord: ReviewEntriesWord) { - return (dispatch: StoreStateDispatch) => { + return async (dispatch: StoreStateDispatch) => { dispatch(addEntryEditToGoal({ newId: updatedWord.id, oldId })); + await dispatch(asyncUpdateGoal()); const update: ReviewUpdateWord = { type: ReviewEntriesActionTypes.UpdateWord, oldId, @@ -174,7 +178,7 @@ export function updateFrontierWord( editSource.audio = (await backend.getWord(editSource.id)).audio; // Update the review entries word in the state. - dispatch(updateWord(editWord.id, editSource)); + await dispatch(updateWord(editWord.id, editSource)); }; } @@ -203,7 +207,7 @@ function refreshWord( return async (dispatch: StoreStateDispatch): Promise => { const newWordId = await wordUpdater(oldWordId); const word = await backend.getWord(newWordId); - dispatch(updateWord(oldWordId, new ReviewEntriesWord(word))); + await dispatch(updateWord(oldWordId, new ReviewEntriesWord(word))); }; } diff --git a/src/goals/ReviewEntries/tests/ReviewEntriesActions.test.tsx b/src/goals/ReviewEntries/Redux/tests/ReviewEntriesActions.test.tsx similarity index 98% rename from src/goals/ReviewEntries/tests/ReviewEntriesActions.test.tsx rename to src/goals/ReviewEntries/Redux/tests/ReviewEntriesActions.test.tsx index bf62285273..b4d3ef1995 100644 --- a/src/goals/ReviewEntries/tests/ReviewEntriesActions.test.tsx +++ b/src/goals/ReviewEntries/Redux/tests/ReviewEntriesActions.test.tsx @@ -25,10 +25,13 @@ jest.mock("backend", () => ({ getWord: (wordId: string) => mockGetWord(wordId), updateWord: (word: Word) => mockUpdateWord(word), })); - jest.mock("backend/localStorage", () => ({ getProjectId: jest.fn(), })); +jest.mock("components/GoalTimeline/Redux/GoalActions", () => ({ + addEntryEditToGoal: () => jest.fn(), + asyncUpdateGoal: () => jest.fn(), +})); const mockStore = configureMockStore([thunk])(); diff --git a/src/goals/ReviewEntries/Redux/tests/ReviewEntriesReducer.test.tsx b/src/goals/ReviewEntries/Redux/tests/ReviewEntriesReducer.test.tsx new file mode 100644 index 0000000000..0192c2a340 --- /dev/null +++ b/src/goals/ReviewEntries/Redux/tests/ReviewEntriesReducer.test.tsx @@ -0,0 +1,51 @@ +import { reviewEntriesReducer } from "goals/ReviewEntries/Redux/ReviewEntriesReducer"; +import { + defaultState, + ReviewEntriesActionTypes, +} from "goals/ReviewEntries/Redux/ReviewEntriesReduxTypes"; +import { + ReviewEntriesSense, + ReviewEntriesWord, +} from "goals/ReviewEntries/ReviewEntriesTypes"; + +describe("ReviewEntriesReducer", () => { + it("Returns default state when passed undefined state", () => { + expect(reviewEntriesReducer(undefined, { type: undefined } as any)).toEqual( + defaultState + ); + }); + + it("Adds a set of words to a list when passed an UpdateAllWords action", () => { + const revWords = [new ReviewEntriesWord(), new ReviewEntriesWord()]; + const state = reviewEntriesReducer(defaultState, { + type: ReviewEntriesActionTypes.UpdateAllWords, + words: revWords, + }); + expect(state).toEqual({ ...defaultState, words: revWords }); + }); + + it("Updates a specified word when passed an UpdateWord action", () => { + const oldId = "id-of-word-to-be-updated"; + const oldWords: ReviewEntriesWord[] = [ + { ...new ReviewEntriesWord(), id: "other-id" }, + { ...new ReviewEntriesWord(), id: oldId, vernacular: "old-vern" }, + ]; + const oldState = { ...defaultState, words: oldWords }; + + const newId = "id-after-update"; + const newRevWord: ReviewEntriesWord = { + ...new ReviewEntriesWord(), + id: newId, + vernacular: "new-vern", + senses: [{ ...new ReviewEntriesSense(), guid: "new-sense-id" }], + }; + const newWords = [oldWords[0], newRevWord]; + + const newState = reviewEntriesReducer(oldState, { + type: ReviewEntriesActionTypes.UpdateWord, + oldId, + updatedWord: newRevWord, + }); + expect(newState).toEqual({ ...oldState, words: newWords }); + }); +}); diff --git a/src/goals/ReviewEntries/ReviewEntriesCompleted.tsx b/src/goals/ReviewEntries/ReviewEntriesCompleted.tsx index e179150888..d612c0d1fe 100644 --- a/src/goals/ReviewEntries/ReviewEntriesCompleted.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesCompleted.tsx @@ -24,15 +24,23 @@ export default function ReviewEntriesCompleted(): ReactElement { {t("reviewEntries.title")} - - {t("reviewEntries.completed.number")} - {changes.entryEdits?.length ?? 0} - + {EditsCount(changes)} {changes.entryEdits?.map((e) => )} ); } +export function EditsCount(changes: EntriesEdited): ReactElement { + const { t } = useTranslation(); + + return ( + + {t("reviewEntries.completed.number")} + {changes.entryEdits?.length ?? 0} + + ); +} + function EditedEntry(props: { edit: EntryEdit }): ReactElement { return ( @@ -82,7 +90,7 @@ function UndoButton(props: UndoButtonProps): ReactElement { return isUndoEnabled ? ( - <> +
) : ( - <> +
- +
); } diff --git a/src/goals/ReviewEntries/ReviewEntriesTypes.ts b/src/goals/ReviewEntries/ReviewEntriesTypes.ts index 0b06f81886..7bcd8b190a 100644 --- a/src/goals/ReviewEntries/ReviewEntriesTypes.ts +++ b/src/goals/ReviewEntries/ReviewEntriesTypes.ts @@ -9,7 +9,7 @@ import { Word, } from "api/models"; import { Goal, GoalName, GoalType } from "types/goals"; -import { newSense, newWord } from "types/word"; +import { newNote, newSense, newWord } from "types/word"; import { cleanDefinitions, cleanGlosses } from "utilities/wordUtilities"; export enum ColumnId { @@ -113,3 +113,35 @@ export class ReviewEntriesSense { return sense.glosses.map((g) => g.def).join(ReviewEntriesSense.SEPARATOR); } } + +/** Reverse map of the ReviewEntriesSense constructor. + * Important: Not everything is preserved! */ +function senseFromReviewEntriesSense(revSense: ReviewEntriesSense): Sense { + return { + ...newSense(), + accessibility: revSense.protected + ? Status.Protected + : revSense.deleted + ? Status.Deleted + : Status.Active, + definitions: revSense.definitions.map((d) => ({ ...d })), + glosses: revSense.glosses.map((g) => ({ ...g })), + grammaticalInfo: revSense.partOfSpeech, + guid: revSense.guid, + semanticDomains: revSense.domains.map((dom) => ({ ...dom })), + }; +} + +/** Reverse map of the ReviewEntriesWord constructor. + * Important: Not everything is preserved! */ +export function wordFromReviewEntriesWord(revWord: ReviewEntriesWord): Word { + return { + ...newWord(revWord.vernacular), + accessibility: revWord.protected ? Status.Protected : Status.Active, + audio: [...revWord.audio], + id: revWord.id, + flag: { ...revWord.flag }, + note: newNote(revWord.noteText), + senses: revWord.senses.map(senseFromReviewEntriesSense), + }; +} diff --git a/src/goals/ReviewEntries/tests/ReviewEntriesReducer.test.tsx b/src/goals/ReviewEntries/tests/ReviewEntriesReducer.test.tsx deleted file mode 100644 index b9873b3331..0000000000 --- a/src/goals/ReviewEntries/tests/ReviewEntriesReducer.test.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { reviewEntriesReducer } from "goals/ReviewEntries/Redux/ReviewEntriesReducer"; -import { - defaultState, - ReviewEntriesActionTypes, -} from "goals/ReviewEntries/Redux/ReviewEntriesReduxTypes"; -import { - ReviewEntriesSense, - ReviewEntriesWord, -} from "goals/ReviewEntries/ReviewEntriesTypes"; -import mockWords from "goals/ReviewEntries/tests/WordsMock"; -import { newSemanticDomain } from "types/semanticDomain"; -import { Bcp47Code } from "types/writingSystem"; - -const mockState = { - ...defaultState, - words: mockWords(), -}; -const reviewEntriesWord: ReviewEntriesWord = { - ...new ReviewEntriesWord(), - id: mockState.words[0].id, - vernacular: "toadTOAD", - senses: [ - { - ...new ReviewEntriesSense(), - guid: "1", - glosses: [{ def: "bupBUP", language: Bcp47Code.En }], - domains: [ - newSemanticDomain("number", "domain"), - newSemanticDomain("number2", "domain2"), - ], - }, - ], -}; -const result: ReviewEntriesWord = { - ...new ReviewEntriesWord(), - id: "a new mock id", - vernacular: "toadTOAD", - senses: [ - { - ...new ReviewEntriesSense(), - guid: "1", - glosses: [{ def: "bupBUP", language: Bcp47Code.En }], - domains: [ - newSemanticDomain("number", "domain"), - newSemanticDomain("number2", "domain2"), - ], - }, - ], -}; - -describe("ReviewEntriesReducer", () => { - it("Returns default state when passed undefined state", () => { - expect(reviewEntriesReducer(undefined, { type: undefined } as any)).toEqual( - defaultState - ); - }); - - it("Adds a set of words to a list when passed an UpdateAllWords action", () => { - expect( - reviewEntriesReducer(defaultState, { - type: ReviewEntriesActionTypes.UpdateAllWords, - words: mockWords(), - }) - ).toEqual(mockState); - }); - - it("Updates a specified word when passed an UpdateWord action", () => { - expect( - reviewEntriesReducer(mockState, { - type: ReviewEntriesActionTypes.UpdateWord, - oldId: mockWords()[0].id, - updatedWord: { ...reviewEntriesWord, id: result.id }, - }) - ).toEqual({ ...mockState, words: [result, mockWords()[1]] }); - }); -}); diff --git a/src/goals/ReviewEntries/tests/WordsMock.ts b/src/goals/ReviewEntries/tests/WordsMock.ts index 01a2bfbca9..45d65b8ecc 100644 --- a/src/goals/ReviewEntries/tests/WordsMock.ts +++ b/src/goals/ReviewEntries/tests/WordsMock.ts @@ -1,17 +1,10 @@ -import { GramCatGroup, Sense, Word } from "api/models"; +import { GramCatGroup } from "api/models"; import { ReviewEntriesSense, ReviewEntriesWord, } from "goals/ReviewEntries/ReviewEntriesTypes"; import { newSemanticDomain } from "types/semanticDomain"; -import { - newDefinition, - newFlag, - newGloss, - newNote, - newSense, - newWord, -} from "types/word"; +import { newDefinition, newFlag, newGloss } from "types/word"; import { Bcp47Code } from "types/writingSystem"; export default function mockWords(): ReviewEntriesWord[] { @@ -57,24 +50,3 @@ export default function mockWords(): ReviewEntriesWord[] { }, ]; } - -export function mockCreateWord(word: ReviewEntriesWord): Word { - return { - ...newWord(word.vernacular), - id: word.id, - senses: word.senses.map((sense) => createMockSense(sense)), - note: newNote(word.noteText), - flag: word.flag, - }; -} - -function createMockSense(sense: ReviewEntriesSense): Sense { - return { - ...newSense(), - guid: sense.guid, - definitions: [...sense.definitions], - glosses: [...sense.glosses], - grammaticalInfo: sense.partOfSpeech, - semanticDomains: [...sense.domains], - }; -} diff --git a/src/goals/ReviewEntries/tests/index.test.tsx b/src/goals/ReviewEntries/tests/index.test.tsx index 7590959b91..ab346fade8 100644 --- a/src/goals/ReviewEntries/tests/index.test.tsx +++ b/src/goals/ReviewEntries/tests/index.test.tsx @@ -7,8 +7,11 @@ import "tests/reactI18nextMock"; import ReviewEntries from "goals/ReviewEntries"; import * as actions from "goals/ReviewEntries/Redux/ReviewEntriesActions"; -import { ReviewEntriesWord } from "goals/ReviewEntries/ReviewEntriesTypes"; -import mockWords, { mockCreateWord } from "goals/ReviewEntries/tests/WordsMock"; +import { + ReviewEntriesWord, + wordFromReviewEntriesWord, +} from "goals/ReviewEntries/ReviewEntriesTypes"; +import mockWords from "goals/ReviewEntries/tests/WordsMock"; import { defaultWritingSystem } from "types/writingSystem"; const mockGetFrontierWords = jest.fn(); @@ -33,16 +36,16 @@ jest.mock("notistack", () => ({ ...jest.requireActual("notistack"), enqueueSnackbar: jest.fn(), })); -jest.mock("uuid", () => ({ v4: () => mockUuid() })); +jest.mock("uuid", () => ({ + v4: () => mockUuid(), +})); jest.mock("backend", () => ({ getFrontierWords: (...args: any[]) => mockGetFrontierWords(...args), })); // Mock the node module used by AudioRecorder. jest.mock("components/Pronunciations/Recorder"); jest.mock("components/TreeView", () => "div"); -jest.mock("components/GoalTimeline/Redux/GoalActions", () => ({ - addEntryEditToGoal: () => jest.fn(), -})); +jest.mock("components/GoalTimeline/Redux/GoalActions", () => ({})); jest.mock("types/hooks", () => ({ useAppDispatch: () => jest.fn(), })); @@ -70,7 +73,7 @@ const mockStore = configureMockStore()(state); function setMockFunctions(): void { jest.clearAllMocks(); mockGetFrontierWords.mockResolvedValue( - mockReviewEntryWords.map(mockCreateWord) + mockReviewEntryWords.map(wordFromReviewEntriesWord) ); mockMaterialTable.mockReturnValue(Fragment); } From 508c4e899bf49e556260fff9ef93378f01d59045 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Wed, 25 Oct 2023 18:16:17 -0400 Subject: [PATCH 05/11] Create a common undo button --- src/components/Buttons/UndoButton.tsx | 61 +++++++++++++ src/components/Buttons/index.ts | 2 + .../MergeDuplicates/MergeDupsCompleted.tsx | 79 ++++------------- .../ReviewEntries/ReviewEntriesCompleted.tsx | 85 +++++-------------- 4 files changed, 99 insertions(+), 128 deletions(-) create mode 100644 src/components/Buttons/UndoButton.tsx diff --git a/src/components/Buttons/UndoButton.tsx b/src/components/Buttons/UndoButton.tsx new file mode 100644 index 0000000000..f5b3c2cff6 --- /dev/null +++ b/src/components/Buttons/UndoButton.tsx @@ -0,0 +1,61 @@ +import { Button, Grid } from "@mui/material"; +import { ReactElement, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; + +import { CancelConfirmDialog } from "components/Dialogs"; + +interface UndoButtonProps { + buttonIdEnabled?: string; + buttonIdCancel?: string; + buttonIdConfirm?: string; + textIdDialog: string; + textIdDisabled: string; + textIdEnabled: string; + isUndoAllowed: () => Promise; + undo: () => Promise; +} + +export default function UndoButton(props: UndoButtonProps): ReactElement { + const isUndoAllowed = props.isUndoAllowed; + + const [isUndoEnabled, setUndoEnabled] = useState(false); + const [undoDialogOpen, setUndoDialogOpen] = useState(false); + + const { t } = useTranslation(); + + useEffect(() => { + if (!undoDialogOpen) { + isUndoAllowed().then(setUndoEnabled); + } + }, [isUndoAllowed, undoDialogOpen]); + + return ( + + {isUndoEnabled ? ( +
+ + setUndoDialogOpen(false)} + handleConfirm={() => + props.undo().then(() => setUndoDialogOpen(false)) + } + buttonIdCancel={props.buttonIdCancel} + buttonIdConfirm={props.buttonIdConfirm} + /> +
+ ) : ( +
+ +
+ )} +
+ ); +} diff --git a/src/components/Buttons/index.ts b/src/components/Buttons/index.ts index a5135428ee..3889259e5f 100644 --- a/src/components/Buttons/index.ts +++ b/src/components/Buttons/index.ts @@ -4,6 +4,7 @@ import IconButtonWithTooltip from "components/Buttons/IconButtonWithTooltip"; import LoadingButton from "components/Buttons/LoadingButton"; import LoadingDoneButton from "components/Buttons/LoadingDoneButton"; import PartOfSpeechButton from "components/Buttons/PartOfSpeechButton"; +import UndoButton from "components/Buttons/UndoButton"; export { FileInputButton, @@ -12,4 +13,5 @@ export { LoadingButton, LoadingDoneButton, PartOfSpeechButton, + UndoButton, }; diff --git a/src/goals/MergeDuplicates/MergeDupsCompleted.tsx b/src/goals/MergeDuplicates/MergeDupsCompleted.tsx index c078e1b4bd..0f9dfbf308 100644 --- a/src/goals/MergeDuplicates/MergeDupsCompleted.tsx +++ b/src/goals/MergeDuplicates/MergeDupsCompleted.tsx @@ -1,13 +1,12 @@ import { ArrowRightAlt } from "@mui/icons-material"; -import { Button, Card, Grid, Paper, Typography } from "@mui/material"; +import { Card, Grid, Paper, Typography } from "@mui/material"; import { ReactElement, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; import { Flag, MergeUndoIds, Sense, Word } from "api/models"; import { getFrontierWords, getWord, undoMerge } from "backend"; -import { FlagButton } from "components/Buttons"; -import { CancelConfirmDialog } from "components/Dialogs"; +import { FlagButton, UndoButton } from "components/Buttons"; import SenseCardContent from "goals/MergeDuplicates/MergeDupsStep/SenseCardContent"; import { MergesCompleted } from "goals/MergeDuplicates/MergeDupsTypes"; import { StoreState } from "types"; @@ -45,6 +44,9 @@ export function MergesCount(changes: MergesCompleted): ReactElement { } function MergeChange(change: MergeUndoIds): ReactElement { + const handleIsUndoAllowed = (): Promise => + getFrontierWords().then((words) => doWordsIncludeMerges(words, change)); + return (
)} { + await undoMerge(change); + }} />
); } -interface UndoButtonProps { - merge: MergeUndoIds; - textId: string; - dialogId: string; - disabledId: string; -} - -function UndoButton(props: UndoButtonProps): ReactElement { - const [isUndoBtnEnabled, setUndoBtnEnabled] = useState(false); - const [undoDialogOpen, setUndoDialogOpen] = useState(false); - const { t } = useTranslation(); - - useEffect(() => { - function checkFrontier(): void { - getFrontierWords().then((words) => - setUndoBtnEnabled( - props.merge ? doWordsIncludeMerges(words, props.merge) : false - ) - ); - } - checkFrontier(); - }); - - if (isUndoBtnEnabled) { - return ( - -
- - setUndoDialogOpen(false)} - handleConfirm={() => - undoMerge(props.merge).then(() => setUndoDialogOpen(false)) - } - buttonIdCancel="merge-undo-cancel" - buttonIdConfirm="merge-undo-confirm" - /> -
-
- ); - } - return ( - -
- -
-
- ); -} - export function doWordsIncludeMerges( words: Word[], merge: MergeUndoIds diff --git a/src/goals/ReviewEntries/ReviewEntriesCompleted.tsx b/src/goals/ReviewEntries/ReviewEntriesCompleted.tsx index d612c0d1fe..73c550e79c 100644 --- a/src/goals/ReviewEntries/ReviewEntriesCompleted.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesCompleted.tsx @@ -1,11 +1,11 @@ import { ArrowRightAlt } from "@mui/icons-material"; -import { Button, Grid, Typography } from "@mui/material"; -import { ReactElement, useEffect, useState } from "react"; +import { Grid, Typography } from "@mui/material"; +import { ReactElement } from "react"; import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; import { getFrontierWords, getWord, updateWord } from "backend"; -import { CancelConfirmDialog } from "components/Dialogs"; +import { UndoButton } from "components/Buttons"; import { EntriesEdited, EntryEdit, @@ -41,7 +41,17 @@ export function EditsCount(changes: EntriesEdited): ReactElement { ); } +async function undoEdit(edit: EntryEdit): Promise { + const oldWord = await getWord(edit.oldId); + await updateWord({ ...oldWord, id: edit.newId }); +} + function EditedEntry(props: { edit: EntryEdit }): ReactElement { + const handleIsUndoAllowed = (): Promise => + getFrontierWords().then( + (words) => words.findIndex((w) => w.id === props.edit.newId) !== -1 + ); + return ( {props.edit.oldId} @@ -58,68 +68,15 @@ function EditedEntry(props: { edit: EntryEdit }): ReactElement { {props.edit.newId} undoEdit(props.edit)} />
); } - -interface UndoButtonProps { - edit: EntryEdit; - textId: string; - dialogId: string; - disabledId: string; -} - -function UndoButton(props: UndoButtonProps): ReactElement { - const [isUndoEnabled, setUndoEnabled] = useState(false); - const [undoDialogOpen, setUndoDialogOpen] = useState(false); - const { t } = useTranslation(); - - useEffect(() => { - function checkFrontier(): void { - getFrontierWords().then((words) => - setUndoEnabled(words.findIndex((w) => w.id === props.edit.newId) !== -1) - ); - } - checkFrontier(); - }); - - return isUndoEnabled ? ( - -
- - setUndoDialogOpen(false)} - handleConfirm={() => - undoEdit(props.edit).then(() => setUndoDialogOpen(false)) - } - buttonIdCancel="edit-undo-cancel" - buttonIdConfirm="edit-undo-confirm" - /> -
-
- ) : ( - -
- -
-
- ); -} - -const undoEdit = async (edit: EntryEdit): Promise => { - const oldWord = await getWord(edit.oldId); - await updateWord({ ...oldWord, id: edit.newId }); -}; From d28c34bff1ee5d22c2c0cb9cf2e59581c23f6104 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 17 Nov 2023 10:36:41 -0500 Subject: [PATCH 06/11] Make prettier --- src/goals/ReviewEntries/ReviewEntriesTypes.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/goals/ReviewEntries/ReviewEntriesTypes.ts b/src/goals/ReviewEntries/ReviewEntriesTypes.ts index 7bcd8b190a..780fa7b8e0 100644 --- a/src/goals/ReviewEntries/ReviewEntriesTypes.ts +++ b/src/goals/ReviewEntries/ReviewEntriesTypes.ts @@ -122,8 +122,8 @@ function senseFromReviewEntriesSense(revSense: ReviewEntriesSense): Sense { accessibility: revSense.protected ? Status.Protected : revSense.deleted - ? Status.Deleted - : Status.Active, + ? Status.Deleted + : Status.Active, definitions: revSense.definitions.map((d) => ({ ...d })), glosses: revSense.glosses.map((g) => ({ ...g })), grammaticalInfo: revSense.partOfSpeech, From 664df719ef1bc472bd3bba002b5f17e4723523c4 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 17 Nov 2023 16:11:28 -0500 Subject: [PATCH 07/11] Adopt WordCard from word-history branch --- src/components/Buttons/FlagButton.tsx | 4 +- .../EntryCellComponents/EntryNote.tsx | 18 ++- .../ProjectExport/DownloadButton.tsx | 4 +- src/components/WordCard/DomainChip.tsx | 37 +++++ src/components/WordCard/SenseCard.tsx | 51 ++++++ src/components/WordCard/SenseCardText.tsx | 122 +++++++++++++++ src/components/WordCard/SummarySenseCard.tsx | 57 +++++++ src/components/WordCard/index.tsx | 148 ++++++++++++++++++ src/components/WordCard/tests/index.test.tsx | 63 ++++++++ .../MergeDupsStep/SenseCardContent.tsx | 116 +------------- .../ReviewEntries/ReviewEntriesCompleted.tsx | 22 ++- src/utilities/tests/utilities.test.ts | 4 +- src/utilities/utilities.ts | 43 +++-- src/utilities/wordUtilities.ts | 33 +++- 14 files changed, 580 insertions(+), 142 deletions(-) create mode 100644 src/components/WordCard/DomainChip.tsx create mode 100644 src/components/WordCard/SenseCard.tsx create mode 100644 src/components/WordCard/SenseCardText.tsx create mode 100644 src/components/WordCard/SummarySenseCard.tsx create mode 100644 src/components/WordCard/index.tsx create mode 100644 src/components/WordCard/tests/index.test.tsx diff --git a/src/components/Buttons/FlagButton.tsx b/src/components/Buttons/FlagButton.tsx index 7d02f6dcef..b9cdc8d4e2 100644 --- a/src/components/Buttons/FlagButton.tsx +++ b/src/components/Buttons/FlagButton.tsx @@ -8,7 +8,7 @@ import { themeColors } from "types/theme"; interface FlagButtonProps { flag: Flag; - buttonId: string; + buttonId?: string; updateFlag?: (flag: Flag) => void; } @@ -56,7 +56,7 @@ export default function FlagButton(props: FlagButtonProps): ReactElement { onClick={ props.updateFlag ? () => setOpen(true) : active ? () => {} : undefined } - buttonId={props.buttonId} + buttonId={props.buttonId ?? "flag-button"} side="top" /> {props.updateFlag && ( diff --git a/src/components/DataEntry/DataEntryTable/EntryCellComponents/EntryNote.tsx b/src/components/DataEntry/DataEntryTable/EntryCellComponents/EntryNote.tsx index 142a48e4d1..b23fdadb9e 100644 --- a/src/components/DataEntry/DataEntryTable/EntryCellComponents/EntryNote.tsx +++ b/src/components/DataEntry/DataEntryTable/EntryCellComponents/EntryNote.tsx @@ -7,8 +7,8 @@ import { EditTextDialog } from "components/Dialogs"; interface EntryNoteProps { noteText: string; - updateNote: (newText: string) => void | Promise; - buttonId: string; + buttonId?: string; + updateNote?: (newText: string) => void | Promise; } /** @@ -18,17 +18,19 @@ export default function EntryNote(props: EntryNoteProps): ReactElement { const [noteOpen, setNoteOpen] = useState(false); const { t } = useTranslation(); + const handleClick = (): void => { + if (props.updateNote) { + setNoteOpen(true); + } + }; + return ( <> - setNoteOpen(true)} - id={props.buttonId} - > + {props.noteText ? : } @@ -37,7 +39,7 @@ export default function EntryNote(props: EntryNoteProps): ReactElement { text={props.noteText} titleId={"addWords.addNote"} close={() => setNoteOpen(false)} - updateText={props.updateNote} + updateText={props.updateNote ?? (() => {})} buttonIdCancel="note-edit-cancel" buttonIdConfirm="note-edit-confirm" textFieldId="note-text-field" diff --git a/src/components/ProjectExport/DownloadButton.tsx b/src/components/ProjectExport/DownloadButton.tsx index c929631eb6..97ee558ba9 100644 --- a/src/components/ProjectExport/DownloadButton.tsx +++ b/src/components/ProjectExport/DownloadButton.tsx @@ -19,10 +19,10 @@ import { ExportStatus } from "components/ProjectExport/Redux/ExportProjectReduxT import { StoreState } from "types"; import { useAppDispatch, useAppSelector } from "types/hooks"; import { themeColors } from "types/theme"; -import { getNowDateTimeString } from "utilities/utilities"; +import { getDateTimeString } from "utilities/utilities"; function makeExportName(projectName: string): string { - return `${projectName}_${getNowDateTimeString()}.zip`; + return `${projectName}_${getDateTimeString()}.zip`; } interface DownloadButtonProps { diff --git a/src/components/WordCard/DomainChip.tsx b/src/components/WordCard/DomainChip.tsx new file mode 100644 index 0000000000..11d3fe6760 --- /dev/null +++ b/src/components/WordCard/DomainChip.tsx @@ -0,0 +1,37 @@ +import { Chip } from "@mui/material"; +import { ReactElement, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; + +import { SemanticDomain } from "api/models"; +import { getUser } from "backend"; +import { friendlySep, getDateTimeString } from "utilities/utilities"; + +interface DomainChipProps { + domain: SemanticDomain; + provenance?: boolean; +} + +export default function DomainChip(props: DomainChipProps): ReactElement { + const { provenance } = props; + const { created, name, id, userId } = props.domain; + + const [username, setUsername] = useState(""); + const { t } = useTranslation(); + + useEffect(() => { + if (provenance && userId) { + getUser(userId).then((u) => setUsername(u.username)); + } + }, [provenance, userId]); + + const labelText = `${id}: ${name}`; + const hoverText = []; + if (provenance && created) { + const val = getDateTimeString(created, friendlySep); + hoverText.push(t("wordHistory.domainAdded", { val })); + } + if (provenance && username) { + hoverText.push(t("wordHistory.user", { val: username })); + } + return ; +} diff --git a/src/components/WordCard/SenseCard.tsx b/src/components/WordCard/SenseCard.tsx new file mode 100644 index 0000000000..088c8a517b --- /dev/null +++ b/src/components/WordCard/SenseCard.tsx @@ -0,0 +1,51 @@ +import { Card, CardContent, Grid } from "@mui/material"; +import { ReactElement } from "react"; + +import { GramCatGroup, Sense } from "api/models"; +import { PartOfSpeechButton } from "components/Buttons"; +import DomainChip from "components/WordCard/DomainChip"; +import SenseCardText from "components/WordCard/SenseCardText"; + +interface SenseCardProps { + languages?: string[]; + minimal?: boolean; + provenance?: boolean; + sense: Sense; +} + +export default function SenseCard(props: SenseCardProps): ReactElement { + const { grammaticalInfo, semanticDomains } = props.sense; + + return ( + + + {/* Part of speech (if any) */} +
+ {grammaticalInfo.catGroup !== GramCatGroup.Unspecified && ( + + )} +
+ + {/* Glosses and (if any) definitions */} + + + {/* Semantic domains */} + + {semanticDomains.map((d) => ( + + + + ))} + +
+
+ ); +} diff --git a/src/components/WordCard/SenseCardText.tsx b/src/components/WordCard/SenseCardText.tsx new file mode 100644 index 0000000000..5b23446f44 --- /dev/null +++ b/src/components/WordCard/SenseCardText.tsx @@ -0,0 +1,122 @@ +import { + Table, + TableBody, + TableCell, + TableRow, + Typography, +} from "@mui/material"; +import { CSSProperties, ReactElement } from "react"; + +import { Sense } from "api/models"; +import theme from "types/theme"; +import { TypographyWithFont } from "utilities/fontComponents"; + +interface SenseInLanguage { + language: string; // bcp-47 code + glossText: string; + definitionText: string; +} + +function getSenseInLanguage( + sense: Sense, + language: string, + displaySep = "; " +): SenseInLanguage { + const glossText = sense.glosses + .filter((g) => g.language === language) + .map((g) => g.def) + .join(displaySep); + const definitionText = sense.definitions + .filter((d) => d.language === language) + .map((d) => d.text) + .join(displaySep); + return { language, glossText, definitionText }; +} + +function getSenseInLanguages( + sense: Sense, + languages?: string[] +): SenseInLanguage[] { + if (!languages) { + languages = sense.glosses.map((g) => g.language); + languages.push(...sense.definitions.map((d) => d.language)); + languages = [...new Set(languages)]; + } + return languages.map((l) => getSenseInLanguage(sense, l)); +} + +interface SenseCardTextProps { + sense: Sense; + hideDefs?: boolean; + languages?: string[]; +} + +// Show glosses and (if not hideDefs) definitions. +export default function SenseCardText(props: SenseCardTextProps): ReactElement { + const senseTextInLangs = getSenseInLanguages(props.sense, props.languages); + + return ( + + + {senseTextInLangs.map((senseInLang, index) => ( + + ))} + +
+ ); +} + +const defStyle: CSSProperties = { + borderLeft: "1px solid black", + marginBottom: theme.spacing(1), + paddingLeft: theme.spacing(1), +}; + +interface SenseTextRowsProps { + senseInLang: SenseInLanguage; + hideDefs?: boolean; +} + +function SenseTextRows(props: SenseTextRowsProps): ReactElement { + const lang = props.senseInLang.language; + return ( + <> + {/* Gloss */} + + + + {lang} + {":"} + + + + + {props.senseInLang.glossText} + + + + + {/* Definition */} + {!!props.senseInLang.definitionText && !props.hideDefs && ( + + + +
+ + {props.senseInLang.definitionText} + +
+
+
+ )} + + ); +} diff --git a/src/components/WordCard/SummarySenseCard.tsx b/src/components/WordCard/SummarySenseCard.tsx new file mode 100644 index 0000000000..5826ac6f45 --- /dev/null +++ b/src/components/WordCard/SummarySenseCard.tsx @@ -0,0 +1,57 @@ +import { Card, CardContent, Chip, Grid, Typography } from "@mui/material"; +import { ReactElement } from "react"; +import { useTranslation } from "react-i18next"; + +import { GramCatGroup, Sense } from "api/models"; +import { PartOfSpeechButton } from "components/Buttons"; +import { groupGramInfo } from "utilities/wordUtilities"; + +interface SummarySenseCardProps { + senses: Sense[]; +} + +export default function SummarySenseCard( + props: SummarySenseCardProps +): ReactElement { + const { t } = useTranslation(); + + const senseGuids = props.senses.map((s) => s.guid).join("_"); + + const groupedGramInfo = groupGramInfo( + props.senses.map((s) => s.grammaticalInfo) + ).filter((info) => info.catGroup !== GramCatGroup.Unspecified); + + // Create a list of distinct semantic domain ids. + const semDoms = props.senses.flatMap((s) => s.semanticDomains); + const domIds = [...new Set(semDoms.map((d) => d.id))].sort(); + + return ( + + + {/* Parts of speech */} + {groupedGramInfo.map((info) => ( + + ))} + + {/* Sense count */} + + {t("wordHistory.senseCount", { val: props.senses.length })} + + + {/* Semantic domain numbers */} + + {domIds.map((id) => ( + + + + ))} + + + + ); +} diff --git a/src/components/WordCard/index.tsx b/src/components/WordCard/index.tsx new file mode 100644 index 0000000000..74d8a2ee31 --- /dev/null +++ b/src/components/WordCard/index.tsx @@ -0,0 +1,148 @@ +import { CloseFullscreen, OpenInFull, PlayArrow } from "@mui/icons-material"; +import { + Badge, + Card, + CardContent, + IconButton, + Typography, +} from "@mui/material"; +import { Fragment, ReactElement, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; + +import { Word } from "api/models"; +import { getUser } from "backend"; +import { FlagButton, IconButtonWithTooltip } from "components/Buttons"; +import { EntryNote } from "components/DataEntry/DataEntryTable/EntryCellComponents"; +import { PronunciationsBackend } from "components/Pronunciations/PronunciationsBackend"; +import SenseCard from "components/WordCard/SenseCard"; +import SummarySenseCard from "components/WordCard/SummarySenseCard"; +import { themeColors } from "types/theme"; +import { TypographyWithFont } from "utilities/fontComponents"; +import { friendlySep, getDateTimeString } from "utilities/utilities"; + +interface WordCardProps { + languages?: string[]; + provenance?: boolean; + word: Word; +} + +export const buttonIdFull = (wordId: string): string => `word-${wordId}-full`; + +export default function WordCard(props: WordCardProps): ReactElement { + const { languages, provenance, word } = props; + const { audio, editedBy, flag, id, note, senses } = word; + const [full, setFull] = useState(false); + const [username, setUsername] = useState(""); + const { t } = useTranslation(); + + useEffect(() => { + if (provenance && editedBy?.length) { + getUser(editedBy[editedBy.length - 1]).then((u) => + setUsername(u.username) + ); + } + }, [editedBy, provenance]); + + return ( + + + {/* Vernacular */} + + {word.vernacular} + + +
+ {/* Condensed audio, note, flag */} + {!full && ( + <> + + {!!note.text && } + {flag.active && } + + )} + {/* Button for expand/condense */} + + ) : ( + + ) + } + onClick={() => setFull(!full)} + /> +
+ + {/* Expanded audio, note, flag */} + {full && ( + <> + {audio.length > 0 && ( + {}} + playerOnly + pronunciationFiles={audio} + wordId={id} + /> + )} + {!!note.text && ( +
+ + {note.text} +
+ )} + {flag.active && ( +
+ + {flag.text} +
+ )} + + )} + + {/* Senses */} + {full ? ( + senses.map((s) => ( + + )) + ) : ( + + )} + + {/* Timestamps */} + {provenance && ( + + {t("wordHistory.wordId", { val: id })} +
+ {t("wordHistory.wordModified", { + val: getDateTimeString(word.modified, friendlySep), + })} + {!!username && ( + <> +
+ {t("wordHistory.user", { val: username })} + + )} +
+ )} +
+
+ ); +} + +export function AudioSummary(props: { count: number }): ReactElement { + return props.count > 0 ? ( + + + + + + ) : ( + + ); +} diff --git a/src/components/WordCard/tests/index.test.tsx b/src/components/WordCard/tests/index.test.tsx new file mode 100644 index 0000000000..6e44038e6a --- /dev/null +++ b/src/components/WordCard/tests/index.test.tsx @@ -0,0 +1,63 @@ +import { ReactTestRenderer, act, create } from "react-test-renderer"; + +import "tests/reactI18nextMock"; + +import { Word } from "api/models"; +import WordCard, { AudioSummary, buttonIdFull } from "components/WordCard"; +import SenseCard from "components/WordCard/SenseCard"; +import SummarySenseCard from "components/WordCard/SummarySenseCard"; +import { newSense, newWord } from "types/word"; + +// Mock the audio components +jest + .spyOn(window.HTMLMediaElement.prototype, "pause") + .mockImplementation(() => {}); +jest.mock("components/Pronunciations/AudioPlayer", () => "div"); +jest.mock("components/Pronunciations/Recorder"); + +const mockWordId = "mock-id"; +const buttonId = buttonIdFull(mockWordId); +const mockWord: Word = { ...newWord(), id: mockWordId }; +mockWord.audio.push("song", "speech", "rap", "poem"); +mockWord.senses.push(newSense(), newSense()); + +let cardHandle: ReactTestRenderer; + +const renderHistoryCell = async (): Promise => { + await act(async () => { + cardHandle = create(); + }); +}; + +beforeEach(async () => { + await renderHistoryCell(); +}); + +describe("HistoryCell", () => { + it("has summary and full views", async () => { + const button = cardHandle.root.findByProps({ id: buttonId }); + expect(cardHandle.root.findByType(AudioSummary).props.count).toEqual( + mockWord.audio.length + ); + expect(cardHandle.root.findAllByType(SenseCard)).toHaveLength(0); + expect(cardHandle.root.findAllByType(SummarySenseCard)).toHaveLength(1); + + await act(async () => { + button.props.onClick(); + }); + expect(cardHandle.root.findAllByType(AudioSummary)).toHaveLength(0); + expect(cardHandle.root.findAllByType(SenseCard)).toHaveLength( + mockWord.senses.length + ); + expect(cardHandle.root.findAllByType(SummarySenseCard)).toHaveLength(0); + + await act(async () => { + button.props.onClick(); + }); + expect(cardHandle.root.findByType(AudioSummary).props.count).toEqual( + mockWord.audio.length + ); + expect(cardHandle.root.findAllByType(SenseCard)).toHaveLength(0); + expect(cardHandle.root.findAllByType(SummarySenseCard)).toHaveLength(1); + }); +}); diff --git a/src/goals/MergeDuplicates/MergeDupsStep/SenseCardContent.tsx b/src/goals/MergeDuplicates/MergeDupsStep/SenseCardContent.tsx index adc68190c1..5bf24d91b6 100644 --- a/src/goals/MergeDuplicates/MergeDupsStep/SenseCardContent.tsx +++ b/src/goals/MergeDuplicates/MergeDupsStep/SenseCardContent.tsx @@ -1,110 +1,10 @@ import { ArrowForwardIos, WarningOutlined } from "@mui/icons-material"; -import { - CardContent, - Chip, - Grid, - IconButton, - Table, - TableBody, - TableCell, - TableRow, - Typography, -} from "@mui/material"; +import { CardContent, Chip, Grid, IconButton } from "@mui/material"; import { ReactElement } from "react"; import { GramCatGroup, Sense, Status } from "api/models"; import { IconButtonWithTooltip, PartOfSpeechButton } from "components/Buttons"; -import theme from "types/theme"; -import { TypographyWithFont } from "utilities/fontComponents"; - -interface SenseInLanguage { - language: string; // bcp-47 code - glossText: string; - definitionText?: string; -} - -function getSenseInLanguage( - sense: Sense, - language: string, - displaySep = "; " -): SenseInLanguage { - return { - language, - glossText: sense.glosses - .filter((g) => g.language === language) - .map((g) => g.def) - .join(displaySep), - definitionText: sense.definitions.length - ? sense.definitions - .filter((d) => d.language === language) - .map((d) => d.text) - .join(displaySep) - : undefined, - }; -} - -function getSenseInLanguages( - sense: Sense, - languages?: string[] -): SenseInLanguage[] { - if (!languages) { - languages = sense.glosses.map((g) => g.language); - languages.push(...sense.definitions.map((d) => d.language)); - languages = [...new Set(languages)]; - } - return languages.map((l) => getSenseInLanguage(sense, l)); -} - -interface SenseTextRowsProps { - senseInLang: SenseInLanguage; -} - -function SenseTextRows(props: SenseTextRowsProps): ReactElement { - const lang = props.senseInLang.language; - return ( - <> - - - - {lang} - {":"} - - - - - {props.senseInLang.glossText} - - - - {!!props.senseInLang.definitionText && ( - - - -
- - {props.senseInLang.definitionText} - -
-
-
- )} - - ); -} +import SenseCardText from "components/WordCard/SenseCardText"; interface SenseCardContentProps { senses: Sense[]; @@ -120,10 +20,6 @@ interface SenseCardContentProps { export default function SenseCardContent( props: SenseCardContentProps ): ReactElement { - const senseTextInLangs = getSenseInLanguages( - props.senses[0], - props.languages - ); const semDoms = [ ...new Set( props.senses.flatMap((s) => @@ -181,13 +77,7 @@ export default function SenseCardContent( )}
{/* List glosses and (if any) definitions. */} - - - {senseTextInLangs.map((senseInLang, index) => ( - - ))} - -
+ {/* List semantic domains. */} {semDoms.map((dom) => ( diff --git a/src/goals/ReviewEntries/ReviewEntriesCompleted.tsx b/src/goals/ReviewEntries/ReviewEntriesCompleted.tsx index 73c550e79c..23d3a0a512 100644 --- a/src/goals/ReviewEntries/ReviewEntriesCompleted.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesCompleted.tsx @@ -1,11 +1,13 @@ import { ArrowRightAlt } from "@mui/icons-material"; import { Grid, Typography } from "@mui/material"; -import { ReactElement } from "react"; +import { Fragment, ReactElement, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; +import { Word } from "api/models"; import { getFrontierWords, getWord, updateWord } from "backend"; import { UndoButton } from "components/Buttons"; +import WordCard from "components/WordCard"; import { EntriesEdited, EntryEdit, @@ -47,14 +49,26 @@ async function undoEdit(edit: EntryEdit): Promise { } function EditedEntry(props: { edit: EntryEdit }): ReactElement { + const { oldId, newId } = props.edit; + + const [oldWord, setOldWord] = useState(); + const [newWord, setNewWord] = useState(); + + useEffect(() => { + getWord(oldId).then(setOldWord); + }, [oldId]); + useEffect(() => { + getWord(newId).then(setNewWord); + }, [newId]); + const handleIsUndoAllowed = (): Promise => getFrontierWords().then( - (words) => words.findIndex((w) => w.id === props.edit.newId) !== -1 + (words) => words.findIndex((w) => w.id === newId) !== -1 ); return ( - {props.edit.oldId} + {oldWord ? : } - {props.edit.newId} + {newWord ? : } { }); }); - describe("getNowDateTimeString", () => { + describe("getDateTimeString", () => { // This tests will fail intermittently if there is a bug with the 0-prepend it("returns string of correct length", () => { const expectedLength = "YYYY-MM-DD_hh-mm-ss".length; - expect(utilities.getNowDateTimeString().length).toBe(expectedLength); + expect(utilities.getDateTimeString()).toHaveLength(expectedLength); }); }); diff --git a/src/utilities/utilities.ts b/src/utilities/utilities.ts index b292d1068b..890bcdaedc 100644 --- a/src/utilities/utilities.ts +++ b/src/utilities/utilities.ts @@ -35,19 +35,44 @@ export function quicksort(arr: T[], score: (item: T) => number): T[] { return [...quicksort(less, score), pivot, ...quicksort(greater, score)]; } -export function getNowDateTimeString(): string { - const now = new Date(Date.now()); +interface DateTimeSeparators { + date?: string; + dateTime?: string; + time?: string; +} + +export const friendlySep: DateTimeSeparators = { + date: "/", + dateTime: " ", + time: ":", +}; + +const pathSep: DateTimeSeparators = { + date: "-", + dateTime: "_", + time: "-", +}; + +/** Create a date-time string for the provided utc-string, or now() if not specified. + * Use path-friendly separators by default if not specified. */ +export function getDateTimeString( + utcString?: string, + sep?: DateTimeSeparators +): string { + const date = new Date(utcString ?? Date.now()); const vals = [ - now.getFullYear(), + date.getFullYear(), // Date.getMonth() starts at 0 for January. - now.getMonth() + 1, - now.getDate(), - now.getHours(), - now.getMinutes(), - now.getSeconds(), + date.getMonth() + 1, + date.getDate(), + date.getHours(), + date.getMinutes(), + date.getSeconds(), ]; const strs = vals.map((value) => (value < 10 ? `0${value}` : `${value}`)); - return `${strs.slice(0, 3).join("-")}_${strs.slice(3, 6).join("-")}`; + const dateString = strs.slice(0, 3).join(sep?.date ?? pathSep.date); + const timeString = strs.slice(3, 6).join(sep?.time ?? pathSep.time); + return `${dateString}${sep?.dateTime ?? pathSep.dateTime}${timeString}`; } // A general-purpose edit distance. diff --git a/src/utilities/wordUtilities.ts b/src/utilities/wordUtilities.ts index e582b8f675..770bc5a98e 100644 --- a/src/utilities/wordUtilities.ts +++ b/src/utilities/wordUtilities.ts @@ -1,4 +1,12 @@ -import { Definition, Flag, Gloss, GramCatGroup, Sense, Word } from "api/models"; +import { + Definition, + Flag, + Gloss, + GramCatGroup, + GrammaticalInfo, + Sense, + Word, +} from "api/models"; import { HEX, colorblindSafePalette } from "types/theme"; import { newDefinition, newGloss } from "types/word"; @@ -55,7 +63,7 @@ export function firstGlossText(sense: Sense): string { } /** - * Given a word-array, return a string-array with any language code found in + * Given a word array, return a string array with any language code found in * a definition or gloss of any sense. */ export function getAnalysisLangsFromWords(words: Word[]): string[] { @@ -77,6 +85,27 @@ function wordReducer(accumulator: string[], word: Word): string[] { return [...new Set([...accumulator, ...newLangs])]; } +/** Given a grammatical-info array, return an array with one per GramCatGroup. */ +export function groupGramInfo( + infos: GrammaticalInfo[], + sep = " ; " +): GrammaticalInfo[] { + const catGroups = [...new Set(infos.map((i) => i.catGroup))].sort(); + + /** Concatenate all grammatical categories of a given group */ + const groupedGramCat = (group: GramCatGroup): string => { + const cats = infos + .filter((i) => i.catGroup === group) + .map((i) => i.grammaticalCategory); + return [...new Set(cats)].sort().join(sep); + }; + + return catGroups.map((c) => ({ + catGroup: c, + grammaticalCategory: groupedGramCat(c), + })); +} + /** Assign a different color to each grammatical category group. */ export function getGramCatGroupColor(group: GramCatGroup): HEX { switch (group) { From ccc5c4cacc7cb1daa0a23806e8874ffd8a91ec34 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 17 Nov 2023 16:39:36 -0500 Subject: [PATCH 08/11] Add translation phrases; Add frontier api function; Improve layout --- .../Controllers/WordControllerTests.cs | 28 ++++ Backend/Controllers/WordController.cs | 17 +++ public/locales/en/translation.json | 7 + src/api/api/word-api.ts | 132 ++++++++++++++++++ src/backend/index.ts | 9 ++ src/components/WordCard/DomainChip.tsx | 4 +- src/components/WordCard/SummarySenseCard.tsx | 2 +- src/components/WordCard/index.tsx | 8 +- .../ReviewEntries/ReviewEntriesCompleted.tsx | 63 ++++----- 9 files changed, 231 insertions(+), 39 deletions(-) diff --git a/Backend.Tests/Controllers/WordControllerTests.cs b/Backend.Tests/Controllers/WordControllerTests.cs index a62c7c07a4..0c99ec1b8c 100644 --- a/Backend.Tests/Controllers/WordControllerTests.cs +++ b/Backend.Tests/Controllers/WordControllerTests.cs @@ -143,6 +143,34 @@ public async Task TestIsFrontierNonemptyMissingProject() Assert.That(result, Is.InstanceOf()); } + [Test] + public async Task TestIsInFrontier() + { + var wordNotInFrontier = await _wordRepo.Add(Util.RandomWord(_projId)); + var falseResult = (ObjectResult)await _wordController.IsInFrontier(_projId, wordNotInFrontier.Id); + Assert.That(falseResult.Value, Is.False); + + var wordInFrontier = await _wordRepo.AddFrontier(Util.RandomWord(_projId)); + var trueResult = (ObjectResult)await _wordController.IsInFrontier(_projId, wordInFrontier.Id); + Assert.That(trueResult.Value, Is.True); + } + + [Test] + public async Task TestIsInFrontierNoPermission() + { + var wordInFrontier = await _wordRepo.AddFrontier(Util.RandomWord(_projId)); + _wordController.ControllerContext.HttpContext = PermissionServiceMock.UnauthorizedHttpContext(); + var result = await _wordController.IsInFrontier(_projId, wordInFrontier.Id); + Assert.That(result, Is.InstanceOf()); + } + + [Test] + public async Task TestIsInFrontierMissingProject() + { + var result = await _wordController.IsInFrontier(MissingId, "anything"); + Assert.That(result, Is.InstanceOf()); + } + [Test] public async Task TestGetFrontier() { diff --git a/Backend/Controllers/WordController.cs b/Backend/Controllers/WordController.cs index 0f6db9917f..46f9862baf 100644 --- a/Backend/Controllers/WordController.cs +++ b/Backend/Controllers/WordController.cs @@ -124,6 +124,23 @@ public async Task GetProjectFrontierWords(string projectId) return Ok(await _wordRepo.GetFrontier(projectId)); } + /// Checks if Frontier has in specified . + [HttpGet("isinfrontier/{wordId}", Name = "IsInFrontier")] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(bool))] + public async Task IsInFrontier(string projectId, string wordId) + { + if (!await _permissionService.HasProjectPermission(HttpContext, Permission.WordEntry, projectId)) + { + return Forbid(); + } + var project = await _projRepo.GetProject(projectId); + if (project is null) + { + return NotFound(projectId); + } + return Ok(await _wordRepo.IsInFrontier(projectId, wordId)); + } + /// /// Checks if a is a duplicate--i.e., are its primary text fields /// (Vernacular, Gloss text, Definition text) contained in a frontier entry? diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 5e3cd90f1b..0d3b04b610 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -490,5 +490,12 @@ "Other": "Other", "Unspecified": "Unspecified" } + }, + "wordCard": { + "senseCount": "Senses: {{ val }}", + "wordId": "Id: {{ val }}", + "wordModified": "Modified: {{ val }}", + "domainAdded": "Added: {{ val }}", + "user": "By user: {{ val }}" } } diff --git a/src/api/api/word-api.ts b/src/api/api/word-api.ts index ce1bbdedec..196a1f3ec0 100644 --- a/src/api/api/word-api.ts +++ b/src/api/api/word-api.ts @@ -391,6 +391,55 @@ export const WordApiAxiosParamCreator = function ( options: localVarRequestOptions, }; }, + /** + * + * @param {string} projectId + * @param {string} wordId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + isInFrontier: async ( + projectId: string, + wordId: string, + options: any = {} + ): Promise => { + // verify required parameter 'projectId' is not null or undefined + assertParamExists("isInFrontier", "projectId", projectId); + // verify required parameter 'wordId' is not null or undefined + assertParamExists("isInFrontier", "wordId", wordId); + const localVarPath = + `/v1/projects/{projectId}/words/isinfrontier/{wordId}` + .replace(`{${"projectId"}}`, encodeURIComponent(String(projectId))) + .replace(`{${"wordId"}}`, encodeURIComponent(String(wordId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { + method: "GET", + ...baseOptions, + ...options, + }; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + setSearchParams(localVarUrlObj, localVarQueryParameter, options.query); + let headersFromBaseOptions = + baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = { + ...localVarHeaderParameter, + ...headersFromBaseOptions, + ...options.headers, + }; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @param {string} projectId @@ -692,6 +741,32 @@ export const WordApiFp = function (configuration?: Configuration) { configuration ); }, + /** + * + * @param {string} projectId + * @param {string} wordId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async isInFrontier( + projectId: string, + wordId: string, + options?: any + ): Promise< + (axios?: AxiosInstance, basePath?: string) => AxiosPromise + > { + const localVarAxiosArgs = await localVarAxiosParamCreator.isInFrontier( + projectId, + wordId, + options + ); + return createRequestFunction( + localVarAxiosArgs, + globalAxios, + BASE_PATH, + configuration + ); + }, /** * * @param {string} projectId @@ -870,6 +945,22 @@ export const WordApiFactory = function ( .isFrontierNonempty(projectId, options) .then((request) => request(axios, basePath)); }, + /** + * + * @param {string} projectId + * @param {string} wordId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + isInFrontier( + projectId: string, + wordId: string, + options?: any + ): AxiosPromise { + return localVarFp + .isInFrontier(projectId, wordId, options) + .then((request) => request(axios, basePath)); + }, /** * * @param {string} projectId @@ -1035,6 +1126,27 @@ export interface WordApiIsFrontierNonemptyRequest { readonly projectId: string; } +/** + * Request parameters for isInFrontier operation in WordApi. + * @export + * @interface WordApiIsInFrontierRequest + */ +export interface WordApiIsInFrontierRequest { + /** + * + * @type {string} + * @memberof WordApiIsInFrontier + */ + readonly projectId: string; + + /** + * + * @type {string} + * @memberof WordApiIsInFrontier + */ + readonly wordId: string; +} + /** * Request parameters for updateDuplicate operation in WordApi. * @export @@ -1215,6 +1327,26 @@ export class WordApi extends BaseAPI { .then((request) => request(this.axios, this.basePath)); } + /** + * + * @param {WordApiIsInFrontierRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof WordApi + */ + public isInFrontier( + requestParameters: WordApiIsInFrontierRequest, + options?: any + ) { + return WordApiFp(this.configuration) + .isInFrontier( + requestParameters.projectId, + requestParameters.wordId, + options + ) + .then((request) => request(this.axios, this.basePath)); + } + /** * * @param {WordApiUpdateDuplicateRequest} requestParameters Request parameters. diff --git a/src/backend/index.ts b/src/backend/index.ts index 0ea96fb74a..1b102f0aab 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -707,6 +707,15 @@ export async function isFrontierNonempty(projectId?: string): Promise { return (await wordApi.isFrontierNonempty(params, defaultOptions())).data; } +export async function isInFrontier( + wordId: string, + projectId?: string +): Promise { + projectId = projectId || LocalStorage.getProjectId(); + const params = { projectId, wordId }; + return (await wordApi.isInFrontier(params, defaultOptions())).data; +} + export async function updateDuplicate( dupId: string, word: Word diff --git a/src/components/WordCard/DomainChip.tsx b/src/components/WordCard/DomainChip.tsx index 11d3fe6760..2b954bea07 100644 --- a/src/components/WordCard/DomainChip.tsx +++ b/src/components/WordCard/DomainChip.tsx @@ -28,10 +28,10 @@ export default function DomainChip(props: DomainChipProps): ReactElement { const hoverText = []; if (provenance && created) { const val = getDateTimeString(created, friendlySep); - hoverText.push(t("wordHistory.domainAdded", { val })); + hoverText.push(t("wordCard.domainAdded", { val })); } if (provenance && username) { - hoverText.push(t("wordHistory.user", { val: username })); + hoverText.push(t("wordCard.user", { val: username })); } return ; } diff --git a/src/components/WordCard/SummarySenseCard.tsx b/src/components/WordCard/SummarySenseCard.tsx index 5826ac6f45..202d402d5b 100644 --- a/src/components/WordCard/SummarySenseCard.tsx +++ b/src/components/WordCard/SummarySenseCard.tsx @@ -40,7 +40,7 @@ export default function SummarySenseCard( {/* Sense count */} - {t("wordHistory.senseCount", { val: props.senses.length })} + {t("wordCard.senseCount", { val: props.senses.length })} {/* Semantic domain numbers */} diff --git a/src/components/WordCard/index.tsx b/src/components/WordCard/index.tsx index 74d8a2ee31..3131234f9c 100644 --- a/src/components/WordCard/index.tsx +++ b/src/components/WordCard/index.tsx @@ -44,7 +44,7 @@ export default function WordCard(props: WordCardProps): ReactElement { }, [editedBy, provenance]); return ( - + {/* Vernacular */} @@ -117,15 +117,15 @@ export default function WordCard(props: WordCardProps): ReactElement { {/* Timestamps */} {provenance && ( - {t("wordHistory.wordId", { val: id })} + {t("wordCard.wordId", { val: id })}
- {t("wordHistory.wordModified", { + {t("wordCard.wordModified", { val: getDateTimeString(word.modified, friendlySep), })} {!!username && ( <>
- {t("wordHistory.user", { val: username })} + {t("wordCard.user", { val: username })} )}
diff --git a/src/goals/ReviewEntries/ReviewEntriesCompleted.tsx b/src/goals/ReviewEntries/ReviewEntriesCompleted.tsx index 23d3a0a512..067acb0cfb 100644 --- a/src/goals/ReviewEntries/ReviewEntriesCompleted.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesCompleted.tsx @@ -1,11 +1,11 @@ import { ArrowRightAlt } from "@mui/icons-material"; -import { Grid, Typography } from "@mui/material"; -import { Fragment, ReactElement, useEffect, useState } from "react"; +import { Grid, List, ListItem, Typography } from "@mui/material"; +import { ReactElement, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; import { Word } from "api/models"; -import { getFrontierWords, getWord, updateWord } from "backend"; +import { getWord, isInFrontier, updateWord } from "backend"; import { UndoButton } from "components/Buttons"; import WordCard from "components/WordCard"; import { @@ -27,7 +27,9 @@ export default function ReviewEntriesCompleted(): ReactElement { {t("reviewEntries.title")} {EditsCount(changes)} - {changes.entryEdits?.map((e) => )} + + {changes.entryEdits?.map((e) => )} + ); } @@ -61,36 +63,33 @@ function EditedEntry(props: { edit: EntryEdit }): ReactElement { getWord(newId).then(setNewWord); }, [newId]); - const handleIsUndoAllowed = (): Promise => - getFrontierWords().then( - (words) => words.findIndex((w) => w.id === newId) !== -1 - ); - return ( - - {oldWord ? : } - - + + {!!oldWord && } + + + + {!!newWord && } + isInFrontier(newId)} + undo={() => undoEdit(props.edit)} /> - {newWord ? : } - undoEdit(props.edit)} - /> - + ); } From b877c617f949479a1f329005693c1f0eef378534 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 17 Nov 2023 16:52:10 -0500 Subject: [PATCH 09/11] Revert tiny change --- src/goals/ReviewEntries/tests/index.test.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/goals/ReviewEntries/tests/index.test.tsx b/src/goals/ReviewEntries/tests/index.test.tsx index ab346fade8..483d22f50e 100644 --- a/src/goals/ReviewEntries/tests/index.test.tsx +++ b/src/goals/ReviewEntries/tests/index.test.tsx @@ -36,9 +36,7 @@ jest.mock("notistack", () => ({ ...jest.requireActual("notistack"), enqueueSnackbar: jest.fn(), })); -jest.mock("uuid", () => ({ - v4: () => mockUuid(), -})); +jest.mock("uuid", () => ({ v4: () => mockUuid() })); jest.mock("backend", () => ({ getFrontierWords: (...args: any[]) => mockGetFrontierWords(...args), })); From 5262014511e424048dfc67e3ac6795a6563f3ae7 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 17 Nov 2023 16:54:49 -0500 Subject: [PATCH 10/11] Mock out Recorder to pass tests --- src/components/GoalTimeline/tests/GoalRedux.test.tsx | 2 +- src/components/GoalTimeline/tests/index.test.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/GoalTimeline/tests/GoalRedux.test.tsx b/src/components/GoalTimeline/tests/GoalRedux.test.tsx index 4e55ad68cd..906e4b1438 100644 --- a/src/components/GoalTimeline/tests/GoalRedux.test.tsx +++ b/src/components/GoalTimeline/tests/GoalRedux.test.tsx @@ -46,10 +46,10 @@ jest.mock("backend", () => ({ getUserEditById: (...args: any[]) => mockGetUserEditById(...args), updateUser: (user: User) => mockUpdateUser(user), })); - jest.mock("browserRouter", () => ({ navigate: (path: Path) => mockNavigate(path), })); +jest.mock("components/Pronunciations/Recorder"); const mockAddGoalToUserEdit = jest.fn(); const mockAddStepToGoal = jest.fn(); diff --git a/src/components/GoalTimeline/tests/index.test.tsx b/src/components/GoalTimeline/tests/index.test.tsx index e77d66a419..8ba3017ee0 100644 --- a/src/components/GoalTimeline/tests/index.test.tsx +++ b/src/components/GoalTimeline/tests/index.test.tsx @@ -19,6 +19,7 @@ jest.mock("components/GoalTimeline/Redux/GoalActions", () => ({ asyncAddGoal: (goal: Goal) => mockChooseGoal(goal), asyncGetUserEdits: () => jest.fn(), })); +jest.mock("components/Pronunciations/Recorder"); jest.mock("types/hooks", () => { return { ...jest.requireActual("types/hooks"), From 33ad76c05aaec7e0fcd1e20877e6b9c645fbf5b5 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Thu, 30 Nov 2023 12:59:23 -0500 Subject: [PATCH 11/11] Add comments --- src/goals/ReviewEntries/ReviewEntriesTypes.ts | 10 ++++++++-- src/utilities/utilities.ts | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/goals/ReviewEntries/ReviewEntriesTypes.ts b/src/goals/ReviewEntries/ReviewEntriesTypes.ts index 780fa7b8e0..4b660d7575 100644 --- a/src/goals/ReviewEntries/ReviewEntriesTypes.ts +++ b/src/goals/ReviewEntries/ReviewEntriesTypes.ts @@ -59,6 +59,8 @@ export class ReviewEntriesWord { flag: Flag; protected: boolean; + /** Construct a ReviewEntriesWord from a Word. + * Important: Some things (e.g., note language) aren't preserved! */ constructor(word?: Word, analysisLang?: string) { if (!word) { word = newWord(); @@ -84,6 +86,9 @@ export class ReviewEntriesSense { deleted: boolean; protected: boolean; + /** Construct a ReviewEntriesSense from a Sense. + * Important: Some things aren't preserved! + * (E.g., distinct glosses with the same language are combined.) */ constructor(sense?: Sense, analysisLang?: string) { if (!sense) { sense = newSense(); @@ -115,7 +120,8 @@ export class ReviewEntriesSense { } /** Reverse map of the ReviewEntriesSense constructor. - * Important: Not everything is preserved! */ + * Important: Some things aren't preserved! + * (E.g., distinct glosses with the same language may have been combined.) */ function senseFromReviewEntriesSense(revSense: ReviewEntriesSense): Sense { return { ...newSense(), @@ -133,7 +139,7 @@ function senseFromReviewEntriesSense(revSense: ReviewEntriesSense): Sense { } /** Reverse map of the ReviewEntriesWord constructor. - * Important: Not everything is preserved! */ + * Important: Some things (e.g., note language) aren't preserved! */ export function wordFromReviewEntriesWord(revWord: ReviewEntriesWord): Word { return { ...newWord(revWord.vernacular), diff --git a/src/utilities/utilities.ts b/src/utilities/utilities.ts index 890bcdaedc..0f180ce4a4 100644 --- a/src/utilities/utilities.ts +++ b/src/utilities/utilities.ts @@ -70,6 +70,7 @@ export function getDateTimeString( date.getSeconds(), ]; const strs = vals.map((value) => (value < 10 ? `0${value}` : `${value}`)); + // TODO: Consider localization of the date-time formatting. const dateString = strs.slice(0, 3).join(sep?.date ?? pathSep.date); const timeString = strs.slice(3, 6).join(sep?.time ?? pathSep.time); return `${dateString}${sep?.dateTime ?? pathSep.dateTime}${timeString}`;