Skip to content

Commit

Permalink
[ExistingDataTable] Show all glosses, with primary analysis lang first (
Browse files Browse the repository at this point in the history
  • Loading branch information
imnasnainaec authored Oct 20, 2023
1 parent 28f5ce6 commit d7d73e5
Show file tree
Hide file tree
Showing 8 changed files with 123 additions and 101 deletions.
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

0 comments on commit d7d73e5

Please sign in to comment.