Skip to content

Commit

Permalink
[ReviewEntriesTable] Add fuzzy/exact filtering (#3436)
Browse files Browse the repository at this point in the history
  • Loading branch information
imnasnainaec authored Nov 6, 2024
1 parent a4aa54c commit d70d5a5
Show file tree
Hide file tree
Showing 9 changed files with 248 additions and 23 deletions.
25 changes: 25 additions & 0 deletions docs/user_guide/assets/licenses/frontend_licenses.txt
Original file line number Diff line number Diff line change
Expand Up @@ -42671,6 +42671,31 @@ This library is a fork of 'better-json-errors' by Kat Marchán, extended and
distributed under the terms of the MIT license above.


levenshtein-search 0.1.2
MIT
MIT License

Copyright (c) 2018 Tal Einat

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


lines-and-columns 1.2.4
MIT
The MIT License (MIT)
Expand Down
3 changes: 2 additions & 1 deletion docs/user_guide/docs/goals.es.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ There are icons at the top of each column to
![Review Entries column sort icon](../images/reviewEntriesColumnSort.png){width=20} sort the data.

In a column with predominantly text content (Vernacular, Glosses, Note, or Flag), you can sort alphabetically or filter
with a text search.
with a text search. By default, the text search is a fuzzy match: it is not case sensitive and it allows for one or two
typos. If you want exact text matches, use quotes around your filter.

In the Number of Senses column or Pronunciations column, you can sort or filter by the number of senses or recordings
that entries have. In the Pronunciations column, you can also filter by speaker name.
Expand Down
3 changes: 2 additions & 1 deletion docs/user_guide/docs/goals.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ There are icons at the top of each column to
![Review Entries column sort icon](images/reviewEntriesColumnSort.png){width=20} sort the data.

In a column with predominantly text content (Vernacular, Glosses, Note, or Flag), you can sort alphabetically or filter
with a text search.
with a text search. By default, the text search is a fuzzy match: it is not case sensitive and it allows for one or two
typos. If you want exact text matches, use quotes around your filter.

In the Number of Senses column or Pronunciations column, you can sort or filter by the number of senses or recordings
that entries have. In the Pronunciations column, you can also filter by speaker name.
Expand Down
3 changes: 2 additions & 1 deletion docs/user_guide/docs/goals.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ There are icons at the top of each column to
![Review Entries column sort icon](../images/reviewEntriesColumnSort.png){width=20} sort the data.

In a column with predominantly text content (Vernacular, Glosses, Note, or Flag), you can sort alphabetically or filter
with a text search.
with a text search. By default, the text search is a fuzzy match: it is not case sensitive and it allows for one or two
typos. If you want exact text matches, use quotes around your filter.

In the Number of Senses column or Pronunciations column, you can sort or filter by the number of senses or recordings
that entries have. In the Pronunciations column, you can also filter by speaker name.
Expand Down
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"i18next-browser-languagedetector": "^8.0.0",
"i18next-http-backend": "^2.6.0",
"js-base64": "^3.7.7",
"levenshtein-search": "^0.1.2",
"make-dir": "^4.0.0",
"material-react-table": "^2.9.2",
"motion": "^10.16.2",
Expand Down
61 changes: 52 additions & 9 deletions src/goals/ReviewEntries/ReviewEntriesTable/filterFn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,58 @@ import {
} from "api/models";
import { type Hash } from "types/hash";

// eslint-disable-next-line @typescript-eslint/no-var-requires
const { fuzzySearch } = require("levenshtein-search");

/** Checks if string starts and ends with quote marks.
* For simplicity, allows mismatched quote types. */
export function isQuoted(filter: string): boolean {
return /^["'\p{Pi}].*["'\p{Pf}]$/u.test(filter);
}

/** Number of typos allowed, depending on filter-length. */
function levDist(len: number): number {
return len < 3 ? 0 : len < 6 ? 1 : 2;
}

/** Checks if value contains a substring that fuzzy-matches the filter. */
export function fuzzyContains(value: string, filter: string): boolean {
filter = filter.toLowerCase();
value = value.toLowerCase();
// `fuzzySearch(...)` returns a generator;
// `.next()` on a generator always returns an object with boolean property `done`
return !fuzzySearch(filter, value, levDist(filter.length)).next().done;
}

/** Check if string matches filter.
* If filter quoted, exact match. Otherwise, fuzzy match. */
export function matchesFilter(value: string, filter: string): boolean {
filter = filter.trim();
return isQuoted(filter)
? value.includes(filter.substring(1, filter.length - 1).trim())
: fuzzyContains(value, filter);
}

/* Custom `filterFn` functions for `MaterialReactTable` columns.
* (Can always assume that `filterValue` will be truthy.) */

/** Requires the accessor return type to be `Dictionary[]`. */
/** Requires the accessor return type to be `string`. */
export const filterFnString: MRT_FilterFn<Word> = (
row,
id,
filterValue: string
) => {
return matchesFilter(row.getValue<string>(id), filterValue);
};

/** Requires the accessor return type to be `Definition[]`. */
export const filterFnDefinitions: MRT_FilterFn<Word> = (
row,
id,
filterValue: string
) => {
const definitions = row.getValue<Definition[]>(id);
const filter = filterValue.trim().toLowerCase();
return definitions.some((d) => d.text.toLowerCase().includes(filter));
return definitions.some((d) => matchesFilter(d.text, filterValue));
};

/** Requires the accessor return type to be `Gloss[]`. */
Expand All @@ -31,8 +71,7 @@ export const filterFnGlosses: MRT_FilterFn<Word> = (
filterValue: string
) => {
const glosses = row.getValue<Gloss[]>(id);
const filter = filterValue.trim().toLowerCase();
return glosses.some((g) => g.def.toLowerCase().includes(filter));
return glosses.some((g) => matchesFilter(g.def, filterValue));
};

/** Requires the accessor return type to be `SemanticDomain[]`. */
Expand Down Expand Up @@ -79,10 +118,15 @@ export const filterFnPronunciations =
/* Match either number of pronunciations or a speaker name.
* (Whitespace will match all audio, even without a speaker.) */
const audio = row.getValue<Pronunciation[]>(id);
const filter = filterValue.trim().toLocaleLowerCase();
const filter = filterValue.trim();
return (
(audio.length && !filter) ||
audio.length === parseInt(filter) ||
audio.some((p) => !filter || speakers[p.speakerId]?.includes(filter))
audio.some(
(p) =>
p.speakerId in speakers &&
matchesFilter(speakers[p.speakerId], filter)
)
);
};

Expand All @@ -97,6 +141,5 @@ export const filterFnFlag: MRT_FilterFn<Word> = (
// A filter has been typed and the word isn't flagged
return false;
}
const filter = filterValue.trim().toLowerCase();
return flag.text.toLowerCase().includes(filter);
return matchesFilter(flag.text, filterValue);
};
2 changes: 2 additions & 0 deletions src/goals/ReviewEntries/ReviewEntriesTable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ export default function ReviewEntriesTable(props: {
Cell: ({ row }: CellProps) => <Cell.Vernacular word={row.original} />,
enableColumnOrdering: false,
enableHiding: false,
filterFn: ff.filterFnString,
header: t("reviewEntries.columns.vernacular"),
id: ColumnId.Vernacular,
size: BaselineColumnSize - 40,
Expand Down Expand Up @@ -327,6 +328,7 @@ export default function ReviewEntriesTable(props: {
// Note column
columnHelper.accessor((w) => w.note.text || undefined, {
Cell: ({ row }: CellProps) => <Cell.Note word={row.original} />,
filterFn: ff.filterFnString,
header: t("reviewEntries.columns.note"),
id: ColumnId.Note,
size: BaselineColumnSize - 40,
Expand Down
Loading

0 comments on commit d70d5a5

Please sign in to comment.