From e64ca0b2203c88d23c3aa684d85a4cce1c485afb Mon Sep 17 00:00:00 2001 From: "D. Ror" Date: Fri, 2 Feb 2024 09:35:39 -0500 Subject: [PATCH 01/20] [MergeDups] When word/sense is protected, give reasons (#2888) --- .vscode/settings.json | 6 +- Backend.Tests/Helper/LiftHelperTests.cs | 265 ++++++++++++++++++ Backend/Helper/LiftHelper.cs | 191 ++++++++++++- Backend/Models/ProtectReason.cs | 79 ++++++ Backend/Models/Sense.cs | 14 +- Backend/Models/Word.cs | 13 + Backend/Services/LiftService.cs | 2 + public/locales/en/translation.json | 35 ++- src/api/.openapi-generator/FILES | 2 + src/api/models/index.ts | 2 + src/api/models/protect-reason.ts | 41 +++ src/api/models/reason-type.ts | 47 ++++ src/api/models/sense.ts | 7 + src/api/models/word.ts | 7 + .../MultilineTooltipTitle/index.tsx | 7 + src/components/Pronunciations/AudioPlayer.tsx | 10 +- .../MergeDupsStep/MergeDragDrop/DropWord.tsx | 72 ++++- .../MergeDupsStep/SenseCardContent.tsx | 87 +++++- 18 files changed, 860 insertions(+), 27 deletions(-) create mode 100644 Backend.Tests/Helper/LiftHelperTests.cs create mode 100644 Backend/Models/ProtectReason.cs create mode 100644 src/api/models/protect-reason.ts create mode 100644 src/api/models/reason-type.ts create mode 100644 src/components/MultilineTooltipTitle/index.tsx diff --git a/.vscode/settings.json b/.vscode/settings.json index 4c8b3939cc..73188d834c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -78,8 +78,10 @@ "sched", "signup", "sillsdev", - "Sldr", - "Subdir", + "sldr", + "subdir", + "subsense", + "subsenses", "subtag", "subtags", "targetdir", diff --git a/Backend.Tests/Helper/LiftHelperTests.cs b/Backend.Tests/Helper/LiftHelperTests.cs new file mode 100644 index 0000000000..f56a66a11e --- /dev/null +++ b/Backend.Tests/Helper/LiftHelperTests.cs @@ -0,0 +1,265 @@ +using System.Linq; +using static BackendFramework.Helper.LiftHelper; +using BackendFramework.Models; +using NUnit.Framework; +using SIL.Lift.Parsing; +using BackendFramework.Helper; + +namespace Backend.Tests.Helper +{ + public class LiftHelperTests + { + [Test] + public void EntryUnprotected() + { + var entry = new LiftEntry + { + CitationForm = new("key", "content"), + DateCreated = new(), + DateDeleted = new(), + DateModified = new(), + Guid = new(), + Id = "id", + LexicalForm = new("key", "content"), + Order = 1, + }; + // A single note with empty type is allowed. + entry.Notes.Add(new("", new("key", "content"))); + entry.Pronunciations.Add(new()); + entry.Pronunciations.Add(new()); + entry.Senses.Add(new()); + entry.Senses.Add(new()); + // The only entry trait not protected is morph type "stem". + entry.Traits.Add(new() { Name = TraitNames.MorphType, Value = "stem" }); + entry.Traits.Add(new() { Name = TraitNames.MorphType, Value = "stem" }); + entry.Traits.First().Annotations.Add(new() { Name = "name", Value = "value" }); + + Assert.That(IsProtected(entry), Is.False); + Assert.That(GetProtectedReasons(entry), Is.Empty); + } + + [Test] + public void EntryAnnotationsProtected() + { + var entry = new LiftEntry(); + entry.Annotations.Add(new()); + entry.Annotations.Add(new()); + Assert.That(IsProtected(entry), Is.True); + var reasons = GetProtectedReasons(entry); + Assert.That(reasons, Has.Count.EqualTo(1)); + Assert.That(reasons.First().Type, Is.EqualTo(ReasonType.Annotations)); + } + + [Test] + public void EntryEtymologiesProtected() + { + var entry = new LiftEntry(); + entry.Etymologies.Add(new()); + entry.Etymologies.Add(new()); + Assert.That(IsProtected(entry), Is.True); + var reasons = GetProtectedReasons(entry); + Assert.That(reasons, Has.Count.EqualTo(1)); + Assert.That(reasons.First().Type, Is.EqualTo(ReasonType.Etymologies)); + } + + [Test] + public void EntryFieldProtected() + { + var entry = new LiftEntry(); + entry.Fields.Add(new()); + entry.Fields.Add(new()); + Assert.That(IsProtected(entry), Is.True); + var reasons = GetProtectedReasons(entry); + Assert.That(reasons, Has.Count.EqualTo(2)); + Assert.That(reasons.First().Type, Is.EqualTo(ReasonType.Field)); + Assert.That(reasons.Last().Type, Is.EqualTo(ReasonType.Field)); + } + + [Test] + public void EntryNoteTypeProtected() + { + var entry = new LiftEntry(); + entry.Notes.Add(new()); + entry.Notes.First().Type = "non-empty"; + Assert.That(IsProtected(entry), Is.True); + var reasons = GetProtectedReasons(entry); + Assert.That(reasons, Has.Count.EqualTo(1)); + Assert.That(reasons.Last().Type, Is.EqualTo(ReasonType.NoteWithType)); + } + + [Test] + public void EntryNotesProtected() + { + var entry = new LiftEntry(); + entry.Notes.Add(new()); + entry.Notes.Add(new()); + Assert.That(IsProtected(entry), Is.True); + var reasons = GetProtectedReasons(entry); + Assert.That(reasons, Has.Count.EqualTo(1)); + Assert.That(reasons.Last().Type, Is.EqualTo(ReasonType.Notes)); + } + + [Test] + public void EntryRelationsProtected() + { + var entry = new LiftEntry(); + entry.Relations.Add(new()); + entry.Relations.Add(new()); + Assert.That(IsProtected(entry), Is.True); + var reasons = GetProtectedReasons(entry); + Assert.That(reasons, Has.Count.EqualTo(1)); + Assert.That(reasons.Last().Type, Is.EqualTo(ReasonType.Relations)); + } + + [Test] + public void EntryVariantsProtected() + { + var entry = new LiftEntry(); + entry.Variants.Add(new()); + entry.Variants.Add(new()); + Assert.That(IsProtected(entry), Is.True); + var reasons = GetProtectedReasons(entry); + Assert.That(reasons, Has.Count.EqualTo(1)); + Assert.That(reasons.Last().Type, Is.EqualTo(ReasonType.Variants)); + } + + [Test] + public void SenseUnprotected() + { + var sense = new LiftSense + { + DateCreated = new(), + DateModified = new(), + Definition = new("key1", "contentA"), + Gloss = new("key1", "contentB"), + GramInfo = new() { Value = "value" }, + Guid = new(), + Id = "id", + Order = 1, + }; + sense.Definition.Add("key2", "contentA"); + sense.Gloss.Add("key3", "contentC"); + // The only sense trait not protected is semantic domain. + sense.Traits.Add(new() { Name = TraitNames.SemanticDomain, Value = "1" }); + sense.Traits.Add(new() { Name = TraitNames.SemanticDomainDdp4, Value = "1.1" }); + sense.Traits.First().Annotations.Add(new() { Name = "name", Value = "value" }); + + Assert.That(IsProtected(sense), Is.False); + Assert.That(GetProtectedReasons(sense), Is.Empty); + } + + [Test] + public void SenseAnnotationsProtected() + { + var sense = new LiftSense(); + sense.Annotations.Add(new()); + sense.Annotations.Add(new()); + Assert.That(IsProtected(sense), Is.True); + var reasons = GetProtectedReasons(sense); + Assert.That(reasons, Has.Count.EqualTo(1)); + Assert.That(reasons.First().Type, Is.EqualTo(ReasonType.Annotations)); + } + + [Test] + public void SenseExamplesProtected() + { + var sense = new LiftSense(); + sense.Examples.Add(new()); + sense.Examples.Add(new()); + Assert.That(IsProtected(sense), Is.True); + var reasons = GetProtectedReasons(sense); + Assert.That(reasons, Has.Count.EqualTo(1)); + Assert.That(reasons.First().Type, Is.EqualTo(ReasonType.Examples)); + } + + [Test] + public void SenseFieldsProtected() + { + var sense = new LiftSense(); + sense.Fields.Add(new()); + sense.Fields.Add(new()); + Assert.That(IsProtected(sense), Is.True); + var reasons = GetProtectedReasons(sense); + Assert.That(reasons, Has.Count.EqualTo(2)); + Assert.That(reasons.First().Type, Is.EqualTo(ReasonType.Field)); + Assert.That(reasons.Last().Type, Is.EqualTo(ReasonType.Field)); + } + + [Test] + public void SenseGramInfoTraitProtected() + { + var sense = new LiftSense() + { + GramInfo = new() + }; + sense.GramInfo.Traits.Add(new()); + sense.GramInfo.Traits.Add(new()); + Assert.That(IsProtected(sense), Is.True); + var reasons = GetProtectedReasons(sense); + Assert.That(reasons, Has.Count.EqualTo(2)); + Assert.That(reasons.First().Type, Is.EqualTo(ReasonType.GramInfoTrait)); + Assert.That(reasons.Last().Type, Is.EqualTo(ReasonType.GramInfoTrait)); + } + + [Test] + public void SenseIllustrationsProtected() + { + var sense = new LiftSense(); + sense.Illustrations.Add(new()); + sense.Illustrations.Add(new()); + Assert.That(IsProtected(sense), Is.True); + var reasons = GetProtectedReasons(sense); + Assert.That(reasons, Has.Count.EqualTo(1)); + Assert.That(reasons.First().Type, Is.EqualTo(ReasonType.Illustrations)); + } + + [Test] + public void SenseNotesProtected() + { + var sense = new LiftSense(); + sense.Notes.Add(new()); + sense.Notes.Add(new()); + Assert.That(IsProtected(sense), Is.True); + var reasons = GetProtectedReasons(sense); + Assert.That(reasons, Has.Count.EqualTo(1)); + Assert.That(reasons.First().Type, Is.EqualTo(ReasonType.Notes)); + } + + [Test] + public void SenseRelationsProtected() + { + var sense = new LiftSense(); + sense.Relations.Add(new()); + sense.Relations.Add(new()); + Assert.That(IsProtected(sense), Is.True); + var reasons = GetProtectedReasons(sense); + Assert.That(reasons, Has.Count.EqualTo(1)); + Assert.That(reasons.First().Type, Is.EqualTo(ReasonType.Relations)); + } + + [Test] + public void SenseReversalsProtected() + { + var sense = new LiftSense(); + sense.Reversals.Add(new()); + sense.Reversals.Add(new()); + Assert.That(IsProtected(sense), Is.True); + var reasons = GetProtectedReasons(sense); + Assert.That(reasons, Has.Count.EqualTo(2)); + Assert.That(reasons.First().Type, Is.EqualTo(ReasonType.Reversals)); + Assert.That(reasons.Last().Type, Is.EqualTo(ReasonType.Reversals)); + } + + [Test] + public void SenseSubsensesProtected() + { + var sense = new LiftSense(); + sense.Subsenses.Add(new()); + sense.Subsenses.Add(new()); + Assert.That(IsProtected(sense), Is.True); + var reasons = GetProtectedReasons(sense); + Assert.That(reasons, Has.Count.EqualTo(1)); + Assert.That(reasons.First().Type, Is.EqualTo(ReasonType.Subsenses)); + } + } +} diff --git a/Backend/Helper/LiftHelper.cs b/Backend/Helper/LiftHelper.cs index 06c8788bf8..621dbae536 100644 --- a/Backend/Helper/LiftHelper.cs +++ b/Backend/Helper/LiftHelper.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.Serialization; +using BackendFramework.Models; using SIL.Lift.Parsing; namespace BackendFramework.Helper @@ -16,6 +18,25 @@ protected InvalidLiftFileException(SerializationInfo info, StreamingContext cont } + public static class TraitNames + { + public const string AnthroCode = "anthrocode"; + public const string DialectLabels = "dialectlabels"; + public const string DomainType = "domaintype"; + public const string DoNotPublishIn = "donotpublishin"; + public const string DoNotUseForParsing = "donotuseforparsing"; + public const string EntryType = "entrytype"; + public const string ExcludeAsHeadWord = "excludeasheadword"; + public const string MinorEntryCondition = "minorentrycondition"; + public const string MorphType = "morphtype"; + public const string PublishIn = "publishin"; + public const string SemanticDomain = "semanticdomain"; + public const string SemanticDomainDdp4 = "semanticdomainddp4"; + public const string SenseType = "sensetype"; + public const string Status = "status"; + public const string UsageType = "usagetype"; + } + public static class LiftHelper { public static string GetLiftRootFromExtractedZip(string dirPath) @@ -57,28 +78,178 @@ public static string GetLiftRootFromExtractedZip(string dirPath) return extractedLiftRootPath; } - /// - /// Determine if a has any data not handled by The Combine. - /// + /// Determine if a has any data not handled by The Combine. public static bool IsProtected(LiftEntry entry) { return entry.Annotations.Count > 0 || entry.Etymologies.Count > 0 || entry.Fields.Count > 0 || (entry.Notes.Count == 1 && !string.IsNullOrEmpty(entry.Notes.First().Type)) || - entry.Notes.Count > 1 || entry.Pronunciations.Count > 0 || entry.Relations.Count > 0 || - entry.Traits.Any(t => !t.Value.Equals("stem", StringComparison.OrdinalIgnoreCase)) || + entry.Notes.Count > 1 || entry.Relations.Count > 0 || + entry.Traits.Any(t => !t.Value.Equals("stem", StringComparison.OrdinalIgnoreCase) || + !t.Name.Replace("-", "").Equals(TraitNames.MorphType, StringComparison.OrdinalIgnoreCase)) || entry.Variants.Count > 0; } - /// - /// Determine if a has any data not handled by The Combine. - /// + /// Determine what data is not handled by The Combine. + public static List GetProtectedReasons(LiftEntry entry) + { + var reasons = new List(); + if (entry.Annotations.Count > 0) + { + reasons.Add(new() { Type = ReasonType.Annotations, Count = entry.Annotations.Count }); + } + if (entry.Etymologies.Count > 0) + { + reasons.Add(new() { Type = ReasonType.Etymologies, Count = entry.Etymologies.Count }); + } + entry.Fields.ForEach(f => + { + reasons.Add(new() { Type = ReasonType.Field, Value = f.Type }); + }); + if (entry.Notes.Count == 1 && !string.IsNullOrEmpty(entry.Notes.First().Type)) + { + reasons.Add(new() { Type = ReasonType.NoteWithType, Value = entry.Notes.First().Type }); + } + if (entry.Notes.Count > 1) + { + reasons.Add(new() { Type = ReasonType.Notes, Count = entry.Notes.Count }); + } + if (entry.Relations.Count > 0) + { + reasons.Add(new() { Type = ReasonType.Relations, Count = entry.Relations.Count }); + } + entry.Traits.ForEach(t => + { + // FieldWorks > Src/LexText/LexTextControls/LiftMerger.cs > ProcessEntryTraits() + // FieldWorks > Src/LexText/LexTextControls/LiftExporter.cs > RangeNames + switch (t.Name.Replace("-", "").ToLowerInvariant()) + { + case TraitNames.DialectLabels: + reasons.Add(new() { Type = ReasonType.TraitDialectLabels, Value = t.Value }); + break; + case TraitNames.DoNotPublishIn: + reasons.Add(new() { Type = ReasonType.TraitDoNotPublishIn, Value = t.Value }); + break; + case TraitNames.DoNotUseForParsing: + reasons.Add(new() { Type = ReasonType.TraitDoNotUseForParsing, Value = t.Value }); + break; + case TraitNames.EntryType: + reasons.Add(new() { Type = ReasonType.TraitEntryType, Value = t.Value }); + break; + case TraitNames.ExcludeAsHeadWord: + reasons.Add(new() { Type = ReasonType.TraitExcludeAsHeadword }); + break; + case TraitNames.MinorEntryCondition: + reasons.Add(new() { Type = ReasonType.TraitMinorEntryCondition, Value = t.Value }); + break; + case TraitNames.MorphType: + if (!t.Value.Equals("stem", StringComparison.OrdinalIgnoreCase)) + { + reasons.Add(new() { Type = ReasonType.TraitMorphType, Value = t.Value }); + } + break; + case TraitNames.PublishIn: + reasons.Add(new() { Type = ReasonType.TraitPublishIn, Value = t.Value }); + break; + default: + reasons.Add(new() { Type = ReasonType.Trait, Value = $"{t.Name} \"{t.Value}\"" }); + break; + } + }); + if (entry.Variants.Count > 0) + { + reasons.Add(new() { Type = ReasonType.Variants, Count = entry.Variants.Count }); + } + + return reasons; + } + + /// Determine if a has any data not handled by The Combine. public static bool IsProtected(LiftSense sense) { - return sense.Examples.Count > 0 || sense.Fields.Count > 0 || + return sense.Annotations.Count > 0 || sense.Examples.Count > 0 || sense.Fields.Count > 0 || sense.GramInfo is not null && sense.GramInfo.Traits.Count > 0 || sense.Illustrations.Count > 0 || sense.Notes.Count > 0 || sense.Relations.Count > 0 || sense.Reversals.Count > 0 || sense.Subsenses.Count > 0 || - (sense.Traits.Any(t => !t.Name.StartsWith("semantic-domain", StringComparison.OrdinalIgnoreCase))); + sense.Traits.Any( + t => !t.Name.Replace("-", "").StartsWith("semanticdomain", StringComparison.OrdinalIgnoreCase)); + } + + /// Determine what data is not handled by The Combine. + public static List GetProtectedReasons(LiftSense sense) + { + var reasons = new List(); + if (sense.Annotations.Count > 0) + { + reasons.Add(new() { Type = ReasonType.Annotations, Count = sense.Annotations.Count }); + } + if (sense.Examples.Count > 0) + { + reasons.Add(new() { Type = ReasonType.Examples, Count = sense.Examples.Count }); + } + sense.Fields.ForEach(f => + { + reasons.Add(new() { Type = ReasonType.Field, Value = f.Type }); + }); + sense.GramInfo?.Traits.ForEach(t => + { + reasons.Add(new() { Type = ReasonType.GramInfoTrait, Value = t.Name }); + }); + if (sense.Illustrations.Count > 0) + { + reasons.Add(new() { Type = ReasonType.Illustrations, Count = sense.Illustrations.Count }); + } + if (sense.Notes.Count > 0) + { + reasons.Add(new() { Type = ReasonType.Notes, Count = sense.Notes.Count }); + } + if (sense.Relations.Count > 0) + { + reasons.Add(new() { Type = ReasonType.Relations, Count = sense.Relations.Count }); + } + sense.Reversals.ForEach(r => + { + reasons.Add(new() { Type = ReasonType.Reversals, Value = r.Type }); + }); + if (sense.Subsenses.Count > 0) + { + reasons.Add(new() { Type = ReasonType.Subsenses, Count = sense.Subsenses.Count }); + } + sense.Traits.ForEach(t => + { + // FieldWorks > Src/LexText/LexTextControls/LiftMerger.cs > ProcessSenseTraits() + // FieldWorks > Src/LexText/LexTextControls/LiftExporter.cs > RangeNames + switch (t.Name.Replace("-", "").ToLowerInvariant()) + { + case TraitNames.AnthroCode: + reasons.Add(new() { Type = ReasonType.TraitAnthroCode, Value = t.Value }); + break; + case TraitNames.DomainType: + reasons.Add(new() { Type = ReasonType.TraitDomainType, Value = t.Value }); + break; + case TraitNames.DoNotPublishIn: + reasons.Add(new() { Type = ReasonType.TraitDoNotPublishIn, Value = t.Value }); + break; + case TraitNames.PublishIn: + reasons.Add(new() { Type = ReasonType.TraitPublishIn, Value = t.Value }); + break; + case TraitNames.SemanticDomain: + case TraitNames.SemanticDomainDdp4: + break; + case TraitNames.SenseType: + reasons.Add(new() { Type = ReasonType.TraitSenseType, Value = t.Value }); + break; + case TraitNames.Status: + reasons.Add(new() { Type = ReasonType.TraitStatus, Value = t.Value }); + break; + case TraitNames.UsageType: + reasons.Add(new() { Type = ReasonType.TraitUsageType, Value = t.Value }); + break; + default: + reasons.Add(new() { Type = ReasonType.Trait, Value = $"{t.Name} ({t.Value})" }); + break; + } + }); + return reasons; } } } diff --git a/Backend/Models/ProtectReason.cs b/Backend/Models/ProtectReason.cs new file mode 100644 index 0000000000..8af151efbf --- /dev/null +++ b/Backend/Models/ProtectReason.cs @@ -0,0 +1,79 @@ +using System; +using System.ComponentModel.DataAnnotations; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace BackendFramework.Models +{ + public enum ReasonType + { + Annotations, + Etymologies, + Examples, + Field, + GramInfoTrait, + Illustrations, + NoteWithType, + Notes, + Relations, + Reversals, + Subsenses, + Trait, + TraitAnthroCode, + TraitDialectLabels, + TraitDomainType, + TraitDoNotPublishIn, + TraitDoNotUseForParsing, + TraitEntryType, + TraitExcludeAsHeadword, + TraitMinorEntryCondition, + TraitMorphType, + TraitPublishIn, + TraitSenseType, + TraitStatus, + TraitUsageType, + Variants, + }; + + public class ProtectReason + { + [Required] + [BsonElement("type")] + [BsonRepresentation(BsonType.String)] + public ReasonType Type { get; set; } + + [BsonElement("count")] + public int? Count { get; set; } + + [BsonElement("value")] + public string? Value { get; set; } + + public ProtectReason Clone() + { + return new ProtectReason + { + Type = Type, + Count = Count, + Value = Value, + }; + } + + public override bool Equals(object? obj) + { + if (obj is not ProtectReason other || GetType() != obj.GetType()) + { + return false; + } + + return + Type == other.Type && Count == other.Count && + ((Value is null && other.Value is null) || + (Value is not null && Value.Equals(other.Value, StringComparison.Ordinal))); + } + + public override int GetHashCode() + { + return HashCode.Combine(Type, Count, Value); + } + } +} diff --git a/Backend/Models/Sense.cs b/Backend/Models/Sense.cs index e5d7a6e8f1..9f18baad47 100644 --- a/Backend/Models/Sense.cs +++ b/Backend/Models/Sense.cs @@ -37,6 +37,9 @@ public class Sense [BsonElement("Glosses")] public List Glosses { get; set; } + [BsonElement("protectReasons")] + public List ProtectReasons { get; set; } + [Required] [BsonElement("SemanticDomains")] public List SemanticDomains { get; set; } @@ -49,6 +52,7 @@ public Sense() GrammaticalInfo = new GrammaticalInfo(); Definitions = new List(); Glosses = new List(); + ProtectReasons = new List(); SemanticDomains = new List(); } @@ -61,6 +65,7 @@ public Sense Clone() GrammaticalInfo = GrammaticalInfo.Clone(), Definitions = new List(), Glosses = new List(), + ProtectReasons = new List(), SemanticDomains = new List(), }; @@ -72,6 +77,10 @@ public Sense Clone() { clone.Glosses.Add(gloss.Clone()); } + foreach (var reason in ProtectReasons) + { + clone.ProtectReasons.Add(reason.Clone()); + } foreach (var sd in SemanticDomains) { clone.SemanticDomains.Add(sd.Clone()); @@ -95,13 +104,16 @@ public override bool Equals(object? obj) other.Definitions.All(Definitions.Contains) && other.Glosses.Count == Glosses.Count && other.Glosses.All(Glosses.Contains) && + other.ProtectReasons.Count == ProtectReasons.Count && + other.ProtectReasons.All(ProtectReasons.Contains) && other.SemanticDomains.Count == SemanticDomains.Count && other.SemanticDomains.All(SemanticDomains.Contains); } public override int GetHashCode() { - return HashCode.Combine(Guid, Accessibility, GrammaticalInfo, Definitions, Glosses, SemanticDomains); + return HashCode.Combine( + Guid, Accessibility, GrammaticalInfo, Definitions, Glosses, ProtectReasons, SemanticDomains); } public bool IsEmpty() diff --git a/Backend/Models/Word.cs b/Backend/Models/Word.cs index 97cd1838ab..2a5e03e917 100644 --- a/Backend/Models/Word.cs +++ b/Backend/Models/Word.cs @@ -54,6 +54,9 @@ public class Word [BsonRepresentation(BsonType.String)] public Status Accessibility { get; set; } + [BsonElement("protectReasons")] + public List ProtectReasons { get; set; } + [Required] [BsonElement("history")] public List History { get; set; } @@ -94,6 +97,7 @@ public Word() Audio = new List(); EditedBy = new List(); History = new List(); + ProtectReasons = new List(); Senses = new List(); Note = new Note(); Flag = new Flag(); @@ -115,6 +119,7 @@ public Word Clone() Audio = new List(), EditedBy = new List(), History = new List(), + ProtectReasons = new List(), Senses = new List(), Note = Note.Clone(), Flag = Flag.Clone(), @@ -132,6 +137,10 @@ public Word Clone() { clone.History.Add(id); } + foreach (var reason in ProtectReasons) + { + clone.ProtectReasons.Add(reason.Clone()); + } foreach (var sense in Senses) { clone.Senses.Add(sense.Clone()); @@ -151,6 +160,9 @@ public bool ContentEquals(Word other) other.Audio.Count == Audio.Count && other.Audio.All(Audio.Contains) && + other.ProtectReasons.Count == ProtectReasons.Count && + other.ProtectReasons.All(ProtectReasons.Contains) && + other.Senses.Count == Senses.Count && other.Senses.All(Senses.Contains) && @@ -189,6 +201,7 @@ public override int GetHashCode() hash.Add(Created); hash.Add(Modified); hash.Add(Accessibility); + hash.Add(ProtectReasons); hash.Add(History); hash.Add(EditedBy); hash.Add(OtherField); diff --git a/Backend/Services/LiftService.cs b/Backend/Services/LiftService.cs index 6075a40ae8..9acb4a412d 100644 --- a/Backend/Services/LiftService.cs +++ b/Backend/Services/LiftService.cs @@ -742,6 +742,7 @@ public void FinishEntry(LiftEntry entry) if (LiftHelper.IsProtected(entry)) { newWord.Accessibility = Status.Protected; + newWord.ProtectReasons = LiftHelper.GetProtectedReasons(entry); } // Add Note if one exists. @@ -796,6 +797,7 @@ public void FinishEntry(LiftEntry entry) if (LiftHelper.IsProtected(sense)) { newSense.Accessibility = Status.Protected; + newSense.ProtectReasons = LiftHelper.GetProtectedReasons(sense); } // Add definitions diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 5c4f5803e5..a10f5a2708 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -411,8 +411,39 @@ "noDups": "Nothing to merge.", "delete": "Delete sense", "deleteDialog": "Delete this sense?", - "protectedSense": "This sense was imported with data that The Combine doesn't handle, so it cannot be deleted or dropped into another sense. You may still move this sense to another word or drop other senses into this one to merge them.", - "protectedWord": "This word was imported with data that The Combine doesn't handle, so to prevent deletion, its final sense cannot be removed." + "protectedSense": "This sense was imported with data that The Combine doesn't handle.", + "protectedSenseInfo": "This sense cannot be deleted or dropped into another sense. You may still move it to another word or drop other senses into this one to merge them.", + "protectedWord": "This word was imported with data that The Combine doesn't handle.", + "protectedWordInfo": "To prevent deletion, the final sense of this word cannot be removed.", + "protectedData": "Protected data: {{ val }}" + }, + "protectReason": { + "annotations": "annotations", + "etymologies": "etymologies", + "examples": "examples", + "field": "\"{{ val }}\" field", + "gramInfoTrait": "grammatical info \"{{ val }}\"", + "illustrations": "illustrations", + "noteWithType": "note with type \"{{ val }}\"", + "notesSense": "notes", + "notesWord": "more than 1 note", + "relations": "relations", + "reversal": "\"{{ val }}\" reversal", + "subsenses": "subsenses", + "traitAnthroCode": "anthro code \"{{ val }}\"", + "traitDialectLabels": "dialect labels \"{{ val }}\"", + "traitDomainType": "domain type \"{{ val }}\"", + "traitDoNotPublishIn": "do not publish in \"{{ val }}\"", + "traitDoNotUseForParsing": "do not use for parsing \"{{ val }}\"", + "traitEntryType": "entry type \"{{ val }}\"", + "traitExcludeAsHeadword": "exclude as headword", + "traitMinorEntryCondition": "minor entry condition \"{{ val }}\"", + "traitMorphType": "morph type \"{{ val }}\" (rather than default \"stem\")", + "traitPublishIn": "publish in \"{{ val }}\"", + "traitSenseType": "sense type \"{{ val }}\"", + "traitStatus": "status \"{{ val }}\"", + "traitUsageType": "usage type \"{{ val }}\"", + "variants": "variants" }, "completed": { "number": "Number of merges completed: " diff --git a/src/api/.openapi-generator/FILES b/src/api/.openapi-generator/FILES index 58f54943ea..873eb5b3b5 100644 --- a/src/api/.openapi-generator/FILES +++ b/src/api/.openapi-generator/FILES @@ -48,6 +48,8 @@ models/permission.ts models/project-role.ts models/project.ts models/pronunciation.ts +models/protect-reason.ts +models/reason-type.ts models/role.ts models/semantic-domain-count.ts models/semantic-domain-full.ts diff --git a/src/api/models/index.ts b/src/api/models/index.ts index 33539e6fa0..5b140bd273 100644 --- a/src/api/models/index.ts +++ b/src/api/models/index.ts @@ -24,6 +24,8 @@ export * from "./permission"; export * from "./project"; export * from "./project-role"; export * from "./pronunciation"; +export * from "./protect-reason"; +export * from "./reason-type"; export * from "./role"; export * from "./semantic-domain"; export * from "./semantic-domain-count"; diff --git a/src/api/models/protect-reason.ts b/src/api/models/protect-reason.ts new file mode 100644 index 0000000000..be298a97d3 --- /dev/null +++ b/src/api/models/protect-reason.ts @@ -0,0 +1,41 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * BackendFramework + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { ReasonType } from "./reason-type"; + +/** + * + * @export + * @interface ProtectReason + */ +export interface ProtectReason { + /** + * + * @type {ReasonType} + * @memberof ProtectReason + */ + type: ReasonType; + /** + * + * @type {number} + * @memberof ProtectReason + */ + count?: number | null; + /** + * + * @type {string} + * @memberof ProtectReason + */ + value?: string | null; +} diff --git a/src/api/models/reason-type.ts b/src/api/models/reason-type.ts new file mode 100644 index 0000000000..f482af4661 --- /dev/null +++ b/src/api/models/reason-type.ts @@ -0,0 +1,47 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * BackendFramework + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +/** + * + * @export + * @enum {string} + */ +export enum ReasonType { + Annotations = "Annotations", + Etymologies = "Etymologies", + Examples = "Examples", + Field = "Field", + GramInfoTrait = "GramInfoTrait", + Illustrations = "Illustrations", + NoteWithType = "NoteWithType", + Notes = "Notes", + Relations = "Relations", + Reversals = "Reversals", + Subsenses = "Subsenses", + Trait = "Trait", + TraitAnthroCode = "TraitAnthroCode", + TraitDialectLabels = "TraitDialectLabels", + TraitDomainType = "TraitDomainType", + TraitDoNotPublishIn = "TraitDoNotPublishIn", + TraitDoNotUseForParsing = "TraitDoNotUseForParsing", + TraitEntryType = "TraitEntryType", + TraitExcludeAsHeadword = "TraitExcludeAsHeadword", + TraitMinorEntryCondition = "TraitMinorEntryCondition", + TraitMorphType = "TraitMorphType", + TraitPublishIn = "TraitPublishIn", + TraitSenseType = "TraitSenseType", + TraitStatus = "TraitStatus", + TraitUsageType = "TraitUsageType", + Variants = "Variants", +} diff --git a/src/api/models/sense.ts b/src/api/models/sense.ts index d0f585a447..622cd09cf5 100644 --- a/src/api/models/sense.ts +++ b/src/api/models/sense.ts @@ -15,6 +15,7 @@ import { Definition } from "./definition"; import { Gloss } from "./gloss"; import { GrammaticalInfo } from "./grammatical-info"; +import { ProtectReason } from "./protect-reason"; import { SemanticDomain } from "./semantic-domain"; import { Status } from "./status"; @@ -42,6 +43,12 @@ export interface Sense { * @memberof Sense */ grammaticalInfo: GrammaticalInfo; + /** + * + * @type {Array} + * @memberof Sense + */ + protectReasons?: Array | null; /** * * @type {Array} diff --git a/src/api/models/word.ts b/src/api/models/word.ts index 94b4e41c1c..9baf004482 100644 --- a/src/api/models/word.ts +++ b/src/api/models/word.ts @@ -15,6 +15,7 @@ import { Flag } from "./flag"; import { Note } from "./note"; import { Pronunciation } from "./pronunciation"; +import { ProtectReason } from "./protect-reason"; import { Sense } from "./sense"; import { Status } from "./status"; @@ -78,6 +79,12 @@ export interface Word { * @memberof Word */ accessibility: Status; + /** + * + * @type {Array} + * @memberof Word + */ + protectReasons?: Array | null; /** * * @type {Array} diff --git a/src/components/MultilineTooltipTitle/index.tsx b/src/components/MultilineTooltipTitle/index.tsx new file mode 100644 index 0000000000..c4ebf98e88 --- /dev/null +++ b/src/components/MultilineTooltipTitle/index.tsx @@ -0,0 +1,7 @@ +import { ReactElement } from "react"; + +export default function MultilineTooltipTitle(props: { + lines: string[]; +}): ReactElement { + return
{props.lines.join("\n")}
; +} diff --git a/src/components/Pronunciations/AudioPlayer.tsx b/src/components/Pronunciations/AudioPlayer.tsx index e917890e80..533bb2bb71 100644 --- a/src/components/Pronunciations/AudioPlayer.tsx +++ b/src/components/Pronunciations/AudioPlayer.tsx @@ -22,6 +22,7 @@ import { Pronunciation, Speaker } from "api/models"; import { getSpeaker } from "backend"; import { SpeakerMenuList } from "components/AppBar/SpeakerMenu"; import { ButtonConfirmation } from "components/Dialogs"; +import MultilineTooltipTitle from "components/MultilineTooltipTitle"; import { playing, resetPronunciations, @@ -165,13 +166,12 @@ export default function AudioPlayer(props: PlayerProps): ReactElement { ); } - const multilineTooltipText = (lines: string[]): ReactElement => ( -
{lines.join("\n")}
- ); - return ( <> - + } + placement="top" + > state.mergeDuplicateGoal.data ); + const { t } = useTranslation(); + const dispatchFlagWord = (flag: Flag): void => { dispatch(flagWord({ wordId: props.wordId, flag })); }; @@ -148,6 +151,71 @@ export function DropWordCardHeader(
); + const reasonText = (reason: ProtectReason): string => { + // Backend/Helper/LiftHelper.cs > GetProtectedReasons(LiftEntry entry) + switch (reason.type) { + case ReasonType.Annotations: + return t("mergeDups.protectReason.annotations"); + case ReasonType.Etymologies: + return t("mergeDups.protectReason.etymologies"); + case ReasonType.Field: + return t("mergeDups.protectReason.field", { val: reason.value }); + case ReasonType.NoteWithType: + return t("mergeDups.protectReason.noteWithType", { val: reason.value }); + case ReasonType.Notes: + return t("mergeDups.protectReason.notesWord"); + case ReasonType.Relations: + return t("mergeDups.protectReason.relations"); + case ReasonType.Trait: + return reason.value ?? "(unknown trait)"; + case ReasonType.TraitDialectLabels: + return t("mergeDups.protectReason.traitDialectLabels", { + val: reason.value, + }); + case ReasonType.TraitDoNotPublishIn: + return t("mergeDups.protectReason.traitDoNotPublishIn", { + val: reason.value, + }); + case ReasonType.TraitDoNotUseForParsing: + return t("mergeDups.protectReason.traitDoNotUseForParsing", { + val: reason.value, + }); + case ReasonType.TraitEntryType: + return t("mergeDups.protectReason.traitEntryType", { + val: reason.value, + }); + case ReasonType.TraitExcludeAsHeadword: + return t("mergeDups.protectReason.traitExcludeAsHeadword"); + case ReasonType.TraitMinorEntryCondition: + return t("mergeDups.protectReason.traitMinorEntryCondition", { + val: reason.value, + }); + case ReasonType.TraitMorphType: + return t("mergeDups.protectReason.traitMorphType", { + val: reason.value, + }); + case ReasonType.TraitPublishIn: + return t("mergeDups.protectReason.traitPublishIn", { + val: reason.value, + }); + case ReasonType.Variants: + return t("mergeDups.protectReason.variants"); + default: + throw new Error(); + } + }; + + const tooltipTexts = [t("mergeDups.helpText.protectedWord")]; + const reasons = words[props.wordId]?.protectReasons; + if (reasons?.length) { + tooltipTexts.push( + t("mergeDups.helpText.protectedData", { + val: reasons.map(reasonText).join("; "), + }) + ); + } + tooltipTexts.push(t("mergeDups.helpText.protectedWordInfo")); + const headerAction = treeWord ? ( <> {props.protectedWithOneChild && ( @@ -156,7 +224,7 @@ export function DropWordCardHeader( icon={} side="top" size="small" - textId="mergeDups.helpText.protectedWord" + text={} /> )} @@ -27,12 +37,79 @@ export default function SenseCardContent( ) ), ]; - const protectedWarning = - !props.sidebar && props.senses[0].accessibility === Status.Protected; const gramInfo = props.senses .map((s) => s.grammaticalInfo) .find((g) => g.catGroup !== GramCatGroup.Unspecified); + const reasonText = (reason: ProtectReason): string => { + // Backend/Helper/LiftHelper.cs > GetProtectedReasons(LiftSense sense) + switch (reason.type) { + case ReasonType.Annotations: + return t("mergeDups.protectReason.annotations"); + case ReasonType.Examples: + return t("mergeDups.protectReason.examples"); + case ReasonType.Field: + return t("mergeDups.protectReason.field", { val: reason.value }); + case ReasonType.GramInfoTrait: + return t("mergeDups.protectReason.gramInfoTrait", { + val: reason.value, + }); + case ReasonType.Illustrations: + return t("mergeDups.protectReason.illustrations"); + case ReasonType.Notes: + return t("mergeDups.protectReason.notesSense"); + case ReasonType.Relations: + return t("mergeDups.protectReason.relations"); + case ReasonType.Reversals: + return t("mergeDups.protectReason.reversal", { val: reason.value }); + case ReasonType.Subsenses: + return t("mergeDups.protectReason.subsenses"); + case ReasonType.Trait: + return reason.value ?? "(unknown trait)"; + case ReasonType.TraitAnthroCode: + return t("mergeDups.protectReason.traitAnthroCode", { + val: reason.value, + }); + case ReasonType.TraitDomainType: + return t("mergeDups.protectReason.traitDomainType", { + val: reason.value, + }); + case ReasonType.TraitDoNotPublishIn: + return t("mergeDups.protectReason.traitDoNotPublishIn", { + val: reason.value, + }); + case ReasonType.TraitPublishIn: + return t("mergeDups.protectReason.traitPublishIn", { + val: reason.value, + }); + case ReasonType.TraitSenseType: + return t("mergeDups.protectReason.traitSenseType", { + val: reason.value, + }); + case ReasonType.TraitStatus: + return t("mergeDups.protectReason.traitStatus", { val: reason.value }); + case ReasonType.TraitUsageType: + return t("mergeDups.protectReason.traitUsageType", { + val: reason.value, + }); + default: + throw new Error(); + } + }; + + const protectedWarning = + !props.sidebar && props.senses[0].accessibility === Status.Protected; + const tooltipTexts = [t("mergeDups.helpText.protectedSense")]; + const reasons = props.senses[0]?.protectReasons; + if (reasons?.length) { + tooltipTexts.push( + t("mergeDups.helpText.protectedData", { + val: reasons.map(reasonText).join("; "), + }) + ); + } + tooltipTexts.push(t("mergeDups.helpText.protectedSenseInfo")); + return ( {/* Icon for part of speech (if any). */} @@ -52,7 +129,7 @@ export default function SenseCardContent( icon={} side="top" size="small" - textId="mergeDups.helpText.protectedSense" + text={} buttonId={`sense-${props.senses[0].guid}-protected`} /> )} From 9a8c0a5f599f61d1aafaead950080a745d1396ea Mon Sep 17 00:00:00 2001 From: Jim Grady Date: Mon, 5 Feb 2024 10:41:53 -0500 Subject: [PATCH 02/20] Add instructions for installing updated Semantic Domain data (#1747) --- docs/deploy/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/deploy/README.md b/docs/deploy/README.md index 160bb61f8d..b9ee813630 100644 --- a/docs/deploy/README.md +++ b/docs/deploy/README.md @@ -386,6 +386,14 @@ Where: - The help text for `setup_combine.py` says that the `--tag` is optional and its default value is `latest`. That is used in the _Development Environment_ scenario; there are no images for _The Combine's_ components in `public.ecr.aws/thecombine` with the tag `latest`. + - The database image contains a script that will initialize the `SemanticDomains` and the `SemanticDomainTree` + collections on _first use_ of the database. The script will not be run automatically when the database is restarted + or updated. If the Semantic Domain data are updated, for example, adding a new language, then the script needs to be + rerun manually: + + ```console + kubectl -n thecombine exec deployment/database -- /docker-entrypoint-initdb.d/update-semantic-domains.sh + ``` ## Maintenance From a796a123cca65c4d049b6479046a88ebd5fb46bb Mon Sep 17 00:00:00 2001 From: "D. Ror" Date: Tue, 6 Feb 2024 10:07:27 -0500 Subject: [PATCH 03/20] Disable CodeCov status checks (#2935) --- codecov.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/codecov.yml b/codecov.yml index 1ccd5457ca..c596c3aef7 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,3 +1,7 @@ +coverage: + status: + patch: off + project: off ignore: - "src/api" - "src/backend/index.ts" From 9f2d5c677b1445382947eafb5e09fff5e2869900 Mon Sep 17 00:00:00 2001 From: Jim Grady Date: Tue, 6 Feb 2024 10:21:37 -0500 Subject: [PATCH 04/20] Dependabot updates for February 2024 (#2934) * Bump follow-redirects from 1.15.3 to 1.15.5 * Bump mongo from 7.0.4-jammy to 7.0.5-jammy in /database * Bump step-security/harden-runner from 2.6.1 to 2.7.0 * Bump actions/upload-artifact from 3.1.3 to 4.3.0 * Bump github/codeql-action from 3.22.12 to 3.23.2 * Bump FedericoCarboni/setup-ffmpeg from 2 to 3 * Bump @typescript-eslint/parser from 6.11.0 to 6.20.0 * Bump dotnet/aspnet in /Backend * Bump @types/react from 18.2.46 to 18.2.51 * Bump dotnet/sdk in /Backend * Bump @mui/icons-material from 5.14.19 to 5.15.7 * Bump react-i18next from 13.5.0 to 14.0.1 * Update frontend license report * Update backend packages and license report * Update Python dependencies * Update unit test snapshots for new MUI Icons --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/backend.yml | 20 +-- .github/workflows/codeql.yml | 8 +- .github/workflows/combine_deploy_image.yml | 2 +- .github/workflows/database.yml | 2 +- .github/workflows/deploy_qa.yml | 4 +- .github/workflows/deploy_release.yml | 2 +- .github/workflows/frontend.yml | 10 +- .github/workflows/maintenance.yml | 2 +- .github/workflows/pages.yml | 2 +- .github/workflows/python.yml | 2 +- .github/workflows/scorecards.yml | 6 +- Backend/BackendFramework.csproj | 16 +- Backend/Dockerfile | 4 +- database/Dockerfile | 2 +- deploy/requirements.txt | 20 +-- dev-requirements.txt | 44 ++--- .../assets/licenses/backend_licenses.txt | 44 +++-- .../assets/licenses/frontend_licenses.txt | 10 +- maintenance/requirements.txt | 14 +- package-lock.json | 153 ++++++++++++++---- package.json | 8 +- .../tests/__snapshots__/index.test.tsx.snap | 24 +-- 22 files changed, 245 insertions(+), 154 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 092abda41f..ed4b9a6754 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -19,7 +19,7 @@ jobs: # See https://docs.stepsecurity.io/harden-runner/getting-started/ for instructions on # configuring harden-runner and identifying allowed endpoints. - name: Harden Runner - uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 + uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 with: disable-sudo: true egress-policy: block @@ -41,14 +41,14 @@ jobs: with: dotnet-version: ${{ matrix.dotnet }} - name: Install ffmpeg - uses: FedericoCarboni/setup-ffmpeg@583042d32dd1cabb8bd09df03bde06080da5c87c # v2 + uses: FedericoCarboni/setup-ffmpeg@36c6454b5a2348e7794ba2d82a21506605921e3d # v3 # Coverage. - name: Run coverage tests run: dotnet test Backend.Tests/Backend.Tests.csproj shell: bash - name: Upload coverage artifact - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 + uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 with: if-no-files-found: error name: coverage @@ -72,7 +72,7 @@ jobs: # See https://docs.stepsecurity.io/harden-runner/getting-started/ for instructions on # configuring harden-runner and identifying allowed endpoints. - name: Harden Runner - uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 + uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 with: disable-sudo: true egress-policy: block @@ -106,7 +106,7 @@ jobs: # See https://docs.stepsecurity.io/harden-runner/getting-started/ for instructions on # configuring harden-runner and identifying allowed endpoints. - name: Harden Runner - uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 + uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 with: disable-sudo: true egress-policy: block @@ -129,19 +129,19 @@ jobs: with: dotnet-version: "6.0.x" - name: Initialize CodeQL - uses: github/codeql-action/init@012739e5082ff0c22ca6d6ab32e07c36df03c4a4 # v3.22.12 + uses: github/codeql-action/init@b7bf0a3ed3ecfa44160715d7c442788f65f0f923 # v3.23.2 with: languages: csharp - name: Autobuild - uses: github/codeql-action/autobuild@012739e5082ff0c22ca6d6ab32e07c36df03c4a4 # v3.22.12 + uses: github/codeql-action/autobuild@b7bf0a3ed3ecfa44160715d7c442788f65f0f923 # v3.23.2 - name: Upload artifacts if build failed - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 if: ${{ failure() }} with: name: tracer-logs path: ${{ runner.temp }}/*.log - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@012739e5082ff0c22ca6d6ab32e07c36df03c4a4 # v3.22.12 + uses: github/codeql-action/analyze@b7bf0a3ed3ecfa44160715d7c442788f65f0f923 # v3.23.2 docker_build: runs-on: ubuntu-22.04 @@ -150,7 +150,7 @@ jobs: # See https://docs.stepsecurity.io/harden-runner/getting-started/ for instructions on # configuring harden-runner and identifying allowed endpoints. - name: Harden Runner - uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 + uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 with: disable-sudo: true disable-file-monitoring: true diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index b4c9befb15..f45ffe75d0 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -45,7 +45,7 @@ jobs: # See https://docs.stepsecurity.io/harden-runner/getting-started/ for instructions on # configuring harden-runner and identifying allowed endpoints. - name: Harden Runner - uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 + uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 with: disable-sudo: true egress-policy: block @@ -63,7 +63,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@012739e5082ff0c22ca6d6ab32e07c36df03c4a4 # v3.22.12 + uses: github/codeql-action/init@b7bf0a3ed3ecfa44160715d7c442788f65f0f923 # v3.23.2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -76,7 +76,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@012739e5082ff0c22ca6d6ab32e07c36df03c4a4 # v3.22.12 + uses: github/codeql-action/autobuild@b7bf0a3ed3ecfa44160715d7c442788f65f0f923 # v3.23.2 # Command-line programs to run using the OS shell. # See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -89,6 +89,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@012739e5082ff0c22ca6d6ab32e07c36df03c4a4 # v3.22.12 + uses: github/codeql-action/analyze@b7bf0a3ed3ecfa44160715d7c442788f65f0f923 # v3.23.2 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/combine_deploy_image.yml b/.github/workflows/combine_deploy_image.yml index ab7fa38548..f0936540ee 100644 --- a/.github/workflows/combine_deploy_image.yml +++ b/.github/workflows/combine_deploy_image.yml @@ -16,7 +16,7 @@ jobs: # See https://docs.stepsecurity.io/harden-runner/getting-started/ for instructions on # configuring harden-runner and identifying allowed endpoints. - name: Harden Runner - uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 + uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 with: disable-sudo: true egress-policy: block diff --git a/.github/workflows/database.yml b/.github/workflows/database.yml index d2cfcef1c5..d7db237f82 100644 --- a/.github/workflows/database.yml +++ b/.github/workflows/database.yml @@ -15,7 +15,7 @@ jobs: # See https://docs.stepsecurity.io/harden-runner/getting-started/ for instructions on # configuring harden-runner and identifying allowed endpoints. - name: Harden Runner - uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 + uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 with: disable-sudo: true egress-policy: block diff --git a/.github/workflows/deploy_qa.yml b/.github/workflows/deploy_qa.yml index 798856c832..d81f5e834f 100644 --- a/.github/workflows/deploy_qa.yml +++ b/.github/workflows/deploy_qa.yml @@ -21,7 +21,7 @@ jobs: # See https://docs.stepsecurity.io/harden-runner/getting-started/ for instructions on # configuring harden-runner and identifying allowed endpoints. - name: Harden Runner - uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 + uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 with: disable-sudo: true egress-policy: block @@ -73,7 +73,7 @@ jobs: # See https://docs.stepsecurity.io/harden-runner/getting-started/ for instructions on # configuring harden-runner and identifying allowed endpoints. - name: Harden Runner - uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 + uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 with: disable-sudo: true egress-policy: block diff --git a/.github/workflows/deploy_release.yml b/.github/workflows/deploy_release.yml index 32846ca004..c347da065d 100644 --- a/.github/workflows/deploy_release.yml +++ b/.github/workflows/deploy_release.yml @@ -20,7 +20,7 @@ jobs: # See https://docs.stepsecurity.io/harden-runner/getting-started/ for instructions on # configuring harden-runner and identifying allowed endpoints. - name: Harden Runner - uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 + uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 with: egress-policy: block allowed-endpoints: > diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index 59e46d2538..9e6e596d96 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -19,7 +19,7 @@ jobs: # See https://docs.stepsecurity.io/harden-runner/getting-started/ for instructions on # configuring harden-runner and identifying allowed endpoints. - name: Harden Runner - uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 + uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 with: disable-sudo: true egress-policy: block @@ -48,7 +48,7 @@ jobs: # See https://docs.stepsecurity.io/harden-runner/getting-started/ for instructions on # configuring harden-runner and identifying allowed endpoints. - name: Harden Runner - uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 + uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 with: disable-sudo: true egress-policy: block @@ -68,7 +68,7 @@ jobs: env: CI: true - name: Upload coverage artifact - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 + uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 with: if-no-files-found: error name: coverage @@ -82,7 +82,7 @@ jobs: # See https://docs.stepsecurity.io/harden-runner/getting-started/ for instructions on # configuring harden-runner and identifying allowed endpoints. - name: Harden Runner - uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 + uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 with: disable-sudo: true egress-policy: block @@ -113,7 +113,7 @@ jobs: # See https://docs.stepsecurity.io/harden-runner/getting-started/ for instructions on # configuring harden-runner and identifying allowed endpoints. - name: Harden Runner - uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 + uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 with: disable-sudo: true egress-policy: block diff --git a/.github/workflows/maintenance.yml b/.github/workflows/maintenance.yml index 9fdba9dffe..b274f2ccbe 100644 --- a/.github/workflows/maintenance.yml +++ b/.github/workflows/maintenance.yml @@ -15,7 +15,7 @@ jobs: # See https://docs.stepsecurity.io/harden-runner/getting-started/ for instructions on # configuring harden-runner and identifying allowed endpoints. - name: Harden Runner - uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 + uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 with: disable-sudo: true egress-policy: block diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index d80c67bda2..5538494d03 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -17,7 +17,7 @@ jobs: # See https://docs.stepsecurity.io/harden-runner/getting-started/ for instructions on # configuring harden-runner and identifying allowed endpoints. - name: Harden Runner - uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 + uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 with: disable-sudo: true egress-policy: block diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index a8fd9340a0..d1a9844c50 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -19,7 +19,7 @@ jobs: # See https://docs.stepsecurity.io/harden-runner/getting-started/ for instructions on # configuring harden-runner and identifying allowed endpoints. - name: Harden Runner - uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 + uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 with: disable-sudo: true egress-policy: block diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index e8f021a3e8..1db19cbece 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -35,7 +35,7 @@ jobs: # See https://docs.stepsecurity.io/harden-runner/getting-started/ for instructions on # configuring harden-runner and identifying allowed endpoints. - name: Harden Runner - uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 + uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 with: disable-sudo: true egress-policy: block @@ -81,7 +81,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 with: name: SARIF file path: results.sarif @@ -89,6 +89,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@012739e5082ff0c22ca6d6ab32e07c36df03c4a4 # v3.22.12 + uses: github/codeql-action/upload-sarif@b7bf0a3ed3ecfa44160715d7c442788f65f0f923 # v3.23.2 with: sarif_file: results.sarif diff --git a/Backend/BackendFramework.csproj b/Backend/BackendFramework.csproj index a73af2fb4f..5d38158308 100644 --- a/Backend/BackendFramework.csproj +++ b/Backend/BackendFramework.csproj @@ -13,9 +13,9 @@ NU1701 - - - + + + @@ -24,16 +24,16 @@ - - + + NU1701 - + NU1701 - + NU1701 - + diff --git a/Backend/Dockerfile b/Backend/Dockerfile index 18033d45be..543e74bbc4 100644 --- a/Backend/Dockerfile +++ b/Backend/Dockerfile @@ -1,5 +1,5 @@ # Docker multi-stage build -FROM mcr.microsoft.com/dotnet/sdk:6.0.417-1-focal-amd64 AS builder +FROM mcr.microsoft.com/dotnet/sdk:6.0.418-focal-amd64 AS builder WORKDIR /app # Copy csproj and restore (fetch dependencies) as distinct layers. @@ -11,7 +11,7 @@ COPY . ./ RUN dotnet publish -c Release -o build # Build runtime image. -FROM mcr.microsoft.com/dotnet/aspnet:6.0.25-focal-amd64 +FROM mcr.microsoft.com/dotnet/aspnet:6.0.26-focal-amd64 ENV ASPNETCORE_URLS=http://+:5000 ENV COMBINE_IS_IN_CONTAINER=1 diff --git a/database/Dockerfile b/database/Dockerfile index bcb1b9079f..ba10046a1e 100644 --- a/database/Dockerfile +++ b/database/Dockerfile @@ -1,4 +1,4 @@ -FROM mongo:7.0.4-jammy +FROM mongo:7.0.5-jammy WORKDIR / diff --git a/deploy/requirements.txt b/deploy/requirements.txt index 952d4ec8f4..94f1f9e83e 100644 --- a/deploy/requirements.txt +++ b/deploy/requirements.txt @@ -4,13 +4,13 @@ # # pip-compile requirements.in # -ansible==9.1.0 +ansible==9.2.0 # via -r requirements.in -ansible-core==2.16.2 +ansible-core==2.16.3 # via ansible cachetools==5.3.2 # via google-auth -certifi==2023.11.17 +certifi==2024.2.2 # via # kubernetes # requests @@ -18,24 +18,24 @@ cffi==1.16.0 # via cryptography charset-normalizer==3.3.2 # via requests -cryptography==41.0.7 +cryptography==42.0.2 # via # ansible-core # pyopenssl -google-auth==2.25.2 +google-auth==2.27.0 # via kubernetes idna==3.6 # via requests -jinja2==3.1.2 +jinja2==3.1.3 # via # -r requirements.in # ansible-core # jinja2-base64-filters jinja2-base64-filters==0.1.4 # via -r requirements.in -kubernetes==28.1.0 +kubernetes==29.0.0 # via -r requirements.in -markupsafe==2.1.3 +markupsafe==2.1.5 # via jinja2 oauthlib==3.2.2 # via @@ -51,7 +51,7 @@ pyasn1-modules==0.3.0 # via google-auth pycparser==2.21 # via cffi -pyopenssl==23.3.0 +pyopenssl==24.0.0 # via -r requirements.in python-dateutil==2.8.2 # via kubernetes @@ -74,7 +74,7 @@ six==1.16.0 # via # kubernetes # python-dateutil -urllib3==1.26.18 +urllib3==2.2.0 # via # kubernetes # requests diff --git a/dev-requirements.txt b/dev-requirements.txt index 3bd12d13a5..56a041eb2f 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -10,15 +10,15 @@ attrs==23.2.0 # flake8-eradicate babel==2.14.0 # via mkdocs-material -beautifulsoup4==4.12.2 +beautifulsoup4==4.12.3 # via mkdocs-htmlproofer-plugin -black==23.12.1 +black==24.1.1 # via -r dev-requirements.in cachetools==5.3.2 # via # google-auth # tox -certifi==2023.11.17 +certifi==2024.2.2 # via # kubernetes # requests @@ -37,13 +37,13 @@ colorama==0.4.6 # -r dev-requirements.in # mkdocs-material # tox -cryptography==41.0.7 +cryptography==42.0.2 # via # pyopenssl # types-pyopenssl distlib==0.3.8 # via virtualenv -dnspython==2.4.2 +dnspython==2.5.0 # via pymongo eradicate==2.3.0 # via flake8-eradicate @@ -51,7 +51,7 @@ filelock==3.13.1 # via # tox # virtualenv -flake8==6.1.0 +flake8==7.0.0 # via # -r dev-requirements.in # flake8-broken-line @@ -61,7 +61,7 @@ flake8==6.1.0 # pep8-naming flake8-broken-line==1.0.0 # via -r dev-requirements.in -flake8-bugbear==23.12.2 +flake8-bugbear==24.1.17 # via -r dev-requirements.in flake8-comprehensions==3.14.0 # via -r dev-requirements.in @@ -69,7 +69,7 @@ flake8-eradicate==1.5.0 # via -r dev-requirements.in ghp-import==2.1.0 # via mkdocs -google-auth==2.25.2 +google-auth==2.27.0 # via kubernetes humanfriendly==10.0 # via -r dev-requirements.in @@ -77,7 +77,7 @@ idna==3.6 # via requests isort==5.13.2 # via -r dev-requirements.in -jinja2==3.1.2 +jinja2==3.1.3 # via # -r dev-requirements.in # jinja2-base64-filters @@ -85,15 +85,15 @@ jinja2==3.1.2 # mkdocs-material jinja2-base64-filters==0.1.4 # via -r dev-requirements.in -kubernetes==27.2.0 +kubernetes==29.0.0 # via -r dev-requirements.in -markdown==3.5.1 +markdown==3.5.2 # via # mkdocs # mkdocs-htmlproofer-plugin # mkdocs-material # pymdown-extensions -markupsafe==2.1.3 +markupsafe==2.1.5 # via # jinja2 # mkdocs @@ -108,7 +108,7 @@ mkdocs==1.5.3 # mkdocs-static-i18n mkdocs-htmlproofer-plugin==1.0.0 # via -r dev-requirements.in -mkdocs-material==9.5.3 +mkdocs-material==9.5.7 # via -r dev-requirements.in mkdocs-material-extensions==1.3.1 # via mkdocs-material @@ -138,13 +138,13 @@ pathspec==0.12.1 # mkdocs pep8-naming==0.13.3 # via -r dev-requirements.in -platformdirs==4.1.0 +platformdirs==4.2.0 # via # black # mkdocs # tox # virtualenv -pluggy==1.3.0 +pluggy==1.4.0 # via tox pyasn1==0.5.1 # via @@ -156,7 +156,7 @@ pycodestyle==2.11.1 # via flake8 pycparser==2.21 # via cffi -pyflakes==3.1.0 +pyflakes==3.2.0 # via flake8 pygments==2.17.2 # via mkdocs-material @@ -164,7 +164,7 @@ pymdown-extensions==10.7 # via mkdocs-material pymongo==4.6.1 # via -r dev-requirements.in -pyopenssl==23.3.0 +pyopenssl==24.0.0 # via -r dev-requirements.in pyproject-api==1.6.1 # via tox @@ -207,21 +207,21 @@ tomli==2.0.1 # mypy # pyproject-api # tox -tox==4.11.4 +tox==4.12.1 # via -r dev-requirements.in -types-pyopenssl==23.3.0.0 +types-pyopenssl==24.0.0.20240130 # via -r dev-requirements.in -types-python-dateutil==2.8.19.14 +types-python-dateutil==2.8.19.20240106 # via -r dev-requirements.in types-pyyaml==6.0.12.12 # via -r dev-requirements.in -types-requests==2.31.0.20231231 +types-requests==2.31.0.20240125 # via -r dev-requirements.in typing-extensions==4.9.0 # via # black # mypy -urllib3==2.1.0 +urllib3==2.2.0 # via # kubernetes # requests diff --git a/docs/user_guide/assets/licenses/backend_licenses.txt b/docs/user_guide/assets/licenses/backend_licenses.txt index dd224bebc7..dddd26599d 100644 --- a/docs/user_guide/assets/licenses/backend_licenses.txt +++ b/docs/user_guide/assets/licenses/backend_licenses.txt @@ -97,11 +97,11 @@ license Type: #################################################################################################### Package:Microsoft.AspNetCore.Authentication.JwtBearer -Version:6.0.25 +Version:7.0.3 project URL:https://asp.net/ Description:ASP.NET Core middleware that enables an application to receive an OpenID Connect bearer token. -This package was built from the source code at https://github.com/dotnet/aspnetcore/tree/36be7ed6d6d56b7da0a2891e3de7ecc2aa48eecd +This package was built from the source code at https://github.com/dotnet/aspnetcore/tree/febee99db845fd8766a13bdb391a07c3ee90b4ba licenseUrl:https://licenses.nuget.org/MIT license Type:MIT @@ -596,7 +596,7 @@ license Type:Apache-2.0 #################################################################################################### Package:Microsoft.IdentityModel.Abstractions -Version:6.34.0 +Version:6.35.0 project URL:https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet Description:A package containing thin abstractions for Microsoft.IdentityModel. licenseUrl:https://licenses.nuget.org/MIT @@ -604,7 +604,7 @@ license Type:MIT #################################################################################################### Package:Microsoft.IdentityModel.JsonWebTokens -Version:6.15.0 +Version:6.15.1 project URL:https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet Description:Includes types that provide support for creating, serializing and validating JSON Web Tokens. licenseUrl:https://licenses.nuget.org/MIT @@ -612,7 +612,7 @@ license Type:MIT #################################################################################################### Package:Microsoft.IdentityModel.JsonWebTokens -Version:6.34.0 +Version:6.35.0 project URL:https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet Description:Includes types that provide support for creating, serializing and validating JSON Web Tokens. licenseUrl:https://licenses.nuget.org/MIT @@ -620,15 +620,7 @@ license Type:MIT #################################################################################################### Package:Microsoft.IdentityModel.Logging -Version:6.10.0 -project URL:https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet -Description:Includes Event Source based logging support. -licenseUrl:https://licenses.nuget.org/MIT -license Type:MIT - -#################################################################################################### -Package:Microsoft.IdentityModel.Logging -Version:6.15.0 +Version:6.15.1 project URL:https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet Description:Includes Event Source based logging support. licenseUrl:https://licenses.nuget.org/MIT @@ -636,7 +628,7 @@ license Type:MIT #################################################################################################### Package:Microsoft.IdentityModel.Logging -Version:6.34.0 +Version:6.35.0 project URL:https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet Description:Includes Event Source based logging support. licenseUrl:https://licenses.nuget.org/MIT @@ -644,7 +636,7 @@ license Type:MIT #################################################################################################### Package:Microsoft.IdentityModel.Protocols -Version:6.10.0 +Version:6.15.1 project URL:https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet Description:Provides base protocol support for OpenIdConnect and WsFederation. licenseUrl:https://licenses.nuget.org/MIT @@ -652,7 +644,7 @@ license Type:MIT #################################################################################################### Package:Microsoft.IdentityModel.Protocols.OpenIdConnect -Version:6.10.0 +Version:6.15.1 project URL:https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet Description:Includes types that provide support for OpenIdConnect protocol. licenseUrl:https://licenses.nuget.org/MIT @@ -660,7 +652,7 @@ license Type:MIT #################################################################################################### Package:Microsoft.IdentityModel.Tokens -Version:6.15.0 +Version:6.15.1 project URL:https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet Description:Includes types that provide support for SecurityTokens, Cryptographic operations: Signing, Verifying Signatures, Encryption. licenseUrl:https://licenses.nuget.org/MIT @@ -668,7 +660,7 @@ license Type:MIT #################################################################################################### Package:Microsoft.IdentityModel.Tokens -Version:6.34.0 +Version:6.35.0 project URL:https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet Description:Includes types that provide support for SecurityTokens, Cryptographic operations: Signing, Verifying Signatures, Encryption. licenseUrl:https://licenses.nuget.org/MIT @@ -989,7 +981,7 @@ license Type: #################################################################################################### Package:SIL.Core -Version:13.0.0 +Version:13.0.1 project URL:https://github.com/sillsdev/libpalaso Description:SIL.Core provides general utilities for language software. It is the base library for all Palaso libraries. licenseUrl:https://licenses.nuget.org/MIT @@ -997,7 +989,7 @@ license Type:MIT #################################################################################################### Package:SIL.Core.Desktop -Version:13.0.0 +Version:13.0.1 project URL:https://github.com/sillsdev/libpalaso Description:SIL.Core.Desktop provides general UI related utilities for language software. licenseUrl:https://licenses.nuget.org/MIT @@ -1005,7 +997,7 @@ license Type:MIT #################################################################################################### Package:SIL.DictionaryServices -Version:13.0.0 +Version:13.0.1 project URL:https://github.com/sillsdev/libpalaso Description:SIL.DictionaryServices contains classes for defining a simple lexical model that can be used across applications. licenseUrl:https://licenses.nuget.org/MIT @@ -1013,7 +1005,7 @@ license Type:MIT #################################################################################################### Package:SIL.Lift -Version:13.0.0 +Version:13.0.1 project URL:https://github.com/sillsdev/libpalaso Description:SIL.Lift contains classes for reading and writing Lexicon Interchange FormaT (LIFT) data. This assembly currently supports LIFT 0.13. licenseUrl:https://licenses.nuget.org/MIT @@ -1021,7 +1013,7 @@ license Type:MIT #################################################################################################### Package:SIL.WritingSystems -Version:13.0.0 +Version:13.0.1 project URL:https://github.com/sillsdev/libpalaso Description:SIL.WritingSystems contains classes for managing and persisting writing systems using the Locale Data Markup Language (LDML) format. This library also contains classes for processing IETF (BCP-47) language tags and accessing the SIL Locale Data Repository (SLDR). licenseUrl:https://licenses.nuget.org/MIT @@ -1734,7 +1726,7 @@ license Type:MS-EULA #################################################################################################### Package:System.IdentityModel.Tokens.Jwt -Version:6.15.0 +Version:6.15.1 project URL:https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet Description:Includes types that provide support for creating, serializing and validating JSON Web Tokens. licenseUrl:https://licenses.nuget.org/MIT @@ -1742,7 +1734,7 @@ license Type:MIT #################################################################################################### Package:System.IdentityModel.Tokens.Jwt -Version:6.34.0 +Version:6.35.0 project URL:https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet Description:Includes types that provide support for creating, serializing and validating JSON Web Tokens. licenseUrl:https://licenses.nuget.org/MIT diff --git a/docs/user_guide/assets/licenses/frontend_licenses.txt b/docs/user_guide/assets/licenses/frontend_licenses.txt index 6a4858e759..407737934e 100644 --- a/docs/user_guide/assets/licenses/frontend_licenses.txt +++ b/docs/user_guide/assets/licenses/frontend_licenses.txt @@ -128,7 +128,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -@babel/runtime 7.23.5 +@babel/runtime 7.23.9 MIT MIT License @@ -1188,7 +1188,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -@mui/icons-material 5.14.19 +@mui/icons-material 5.15.7 MIT The MIT License (MIT) @@ -40572,7 +40572,7 @@ MIT SOFTWARE -@types/react 18.2.46 +@types/react 18.2.51 MIT MIT License @@ -43336,11 +43336,11 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -react-i18next 13.5.0 +react-i18next 14.0.1 MIT The MIT License (MIT) -Copyright (c) 2023 i18next +Copyright (c) 2024 i18next Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/maintenance/requirements.txt b/maintenance/requirements.txt index fb89f18995..dfba730e59 100644 --- a/maintenance/requirements.txt +++ b/maintenance/requirements.txt @@ -6,7 +6,7 @@ # cachetools==5.3.2 # via google-auth -certifi==2023.11.17 +certifi==2024.2.2 # via # kubernetes # requests @@ -14,17 +14,17 @@ cffi==1.16.0 # via cryptography charset-normalizer==3.3.2 # via requests -cryptography==41.0.7 +cryptography==42.0.2 # via pyopenssl -dnspython==2.4.2 +dnspython==2.5.0 # via pymongo -google-auth==2.25.2 +google-auth==2.27.0 # via kubernetes humanfriendly==10.0 # via -r requirements.in idna==3.6 # via requests -kubernetes==28.1.0 +kubernetes==29.0.0 # via -r requirements.in oauthlib==3.2.2 # via @@ -40,7 +40,7 @@ pycparser==2.21 # via cffi pymongo==4.6.1 # via -r requirements.in -pyopenssl==23.3.0 +pyopenssl==24.0.0 # via -r requirements.in python-dateutil==2.8.2 # via kubernetes @@ -58,7 +58,7 @@ six==1.16.0 # via # kubernetes # python-dateutil -urllib3==1.26.18 +urllib3==2.2.0 # via # kubernetes # requests diff --git a/package-lock.json b/package-lock.json index 98fa6e5948..92b76bcae8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@material-table/core": "^6.3.0", "@matt-block/react-recaptcha-v2": "^2.0.1", "@microsoft/signalr": "^8.0.0", - "@mui/icons-material": "^5.14.19", + "@mui/icons-material": "^5.15.7", "@mui/material": "^5.14.16", "@mui/x-date-pickers": "^6.18.7", "@redux-devtools/extension": "^3.2.5", @@ -42,7 +42,7 @@ "react-beautiful-dnd": "^13.1.1", "react-chartjs-2": "^5.2.0", "react-dom": "^18.2.0", - "react-i18next": "^13.5.0", + "react-i18next": "^14.0.1", "react-modal": "^3.16.1", "react-redux": "^8.1.3", "react-router-dom": "^6.16.0", @@ -66,7 +66,7 @@ "@types/loadable__component": "^5.13.8", "@types/node": "^20.10.6", "@types/nspell": "^2.1.5", - "@types/react": "^18.2.46", + "@types/react": "^18.2.51", "@types/react-beautiful-dnd": "^13.1.4", "@types/react-dom": "^18.2.15", "@types/react-modal": "^3.16.0", @@ -77,7 +77,7 @@ "@types/uuid": "^9.0.4", "@types/validator": "^13.11.1", "@typescript-eslint/eslint-plugin": "^6.7.2", - "@typescript-eslint/parser": "^6.3.0", + "@typescript-eslint/parser": "^6.20.0", "css-mediaquery": "^0.1.2", "eslint": "^8.51.0", "eslint-import-resolver-typescript": "^3.6.0", @@ -2375,9 +2375,9 @@ "dev": true }, "node_modules/@babel/runtime": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.5.tgz", - "integrity": "sha512-NdUTHcPe4C99WxPub+K9l9tK5/lV4UXIoaHSYgzco9BCyjKAAwzdBI+wWtYqHt7LJdbo74ZjRPJgzVweq1sz0w==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", + "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -4508,11 +4508,11 @@ } }, "node_modules/@mui/icons-material": { - "version": "5.14.19", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.14.19.tgz", - "integrity": "sha512-yjP8nluXxZGe3Y7pS+yxBV+hWZSsSBampCxkZwaw+1l+feL+rfP74vbEFbMrX/Kil9I/Y1tWfy5bs/eNvwNpWw==", + "version": "5.15.7", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.7.tgz", + "integrity": "sha512-EDAc8TVJGIA/imAvR3u4nANl2W5h3QeHieu2gK7Ypez/nIA55p08tHjf8UrMXEpxCAvfZO6piY9S9uaxETdicA==", "dependencies": { - "@babel/runtime": "^7.23.4" + "@babel/runtime": "^7.23.9" }, "engines": { "node": ">=12.0.0" @@ -8740,9 +8740,9 @@ "dev": true }, "node_modules/@types/react": { - "version": "18.2.46", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.46.tgz", - "integrity": "sha512-nNCvVBcZlvX4NU1nRRNV/mFl1nNRuTuslAJglQsq+8ldXe5Xv0Wd2f7WTE3jOxhLH2BFfiZGC6GCp+kHQbgG+w==", + "version": "18.2.51", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.51.tgz", + "integrity": "sha512-XeoMaU4CzyjdRr3c4IQQtiH7Rpo18V07rYZUucEZQwOUEtGgTXv7e6igQiQ+xnV6MbMe1qjEmKdgMNnfppnXfg==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -9166,15 +9166,15 @@ "dev": true }, "node_modules/@typescript-eslint/parser": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.11.0.tgz", - "integrity": "sha512-+whEdjk+d5do5nxfxx73oanLL9ghKO3EwM9kBCkUtWMRwWuPaFv9ScuqlYfQ6pAD6ZiJhky7TZ2ZYhrMsfMxVQ==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.20.0.tgz", + "integrity": "sha512-bYerPDF/H5v6V76MdMYhjwmwgMA+jlPVqjSDq2cRqMi8bP5sR3Z+RLOiOMad3nsnmDVmn2gAFCyNgh/dIrfP/w==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.11.0", - "@typescript-eslint/types": "6.11.0", - "@typescript-eslint/typescript-estree": "6.11.0", - "@typescript-eslint/visitor-keys": "6.11.0", + "@typescript-eslint/scope-manager": "6.20.0", + "@typescript-eslint/types": "6.20.0", + "@typescript-eslint/typescript-estree": "6.20.0", + "@typescript-eslint/visitor-keys": "6.20.0", "debug": "^4.3.4" }, "engines": { @@ -9193,6 +9193,90 @@ } } }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.20.0.tgz", + "integrity": "sha512-p4rvHQRDTI1tGGMDFQm+GtxP1ZHyAh64WANVoyEcNMpaTFn3ox/3CcgtIlELnRfKzSs/DwYlDccJEtr3O6qBvA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.20.0", + "@typescript-eslint/visitor-keys": "6.20.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.20.0.tgz", + "integrity": "sha512-MM9mfZMAhiN4cOEcUOEx+0HmuaW3WBfukBZPCfwSqFnQy0grXYtngKCqpQN339X3RrwtzspWJrpbrupKYUSBXQ==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.20.0.tgz", + "integrity": "sha512-RnRya9q5m6YYSpBN7IzKu9FmLcYtErkDkc8/dKv81I9QiLLtVBHrjz+Ev/crAqgMNW2FCsoZF4g2QUylMnJz+g==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.20.0", + "@typescript-eslint/visitor-keys": "6.20.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.20.0.tgz", + "integrity": "sha512-E8Cp98kRe4gKHjJD4NExXKz/zOJ1A2hhZc+IMVD6i7w4yjIvh6VyuRI0gRtxAsXtoC35uGMaQ9rjI2zJaXDEAw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.20.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/@typescript-eslint/parser/node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -9210,6 +9294,21 @@ } } }, + "node_modules/@typescript-eslint/parser/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@typescript-eslint/parser/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -14576,9 +14675,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", "funding": [ { "type": "individual", @@ -23212,9 +23311,9 @@ "dev": true }, "node_modules/react-i18next": { - "version": "13.5.0", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-13.5.0.tgz", - "integrity": "sha512-CFJ5NDGJ2MUyBohEHxljOq/39NQ972rh1ajnadG9BjTk+UXbHLq4z5DKEbEQBDoIhUmmbuS/fIMJKo6VOax1HA==", + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.0.1.tgz", + "integrity": "sha512-TMV8hFismBmpMdIehoFHin/okfvgjFhp723RYgIqB4XyhDobVMyukyM3Z8wtTRmajyFMZrBl/OaaXF2P6WjUAw==", "dependencies": { "@babel/runtime": "^7.22.5", "html-parse-stringify": "^3.0.1" diff --git a/package.json b/package.json index 97af8c6156..56d6151bb5 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "@material-table/core": "^6.3.0", "@matt-block/react-recaptcha-v2": "^2.0.1", "@microsoft/signalr": "^8.0.0", - "@mui/icons-material": "^5.14.19", + "@mui/icons-material": "^5.15.7", "@mui/material": "^5.14.16", "@mui/x-date-pickers": "^6.18.7", "@redux-devtools/extension": "^3.2.5", @@ -70,7 +70,7 @@ "react-beautiful-dnd": "^13.1.1", "react-chartjs-2": "^5.2.0", "react-dom": "^18.2.0", - "react-i18next": "^13.5.0", + "react-i18next": "^14.0.1", "react-modal": "^3.16.1", "react-redux": "^8.1.3", "react-router-dom": "^6.16.0", @@ -94,7 +94,7 @@ "@types/loadable__component": "^5.13.8", "@types/node": "^20.10.6", "@types/nspell": "^2.1.5", - "@types/react": "^18.2.46", + "@types/react": "^18.2.51", "@types/react-beautiful-dnd": "^13.1.4", "@types/react-dom": "^18.2.15", "@types/react-modal": "^3.16.0", @@ -105,7 +105,7 @@ "@types/uuid": "^9.0.4", "@types/validator": "^13.11.1", "@typescript-eslint/eslint-plugin": "^6.7.2", - "@typescript-eslint/parser": "^6.3.0", + "@typescript-eslint/parser": "^6.20.0", "css-mediaquery": "^0.1.2", "eslint": "^8.51.0", "eslint-import-resolver-typescript": "^3.6.0", diff --git a/src/components/TreeView/TreeDepiction/tests/__snapshots__/index.test.tsx.snap b/src/components/TreeView/TreeDepiction/tests/__snapshots__/index.test.tsx.snap index c04e2652ff..c9c09dff1c 100644 --- a/src/components/TreeView/TreeDepiction/tests/__snapshots__/index.test.tsx.snap +++ b/src/components/TreeView/TreeDepiction/tests/__snapshots__/index.test.tsx.snap @@ -710,7 +710,7 @@ Array [ viewBox="0 0 24 24" >
@@ -800,7 +800,7 @@ Array [ viewBox="0 0 24 24" > @@ -890,7 +890,7 @@ Array [ viewBox="0 0 24 24" > @@ -1340,7 +1340,7 @@ Array [ viewBox="0 0 24 24" > @@ -1714,7 +1714,7 @@ Array [ viewBox="0 0 24 24" > @@ -2692,7 +2692,7 @@ Array [ viewBox="0 0 24 24" > @@ -2782,7 +2782,7 @@ Array [ viewBox="0 0 24 24" > @@ -2872,7 +2872,7 @@ Array [ viewBox="0 0 24 24" > @@ -2962,7 +2962,7 @@ Array [ viewBox="0 0 24 24" > @@ -4029,7 +4029,7 @@ Array [ viewBox="0 0 24 24" > @@ -4119,7 +4119,7 @@ Array [ viewBox="0 0 24 24" > @@ -4209,7 +4209,7 @@ Array [ viewBox="0 0 24 24" > From 8c9b9632a2248973b00e3f08717a20e811cd6733 Mon Sep 17 00:00:00 2001 From: "D. Ror" Date: Tue, 6 Feb 2024 17:06:53 -0500 Subject: [PATCH 05/20] Stop audio/flag/note summary acting like a button (#2895) --- .../AnnouncementBanner/AnnouncementBanner.tsx | 2 +- src/components/Buttons/FlagButton.tsx | 12 +++--- .../EntryCellComponents/EntryNote.tsx | 39 +++++++++---------- src/components/WordCard/index.tsx | 20 ++++++---- src/types/theme.ts | 8 ++-- 5 files changed, 41 insertions(+), 40 deletions(-) diff --git a/src/components/AnnouncementBanner/AnnouncementBanner.tsx b/src/components/AnnouncementBanner/AnnouncementBanner.tsx index 5db0fffa96..124c375440 100644 --- a/src/components/AnnouncementBanner/AnnouncementBanner.tsx +++ b/src/components/AnnouncementBanner/AnnouncementBanner.tsx @@ -47,7 +47,7 @@ export default function AnnouncementBanner(): ReactElement { } return banner ? ( - + diff --git a/src/components/Buttons/FlagButton.tsx b/src/components/Buttons/FlagButton.tsx index 941b13c544..b759db86d3 100644 --- a/src/components/Buttons/FlagButton.tsx +++ b/src/components/Buttons/FlagButton.tsx @@ -1,10 +1,9 @@ import { Flag as FlagFilled, FlagOutlined } from "@mui/icons-material"; -import { Fragment, ReactElement, useEffect, useState } from "react"; +import { Fragment, type ReactElement, useEffect, useState } from "react"; -import { Flag } from "api/models"; +import { type Flag } from "api/models"; import { IconButtonWithTooltip } from "components/Buttons"; import { DeleteEditTextDialog } from "components/Dialogs"; -import { themeColors } from "types/theme"; interface FlagButtonProps { flag: Flag; @@ -12,6 +11,7 @@ interface FlagButtonProps { updateFlag?: (flag: Flag) => void; } +/** A flag adding/editing/viewing button */ export default function FlagButton(props: FlagButtonProps): ReactElement { const [open, setOpen] = useState(false); const [active, setActive] = useState(); @@ -43,7 +43,7 @@ export default function FlagButton(props: FlagButtonProps): ReactElement { + t.palette.error.main }} /> ) : props.updateFlag ? ( ) : ( @@ -53,9 +53,7 @@ export default function FlagButton(props: FlagButtonProps): ReactElement { text={text} textId={active ? "flags.edit" : "flags.add"} size="small" - onClick={ - props.updateFlag ? () => setOpen(true) : active ? () => {} : undefined - } + onClick={props.updateFlag ? () => setOpen(true) : undefined} buttonId={props.buttonId ?? "flag-button"} side="top" /> diff --git a/src/components/DataEntry/DataEntryTable/EntryCellComponents/EntryNote.tsx b/src/components/DataEntry/DataEntryTable/EntryCellComponents/EntryNote.tsx index b23fdadb9e..52991fb655 100644 --- a/src/components/DataEntry/DataEntryTable/EntryCellComponents/EntryNote.tsx +++ b/src/components/DataEntry/DataEntryTable/EntryCellComponents/EntryNote.tsx @@ -1,8 +1,7 @@ import { AddComment, Comment } from "@mui/icons-material"; -import { IconButton, Tooltip } from "@mui/material"; -import { ReactElement, useState } from "react"; -import { useTranslation } from "react-i18next"; +import { type ReactElement, useState } from "react"; +import { IconButtonWithTooltip } from "components/Buttons"; import { EditTextDialog } from "components/Dialogs"; interface EntryNoteProps { @@ -11,29 +10,27 @@ interface EntryNoteProps { updateNote?: (newText: string) => void | Promise; } -/** - * A note adding/editing button - */ +/** A note adding/editing/viewing button */ export default function EntryNote(props: EntryNoteProps): ReactElement { const [noteOpen, setNoteOpen] = useState(false); - const { t } = useTranslation(); - - const handleClick = (): void => { - if (props.updateNote) { - setNoteOpen(true); - } - }; return ( <> - - - {props.noteText ? : } - - + t.palette.grey[700] }} /> + ) : ( + t.palette.grey[700] }} /> + ) + } + onClick={props.updateNote ? () => setNoteOpen(true) : undefined} + side="top" + size="small" + text={props.noteText} + textId="addWords.addNote" + /> - + t.palette.grey[300], minWidth: "200px" }} + > + {/* Vernacular */} {word.vernacular} @@ -65,9 +66,9 @@ export default function WordCard(props: WordCardProps): ReactElement { buttonId={buttonIdFull(word.id)} icon={ full ? ( - + t.palette.grey[900] }} /> ) : ( - + t.palette.grey[600] }} /> ) } onClick={() => setFull(!full)} @@ -132,9 +133,12 @@ export default function WordCard(props: WordCardProps): ReactElement { export function AudioSummary(props: { count: number }): ReactElement { return props.count > 0 ? ( - - - + + t.palette.common.black }} + > + t.palette.success.main }} /> ) : ( diff --git a/src/types/theme.ts b/src/types/theme.ts index a8e839f6b4..dbc574d677 100644 --- a/src/types/theme.ts +++ b/src/types/theme.ts @@ -11,9 +11,9 @@ export type HEX = `#${string}`; export const themeColors: { [key: string]: HEX } = { primary: blue[600], secondary: grey[200], - error: red[600], - warn: orange[300], - success: green[600], + error: red[600], // also audio Record icons + warning: orange[300], + success: green[600], // also audio Play icons highlight: yellow[200], lightShade: blue[700], // AppBarTypes.ts darkShade: blue[900], // AppBarTypes.ts @@ -26,6 +26,8 @@ const palette: PaletteOptions = { primary: { main: themeColors.primary }, secondary: { main: themeColors.secondary }, error: { main: themeColors.error }, + warning: { main: themeColors.warning }, + success: { main: themeColors.success }, background: { default: themeColors.secondary }, contrastThreshold: 3, tonalOffset: 0.2, From e3363ca4846f03e6d3b696be20e0ad5321477049 Mon Sep 17 00:00:00 2001 From: "D. Ror" Date: Tue, 6 Feb 2024 17:12:13 -0500 Subject: [PATCH 06/20] Retire unused UI strings from translation file (#2905) --- public/locales/en/translation.json | 38 +++--------------------------- 1 file changed, 3 insertions(+), 35 deletions(-) diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index a10f5a2708..e579df56cd 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -29,11 +29,9 @@ "addNote": "Add a note", "domain": "Domain", "domainTitle": "Domain: {{ val1 }} ({{ val2 }})", - "gloss": "Gloss", "glosses": "Glosses", "pressEnter": "Press enter to save word", - "vernacular": "Vernacular", - "wordInDatabase": "This word is already in the database" + "vernacular": "Vernacular" }, "appBar": { "dataEntry": "Data Entry", @@ -61,12 +59,10 @@ "usernameRequirements": "Username must be 3 characters or longer.", "usernameTaken": "Username not available.", "emailTaken": "Email address not available.", - "loggingIn": "Logging in...", "required": "Required", "confirmPassword": "Confirm Password", "confirmPasswordError": "Passwords do not match.", - "email": "Email", - "emailError": "Please enter a valid email." + "email": "Email" }, "passwordReset": { "invalidURL": "Password Reset link is invalid.", @@ -150,7 +146,6 @@ "users": "Users" }, "language": { - "header": "Language", "vernacular": "Vernacular", "vernacularLanguage": "Vernacular Language", "analysis": "Analysis", @@ -209,7 +204,6 @@ "delete": "Delete this speaker", "edit": "Edit speaker's name", "consent": { - "play": "Listen to the audio consent for this speaker", "add": "Add consent for this speaker", "record": "Record audio consent", "view": "View this speaker's image consent", @@ -223,7 +217,6 @@ "header": "Import Data", "body": "Imported data will be added to this project. The Combine will make no attempt to deduplicate, overwrite, or sync.", "chooseFile": "Choose File", - "done": "Done!", "notAllowed": "You have already imported a Lift file to this project." }, "archive": { @@ -249,10 +242,6 @@ "on": "On", "hint": "In Data Entry, suggest existing Vernaculars similar to the Vernacular being typed." }, - "definitions": { - "label": "Display Definitions", - "hint": "In Review Entries, show a Definitions column; in Merge Duplicates, show the Definition below the Gloss." - }, "invite": { "inviteByEmailLabel": "Invite by Email", "userExists": "This user is already registered.", @@ -260,14 +249,12 @@ "searchPlaceholder": "Search...", "searchTitle": "Find Users", "emailLabel": "Email", - "messageLabel": "Message", "toastSuccess": "User added to the project.", "toastFail": "Failed to add user to the project." } }, "goal": { "selector": { - "selectOption": "Select this goal", "past": "You've completed:", "present": "Our recommendation:", "other": "Other options:", @@ -297,7 +284,6 @@ }, "reviewEntries": { "title": "Review Entries", - "sense": "Sense", "noVernacular": "No vernacular input!", "noDefinition": "No definitions input", "noGloss": "No glosses input", @@ -362,18 +348,11 @@ "characterSet": { "title": "Vernacular Character Set:", "help": "Define the characters that are valid for this language", - "addButton": "Add Chars", - "addButtonTitle": "Add the characters in the box to the character set", - "deleteButton": "Delete Selected", - "deleteButtonTitle": "Remove the selected characters from the character set", "acceptedCharacters": "Accepted Characters", "rejectedCharacters": "Rejected Characters", - "noCharacters": "No characters yet! Type characters into the box below to get started.", - "required": "Add characters by entering them in this box", "advanced": "Advanced", "occurrences": "{{ val }} occurrences", "find": "Find", - "replace": "Replace", "replaceWith": "Replace with", "replaceAll": "Replace all occurrences of: {{ val }}", "replaceAllWith": "Replace with: {{ val }}", @@ -381,12 +360,6 @@ "findAndReplaceError": "Error processing: find ( {{ val1 }} ), replace ( {{ val2 }} )", "apply": "apply" }, - "sampleWords": { - "title": "Sample Words", - "description": "Here are some words that don't fit your current character set:", - "ignore": "Ignore this word for now", - "add": "Add characters in this word to the set" - }, "dialog": { "title": "Unsaved changes", "content": "Discard your changes to the character set?", @@ -402,12 +375,8 @@ "title": "Merge Duplicates", "helpText": { "dragCard": "Drag a card here to merge", - "root": "Drag words here to start merging them", - "dups": "Drag duplicate words here", - "sense": "Drag new sense here", "saveAndContinue": "Save changes and load a new set of words", "defer": "Discard changes and load a new set of words", - "list": "Drag this word to the right to start merging it with other words", "noDups": "Nothing to merge.", "delete": "Delete sense", "deleteDialog": "Delete this sense?", @@ -463,8 +432,7 @@ "add": "Add flag", "edit": "Edit flag", "remove": "Remove flag", - "save": "Save flag", - "text": "Text: " + "save": "Save flag" }, "buttons": { "accept": "Accept", From 81dd2704a6f5d5f30f5a83776fd31d136f7dd687 Mon Sep 17 00:00:00 2001 From: "D. Ror" Date: Thu, 8 Feb 2024 11:36:20 -0500 Subject: [PATCH 07/20] Prevent Vern/Sense Dialog button invisible overflow (#2942) --- .../DataEntry/DataEntryTable/NewEntry/StyledMenuItem.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/DataEntry/DataEntryTable/NewEntry/StyledMenuItem.ts b/src/components/DataEntry/DataEntryTable/NewEntry/StyledMenuItem.ts index 706d038a8d..5670c4a1f3 100644 --- a/src/components/DataEntry/DataEntryTable/NewEntry/StyledMenuItem.ts +++ b/src/components/DataEntry/DataEntryTable/NewEntry/StyledMenuItem.ts @@ -12,6 +12,7 @@ const StyledMenuItem = styled(MenuItem)(({ theme }) => ({ color: theme.palette.common.white, }, }, + overflow: "clip", })); export default StyledMenuItem; From c2635bab301d7a9fc3a3ccdcb5e7ab3a63df379a Mon Sep 17 00:00:00 2001 From: "D. Ror" Date: Mon, 12 Feb 2024 10:42:05 -0500 Subject: [PATCH 08/20] Cleanup `.find`/`.findIndex`/`.every`/`.some` usage (#2889) This primarily fixes the handful of places we use .find or .findIndex where .every or .some is more appropriate. --- src/backend/index.ts | 10 ++----- src/components/AppBar/AppBarTypes.ts | 2 +- .../DataEntryTable/NewEntry/SenseDialog.tsx | 2 +- .../DataEntryTable/NewEntry/VernDialog.tsx | 4 +-- .../DataEntry/DataEntryTable/index.tsx | 27 +++++++++---------- .../DataEntryTable/tests/index.test.tsx | 2 +- src/components/DataEntry/utilities.ts | 4 +-- .../ProjectSchedule/DateScheduleEdit.tsx | 2 +- .../ProjectSchedule/ProjectPickersDay.tsx | 14 +++++----- .../ProjectSettings/ProjectSelect.tsx | 2 +- .../ProjectSettings/tests/SettingsTabTypes.ts | 2 +- .../CharacterDetail/CharacterWords.tsx | 2 +- .../Redux/CharacterInventoryReducer.ts | 4 +-- .../MergeDuplicates/MergeDupsCompleted.tsx | 2 +- .../MergeDuplicates/Redux/MergeDupsReducer.ts | 18 ++++++------- .../Redux/tests/MergeDupsReducer.test.tsx | 2 +- src/types/word.ts | 2 +- 17 files changed, 45 insertions(+), 56 deletions(-) diff --git a/src/backend/index.ts b/src/backend/index.ts index 373019ed22..398dbb5aac 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -65,14 +65,8 @@ axiosInstance.interceptors.response.use(undefined, (err: AxiosError) => { status >= StatusCodes.BAD_REQUEST && status <= StatusCodes.NETWORK_AUTHENTICATION_REQUIRED ) { - // Suppress error pop-ups for URLs the frontend already explicitly - // handles. - if ( - url !== undefined && - whiteListedErrorUrls.some((whiteListedUrl) => - url.endsWith(whiteListedUrl) - ) - ) { + // Suppress error pop-ups for URLs the frontend already explicitly handles. + if (url && whiteListedErrorUrls.some((u) => url.endsWith(u))) { return Promise.reject(err); } diff --git a/src/components/AppBar/AppBarTypes.ts b/src/components/AppBar/AppBarTypes.ts index 33696b9a58..92d5d99eb7 100644 --- a/src/components/AppBar/AppBarTypes.ts +++ b/src/components/AppBar/AppBarTypes.ts @@ -9,7 +9,7 @@ export interface TabProps { } export function tabColor(currentTab: Path, tabName: Path): string { - return currentTab.indexOf(tabName) !== -1 + return currentTab.indexOf(tabName) > -1 ? themeColors.darkShade : themeColors.lightShade; } diff --git a/src/components/DataEntry/DataEntryTable/NewEntry/SenseDialog.tsx b/src/components/DataEntry/DataEntryTable/NewEntry/SenseDialog.tsx index eec313cd2c..5e5d5a9cce 100644 --- a/src/components/DataEntry/DataEntryTable/NewEntry/SenseDialog.tsx +++ b/src/components/DataEntry/DataEntryTable/NewEntry/SenseDialog.tsx @@ -57,7 +57,7 @@ interface SenseListProps { export function SenseList(props: SenseListProps): ReactElement { const { t } = useTranslation(); - const hasPartsOfSpeech = !!props.selectedWord.senses.find( + const hasPartsOfSpeech = props.selectedWord.senses.some( (s) => s.grammaticalInfo.catGroup !== GramCatGroup.Unspecified ); diff --git a/src/components/DataEntry/DataEntryTable/NewEntry/VernDialog.tsx b/src/components/DataEntry/DataEntryTable/NewEntry/VernDialog.tsx index cfb5b89de0..7f9d3db566 100644 --- a/src/components/DataEntry/DataEntryTable/NewEntry/VernDialog.tsx +++ b/src/components/DataEntry/DataEntryTable/NewEntry/VernDialog.tsx @@ -57,8 +57,8 @@ interface VernListProps { export function VernList(props: VernListProps): ReactElement { const { t } = useTranslation(); - const hasPartsOfSpeech = !!props.vernacularWords.find((w) => - w.senses.find( + const hasPartsOfSpeech = props.vernacularWords.some((w) => + w.senses.some( (s) => s.grammaticalInfo.catGroup !== GramCatGroup.Unspecified ) ); diff --git a/src/components/DataEntry/DataEntryTable/index.tsx b/src/components/DataEntry/DataEntryTable/index.tsx index 5b80b340c9..3a3cb6b38b 100644 --- a/src/components/DataEntry/DataEntryTable/index.tsx +++ b/src/components/DataEntry/DataEntryTable/index.tsx @@ -89,7 +89,7 @@ export function addSemanticDomainToSense( if (!sense) { throw new Error("Word has no sense with specified guid"); } - if (sense.semanticDomains.find((s) => s.id == semDom.id)) { + if (sense.semanticDomains.some((s) => s.id == semDom.id)) { return word; } sense.semanticDomains.push(makeSemDomCurrent(semDom)); @@ -302,7 +302,7 @@ export default function DataEntryTable( const switchSense = useCallback( (oldGuid: string, newGuid: string): void => { const entry = state.recentWords.find((w) => w.senseGuid === oldGuid); - if (!entry || !entry.word.senses.find((s) => s.guid === newGuid)) { + if (!entry || entry.word.senses.every((s) => s.guid !== newGuid)) { return; } setState((prevState) => { @@ -325,7 +325,7 @@ export default function DataEntryTable( setState((prevState) => { const recentWords = [...prevState.recentWords]; word.senses.forEach((s) => { - if (s.semanticDomains.find((dom) => dom.id === domId)) { + if (s.semanticDomains.some((dom) => dom.id === domId)) { recentWords.push({ word, senseGuid: s.guid }); } }); @@ -455,8 +455,8 @@ export default function DataEntryTable( const soloSense = selectedDup?.senses.length === 1 ? selectedDup.senses[0] : undefined; const emptySense = - soloSense?.definitions.find((d) => d.text) || - soloSense?.glosses.find((g) => g.def) + soloSense?.definitions.some((d) => d.text) || + soloSense?.glosses.some((g) => g.def) ? undefined : soloSense; @@ -509,7 +509,7 @@ export default function DataEntryTable( if (!oldEntry) { return; } - if (!oldEntry.word.senses.find((s) => s.guid === newGuid)) { + if (oldEntry.word.senses.every((s) => s.guid !== newGuid)) { return; } switchSense(oldGuid, newGuid); @@ -530,7 +530,7 @@ export default function DataEntryTable( // and only keep Retire ids if they are still in the display. if ( prevState.defunctWordIds[id] === DefunctStatus.Recent || - prevState.recentWords.find((w) => w.word.id === id) + prevState.recentWords.some((w) => w.word.id === id) ) { defunctWordIds[id] = DefunctStatus.Retire; } @@ -571,7 +571,7 @@ export default function DataEntryTable( return; } const oldId = ids.find((id) => - state.recentWords.find((w) => w.word.id === id) + state.recentWords.some((w) => w.word.id === id) ); if (oldId) { // Do an update if there's one to be done. @@ -655,8 +655,7 @@ export default function DataEntryTable( audio: Pronunciation[], oldId: string ): Promise => { - const isInDisplay = - state.recentWords.findIndex((w) => w.word.id === oldId) > -1; + const isInDisplay = state.recentWords.some((w) => w.word.id === oldId); defunctWord(oldId); const newWord = await backend.updateDuplicate(oldId, word); @@ -800,7 +799,7 @@ export default function DataEntryTable( // If selected sense already has this domain, add audio without updating first. if ( oldSense.glosses[0].def === state.newGloss && - oldSense.semanticDomains.find((d) => d.id === semDom.id) + oldSense.semanticDomains.some((d) => d.id === semDom.id) ) { enqueueSnackbar( t("addWords.senseInWord", { @@ -831,7 +830,7 @@ export default function DataEntryTable( // Otherwise, if new gloss matches a sense, update that sense. for (const sense of oldWord.senses) { if (sense.glosses?.length && sense.glosses[0].def === state.newGloss) { - if (sense.semanticDomains.find((d) => d.id === semDom.id)) { + if (sense.semanticDomains.some((d) => d.id === semDom.id)) { // User is trying to add a sense that already exists. enqueueSnackbar( t("addWords.senseInWord", { @@ -955,8 +954,8 @@ export default function DataEntryTable( // If a sense with a new guid was added, it needs to replace the old sense in the display. if (newWord.senses.length > oldEntry.word.senses.length) { - const newSense = newWord.senses.find( - (sense) => !oldEntry.word.senses.find((s) => s.guid === sense.guid) + const newSense = newWord.senses.find((sense) => + oldEntry.word.senses.every((s) => s.guid !== sense.guid) ); if (newSense) { queueSenseSwitch(oldEntry.senseGuid, newSense.guid); diff --git a/src/components/DataEntry/DataEntryTable/tests/index.test.tsx b/src/components/DataEntry/DataEntryTable/tests/index.test.tsx index 32244a77a1..baf6075ee7 100644 --- a/src/components/DataEntry/DataEntryTable/tests/index.test.tsx +++ b/src/components/DataEntry/DataEntryTable/tests/index.test.tsx @@ -429,7 +429,7 @@ describe("DataEntryTable", () => { // Confirm the semantic domain was added. const wordUpdated: Word = mockUpdateWord.mock.calls[0][0]; const doms = wordUpdated.senses[0].semanticDomains; - expect(doms.find((d) => d.id === mockSemDomId)).toBeTruthy(); + expect(doms.some((d) => d.id === mockSemDomId)).toBeTruthy(); }); }); }); diff --git a/src/components/DataEntry/utilities.ts b/src/components/DataEntry/utilities.ts index 1d641ae770..21874e3d4e 100644 --- a/src/components/DataEntry/utilities.ts +++ b/src/components/DataEntry/utilities.ts @@ -5,7 +5,7 @@ import { DomainWord } from "types/word"; * (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)) && + (!domainId || sense.semanticDomains.some((d) => d.id === domainId)) && // The undefined is for Statuses created before .accessibility was required. [Status.Active, Status.Protected, undefined].includes(sense.accessibility) ); @@ -18,7 +18,7 @@ export function filterWordsWithSenses( domainId?: string ): Word[] { return words.filter((w) => - w.senses.find((s) => isActiveInDomain(s, domainId)) + w.senses.some((s) => isActiveInDomain(s, domainId)) ); } diff --git a/src/components/ProjectSettings/ProjectSchedule/DateScheduleEdit.tsx b/src/components/ProjectSettings/ProjectSchedule/DateScheduleEdit.tsx index 5c4a39c394..5094f5a671 100644 --- a/src/components/ProjectSettings/ProjectSchedule/DateScheduleEdit.tsx +++ b/src/components/ProjectSettings/ProjectSchedule/DateScheduleEdit.tsx @@ -47,7 +47,7 @@ export default function DateScheduleEdit( d.getMonth() === date.getMonth() && d.getDate() === date.getDate() ); - if (index >= 0) { + if (index > -1) { schedule.splice(index, 1); } else { schedule.push(date); diff --git a/src/components/ProjectSettings/ProjectSchedule/ProjectPickersDay.tsx b/src/components/ProjectSettings/ProjectSchedule/ProjectPickersDay.tsx index 8681b71967..8f0e6c5b6e 100644 --- a/src/components/ProjectSettings/ProjectSchedule/ProjectPickersDay.tsx +++ b/src/components/ProjectSettings/ProjectSchedule/ProjectPickersDay.tsx @@ -16,14 +16,12 @@ export default function ProjectPickersDay( ): ReactElement { const { days, ...pickersDayProps } = props; const date = pickersDayProps.day.toDate(); - const selected = days - ? days.findIndex( - (d) => - d.getDate() === date.getDate() && - d.getMonth() === date.getMonth() && - d.getFullYear() === date.getFullYear() - ) > -1 - : false; + const selected = days?.some( + (d) => + d.getDate() === date.getDate() && + d.getMonth() === date.getMonth() && + d.getFullYear() === date.getFullYear() + ); return ; } diff --git a/src/components/ProjectSettings/ProjectSelect.tsx b/src/components/ProjectSettings/ProjectSelect.tsx index 8e789d28af..54d291e1a9 100644 --- a/src/components/ProjectSettings/ProjectSelect.tsx +++ b/src/components/ProjectSettings/ProjectSelect.tsx @@ -30,7 +30,7 @@ export default function ProjectSelect( // This prevents an out-of-range Select error while useEffect is underway. const projectList = [...projList]; - if (!projectList.find((p) => p.name === props.project.name)) { + if (projectList.every((p) => p.name !== props.project.name)) { projectList.push(props.project); } projectList.sort((a: Project, b: Project) => a.name.localeCompare(b.name)); diff --git a/src/components/ProjectSettings/tests/SettingsTabTypes.ts b/src/components/ProjectSettings/tests/SettingsTabTypes.ts index b9acc22b6d..9ba6fd7cfc 100644 --- a/src/components/ProjectSettings/tests/SettingsTabTypes.ts +++ b/src/components/ProjectSettings/tests/SettingsTabTypes.ts @@ -72,7 +72,7 @@ function tabHasSomeSetting( tab: ProjectSettingsTab, settings: Setting[] ): boolean { - return settingsByTab[tab].findIndex((s) => settings.includes(s)) !== -1; + return settingsByTab[tab].some((s) => settings.includes(s)); } /** Given a project permission `perm` and a boolean `hasSchedule` (indicating whether any diff --git a/src/goals/CharacterInventory/CharInv/CharacterDetail/CharacterWords.tsx b/src/goals/CharacterInventory/CharInv/CharacterDetail/CharacterWords.tsx index bb56bebaf4..f2a2f6cc66 100644 --- a/src/goals/CharacterInventory/CharInv/CharacterDetail/CharacterWords.tsx +++ b/src/goals/CharacterInventory/CharInv/CharacterDetail/CharacterWords.tsx @@ -44,7 +44,7 @@ function getWordsContainingChar( ): string[] { const wordsWithChar: string[] = []; for (const word of words) { - if (word.indexOf(character) !== -1 && !wordsWithChar.includes(word)) { + if (word.indexOf(character) > -1 && !wordsWithChar.includes(word)) { wordsWithChar.push(word); if (wordsWithChar.length === maxCount) { break; diff --git a/src/goals/CharacterInventory/Redux/CharacterInventoryReducer.ts b/src/goals/CharacterInventory/Redux/CharacterInventoryReducer.ts index f40f7b0ce6..f48ec4f166 100644 --- a/src/goals/CharacterInventory/Redux/CharacterInventoryReducer.ts +++ b/src/goals/CharacterInventory/Redux/CharacterInventoryReducer.ts @@ -16,7 +16,7 @@ const characterInventorySlice = createSlice({ } const index = state.validCharacters.findIndex((c) => c == action.payload); - if (index !== -1) { + if (index > -1) { state.validCharacters.splice(index, 1); } @@ -39,7 +39,7 @@ const characterInventorySlice = createSlice({ const index = state.rejectedCharacters.findIndex( (c) => c == action.payload ); - if (index !== -1) { + if (index > -1) { state.rejectedCharacters.splice(index, 1); } diff --git a/src/goals/MergeDuplicates/MergeDupsCompleted.tsx b/src/goals/MergeDuplicates/MergeDupsCompleted.tsx index 802bdeaa2c..bd794dda97 100644 --- a/src/goals/MergeDuplicates/MergeDupsCompleted.tsx +++ b/src/goals/MergeDuplicates/MergeDupsCompleted.tsx @@ -100,7 +100,7 @@ export function doWordsIncludeMerges( // The undo operation will fail if any of the children are in the frontier. return ( merge.parentIds.every((id) => wordIds.includes(id)) && - !merge.childIds.some((id) => wordIds.includes(id)) + merge.childIds.every((id) => !wordIds.includes(id)) ); } diff --git a/src/goals/MergeDuplicates/Redux/MergeDupsReducer.ts b/src/goals/MergeDuplicates/Redux/MergeDupsReducer.ts index 55de7981f0..527856e4ee 100644 --- a/src/goals/MergeDuplicates/Redux/MergeDupsReducer.ts +++ b/src/goals/MergeDuplicates/Redux/MergeDupsReducer.ts @@ -100,9 +100,8 @@ const mergeDuplicatesSlice = createSlice({ const nonDeletedSenses = Object.values(state.tree.words).flatMap((w) => Object.values(w.sensesGuids).flatMap((s) => s) ); - const deletedWords = possibleWords.filter( - (w) => - !w.senses.map((s) => s.guid).find((g) => nonDeletedSenses.includes(g)) + const deletedWords = possibleWords.filter((w) => + w.senses.every((s) => !nonDeletedSenses.includes(s.guid)) ); state.mergeWords = deletedWords.map((w) => newMergeWords(w, [{ srcWordId: w.id, getAudio: false }], true) @@ -230,7 +229,7 @@ const mergeDuplicatesSlice = createSlice({ const newOrder = action.payload.destOrder; // Ensure the move is valid. - if (oldOrder !== -1 && newOrder !== undefined && oldOrder !== newOrder) { + if (oldOrder > -1 && newOrder !== undefined && oldOrder !== newOrder) { // Move the sense pair to its new place. const pair = sensePairs.splice(oldOrder, 1)[0]; sensePairs.splice(newOrder, 0, pair); @@ -341,9 +340,8 @@ function createMergeWords( if ( onlyChild[0].srcWordId === wordId && onlyChild.length === word.senses.length && - !onlyChild.find( - (ms) => - ![Status.Active, Status.Protected].includes(ms.sense.accessibility) + onlyChild.every((ms) => + [Status.Active, Status.Protected].includes(ms.sense.accessibility) ) && compareFlags(mergeWord.flag, word.flag) === 0 ) { @@ -371,8 +369,8 @@ function createMergeWords( parent.senses.push(mergeSense.sense); } }); - const getAudio = !msList.find( - (ms) => ms.sense.accessibility === Status.Separate + const getAudio = msList.every( + (ms) => ms.sense.accessibility !== Status.Separate ); return { srcWordId: msList[0].srcWordId, getAudio }; } @@ -429,7 +427,7 @@ function combineIntoFirstSense(mergeSenses: MergeTreeSense[]): void { // Put the duplicate's domains in the main sense if the id is new. dupSense.semanticDomains.forEach((dom) => { - if (!mainSense.semanticDomains.find((d) => d.id === dom.id)) { + if (mainSense.semanticDomains.every((d) => d.id !== dom.id)) { mainSense.semanticDomains.push({ ...dom }); } }); diff --git a/src/goals/MergeDuplicates/Redux/tests/MergeDupsReducer.test.tsx b/src/goals/MergeDuplicates/Redux/tests/MergeDupsReducer.test.tsx index cdfb809dbf..e283c7d6f9 100644 --- a/src/goals/MergeDuplicates/Redux/tests/MergeDupsReducer.test.tsx +++ b/src/goals/MergeDuplicates/Redux/tests/MergeDupsReducer.test.tsx @@ -60,7 +60,7 @@ describe("MergeDupsReducer", () => { for (const mergeSenseId of Object.keys(words[wordId].sensesGuids)) { const guids = words[wordId].sensesGuids[mergeSenseId]; const order = guids.findIndex((g) => g === guid); - if (order !== -1) { + if (order > -1) { return { wordId, mergeSenseId, order }; } } diff --git a/src/types/word.ts b/src/types/word.ts index b4f8624a1a..40256e49a5 100644 --- a/src/types/word.ts +++ b/src/types/word.ts @@ -37,7 +37,7 @@ export function updateSpeakerInAudio( !p.protected && p.fileName === update.fileName && p.speakerId !== update.speakerId; - if (audio.findIndex(updatePredicate) === -1) { + if (!audio.some(updatePredicate)) { return; } return audio.map((a) => (updatePredicate(a) ? update : a)); From 0ce9cd006de54cc779e03a711618004e94cc686e Mon Sep 17 00:00:00 2001 From: "D. Ror" Date: Mon, 12 Feb 2024 11:25:33 -0500 Subject: [PATCH 09/20] [AudioPlayer] Catch 2-finger mousepad tap as right click (#2943) --- src/components/Pronunciations/AudioPlayer.tsx | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/components/Pronunciations/AudioPlayer.tsx b/src/components/Pronunciations/AudioPlayer.tsx index 533bb2bb71..8222b8410f 100644 --- a/src/components/Pronunciations/AudioPlayer.tsx +++ b/src/components/Pronunciations/AudioPlayer.tsx @@ -11,7 +11,9 @@ import { } from "@mui/material"; import { CSSProperties, + MouseEvent, ReactElement, + TouchEvent, useCallback, useEffect, useState, @@ -124,12 +126,14 @@ export default function AudioPlayer(props: PlayerProps): ReactElement { document.removeEventListener("contextmenu", preventEventOnce, false); } - function handleTouch(event: any): void { + /** If audio can be deleted or speaker changed, a touchscreen press should open an + * options menu instead of the context menu. */ + function handleTouch(e: TouchEvent): void { if (canChangeSpeaker || canDeleteAudio) { // Temporarily disable context menu since some browsers // interpret a long-press touch as a right-click. disableContextMenu(); - setAnchor(event.currentTarget); + setAnchor(e.currentTarget); } } @@ -140,14 +144,22 @@ export default function AudioPlayer(props: PlayerProps): ReactElement { setSpeakerDialog(false); } + /** If speaker can be changed, a right click should open the speaker menu instead of + * the context menu. */ function handleOnAuxClick(): void { if (canChangeSpeaker) { - // Temporarily disable context menu triggered by right-click. disableContextMenu(); setSpeakerDialog(true); } } + /** Catch a multi-finger mousepad tap as a right click. */ + function handleOnMouseDown(e: MouseEvent): void { + if (e.buttons > 1) { + handleOnAuxClick(); + } + } + const tooltipTexts = [t("pronunciations.playTooltip")]; if (canDeleteAudio) { tooltipTexts.push(t("pronunciations.deleteTooltip")); @@ -176,6 +188,7 @@ export default function AudioPlayer(props: PlayerProps): ReactElement { tabIndex={-1} onAuxClick={handleOnAuxClick} onClick={deleteOrTogglePlay} + onMouseDown={handleOnMouseDown} onTouchStart={handleTouch} onTouchEnd={enableContextMenu} aria-label="play" From 854c085713bdfc853bcfb571157d60dd877d0b0d Mon Sep 17 00:00:00 2001 From: "D. Ror" Date: Tue, 13 Feb 2024 13:29:39 -0500 Subject: [PATCH 10/20] [Consent Export] Include file-type extensions; Convert webm to wav (#2900) --- Backend/Controllers/SpeakerController.cs | 27 +++++++++++++++++++----- Backend/Helper/FileStorage.cs | 16 ++++++++++++-- Backend/Services/LiftService.cs | 17 ++++++++++----- 3 files changed, 48 insertions(+), 12 deletions(-) diff --git a/Backend/Controllers/SpeakerController.cs b/Backend/Controllers/SpeakerController.cs index 5b99f812a3..54a0f57e8d 100644 --- a/Backend/Controllers/SpeakerController.cs +++ b/Backend/Controllers/SpeakerController.cs @@ -116,6 +116,13 @@ public async Task DeleteSpeaker(string projectId, string speakerI return NotFound(speakerId); } + // Delete consent file + var path = FileStorage.GetConsentFilePath(speakerId); + if (path is not null) + { + IO.File.Delete(path); + } + // Delete speaker and return success return Ok(await _speakerRepo.Delete(projectId, speakerId)); } @@ -145,8 +152,8 @@ public async Task RemoveConsent(string projectId, string speakerI { return StatusCode(StatusCodes.Status304NotModified, speakerId); } - var path = FileStorage.GenerateConsentFilePath(speaker.Id); - if (IO.File.Exists(path)) + var path = FileStorage.GetConsentFilePath(speaker.Id); + if (path is not null) { IO.File.Delete(path); } @@ -232,9 +239,12 @@ public async Task UploadConsent( { return BadRequest("Empty File"); } + + var extension = IO.Path.GetExtension(file.FileName) ?? ""; if (file.ContentType.Contains("audio")) { speaker.Consent = ConsentType.Audio; + extension = ".webm"; } else if (file.ContentType.Contains("image")) { @@ -245,8 +255,15 @@ public async Task UploadConsent( return BadRequest("File should be audio or image"); } + // Delete old consent file + var old = FileStorage.GetConsentFilePath(speaker.Id); + if (old is not null) + { + IO.File.Delete(old); + } + // Copy file data to a new local file - var path = FileStorage.GenerateConsentFilePath(speakerId); + var path = FileStorage.GenerateConsentFilePath(speakerId, extension); await using (var fs = new IO.FileStream(path, IO.FileMode.OpenOrCreate)) { await file.CopyToAsync(fs); @@ -284,8 +301,8 @@ public IActionResult DownloadConsent(string speakerId) } // Ensure file exists - var path = FileStorage.GenerateConsentFilePath(speakerId); - if (!IO.File.Exists(path)) + var path = FileStorage.GetConsentFilePath(speakerId); + if (path is null) { return NotFound(speakerId); } diff --git a/Backend/Helper/FileStorage.cs b/Backend/Helper/FileStorage.cs index 60b02ffa4b..124c658978 100644 --- a/Backend/Helper/FileStorage.cs +++ b/Backend/Helper/FileStorage.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Linq; using System.Runtime.Serialization; namespace BackendFramework.Helper @@ -96,9 +97,20 @@ public static string GenerateAvatarFilePath(string userId) /// Generate the path to where Consent audio/images are stored. /// /// Throws when id invalid. - public static string GenerateConsentFilePath(string speakerId) + public static string GenerateConsentFilePath(string speakerId, string? extension = null) { - return GenerateFilePath(ConsentDir, Sanitization.SanitizeId(speakerId)); + var fileName = Path.ChangeExtension(Sanitization.SanitizeId(speakerId), extension); + return GenerateFilePath(ConsentDir, fileName); + } + + /// + /// Get the path of a Consent audio/images, or null if it doesn't exist. + /// + /// Throws when id invalid. + public static string? GetConsentFilePath(string speakerId) + { + var searchPattern = $"*{Sanitization.SanitizeId(speakerId)}*"; + return Directory.GetFiles(GenerateDirPath(ConsentDir, true), searchPattern).FirstOrDefault(); } /// diff --git a/Backend/Services/LiftService.cs b/Backend/Services/LiftService.cs index 9acb4a412d..d9f082b2d0 100644 --- a/Backend/Services/LiftService.cs +++ b/Backend/Services/LiftService.cs @@ -347,12 +347,19 @@ public async Task LiftExport( { if (speaker.Consent != ConsentType.None) { - var src = FileStorage.GenerateConsentFilePath(speaker.Id); - if (File.Exists(src)) + var src = FileStorage.GetConsentFilePath(speaker.Id); + if (src is not null) { - var dest = Path.Combine(consentDir, speaker.Id); - File.Copy(src, dest, true); - + var dest = Path.Combine(consentDir, Path.GetFileName(src)); + if (Path.GetExtension(dest).Equals(".webm", StringComparison.OrdinalIgnoreCase)) + { + dest = Path.ChangeExtension(dest, ".wav"); + await FFmpeg.Conversions.New().Start($"-y -i \"{src}\" \"{dest}\""); + } + else + { + File.Copy(src, dest); + } } } } From 71b75cc92ae2374823db3d777fe821fc768e3947 Mon Sep 17 00:00:00 2001 From: "D. Ror" Date: Wed, 14 Feb 2024 09:44:56 -0500 Subject: [PATCH 11/20] Extract test function as general-use utility (#2947) --- .../GoalTimeline/tests/GoalRedux.test.tsx | 2 +- .../tests/ChooseProject.test.tsx | 22 +++++++------------ .../CharacterDetail/tests/index.test.tsx | 19 ++++------------ src/utilities/testRendererUtilities.tsx | 13 +++++++++++ ...lities.tsx => testingLibraryUtilities.tsx} | 12 +++++----- 5 files changed, 31 insertions(+), 37 deletions(-) create mode 100644 src/utilities/testRendererUtilities.tsx rename src/utilities/{testUtilities.tsx => testingLibraryUtilities.tsx} (77%) diff --git a/src/components/GoalTimeline/tests/GoalRedux.test.tsx b/src/components/GoalTimeline/tests/GoalRedux.test.tsx index 906e4b1438..0a62d9bde8 100644 --- a/src/components/GoalTimeline/tests/GoalRedux.test.tsx +++ b/src/components/GoalTimeline/tests/GoalRedux.test.tsx @@ -33,7 +33,7 @@ import { GoalStatus, GoalType } from "types/goals"; import { Path } from "types/path"; import { newUser } from "types/user"; import * as goalUtilities from "utilities/goalUtilities"; -import { renderWithProviders } from "utilities/testUtilities"; +import { renderWithProviders } from "utilities/testingLibraryUtilities"; jest.mock("backend", () => ({ addGoalToUserEdit: (...args: any[]) => mockAddGoalToUserEdit(...args), diff --git a/src/components/ProjectScreen/tests/ChooseProject.test.tsx b/src/components/ProjectScreen/tests/ChooseProject.test.tsx index c65824877a..1c9a838d7f 100644 --- a/src/components/ProjectScreen/tests/ChooseProject.test.tsx +++ b/src/components/ProjectScreen/tests/ChooseProject.test.tsx @@ -3,9 +3,10 @@ import renderer from "react-test-renderer"; import "tests/reactI18nextMock"; -import { Project } from "api/models"; +import { type Project } from "api/models"; import ChooseProject from "components/ProjectScreen/ChooseProject"; import { newProject } from "types/project"; +import { testInstanceHasText } from "utilities/testRendererUtilities"; import { randomIntString } from "utilities/utilities"; jest.mock("backend", () => ({ @@ -27,13 +28,6 @@ const mockProj = (name: string): Project => ({ let testRenderer: renderer.ReactTestRenderer; -const hasText = (item: renderer.ReactTestInstance, text: string): boolean => { - const found = item.findAll( - (node) => node.children.length === 1 && node.children[0] === text - ); - return found.length !== 0; -}; - it("renders with projects in alphabetical order", async () => { const unordered = ["In the middle", "should be last", "alphabetically first"]; mockGetProjects.mockResolvedValue(unordered.map((name) => mockProj(name))); @@ -42,10 +36,10 @@ it("renders with projects in alphabetical order", async () => { }); const items = testRenderer.root.findAllByType(ListItemButton); expect(items).toHaveLength(unordered.length); - expect(hasText(items[0], unordered[0])).toBeFalsy; - expect(hasText(items[1], unordered[1])).toBeFalsy; - expect(hasText(items[2], unordered[2])).toBeFalsy; - expect(hasText(items[0], unordered[2])).toBeTruthy; - expect(hasText(items[1], unordered[0])).toBeTruthy; - expect(hasText(items[2], unordered[1])).toBeTruthy; + expect(testInstanceHasText(items[0], unordered[0])).toBeFalsy(); + expect(testInstanceHasText(items[1], unordered[1])).toBeFalsy(); + expect(testInstanceHasText(items[2], unordered[2])).toBeFalsy(); + expect(testInstanceHasText(items[0], unordered[2])).toBeTruthy(); + expect(testInstanceHasText(items[1], unordered[0])).toBeTruthy(); + expect(testInstanceHasText(items[2], unordered[1])).toBeTruthy(); }); diff --git a/src/goals/CharacterInventory/CharInv/CharacterDetail/tests/index.test.tsx b/src/goals/CharacterInventory/CharInv/CharacterDetail/tests/index.test.tsx index 0404a736a4..2ca7dc4820 100644 --- a/src/goals/CharacterInventory/CharInv/CharacterDetail/tests/index.test.tsx +++ b/src/goals/CharacterInventory/CharInv/CharacterDetail/tests/index.test.tsx @@ -1,10 +1,5 @@ import { Provider } from "react-redux"; -import { - ReactTestInstance, - ReactTestRenderer, - act, - create, -} from "react-test-renderer"; +import { type ReactTestRenderer, act, create } from "react-test-renderer"; import configureMockStore from "redux-mock-store"; import "tests/reactI18nextMock"; @@ -17,7 +12,8 @@ import { } from "goals/CharacterInventory/CharInv/CharacterDetail/FindAndReplace"; import CharacterReplaceDialog from "goals/CharacterInventory/CharInv/CharacterDetail/FindAndReplace/CharacterReplaceDialog"; import { defaultState } from "goals/CharacterInventory/Redux/CharacterInventoryReduxTypes"; -import { StoreState } from "types"; +import { type StoreState } from "types"; +import { testInstanceHasText } from "utilities/testRendererUtilities"; // Dialog uses portals, which are not supported in react-test-renderer. jest.mock("@mui/material", () => { @@ -66,13 +62,6 @@ async function renderCharacterDetail(): Promise { }); } -const hasText = (item: ReactTestInstance, text: string): boolean => { - const found = item.findAll( - (node) => node.children.length === 1 && node.children[0] === text - ); - return found.length !== 0; -}; - beforeEach(async () => { jest.resetAllMocks(); await renderCharacterDetail(); @@ -80,7 +69,7 @@ beforeEach(async () => { describe("CharacterDetail", () => { it("renders with example word", () => { - expect(hasText(charMaster.root, mockPrefix)).toBeTruthy(); + expect(testInstanceHasText(charMaster.root, mockPrefix)).toBeTruthy(); }); describe("FindAndReplace", () => { diff --git a/src/utilities/testRendererUtilities.tsx b/src/utilities/testRendererUtilities.tsx new file mode 100644 index 0000000000..7bdf639ca7 --- /dev/null +++ b/src/utilities/testRendererUtilities.tsx @@ -0,0 +1,13 @@ +import { type ReactTestInstance } from "react-test-renderer"; + +/** Checks if any node in the given `react-test-renderer` instance has the given text. */ +export function testInstanceHasText( + instance: ReactTestInstance, + text: string +): boolean { + return ( + instance.findAll( + (node) => node.children.length === 1 && node.children[0] === text + ).length > 0 + ); +} diff --git a/src/utilities/testUtilities.tsx b/src/utilities/testingLibraryUtilities.tsx similarity index 77% rename from src/utilities/testUtilities.tsx rename to src/utilities/testingLibraryUtilities.tsx index 0f7020dcec..3eb1d7f7aa 100644 --- a/src/utilities/testUtilities.tsx +++ b/src/utilities/testingLibraryUtilities.tsx @@ -7,18 +7,16 @@ import { PersistGate } from "redux-persist/integration/react"; import { defaultState } from "components/App/DefaultState"; import { type AppStore, type RootState, persistor, setupStore } from "store"; -// These test utilities are leveraged from the Redux documentation for Writing Tests: -// https://redux.js.org/usage/writing-tests -// Specifically, see the section on "Integration Testing Connected Components -// and Redux Logic" - -// This type interface extends the default options for render from RTL, as well -// as allows the user to specify other things such as initialState, store. +/** This extends the default options for `render` from `@testing-library/react`, + * allowing the user to specify other things such as `initialState`, `store`. */ interface ExtendedRenderOptions extends Omit { preloadedState?: PreloadedState; store?: AppStore; } +/** This test utility is leveraged from the Redux documentation for Writing Tests: + * https://redux.js.org/usage/writing-tests. Specifically, see the section on + * "Integration Testing Connected Components and Redux Logic" */ // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function renderWithProviders( ui: ReactElement, From 86772508170215ce2f63b704ef600f4a9a77521b Mon Sep 17 00:00:00 2001 From: "D. Ror" Date: Wed, 14 Feb 2024 11:55:05 -0500 Subject: [PATCH 12/20] [Backend/Models] Simplify clone functions (#2906) --- Backend/Models/MergeWordSet.cs | 11 ++--- Backend/Models/MergeWords.cs | 26 +++------- Backend/Models/Project.cs | 85 +++++++++----------------------- Backend/Models/SemanticDomain.cs | 12 ++--- Backend/Models/Sense.cs | 39 ++++----------- Backend/Models/Statistics.cs | 8 +-- Backend/Models/User.cs | 22 ++------- Backend/Models/UserEdit.cs | 26 +++------- Backend/Models/Word.cs | 49 +++++------------- 9 files changed, 74 insertions(+), 204 deletions(-) diff --git a/Backend/Models/MergeWordSet.cs b/Backend/Models/MergeWordSet.cs index f88dcfb594..48a962278c 100644 --- a/Backend/Models/MergeWordSet.cs +++ b/Backend/Models/MergeWordSet.cs @@ -27,23 +27,18 @@ public MergeWordSet() Id = ""; ProjectId = ""; UserId = ""; - WordIds = new List(); + WordIds = new(); } public MergeWordSet Clone() { - var clone = new MergeWordSet + return new() { Id = Id, ProjectId = ProjectId, UserId = UserId, - WordIds = new List() + WordIds = WordIds.Select(id => id).ToList() }; - foreach (var id in WordIds) - { - clone.WordIds.Add(id); - } - return clone; } public bool ContentEquals(MergeWordSet other) diff --git a/Backend/Models/MergeWords.cs b/Backend/Models/MergeWords.cs index 2490938770..a3cd3e4a67 100644 --- a/Backend/Models/MergeWords.cs +++ b/Backend/Models/MergeWords.cs @@ -22,8 +22,8 @@ public class MergeWords public MergeWords() { - Parent = new Word(); - Children = new List(); + Parent = new(); + Children = new(); DeleteOnly = false; } } @@ -59,8 +59,8 @@ public class MergeUndoIds public MergeUndoIds() { - ParentIds = new List(); - ChildIds = new List(); + ParentIds = new(); + ChildIds = new(); } public MergeUndoIds(List parentIds, List childIds) @@ -71,23 +71,11 @@ public MergeUndoIds(List parentIds, List childIds) public MergeUndoIds Clone() { - var clone = new MergeUndoIds + return new() { - ParentIds = new List(), - ChildIds = new List() + ParentIds = ParentIds.Select(id => id).ToList(), + ChildIds = ChildIds.Select(id => id).ToList() }; - - foreach (var id in ParentIds) - { - clone.ParentIds.Add(id); - } - - foreach (var id in ChildIds) - { - clone.ChildIds.Add(id); - } - - return clone; } public bool ContentEquals(MergeUndoIds other) diff --git a/Backend/Models/Project.cs b/Backend/Models/Project.cs index ccfe2c4463..3725c8b390 100644 --- a/Backend/Models/Project.cs +++ b/Backend/Models/Project.cs @@ -92,22 +92,22 @@ public Project() DefinitionsEnabled = false; GrammaticalInfoEnabled = false; AutocompleteSetting = AutocompleteSetting.On; - SemDomWritingSystem = new WritingSystem(); - VernacularWritingSystem = new WritingSystem(); - AnalysisWritingSystems = new List(); - SemanticDomains = new List(); - ValidCharacters = new List(); - RejectedCharacters = new List(); - CustomFields = new List(); - WordFields = new List(); - PartsOfSpeech = new List(); - InviteTokens = new List(); - WorkshopSchedule = new List(); + SemDomWritingSystem = new(); + VernacularWritingSystem = new(); + AnalysisWritingSystems = new(); + SemanticDomains = new(); + ValidCharacters = new(); + RejectedCharacters = new(); + CustomFields = new(); + WordFields = new(); + PartsOfSpeech = new(); + InviteTokens = new(); + WorkshopSchedule = new(); } public Project Clone() { - var clone = new Project + return new() { Id = Id, Name = Name, @@ -118,55 +118,16 @@ public Project Clone() AutocompleteSetting = AutocompleteSetting, SemDomWritingSystem = SemDomWritingSystem.Clone(), VernacularWritingSystem = VernacularWritingSystem.Clone(), - AnalysisWritingSystems = new List(), - SemanticDomains = new List(), - ValidCharacters = new List(), - RejectedCharacters = new List(), - CustomFields = new List(), - WordFields = new List(), - PartsOfSpeech = new List(), - InviteTokens = new List(), - WorkshopSchedule = new List(), + AnalysisWritingSystems = AnalysisWritingSystems.Select(ws => ws.Clone()).ToList(), + SemanticDomains = SemanticDomains.Select(sd => sd.Clone()).ToList(), + ValidCharacters = ValidCharacters.Select(vc => vc).ToList(), + RejectedCharacters = RejectedCharacters.Select(rc => rc).ToList(), + CustomFields = CustomFields.Select(cf => cf.Clone()).ToList(), + WordFields = WordFields.Select(wf => wf).ToList(), + PartsOfSpeech = PartsOfSpeech.Select(ps => ps).ToList(), + InviteTokens = InviteTokens.Select(it => it.Clone()).ToList(), + WorkshopSchedule = WorkshopSchedule.Select(dt => dt).ToList(), }; - - foreach (var aw in AnalysisWritingSystems) - { - clone.AnalysisWritingSystems.Add(aw.Clone()); - } - foreach (var sd in SemanticDomains) - { - clone.SemanticDomains.Add(sd.Clone()); - } - foreach (var cs in ValidCharacters) - { - clone.ValidCharacters.Add(cs); - } - foreach (var cs in RejectedCharacters) - { - clone.RejectedCharacters.Add(cs); - } - foreach (var cf in CustomFields) - { - clone.CustomFields.Add(cf.Clone()); - } - foreach (var wf in WordFields) - { - clone.WordFields.Add(wf); - } - foreach (var pos in PartsOfSpeech) - { - clone.PartsOfSpeech.Add(pos); - } - foreach (var it in InviteTokens) - { - clone.InviteTokens.Add(it.Clone()); - } - foreach (var dt in WorkshopSchedule) - { - clone.WorkshopSchedule.Add(dt); - } - - return clone; } public bool ContentEquals(Project other) @@ -362,8 +323,8 @@ public class UserCreatedProject public UserCreatedProject() { - Project = new Project(); - User = new User(); + Project = new(); + User = new(); } } diff --git a/Backend/Models/SemanticDomain.cs b/Backend/Models/SemanticDomain.cs index fc076dae2f..f359a995f7 100644 --- a/Backend/Models/SemanticDomain.cs +++ b/Backend/Models/SemanticDomain.cs @@ -94,7 +94,7 @@ public SemanticDomainFull() Name = ""; Id = ""; Description = ""; - Questions = new List(); + Questions = new(); Lang = ""; } @@ -102,13 +102,7 @@ public SemanticDomainFull() { var clone = (SemanticDomainFull)base.Clone(); clone.Description = Description; - clone.Questions = new List(); - - foreach (var question in Questions) - { - clone.Questions.Add(question); - } - + clone.Questions = Questions.Select(q => q).ToList(); return clone; } @@ -173,7 +167,7 @@ public SemanticDomainTreeNode(SemanticDomain sd) Lang = sd.Lang; Name = sd.Name; Id = sd.Id; - Children = new List(); + Children = new(); } } } diff --git a/Backend/Models/Sense.cs b/Backend/Models/Sense.cs index 9f18baad47..4f9e09991d 100644 --- a/Backend/Models/Sense.cs +++ b/Backend/Models/Sense.cs @@ -49,44 +49,25 @@ public Sense() // By default generate a new, unique Guid for each new Sense. Guid = Guid.NewGuid(); Accessibility = Status.Active; - GrammaticalInfo = new GrammaticalInfo(); - Definitions = new List(); - Glosses = new List(); - ProtectReasons = new List(); - SemanticDomains = new List(); + GrammaticalInfo = new(); + Definitions = new(); + Glosses = new(); + ProtectReasons = new(); + SemanticDomains = new(); } public Sense Clone() { - var clone = new Sense + return new() { Guid = Guid, Accessibility = Accessibility, GrammaticalInfo = GrammaticalInfo.Clone(), - Definitions = new List(), - Glosses = new List(), - ProtectReasons = new List(), - SemanticDomains = new List(), + Definitions = Definitions.Select(d => d.Clone()).ToList(), + Glosses = Glosses.Select(g => g.Clone()).ToList(), + ProtectReasons = ProtectReasons.Select(pr => pr.Clone()).ToList(), + SemanticDomains = SemanticDomains.Select(sd => sd.Clone()).ToList(), }; - - foreach (var definition in Definitions) - { - clone.Definitions.Add(definition.Clone()); - } - foreach (var gloss in Glosses) - { - clone.Glosses.Add(gloss.Clone()); - } - foreach (var reason in ProtectReasons) - { - clone.ProtectReasons.Add(reason.Clone()); - } - foreach (var sd in SemanticDomains) - { - clone.SemanticDomains.Add(sd.Clone()); - } - - return clone; } public override bool Equals(object? obj) diff --git a/Backend/Models/Statistics.cs b/Backend/Models/Statistics.cs index ef1d153090..65dc326b1a 100644 --- a/Backend/Models/Statistics.cs +++ b/Backend/Models/Statistics.cs @@ -27,7 +27,7 @@ public SemanticDomainUserCount() { Id = ""; Username = ""; - DomainSet = new HashSet(); + DomainSet = new(); DomainCount = 0; WordCount = 0; } @@ -90,8 +90,8 @@ public class ChartRootData public ChartRootData() { - Dates = new List(); - Datasets = new List(); + Dates = new(); + Datasets = new(); } } @@ -107,7 +107,7 @@ public class Dataset public Dataset(string userName, int data) { UserName = userName; - Data = new List() { data }; + Data = new() { data }; } } diff --git a/Backend/Models/User.cs b/Backend/Models/User.cs index 26ec30be15..85bbdd6de7 100644 --- a/Backend/Models/User.cs +++ b/Backend/Models/User.cs @@ -96,13 +96,13 @@ public User() UILang = ""; Token = ""; IsAdmin = false; - WorkedProjects = new Dictionary(); - ProjectRoles = new Dictionary(); + WorkedProjects = new(); + ProjectRoles = new(); } public User Clone() { - var clone = new User + return new() { Id = Id, Avatar = Avatar, @@ -117,21 +117,9 @@ public User Clone() UILang = UILang, Token = Token, IsAdmin = IsAdmin, - WorkedProjects = new Dictionary(), - ProjectRoles = new Dictionary() + WorkedProjects = WorkedProjects.ToDictionary(kv => kv.Key, kv => kv.Value), + ProjectRoles = ProjectRoles.ToDictionary(kv => kv.Key, kv => kv.Value), }; - - foreach (var projId in WorkedProjects.Keys) - { - clone.WorkedProjects.Add(projId, WorkedProjects[projId]); - } - - foreach (var projId in ProjectRoles.Keys) - { - clone.ProjectRoles.Add(projId, ProjectRoles[projId]); - } - - return clone; } public bool ContentEquals(User other) diff --git a/Backend/Models/UserEdit.cs b/Backend/Models/UserEdit.cs index 7d683e740e..3f1c51263f 100644 --- a/Backend/Models/UserEdit.cs +++ b/Backend/Models/UserEdit.cs @@ -27,24 +27,17 @@ public UserEdit() { Id = ""; ProjectId = ""; - Edits = new List(); + Edits = new(); } public UserEdit Clone() { - var clone = new UserEdit + return new() { Id = Id, ProjectId = ProjectId, - Edits = new List() + Edits = Edits.Select(e => e.Clone()).ToList() }; - - foreach (var edit in Edits) - { - clone.Edits.Add(edit.Clone()); - } - - return clone; } public bool ContentEquals(UserEdit other) @@ -133,26 +126,19 @@ public Edit() { Guid = Guid.NewGuid(); GoalType = 0; - StepData = new List(); + StepData = new(); Changes = "{}"; } public Edit Clone() { - var clone = new Edit + return new() { Guid = Guid, GoalType = GoalType, - StepData = new List(), + StepData = StepData.Select(sd => sd).ToList(), Changes = Changes }; - - foreach (var step in StepData) - { - clone.StepData.Add(step); - } - - return clone; } public override bool Equals(object? obj) diff --git a/Backend/Models/Word.cs b/Backend/Models/Word.cs index 2a5e03e917..3266fbbc68 100644 --- a/Backend/Models/Word.cs +++ b/Backend/Models/Word.cs @@ -94,18 +94,18 @@ public Word() OtherField = ""; ProjectId = ""; Accessibility = Status.Active; - Audio = new List(); - EditedBy = new List(); - History = new List(); - ProtectReasons = new List(); - Senses = new List(); - Note = new Note(); - Flag = new Flag(); + Audio = new(); + EditedBy = new(); + History = new(); + ProtectReasons = new(); + Senses = new(); + Note = new(); + Flag = new(); } public Word Clone() { - var clone = new Word + return new() { Id = Id, Guid = Guid, @@ -116,37 +116,14 @@ public Word Clone() OtherField = OtherField, ProjectId = ProjectId, Accessibility = Accessibility, - Audio = new List(), - EditedBy = new List(), - History = new List(), - ProtectReasons = new List(), - Senses = new List(), + Audio = Audio.Select(p => p.Clone()).ToList(), + EditedBy = EditedBy.Select(id => id).ToList(), + History = History.Select(id => id).ToList(), + ProtectReasons = ProtectReasons.Select(pr => pr.Clone()).ToList(), + Senses = Senses.Select(s => s.Clone()).ToList(), Note = Note.Clone(), Flag = Flag.Clone(), }; - - foreach (var audio in Audio) - { - clone.Audio.Add(audio.Clone()); - } - foreach (var id in EditedBy) - { - clone.EditedBy.Add(id); - } - foreach (var id in History) - { - clone.History.Add(id); - } - foreach (var reason in ProtectReasons) - { - clone.ProtectReasons.Add(reason.Clone()); - } - foreach (var sense in Senses) - { - clone.Senses.Add(sense.Clone()); - } - - return clone; } public bool ContentEquals(Word other) From 6ea0515dcb0fa9b5c7c2c540242dc132e93e3a5c Mon Sep 17 00:00:00 2001 From: "D. Ror" Date: Wed, 14 Feb 2024 12:01:38 -0500 Subject: [PATCH 13/20] [DataEntry > NewEntry] Prevent double submission (#2946) --- .../DataEntryTable/NewEntry/index.tsx | 25 ++- .../NewEntry/tests/index.test.tsx | 202 +++++++++++++++--- 2 files changed, 187 insertions(+), 40 deletions(-) diff --git a/src/components/DataEntry/DataEntryTable/NewEntry/index.tsx b/src/components/DataEntry/DataEntryTable/NewEntry/index.tsx index db872fcdf5..08478b014c 100644 --- a/src/components/DataEntry/DataEntryTable/NewEntry/index.tsx +++ b/src/components/DataEntry/DataEntryTable/NewEntry/index.tsx @@ -26,7 +26,13 @@ import { StoreState } from "types"; import theme from "types/theme"; import { FileWithSpeakerId } from "types/word"; -const idAffix = "new-entry"; +export enum NewEntryId { + ButtonDelete = "new-entry-delete-button", + ButtonNote = "new-entry-note-button", + GridNewEntry = "new-entry", + TextFieldGloss = "new-entry-gloss-textfield", + TextFieldVern = "new-entry-vernacular-textfield", +} export enum FocusTarget { Gloss, @@ -103,6 +109,7 @@ export default function NewEntry(props: NewEntryProps): ReactElement { const [senseOpen, setSenseOpen] = useState(false); const [shouldFocus, setShouldFocus] = useState(); + const [submitting, setSubmitting] = useState(false); const [vernOpen, setVernOpen] = useState(false); const [wasTreeClosed, setWasTreeClosed] = useState(false); @@ -124,6 +131,7 @@ export default function NewEntry(props: NewEntryProps): ReactElement { const resetState = useCallback((): void => { resetNewEntry(); + setSubmitting(false); setVernOpen(false); focus(FocusTarget.Vernacular); }, [focus, resetNewEntry]); @@ -169,6 +177,11 @@ export default function NewEntry(props: NewEntryProps): ReactElement { }; const addNewEntryAndReset = async (): Promise => { + // Prevent double-submission + if (submitting) { + return; + } + setSubmitting(true); await addNewEntry(); resetState(); }; @@ -228,7 +241,7 @@ export default function NewEntry(props: NewEntryProps): ReactElement { }; return ( - + handleEnter(true)} vernacularLang={vernacularLang} - textFieldId={`${idAffix}-vernacular`} + textFieldId={NewEntryId.TextFieldVern} onUpdate={() => conditionalFocus(FocusTarget.Vernacular)} /> handleEnter(false)} analysisLang={analysisLang} - textFieldId={`${idAffix}-gloss`} + textFieldId={NewEntryId.TextFieldGloss} onUpdate={() => conditionalFocus(FocusTarget.Gloss)} /> @@ -289,9 +302,9 @@ export default function NewEntry(props: NewEntryProps): ReactElement { {!selectedDup?.id && ( // note is not available if user selected to modify an exiting entry )} @@ -306,8 +319,8 @@ export default function NewEntry(props: NewEntryProps): ReactElement { resetState()} - buttonId={`${idAffix}-delete`} /> diff --git a/src/components/DataEntry/DataEntryTable/NewEntry/tests/index.test.tsx b/src/components/DataEntry/DataEntryTable/NewEntry/tests/index.test.tsx index 113d5bfdeb..0214822a07 100644 --- a/src/components/DataEntry/DataEntryTable/NewEntry/tests/index.test.tsx +++ b/src/components/DataEntry/DataEntryTable/NewEntry/tests/index.test.tsx @@ -1,50 +1,184 @@ -import { createRef } from "react"; +import { type ReactElement, createRef } from "react"; import { Provider } from "react-redux"; -import renderer from "react-test-renderer"; +import { type ReactTestRenderer, act, create } from "react-test-renderer"; import configureMockStore from "redux-mock-store"; import "tests/reactI18nextMock"; -import NewEntry from "components/DataEntry/DataEntryTable/NewEntry"; +import { + GlossWithSuggestions, + VernWithSuggestions, +} from "components/DataEntry/DataEntryTable/EntryCellComponents"; +import NewEntry, { + NewEntryId, +} from "components/DataEntry/DataEntryTable/NewEntry"; import { newWritingSystem } from "types/writingSystem"; -jest.mock("@mui/material/Autocomplete", () => "div"); +jest.mock( + "@mui/material/Autocomplete", + () => (props: any) => mockAutocomplete(props) +); jest.mock("components/Pronunciations/PronunciationsFrontend", () => "div"); +/** Bypass the Autocomplete and render its internal input with the props of both. */ +const mockAutocomplete = (props: { + renderInput: (params: any) => ReactElement; +}): ReactElement => { + const { renderInput, ...params } = props; + return renderInput(params); +}; + +const mockAddNewAudio = jest.fn(); +const mockAddNewEntry = jest.fn(); +const mockDelNewAudio = jest.fn(); +const mockSetNewGloss = jest.fn(); +const mockSetNewNote = jest.fn(); +const mockSetNewVern = jest.fn(); +const mockSetSelectedDup = jest.fn(); +const mockSetSelectedSense = jest.fn(); +const mockRepNewAudio = jest.fn(); +const mockResetNewEntry = jest.fn(); +const mockUpdateWordWithNewGloss = jest.fn(); + const mockStore = configureMockStore()({ treeViewState: { open: false } }); +let renderer: ReactTestRenderer; + +const renderNewEntry = async ( + vern = "", + gloss = "", + note = "" +): Promise => { + await act(async () => { + renderer = create( + + ()} + // Parent component handles vern suggestion state: + setSelectedDup={mockSetSelectedDup} + setSelectedSense={mockSetSelectedSense} + suggestedVerns={[]} + suggestedDups={[]} + /> + + ); + }); +}; + +beforeEach(() => { + jest.resetAllMocks(); +}); + describe("NewEntry", () => { - it("renders without crashing", () => { - renderer.act(() => { - renderer.create( - - ()} - // Parent component handles vern suggestion state: - setSelectedDup={jest.fn()} - setSelectedSense={jest.fn()} - suggestedVerns={[]} - suggestedDups={[]} - /> - - ); + it("does not submit without a vernacular", async () => { + await renderNewEntry("", "gloss"); + await act(async () => { + renderer.root.findByType(GlossWithSuggestions).props.handleEnter(); + }); + expect(mockAddNewEntry).not.toHaveBeenCalled(); + }); + + it("does not submit with vernacular Enter if gloss is empty", async () => { + await renderNewEntry("vern", ""); + await act(async () => { + renderer.root.findByType(VernWithSuggestions).props.handleEnter(); + }); + expect(mockAddNewEntry).not.toHaveBeenCalled(); + }); + + it("does submit with gloss Enter if gloss is empty", async () => { + await renderNewEntry("vern", ""); + await act(async () => { + renderer.root.findByType(GlossWithSuggestions).props.handleEnter(); + }); + expect(mockAddNewEntry).toHaveBeenCalledTimes(1); + }); + + it("resets when the delete button is clicked", async () => { + await renderNewEntry(); + expect(mockResetNewEntry).not.toHaveBeenCalled(); + await act(async () => { + renderer.root + .findByProps({ id: NewEntryId.ButtonDelete }) + .props.onClick(); + }); + expect(mockResetNewEntry).toHaveBeenCalledTimes(1); + }); + + it("resets new entry after awaiting add", async () => { + await renderNewEntry("vern", "gloss"); + + // Use a mock timer to control when addNewEntry completes + jest.useFakeTimers(); + mockAddNewEntry.mockImplementation( + async () => await new Promise((res) => setTimeout(res, 1000)) + ); + + // Submit a new entry + await act(async () => { + renderer.root.findByType(GlossWithSuggestions).props.handleEnter(); }); + expect(mockAddNewEntry).toHaveBeenCalledTimes(1); + expect(mockResetNewEntry).not.toHaveBeenCalled(); + + // Run the timers and confirm a reset + await act(async () => { + jest.runAllTimers(); + }); + expect(mockAddNewEntry).toHaveBeenCalledTimes(1); + expect(mockResetNewEntry).toHaveBeenCalledTimes(1); + + jest.useRealTimers(); + }); + + it("doesn't allow double submission", async () => { + await renderNewEntry("vern", "gloss"); + + // Use a mock timer to control when addNewEntry completes + jest.useFakeTimers(); + mockAddNewEntry.mockImplementation( + async () => await new Promise((res) => setTimeout(res, 1000)) + ); + + // Submit a new entry + const gloss = renderer.root.findByType(GlossWithSuggestions); + await act(async () => { + gloss.props.handleEnter(); + }); + expect(mockAddNewEntry).toHaveBeenCalledTimes(1); + expect(mockResetNewEntry).not.toHaveBeenCalled(); + + // Attempt a second submission before the first one completes + await act(async () => { + gloss.props.handleEnter(); + }); + expect(mockAddNewEntry).toHaveBeenCalledTimes(1); + expect(mockResetNewEntry).not.toHaveBeenCalled(); + + // Run the timers and confirm no second submission + await act(async () => { + jest.runAllTimers(); + }); + expect(mockAddNewEntry).toHaveBeenCalledTimes(1); + expect(mockResetNewEntry).toHaveBeenCalledTimes(1); + + jest.useRealTimers(); }); }); From d312c1a5a34ef97d70666d1882ef420c46d4a772 Mon Sep 17 00:00:00 2001 From: "D. Ror" Date: Fri, 23 Feb 2024 14:20:41 -0500 Subject: [PATCH 14/20] [MergeDups] Unify reducer with assumption that sidebar must have multiple senses (#2907) --- .../MergeDuplicates/Redux/MergeDupsReducer.ts | 130 ++++--- .../Redux/tests/MergeDupsReducer.test.tsx | 357 +++++++++--------- 2 files changed, 239 insertions(+), 248 deletions(-) diff --git a/src/goals/MergeDuplicates/Redux/MergeDupsReducer.ts b/src/goals/MergeDuplicates/Redux/MergeDupsReducer.ts index 527856e4ee..68fe773b77 100644 --- a/src/goals/MergeDuplicates/Redux/MergeDupsReducer.ts +++ b/src/goals/MergeDuplicates/Redux/MergeDupsReducer.ts @@ -10,8 +10,10 @@ import { } from "api/models"; import { type MergeData, + type MergeTreeReference, type MergeTreeSense, type MergeTreeWord, + type Sidebar, convertSenseToMergeTreeSense, convertWordToMergeTreeWord, defaultSidebar, @@ -31,12 +33,14 @@ const mergeDuplicatesSlice = createSlice({ clearMergeWordsAction: (state) => { state.mergeWords = []; }, + clearTreeAction: () => { return defaultState; }, + combineSenseAction: (state, action) => { - const srcRef = action.payload.src; - const destRef = action.payload.dest; + const srcRef: MergeTreeReference = action.payload.src; + const destRef: MergeTreeReference = action.payload.dest; // Ignore dropping a sense (or one of its sub-senses) into itself. if (srcRef.mergeSenseId !== destRef.mergeSenseId) { @@ -45,13 +49,19 @@ const mergeDuplicatesSlice = createSlice({ const srcGuids = words[srcWordId].sensesGuids[srcRef.mergeSenseId]; const destGuids: string[] = []; if (srcRef.order === undefined || srcGuids.length === 1) { + // A sense from a word dropped into another sense. destGuids.push(...srcGuids); delete words[srcWordId].sensesGuids[srcRef.mergeSenseId]; if (!Object.keys(words[srcWordId].sensesGuids).length) { delete words[srcWordId]; } } else { + // A sense from the sidebar dropped into another sense. destGuids.push(srcGuids.splice(srcRef.order, 1)[0]); + if (srcGuids.length < 2) { + // If not multiple senses in the sidebar, reset the sidebar. + state.tree.sidebar = defaultSidebar; + } } words[destRef.wordId].sensesGuids[destRef.mergeSenseId].push( @@ -60,39 +70,40 @@ const mergeDuplicatesSlice = createSlice({ state.tree.words = words; } }, + deleteSenseAction: (state, action) => { - const srcRef = action.payload; + const srcRef: MergeTreeReference = action.payload; const srcWordId = srcRef.wordId; const words = state.tree.words; const sensesGuids = words[srcWordId].sensesGuids; - if (srcRef.order !== undefined) { - sensesGuids[srcRef.mergeSenseId].splice(srcRef.order, 1); - if (!sensesGuids[srcRef.mergeSenseId].length) { - delete sensesGuids[srcRef.mergeSenseId]; - } - } else { + const srcGuids = sensesGuids[srcRef.mergeSenseId]; + if (srcRef.order === undefined || srcGuids.length === 1) { + // A sense deleted from a word. delete sensesGuids[srcRef.mergeSenseId]; - } - if (!Object.keys(words[srcWordId].sensesGuids).length) { - delete words[srcWordId]; - } + if (!Object.keys(sensesGuids).length) { + delete words[srcWordId]; + } - const sidebar = state.tree.sidebar; - // If the sense is being deleted from the words column - // and the sense is also shown in the sidebar, - // then reset the sidebar. - if ( - sidebar.wordId === srcRef.wordId && - sidebar.mergeSenseId === srcRef.mergeSenseId && - srcRef.order === undefined - ) { - state.tree.sidebar = defaultSidebar; + // If the deleted sense was open in the sidebar, reset the sidebar. + const { mergeSenseId, wordId } = state.tree.sidebar; + if (mergeSenseId === srcRef.mergeSenseId && wordId === srcRef.wordId) { + state.tree.sidebar = defaultSidebar; + } + } else { + // A sense deleted from the sidebar. + srcGuids.splice(srcRef.order, 1); + if (srcGuids.length < 2) { + // If not multiple senses in the sidebar, reset the sidebar. + state.tree.sidebar = defaultSidebar; + } } }, + flagWordAction: (state, action) => { state.tree.words[action.payload.wordId].flag = action.payload.flag; }, + getMergeWordsAction: (state) => { // Handle words with all senses deleted. const possibleWords = Object.values(state.data.words); @@ -125,18 +136,21 @@ const mergeDuplicatesSlice = createSlice({ } } }, + moveSenseAction: (state, action) => { - const srcWordId = action.payload.src.wordId; const destWordId = action.payload.destWordId; - const srcOrder = action.payload.src.order; - if (srcOrder === undefined && srcWordId !== destWordId) { - const mergeSenseId = action.payload.src.mergeSenseId; + const srcRef: MergeTreeReference = action.payload.src; + const srcWordId = srcRef.wordId; + // Verify that this is a valid movement of a word sense. + if (srcRef.order === undefined && srcWordId !== destWordId) { + const mergeSenseId = srcRef.mergeSenseId; const words = state.tree.words; // Check if dropping the sense into a new word. if (words[destWordId] === undefined) { if (Object.keys(words[srcWordId].sensesGuids).length === 1) { + // Don't do anything if the sense was alone in its word. return; } words[destWordId] = newMergeTreeWord(); @@ -146,9 +160,7 @@ const mergeDuplicatesSlice = createSlice({ const guids = words[srcWordId].sensesGuids[mergeSenseId]; const sensesPairs = Object.entries(words[destWordId].sensesGuids); sensesPairs.splice(action.payload.destOrder, 0, [mergeSenseId, guids]); - const newSensesGuids: Hash = {}; - sensesPairs.forEach(([key, value]) => (newSensesGuids[key] = value)); - words[destWordId].sensesGuids = newSensesGuids; + words[destWordId].sensesGuids = Object.fromEntries(sensesPairs); // Cleanup the srcWord. delete words[srcWordId].sensesGuids[mergeSenseId]; @@ -157,50 +169,36 @@ const mergeDuplicatesSlice = createSlice({ } } }, + moveDuplicateAction: (state, action) => { - const srcRef = action.payload.src; - // Verify that the ref.order field is defined - if (srcRef.order !== undefined) { + const srcRef: MergeTreeReference = action.payload.src; + const words = state.tree.words; + const srcGuids = words[srcRef.wordId].sensesGuids[srcRef.mergeSenseId]; + // Verify that this is a valid movement of a sidebar sense. + if (srcRef.order !== undefined && srcGuids.length > 1) { const destWordId = action.payload.destWordId; - const words = state.tree.words; - - const srcWordId = srcRef.wordId; - let mergeSenseId = srcRef.mergeSenseId; // Get guid of sense being restored from the sidebar. - const srcGuids = words[srcWordId].sensesGuids[mergeSenseId]; const guid = srcGuids.splice(srcRef.order, 1)[0]; + if (srcGuids.length < 2) { + // If not multiple senses in the sidebar, reset the sidebar. + state.tree.sidebar = defaultSidebar; + } // Check if dropping the sense into a new word. if (words[destWordId] === undefined) { words[destWordId] = newMergeTreeWord(); } - if (srcGuids.length === 0) { - // If there are no guids left, this is a full move. - if (srcWordId === destWordId) { - return; - } - delete words[srcWordId].sensesGuids[mergeSenseId]; - if (!Object.keys(words[srcWordId].sensesGuids).length) { - delete words[srcWordId]; - } - } else { - // Otherwise, create a new sense in the destWord. - mergeSenseId = v4(); - } - // Update the destWord. const sensesPairs = Object.entries(words[destWordId].sensesGuids); - sensesPairs.splice(action.payload.destOrder, 0, [mergeSenseId, [guid]]); - const newSensesGuids: Hash = {}; - sensesPairs.forEach(([key, value]) => (newSensesGuids[key] = value)); - words[destWordId].sensesGuids = newSensesGuids; + sensesPairs.splice(action.payload.destOrder, 0, [v4(), [guid]]); + words[destWordId].sensesGuids = Object.fromEntries(sensesPairs); } }, - orderDuplicateAction: (state, action) => { - const ref = action.payload.src; + orderDuplicateAction: (state, action) => { + const ref: MergeTreeReference = action.payload.src; const oldOrder = ref.order; const newOrder = action.payload.destOrder; @@ -218,13 +216,15 @@ const mergeDuplicatesSlice = createSlice({ state.tree.words[ref.wordId].sensesGuids = sensesGuids; } }, + orderSenseAction: (state, action) => { - const word = state.tree.words[action.payload.src.wordId]; + const ref: MergeTreeReference = action.payload.src; + const word = state.tree.words[ref.wordId]; // Convert the Hash to an array to expose the order. const sensePairs = Object.entries(word.sensesGuids); - const mergeSenseId = action.payload.src.mergeSenseId; + const mergeSenseId = ref.mergeSenseId; const oldOrder = sensePairs.findIndex((p) => p[0] === mergeSenseId); const newOrder = action.payload.destOrder; @@ -240,12 +240,17 @@ const mergeDuplicatesSlice = createSlice({ word.sensesGuids[key] = value; } - state.tree.words[action.payload.src.wordId] = word; + state.tree.words[ref.wordId] = word; } }, + setSidebarAction: (state, action) => { - state.tree.sidebar = action.payload; + const sidebar: Sidebar = action.payload; + // Only open sidebar with multiple senses. + state.tree.sidebar = + sidebar.mergeSenses.length > 1 ? sidebar : defaultSidebar; }, + setDataAction: (state, action) => { if (action.payload.length === 0) { state = defaultState; @@ -265,6 +270,7 @@ const mergeDuplicatesSlice = createSlice({ state.mergeWords = []; } }, + setVernacularAction: (state, action) => { state.tree.words[action.payload.wordId].vern = action.payload.vern; }, diff --git a/src/goals/MergeDuplicates/Redux/tests/MergeDupsReducer.test.tsx b/src/goals/MergeDuplicates/Redux/tests/MergeDupsReducer.test.tsx index e283c7d6f9..69c6e6ebfa 100644 --- a/src/goals/MergeDuplicates/Redux/tests/MergeDupsReducer.test.tsx +++ b/src/goals/MergeDuplicates/Redux/tests/MergeDupsReducer.test.tsx @@ -4,6 +4,7 @@ import { type MergeTreeReference, type MergeTreeWord, convertSenseToMergeTreeSense, + defaultSidebar, defaultTree, newMergeTreeWord, } from "goals/MergeDuplicates/MergeDupsTreeTypes"; @@ -37,7 +38,8 @@ jest.mock("uuid"); const mockUuid = require("uuid") as { v4: jest.Mock }; let uuidIndex = 0; -// getMockUuid(false) gives the next uuid to be assigned by our mocked v4. +/** When `increment` (default `true`) is set to `false`, + * returns the next uuid to be assigned by our mocked `v4`. */ function getMockUuid(increment = true): string { const uuid = `mockUuid${uuidIndex}`; if (increment) { @@ -85,209 +87,205 @@ describe("MergeDupsReducer", () => { word2: newMergeTreeWord("senses:A01", { word2_senseA: ["word2_senseA_0", "word2_senseA_1"], }), - word3: newMergeTreeWord("senses:A0B01", { + word3: newMergeTreeWord("senses:A0B012", { word3_senseA: ["word3_senseA_0"], - word3_senseB: ["word3_senseB_0", "word3_senseB_1"], + word3_senseB: ["word3_senseB_0", "word3_senseB_1", "word3_senseB_2"], }), }; } + /** MergeTreeState with open sidebar */ const mockState: MergeTreeState = { ...defaultState, - tree: { ...defaultTree, words: testTreeWords() }, + tree: { + ...defaultTree, + sidebar: { + ...defaultSidebar, + mergeSenseId: "word2_senseA", + wordId: "word2", + }, + words: testTreeWords(), + }, }; - function checkTreeWords( + /** Check whether the action correctly updates the tree words. + * Also, whether the sidebar was closed by the action (default: `false`). */ + function checkTree( action: Action | PayloadAction, - expected: Hash + expectedWords: Hash, + sidebarClosed = false ): void { - const result = mergeDupStepReducer(mockState, action).tree.words; - // We have to stringify for this test, - // because the order of the .sensesGuids matters. - expect(JSON.stringify(result)).toEqual(JSON.stringify(expected)); + const { sidebar, words } = mergeDupStepReducer(mockState, action).tree; + expect(!sidebar.wordId).toEqual(sidebarClosed); + // Stringify for this test, because order within `.sensesGuids` matters. + expect(JSON.stringify(words)).toEqual(JSON.stringify(expectedWords)); } describe("combineSense", () => { - it("combine sense from sidebar into other sense", () => { + it("combine sense from 2-sense sidebar into other sense; closes sidebar", () => { const srcWordId = "word2"; + const srcSenseId = `${srcWordId}_senseA`; const srcRef: MergeTreeReference = { wordId: srcWordId, - mergeSenseId: `${srcWordId}_senseA`, + mergeSenseId: srcSenseId, order: 0, }; const destWordId = "word1"; + const destSenseId = `${destWordId}_senseA`; const destRef: MergeTreeReference = { wordId: destWordId, - mergeSenseId: `${destWordId}_senseA`, + mergeSenseId: destSenseId, }; const testAction = combineSense({ src: srcRef, dest: destRef }); const expectedWords = testTreeWords(); - expectedWords[srcWordId].sensesGuids = { - word2_senseA: ["word2_senseA_1"], - }; - expectedWords[destWordId].sensesGuids = { - word1_senseA: ["word1_senseA_0", "word2_senseA_0"], - }; + // Sidebar sense _0 moved so _1 remains. + expectedWords[srcWordId].sensesGuids[srcSenseId] = [`${srcSenseId}_1`]; + expectedWords[destWordId].sensesGuids[destSenseId] = [ + `${destSenseId}_0`, + `${srcSenseId}_0`, + ]; - checkTreeWords(testAction, expectedWords); + checkTree(testAction, expectedWords, true); }); - it("combine last sense from sidebar into other sense", () => { + it("combine sense from 3-sense sidebar into other sense", () => { const srcWordId = "word3"; + const srcSenseId = `${srcWordId}_senseB`; const srcRef: MergeTreeReference = { wordId: srcWordId, - mergeSenseId: `${srcWordId}_senseA`, - order: 0, + mergeSenseId: srcSenseId, + order: 1, }; const destWordId = "word1"; + const destSenseId = `${destWordId}_senseA`; const destRef: MergeTreeReference = { wordId: destWordId, - mergeSenseId: `${destWordId}_senseA`, + mergeSenseId: destSenseId, }; const testAction = combineSense({ src: srcRef, dest: destRef }); const expectedWords = testTreeWords(); - expectedWords[destWordId].sensesGuids = { - word1_senseA: ["word1_senseA_0", "word3_senseA_0"], - }; - expectedWords[srcWordId].sensesGuids = { - word3_senseB: ["word3_senseB_0", "word3_senseB_1"], - }; - - checkTreeWords(testAction, expectedWords); - }); - - it("combine last sidebar sense from a word's last sense into other word's sense", () => { - const srcWordId = "word1"; - const srcRef: MergeTreeReference = { - wordId: srcWordId, - mergeSenseId: `${srcWordId}_senseA`, - order: 0, - }; - - const destWordId = "word3"; - const destRef: MergeTreeReference = { - wordId: destWordId, - mergeSenseId: `${destWordId}_senseA`, - }; - - const testAction = combineSense({ src: srcRef, dest: destRef }); - - const expectedWords = testTreeWords(); - delete expectedWords[srcWordId]; - expectedWords[destWordId].sensesGuids = { - word3_senseA: ["word3_senseA_0", "word1_senseA_0"], - word3_senseB: ["word3_senseB_0", "word3_senseB_1"], - }; + // Sidebar sense _1 moved so _0, _2 remain. + expectedWords[srcWordId].sensesGuids[srcSenseId] = [ + `${srcSenseId}_0`, + `${srcSenseId}_2`, + ]; + expectedWords[destWordId].sensesGuids[destSenseId] = [ + `${destSenseId}_0`, + `${srcSenseId}_1`, + ]; - checkTreeWords(testAction, expectedWords); + checkTree(testAction, expectedWords); }); it("combine sense into other sense in same word", () => { const wordId = "word3"; - const srcRef: MergeTreeReference = { - wordId, - mergeSenseId: `${wordId}_senseB`, - }; - const destRef: MergeTreeReference = { - wordId, - mergeSenseId: `${wordId}_senseA`, - }; + const srcSenseId = `${wordId}_senseB`; + const srcRef: MergeTreeReference = { wordId, mergeSenseId: srcSenseId }; + const destSenseId = `${wordId}_senseA`; + const destRef: MergeTreeReference = { wordId, mergeSenseId: destSenseId }; const testAction = combineSense({ src: srcRef, dest: destRef }); const expectedWords = testTreeWords(); + // The word now has a single combined sense instead of two senses. expectedWords[wordId].sensesGuids = { - word3_senseA: ["word3_senseA_0", "word3_senseB_0", "word3_senseB_1"], + [destSenseId]: [ + `${destSenseId}_0`, + `${srcSenseId}_0`, + `${srcSenseId}_1`, + `${srcSenseId}_2`, + ], }; - checkTreeWords(testAction, expectedWords); + checkTree(testAction, expectedWords); }); - it("combine sense into other sense in different word", () => { + it("combine sense into other sense in other word", () => { const srcWordId = "word3"; + const srcSenseId = `${srcWordId}_senseA`; const srcRef: MergeTreeReference = { wordId: srcWordId, - mergeSenseId: `${srcWordId}_senseA`, + mergeSenseId: srcSenseId, }; const destWordId = "word1"; + const destSenseId = `${destWordId}_senseA`; const destRef: MergeTreeReference = { wordId: destWordId, - mergeSenseId: `${destWordId}_senseA`, + mergeSenseId: destSenseId, }; const testAction = combineSense({ src: srcRef, dest: destRef }); const expectedWords = testTreeWords(); expectedWords[srcWordId].sensesGuids = { - word3_senseB: ["word3_senseB_0", "word3_senseB_1"], + // _senseA moved so _senseB remains. + word3_senseB: ["word3_senseB_0", "word3_senseB_1", "word3_senseB_2"], }; expectedWords[destWordId].sensesGuids = { - word1_senseA: ["word1_senseA_0", "word3_senseA_0"], + [destSenseId]: [`${destSenseId}_0`, `${srcSenseId}_0`], }; - checkTreeWords(testAction, expectedWords); + checkTree(testAction, expectedWords); }); - it("combine last sense into other sense in different word", () => { + it("combines last sense into other sense in other word", () => { const srcWordId = "word1"; + const srcSenseId = `${srcWordId}_senseA`; const srcRef: MergeTreeReference = { wordId: srcWordId, - mergeSenseId: `${srcWordId}_senseA`, + mergeSenseId: srcSenseId, }; const destWordId = "word3"; + const destSenseId = `${destWordId}_senseA`; const destRef: MergeTreeReference = { wordId: destWordId, - mergeSenseId: `${destWordId}_senseA`, + mergeSenseId: destSenseId, }; const testAction = combineSense({ src: srcRef, dest: destRef }); const expectedWords = testTreeWords(); delete expectedWords[srcWordId]; - expectedWords[destWordId].sensesGuids = { - word3_senseA: ["word3_senseA_0", "word1_senseA_0"], - word3_senseB: ["word3_senseB_0", "word3_senseB_1"], - }; + expectedWords[destWordId].sensesGuids[destSenseId] = [ + `${destSenseId}_0`, + `${srcSenseId}_0`, + ]; - checkTreeWords(testAction, expectedWords); + checkTree(testAction, expectedWords); }); }); describe("deleteSense", () => { it("deletes one-sense sense from a word with multiple senses", () => { const wordId = "word3"; - const testRef: MergeTreeReference = { - wordId, - mergeSenseId: `${wordId}_senseA`, - }; + const mergeSenseId = `${wordId}_senseA`; + const testRef: MergeTreeReference = { wordId, mergeSenseId }; const testAction = deleteSense(testRef); const expectedWords = testTreeWords(); - delete expectedWords[wordId].sensesGuids[testRef.mergeSenseId]; + delete expectedWords[wordId].sensesGuids[mergeSenseId]; - checkTreeWords(testAction, expectedWords); + checkTree(testAction, expectedWords); }); it("deletes multi-sense sense from a word with multiple senses", () => { const wordId = "word3"; - const testRef: MergeTreeReference = { - wordId, - mergeSenseId: `${wordId}_senseB`, - }; + const mergeSenseId = `${wordId}_senseB`; + const testRef: MergeTreeReference = { wordId, mergeSenseId }; const testAction = deleteSense(testRef); const expectedWords = testTreeWords(); - delete expectedWords[wordId].sensesGuids[testRef.mergeSenseId]; + delete expectedWords[wordId].sensesGuids[mergeSenseId]; - checkTreeWords(testAction, expectedWords); + checkTree(testAction, expectedWords); }); it("deletes word when deleting final sense", () => { @@ -302,39 +300,39 @@ describe("MergeDupsReducer", () => { const expectedWords = testTreeWords(); delete expectedWords[wordId]; - checkTreeWords(testAction, expectedWords); + // Also closes sidebar, since deleted sense was open in the sidebar. + checkTree(testAction, expectedWords, true); }); - it("deletes a sense from the sidebar", () => { + it("deletes sense from 2-sense sidebar; closes sidebar", () => { const wordId = "word2"; - const testRef: MergeTreeReference = { - wordId, - mergeSenseId: `${wordId}_senseA`, - order: 0, - }; + const mergeSenseId = `${wordId}_senseA`; + const testRef: MergeTreeReference = { wordId, mergeSenseId, order: 0 }; const testAction = deleteSense(testRef); const expectedWords = testTreeWords(); - expectedWords[wordId].sensesGuids = { word2_senseA: ["word2_senseA_1"] }; + // Sidebar sense _0 removed so _1 remains. + expectedWords[wordId].sensesGuids[mergeSenseId] = ["word2_senseA_1"]; - checkTreeWords(testAction, expectedWords); + checkTree(testAction, expectedWords, true); }); - it("delete last sidebar sense from a word's last sense", () => { - const srcWordId = "word1"; - const srcRef: MergeTreeReference = { - wordId: srcWordId, - mergeSenseId: `${srcWordId}_senseA`, - order: 0, - }; + it("deletes sense from 3-sense sidebar", () => { + const wordId = "word3"; + const mergeSenseId = `${wordId}_senseB`; + const testRef: MergeTreeReference = { wordId, mergeSenseId, order: 2 }; - const testAction = deleteSense(srcRef); + const testAction = deleteSense(testRef); const expectedWords = testTreeWords(); - delete expectedWords[srcWordId]; + // Sidebar sense _2 removed so _0, _1 remain. + expectedWords[wordId].sensesGuids[mergeSenseId] = [ + `${mergeSenseId}_0`, + `${mergeSenseId}_1`, + ]; - checkTreeWords(testAction, expectedWords); + checkTree(testAction, expectedWords); }); }); @@ -347,12 +345,12 @@ describe("MergeDupsReducer", () => { const expectedWords = testTreeWords(); expectedWords[wordId].flag = testFlag; - checkTreeWords(testAction, expectedWords); + checkTree(testAction, expectedWords); }); }); describe("getMergeWords", () => { - it("sense moved from one word to another", () => { + it("moves sense from one word to another", () => { const store = setupStore(mergeTwoWordsScenario.initialState()); store.dispatch(getMergeWords()); const mergeArray = store.getState().mergeDuplicateGoal.mergeWords; @@ -369,7 +367,7 @@ describe("MergeDupsReducer", () => { expect(defs).toEqual(expectedResult[0].defs); }); - it("sense from one word combined with sense in another", () => { + it("combines sense from one word with sense in another", () => { const store = setupStore(mergeTwoSensesScenario.initialState()); store.dispatch(getMergeWords()); const mergeArray = store.getState().mergeDuplicateGoal.mergeWords; @@ -386,7 +384,7 @@ describe("MergeDupsReducer", () => { expect(defs).toEqual(expectedResult[0].defs); }); - it("combine senses with definitions", () => { + it("combines senses with definitions", () => { const store = setupStore(mergeTwoDefinitionsScenario.initialState()); store.dispatch(getMergeWords()); const mergeArray = store.getState().mergeDuplicateGoal.mergeWords; @@ -405,146 +403,133 @@ describe("MergeDupsReducer", () => { }); describe("moveSense", () => { - it("moves a sense out from sidebar to same word", () => { + it("moves sense from 2-sense sidebar to start of same word; closes sidebar", () => { const wordId = "word2"; - const testRef: MergeTreeReference = { - wordId, - mergeSenseId: `${wordId}_senseA`, - order: 0, - }; - const srcGuid = `${testRef.mergeSenseId}_${testRef.order}`; + const mergeSenseId = `${wordId}_senseA`; + const testRef: MergeTreeReference = { wordId, mergeSenseId, order: 0 }; + const srcGuid = `${mergeSenseId}_${testRef.order}`; // Intercept the uuid that will be assigned. const nextGuid = getMockUuid(false); const testAction = moveSense({ src: testRef, destWordId: wordId, - destOrder: 1, + destOrder: 0, }); const expectedWords = testTreeWords(); - expectedWords[wordId].sensesGuids = { word2_senseA: ["word2_senseA_1"] }; - // A new guid is used when a sense is added to a merge word, so use the intercepted - // nextGuid for the new sense expected from moving a sense out of a sidebar. - expectedWords[wordId].sensesGuids[nextGuid] = [srcGuid]; + expectedWords[wordId].sensesGuids = { + // A new guid is used when a sense is added to a merge word, so use the intercepted + // nextGuid for the new sense expected from moving a sense out of a sidebar. + [nextGuid]: [srcGuid], + // Sidebar sense _0 moved so _1 remains. + [mergeSenseId]: ["word2_senseA_1"], + }; - checkTreeWords(testAction, expectedWords); + checkTree(testAction, expectedWords, true); }); - it("moves a sense out from sidebar to different word", () => { + it("moves sense from 2-sense sidebar to end of other word; closes sidebar", () => { const srcWordId = "word2"; + const mergeSenseId = `${srcWordId}_senseA`; const testRef: MergeTreeReference = { wordId: srcWordId, - mergeSenseId: `${srcWordId}_senseA`, + mergeSenseId, order: 1, }; - const srcGuid = `${testRef.mergeSenseId}_${testRef.order}`; + const srcGuid = `${mergeSenseId}_${testRef.order}`; const destWordId = "word3"; // Intercept the uuid that will be assigned. const nextGuid = getMockUuid(false); - const testAction = moveSense({ - src: testRef, - destWordId: destWordId, - destOrder: 2, - }); + const testAction = moveSense({ src: testRef, destWordId, destOrder: 2 }); const expectedWords = testTreeWords(); - expectedWords[srcWordId].sensesGuids = { - word2_senseA: ["word2_senseA_0"], - }; + // Sidebar sense _1 moved so _0 remains. + expectedWords[srcWordId].sensesGuids[mergeSenseId] = ["word2_senseA_0"]; // A new guid is used when a sense is added to a merge word, so use the intercepted // nextGuid for the new sense expected from moving a sense out of a sidebar. expectedWords[destWordId].sensesGuids[nextGuid] = [srcGuid]; - checkTreeWords(testAction, expectedWords); + checkTree(testAction, expectedWords, true); }); - it("moves last sense out from sidebar to different word", () => { + it("moves sense from 3-sense sidebar to new word", () => { const srcWordId = "word3"; + const mergeSenseId = `${srcWordId}_senseB`; const testRef: MergeTreeReference = { wordId: srcWordId, - mergeSenseId: `${srcWordId}_senseA`, + mergeSenseId, order: 0, }; + const srcGuid = `${mergeSenseId}_${testRef.order}`; - const destWordId = "word1"; + const destWordId = "new-word-id"; - const testAction = moveSense({ - src: testRef, - destWordId: destWordId, - destOrder: 1, - }); + // Intercept the uuid that will be assigned. + const nextGuid = getMockUuid(false); + const testAction = moveSense({ src: testRef, destWordId, destOrder: 0 }); const expectedWords = testTreeWords(); - expectedWords[srcWordId].sensesGuids = { - word3_senseB: ["word3_senseB_0", "word3_senseB_1"], - }; - expectedWords[destWordId].sensesGuids = { - word1_senseA: ["word1_senseA_0"], - word3_senseA: ["word3_senseA_0"], - }; + // Sidebar sense _0 moved so _1, _2 remain. + expectedWords[srcWordId].sensesGuids[mergeSenseId] = [ + `${mergeSenseId}_1`, + `${mergeSenseId}_2`, + ]; + expectedWords[destWordId] = newMergeTreeWord(); + // A new guid is used when a sense is added to a merge word, so use the intercepted + // nextGuid for the new sense expected from moving a sense out of a sidebar. + expectedWords[destWordId].sensesGuids[nextGuid] = [srcGuid]; - checkTreeWords(testAction, expectedWords); + checkTree(testAction, expectedWords); }); - it("moves a sense to a different word", () => { + it("moves sense to end of other word", () => { const srcWordId = "word3"; - const testRef: MergeTreeReference = { - wordId: srcWordId, - mergeSenseId: `${srcWordId}_senseB`, - }; + const mergeSenseId = `${srcWordId}_senseB`; + const testRef: MergeTreeReference = { wordId: srcWordId, mergeSenseId }; const destWordId = "word1"; - const testAction = moveSense({ - src: testRef, - destWordId: destWordId, - destOrder: 1, - }); + const testAction = moveSense({ src: testRef, destWordId, destOrder: 1 }); const expectedWords = testTreeWords(); expectedWords[srcWordId].sensesGuids = { + // _senseB moved so _senseA remains. word3_senseA: ["word3_senseA_0"], }; expectedWords[destWordId].sensesGuids = { word1_senseA: ["word1_senseA_0"], - word3_senseB: ["word3_senseB_0", "word3_senseB_1"], + [mergeSenseId]: ["word3_senseB_0", "word3_senseB_1", "word3_senseB_2"], }; - checkTreeWords(testAction, expectedWords); + checkTree(testAction, expectedWords); }); - it("moves last sense to a different word", () => { + it("moves last sense to start of other word", () => { const srcWordId = "word1"; - const testRef: MergeTreeReference = { - wordId: srcWordId, - mergeSenseId: `${srcWordId}_senseA`, - }; + const mergeSenseId = `${srcWordId}_senseA`; + const testRef: MergeTreeReference = { wordId: srcWordId, mergeSenseId }; const destWordId = "word2"; - const testAction = moveSense({ - src: testRef, - destWordId: destWordId, - destOrder: 1, - }); + const testAction = moveSense({ src: testRef, destWordId, destOrder: 0 }); expect(testAction.type).toEqual("mergeDupStepReducer/moveSenseAction"); const expectedWords = testTreeWords(); delete expectedWords[srcWordId]; expectedWords[destWordId].sensesGuids = { + [mergeSenseId]: ["word1_senseA_0"], word2_senseA: ["word2_senseA_0", "word2_senseA_1"], - word1_senseA: ["word1_senseA_0"], }; - checkTreeWords(testAction, expectedWords); + checkTree(testAction, expectedWords); }); }); describe("orderSense", () => { - it("order sidebar sense", () => { + it("orders sidebar sense", () => { const wordId = "word2"; const mergeSenseId = `${wordId}_senseA`; const testRef: MergeTreeReference = { wordId, mergeSenseId, order: 0 }; @@ -557,10 +542,10 @@ describe("MergeDupsReducer", () => { "word2_senseA_0", ]; - checkTreeWords(testAction, expectedWords); + checkTree(testAction, expectedWords); }); - it("order word sense", () => { + it("orders word sense", () => { const wordId = "word3"; const mergeSenseId = `${wordId}_senseA`; const testRef: MergeTreeReference = { wordId, mergeSenseId }; @@ -569,11 +554,11 @@ describe("MergeDupsReducer", () => { const expectedWords = testTreeWords(); expectedWords[wordId].sensesGuids = { - word3_senseB: ["word3_senseB_0", "word3_senseB_1"], + word3_senseB: ["word3_senseB_0", "word3_senseB_1", "word3_senseB_2"], word3_senseA: ["word3_senseA_0"], }; - checkTreeWords(testAction, expectedWords); + checkTree(testAction, expectedWords); }); }); From 179bb36683738eb09a9cb7d04c06dfb09d93988f Mon Sep 17 00:00:00 2001 From: "D. Ror" Date: Fri, 23 Feb 2024 14:27:32 -0500 Subject: [PATCH 15/20] [RecentEntry] Disable row buttons when editing vern/gloss (#2944) --- .../Buttons/IconButtonWithTooltip.tsx | 3 +- .../EntryCellComponents/DeleteEntry.tsx | 2 + .../EntryCellComponents/EntryNote.tsx | 12 +++- .../DataEntry/DataEntryTable/RecentEntry.tsx | 69 ++++++++++--------- .../DataEntryTable/tests/RecentEntry.test.tsx | 64 ++++++++++++++++- src/components/Pronunciations/AudioPlayer.tsx | 26 +++++-- .../Pronunciations/AudioRecorder.tsx | 2 + .../Pronunciations/PronunciationsBackend.tsx | 9 ++- .../Pronunciations/RecorderIcon.tsx | 11 ++- .../tests/AudioRecorder.test.tsx | 4 +- 10 files changed, 151 insertions(+), 51 deletions(-) diff --git a/src/components/Buttons/IconButtonWithTooltip.tsx b/src/components/Buttons/IconButtonWithTooltip.tsx index 51527916db..fd854f1e77 100644 --- a/src/components/Buttons/IconButtonWithTooltip.tsx +++ b/src/components/Buttons/IconButtonWithTooltip.tsx @@ -3,6 +3,7 @@ import { MouseEventHandler, ReactElement, ReactNode } from "react"; import { useTranslation } from "react-i18next"; interface IconButtonWithTooltipProps { + disabled?: boolean; icon: ReactElement; text?: ReactNode; textId?: string; @@ -27,7 +28,7 @@ export default function IconButtonWithTooltip( onClick={props.onClick} size={props.size || "medium"} id={props.buttonId} - disabled={!props.onClick} + disabled={props.disabled || !props.onClick} > {props.icon} diff --git a/src/components/DataEntry/DataEntryTable/EntryCellComponents/DeleteEntry.tsx b/src/components/DataEntry/DataEntryTable/EntryCellComponents/DeleteEntry.tsx index 5e6377311c..7ab950514c 100644 --- a/src/components/DataEntry/DataEntryTable/EntryCellComponents/DeleteEntry.tsx +++ b/src/components/DataEntry/DataEntryTable/EntryCellComponents/DeleteEntry.tsx @@ -12,6 +12,7 @@ interface DeleteEntryProps { // if no confirmId is specified, then there is no popup // and deletion will happen when the button is pressed confirmId?: string; + disabled?: boolean; wordId?: string; } @@ -34,6 +35,7 @@ export default function DeleteEntry(props: DeleteEntryProps): ReactElement { <> void | Promise; } @@ -18,11 +19,16 @@ export default function EntryNote(props: EntryNoteProps): ReactElement { <> t.palette.grey[700] }} /> + t.palette.grey[props.disabled ? 400 : 700] }} + /> ) : ( - t.palette.grey[700] }} /> + t.palette.grey[props.disabled ? 400 : 700] }} + /> ) } onClick={props.updateNote ? () => setNoteOpen(true) : undefined} diff --git a/src/components/DataEntry/DataEntryTable/RecentEntry.tsx b/src/components/DataEntry/DataEntryTable/RecentEntry.tsx index 95264f1f85..84419bc9d1 100644 --- a/src/components/DataEntry/DataEntryTable/RecentEntry.tsx +++ b/src/components/DataEntry/DataEntryTable/RecentEntry.tsx @@ -40,9 +40,19 @@ export function RecentEntry(props: RecentEntryProps): ReactElement { if (sense.glosses.length < 1) { sense.glosses.push(newGloss("", props.analysisLang.bcp47)); } + const [editing, setEditing] = useState(false); const [gloss, setGloss] = useState(firstGlossText(sense)); const [vernacular, setVernacular] = useState(props.entry.vernacular); + const updateGlossField = (gloss: string): void => { + setEditing(gloss !== firstGlossText(sense)); + setGloss(gloss); + }; + const updateVernField = (vern: string): void => { + setEditing(vern !== props.entry.vernacular); + setVernacular(vern); + }; + function conditionallyUpdateGloss(): void { if (firstGlossText(sense) !== gloss) { props.updateGloss(props.rowIndex, gloss); @@ -77,7 +87,7 @@ export function RecentEntry(props: RecentEntryProps): ReactElement { 1} - updateVernField={setVernacular} + updateVernField={updateVernField} onBlur={() => conditionallyUpdateVern()} handleEnter={() => { vernacular && props.focusNewEntry(); @@ -98,7 +108,7 @@ export function RecentEntry(props: RecentEntryProps): ReactElement { conditionallyUpdateGloss()} handleEnter={() => { gloss && props.focusNewEntry(); @@ -116,13 +126,12 @@ export function RecentEntry(props: RecentEntryProps): ReactElement { position: "relative", }} > - {!props.disabled && ( - - )} + - {!props.disabled && ( - { - props.delAudioFromWord(props.entry.id, fileName); - }} - replaceAudio={(audio) => - props.repAudioInWord(props.entry.id, audio) - } - uploadAudio={(file) => { - props.addAudioToWord(props.entry.id, file); - }} - /> - )} + { + props.delAudioFromWord(props.entry.id, fileName); + }} + replaceAudio={(audio) => props.repAudioInWord(props.entry.id, audio)} + uploadAudio={(file) => { + props.addAudioToWord(props.entry.id, file); + }} + /> - {!props.disabled && ( - - )} + ); diff --git a/src/components/DataEntry/DataEntryTable/tests/RecentEntry.test.tsx b/src/components/DataEntry/DataEntryTable/tests/RecentEntry.test.tsx index 1101939a44..3735826258 100644 --- a/src/components/DataEntry/DataEntryTable/tests/RecentEntry.test.tsx +++ b/src/components/DataEntry/DataEntryTable/tests/RecentEntry.test.tsx @@ -13,6 +13,7 @@ import "tests/reactI18nextMock"; import { Word } from "api/models"; import { defaultState } from "components/App/DefaultState"; import { + DeleteEntry, EntryNote, GlossWithSuggestions, VernWithSuggestions, @@ -21,6 +22,7 @@ import RecentEntry from "components/DataEntry/DataEntryTable/RecentEntry"; import { EditTextDialog } from "components/Dialogs"; import AudioPlayer from "components/Pronunciations/AudioPlayer"; import AudioRecorder from "components/Pronunciations/AudioRecorder"; +import PronunciationsBackend from "components/Pronunciations/PronunciationsBackend"; import theme from "types/theme"; import { newPronunciation, simpleWord } from "types/word"; import { newWritingSystem } from "types/writingSystem"; @@ -89,6 +91,34 @@ describe("ExistingEntry", () => { }); describe("vernacular", () => { + it("disables buttons if changing", async () => { + await renderWithWord(mockWord); + const vern = testHandle.findByType(VernWithSuggestions); + const note = testHandle.findByType(EntryNote); + const audio = testHandle.findByType(PronunciationsBackend); + const del = testHandle.findByType(DeleteEntry); + + expect(note.props.disabled).toBeFalsy(); + expect(audio.props.disabled).toBeFalsy(); + expect(del.props.disabled).toBeFalsy(); + + async function updateVern(text: string): Promise { + await act(async () => { + await vern.props.updateVernField(text); + }); + } + + await updateVern(mockText); + expect(note.props.disabled).toBeTruthy(); + expect(audio.props.disabled).toBeTruthy(); + expect(del.props.disabled).toBeTruthy(); + + await updateVern(mockVern); + expect(note.props.disabled).toBeFalsy(); + expect(audio.props.disabled).toBeFalsy(); + expect(del.props.disabled).toBeFalsy(); + }); + it("updates if changed", async () => { await renderWithWord(mockWord); testHandle = testHandle.findByType(VernWithSuggestions); @@ -102,11 +132,39 @@ describe("ExistingEntry", () => { await updateVernAndBlur(mockVern); expect(mockUpdateVern).toHaveBeenCalledTimes(0); await updateVernAndBlur(mockText); - expect(mockUpdateVern).toBeCalledWith(0, mockText); + expect(mockUpdateVern).toHaveBeenCalledWith(0, mockText); }); }); describe("gloss", () => { + it("disables buttons if changing", async () => { + await renderWithWord(mockWord); + const gloss = testHandle.findByType(GlossWithSuggestions); + const note = testHandle.findByType(EntryNote); + const audio = testHandle.findByType(PronunciationsBackend); + const del = testHandle.findByType(DeleteEntry); + + expect(note.props.disabled).toBeFalsy(); + expect(audio.props.disabled).toBeFalsy(); + expect(del.props.disabled).toBeFalsy(); + + async function updateGloss(text: string): Promise { + await act(async () => { + await gloss.props.updateGlossField(text); + }); + } + + await updateGloss(mockText); + expect(note.props.disabled).toBeTruthy(); + expect(audio.props.disabled).toBeTruthy(); + expect(del.props.disabled).toBeTruthy(); + + await updateGloss(mockGloss); + expect(note.props.disabled).toBeFalsy(); + expect(audio.props.disabled).toBeFalsy(); + expect(del.props.disabled).toBeFalsy(); + }); + it("updates if changed", async () => { await renderWithWord(mockWord); testHandle = testHandle.findByType(GlossWithSuggestions); @@ -120,7 +178,7 @@ describe("ExistingEntry", () => { await updateGlossAndBlur(mockGloss); expect(mockUpdateGloss).toHaveBeenCalledTimes(0); await updateGlossAndBlur(mockText); - expect(mockUpdateGloss).toBeCalledWith(0, mockText); + expect(mockUpdateGloss).toHaveBeenCalledWith(0, mockText); }); }); @@ -131,7 +189,7 @@ describe("ExistingEntry", () => { await act(async () => { testHandle.props.updateText(mockText); }); - expect(mockUpdateNote).toBeCalledWith(0, mockText); + expect(mockUpdateNote).toHaveBeenCalledWith(0, mockText); }); }); }); diff --git a/src/components/Pronunciations/AudioPlayer.tsx b/src/components/Pronunciations/AudioPlayer.tsx index 8222b8410f..74a2ff3a9f 100644 --- a/src/components/Pronunciations/AudioPlayer.tsx +++ b/src/components/Pronunciations/AudioPlayer.tsx @@ -10,7 +10,6 @@ import { Tooltip, } from "@mui/material"; import { - CSSProperties, MouseEvent, ReactElement, TouchEvent, @@ -32,11 +31,11 @@ import { import { PronunciationsStatus } from "components/Pronunciations/Redux/PronunciationsReduxTypes"; import { StoreState } from "types"; import { useAppDispatch, useAppSelector } from "types/hooks"; -import { themeColors } from "types/theme"; interface PlayerProps { audio: Pronunciation; deleteAudio?: (fileName: string) => void; + disabled?: boolean; onClick?: () => void; pronunciationUrl?: string; size?: "large" | "medium" | "small"; @@ -44,8 +43,6 @@ interface PlayerProps { warningTextId?: string; } -const iconStyle: CSSProperties = { color: themeColors.success }; - export default function AudioPlayer(props: PlayerProps): ReactElement { const isPlaying = useAppSelector( (state: StoreState) => @@ -178,6 +175,22 @@ export default function AudioPlayer(props: PlayerProps): ReactElement { ); } + const icon = isPlaying ? ( + + props.disabled ? t.palette.grey[400] : t.palette.success.main, + }} + /> + ) : ( + + props.disabled ? t.palette.grey[400] : t.palette.success.main, + }} + /> + ); + return ( <> - {isPlaying ? : } + {icon} - {isPlaying ? : } + {icon} {canChangeSpeaker && ( void; @@ -50,6 +51,7 @@ export default function AudioRecorder(props: RecorderProps): ReactElement { return ( {!props.playerOnly && !!props.uploadAudio && ( - + )} {audioButtons} @@ -60,6 +66,7 @@ function propsAreEqual( return false; } return ( + prev.disabled === next.disabled && prev.wordId === next.wordId && JSON.stringify(prev.audio) === JSON.stringify(next.audio) ); diff --git a/src/components/Pronunciations/RecorderIcon.tsx b/src/components/Pronunciations/RecorderIcon.tsx index 263c6dab7f..58526d3edb 100644 --- a/src/components/Pronunciations/RecorderIcon.tsx +++ b/src/components/Pronunciations/RecorderIcon.tsx @@ -16,6 +16,7 @@ export const recordButtonId = "recordingButton"; export const recordIconId = "recordingIcon"; interface RecorderIconProps { + disabled?: boolean; id: string; startRecording: () => void; stopRecording: () => void; @@ -61,6 +62,7 @@ export default function RecorderIcon(props: RecorderIconProps): ReactElement { + props.disabled + ? t.palette.grey[400] + : isRecording + ? themeColors.recordActive + : themeColors.recordIdle, }} /> diff --git a/src/components/Pronunciations/tests/AudioRecorder.test.tsx b/src/components/Pronunciations/tests/AudioRecorder.test.tsx index 546bca627f..82128d60a0 100644 --- a/src/components/Pronunciations/tests/AudioRecorder.test.tsx +++ b/src/components/Pronunciations/tests/AudioRecorder.test.tsx @@ -85,7 +85,7 @@ describe("Pronunciations", () => { ); }); const icon = testRenderer.root.findByProps({ id: recordIconId }); - expect(icon.props.sx.color).toEqual(themeColors.recordIdle); + expect(icon.props.sx.color({})).toEqual(themeColors.recordIdle); }); test("style depends on pronunciations state", () => { @@ -103,6 +103,6 @@ describe("Pronunciations", () => { ); }); const icon = testRenderer.root.findByProps({ id: recordIconId }); - expect(icon.props.sx.color).toEqual(themeColors.recordActive); + expect(icon.props.sx.color({})).toEqual(themeColors.recordActive); }); }); From 4979ed87b11cf4be73f7e556c1215e76c7bbbfe3 Mon Sep 17 00:00:00 2001 From: "D. Ror" Date: Fri, 23 Feb 2024 15:06:49 -0500 Subject: [PATCH 16/20] Fix uncalled/impotent test functions (#2945) --- .../AppBar/tests/NavigationButtons.test.tsx | 10 +++--- .../AppBar/tests/SpeakerMenu.test.tsx | 2 +- .../DataEntryTable/tests/index.test.tsx | 6 ++-- src/components/DataEntry/tests/index.test.tsx | 4 +-- .../PasswordReset/tests/ResetPage.test.tsx | 2 +- .../tests/ProjectImport.test.tsx | 4 +-- .../tests/ProjectSpeakersList.test.tsx | 2 +- .../ProjectUsers/tests/SortOptions.test.tsx | 1 - .../tests/SpeakerConsentListItemIcon.test.tsx | 34 +++++++++---------- .../Redux/tests/ReviewEntriesActions.test.tsx | 6 ++-- src/utilities/tests/dictionaryLoader.test.ts | 4 +-- 11 files changed, 35 insertions(+), 40 deletions(-) diff --git a/src/components/AppBar/tests/NavigationButtons.test.tsx b/src/components/AppBar/tests/NavigationButtons.test.tsx index 2136106f80..429fdfeed0 100644 --- a/src/components/AppBar/tests/NavigationButtons.test.tsx +++ b/src/components/AppBar/tests/NavigationButtons.test.tsx @@ -27,7 +27,7 @@ const mockStore = configureMockStore()({ let testRenderer: renderer.ReactTestRenderer; let entryButton: ReactTestInstance; -let cleanButton: ReactTestInstance; +let cleanButton: ReactTestInstance | null; const renderNavButtons = async ( path: Path, @@ -57,9 +57,7 @@ const renderNavButtonsWithPermission = async ( const cleanupButtons = testRenderer.root.findAllByProps({ id: dataCleanupButtonId, }); - if (cleanupButtons.length) { - cleanButton = cleanupButtons[0]; - } + cleanButton = cleanupButtons.length ? cleanupButtons[0] : null; }; beforeEach(() => { @@ -74,9 +72,9 @@ describe("NavigationButtons", () => { perm === Permission.CharacterInventory || perm === Permission.MergeAndReviewEntries ) { - expect(cleanButton).toBeTruthy; + expect(cleanButton).toBeTruthy(); } else { - expect(cleanButton).toBeUndefined; + expect(cleanButton).toBeNull(); } } }); diff --git a/src/components/AppBar/tests/SpeakerMenu.test.tsx b/src/components/AppBar/tests/SpeakerMenu.test.tsx index f1f19ed782..cd8c94b536 100644 --- a/src/components/AppBar/tests/SpeakerMenu.test.tsx +++ b/src/components/AppBar/tests/SpeakerMenu.test.tsx @@ -54,7 +54,7 @@ describe("SpeakerMenuList", () => { it("has one disabled menu item if no speakers", async () => { await renderMenuList(); const menuItem = testRenderer.root.findByType(MenuItem); - expect(menuItem).toBeDisabled; + expect(menuItem.props.disabled).toBeTruthy(); }); it("has divider and one more menu item than speakers", async () => { diff --git a/src/components/DataEntry/DataEntryTable/tests/index.test.tsx b/src/components/DataEntry/DataEntryTable/tests/index.test.tsx index baf6075ee7..067d47be7b 100644 --- a/src/components/DataEntry/DataEntryTable/tests/index.test.tsx +++ b/src/components/DataEntry/DataEntryTable/tests/index.test.tsx @@ -151,7 +151,6 @@ describe("DataEntryTable", () => { it("creates word when new entry has vernacular", async () => { expect(mockCreateWord).not.toHaveBeenCalled(); testHandle = testRenderer.root.findByType(NewEntry); - expect(testHandle).not.toBeNull; // Set newVern but not newGloss. await act(async () => testHandle.props.setNewVern("hasVern")); testHandle = testRenderer.root.findByProps({ id: exitButtonId }); @@ -161,7 +160,6 @@ describe("DataEntryTable", () => { it("doesn't create word when new entry has no vernacular", async () => { testHandle = testRenderer.root.findByType(NewEntry); - expect(testHandle).not.toBeNull; // Set newGloss but not newVern. await act(async () => testHandle.props.setNewGloss("hasGloss")); testHandle = testRenderer.root.findByProps({ id: exitButtonId }); @@ -231,8 +229,8 @@ describe("DataEntryTable", () => { describe("makeSemDomCurrent", () => { it("adds timestamp and the current user", () => { - expect(mockSemDom.created).toBeUndefined; - expect(mockSemDom.userId).toBeUndefined; + expect(mockSemDom.created).toBeUndefined(); + expect(mockSemDom.userId).toBeUndefined(); const currentDom = makeSemDomCurrent(mockSemDom); expect(currentDom.created).not.toBeUndefined(); diff --git a/src/components/DataEntry/tests/index.test.tsx b/src/components/DataEntry/tests/index.test.tsx index a459dc7ef9..0e272443c8 100644 --- a/src/components/DataEntry/tests/index.test.tsx +++ b/src/components/DataEntry/tests/index.test.tsx @@ -58,13 +58,13 @@ describe("DataEntry", () => { it("displays TreeView when state says the tree is open", async () => { await renderDataEntry({ currentDomain: mockDomain, open: true }); const dialog = testHandle.root.findByProps({ id: treeViewDialogId }); - expect(dialog.props.open).toBeTruthy; + expect(dialog.props.open).toBeTruthy(); }); it("doesn't displays TreeView when state says the tree is closed", async () => { await renderDataEntry({ currentDomain: mockDomain, open: false }); const dialog = testHandle.root.findByProps({ id: treeViewDialogId }); - expect(dialog.props.open).toBeFalsy; + expect(dialog.props.open).toBeFalsy(); }); it("dispatches to open the tree", async () => { diff --git a/src/components/PasswordReset/tests/ResetPage.test.tsx b/src/components/PasswordReset/tests/ResetPage.test.tsx index 85468addb8..ae7dd7bed7 100644 --- a/src/components/PasswordReset/tests/ResetPage.test.tsx +++ b/src/components/PasswordReset/tests/ResetPage.test.tsx @@ -178,6 +178,6 @@ describe("PasswordReset", () => { expect(screen.queryAllByTestId(id)).toHaveLength(0); } // The textId will show up as text because t() is mocked to return its input. - expect(screen.queryAllByText("passwordReset.invalidURL")).toBeTruthy; + expect(screen.queryAllByText("passwordReset.invalidURL")).toBeTruthy(); }); }); diff --git a/src/components/ProjectSettings/tests/ProjectImport.test.tsx b/src/components/ProjectSettings/tests/ProjectImport.test.tsx index 6288e1185a..aef6b9050c 100644 --- a/src/components/ProjectSettings/tests/ProjectImport.test.tsx +++ b/src/components/ProjectSettings/tests/ProjectImport.test.tsx @@ -27,7 +27,7 @@ const renderImport = async (): Promise => { describe("ProjectImport", () => { it("upload button disabled when no file selected", async () => { await renderImport(); - expect(uploadButton.props.disabled).toBeTruthy; + expect(uploadButton.props.disabled).toBeTruthy(); }); it("upload button enabled when file selected", async () => { @@ -35,6 +35,6 @@ describe("ProjectImport", () => { const selectButton = testRenderer.root.findByType(FileInputButton); const mockFile = { name: "name-of-a.file" } as File; await renderer.act(async () => selectButton.props.updateFile(mockFile)); - expect(uploadButton.props.disabled).toBeFalsy; + expect(uploadButton.props.disabled).toBeFalsy(); }); }); diff --git a/src/components/ProjectUsers/tests/ProjectSpeakersList.test.tsx b/src/components/ProjectUsers/tests/ProjectSpeakersList.test.tsx index 9a42c9368b..ac4cade32e 100644 --- a/src/components/ProjectUsers/tests/ProjectSpeakersList.test.tsx +++ b/src/components/ProjectUsers/tests/ProjectSpeakersList.test.tsx @@ -44,6 +44,6 @@ describe("ProjectSpeakersList", () => { expect(testRenderer.root.findAllByType(SpeakerListItem)).toHaveLength( mockSpeakers.length ); - expect(testRenderer.root.findByType(AddSpeakerListItem)).toBeTruthy; + expect(testRenderer.root.findByType(AddSpeakerListItem)).toBeTruthy(); }); }); diff --git a/src/components/ProjectUsers/tests/SortOptions.test.tsx b/src/components/ProjectUsers/tests/SortOptions.test.tsx index 2ca50cf237..38d41b7531 100644 --- a/src/components/ProjectUsers/tests/SortOptions.test.tsx +++ b/src/components/ProjectUsers/tests/SortOptions.test.tsx @@ -36,7 +36,6 @@ describe("SortOptions", () => { const mockReverse = jest.fn(); renderSortOptions({ onReverseClick: mockReverse }); const button = testRenderer.root.findByProps({ id: reverseButtonId }); - expect(button).not.toBeNull(); button.props.onClick(); expect(mockReverse).toHaveBeenCalledTimes(1); }); diff --git a/src/components/ProjectUsers/tests/SpeakerConsentListItemIcon.test.tsx b/src/components/ProjectUsers/tests/SpeakerConsentListItemIcon.test.tsx index 64ab376633..13ae4e0646 100644 --- a/src/components/ProjectUsers/tests/SpeakerConsentListItemIcon.test.tsx +++ b/src/components/ProjectUsers/tests/SpeakerConsentListItemIcon.test.tsx @@ -66,9 +66,9 @@ describe("SpeakerConsentListItemIcon", () => { ...mockSpeaker, consent: ConsentType.None, }); - expect(screen.queryByTestId(ListItemIconId.AddConsent)).not.toBeNull; - expect(screen.queryByTestId(ListItemIconId.PlayAudio)).toBeNull; - expect(screen.queryByTestId(ListItemIconId.ShowImage)).toBeNull; + expect(screen.queryByTestId(ListItemIconId.AddConsent)).not.toBeNull(); + expect(screen.queryByTestId(ListItemIconId.PlayAudio)).toBeNull(); + expect(screen.queryByTestId(ListItemIconId.ShowImage)).toBeNull(); }); it("opens menu when clicked", async () => { @@ -77,14 +77,14 @@ describe("SpeakerConsentListItemIcon", () => { ...mockSpeaker, consent: ConsentType.None, }); - expect(screen.queryByRole("menu")).toBeNull; - expect(screen.queryByTestId(ListItemIconId.RecordAudio)).toBeNull; - expect(screen.queryByTestId(ListItemIconId.UploadAudio)).toBeNull; + expect(screen.queryByRole("menu")).toBeNull(); + expect(screen.queryByTestId(ListItemIconId.RecordAudio)).toBeNull(); + expect(screen.queryByTestId(ListItemIconId.UploadAudio)).toBeNull(); await agent.click(screen.getByRole("button")); - expect(screen.queryByRole("menu")).not.toBeNull; - expect(screen.queryByTestId(ListItemIconId.RecordAudio)).not.toBeNull; - expect(screen.queryByTestId(ListItemIconId.UploadAudio)).not.toBeNull; + expect(screen.queryByRole("menu")).not.toBeNull(); + expect(screen.queryByTestId(ListItemIconId.RecordAudio)).not.toBeNull(); + expect(screen.queryByTestId(ListItemIconId.UploadAudio)).not.toBeNull(); }); }); @@ -94,9 +94,9 @@ describe("SpeakerConsentListItemIcon", () => { ...mockSpeaker, consent: ConsentType.Audio, }); - expect(screen.queryByTestId(ListItemIconId.AddConsent)).toBeNull; - expect(screen.queryByTestId(ListItemIconId.PlayAudio)).not.toBeNull; - expect(screen.queryByTestId(ListItemIconId.ShowImage)).toBeNull; + expect(screen.queryByTestId(ListItemIconId.AddConsent)).toBeNull(); + expect(screen.queryByTestId(ListItemIconId.PlayAudio)).not.toBeNull(); + expect(screen.queryByTestId(ListItemIconId.ShowImage)).toBeNull(); }); }); @@ -106,9 +106,9 @@ describe("SpeakerConsentListItemIcon", () => { ...mockSpeaker, consent: ConsentType.Image, }); - expect(screen.queryByTestId(ListItemIconId.AddConsent)).toBeNull; - expect(screen.queryByTestId(ListItemIconId.PlayAudio)).toBeNull; - expect(screen.queryByTestId(ListItemIconId.ShowImage)).not.toBeNull; + expect(screen.queryByTestId(ListItemIconId.AddConsent)).toBeNull(); + expect(screen.queryByTestId(ListItemIconId.PlayAudio)).toBeNull(); + expect(screen.queryByTestId(ListItemIconId.ShowImage)).not.toBeNull(); }); it("opens dialog when clicked", async () => { @@ -117,10 +117,10 @@ describe("SpeakerConsentListItemIcon", () => { ...mockSpeaker, consent: ConsentType.Image, }); - expect(screen.queryAllByRole("dialog")).toBeNull; + expect(screen.queryByRole("dialog")).toBeNull(); await agent.click(screen.getByRole("button")); - expect(screen.queryAllByRole("dialog")).not.toBeNull; + expect(screen.queryByRole("dialog")).not.toBeNull(); }); }); }); diff --git a/src/goals/ReviewEntries/Redux/tests/ReviewEntriesActions.test.tsx b/src/goals/ReviewEntries/Redux/tests/ReviewEntriesActions.test.tsx index 9380aae120..a078a11340 100644 --- a/src/goals/ReviewEntries/Redux/tests/ReviewEntriesActions.test.tsx +++ b/src/goals/ReviewEntries/Redux/tests/ReviewEntriesActions.test.tsx @@ -217,7 +217,7 @@ describe("ReviewEntriesActions", () => { // Verify the replacement word in state has the audio removed const words = store.getState().reviewEntriesState.words; - expect(words.find((w) => w.id === wordId)).toBeNull; + expect(words.find((w) => w.id === wordId)).toBeUndefined(); const wordInState = words.find((w) => w.id === newId); expect(wordInState?.audio).toHaveLength(0); }); @@ -247,7 +247,7 @@ describe("ReviewEntriesActions", () => { // Verify the replacement word in state has the updated speaker id const words = store.getState().reviewEntriesState.words; - expect(words.find((w) => w.id === wordId)).toBeNull; + expect(words.find((w) => w.id === wordId)).toBeUndefined(); const audioInState = words.find((w) => w.id === newId)?.audio; expect(audioInState).toHaveLength(1); expect(audioInState![0].speakerId).toEqual(speakerId); @@ -276,7 +276,7 @@ describe("ReviewEntriesActions", () => { // Verify the replacement word in state has the audio added const words = store.getState().reviewEntriesState.words; - expect(words.find((w) => w.id === wordId)).toBeNull; + expect(words.find((w) => w.id === wordId)).toBeUndefined(); const audioInState = words.find((w) => w.id === newId)?.audio; expect(audioInState).toHaveLength(1); expect(audioInState![0].fileName).toEqual(pro.fileName); diff --git a/src/utilities/tests/dictionaryLoader.test.ts b/src/utilities/tests/dictionaryLoader.test.ts index 2ba97d462f..9d84027bc3 100644 --- a/src/utilities/tests/dictionaryLoader.test.ts +++ b/src/utilities/tests/dictionaryLoader.test.ts @@ -57,8 +57,8 @@ describe("DictionaryLoader", () => { it("loads nothing for empty or non-existent key", async () => { const loader = new DictionaryLoader(bcp47); - expect(await loader.loadDictPart("")).toBeUndefined; - expect(await loader.loadDictPart("not-a-key")).toBeUndefined; + expect(await loader.loadDictPart("")).toBeUndefined(); + expect(await loader.loadDictPart("not-a-key")).toBeUndefined(); }); it("doesn't load the same part more than once", async () => { From 8bf5264f55003fcfb1e6288d057104444a4a2ccb Mon Sep 17 00:00:00 2001 From: Jim Grady Date: Fri, 1 Mar 2024 13:49:18 -0500 Subject: [PATCH 17/20] Dependabot updates for March 2024 (#2999) * Bump MongoDB.Driver from 2.23.1 to 2.24.0 in /Backend * Bump Microsoft.AspNetCore.Authentication.JwtBearer in /Backend * Bump Microsoft.NET.Test.Sdk from 17.8.0 to 17.9.0 in /Backend.Tests * Bump coverlet.msbuild from 6.0.0 to 6.0.1 in /Backend.Tests * Bump coverlet.collector from 6.0.0 to 6.0.1 in /Backend.Tests * Bump @types/uuid from 9.0.7 to 9.0.8 * Bump js-base64 from 3.7.5 to 3.7.7 * Bump @types/react from 18.2.51 to 18.2.61 * Bump node from 18.19.0-bookworm-slim to 18.19.1-bookworm-slim * Bump @typescript-eslint/parser from 6.20.0 to 7.1.0 * Bump mongo from 7.0.5-jammy to 7.0.6-jammy in /database * Bump actions/download-artifact from 4.1.1 to 4.1.3 * Bump docker/setup-buildx-action from 3.0.0 to 3.1.0 * Bump actions/setup-node from 4.0.1 to 4.0.2 * Bump github/codeql-action from 3.23.2 to 3.24.6 * Bump react-i18next from 14.0.1 to 14.0.5 * Bump dotnet/aspnet in /Backend * Bump dotnet/sdk in /Backend * Update Python dependencies * Update license reports --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/backend.yml | 8 +- .github/workflows/codeql.yml | 6 +- .github/workflows/combine_deploy_image.yml | 2 +- .github/workflows/frontend.yml | 6 +- .github/workflows/scorecards.yml | 2 +- Backend.Tests/Backend.Tests.csproj | 6 +- Backend/BackendFramework.csproj | 4 +- Backend/Dockerfile | 4 +- Dockerfile | 2 +- database/Dockerfile | 2 +- deploy/requirements.txt | 14 +- dev-requirements.txt | 36 +- .../assets/licenses/backend_licenses.txt | 70 +--- .../assets/licenses/frontend_licenses.txt | 8 +- maintenance/requirements.txt | 14 +- package-lock.json | 328 +++++++----------- package.json | 14 +- 17 files changed, 203 insertions(+), 323 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index ed4b9a6754..569590fa4f 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -85,7 +85,7 @@ jobs: - name: Checkout repository uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Download coverage artifact - uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 + uses: actions/download-artifact@87c55149d96e628cc2ef7e6fc2aab372015aec85 # v4.1.3 with: name: coverage - name: Upload coverage report @@ -129,11 +129,11 @@ jobs: with: dotnet-version: "6.0.x" - name: Initialize CodeQL - uses: github/codeql-action/init@b7bf0a3ed3ecfa44160715d7c442788f65f0f923 # v3.23.2 + uses: github/codeql-action/init@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6 with: languages: csharp - name: Autobuild - uses: github/codeql-action/autobuild@b7bf0a3ed3ecfa44160715d7c442788f65f0f923 # v3.23.2 + uses: github/codeql-action/autobuild@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6 - name: Upload artifacts if build failed uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 if: ${{ failure() }} @@ -141,7 +141,7 @@ jobs: name: tracer-logs path: ${{ runner.temp }}/*.log - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@b7bf0a3ed3ecfa44160715d7c442788f65f0f923 # v3.23.2 + uses: github/codeql-action/analyze@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6 docker_build: runs-on: ubuntu-22.04 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index f45ffe75d0..7f0bfc69b3 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -63,7 +63,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@b7bf0a3ed3ecfa44160715d7c442788f65f0f923 # v3.23.2 + uses: github/codeql-action/init@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -76,7 +76,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@b7bf0a3ed3ecfa44160715d7c442788f65f0f923 # v3.23.2 + uses: github/codeql-action/autobuild@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6 # Command-line programs to run using the OS shell. # See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -89,6 +89,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@b7bf0a3ed3ecfa44160715d7c442788f65f0f923 # v3.23.2 + uses: github/codeql-action/analyze@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/combine_deploy_image.yml b/.github/workflows/combine_deploy_image.yml index f0936540ee..fbf2c1cab9 100644 --- a/.github/workflows/combine_deploy_image.yml +++ b/.github/workflows/combine_deploy_image.yml @@ -40,7 +40,7 @@ jobs: - name: Set up QEMU uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 + uses: docker/setup-buildx-action@0d103c3126aa41d772a8362f6aa67afac040f80c # v3.1.0 - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@010d0da01d0b5a38af31e9c3470dbfdabdecca3a # v4.0.1 with: diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index 9e6e596d96..e19ede4f6c 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -31,7 +31,7 @@ jobs: - name: Checkout repository uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: ${{ matrix.node-version }} - run: npm ci @@ -60,7 +60,7 @@ jobs: - name: Checkout repository uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: ${{ matrix.node-version }} - run: npm ci @@ -95,7 +95,7 @@ jobs: - name: Checkout repository uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Download coverage artifact - uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 + uses: actions/download-artifact@87c55149d96e628cc2ef7e6fc2aab372015aec85 # v4.1.3 with: name: coverage - name: Upload coverage report diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 1db19cbece..eab14705cb 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -89,6 +89,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@b7bf0a3ed3ecfa44160715d7c442788f65f0f923 # v3.23.2 + uses: github/codeql-action/upload-sarif@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6 with: sarif_file: results.sarif diff --git a/Backend.Tests/Backend.Tests.csproj b/Backend.Tests/Backend.Tests.csproj index f4b913abc3..267e9ed42d 100644 --- a/Backend.Tests/Backend.Tests.csproj +++ b/Backend.Tests/Backend.Tests.csproj @@ -12,11 +12,11 @@ $(NoWarn);CA1305;CS1591 - + - - + + diff --git a/Backend/BackendFramework.csproj b/Backend/BackendFramework.csproj index 5d38158308..f0c49a8fed 100644 --- a/Backend/BackendFramework.csproj +++ b/Backend/BackendFramework.csproj @@ -13,11 +13,11 @@ NU1701 - + - + diff --git a/Backend/Dockerfile b/Backend/Dockerfile index 543e74bbc4..5fac8b9084 100644 --- a/Backend/Dockerfile +++ b/Backend/Dockerfile @@ -1,5 +1,5 @@ # Docker multi-stage build -FROM mcr.microsoft.com/dotnet/sdk:6.0.418-focal-amd64 AS builder +FROM mcr.microsoft.com/dotnet/sdk:6.0.419-focal-amd64 AS builder WORKDIR /app # Copy csproj and restore (fetch dependencies) as distinct layers. @@ -11,7 +11,7 @@ COPY . ./ RUN dotnet publish -c Release -o build # Build runtime image. -FROM mcr.microsoft.com/dotnet/aspnet:6.0.26-focal-amd64 +FROM mcr.microsoft.com/dotnet/aspnet:6.0.27-focal-amd64 ENV ASPNETCORE_URLS=http://+:5000 ENV COMBINE_IS_IN_CONTAINER=1 diff --git a/Dockerfile b/Dockerfile index 3c2c72db18..96bf055f4d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,7 @@ COPY docs/user_guide docs/user_guide RUN tox -e user-guide # Frontend build environment. -FROM node:18.19.0-bookworm-slim AS frontend_builder +FROM node:18.19.1-bookworm-slim AS frontend_builder WORKDIR /app # Install app dependencies. diff --git a/database/Dockerfile b/database/Dockerfile index ba10046a1e..6b426ebeb2 100644 --- a/database/Dockerfile +++ b/database/Dockerfile @@ -1,4 +1,4 @@ -FROM mongo:7.0.5-jammy +FROM mongo:7.0.6-jammy WORKDIR / diff --git a/deploy/requirements.txt b/deploy/requirements.txt index 94f1f9e83e..3794cf694b 100644 --- a/deploy/requirements.txt +++ b/deploy/requirements.txt @@ -4,11 +4,11 @@ # # pip-compile requirements.in # -ansible==9.2.0 +ansible==9.3.0 # via -r requirements.in -ansible-core==2.16.3 +ansible-core==2.16.4 # via ansible -cachetools==5.3.2 +cachetools==5.3.3 # via google-auth certifi==2024.2.2 # via @@ -18,11 +18,11 @@ cffi==1.16.0 # via cryptography charset-normalizer==3.3.2 # via requests -cryptography==42.0.2 +cryptography==42.0.5 # via # ansible-core # pyopenssl -google-auth==2.27.0 +google-auth==2.28.1 # via kubernetes idna==3.6 # via requests @@ -53,7 +53,7 @@ pycparser==2.21 # via cffi pyopenssl==24.0.0 # via -r requirements.in -python-dateutil==2.8.2 +python-dateutil==2.9.0 # via kubernetes pyyaml==6.0.1 # via @@ -74,7 +74,7 @@ six==1.16.0 # via # kubernetes # python-dateutil -urllib3==2.2.0 +urllib3==2.2.1 # via # kubernetes # requests diff --git a/dev-requirements.txt b/dev-requirements.txt index 56a041eb2f..0c2ce8adaa 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -12,9 +12,9 @@ babel==2.14.0 # via mkdocs-material beautifulsoup4==4.12.3 # via mkdocs-htmlproofer-plugin -black==24.1.1 +black==24.2.0 # via -r dev-requirements.in -cachetools==5.3.2 +cachetools==5.3.3 # via # google-auth # tox @@ -37,13 +37,13 @@ colorama==0.4.6 # -r dev-requirements.in # mkdocs-material # tox -cryptography==42.0.2 +cryptography==42.0.5 # via # pyopenssl # types-pyopenssl distlib==0.3.8 # via virtualenv -dnspython==2.5.0 +dnspython==2.6.1 # via pymongo eradicate==2.3.0 # via flake8-eradicate @@ -61,7 +61,7 @@ flake8==7.0.0 # pep8-naming flake8-broken-line==1.0.0 # via -r dev-requirements.in -flake8-bugbear==24.1.17 +flake8-bugbear==24.2.6 # via -r dev-requirements.in flake8-comprehensions==3.14.0 # via -r dev-requirements.in @@ -69,7 +69,7 @@ flake8-eradicate==1.5.0 # via -r dev-requirements.in ghp-import==2.1.0 # via mkdocs -google-auth==2.27.0 +google-auth==2.28.1 # via kubernetes humanfriendly==10.0 # via -r dev-requirements.in @@ -106,13 +106,13 @@ mkdocs==1.5.3 # mkdocs-htmlproofer-plugin # mkdocs-material # mkdocs-static-i18n -mkdocs-htmlproofer-plugin==1.0.0 +mkdocs-htmlproofer-plugin==1.1.0 # via -r dev-requirements.in -mkdocs-material==9.5.7 +mkdocs-material==9.5.12 # via -r dev-requirements.in mkdocs-material-extensions==1.3.1 # via mkdocs-material -mkdocs-static-i18n==1.2.0 +mkdocs-static-i18n==1.2.2 # via -r dev-requirements.in mypy==1.8.0 # via -r dev-requirements.in @@ -162,7 +162,7 @@ pygments==2.17.2 # via mkdocs-material pymdown-extensions==10.7 # via mkdocs-material -pymongo==4.6.1 +pymongo==4.6.2 # via -r dev-requirements.in pyopenssl==24.0.0 # via -r dev-requirements.in @@ -170,7 +170,7 @@ pyproject-api==1.6.1 # via tox pyreadline3==3.4.1 # via -r dev-requirements.in -python-dateutil==2.8.2 +python-dateutil==2.9.0 # via # ghp-import # kubernetes @@ -207,28 +207,28 @@ tomli==2.0.1 # mypy # pyproject-api # tox -tox==4.12.1 +tox==4.13.0 # via -r dev-requirements.in -types-pyopenssl==24.0.0.20240130 +types-pyopenssl==24.0.0.20240228 # via -r dev-requirements.in types-python-dateutil==2.8.19.20240106 # via -r dev-requirements.in types-pyyaml==6.0.12.12 # via -r dev-requirements.in -types-requests==2.31.0.20240125 +types-requests==2.31.0.20240218 # via -r dev-requirements.in -typing-extensions==4.9.0 +typing-extensions==4.10.0 # via # black # mypy -urllib3==2.2.0 +urllib3==2.2.1 # via # kubernetes # requests # types-requests -virtualenv==20.25.0 +virtualenv==20.25.1 # via tox -watchdog==3.0.0 +watchdog==4.0.0 # via mkdocs websocket-client==1.7.0 # via kubernetes diff --git a/docs/user_guide/assets/licenses/backend_licenses.txt b/docs/user_guide/assets/licenses/backend_licenses.txt index dddd26599d..e483b83977 100644 --- a/docs/user_guide/assets/licenses/backend_licenses.txt +++ b/docs/user_guide/assets/licenses/backend_licenses.txt @@ -97,11 +97,11 @@ license Type: #################################################################################################### Package:Microsoft.AspNetCore.Authentication.JwtBearer -Version:7.0.3 +Version:6.0.27 project URL:https://asp.net/ Description:ASP.NET Core middleware that enables an application to receive an OpenID Connect bearer token. -This package was built from the source code at https://github.com/dotnet/aspnetcore/tree/febee99db845fd8766a13bdb391a07c3ee90b4ba +This package was built from the source code at https://github.com/dotnet/aspnetcore/tree/417d650029c720dbd2138bcafbb78e2e4ff31bff licenseUrl:https://licenses.nuget.org/MIT license Type:MIT @@ -602,14 +602,6 @@ Description:A package containing thin abstractions for Microsoft.IdentityModel. licenseUrl:https://licenses.nuget.org/MIT license Type:MIT -#################################################################################################### -Package:Microsoft.IdentityModel.JsonWebTokens -Version:6.15.1 -project URL:https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet -Description:Includes types that provide support for creating, serializing and validating JSON Web Tokens. -licenseUrl:https://licenses.nuget.org/MIT -license Type:MIT - #################################################################################################### Package:Microsoft.IdentityModel.JsonWebTokens Version:6.35.0 @@ -618,14 +610,6 @@ Description:Includes types that provide support for creating, serializing and va licenseUrl:https://licenses.nuget.org/MIT license Type:MIT -#################################################################################################### -Package:Microsoft.IdentityModel.Logging -Version:6.15.1 -project URL:https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet -Description:Includes Event Source based logging support. -licenseUrl:https://licenses.nuget.org/MIT -license Type:MIT - #################################################################################################### Package:Microsoft.IdentityModel.Logging Version:6.35.0 @@ -636,7 +620,7 @@ license Type:MIT #################################################################################################### Package:Microsoft.IdentityModel.Protocols -Version:6.15.1 +Version:6.35.0 project URL:https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet Description:Provides base protocol support for OpenIdConnect and WsFederation. licenseUrl:https://licenses.nuget.org/MIT @@ -644,20 +628,12 @@ license Type:MIT #################################################################################################### Package:Microsoft.IdentityModel.Protocols.OpenIdConnect -Version:6.15.1 +Version:6.35.0 project URL:https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet Description:Includes types that provide support for OpenIdConnect protocol. licenseUrl:https://licenses.nuget.org/MIT license Type:MIT -#################################################################################################### -Package:Microsoft.IdentityModel.Tokens -Version:6.15.1 -project URL:https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet -Description:Includes types that provide support for SecurityTokens, Cryptographic operations: Signing, Verifying Signatures, Encryption. -licenseUrl:https://licenses.nuget.org/MIT -license Type:MIT - #################################################################################################### Package:Microsoft.IdentityModel.Tokens Version:6.35.0 @@ -809,34 +785,34 @@ license Type:MIT #################################################################################################### Package:MongoDB.Bson -Version:2.23.1 +Version:2.24.0 project URL:https://www.mongodb.com/docs/drivers/csharp/ Description:MongoDB's Official Bson Library. -licenseUrl:https://www.nuget.org/packages/MongoDB.Bson/2.23.1/License +licenseUrl:https://www.nuget.org/packages/MongoDB.Bson/2.24.0/License license Type:LICENSE.md #################################################################################################### Package:MongoDB.Driver -Version:2.23.1 +Version:2.24.0 project URL:https://www.mongodb.com/docs/drivers/csharp/ Description:Official .NET driver for MongoDB. -licenseUrl:https://www.nuget.org/packages/MongoDB.Driver/2.23.1/License +licenseUrl:https://www.nuget.org/packages/MongoDB.Driver/2.24.0/License license Type:LICENSE.md #################################################################################################### Package:MongoDB.Driver.Core -Version:2.23.1 +Version:2.24.0 project URL:https://www.mongodb.com/docs/drivers/csharp/ Description:Core Component of the Official MongoDB .NET Driver. -licenseUrl:https://www.nuget.org/packages/MongoDB.Driver.Core/2.23.1/License +licenseUrl:https://www.nuget.org/packages/MongoDB.Driver.Core/2.24.0/License license Type:LICENSE.md #################################################################################################### Package:MongoDB.Libmongocrypt -Version:1.8.0 +Version:1.8.2 project URL:http://www.mongodb.org/display/DOCS/CSharp+Language+Center Description:Libmongocrypt wrapper for the .NET driver. -licenseUrl:https://www.nuget.org/packages/MongoDB.Libmongocrypt/1.8.0/License +licenseUrl:https://www.nuget.org/packages/MongoDB.Libmongocrypt/1.8.2/License license Type:License.txt #################################################################################################### @@ -1100,30 +1076,30 @@ license Type:MS-EULA #################################################################################################### Package:System.Buffers -Version:4.3.0 +Version:4.5.1 project URL:https://dot.net/ Description:Provides resource pooling of any type for performance-critical applications that allocate and deallocate objects frequently. Commonly Used Types: System.Buffers.ArrayPool +7601f4f6225089ffb291dc7d58293c7bbf5c5d4f When using NuGet 3.x this package requires at least version 3.4. -licenseUrl:http://go.microsoft.com/fwlink/?LinkId=329770 -license Type:MS-EULA +licenseUrl:https://github.com/dotnet/corefx/blob/master/LICENSE.TXT +license Type:MIT #################################################################################################### Package:System.Buffers -Version:4.5.1 +Version:4.3.0 project URL:https://dot.net/ Description:Provides resource pooling of any type for performance-critical applications that allocate and deallocate objects frequently. Commonly Used Types: System.Buffers.ArrayPool -7601f4f6225089ffb291dc7d58293c7bbf5c5d4f When using NuGet 3.x this package requires at least version 3.4. -licenseUrl:https://github.com/dotnet/corefx/blob/master/LICENSE.TXT -license Type:MIT +licenseUrl:http://go.microsoft.com/fwlink/?LinkId=329770 +license Type:MS-EULA #################################################################################################### Package:System.Buffers @@ -1724,14 +1700,6 @@ When using NuGet 3.x this package requires at least version 3.4. licenseUrl:http://go.microsoft.com/fwlink/?LinkId=329770 license Type:MS-EULA -#################################################################################################### -Package:System.IdentityModel.Tokens.Jwt -Version:6.15.1 -project URL:https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet -Description:Includes types that provide support for creating, serializing and validating JSON Web Tokens. -licenseUrl:https://licenses.nuget.org/MIT -license Type:MIT - #################################################################################################### Package:System.IdentityModel.Tokens.Jwt Version:6.35.0 diff --git a/docs/user_guide/assets/licenses/frontend_licenses.txt b/docs/user_guide/assets/licenses/frontend_licenses.txt index 407737934e..9a041da7f9 100644 --- a/docs/user_guide/assets/licenses/frontend_licenses.txt +++ b/docs/user_guide/assets/licenses/frontend_licenses.txt @@ -40572,7 +40572,7 @@ MIT SOFTWARE -@types/react 18.2.51 +@types/react 18.2.61 MIT MIT License @@ -41808,7 +41808,7 @@ The above copyright notice and this permission notice shall be included in all c 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. -follow-redirects 1.15.3 +follow-redirects 1.15.5 MIT Copyright 2014–present Olivier Lalonde , James Talmage , Ruben Verborgh @@ -42435,7 +42435,7 @@ 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. -js-base64 3.7.5 +js-base64 3.7.7 BSD-3-Clause Copyright (c) 2014, Dan Kogai All rights reserved. @@ -43336,7 +43336,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -react-i18next 14.0.1 +react-i18next 14.0.5 MIT The MIT License (MIT) diff --git a/maintenance/requirements.txt b/maintenance/requirements.txt index dfba730e59..0220a85259 100644 --- a/maintenance/requirements.txt +++ b/maintenance/requirements.txt @@ -4,7 +4,7 @@ # # pip-compile requirements.in # -cachetools==5.3.2 +cachetools==5.3.3 # via google-auth certifi==2024.2.2 # via @@ -14,11 +14,11 @@ cffi==1.16.0 # via cryptography charset-normalizer==3.3.2 # via requests -cryptography==42.0.2 +cryptography==42.0.5 # via pyopenssl -dnspython==2.5.0 +dnspython==2.6.1 # via pymongo -google-auth==2.27.0 +google-auth==2.28.1 # via kubernetes humanfriendly==10.0 # via -r requirements.in @@ -38,11 +38,11 @@ pyasn1-modules==0.3.0 # via google-auth pycparser==2.21 # via cffi -pymongo==4.6.1 +pymongo==4.6.2 # via -r requirements.in pyopenssl==24.0.0 # via -r requirements.in -python-dateutil==2.8.2 +python-dateutil==2.9.0 # via kubernetes pyyaml==6.0.1 # via kubernetes @@ -58,7 +58,7 @@ six==1.16.0 # via # kubernetes # python-dateutil -urllib3==2.2.0 +urllib3==2.2.1 # via # kubernetes # requests diff --git a/package-lock.json b/package-lock.json index 92b76bcae8..0e400f6efa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,7 @@ "i18next": "^23.4.6", "i18next-browser-languagedetector": "^7.1.0", "i18next-http-backend": "^2.2.2", - "js-base64": "^3.7.5", + "js-base64": "^3.7.7", "make-dir": "^4.0.0", "motion": "^10.16.2", "mui-language-picker": "^1.2.8", @@ -42,7 +42,7 @@ "react-beautiful-dnd": "^13.1.1", "react-chartjs-2": "^5.2.0", "react-dom": "^18.2.0", - "react-i18next": "^14.0.1", + "react-i18next": "^14.0.5", "react-modal": "^3.16.1", "react-redux": "^8.1.3", "react-router-dom": "^6.16.0", @@ -66,7 +66,7 @@ "@types/loadable__component": "^5.13.8", "@types/node": "^20.10.6", "@types/nspell": "^2.1.5", - "@types/react": "^18.2.51", + "@types/react": "^18.2.61", "@types/react-beautiful-dnd": "^13.1.4", "@types/react-dom": "^18.2.15", "@types/react-modal": "^3.16.0", @@ -74,17 +74,17 @@ "@types/recordrtc": "^5.6.12", "@types/redux-mock-store": "^1.0.4", "@types/segment-analytics": "^0.0.37", - "@types/uuid": "^9.0.4", + "@types/uuid": "^9.0.8", "@types/validator": "^13.11.1", - "@typescript-eslint/eslint-plugin": "^6.7.2", - "@typescript-eslint/parser": "^6.20.0", + "@typescript-eslint/eslint-plugin": "^7.1.0", + "@typescript-eslint/parser": "^7.1.0", "css-mediaquery": "^0.1.2", "eslint": "^8.51.0", "eslint-import-resolver-typescript": "^3.6.0", "eslint-plugin-import": "^2.29.0", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.3.0", - "eslint-plugin-unused-imports": "^3.0.0", + "eslint-plugin-unused-imports": "^3.1.0", "hunspell-reader": "^7.0.0", "jest-canvas-mock": "^2.5.2", "license-checker-rseidelsohn": "^4.2.11", @@ -3020,9 +3020,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", - "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -3066,9 +3066,9 @@ "dev": true }, "node_modules/@eslint/js": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.53.0.tgz", - "integrity": "sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -3127,13 +3127,13 @@ } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.13", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", - "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^2.0.1", - "debug": "^4.1.1", + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", "minimatch": "^3.0.5" }, "engines": { @@ -3177,9 +3177,9 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", - "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", + "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, "node_modules/@isaacs/cliui": { @@ -8740,9 +8740,9 @@ "dev": true }, "node_modules/@types/react": { - "version": "18.2.51", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.51.tgz", - "integrity": "sha512-XeoMaU4CzyjdRr3c4IQQtiH7Rpo18V07rYZUucEZQwOUEtGgTXv7e6igQiQ+xnV6MbMe1qjEmKdgMNnfppnXfg==", + "version": "18.2.61", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.61.tgz", + "integrity": "sha512-NURTN0qNnJa7O/k4XUkEW2yfygA+NxS0V5h1+kp9jPwhzZy95q3ADoGMP0+JypMhrZBTTgjKAUlTctde1zzeQA==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -8908,9 +8908,9 @@ "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" }, "node_modules/@types/uuid": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.7.tgz", - "integrity": "sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g==", + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", "dev": true }, "node_modules/@types/validator": { @@ -8944,16 +8944,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.11.0.tgz", - "integrity": "sha512-uXnpZDc4VRjY4iuypDBKzW1rz9T5YBBK0snMn8MaTSNd2kMlj50LnLBABELjJiOL5YHk7ZD8hbSpI9ubzqYI0w==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.1.0.tgz", + "integrity": "sha512-j6vT/kCulhG5wBmGtstKeiVr1rdXE4nk+DT1k6trYkwlrvW9eOF5ZbgKnd/YR6PcM4uTEXa0h6Fcvf6X7Dxl0w==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.11.0", - "@typescript-eslint/type-utils": "6.11.0", - "@typescript-eslint/utils": "6.11.0", - "@typescript-eslint/visitor-keys": "6.11.0", + "@typescript-eslint/scope-manager": "7.1.0", + "@typescript-eslint/type-utils": "7.1.0", + "@typescript-eslint/utils": "7.1.0", + "@typescript-eslint/visitor-keys": "7.1.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -8969,8 +8969,8 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", - "eslint": "^7.0.0 || ^8.0.0" + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -9166,15 +9166,15 @@ "dev": true }, "node_modules/@typescript-eslint/parser": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.20.0.tgz", - "integrity": "sha512-bYerPDF/H5v6V76MdMYhjwmwgMA+jlPVqjSDq2cRqMi8bP5sR3Z+RLOiOMad3nsnmDVmn2gAFCyNgh/dIrfP/w==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.1.0.tgz", + "integrity": "sha512-V1EknKUubZ1gWFjiOZhDSNToOjs63/9O0puCgGS8aDOgpZY326fzFu15QAUjwaXzRZjf/qdsdBrckYdv9YxB8w==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.20.0", - "@typescript-eslint/types": "6.20.0", - "@typescript-eslint/typescript-estree": "6.20.0", - "@typescript-eslint/visitor-keys": "6.20.0", + "@typescript-eslint/scope-manager": "7.1.0", + "@typescript-eslint/types": "7.1.0", + "@typescript-eslint/typescript-estree": "7.1.0", + "@typescript-eslint/visitor-keys": "7.1.0", "debug": "^4.3.4" }, "engines": { @@ -9185,7 +9185,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -9193,90 +9193,6 @@ } } }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.20.0.tgz", - "integrity": "sha512-p4rvHQRDTI1tGGMDFQm+GtxP1ZHyAh64WANVoyEcNMpaTFn3ox/3CcgtIlELnRfKzSs/DwYlDccJEtr3O6qBvA==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.20.0", - "@typescript-eslint/visitor-keys": "6.20.0" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.20.0.tgz", - "integrity": "sha512-MM9mfZMAhiN4cOEcUOEx+0HmuaW3WBfukBZPCfwSqFnQy0grXYtngKCqpQN339X3RrwtzspWJrpbrupKYUSBXQ==", - "dev": true, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.20.0.tgz", - "integrity": "sha512-RnRya9q5m6YYSpBN7IzKu9FmLcYtErkDkc8/dKv81I9QiLLtVBHrjz+Ev/crAqgMNW2FCsoZF4g2QUylMnJz+g==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.20.0", - "@typescript-eslint/visitor-keys": "6.20.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.20.0.tgz", - "integrity": "sha512-E8Cp98kRe4gKHjJD4NExXKz/zOJ1A2hhZc+IMVD6i7w4yjIvh6VyuRI0gRtxAsXtoC35uGMaQ9rjI2zJaXDEAw==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.20.0", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/@typescript-eslint/parser/node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -9294,21 +9210,6 @@ } } }, - "node_modules/@typescript-eslint/parser/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@typescript-eslint/parser/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -9316,13 +9217,13 @@ "dev": true }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.11.0.tgz", - "integrity": "sha512-0A8KoVvIURG4uhxAdjSaxy8RdRE//HztaZdG8KiHLP8WOXSk0vlF7Pvogv+vlJA5Rnjj/wDcFENvDaHb+gKd1A==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.1.0.tgz", + "integrity": "sha512-6TmN4OJiohHfoOdGZ3huuLhpiUgOGTpgXNUPJgeZOZR3DnIpdSgtt83RS35OYNNXxM4TScVlpVKC9jyQSETR1A==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.11.0", - "@typescript-eslint/visitor-keys": "6.11.0" + "@typescript-eslint/types": "7.1.0", + "@typescript-eslint/visitor-keys": "7.1.0" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -9333,13 +9234,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.11.0.tgz", - "integrity": "sha512-nA4IOXwZtqBjIoYrJcYxLRO+F9ri+leVGoJcMW1uqr4r1Hq7vW5cyWrA43lFbpRvQ9XgNrnfLpIkO3i1emDBIA==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.1.0.tgz", + "integrity": "sha512-UZIhv8G+5b5skkcuhgvxYWHjk7FW7/JP5lPASMEUoliAPwIH/rxoUSQPia2cuOj9AmDZmwUl1usKm85t5VUMew==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.11.0", - "@typescript-eslint/utils": "6.11.0", + "@typescript-eslint/typescript-estree": "7.1.0", + "@typescript-eslint/utils": "7.1.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -9351,7 +9252,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -9383,9 +9284,9 @@ "dev": true }, "node_modules/@typescript-eslint/types": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.11.0.tgz", - "integrity": "sha512-ZbEzuD4DwEJxwPqhv3QULlRj8KYTAnNsXxmfuUXFCxZmO6CF2gM/y+ugBSAQhrqaJL3M+oe4owdWunaHM6beqA==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.1.0.tgz", + "integrity": "sha512-qTWjWieJ1tRJkxgZYXx6WUYtWlBc48YRxgY2JN1aGeVpkhmnopq+SUC8UEVGNXIvWH7XyuTjwALfG6bFEgCkQA==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -9396,16 +9297,17 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.11.0.tgz", - "integrity": "sha512-Aezzv1o2tWJwvZhedzvD5Yv7+Lpu1by/U1LZ5gLc4tCx8jUmuSCMioPFRjliN/6SJIvY6HpTtJIWubKuYYYesQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.1.0.tgz", + "integrity": "sha512-k7MyrbD6E463CBbSpcOnwa8oXRdHzH1WiVzOipK3L5KSML92ZKgUBrTlehdi7PEIMT8k0bQixHUGXggPAlKnOQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.11.0", - "@typescript-eslint/visitor-keys": "6.11.0", + "@typescript-eslint/types": "7.1.0", + "@typescript-eslint/visitor-keys": "7.1.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", + "minimatch": "9.0.3", "semver": "^7.5.4", "ts-api-utils": "^1.0.1" }, @@ -9422,6 +9324,15 @@ } } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -9439,6 +9350,21 @@ } } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@typescript-eslint/typescript-estree/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -9446,17 +9372,17 @@ "dev": true }, "node_modules/@typescript-eslint/utils": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.11.0.tgz", - "integrity": "sha512-p23ibf68fxoZy605dc0dQAEoUsoiNoP3MD9WQGiHLDuTSOuqoTsa4oAy+h3KDkTcxbbfOtUjb9h3Ta0gT4ug2g==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.1.0.tgz", + "integrity": "sha512-WUFba6PZC5OCGEmbweGpnNJytJiLG7ZvDBJJoUcX4qZYf1mGZ97mO2Mps6O2efxJcJdRNpqweCistDbZMwIVHw==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.11.0", - "@typescript-eslint/types": "6.11.0", - "@typescript-eslint/typescript-estree": "6.11.0", + "@typescript-eslint/scope-manager": "7.1.0", + "@typescript-eslint/types": "7.1.0", + "@typescript-eslint/typescript-estree": "7.1.0", "semver": "^7.5.4" }, "engines": { @@ -9467,16 +9393,16 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^8.56.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.11.0.tgz", - "integrity": "sha512-+SUN/W7WjBr05uRxPggJPSzyB8zUpaYo2hByKasWbqr3PM8AXfZt8UHdNpBS1v9SA62qnSSMF3380SwDqqprgQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.1.0.tgz", + "integrity": "sha512-FhUqNWluiGNzlvnDZiXad4mZRhtghdoKW6e98GoEOYSu5cND+E39rG5KwJMUzeENwm1ztYBRqof8wMLP+wNPIA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.11.0", + "@typescript-eslint/types": "7.1.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -13114,16 +13040,16 @@ } }, "node_modules/eslint": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.53.0.tgz", - "integrity": "sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.3", - "@eslint/js": "8.53.0", - "@humanwhocodes/config-array": "^0.11.13", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -13925,9 +13851,9 @@ "dev": true }, "node_modules/eslint-plugin-unused-imports": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-3.0.0.tgz", - "integrity": "sha512-sduiswLJfZHeeBJ+MQaG+xYzSWdRXoSw61DpU13mzWumCkR0ufD0HmO4kdNokjrkluMHpj/7PJeN35pgbhW3kw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-3.1.0.tgz", + "integrity": "sha512-9l1YFCzXKkw1qtAru1RWUtG2EVDZY0a0eChKXcL+EZ5jitG7qxdctu4RnvhOJHv4xfmUf7h+JJPINlVpGhZMrw==", "dev": true, "dependencies": { "eslint-rule-composer": "^0.3.0" @@ -13936,8 +13862,8 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^6.0.0", - "eslint": "^8.0.0" + "@typescript-eslint/eslint-plugin": "6 - 7", + "eslint": "8" }, "peerDependenciesMeta": { "@typescript-eslint/eslint-plugin": { @@ -14897,20 +14823,6 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -15133,9 +15045,9 @@ } }, "node_modules/globals": { - "version": "13.23.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", - "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -19091,9 +19003,9 @@ } }, "node_modules/js-base64": { - "version": "3.7.5", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.5.tgz", - "integrity": "sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==" + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz", + "integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==" }, "node_modules/js-cookie": { "version": "3.0.1", @@ -23311,11 +23223,11 @@ "dev": true }, "node_modules/react-i18next": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.0.1.tgz", - "integrity": "sha512-TMV8hFismBmpMdIehoFHin/okfvgjFhp723RYgIqB4XyhDobVMyukyM3Z8wtTRmajyFMZrBl/OaaXF2P6WjUAw==", + "version": "14.0.5", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.0.5.tgz", + "integrity": "sha512-5+bQSeEtgJrMBABBL5lO7jPdSNAbeAZ+MlFWDw//7FnVacuVu3l9EeWFzBQvZsKy+cihkbThWOAThEdH8YjGEw==", "dependencies": { - "@babel/runtime": "^7.22.5", + "@babel/runtime": "^7.23.9", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { diff --git a/package.json b/package.json index 56d6151bb5..077165fc3a 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "i18next": "^23.4.6", "i18next-browser-languagedetector": "^7.1.0", "i18next-http-backend": "^2.2.2", - "js-base64": "^3.7.5", + "js-base64": "^3.7.7", "make-dir": "^4.0.0", "motion": "^10.16.2", "mui-language-picker": "^1.2.8", @@ -70,7 +70,7 @@ "react-beautiful-dnd": "^13.1.1", "react-chartjs-2": "^5.2.0", "react-dom": "^18.2.0", - "react-i18next": "^14.0.1", + "react-i18next": "^14.0.5", "react-modal": "^3.16.1", "react-redux": "^8.1.3", "react-router-dom": "^6.16.0", @@ -94,7 +94,7 @@ "@types/loadable__component": "^5.13.8", "@types/node": "^20.10.6", "@types/nspell": "^2.1.5", - "@types/react": "^18.2.51", + "@types/react": "^18.2.61", "@types/react-beautiful-dnd": "^13.1.4", "@types/react-dom": "^18.2.15", "@types/react-modal": "^3.16.0", @@ -102,17 +102,17 @@ "@types/recordrtc": "^5.6.12", "@types/redux-mock-store": "^1.0.4", "@types/segment-analytics": "^0.0.37", - "@types/uuid": "^9.0.4", + "@types/uuid": "^9.0.8", "@types/validator": "^13.11.1", - "@typescript-eslint/eslint-plugin": "^6.7.2", - "@typescript-eslint/parser": "^6.20.0", + "@typescript-eslint/eslint-plugin": "^7.1.0", + "@typescript-eslint/parser": "^7.1.0", "css-mediaquery": "^0.1.2", "eslint": "^8.51.0", "eslint-import-resolver-typescript": "^3.6.0", "eslint-plugin-import": "^2.29.0", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.3.0", - "eslint-plugin-unused-imports": "^3.0.0", + "eslint-plugin-unused-imports": "^3.1.0", "hunspell-reader": "^7.0.0", "jest-canvas-mock": "^2.5.2", "license-checker-rseidelsohn": "^4.2.11", From 842fecfd7d7ecff4a4e638db3556a3104971d05a Mon Sep 17 00:00:00 2001 From: "D. Ror" Date: Fri, 8 Mar 2024 10:52:52 -0500 Subject: [PATCH 18/20] Clean up goal redux to match other redux (#2974) --- src/components/App/DefaultState.ts | 4 +-- src/components/GoalTimeline/index.tsx | 5 +--- .../GoalTimeline/tests/GoalRedux.test.tsx | 18 ++++++------- .../GoalTimeline/tests/index.test.tsx | 8 +++--- .../ProjectScreen/CreateProjectActions.ts | 2 +- .../Redux/CharacterInventoryActions.ts | 8 +++--- .../tests/CharacterInventoryActions.test.tsx | 10 ++++---- src/goals/DefaultGoal/BaseGoalScreen.tsx | 2 +- src/goals/DefaultGoal/NextGoalScreen.tsx | 2 +- .../MergeDupsStep/SaveDeferButtons.tsx | 2 +- .../MergeDuplicates/Redux/MergeDupsActions.ts | 8 +++--- .../Redux/GoalActions.ts | 6 ++--- .../Redux/GoalReducer.ts | 2 +- .../Redux/GoalReduxTypes.ts} | 20 +++++++-------- .../Redux/ReviewEntriesActions.ts | 5 +--- .../Redux/tests/ReviewEntriesActions.test.tsx | 2 +- src/goals/ReviewEntries/tests/index.test.tsx | 2 +- src/rootReducer.ts | 2 +- src/types/goals.ts | 25 ++++++------------- src/types/index.ts | 2 +- 20 files changed, 60 insertions(+), 75 deletions(-) rename src/{components/GoalTimeline => goals}/Redux/GoalActions.ts (99%) rename src/{components/GoalTimeline => goals}/Redux/GoalReducer.ts (98%) rename src/{components/GoalTimeline/DefaultState.ts => goals/Redux/GoalReduxTypes.ts} (61%) diff --git a/src/components/App/DefaultState.ts b/src/components/App/DefaultState.ts index 25186a03dd..afb78a040e 100644 --- a/src/components/App/DefaultState.ts +++ b/src/components/App/DefaultState.ts @@ -1,4 +1,3 @@ -import { defaultState as goalTimelineState } from "components/GoalTimeline/DefaultState"; import { defaultState as loginState } from "components/Login/Redux/LoginReduxTypes"; import { defaultState as currentProjectState } from "components/Project/ProjectReduxTypes"; import { defaultState as exportProjectState } from "components/ProjectExport/Redux/ExportProjectReduxTypes"; @@ -6,6 +5,7 @@ import { defaultState as pronunciationsState } from "components/Pronunciations/R import { defaultState as treeViewState } from "components/TreeView/Redux/TreeViewReduxTypes"; import { defaultState as characterInventoryState } from "goals/CharacterInventory/Redux/CharacterInventoryReduxTypes"; import { defaultState as mergeDuplicateGoal } from "goals/MergeDuplicates/Redux/MergeDupsReduxTypes"; +import { defaultState as goalsState } from "goals/Redux/GoalReduxTypes"; import { defaultState as reviewEntriesState } from "goals/ReviewEntries/Redux/ReviewEntriesReduxTypes"; import { defaultState as analyticsState } from "types/Redux/analyticsReduxTypes"; @@ -23,7 +23,7 @@ export const defaultState = { pronunciationsState: { ...pronunciationsState }, //goal timeline and current goal - goalsState: { ...goalTimelineState }, + goalsState: { ...goalsState }, //merge duplicates goal and review deferred duplicates goal mergeDuplicateGoal: { ...mergeDuplicateGoal }, diff --git a/src/components/GoalTimeline/index.tsx b/src/components/GoalTimeline/index.tsx index b354192fe7..54954cbc59 100644 --- a/src/components/GoalTimeline/index.tsx +++ b/src/components/GoalTimeline/index.tsx @@ -10,10 +10,7 @@ import { useTranslation } from "react-i18next"; import { getCurrentPermissions, getGraylistEntries } from "backend"; import GoalList from "components/GoalTimeline/GoalList"; -import { - asyncAddGoal, - asyncGetUserEdits, -} from "components/GoalTimeline/Redux/GoalActions"; +import { asyncAddGoal, asyncGetUserEdits } from "goals/Redux/GoalActions"; import { StoreState } from "types"; import { Goal, GoalType } from "types/goals"; import { useAppDispatch, useAppSelector } from "types/hooks"; diff --git a/src/components/GoalTimeline/tests/GoalRedux.test.tsx b/src/components/GoalTimeline/tests/GoalRedux.test.tsx index 0a62d9bde8..2a2afbff6e 100644 --- a/src/components/GoalTimeline/tests/GoalRedux.test.tsx +++ b/src/components/GoalTimeline/tests/GoalRedux.test.tsx @@ -5,15 +5,6 @@ import "tests/reactI18nextMock"; import { Edit, MergeUndoIds, Permission, User, UserEdit } from "api/models"; import * as LocalStorage from "backend/localStorage"; import GoalTimeline from "components/GoalTimeline"; -import { - addCharInvChangesToGoal, - addCompletedMergeToGoal, - asyncAddGoal, - asyncAdvanceStep, - asyncGetUserEdits, - asyncUpdateGoal, - setCurrentGoal, -} from "components/GoalTimeline/Redux/GoalActions"; import { CharacterChange, CharacterStatus, @@ -28,6 +19,15 @@ import { ReviewDeferredDups, } from "goals/MergeDuplicates/MergeDupsTypes"; import { goalDataMock } from "goals/MergeDuplicates/Redux/tests/MergeDupsDataMock"; +import { + addCharInvChangesToGoal, + addCompletedMergeToGoal, + asyncAddGoal, + asyncAdvanceStep, + asyncGetUserEdits, + asyncUpdateGoal, + setCurrentGoal, +} from "goals/Redux/GoalActions"; import { setupStore } from "store"; import { GoalStatus, GoalType } from "types/goals"; import { Path } from "types/path"; diff --git a/src/components/GoalTimeline/tests/index.test.tsx b/src/components/GoalTimeline/tests/index.test.tsx index 8ba3017ee0..6ac4e07b3e 100644 --- a/src/components/GoalTimeline/tests/index.test.tsx +++ b/src/components/GoalTimeline/tests/index.test.tsx @@ -7,19 +7,19 @@ import "tests/reactI18nextMock"; import { Permission } from "api/models"; import GoalTimeline, { createSuggestionData } from "components/GoalTimeline"; -import { defaultState } from "components/GoalTimeline/DefaultState"; -import { Goal, GoalType, GoalsState } from "types/goals"; +import { type GoalsState, defaultState } from "goals/Redux/GoalReduxTypes"; +import { Goal, GoalType } from "types/goals"; import { goalTypeToGoal } from "utilities/goalUtilities"; jest.mock("backend", () => ({ getCurrentPermissions: () => mockGetCurrentPermissions(), getGraylistEntries: (maxLists: number) => mockGetGraylistEntries(maxLists), })); -jest.mock("components/GoalTimeline/Redux/GoalActions", () => ({ +jest.mock("components/Pronunciations/Recorder"); +jest.mock("goals/Redux/GoalActions", () => ({ asyncAddGoal: (goal: Goal) => mockChooseGoal(goal), asyncGetUserEdits: () => jest.fn(), })); -jest.mock("components/Pronunciations/Recorder"); jest.mock("types/hooks", () => { return { ...jest.requireActual("types/hooks"), diff --git a/src/components/ProjectScreen/CreateProjectActions.ts b/src/components/ProjectScreen/CreateProjectActions.ts index 7270655fc4..25dedabca9 100644 --- a/src/components/ProjectScreen/CreateProjectActions.ts +++ b/src/components/ProjectScreen/CreateProjectActions.ts @@ -1,8 +1,8 @@ import { WritingSystem } from "api/models"; import { createProject, finishUploadLift, getProject } from "backend"; import router from "browserRouter"; -import { asyncCreateUserEdits } from "components/GoalTimeline/Redux/GoalActions"; import { setNewCurrentProject } from "components/Project/ProjectActions"; +import { asyncCreateUserEdits } from "goals/Redux/GoalActions"; import { StoreStateDispatch } from "types/Redux/actions"; import { Path } from "types/path"; import { newProject } from "types/project"; diff --git a/src/goals/CharacterInventory/Redux/CharacterInventoryActions.ts b/src/goals/CharacterInventory/Redux/CharacterInventoryActions.ts index c14ac5bc33..56d02e5a6c 100644 --- a/src/goals/CharacterInventory/Redux/CharacterInventoryActions.ts +++ b/src/goals/CharacterInventory/Redux/CharacterInventoryActions.ts @@ -3,10 +3,6 @@ import { Action, PayloadAction } from "@reduxjs/toolkit"; import { Project } from "api/models"; import { getFrontierWords } from "backend"; import router from "browserRouter"; -import { - addCharInvChangesToGoal, - asyncUpdateGoal, -} from "components/GoalTimeline/Redux/GoalActions"; import { asyncUpdateCurrentProject } from "components/Project/ProjectActions"; import { CharacterStatus, @@ -27,6 +23,10 @@ import { CharacterSetEntry, getCharacterStatus, } from "goals/CharacterInventory/Redux/CharacterInventoryReduxTypes"; +import { + addCharInvChangesToGoal, + asyncUpdateGoal, +} from "goals/Redux/GoalActions"; import { StoreState } from "types"; import { StoreStateDispatch } from "types/Redux/actions"; import { Path } from "types/path"; diff --git a/src/goals/CharacterInventory/Redux/tests/CharacterInventoryActions.test.tsx b/src/goals/CharacterInventory/Redux/tests/CharacterInventoryActions.test.tsx index 1151c04b77..1a71e2c5ba 100644 --- a/src/goals/CharacterInventory/Redux/tests/CharacterInventoryActions.test.tsx +++ b/src/goals/CharacterInventory/Redux/tests/CharacterInventoryActions.test.tsx @@ -27,15 +27,15 @@ jest.mock("backend", () => ({ getFrontierWords: (...args: any[]) => mockGetFrontierWords(...args), })); jest.mock("browserRouter"); -jest.mock("components/GoalTimeline/Redux/GoalActions", () => ({ - asyncUpdateGoal: (...args: any[]) => mockAsyncUpdateGoal(...args), - addCharInvChangesToGoal: (...args: any[]) => - mockAddCharInvChangesToGoal(...args), -})); jest.mock("components/Project/ProjectActions", () => ({ asyncUpdateCurrentProject: (...args: any[]) => mockAsyncUpdateCurrentProject(...args), })); +jest.mock("goals/Redux/GoalActions", () => ({ + asyncUpdateGoal: (...args: any[]) => mockAsyncUpdateGoal(...args), + addCharInvChangesToGoal: (...args: any[]) => + mockAddCharInvChangesToGoal(...args), +})); const mockAddCharInvChangesToGoal = jest.fn(); const mockAsyncUpdateCurrentProject = jest.fn(); diff --git a/src/goals/DefaultGoal/BaseGoalScreen.tsx b/src/goals/DefaultGoal/BaseGoalScreen.tsx index dbcb5fd7f4..3e7b4fbefb 100644 --- a/src/goals/DefaultGoal/BaseGoalScreen.tsx +++ b/src/goals/DefaultGoal/BaseGoalScreen.tsx @@ -1,11 +1,11 @@ import loadable from "@loadable/component"; import { ReactElement, useEffect } from "react"; -import { setCurrentGoal } from "components/GoalTimeline/Redux/GoalActions"; import PageNotFound from "components/PageNotFound/component"; import DisplayProgress from "goals/DefaultGoal/DisplayProgress"; import Loading from "goals/DefaultGoal/Loading"; import { clearTree } from "goals/MergeDuplicates/Redux/MergeDupsActions"; +import { setCurrentGoal } from "goals/Redux/GoalActions"; import { resetReviewEntries } from "goals/ReviewEntries/Redux/ReviewEntriesActions"; import { StoreState } from "types"; import { Goal, GoalStatus, GoalType } from "types/goals"; diff --git a/src/goals/DefaultGoal/NextGoalScreen.tsx b/src/goals/DefaultGoal/NextGoalScreen.tsx index ff4d45f03d..f001c263a9 100644 --- a/src/goals/DefaultGoal/NextGoalScreen.tsx +++ b/src/goals/DefaultGoal/NextGoalScreen.tsx @@ -1,9 +1,9 @@ import { ReactElement } from "react"; import { useNavigate } from "react-router-dom"; -import { asyncAddGoal } from "components/GoalTimeline/Redux/GoalActions"; import PageNotFound from "components/PageNotFound/component"; import MergeDupsContinueDialog from "goals/MergeDuplicates/MergeDupsContinueDialog"; +import { asyncAddGoal } from "goals/Redux/GoalActions"; import { StoreState } from "types"; import { GoalType } from "types/goals"; import { useAppDispatch, useAppSelector } from "types/hooks"; diff --git a/src/goals/MergeDuplicates/MergeDupsStep/SaveDeferButtons.tsx b/src/goals/MergeDuplicates/MergeDupsStep/SaveDeferButtons.tsx index 2417432cec..d1a4655989 100644 --- a/src/goals/MergeDuplicates/MergeDupsStep/SaveDeferButtons.tsx +++ b/src/goals/MergeDuplicates/MergeDupsStep/SaveDeferButtons.tsx @@ -3,12 +3,12 @@ import { ReactElement, useState } from "react"; import { useTranslation } from "react-i18next"; import { LoadingButton } from "components/Buttons"; -import { asyncAdvanceStep } from "components/GoalTimeline/Redux/GoalActions"; import { deferMerge, mergeAll, setSidebar, } from "goals/MergeDuplicates/Redux/MergeDupsActions"; +import { asyncAdvanceStep } from "goals/Redux/GoalActions"; import { useAppDispatch } from "types/hooks"; import theme from "types/theme"; diff --git a/src/goals/MergeDuplicates/Redux/MergeDupsActions.ts b/src/goals/MergeDuplicates/Redux/MergeDupsActions.ts index dd753a3683..25ef8cbdf2 100644 --- a/src/goals/MergeDuplicates/Redux/MergeDupsActions.ts +++ b/src/goals/MergeDuplicates/Redux/MergeDupsActions.ts @@ -2,10 +2,6 @@ import { Action, PayloadAction } from "@reduxjs/toolkit"; import { Word } from "api/models"; import * as backend from "backend"; -import { - addCompletedMergeToGoal, - asyncUpdateGoal, -} from "components/GoalTimeline/Redux/GoalActions"; import { defaultSidebar, MergeTreeReference, @@ -38,6 +34,10 @@ import { OrderSensePayload, SetVernacularPayload, } from "goals/MergeDuplicates/Redux/MergeDupsReduxTypes"; +import { + addCompletedMergeToGoal, + asyncUpdateGoal, +} from "goals/Redux/GoalActions"; import { StoreState } from "types"; import { StoreStateDispatch } from "types/Redux/actions"; diff --git a/src/components/GoalTimeline/Redux/GoalActions.ts b/src/goals/Redux/GoalActions.ts similarity index 99% rename from src/components/GoalTimeline/Redux/GoalActions.ts rename to src/goals/Redux/GoalActions.ts index 8fefda2b4c..d798161155 100644 --- a/src/components/GoalTimeline/Redux/GoalActions.ts +++ b/src/goals/Redux/GoalActions.ts @@ -5,6 +5,8 @@ import * as Backend from "backend"; import { getDuplicates, getGraylistEntries } from "backend"; import { getCurrentUser, getProjectId } from "backend/localStorage"; import router from "browserRouter"; +import { CharacterChange } from "goals/CharacterInventory/CharacterInventoryTypes"; +import { dispatchMergeStepData } from "goals/MergeDuplicates/Redux/MergeDupsActions"; import { addCharInvChangesToGoalAction, addCompletedMergeToGoalAction, @@ -15,9 +17,7 @@ import { setGoalDataAction, setGoalStatusAction, updateStepFromDataAction, -} from "components/GoalTimeline/Redux/GoalReducer"; -import { CharacterChange } from "goals/CharacterInventory/CharacterInventoryTypes"; -import { dispatchMergeStepData } from "goals/MergeDuplicates/Redux/MergeDupsActions"; +} from "goals/Redux/GoalReducer"; import { EntryEdit } from "goals/ReviewEntries/ReviewEntriesTypes"; import { StoreState } from "types"; import { StoreStateDispatch } from "types/Redux/actions"; diff --git a/src/components/GoalTimeline/Redux/GoalReducer.ts b/src/goals/Redux/GoalReducer.ts similarity index 98% rename from src/components/GoalTimeline/Redux/GoalReducer.ts rename to src/goals/Redux/GoalReducer.ts index a54b8b3141..64da779845 100644 --- a/src/components/GoalTimeline/Redux/GoalReducer.ts +++ b/src/goals/Redux/GoalReducer.ts @@ -1,10 +1,10 @@ import { createSlice } from "@reduxjs/toolkit"; -import { defaultState } from "components/GoalTimeline/DefaultState"; import { MergeDupsData, MergesCompleted, } from "goals/MergeDuplicates/MergeDupsTypes"; +import { defaultState } from "goals/Redux/GoalReduxTypes"; import { EntriesEdited, EntryEdit, diff --git a/src/components/GoalTimeline/DefaultState.ts b/src/goals/Redux/GoalReduxTypes.ts similarity index 61% rename from src/components/GoalTimeline/DefaultState.ts rename to src/goals/Redux/GoalReduxTypes.ts index aa0ec6565c..e27e8b3261 100644 --- a/src/components/GoalTimeline/DefaultState.ts +++ b/src/goals/Redux/GoalReduxTypes.ts @@ -1,4 +1,13 @@ -import { Goal, GoalsState, GoalType } from "types/goals"; +import { Goal, GoalType } from "types/goals"; + +// The representation of goals in the redux store +export interface GoalsState { + allGoalTypes: GoalType[]; + currentGoal: Goal; + goalTypeSuggestions: GoalType[]; + history: Goal[]; + previousGoalType: GoalType; +} // GoalType.ReviewDeferredDups is also implemented, // but is conditionally available @@ -15,12 +24,3 @@ export const defaultState: GoalsState = { history: [], previousGoalType: GoalType.Default, }; - -export function emptyGoalState(): GoalsState { - return { - ...defaultState, - allGoalTypes: [], - currentGoal: { ...new Goal(), guid: expect.any(String) }, - goalTypeSuggestions: [], - }; -} diff --git a/src/goals/ReviewEntries/Redux/ReviewEntriesActions.ts b/src/goals/ReviewEntries/Redux/ReviewEntriesActions.ts index f8e002b989..039afd8816 100644 --- a/src/goals/ReviewEntries/Redux/ReviewEntriesActions.ts +++ b/src/goals/ReviewEntries/Redux/ReviewEntriesActions.ts @@ -2,11 +2,8 @@ import { Action, PayloadAction } from "@reduxjs/toolkit"; import { Pronunciation, Sense, Word } from "api/models"; import * as backend from "backend"; -import { - addEntryEditToGoal, - asyncUpdateGoal, -} from "components/GoalTimeline/Redux/GoalActions"; import { uploadFileFromPronunciation } from "components/Pronunciations/utilities"; +import { addEntryEditToGoal, asyncUpdateGoal } from "goals/Redux/GoalActions"; import { deleteWordAction, resetReviewEntriesAction, diff --git a/src/goals/ReviewEntries/Redux/tests/ReviewEntriesActions.test.tsx b/src/goals/ReviewEntries/Redux/tests/ReviewEntriesActions.test.tsx index a078a11340..696f6a0e3c 100644 --- a/src/goals/ReviewEntries/Redux/tests/ReviewEntriesActions.test.tsx +++ b/src/goals/ReviewEntries/Redux/tests/ReviewEntriesActions.test.tsx @@ -46,7 +46,7 @@ jest.mock("backend", () => ({ updateWord: (word: Word) => mockUpdateWord(word), uploadAudio: (args: any[]) => mockUploadAudio(...args), })); -jest.mock("components/GoalTimeline/Redux/GoalActions", () => ({ +jest.mock("goals/Redux/GoalActions", () => ({ addEntryEditToGoal: () => jest.fn(), asyncUpdateGoal: () => jest.fn(), })); diff --git a/src/goals/ReviewEntries/tests/index.test.tsx b/src/goals/ReviewEntries/tests/index.test.tsx index 491a28ebd6..aeaccd2480 100644 --- a/src/goals/ReviewEntries/tests/index.test.tsx +++ b/src/goals/ReviewEntries/tests/index.test.tsx @@ -38,7 +38,7 @@ jest.mock("backend", () => ({ getFrontierWords: (...args: any[]) => mockGetFrontierWords(...args), })); jest.mock("components/TreeView", () => "div"); -jest.mock("components/GoalTimeline/Redux/GoalActions", () => ({})); +jest.mock("goals/Redux/GoalActions", () => ({})); jest.mock("types/hooks", () => ({ useAppDispatch: () => jest.fn(), })); diff --git a/src/rootReducer.ts b/src/rootReducer.ts index edb50d192b..44f7357bea 100644 --- a/src/rootReducer.ts +++ b/src/rootReducer.ts @@ -1,6 +1,5 @@ import { combineReducers, Reducer } from "redux"; -import goalsReducer from "components/GoalTimeline/Redux/GoalReducer"; import loginReducer from "components/Login/Redux/LoginReducer"; import projectReducer from "components/Project/ProjectReducer"; import exportProjectReducer from "components/ProjectExport/Redux/ExportProjectReducer"; @@ -8,6 +7,7 @@ import pronunciationsReducer from "components/Pronunciations/Redux/Pronunciation import treeViewReducer from "components/TreeView/Redux/TreeViewReducer"; import characterInventoryReducer from "goals/CharacterInventory/Redux/CharacterInventoryReducer"; import mergeDupStepReducer from "goals/MergeDuplicates/Redux/MergeDupsReducer"; +import goalsReducer from "goals/Redux/GoalReducer"; import reviewEntriesReducer from "goals/ReviewEntries/Redux/ReviewEntriesReducer"; import { StoreState } from "types"; import analyticsReducer from "types/Redux/analytics"; diff --git a/src/types/goals.ts b/src/types/goals.ts index bc8baab366..716b5d9300 100644 --- a/src/types/goals.ts +++ b/src/types/goals.ts @@ -1,17 +1,17 @@ import { v4 } from "uuid"; -import { User } from "api/models"; +import { type User } from "api/models"; import { - CharInvChanges, - CharInvData, - CharInvStepData, + type CharInvChanges, + type CharInvData, + type CharInvStepData, } from "goals/CharacterInventory/CharacterInventoryTypes"; import { - MergeDupsData, - MergeStepData, - MergesCompleted, + type MergeDupsData, + type MergeStepData, + type MergesCompleted, } from "goals/MergeDuplicates/MergeDupsTypes"; -import { EntriesEdited } from "goals/ReviewEntries/ReviewEntriesTypes"; +import { type EntriesEdited } from "goals/ReviewEntries/ReviewEntriesTypes"; import { newUser } from "types/user"; export type GoalData = CharInvData | MergeDupsData; @@ -23,15 +23,6 @@ export interface GoalProps { goal?: Goal; } -// The representation of goals in the redux store -export interface GoalsState { - allGoalTypes: GoalType[]; - currentGoal: Goal; - goalTypeSuggestions: GoalType[]; - history: Goal[]; - previousGoalType: GoalType; -} - // The enum value is a permanent id for UserEdits and should not be changed. export enum GoalType { Default = -1, diff --git a/src/types/index.ts b/src/types/index.ts index 024d8c07b1..3a90814fb3 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -5,9 +5,9 @@ import { PronunciationsState } from "components/Pronunciations/Redux/Pronunciati import { TreeViewState } from "components/TreeView/Redux/TreeViewReduxTypes"; import { CharacterInventoryState } from "goals/CharacterInventory/Redux/CharacterInventoryReduxTypes"; import { MergeTreeState } from "goals/MergeDuplicates/Redux/MergeDupsReduxTypes"; +import { GoalsState } from "goals/Redux/GoalReduxTypes"; import { ReviewEntriesState } from "goals/ReviewEntries/Redux/ReviewEntriesReduxTypes"; import { AnalyticsState } from "types/Redux/analyticsReduxTypes"; -import { GoalsState } from "types/goals"; //root store structure export interface StoreState { From 39f719a4be730c10f51f6895f5185d1a6495d41f Mon Sep 17 00:00:00 2001 From: "D. Ror" Date: Fri, 8 Mar 2024 13:36:55 -0500 Subject: [PATCH 19/20] Replace deprecated toBeCalled/toBeCalledWith (#2950) --- src/components/AppBar/tests/Logo.test.tsx | 2 +- src/components/DataEntry/tests/index.test.tsx | 2 +- src/components/Login/tests/Login.test.tsx | 6 +++--- src/components/Login/tests/Signup.test.tsx | 4 ++-- .../ProjectSettings/tests/ProjectAutocomplete.test.tsx | 4 ++-- .../ProjectSettings/tests/ProjectLanguages.test.tsx | 2 +- src/components/ProjectSettings/tests/ProjectName.test.tsx | 2 +- .../TreeView/Redux/tests/TreeViewActions.test.tsx | 2 +- src/utilities/tests/dictionaryLoader.test.ts | 2 +- 9 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/components/AppBar/tests/Logo.test.tsx b/src/components/AppBar/tests/Logo.test.tsx index 1aea4287d6..ba558672c4 100644 --- a/src/components/AppBar/tests/Logo.test.tsx +++ b/src/components/AppBar/tests/Logo.test.tsx @@ -24,6 +24,6 @@ beforeAll(() => { describe("Logo", () => { it("navigates to Project Screen on click", () => { testRenderer.root.findByType(Button).props.onClick(); - expect(mockNavigate).toBeCalledWith(Path.ProjScreen); + expect(mockNavigate).toHaveBeenCalledWith(Path.ProjScreen); }); }); diff --git a/src/components/DataEntry/tests/index.test.tsx b/src/components/DataEntry/tests/index.test.tsx index 0e272443c8..b2fddd36f6 100644 --- a/src/components/DataEntry/tests/index.test.tsx +++ b/src/components/DataEntry/tests/index.test.tsx @@ -74,7 +74,7 @@ describe("DataEntry", () => { it("fetches domain", async () => { await renderDataEntry({ currentDomain: mockDomain }); - expect(mockGetSemanticDomainFull).toBeCalledWith( + expect(mockGetSemanticDomainFull).toHaveBeenCalledWith( mockDomain.id, mockDomain.lang ); diff --git a/src/components/Login/tests/Login.test.tsx b/src/components/Login/tests/Login.test.tsx index 35dc189d4c..6512e99de8 100644 --- a/src/components/Login/tests/Login.test.tsx +++ b/src/components/Login/tests/Login.test.tsx @@ -68,7 +68,7 @@ describe("Login", () => { }); expect(fieldPass.props.error).toBeFalsy(); expect(fieldUser.props.error).toBeTruthy(); - expect(mockAsyncLogIn).not.toBeCalled(); + expect(mockAsyncLogIn).not.toHaveBeenCalled(); }); it("errors when no password", async () => { @@ -82,7 +82,7 @@ describe("Login", () => { }); expect(fieldPass.props.error).toBeTruthy(); expect(fieldUser.props.error).toBeFalsy(); - expect(mockAsyncLogIn).not.toBeCalled(); + expect(mockAsyncLogIn).not.toHaveBeenCalled(); }); it("submits when username and password", async () => { @@ -97,7 +97,7 @@ describe("Login", () => { }); expect(fieldPass.props.error).toBeFalsy(); expect(fieldUser.props.error).toBeFalsy(); - expect(mockAsyncLogIn).toBeCalled(); + expect(mockAsyncLogIn).toHaveBeenCalled(); }); }); }); diff --git a/src/components/Login/tests/Signup.test.tsx b/src/components/Login/tests/Signup.test.tsx index b12429a002..3dd5580fd5 100644 --- a/src/components/Login/tests/Signup.test.tsx +++ b/src/components/Login/tests/Signup.test.tsx @@ -88,9 +88,9 @@ const submitAndCheckError = async (id?: SignupId): Promise => { // Expect signUp only when no field expected to error. if (id === undefined) { - expect(mockAsyncSignUp).toBeCalled(); + expect(mockAsyncSignUp).toHaveBeenCalled(); } else { - expect(mockAsyncSignUp).not.toBeCalled(); + expect(mockAsyncSignUp).not.toHaveBeenCalled(); } }; diff --git a/src/components/ProjectSettings/tests/ProjectAutocomplete.test.tsx b/src/components/ProjectSettings/tests/ProjectAutocomplete.test.tsx index 2dc8125d2c..171bd52b87 100644 --- a/src/components/ProjectSettings/tests/ProjectAutocomplete.test.tsx +++ b/src/components/ProjectSettings/tests/ProjectAutocomplete.test.tsx @@ -29,12 +29,12 @@ describe("ProjectAutocomplete", () => { await renderAutocomplete(); const selectChange = testRenderer.root.findByType(Select).props.onChange; await renderer.act(async () => selectChange({ target: { value: "Off" } })); - expect(mockUpdateProject).toBeCalledWith({ + expect(mockUpdateProject).toHaveBeenCalledWith({ ...mockProject, autocompleteSetting: AutocompleteSetting.Off, }); await renderer.act(async () => selectChange({ target: { value: "On" } })); - expect(mockUpdateProject).toBeCalledWith({ + expect(mockUpdateProject).toHaveBeenCalledWith({ ...mockProject, autocompleteSetting: AutocompleteSetting.On, }); diff --git a/src/components/ProjectSettings/tests/ProjectLanguages.test.tsx b/src/components/ProjectSettings/tests/ProjectLanguages.test.tsx index dc7b504286..56b5ab45bc 100644 --- a/src/components/ProjectSettings/tests/ProjectLanguages.test.tsx +++ b/src/components/ProjectSettings/tests/ProjectLanguages.test.tsx @@ -103,7 +103,7 @@ describe("ProjectLanguages", () => { .findByProps({ id: "analysis-language-new-confirm" }) .props.onClick(); }); - expect(mockUpdateProject).toBeCalledWith( + expect(mockUpdateProject).toHaveBeenCalledWith( mockProject([...mockAnalysisWritingSystems, newLang]) ); }); diff --git a/src/components/ProjectSettings/tests/ProjectName.test.tsx b/src/components/ProjectSettings/tests/ProjectName.test.tsx index bf2fe3a1cb..f775aa01f2 100644 --- a/src/components/ProjectSettings/tests/ProjectName.test.tsx +++ b/src/components/ProjectSettings/tests/ProjectName.test.tsx @@ -37,7 +37,7 @@ describe("ProjectName", () => { textField.props.onChange({ target: { value: name } }) ); await renderer.act(async () => saveButton.props.onClick()); - expect(mockUpdateProject).toBeCalledWith({ ...mockProject, name }); + expect(mockUpdateProject).toHaveBeenCalledWith({ ...mockProject, name }); }); it("toasts on error", async () => { diff --git a/src/components/TreeView/Redux/tests/TreeViewActions.test.tsx b/src/components/TreeView/Redux/tests/TreeViewActions.test.tsx index 19eebad99b..a92768ff60 100644 --- a/src/components/TreeView/Redux/tests/TreeViewActions.test.tsx +++ b/src/components/TreeView/Redux/tests/TreeViewActions.test.tsx @@ -72,7 +72,7 @@ describe("TreeViewActions", () => { treeViewState: nonDefaultState, }); await store.dispatch(initTreeDomain(mockLang)); - expect(mockGetSemDomTreeNode).toBeCalledWith(mockId, mockLang); + expect(mockGetSemDomTreeNode).toHaveBeenCalledWith(mockId, mockLang); }); }); }); diff --git a/src/utilities/tests/dictionaryLoader.test.ts b/src/utilities/tests/dictionaryLoader.test.ts index 9d84027bc3..f14a4b9d50 100644 --- a/src/utilities/tests/dictionaryLoader.test.ts +++ b/src/utilities/tests/dictionaryLoader.test.ts @@ -17,7 +17,7 @@ describe("DictionaryLoader", () => { const loader = new DictionaryLoader(bcp47); expect(loader.lang === bcp47); expect(mockGetKeys).toHaveBeenCalledTimes(1); - expect(mockGetKeys).toBeCalledWith(bcp47); + expect(mockGetKeys).toHaveBeenCalledWith(bcp47); }); }); From 3910d84c5909c04055f7f1a715b60ae9c84f0178 Mon Sep 17 00:00:00 2001 From: Jim Grady Date: Fri, 8 Mar 2024 14:08:36 -0500 Subject: [PATCH 20/20] Set Kubernetes resource requests and limits (#3002) --- .../templates/deployment-cert-proxy-server.yaml | 9 +++++++-- .../templates/deployment-nuc-proxy.yaml | 9 +++++++-- .../charts/backend/templates/deployment-backend.yaml | 9 ++++++++- deploy/helm/thecombine/charts/backend/values.yaml | 2 ++ .../thecombine/charts/database/templates/database.yaml | 9 ++++++++- deploy/helm/thecombine/charts/database/values.yaml | 2 ++ .../charts/frontend/templates/deployment-frontend.yaml | 9 ++++++++- deploy/helm/thecombine/charts/frontend/values.yaml | 2 ++ .../maintenance/templates/cronjob-daily-backup.yaml | 7 ++++++- .../maintenance/templates/deployment-maintenance.yaml | 9 ++++++++- deploy/helm/thecombine/charts/maintenance/values.yaml | 2 ++ deploy/helm/thecombine/values.yaml | 2 ++ deploy/scripts/setup_files/profiles/dev.yaml | 1 + deploy/scripts/setup_files/profiles/prod.yaml | 1 + deploy/scripts/setup_files/profiles/staging.yaml | 1 + 15 files changed, 65 insertions(+), 9 deletions(-) diff --git a/deploy/helm/cert-proxy-server/templates/deployment-cert-proxy-server.yaml b/deploy/helm/cert-proxy-server/templates/deployment-cert-proxy-server.yaml index 5707356b52..7c23b4cb71 100644 --- a/deploy/helm/cert-proxy-server/templates/deployment-cert-proxy-server.yaml +++ b/deploy/helm/cert-proxy-server/templates/deployment-cert-proxy-server.yaml @@ -61,9 +61,14 @@ spec: - name: CERT_PROXY_NAMESPACE value: {{ .Release.Namespace }} image: {{ include "cert-proxy-server.containerImage" . }} - imagePullPolicy: {{ .Values.global.imagePullPolicy }} + imagePullPolicy: Always name: combine-cert-proxy - resources: {} + resources: + requests: + cpu: 2m + memory: 100M + limits: + memory: 150M restartPolicy: Always {{- if ne .Values.global.pullSecretName "None" }} imagePullSecrets: diff --git a/deploy/helm/cert-proxy-server/templates/deployment-nuc-proxy.yaml b/deploy/helm/cert-proxy-server/templates/deployment-nuc-proxy.yaml index f81aed8e7a..3ac30a1463 100644 --- a/deploy/helm/cert-proxy-server/templates/deployment-nuc-proxy.yaml +++ b/deploy/helm/cert-proxy-server/templates/deployment-nuc-proxy.yaml @@ -45,10 +45,15 @@ spec: key: SERVER_NAME name: {{ .Values.envNginxProxy }} image: nginx:1.21 - imagePullPolicy: IfNotPresent + imagePullPolicy: Always ports: - containerPort: 80 - resources: {} + resources: + requests: + cpu: 1m + memory: 10M + limits: + memory: 50M volumeMounts: - name: nginx-html mountPath: /usr/share/nginx/html diff --git a/deploy/helm/thecombine/charts/backend/templates/deployment-backend.yaml b/deploy/helm/thecombine/charts/backend/templates/deployment-backend.yaml index 1513eaee6b..aec75369c4 100644 --- a/deploy/helm/thecombine/charts/backend/templates/deployment-backend.yaml +++ b/deploy/helm/thecombine/charts/backend/templates/deployment-backend.yaml @@ -79,7 +79,14 @@ spec: name: env-backend-secrets ports: - containerPort: 5000 - resources: {} + resources: + requests: + cpu: 5m + memory: 960Mi +{{- if .Values.global.includeResourceLimits }} + limits: + memory: 4Gi +{{- end }} volumeMounts: - mountPath: /home/app/.CombineFiles name: backend-data diff --git a/deploy/helm/thecombine/charts/backend/values.yaml b/deploy/helm/thecombine/charts/backend/values.yaml index 36d35d23b5..3bc97872dc 100644 --- a/deploy/helm/thecombine/charts/backend/values.yaml +++ b/deploy/helm/thecombine/charts/backend/values.yaml @@ -21,6 +21,8 @@ global: combineJwtSecretKey: "Override" combineSmtpUsername: "Override" combineSmtpPassword: "Override" + # Values for pulling container image from image registry + imagePullPolicy: "Override" imageTag: "latest" # Define the image registry to use (may be blank for local images) imageRegistry: "" diff --git a/deploy/helm/thecombine/charts/database/templates/database.yaml b/deploy/helm/thecombine/charts/database/templates/database.yaml index 836feea552..6e681ca546 100644 --- a/deploy/helm/thecombine/charts/database/templates/database.yaml +++ b/deploy/helm/thecombine/charts/database/templates/database.yaml @@ -48,7 +48,14 @@ spec: name: database ports: - containerPort: 27017 - resources: {} + resources: + requests: + cpu: 25m + memory: 950Mi +{{- if .Values.global.includeResourceLimits }} + limits: + memory: 2Gi +{{- end }} volumeMounts: - mountPath: /data/db name: database-data diff --git a/deploy/helm/thecombine/charts/database/values.yaml b/deploy/helm/thecombine/charts/database/values.yaml index 8076f898c5..ab2740d6ad 100644 --- a/deploy/helm/thecombine/charts/database/values.yaml +++ b/deploy/helm/thecombine/charts/database/values.yaml @@ -6,6 +6,8 @@ global: # Update strategy should be "Recreate" or "Rolling Update" updateStrategy: Recreate pullSecretName: "None" + # Values for pulling container image from image registry + imagePullPolicy: "Override" imageTag: "latest" # Define the image registry to use (may be blank for local images) imageRegistry: "" diff --git a/deploy/helm/thecombine/charts/frontend/templates/deployment-frontend.yaml b/deploy/helm/thecombine/charts/frontend/templates/deployment-frontend.yaml index 4fd7d96181..1f350c41a3 100644 --- a/deploy/helm/thecombine/charts/frontend/templates/deployment-frontend.yaml +++ b/deploy/helm/thecombine/charts/frontend/templates/deployment-frontend.yaml @@ -74,7 +74,14 @@ spec: ports: - containerPort: 80 - containerPort: 443 - resources: {} + resources: + requests: + cpu: 1m + memory: 15M +{{- if .Values.global.includeResourceLimits }} + limits: + memory: 40M +{{- end }} volumeMounts: - mountPath: /usr/share/nginx/fonts name: font-data diff --git a/deploy/helm/thecombine/charts/frontend/values.yaml b/deploy/helm/thecombine/charts/frontend/values.yaml index 345ee3a1f6..145e78a047 100644 --- a/deploy/helm/thecombine/charts/frontend/values.yaml +++ b/deploy/helm/thecombine/charts/frontend/values.yaml @@ -8,6 +8,8 @@ global: pullSecretName: aws-login-credentials # Update strategy should be "Recreate" or "Rolling Update" updateStrategy: Recreate + # Values for pulling container image from image registry + imagePullPolicy: "Override" imageTag: "latest" # Define the image registry to use (may be blank for local images) imageRegistry: "" diff --git a/deploy/helm/thecombine/charts/maintenance/templates/cronjob-daily-backup.yaml b/deploy/helm/thecombine/charts/maintenance/templates/cronjob-daily-backup.yaml index f92315e7cb..e37bd8df77 100644 --- a/deploy/helm/thecombine/charts/maintenance/templates/cronjob-daily-backup.yaml +++ b/deploy/helm/thecombine/charts/maintenance/templates/cronjob-daily-backup.yaml @@ -30,7 +30,12 @@ spec: - deployment/maintenance - -- - combine-backup-job.sh - resources: {} + resources: + requests: + cpu: 200m + memory: 150M + limits: + memory: 150M securityContext: capabilities: {} terminationMessagePath: /dev/termination-log diff --git a/deploy/helm/thecombine/charts/maintenance/templates/deployment-maintenance.yaml b/deploy/helm/thecombine/charts/maintenance/templates/deployment-maintenance.yaml index 3fd5f6d5c6..6ffb0a0d5a 100644 --- a/deploy/helm/thecombine/charts/maintenance/templates/deployment-maintenance.yaml +++ b/deploy/helm/thecombine/charts/maintenance/templates/deployment-maintenance.yaml @@ -103,7 +103,14 @@ spec: configMapKeyRef: key: local_font_url name: env-maintenance - resources: {} + resources: + requests: + cpu: 200m + memory: 1200Mi +{{- if .Values.global.includeResourceLimits }} + limits: + memory: 4Gi +{{- end }} volumeMounts: - mountPath: {{ .Values.fontsDir }} name: font-data diff --git a/deploy/helm/thecombine/charts/maintenance/values.yaml b/deploy/helm/thecombine/charts/maintenance/values.yaml index 83990d1b07..d1424183e6 100644 --- a/deploy/helm/thecombine/charts/maintenance/values.yaml +++ b/deploy/helm/thecombine/charts/maintenance/values.yaml @@ -19,6 +19,8 @@ global: awsSecretAccessKey: "Override" pullSecretName: "None" awsS3Access: aws-s3-credentials + # Values for pulling container image from image registry + imagePullPolicy: "Override" imageTag: "latest" # Define the image registry to use (may be blank for local images) imageRegistry: "" diff --git a/deploy/helm/thecombine/values.yaml b/deploy/helm/thecombine/values.yaml index 6f93b72f24..49a47078a1 100644 --- a/deploy/helm/thecombine/values.yaml +++ b/deploy/helm/thecombine/values.yaml @@ -39,6 +39,8 @@ global: # Update strategy should be "Recreate" or "Rolling Update" updateStrategy: Recreate + includeResourceLimits: false + aws-login: enabled: true diff --git a/deploy/scripts/setup_files/profiles/dev.yaml b/deploy/scripts/setup_files/profiles/dev.yaml index b7c736de7c..9f1e3feb88 100644 --- a/deploy/scripts/setup_files/profiles/dev.yaml +++ b/deploy/scripts/setup_files/profiles/dev.yaml @@ -17,6 +17,7 @@ charts: global: imageRegistry: "" imagePullPolicy: Never + includeResourceLimits: false awsS3Location: dev.thecombine.app ingressClass: nginx diff --git a/deploy/scripts/setup_files/profiles/prod.yaml b/deploy/scripts/setup_files/profiles/prod.yaml index 03224a0d5e..2e45809325 100644 --- a/deploy/scripts/setup_files/profiles/prod.yaml +++ b/deploy/scripts/setup_files/profiles/prod.yaml @@ -32,6 +32,7 @@ charts: fontStorageAccessMode: ReadWriteMany imagePullPolicy: Always pullSecretName: None + includeResourceLimits: true certManager: enabled: false cert-proxy-server: diff --git a/deploy/scripts/setup_files/profiles/staging.yaml b/deploy/scripts/setup_files/profiles/staging.yaml index 026e550d36..6c86d40934 100644 --- a/deploy/scripts/setup_files/profiles/staging.yaml +++ b/deploy/scripts/setup_files/profiles/staging.yaml @@ -15,6 +15,7 @@ charts: awsS3Location: prod.thecombine.app fontStorageAccessMode: ReadWriteMany imagePullPolicy: Always + includeResourceLimits: true tlsSecretName: thecombine-app-tls certManager: enabled: false