From d7d73e5f055133614b89c97ae101a17519d614d3 Mon Sep 17 00:00:00 2001 From: "D. Ror" Date: Fri, 20 Oct 2023 09:11:00 -0400 Subject: [PATCH] [ExistingDataTable] Show all glosses, with primary analysis lang first (#2688) --- .../ImmutableExistingData.tsx | 46 ++++++------- .../DataEntry/ExistingDataTable/index.tsx | 9 +-- .../tests/ImmutableExistingData.test.tsx | 14 ++-- src/components/DataEntry/index.tsx | 16 +++-- src/components/DataEntry/tests/index.test.tsx | 3 +- .../DataEntry/tests/utilities.test.ts | 68 ++++++++++--------- src/components/DataEntry/utilities.ts | 62 ++++++++++------- src/types/word.ts | 6 +- 8 files changed, 123 insertions(+), 101 deletions(-) diff --git a/src/components/DataEntry/ExistingDataTable/ImmutableExistingData.tsx b/src/components/DataEntry/ExistingDataTable/ImmutableExistingData.tsx index cfe67a86e5..259c8ff10e 100644 --- a/src/components/DataEntry/ExistingDataTable/ImmutableExistingData.tsx +++ b/src/components/DataEntry/ExistingDataTable/ImmutableExistingData.tsx @@ -1,17 +1,21 @@ import { Grid } from "@mui/material"; -import { ReactElement } from "react"; +import { CSSProperties, ReactElement } from "react"; import { Gloss } from "api/models"; import { TypographyWithFont } from "utilities/fontComponents"; +/** Style with a top dotted line if the index isn't 0. */ +function TopStyle(index: number, style?: "solid" | "dotted"): CSSProperties { + return index ? { borderTopStyle: style ?? "solid", borderTopWidth: 1 } : {}; +} + interface ImmutableExistingDataProps { - gloss: Gloss; + glosses: Gloss[]; + index: number; vernacular: string; } -/** - * Displays a word users cannot edit any more - */ +/** Displays a word-sense that the user cannot edit. */ export default function ImmutableExistingData( props: ImmutableExistingDataProps ): ReactElement { @@ -19,13 +23,8 @@ export default function ImmutableExistingData( {props.vernacular} @@ -33,21 +32,20 @@ export default function ImmutableExistingData( - - {props.gloss.def} - + {props.glosses.map((g, i) => ( + + {g.def} + + ))} ); diff --git a/src/components/DataEntry/ExistingDataTable/index.tsx b/src/components/DataEntry/ExistingDataTable/index.tsx index 6404450829..0888c13f07 100644 --- a/src/components/DataEntry/ExistingDataTable/index.tsx +++ b/src/components/DataEntry/ExistingDataTable/index.tsx @@ -35,11 +35,12 @@ export default function ExistingDataTable( const list = (): ReactElement => ( - {props.domainWords.map((domainWord) => ( + {props.domainWords.map((w, i) => ( ))} diff --git a/src/components/DataEntry/ExistingDataTable/tests/ImmutableExistingData.test.tsx b/src/components/DataEntry/ExistingDataTable/tests/ImmutableExistingData.test.tsx index 8e0703af83..a4e4b93f13 100644 --- a/src/components/DataEntry/ExistingDataTable/tests/ImmutableExistingData.test.tsx +++ b/src/components/DataEntry/ExistingDataTable/tests/ImmutableExistingData.test.tsx @@ -1,13 +1,17 @@ -import renderer from "react-test-renderer"; +import { act, create } from "react-test-renderer"; import ImmutableExistingData from "components/DataEntry/ExistingDataTable/ImmutableExistingData"; import { newGloss } from "types/word"; describe("ImmutableExistingData", () => { - it("render without crashing", () => { - renderer.act(() => { - renderer.create( - + it("renders", async () => { + await act(async () => { + create( + ); }); }); diff --git a/src/components/DataEntry/index.tsx b/src/components/DataEntry/index.tsx index 8bc04848ec..6b37c8e8d0 100644 --- a/src/components/DataEntry/index.tsx +++ b/src/components/DataEntry/index.tsx @@ -13,10 +13,7 @@ import AppBar from "components/AppBar/AppBarComponent"; import DataEntryHeader from "components/DataEntry/DataEntryHeader"; import DataEntryTable from "components/DataEntry/DataEntryTable"; import ExistingDataTable from "components/DataEntry/ExistingDataTable"; -import { - filterWordsByDomain, - sortDomainWordsByVern, -} from "components/DataEntry/utilities"; +import { filterWordsByDomain } from "components/DataEntry/utilities"; import TreeView from "components/TreeView"; import { closeTreeAction, @@ -46,6 +43,10 @@ const paperStyle = { export default function DataEntry(): ReactElement { const dispatch = useAppDispatch(); + const analysisLang = useAppSelector( + (state: StoreState) => + state.currentProjectState.project.analysisWritingSystems[0].bcp47 + ); const { currentDomain, open } = useAppSelector( (state: StoreState) => state.treeViewState ); @@ -95,10 +96,11 @@ export default function DataEntry(): ReactElement { }, [domain, questionsVisible, updateHeight, windowWidth]); const returnControlToCaller = useCallback(async () => { - const words = filterWordsByDomain(await getFrontierWords(), id); - setDomainWords(sortDomainWordsByVern(words)); + setDomainWords( + filterWordsByDomain(await getFrontierWords(), id, analysisLang) + ); dispatch(closeTreeAction()); - }, [dispatch, id]); + }, [analysisLang, dispatch, id]); return ( diff --git a/src/components/DataEntry/tests/index.test.tsx b/src/components/DataEntry/tests/index.test.tsx index a1f7f426ff..85a4ec1aff 100644 --- a/src/components/DataEntry/tests/index.test.tsx +++ b/src/components/DataEntry/tests/index.test.tsx @@ -8,6 +8,7 @@ import DataEntry, { smallScreenThreshold, treeViewDialogId, } from "components/DataEntry"; +import { defaultState as currentProjectState } from "components/Project/ProjectReduxTypes"; import { openTreeAction } from "components/TreeView/Redux/TreeViewActions"; import { TreeViewAction, @@ -97,7 +98,7 @@ async function renderDataEntry( spyOnUseWindowSize(windowWidth); await renderer.act(async () => { testHandle = renderer.create( - + ); diff --git a/src/components/DataEntry/tests/utilities.test.ts b/src/components/DataEntry/tests/utilities.test.ts index 5a72dcfe21..8b5c63f11f 100644 --- a/src/components/DataEntry/tests/utilities.test.ts +++ b/src/components/DataEntry/tests/utilities.test.ts @@ -1,46 +1,64 @@ -import { Status, Word } from "api/models"; +import { Sense, Status, Word } from "api/models"; import { filterWordsByDomain, filterWordsWithSenses, - sortDomainWordsByVern, } from "components/DataEntry/utilities"; import { newSemanticDomain } from "types/semanticDomain"; import { DomainWord, newSense, simpleWord } from "types/word"; const mockWord = simpleWord("vern", "gloss"); +const domainSense = (accessibility: Status, domainId?: string): Sense => { + const semanticDomains = [newSemanticDomain(domainId)]; + return { ...newSense(), accessibility, semanticDomains }; +}; describe("DataEntryComponent", () => { describe("filterWordsWithSenses", () => { it("returns empty Word Array when given empty Word Array.", () => { - const words: Word[] = []; - const expectedWords: Word[] = []; - expect(filterWordsWithSenses(words)).toEqual(expectedWords); + expect(filterWordsWithSenses([])).toEqual([]); }); it("removes words with no Active/Protected sense.", () => { const words: Word[] = [ - { - ...mockWord, - senses: [{ ...newSense(), accessibility: Status.Deleted }], - }, - { - ...mockWord, - senses: [{ ...newSense(), accessibility: Status.Duplicate }], - }, + { ...mockWord, senses: [domainSense(Status.Deleted)] }, + { ...mockWord, senses: [domainSense(Status.Duplicate)] }, ]; expect(filterWordsWithSenses(words)).toHaveLength(0); }); it("keeps words with an Active/Protected sense.", () => { const words: Word[] = [ - mockWord, - { - ...mockWord, - senses: [{ ...newSense(), accessibility: Status.Protected }], - }, + { ...mockWord, senses: [domainSense(Status.Active)] }, + { ...mockWord, senses: [domainSense(Status.Protected)] }, ]; expect(filterWordsWithSenses(words)).toHaveLength(2); }); + + it("removes words with inactive sense even in specified domain.", () => { + const domId = "domain-id"; + const words: Word[] = [ + { ...mockWord, senses: [domainSense(Status.Deleted, domId)] }, + { ...mockWord, senses: [domainSense(Status.Duplicate, domId)] }, + ]; + expect(filterWordsWithSenses(words, domId)).toHaveLength(0); + }); + + it("removes words with sense in wrong domain.", () => { + const words: Word[] = [ + { ...mockWord, senses: [domainSense(Status.Active, "one wrong")] }, + { ...mockWord, senses: [domainSense(Status.Protected, "other wrong")] }, + ]; + expect(filterWordsWithSenses(words, "right one")).toHaveLength(0); + }); + + it("keeps words with an Active/Protected sense in specified domain.", () => { + const domId = "domain-id"; + const words: Word[] = [ + { ...mockWord, senses: [domainSense(Status.Active, domId)] }, + { ...mockWord, senses: [domainSense(Status.Protected, domId)] }, + ]; + expect(filterWordsWithSenses(words, domId)).toHaveLength(2); + }); }); describe("filterWordsByDomain", () => { @@ -83,18 +101,4 @@ describe("DataEntryComponent", () => { ).toStrictEqual([new DomainWord(expectedWord, senseIndex)]); }); }); - - describe("sortDomainWordByVern", () => { - it("sorts words alphabetically.", () => { - const words = [mockWord, mockWord, mockWord].map( - (w) => new DomainWord(w) - ); - words[0].vernacular = "Always"; - words[1].vernacular = "Be"; - words[2].vernacular = "?character"; - - const expectedList: DomainWord[] = [words[2], words[0], words[1]]; - expect(sortDomainWordsByVern([...words])).toStrictEqual(expectedList); - }); - }); }); diff --git a/src/components/DataEntry/utilities.ts b/src/components/DataEntry/utilities.ts index b9d438a0e2..1d641ae770 100644 --- a/src/components/DataEntry/utilities.ts +++ b/src/components/DataEntry/utilities.ts @@ -1,39 +1,51 @@ -import { Status, Word } from "api/models"; +import { Sense, Status, Word } from "api/models"; import { DomainWord } from "types/word"; -/** Filter out words that do not have at least 1 active sense */ -export function filterWordsWithSenses(words: Word[]): Word[] { +/** Checks whether a sense is active + * (and in the specified domain if domainId is provided). */ +function isActiveInDomain(sense: Sense, domainId?: string): boolean { + return ( + (!domainId || !!sense.semanticDomains.find((d) => d.id === domainId)) && + // The undefined is for Statuses created before .accessibility was required. + [Status.Active, Status.Protected, undefined].includes(sense.accessibility) + ); +} + +/** Filter out words that do not have at least 1 active sense + * (and in the specified domain if domainId is provided). */ +export function filterWordsWithSenses( + words: Word[], + domainId?: string +): Word[] { return words.filter((w) => - w.senses.find((s) => - [Status.Active, Status.Protected].includes(s.accessibility) - ) + w.senses.find((s) => isActiveInDomain(s, domainId)) ); } +/** Filter out sense's glosses with empty def + * (and if lang is specified, put glosses in that lang first). */ +function filterGlosses(sense: Sense, lang?: string): Sense { + const glosses = sense.glosses.filter((g) => g.def.trim()); + if (lang) { + glosses.sort((a, b) => +(b.language === lang) - +(a.language === lang)); + } + return { ...sense, glosses }; +} + export function filterWordsByDomain( words: Word[], - domainId: string + domainId: string, + lang?: string ): DomainWord[] { const domainWords: DomainWord[] = []; - for (const currentWord of words) { - const senses = currentWord.senses.filter((s) => - // The undefined is for Statuses created before .accessibility was required in the frontend. - [Status.Active, Status.Protected, undefined].includes(s.accessibility) + const wordsInDomain = filterWordsWithSenses(words, domainId); + wordsInDomain.sort((a, b) => a.vernacular.localeCompare(b.vernacular)); + for (const w of wordsInDomain) { + domainWords.push( + ...w.senses + .filter((s) => isActiveInDomain(s, domainId)) + .map((s) => new DomainWord({ ...w, senses: [filterGlosses(s, lang)] })) ); - for (const sense of senses) { - if (sense.semanticDomains.map((dom) => dom.id).includes(domainId)) { - // Only the first gloss is shown, and no definitions. - domainWords.push(new DomainWord({ ...currentWord, senses: [sense] })); - } - } } return domainWords; } - -export function sortDomainWordsByVern(words: DomainWord[]): DomainWord[] { - return words.sort( - (a, b) => - a.vernacular.localeCompare(b.vernacular) || - a.gloss.def.localeCompare(b.gloss.def) - ); -} diff --git a/src/types/word.ts b/src/types/word.ts index 68e0230423..c82dd30191 100644 --- a/src/types/word.ts +++ b/src/types/word.ts @@ -78,14 +78,14 @@ export class DomainWord { wordGuid: string; vernacular: string; senseGuid: string; - gloss: Gloss; + glosses: Gloss[]; - constructor(word: Word, senseIndex = 0, glossIndex = 0) { + constructor(word: Word, senseIndex = 0) { const sense = word.senses[senseIndex] ?? newSense(); - this.gloss = sense.glosses[glossIndex] ?? newGloss(); this.wordGuid = word.guid; this.vernacular = word.vernacular; this.senseGuid = sense.guid; + this.glosses = sense.glosses; } }