diff --git a/Backend/Models/User.cs b/Backend/Models/User.cs index 85bbdd6de7..d6407b1416 100644 --- a/Backend/Models/User.cs +++ b/Backend/Models/User.cs @@ -66,10 +66,14 @@ public class User [BsonElement("username")] public string Username { get; set; } - /// Not implemented in frontend. [BsonElement("uiLang")] public string UILang { get; set; } + [Required] + [BsonElement("glossSuggestion")] + [BsonRepresentation(BsonType.String)] + public AutocompleteSetting GlossSuggestion { get; set; } + [Required] [BsonElement("token")] public string Token { get; set; } @@ -94,6 +98,7 @@ public User() Password = ""; Username = ""; UILang = ""; + GlossSuggestion = AutocompleteSetting.On; Token = ""; IsAdmin = false; WorkedProjects = new(); @@ -115,6 +120,7 @@ public User Clone() Password = Password, Username = Username, UILang = UILang, + GlossSuggestion = GlossSuggestion, Token = Token, IsAdmin = IsAdmin, WorkedProjects = WorkedProjects.ToDictionary(kv => kv.Key, kv => kv.Value), @@ -136,6 +142,7 @@ public bool ContentEquals(User other) other.Password.Equals(Password, StringComparison.Ordinal) && other.Username.Equals(Username, StringComparison.Ordinal) && other.UILang.Equals(UILang, StringComparison.Ordinal) && + other.GlossSuggestion.Equals(GlossSuggestion) && other.Token.Equals(Token, StringComparison.Ordinal) && other.IsAdmin == IsAdmin && @@ -172,6 +179,7 @@ public override int GetHashCode() hash.Add(Password); hash.Add(Username); hash.Add(UILang); + hash.Add(GlossSuggestion); hash.Add(Token); hash.Add(IsAdmin); return hash.ToHashCode(); diff --git a/Backend/Repositories/UserRepository.cs b/Backend/Repositories/UserRepository.cs index e576ea2966..0ab2f006bb 100644 --- a/Backend/Repositories/UserRepository.cs +++ b/Backend/Repositories/UserRepository.cs @@ -196,7 +196,8 @@ public async Task Update(string userId, User user, bool updateIs .Set(x => x.ProjectRoles, user.ProjectRoles) .Set(x => x.Agreement, user.Agreement) .Set(x => x.Username, user.Username) - .Set(x => x.UILang, user.UILang); + .Set(x => x.UILang, user.UILang) + .Set(x => x.GlossSuggestion, user.GlossSuggestion); // If .Avatar or .Token has been set to null or "", // this prevents it from being erased in the database diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 71f874e7de..98ac6e39d1 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -126,6 +126,8 @@ }, "userSettings": { "contact": "Contact info", + "glossSuggestion": "Gloss spelling suggestions", + "glossSuggestionHint": "In Data Entry, give spelling suggestions for the Gloss being typed.", "phone": "Phone number", "uiLanguage": "User-interface language", "uiLanguageDefault": "(Default to browser language)", diff --git a/src/api/models/user.ts b/src/api/models/user.ts index a563dd8b9c..fef3669aef 100644 --- a/src/api/models/user.ts +++ b/src/api/models/user.ts @@ -12,6 +12,8 @@ * Do not edit the class manually. */ +import { AutocompleteSetting } from "./autocomplete-setting"; + /** * * @export @@ -108,4 +110,10 @@ export interface User { * @memberof User */ isAdmin: boolean; + /** + * + * @type {AutocompleteSetting} + * @memberof User + */ + glossSuggestion: AutocompleteSetting; } diff --git a/src/components/DataEntry/DataEntryTable/index.tsx b/src/components/DataEntry/DataEntryTable/index.tsx index b38626adec..ce9f8325cc 100644 --- a/src/components/DataEntry/DataEntryTable/index.tsx +++ b/src/components/DataEntry/DataEntryTable/index.tsx @@ -26,7 +26,7 @@ import { Word, } from "api/models"; import * as backend from "backend"; -import { getUserId } from "backend/localStorage"; +import { getCurrentUser, getUserId } from "backend/localStorage"; import NewEntry from "components/DataEntry/DataEntryTable/NewEntry"; import RecentEntry from "components/DataEntry/DataEntryTable/RecentEntry"; import { filterWordsWithSenses } from "components/DataEntry/utilities"; @@ -271,7 +271,11 @@ export default function DataEntryTable( const newVernInput = useRef(null); const spellChecker = useContext(SpellCheckerContext); useEffect(() => { - spellChecker.updateLang(analysisLang.bcp47); + spellChecker.updateLang( + getCurrentUser()?.glossSuggestion === AutocompleteSetting.Off + ? undefined + : analysisLang.bcp47 + ); }, [analysisLang.bcp47, spellChecker]); const { t } = useTranslation(); diff --git a/src/components/DataEntry/DataEntryTable/tests/index.test.tsx b/src/components/DataEntry/DataEntryTable/tests/index.test.tsx index 1e730b1766..d3ef2c6e59 100644 --- a/src/components/DataEntry/DataEntryTable/tests/index.test.tsx +++ b/src/components/DataEntry/DataEntryTable/tests/index.test.tsx @@ -26,6 +26,7 @@ import { newSemanticDomainTreeNode, semDomFromTreeNode, } from "types/semanticDomain"; +import { newUser } from "types/user"; import { multiSenseWord, newGloss, @@ -53,6 +54,7 @@ jest.mock("backend", () => ({ updateWord: (...args: any[]) => mockUpdateWord(...args), })); jest.mock("backend/localStorage", () => ({ + getCurrentUser: () => mockUser, getUserId: () => mockUserId, })); jest.mock("components/DataEntry/DataEntryTable/NewEntry/SenseDialog"); @@ -78,6 +80,7 @@ const mockMultiWord = multiSenseWord("vern", ["gloss1", "gloss2"]); const mockSemDomId = "semDomId"; const mockTreeNode = newSemanticDomainTreeNode(mockSemDomId); const mockSemDom = semDomFromTreeNode(mockTreeNode); +const mockUser = newUser(); const mockUserId = "mockUserId"; const mockStore = configureMockStore()(defaultState); diff --git a/src/components/ProjectSettings/ProjectAutocomplete.tsx b/src/components/ProjectSettings/ProjectAutocomplete.tsx index 2f6dce3259..1c0f4c7402 100644 --- a/src/components/ProjectSettings/ProjectAutocomplete.tsx +++ b/src/components/ProjectSettings/ProjectAutocomplete.tsx @@ -40,7 +40,7 @@ export default function ProjectAutocomplete( title={t("projectSettings.autocomplete.hint")} placement={document.body.dir === "rtl" ? "left" : "right"} > - + diff --git a/src/components/UserSettings/UserSettings.tsx b/src/components/UserSettings/UserSettings.tsx index 020563d743..1b567c1ab6 100644 --- a/src/components/UserSettings/UserSettings.tsx +++ b/src/components/UserSettings/UserSettings.tsx @@ -1,4 +1,4 @@ -import { Email, Phone } from "@mui/icons-material"; +import { Email, HelpOutline, Phone } from "@mui/icons-material"; import { Button, Card, @@ -7,13 +7,14 @@ import { MenuItem, Select, TextField, + Tooltip, Typography, } from "@mui/material"; import { enqueueSnackbar } from "notistack"; import { FormEvent, Fragment, ReactElement, useState } from "react"; import { useTranslation } from "react-i18next"; -import { User } from "api/models"; +import { AutocompleteSetting, User } from "api/models"; import { isEmailTaken, updateUser } from "backend"; import { getAvatar, getCurrentUser } from "backend/localStorage"; import { asyncLoadSemanticDomains } from "components/Project/ProjectActions"; @@ -34,6 +35,7 @@ export enum UserSettingsIds { FieldName = "user-settings-name", FieldPhone = "user-settings-phone", FieldUsername = "user-settings-username", + SelectGlossSuggestion = "user-settings-gloss-suggestion", SelectUiLang = "user-settings-ui-lang", } @@ -57,6 +59,9 @@ export function UserSettings(props: { const [phone, setPhone] = useState(props.user.phone); const [email, setEmail] = useState(props.user.email); const [uiLang, setUiLang] = useState(props.user.uiLang ?? ""); + const [glossSuggestion, setGlossSuggestion] = useState( + props.user.glossSuggestion + ); const [emailTaken, setEmailTaken] = useState(false); const [avatar, setAvatar] = useState(getAvatar()); @@ -72,7 +77,8 @@ export function UserSettings(props: { name === props.user.name && phone === props.user.phone && punycode.toUnicode(email) === props.user.email && - uiLang === (props.user.uiLang ?? ""); + uiLang === (props.user.uiLang ?? "") && + glossSuggestion === props.user.glossSuggestion; async function onSubmit(e: FormEvent): Promise { e.preventDefault(); @@ -83,6 +89,7 @@ export function UserSettings(props: { phone, email: punycode.toUnicode(email), uiLang, + glossSuggestion, hasAvatar: !!avatar, }); @@ -226,6 +233,42 @@ export function UserSettings(props: { + + + + {t("userSettings.glossSuggestion")} + + + + + + + + + + + + + +