From 04400e28df9168a6045ad45f3d7955ed962c842d Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Wed, 13 Nov 2024 15:08:21 -0500 Subject: [PATCH] Add project setting for overriding protected words/senses --- Backend.Tests/Models/ProjectTests.cs | 3 +- Backend/Models/Project.cs | 17 +++++-- Backend/Models/User.cs | 4 +- Backend/Repositories/ProjectRepository.cs | 1 + public/locales/en/translation.json | 4 ++ src/api/.openapi-generator/FILES | 2 +- src/api/models/index.ts | 2 +- ...ocomplete-setting.ts => off-on-setting.ts} | 2 +- src/api/models/project.ts | 12 +++-- src/api/models/user.ts | 14 +++--- .../DataEntry/DataEntryTable/index.tsx | 7 ++- .../ProjectSettings/ProjectAutocomplete.tsx | 10 ++-- .../ProjectProtectedOverride.tsx | 48 +++++++++++++++++++ src/components/ProjectSettings/index.tsx | 17 +++++++ .../tests/ProjectAutocomplete.test.tsx | 6 +-- .../ProjectSettings/tests/SettingsTabTypes.ts | 2 + src/components/UserSettings/UserSettings.tsx | 8 ++-- .../MergeDupsStep/SaveDeferButtons.tsx | 6 ++- src/types/project.ts | 5 +- src/types/user.ts | 4 +- 20 files changed, 133 insertions(+), 41 deletions(-) rename src/api/models/{autocomplete-setting.ts => off-on-setting.ts} (93%) create mode 100644 src/components/ProjectSettings/ProjectProtectedOverride.tsx diff --git a/Backend.Tests/Models/ProjectTests.cs b/Backend.Tests/Models/ProjectTests.cs index 59e73a98fd..dd28d3ef42 100644 --- a/Backend.Tests/Models/ProjectTests.cs +++ b/Backend.Tests/Models/ProjectTests.cs @@ -107,7 +107,8 @@ public void TestClone() LiftImported = true, DefinitionsEnabled = true, GrammaticalInfoEnabled = true, - AutocompleteSetting = AutocompleteSetting.On, + AutocompleteSetting = OffOnSetting.On, + ProtectedDataOverrideEnabled = OffOnSetting.Off, SemDomWritingSystem = new("fr", "Français"), VernacularWritingSystem = new("en", "English", "Calibri"), AnalysisWritingSystems = new() { new("es", "Español") }, diff --git a/Backend/Models/Project.cs b/Backend/Models/Project.cs index ba6c3fe6f8..443f289083 100644 --- a/Backend/Models/Project.cs +++ b/Backend/Models/Project.cs @@ -38,7 +38,12 @@ public class Project [Required] [BsonElement("autocompleteSetting")] [BsonRepresentation(BsonType.String)] - public AutocompleteSetting AutocompleteSetting { get; set; } + public OffOnSetting AutocompleteSetting { get; set; } + + [Required] + [BsonElement("protectedDataOverrideEnabled")] + [BsonRepresentation(BsonType.String)] + public OffOnSetting ProtectedDataOverrideEnabled { get; set; } [Required] [BsonElement("semDomWritingSystem")] @@ -92,7 +97,8 @@ public Project() LiftImported = false; DefinitionsEnabled = false; GrammaticalInfoEnabled = false; - AutocompleteSetting = AutocompleteSetting.On; + AutocompleteSetting = OffOnSetting.On; + ProtectedDataOverrideEnabled = OffOnSetting.Off; SemDomWritingSystem = new(); VernacularWritingSystem = new(); AnalysisWritingSystems = new(); @@ -117,6 +123,7 @@ public Project Clone() DefinitionsEnabled = DefinitionsEnabled, GrammaticalInfoEnabled = GrammaticalInfoEnabled, AutocompleteSetting = AutocompleteSetting, + ProtectedDataOverrideEnabled = ProtectedDataOverrideEnabled, SemDomWritingSystem = SemDomWritingSystem.Clone(), VernacularWritingSystem = VernacularWritingSystem.Clone(), AnalysisWritingSystems = AnalysisWritingSystems.Select(ws => ws.Clone()).ToList(), @@ -140,6 +147,7 @@ public bool ContentEquals(Project other) other.DefinitionsEnabled == DefinitionsEnabled && other.GrammaticalInfoEnabled == GrammaticalInfoEnabled && other.AutocompleteSetting.Equals(AutocompleteSetting) && + other.ProtectedDataOverrideEnabled.Equals(ProtectedDataOverrideEnabled) && other.SemDomWritingSystem.Equals(SemDomWritingSystem) && other.VernacularWritingSystem.Equals(VernacularWritingSystem) && @@ -186,11 +194,12 @@ public override int GetHashCode() var hash = new HashCode(); hash.Add(Id); hash.Add(Name); + hash.Add(IsActive); hash.Add(LiftImported); hash.Add(DefinitionsEnabled); hash.Add(GrammaticalInfoEnabled); - hash.Add(IsActive); hash.Add(AutocompleteSetting); + hash.Add(ProtectedDataOverrideEnabled); hash.Add(SemDomWritingSystem); hash.Add(VernacularWritingSystem); hash.Add(AnalysisWritingSystems); @@ -329,7 +338,7 @@ public UserCreatedProject() } } - public enum AutocompleteSetting + public enum OffOnSetting { Off, On diff --git a/Backend/Models/User.cs b/Backend/Models/User.cs index d6407b1416..1814dd8758 100644 --- a/Backend/Models/User.cs +++ b/Backend/Models/User.cs @@ -72,7 +72,7 @@ public class User [Required] [BsonElement("glossSuggestion")] [BsonRepresentation(BsonType.String)] - public AutocompleteSetting GlossSuggestion { get; set; } + public OffOnSetting GlossSuggestion { get; set; } [Required] [BsonElement("token")] @@ -98,7 +98,7 @@ public User() Password = ""; Username = ""; UILang = ""; - GlossSuggestion = AutocompleteSetting.On; + GlossSuggestion = OffOnSetting.On; Token = ""; IsAdmin = false; WorkedProjects = new(); diff --git a/Backend/Repositories/ProjectRepository.cs b/Backend/Repositories/ProjectRepository.cs index 40652f550c..3baa99b512 100644 --- a/Backend/Repositories/ProjectRepository.cs +++ b/Backend/Repositories/ProjectRepository.cs @@ -99,6 +99,7 @@ public async Task Update(string projectId, Project project) .Set(x => x.DefinitionsEnabled, project.DefinitionsEnabled) .Set(x => x.GrammaticalInfoEnabled, project.GrammaticalInfoEnabled) .Set(x => x.AutocompleteSetting, project.AutocompleteSetting) + .Set(x => x.ProtectedDataOverrideEnabled, project.ProtectedDataOverrideEnabled) .Set(x => x.SemDomWritingSystem, project.SemDomWritingSystem) .Set(x => x.VernacularWritingSystem, project.VernacularWritingSystem) .Set(x => x.AnalysisWritingSystems, project.AnalysisWritingSystems) diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 7e757db4b4..181bf97dd1 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -265,6 +265,10 @@ "on": "On", "hint": "In Data Entry, suggest existing Vernaculars similar to the Vernacular being typed." }, + "protectedDataOverride": { + "hint": "In Merge Duplicates, allow overriding protection of protected words and senses.", + "label": "Protected Data Override" + }, "invite": { "inviteByEmailLabel": "Invite by Email", "userExists": "This user is already registered.", diff --git a/src/api/.openapi-generator/FILES b/src/api/.openapi-generator/FILES index 873eb5b3b5..cf9d132ea8 100644 --- a/src/api/.openapi-generator/FILES +++ b/src/api/.openapi-generator/FILES @@ -21,7 +21,6 @@ common.ts configuration.ts git_push.sh index.ts -models/autocomplete-setting.ts models/banner-type.ts models/chart-root-data.ts models/consent-type.ts @@ -42,6 +41,7 @@ models/merge-source-word.ts models/merge-undo-ids.ts models/merge-words.ts models/note.ts +models/off-on-setting.ts models/password-reset-data.ts models/password-reset-request-data.ts models/permission.ts diff --git a/src/api/models/index.ts b/src/api/models/index.ts index 5b140bd273..dc5992c9b2 100644 --- a/src/api/models/index.ts +++ b/src/api/models/index.ts @@ -1,4 +1,3 @@ -export * from "./autocomplete-setting"; export * from "./banner-type"; export * from "./chart-root-data"; export * from "./consent-type"; @@ -18,6 +17,7 @@ export * from "./merge-source-word"; export * from "./merge-undo-ids"; export * from "./merge-words"; export * from "./note"; +export * from "./off-on-setting"; export * from "./password-reset-data"; export * from "./password-reset-request-data"; export * from "./permission"; diff --git a/src/api/models/autocomplete-setting.ts b/src/api/models/off-on-setting.ts similarity index 93% rename from src/api/models/autocomplete-setting.ts rename to src/api/models/off-on-setting.ts index b54c9ab6fa..a41171e274 100644 --- a/src/api/models/autocomplete-setting.ts +++ b/src/api/models/off-on-setting.ts @@ -17,7 +17,7 @@ * @export * @enum {string} */ -export enum AutocompleteSetting { +export enum OffOnSetting { Off = "Off", On = "On", } diff --git a/src/api/models/project.ts b/src/api/models/project.ts index 0958b4d28c..898556b259 100644 --- a/src/api/models/project.ts +++ b/src/api/models/project.ts @@ -12,9 +12,9 @@ * Do not edit the class manually. */ -import { AutocompleteSetting } from "./autocomplete-setting"; import { CustomField } from "./custom-field"; import { EmailInvite } from "./email-invite"; +import { OffOnSetting } from "./off-on-setting"; import { SemanticDomainFull } from "./semantic-domain-full"; import { WritingSystem } from "./writing-system"; @@ -62,10 +62,16 @@ export interface Project { grammaticalInfoEnabled: boolean; /** * - * @type {AutocompleteSetting} + * @type {OffOnSetting} * @memberof Project */ - autocompleteSetting: AutocompleteSetting; + autocompleteSetting: OffOnSetting; + /** + * + * @type {OffOnSetting} + * @memberof Project + */ + protectedDataOverrideEnabled: OffOnSetting; /** * * @type {WritingSystem} diff --git a/src/api/models/user.ts b/src/api/models/user.ts index fef3669aef..b88fa4d43a 100644 --- a/src/api/models/user.ts +++ b/src/api/models/user.ts @@ -12,7 +12,7 @@ * Do not edit the class manually. */ -import { AutocompleteSetting } from "./autocomplete-setting"; +import { OffOnSetting } from "./off-on-setting"; /** * @@ -100,20 +100,20 @@ export interface User { uiLang?: string | null; /** * - * @type {string} + * @type {OffOnSetting} * @memberof User */ - token: string; + glossSuggestion: OffOnSetting; /** * - * @type {boolean} + * @type {string} * @memberof User */ - isAdmin: boolean; + token: string; /** * - * @type {AutocompleteSetting} + * @type {boolean} * @memberof User */ - glossSuggestion: AutocompleteSetting; + isAdmin: boolean; } diff --git a/src/components/DataEntry/DataEntryTable/index.tsx b/src/components/DataEntry/DataEntryTable/index.tsx index ce9f8325cc..3d20bda5dd 100644 --- a/src/components/DataEntry/DataEntryTable/index.tsx +++ b/src/components/DataEntry/DataEntryTable/index.tsx @@ -17,8 +17,8 @@ import { useTranslation } from "react-i18next"; import { v4 } from "uuid"; import { - AutocompleteSetting, Note, + OffOnSetting, Pronunciation, SemanticDomain, SemanticDomainTreeNode, @@ -250,8 +250,7 @@ export default function DataEntryTable( ); const suggestVerns = useAppSelector( (state: StoreState) => - state.currentProjectState.project.autocompleteSetting === - AutocompleteSetting.On + state.currentProjectState.project.autocompleteSetting === OffOnSetting.On ); const vernacularLang = useAppSelector( (state: StoreState) => @@ -272,7 +271,7 @@ export default function DataEntryTable( const spellChecker = useContext(SpellCheckerContext); useEffect(() => { spellChecker.updateLang( - getCurrentUser()?.glossSuggestion === AutocompleteSetting.Off + getCurrentUser()?.glossSuggestion === OffOnSetting.Off ? undefined : analysisLang.bcp47 ); diff --git a/src/components/ProjectSettings/ProjectAutocomplete.tsx b/src/components/ProjectSettings/ProjectAutocomplete.tsx index 1c0f4c7402..5890af880f 100644 --- a/src/components/ProjectSettings/ProjectAutocomplete.tsx +++ b/src/components/ProjectSettings/ProjectAutocomplete.tsx @@ -3,7 +3,7 @@ import { Grid, MenuItem, Select, Tooltip } from "@mui/material"; import { type ReactElement } from "react"; import { useTranslation } from "react-i18next"; -import { AutocompleteSetting } from "api/models"; +import { OffOnSetting } from "api/models"; import { type ProjectSettingProps } from "components/ProjectSettings/ProjectSettingsTypes"; export default function ProjectAutocomplete( @@ -12,7 +12,7 @@ export default function ProjectAutocomplete( const { t } = useTranslation(); const updateAutocompleteSetting = async ( - autocompleteSetting: AutocompleteSetting + autocompleteSetting: OffOnSetting ): Promise => { await props.setProject({ ...props.project, autocompleteSetting }); }; @@ -24,13 +24,13 @@ export default function ProjectAutocomplete( variant="standard" value={props.project.autocompleteSetting} onChange={(e) => - updateAutocompleteSetting(e.target.value as AutocompleteSetting) + updateAutocompleteSetting(e.target.value as OffOnSetting) } > - + {t("projectSettings.autocomplete.off")} - + {t("projectSettings.autocomplete.on")} diff --git a/src/components/ProjectSettings/ProjectProtectedOverride.tsx b/src/components/ProjectSettings/ProjectProtectedOverride.tsx new file mode 100644 index 0000000000..89b03f82b9 --- /dev/null +++ b/src/components/ProjectSettings/ProjectProtectedOverride.tsx @@ -0,0 +1,48 @@ +import { HelpOutline } from "@mui/icons-material"; +import { Grid, MenuItem, Select, Tooltip } from "@mui/material"; +import { type ReactElement } from "react"; +import { useTranslation } from "react-i18next"; + +import { OffOnSetting } from "api/models"; +import { type ProjectSettingProps } from "components/ProjectSettings/ProjectSettingsTypes"; + +export default function ProjectProtectedOverride( + props: ProjectSettingProps +): ReactElement { + const { t } = useTranslation(); + + const updateProtectOverrideSetting = async ( + protectedDataOverrideEnabled: OffOnSetting + ): Promise => { + await props.setProject({ ...props.project, protectedDataOverrideEnabled }); + }; + + return ( + + + + + + + + + + + ); +} diff --git a/src/components/ProjectSettings/index.tsx b/src/components/ProjectSettings/index.tsx index c21fa820fe..382747a3d1 100644 --- a/src/components/ProjectSettings/index.tsx +++ b/src/components/ProjectSettings/index.tsx @@ -10,6 +10,7 @@ import { People, PersonAdd, RecordVoiceOver, + RemoveModerator, Settings, Sms, } from "@mui/icons-material"; @@ -53,6 +54,7 @@ import ProjectLanguages, { SemanticDomainLanguage, } from "components/ProjectSettings/ProjectLanguages"; import ProjectName from "components/ProjectSettings/ProjectName"; +import ProjectProtectedOverride from "components/ProjectSettings/ProjectProtectedOverride"; import ProjectSchedule from "components/ProjectSettings/ProjectSchedule"; import ProjectSelect from "components/ProjectSettings/ProjectSelect"; import ActiveProjectUsers from "components/ProjectUsers/ActiveProjectUsers"; @@ -80,6 +82,7 @@ export enum Setting { Import = "SettingImport", Languages = "SettingLanguages", Name = "SettingName", + ProtectOverride = "SettingProtectOverride", Schedule = "SettingSchedule", Speakers = "SettingSpeakers", UserAdd = "SettingUserAdd", @@ -176,6 +179,20 @@ export default function ProjectSettingsComponent(): ReactElement { /> )} + {/* Protected data override toggle */} + {permissions.includes(Permission.DeleteEditSettingsAndUsers) && ( + } + title={t("projectSettings.protectedDataOverride.label")} + body={ + + } + /> + )} + {/* Archive project */} {permissions.includes(Permission.Archive) && ( { await renderer.act(async () => selectChange({ target: { value: "Off" } })); expect(mockSetProject).toHaveBeenCalledWith({ ...mockProject, - autocompleteSetting: AutocompleteSetting.Off, + autocompleteSetting: OffOnSetting.Off, }); await renderer.act(async () => selectChange({ target: { value: "On" } })); expect(mockSetProject).toHaveBeenCalledWith({ ...mockProject, - autocompleteSetting: AutocompleteSetting.On, + autocompleteSetting: OffOnSetting.On, }); }); }); diff --git a/src/components/ProjectSettings/tests/SettingsTabTypes.ts b/src/components/ProjectSettings/tests/SettingsTabTypes.ts index f98a47a9a6..e5e3f3f830 100644 --- a/src/components/ProjectSettings/tests/SettingsTabTypes.ts +++ b/src/components/ProjectSettings/tests/SettingsTabTypes.ts @@ -8,6 +8,7 @@ const settingsByTab: Record = { Setting.Archive, Setting.Autocomplete, Setting.Name, + Setting.ProtectOverride, ], [ProjectSettingsTab.ImportExport]: [Setting.Export, Setting.Import], [ProjectSettingsTab.Languages]: [Setting.Languages], @@ -34,6 +35,7 @@ const settingsByPermission: Record = { Setting.DomainsCustom, Setting.Languages, Setting.Name, + Setting.ProtectOverride, Setting.Speakers, Setting.UserAdd, Setting.Users, diff --git a/src/components/UserSettings/UserSettings.tsx b/src/components/UserSettings/UserSettings.tsx index ecf98a692c..8d6f69e0f5 100644 --- a/src/components/UserSettings/UserSettings.tsx +++ b/src/components/UserSettings/UserSettings.tsx @@ -15,7 +15,7 @@ import { FormEvent, Fragment, ReactElement, useState } from "react"; import { useTranslation } from "react-i18next"; import { show } from "vanilla-cookieconsent"; -import { AutocompleteSetting, User } from "api/models"; +import { OffOnSetting, User } from "api/models"; import { isEmailTaken, updateUser } from "backend"; import { getAvatar, getCurrentUser } from "backend/localStorage"; import { asyncLoadSemanticDomains } from "components/Project/ProjectActions"; @@ -252,15 +252,15 @@ export function UserSettings(props: { data-testid={UserSettingsIds.SelectGlossSuggestion} id={UserSettingsIds.SelectGlossSuggestion} onChange={(e) => - setGlossSuggestion(e.target.value as AutocompleteSetting) + setGlossSuggestion(e.target.value as OffOnSetting) } value={glossSuggestion} variant="standard" > - + {t("projectSettings.autocomplete.off")} - + {t("projectSettings.autocomplete.on")} diff --git a/src/goals/MergeDuplicates/MergeDupsStep/SaveDeferButtons.tsx b/src/goals/MergeDuplicates/MergeDupsStep/SaveDeferButtons.tsx index d5c4fe672b..47a3da1897 100644 --- a/src/goals/MergeDuplicates/MergeDupsStep/SaveDeferButtons.tsx +++ b/src/goals/MergeDuplicates/MergeDupsStep/SaveDeferButtons.tsx @@ -2,6 +2,7 @@ import { Checkbox, FormControlLabel, Grid } from "@mui/material"; import { ReactElement, useState } from "react"; import { useTranslation } from "react-i18next"; +import { OffOnSetting } from "api/models"; import { LoadingButton } from "components/Buttons"; import { deferMerge, @@ -18,7 +19,10 @@ export default function SaveDeferButtons(): ReactElement { const dispatch = useAppDispatch(); const hasProtected = useAppSelector( - (state: StoreState) => state.mergeDuplicateGoal.hasProtected + (state: StoreState) => + state.mergeDuplicateGoal.hasProtected && + state.currentProjectState.project.protectedDataOverrideEnabled == + OffOnSetting.On ); const overrideProtection = useAppSelector( (state: StoreState) => state.mergeDuplicateGoal.overrideProtection diff --git a/src/types/project.ts b/src/types/project.ts index c0d70abf89..0bd6b05350 100644 --- a/src/types/project.ts +++ b/src/types/project.ts @@ -1,4 +1,4 @@ -import { AutocompleteSetting, ConsentType, Project, Speaker } from "api/models"; +import { ConsentType, OffOnSetting, Project, Speaker } from "api/models"; import { newWritingSystem } from "types/writingSystem"; import { randomIntString } from "utilities/utilities"; @@ -10,6 +10,8 @@ export function newProject(name = ""): Project { liftImported: false, definitionsEnabled: false, grammaticalInfoEnabled: false, + autocompleteSetting: OffOnSetting.On, + protectedDataOverrideEnabled: OffOnSetting.Off, semanticDomains: [], semDomWritingSystem: newWritingSystem(), vernacularWritingSystem: newWritingSystem(), @@ -17,7 +19,6 @@ export function newProject(name = ""): Project { validCharacters: [], rejectedCharacters: [], inviteTokens: [], - autocompleteSetting: AutocompleteSetting.On, }; } diff --git a/src/types/user.ts b/src/types/user.ts index 29d6d0177c..6ea2534882 100644 --- a/src/types/user.ts +++ b/src/types/user.ts @@ -1,4 +1,4 @@ -import { AutocompleteSetting, User } from "api/models"; +import { OffOnSetting, User } from "api/models"; export function newUser(name = "", username = "", password = ""): User { return { @@ -12,7 +12,7 @@ export function newUser(name = "", username = "", password = ""): User { phone: "", projectRoles: {}, workedProjects: {}, - glossSuggestion: AutocompleteSetting.On, + glossSuggestion: OffOnSetting.On, token: "", isAdmin: false, };