Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ExistingDataTable] Show all glosses, with primary analysis lang first #2688

Merged
merged 10 commits into from
Oct 20, 2023
Original file line number Diff line number Diff line change
@@ -1,53 +1,51 @@
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 {
return (
<Grid container wrap="nowrap" justifyContent="space-around">
<Grid
item
style={{ ...TopStyle(props.index), position: "relative" }}
xs={5}
key={"vernacular_" + props.vernacular}
style={{
borderBottomStyle: "dotted",
borderBottomWidth: 1,
position: "relative",
}}
>
<TypographyWithFont variant="body1" vernacular>
{props.vernacular}
</TypographyWithFont>
</Grid>
<Grid
item
style={{ ...TopStyle(props.index), position: "relative" }}
xs={5}
key={"gloss_" + props.gloss.def}
style={{
borderBottomStyle: "dotted",
borderBottomWidth: 1,
position: "relative",
}}
>
<TypographyWithFont
analysis
lang={props.gloss.language}
variant="body1"
>
{props.gloss.def}
</TypographyWithFont>
{props.glosses.map((g, i) => (
<TypographyWithFont
analysis
key={i}
lang={g.language}
style={TopStyle(i, "dotted")}
variant="body1"
>
{g.def}
</TypographyWithFont>
))}
</Grid>
</Grid>
);
Expand Down
9 changes: 5 additions & 4 deletions src/components/DataEntry/ExistingDataTable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,12 @@ export default function ExistingDataTable(

const list = (): ReactElement => (
<List style={{ minWidth: "300px" }}>
{props.domainWords.map((domainWord) => (
{props.domainWords.map((w, i) => (
<ImmutableExistingData
key={`${domainWord.wordGuid}-${domainWord.senseGuid}`}
gloss={domainWord.gloss}
vernacular={domainWord.vernacular}
glosses={w.glosses}
index={i}
key={`${w.wordGuid}-${w.senseGuid}`}
vernacular={w.vernacular}
/>
))}
</List>
Expand Down
Original file line number Diff line number Diff line change
@@ -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(
<ImmutableExistingData gloss={newGloss()} vernacular={""} />
it("renders", async () => {
await act(async () => {
create(
<ImmutableExistingData
glosses={[newGloss()]}
index={0}
vernacular={""}
/>
);
});
});
Expand Down
16 changes: 9 additions & 7 deletions src/components/DataEntry/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
);
Expand Down Expand Up @@ -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 (
<Grid container justifyContent="center" spacing={3} wrap={"nowrap"}>
Expand Down
3 changes: 2 additions & 1 deletion src/components/DataEntry/tests/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -97,7 +98,7 @@ async function renderDataEntry(
spyOnUseWindowSize(windowWidth);
await renderer.act(async () => {
testHandle = renderer.create(
<Provider store={mockStore({ treeViewState })}>
<Provider store={mockStore({ currentProjectState, treeViewState })}>
<DataEntry />
</Provider>
);
Expand Down
68 changes: 36 additions & 32 deletions src/components/DataEntry/tests/utilities.test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand Down Expand Up @@ -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);
});
});
});
62 changes: 37 additions & 25 deletions src/components/DataEntry/utilities.ts
Original file line number Diff line number Diff line change
@@ -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)
);
}
6 changes: 3 additions & 3 deletions src/types/word.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down