Skip to content

Commit

Permalink
Merge branch 'master' into arm
Browse files Browse the repository at this point in the history
  • Loading branch information
imnasnainaec committed Nov 8, 2024
2 parents 67a0ac1 + d70d5a5 commit 9159582
Show file tree
Hide file tree
Showing 12 changed files with 275 additions and 33 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
26 changes: 21 additions & 5 deletions src/components/Pronunciations/AudioRecorder.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ReactElement, useContext } from "react";
import { ReactElement, useContext, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "react-toastify";

Expand All @@ -22,15 +22,28 @@ export default function AudioRecorder(props: RecorderProps): ReactElement {
(state: StoreState) => state.currentProjectState.speaker?.id
);
const recorder = useContext(RecorderContext);
const [clicked, setClicked] = useState(false);
const { t } = useTranslation();

async function startRecording(): Promise<void> {
useEffect(() => {
// Re-enable clicking when the word id has changed
setClicked(false);
}, [props.id]);

async function startRecording(): Promise<boolean> {
if (clicked) {
// Prevent recording again before this word has updated.
return false;
}

const recordingId = recorder.getRecordingId();
if (recordingId && recordingId !== props.id) {
// Prevent interfering with an active recording on a different entry.
return;
return false;
}

setClicked(true);

// Prevent starting a recording before a previous one is finished.
await stopRecording();

Expand All @@ -40,10 +53,12 @@ export default function AudioRecorder(props: RecorderProps): ReactElement {
errorMessage += ` ${t("pronunciations.recordingPermission")}`;
}
toast.error(errorMessage);
return false;
}
return true;
}

async function stopRecording(): Promise<string | undefined> {
async function stopRecording(): Promise<void> {
// Prevent triggering this function if no recording is active.
if (recorder.getRecordingId() === undefined) {
return;
Expand All @@ -53,8 +68,9 @@ export default function AudioRecorder(props: RecorderProps): ReactElement {
props.onClick();
}
const file = await recorder.stopRecording();
if (!file) {
if (!file || !file.size) {
toast.error(t("pronunciations.recordingError"));
setClicked(false);
return;
}
if (!props.noSpeaker) {
Expand Down
9 changes: 5 additions & 4 deletions src/components/Pronunciations/RecorderIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const recordIconId = "recordingIcon";
interface RecorderIconProps {
disabled?: boolean;
id: string;
startRecording: () => void;
startRecording: () => Promise<boolean>;
stopRecording: () => void;
}

Expand All @@ -41,11 +41,12 @@ export default function RecorderIcon(props: RecorderIconProps): ReactElement {
checkMicPermission().then(setHasMic);
}, []);

function toggleIsRecordingToTrue(): void {
async function toggleIsRecordingToTrue(): Promise<void> {
if (!isRecording) {
// Only start a recording if there's not another on in progress.
dispatch(recording(props.id));
props.startRecording();
if (await props.startRecording()) {
dispatch(recording(props.id));
}
} else {
// This triggers if user clicks-and-holds on one entry's record icon,
// drags the mouse outside that icon before releasing,
Expand Down
2 changes: 1 addition & 1 deletion src/components/Pronunciations/tests/RecorderIcon.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ function mockRecordingState(wordId: string): Partial<StoreState> {

const mockWordId = "1234567890";

const mockStartRecording = jest.fn();
const mockStartRecording = jest.fn(() => Promise.resolve(true));
const mockStopRecording = jest.fn();

const renderRecorderIcon = async (wordId = ""): Promise<void> => {
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 9159582

Please sign in to comment.