diff --git a/Backend.Tests/Controllers/WordControllerTests.cs b/Backend.Tests/Controllers/WordControllerTests.cs index bc0226c802..bbb9c661ec 100644 --- a/Backend.Tests/Controllers/WordControllerTests.cs +++ b/Backend.Tests/Controllers/WordControllerTests.cs @@ -397,5 +397,40 @@ public async Task TestUpdateWordMissingIds() var wordResult = await _wordController.UpdateWord(_projId, MissingId, modWord); Assert.That(wordResult, Is.InstanceOf()); } + + [Test] + public async Task TestGetWordHistoryNoPermission() + { + _wordController.ControllerContext.HttpContext = PermissionServiceMock.UnauthorizedHttpContext(); + + var origWord = await _wordRepo.Create(Util.RandomWord(_projId)); + var result = await _wordController.GetWordHistory(_projId, origWord.Id); + Assert.That(result, Is.InstanceOf()); + } + + [Test] + public async Task TestGetWordHistoryMissingIds() + { + var origWord = await _wordRepo.Create(Util.RandomWord(_projId)); + var projectResult = await _wordController.GetWordHistory(MissingId, origWord.Id); + Assert.That(projectResult, Is.InstanceOf()); + + var wordResult = await _wordController.GetWordHistory(_projId, MissingId); + Assert.That(wordResult, Is.InstanceOf()); + } + + [Test] + public async Task TestGetWordHistoryParentCount() + { + var father = await _wordRepo.Create(Util.RandomWord(_projId)); + var mother = await _wordRepo.Create(Util.RandomWord(_projId)); + var word = Util.RandomWord(_projId); + word.History = new List { father.Id, mother.Id }; + word = await _wordRepo.Create(word); + var result = await _wordController.GetWordHistory(_projId, word.Id); + Assert.That(result, Is.InstanceOf()); + var pedigree = (Pedigree)((OkObjectResult)result).Value!; + Assert.That(pedigree.Parents, Has.Count.EqualTo(2)); + } } } diff --git a/Backend/Interfaces/IWordService.cs b/Backend/Interfaces/IWordService.cs index ec549ab02c..c0e704fd50 100644 --- a/Backend/Interfaces/IWordService.cs +++ b/Backend/Interfaces/IWordService.cs @@ -10,6 +10,6 @@ public interface IWordService Task FindContainingWord(Word word); Task Delete(string projectId, string wordId, string fileName); Task DeleteFrontierWord(string projectId, string wordId); - Task GeneratePedigree(string projId, Word word); + Task GeneratePedigree(string projectId, Word word); } } diff --git a/Backend/Services/WordService.cs b/Backend/Services/WordService.cs index e0a48f0f89..b47f415a1f 100644 --- a/Backend/Services/WordService.cs +++ b/Backend/Services/WordService.cs @@ -141,7 +141,7 @@ public async Task Update(string projectId, string wordId, Word word) } /// Builds a from a 's history. - public async Task GeneratePedigree(string projId, Word word) + public async Task GeneratePedigree(string projectId, Word word) { var tree = new Pedigree(word); // Iterate backwards through the history and construct the pedigree depth-first. @@ -150,10 +150,10 @@ public async Task GeneratePedigree(string projId, Word word) var id = word.History[i]; if (!tree.HasAncestor(id)) { - var parent = await _wordRepo.GetWord(projId, id); + var parent = await _wordRepo.GetWord(projectId, id); if (parent is not null) { - tree.Parents.Add(await GeneratePedigree(projId, parent)); + tree.Parents.Add(await GeneratePedigree(projectId, parent)); } } } diff --git a/src/components/DataEntry/DataEntryTable/EntryCellComponents/EntryNote.tsx b/src/components/DataEntry/DataEntryTable/EntryCellComponents/EntryNote.tsx index 6d96c509e2..b23fdadb9e 100644 --- a/src/components/DataEntry/DataEntryTable/EntryCellComponents/EntryNote.tsx +++ b/src/components/DataEntry/DataEntryTable/EntryCellComponents/EntryNote.tsx @@ -6,8 +6,8 @@ import { useTranslation } from "react-i18next"; import { EditTextDialog } from "components/Dialogs"; interface EntryNoteProps { - buttonId?: string; noteText: string; + buttonId?: string; updateNote?: (newText: string) => void | Promise; } diff --git a/src/components/WordCard/SenseCard.tsx b/src/components/WordCard/SenseCard.tsx index 0f5bddfdd2..088c8a517b 100644 --- a/src/components/WordCard/SenseCard.tsx +++ b/src/components/WordCard/SenseCard.tsx @@ -14,30 +14,32 @@ interface SenseCardProps { } export default function SenseCard(props: SenseCardProps): ReactElement { - const gramInfo = props.sense.grammaticalInfo; + const { grammaticalInfo, semanticDomains } = props.sense; return ( - - - {/* Icon for part of speech (if any). */} + + + {/* Part of speech (if any) */}
- {gramInfo.catGroup !== GramCatGroup.Unspecified && ( + {grammaticalInfo.catGroup !== GramCatGroup.Unspecified && ( )}
- {/* List glosses and (if any) definitions. */} + + {/* Glosses and (if any) definitions */} - {/* List semantic domains. */} - - {props.sense.semanticDomains.map((d) => ( + + {/* Semantic domains */} + + {semanticDomains.map((d) => ( diff --git a/src/components/WordCard/SenseCardText.tsx b/src/components/WordCard/SenseCardText.tsx index 2bd143e4d7..5b23446f44 100644 --- a/src/components/WordCard/SenseCardText.tsx +++ b/src/components/WordCard/SenseCardText.tsx @@ -5,7 +5,7 @@ import { TableRow, Typography, } from "@mui/material"; -import { ReactElement } from "react"; +import { CSSProperties, ReactElement } from "react"; import { Sense } from "api/models"; import theme from "types/theme"; @@ -14,7 +14,7 @@ import { TypographyWithFont } from "utilities/fontComponents"; interface SenseInLanguage { language: string; // bcp-47 code glossText: string; - definitionText?: string; + definitionText: string; } function getSenseInLanguage( @@ -22,19 +22,15 @@ function getSenseInLanguage( 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, - }; + 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( @@ -50,12 +46,12 @@ function getSenseInLanguages( } interface SenseCardTextProps { - languages?: string[]; - minimal?: boolean; sense: Sense; + hideDefs?: boolean; + languages?: string[]; } -// Show glosses and (if not minimal) definitions +// Show glosses and (if not hideDefs) definitions. export default function SenseCardText(props: SenseCardTextProps): ReactElement { const senseTextInLangs = getSenseInLanguages(props.sense, props.languages); @@ -64,7 +60,7 @@ export default function SenseCardText(props: SenseCardTextProps): ReactElement { {senseTextInLangs.map((senseInLang, index) => ( @@ -74,6 +70,12 @@ export default function SenseCardText(props: SenseCardTextProps): ReactElement { ); } +const defStyle: CSSProperties = { + borderLeft: "1px solid black", + marginBottom: theme.spacing(1), + paddingLeft: theme.spacing(1), +}; + interface SenseTextRowsProps { senseInLang: SenseInLanguage; hideDefs?: boolean; @@ -83,6 +85,7 @@ function SenseTextRows(props: SenseTextRowsProps): ReactElement { const lang = props.senseInLang.language; return ( <> + {/* Gloss */} @@ -100,17 +103,13 @@ function SenseTextRows(props: SenseTextRowsProps): ReactElement { + + {/* Definition */} {!!props.senseInLang.definitionText && !props.hideDefs && ( -
+
{props.senseInLang.definitionText} diff --git a/src/components/WordCard/SummarySenseCard.tsx b/src/components/WordCard/SummarySenseCard.tsx index e0ac2979ac..5826ac6f45 100644 --- a/src/components/WordCard/SummarySenseCard.tsx +++ b/src/components/WordCard/SummarySenseCard.tsx @@ -1,4 +1,4 @@ -import { Card, CardContent, Chip, Typography } from "@mui/material"; +import { Card, CardContent, Chip, Grid, Typography } from "@mui/material"; import { ReactElement } from "react"; import { useTranslation } from "react-i18next"; @@ -26,9 +26,9 @@ export default function SummarySenseCard( const domIds = [...new Set(semDoms.map((d) => d.id))].sort(); return ( - - - {/* Icon for part of speech (if any). */} + + + {/* Parts of speech */} {groupedGramInfo.map((info) => ( ))} - {/* Number of senses. */} + + {/* Sense count */} {t("wordHistory.senseCount", { val: props.senses.length })} - {/* Semantic domain numbers. */} - {domIds.map((id) => ( - - ))} + + {/* Semantic domain numbers */} + + {domIds.map((id) => ( + + + + ))} + ); diff --git a/src/components/WordCard/index.tsx b/src/components/WordCard/index.tsx index 6639f0da9f..416d0aa7ee 100644 --- a/src/components/WordCard/index.tsx +++ b/src/components/WordCard/index.tsx @@ -6,7 +6,7 @@ import { IconButton, Typography, } from "@mui/material"; -import { ReactElement, useState } from "react"; +import { Fragment, ReactElement, useState } from "react"; import { useTranslation } from "react-i18next"; import { Word } from "api/models"; @@ -35,31 +35,31 @@ export default function WordCard(props: WordCardProps): ReactElement { return ( - + {/* Vernacular */} {word.vernacular} - {/* Icons for audio, note, flag (if any). */} + + {/* Icons for audio, note, flag (if any); button for expand/collapse */}
{!full && } {!!note.text && } {flag.active && } - {full ? ( - } - onClick={() => setFull(false)} - /> - ) : ( - } - onClick={() => setFull(true)} - /> - )} + + ) : ( + + ) + } + onClick={() => setFull(!full)} + />
- {/* Audio playback. */} + + {/* Audio playback */} {audio.length > 0 && full && ( {}} @@ -68,7 +68,8 @@ export default function WordCard(props: WordCardProps): ReactElement { wordId={id} /> )} - {/* Senses. */} + + {/* Senses */} {full ? ( senses.map((s) => ( )} + {/* Timestamps */} {provenance && ( @@ -104,6 +106,6 @@ export function AudioSummary(props: { count: number }): ReactElement { ) : ( -
+ ); } diff --git a/src/components/WordCard/tests/index.test.tsx b/src/components/WordCard/tests/index.test.tsx index b33bc5913f..6e44038e6a 100644 --- a/src/components/WordCard/tests/index.test.tsx +++ b/src/components/WordCard/tests/index.test.tsx @@ -34,7 +34,7 @@ beforeEach(async () => { }); describe("HistoryCell", () => { - it("has full and summary views", async () => { + 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 diff --git a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/HistoryCell.tsx b/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/HistoryCell.tsx index 103e053231..b13c70f0ad 100644 --- a/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/HistoryCell.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesComponent/CellComponents/HistoryCell.tsx @@ -16,9 +16,11 @@ interface HistoryCellProps { export default function HistoryCell(props: HistoryCellProps): ReactElement { const [history, setHistory] = useState(); + const getHistory = async (): Promise => { await getWordHistory(props.wordId).then(setHistory); }; + return ( <> + {/* Word */} + {parents.length > 0 && ( <> + {/* Arrow */} + + {/* Parent word(s) */} {showParents && (