diff --git a/src/Altinn.App.Core/Features/Validation/ValidationAppSI.cs b/src/Altinn.App.Core/Features/Validation/ValidationAppSI.cs index aa5c78e6d..0f90de627 100644 --- a/src/Altinn.App.Core/Features/Validation/ValidationAppSI.cs +++ b/src/Altinn.App.Core/Features/Validation/ValidationAppSI.cs @@ -1,4 +1,5 @@ using Altinn.App.Core.Configuration; +using Altinn.App.Core.Helpers; using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.AppModel; @@ -230,8 +231,8 @@ public async Task> ValidateDataElement(Instance instance, { var layoutSet = _appResourcesService.GetLayoutSetForTask(dataType.TaskId); var evaluationState = await _layoutEvaluatorStateInitializer.Init(instance, data, layoutSet?.Id); - // Remove hidden data before validation - LayoutEvaluator.RemoveHiddenData(evaluationState); + // Remove hidden data before validation, set rows to null to preserve indices + LayoutEvaluator.RemoveHiddenData(evaluationState, RowRemovalOption.SetToNull); // Evaluate expressions in layout and validate that all required data is included and that maxLength // is respected on groups var layoutErrors = LayoutEvaluator.RunLayoutValidationsForRequired(evaluationState, dataElement.Id); diff --git a/src/Altinn.App.Core/Helpers/DataModel/DataModel.cs b/src/Altinn.App.Core/Helpers/DataModel/DataModel.cs index 396bc4275..9aed92f8d 100644 --- a/src/Altinn.App.Core/Helpers/DataModel/DataModel.cs +++ b/src/Altinn.App.Core/Helpers/DataModel/DataModel.cs @@ -207,7 +207,7 @@ private static bool IsPropertyWithJsonName(PropertyInfo propertyInfo, string key } /// - public void RemoveField(string key, bool deleteRows = false) + public void RemoveField(string key, RowRemovalOption rowRemovalOption) { var keys_split = key.Split('.'); var keys = keys_split[0..^1]; @@ -242,16 +242,18 @@ public void RemoveField(string key, bool deleteRows = false) throw new ArgumentException($"Tried to remove row {key}, ended in a non-list ({propertyValue?.GetType()})"); } - if (deleteRows) + switch (rowRemovalOption) { - listValue.RemoveAt(lastGroupIndex.Value); - } - else - { - - var genericType = listValue.GetType().GetGenericArguments().FirstOrDefault(); - var nullValue = genericType?.IsValueType == true ? Activator.CreateInstance(genericType) : null; - listValue[lastGroupIndex.Value] = nullValue; + case RowRemovalOption.DeleteRow: + listValue.RemoveAt(lastGroupIndex.Value); + break; + case RowRemovalOption.SetToNull: + var genericType = listValue.GetType().GetGenericArguments().FirstOrDefault(); + var nullValue = genericType?.IsValueType == true ? Activator.CreateInstance(genericType) : null; + listValue[lastGroupIndex.Value] = nullValue; + break; + case RowRemovalOption.Ignore: + return; } } else diff --git a/src/Altinn.App.Core/Helpers/IDataModel.cs b/src/Altinn.App.Core/Helpers/IDataModel.cs index 629390809..2f267da9e 100644 --- a/src/Altinn.App.Core/Helpers/IDataModel.cs +++ b/src/Altinn.App.Core/Helpers/IDataModel.cs @@ -39,7 +39,7 @@ public interface IDataModelAccessor /// /// Remove a value from the wrapped datamodel /// - void RemoveField(string key, bool deleteRows = false); + void RemoveField(string key, RowRemovalOption rowRemovalOption); /// /// Verify that a Key is a valid lookup for the datamodel @@ -47,5 +47,26 @@ public interface IDataModelAccessor bool VerifyKey(string key); } +/// +/// Option for how to handle row removal +/// +public enum RowRemovalOption +{ + /// + /// Remove the row from the data model + /// + DeleteRow, + + /// + /// Set the row to null, used to preserve row indices + /// + SetToNull, + + /// + /// Ignore row removal + /// + Ignore +} + diff --git a/src/Altinn.App.Core/Implementation/DefaultTaskEvents.cs b/src/Altinn.App.Core/Implementation/DefaultTaskEvents.cs index 0d4985c44..8218233e7 100644 --- a/src/Altinn.App.Core/Implementation/DefaultTaskEvents.cs +++ b/src/Altinn.App.Core/Implementation/DefaultTaskEvents.cs @@ -257,10 +257,10 @@ private async Task RemoveHiddenData(Instance instance, Guid instanceGuid, List - public static void RemoveHiddenData(LayoutEvaluatorState state, bool deleteRows = false) + public static void RemoveHiddenData(LayoutEvaluatorState state, RowRemovalOption rowRemovalOption) { var fields = GetHiddenFieldsForRemoval(state); foreach (var field in fields) { - state.RemoveDataField(field, deleteRows); + state.RemoveDataField(field, rowRemovalOption); } } diff --git a/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorState.cs b/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorState.cs index 5ebb63f7d..fd7e9f953 100644 --- a/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorState.cs +++ b/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorState.cs @@ -180,9 +180,9 @@ public ComponentContext GetComponentContext(string pageName, string componentId, /// /// Set the value of a field to null. /// - public void RemoveDataField(string key, bool deleteRows = false) + public void RemoveDataField(string key, RowRemovalOption rowRemovalOption) { - _dataModel.RemoveField(key, deleteRows); + _dataModel.RemoveField(key, rowRemovalOption); } /// diff --git a/test/Altinn.App.Core.Tests/Helpers/JsonDataModel.cs b/test/Altinn.App.Core.Tests/Helpers/JsonDataModel.cs index 2816a3237..f5bd722dd 100644 --- a/test/Altinn.App.Core.Tests/Helpers/JsonDataModel.cs +++ b/test/Altinn.App.Core.Tests/Helpers/JsonDataModel.cs @@ -143,7 +143,7 @@ public string AddIndicies(string key, ReadOnlySpan indicies = default) } /// - public void RemoveField(string key, bool deleteRows = false) + public void RemoveField(string key, RowRemovalOption rowRemovalOption) { throw new NotImplementedException("Impossible to remove fields in a json model"); } diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/Test2/RunTest2.cs b/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/Test2/RunTest2.cs index 1a98cd95f..19156960b 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/Test2/RunTest2.cs +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/Test2/RunTest2.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Text.Json.Serialization; using System.Threading.Tasks; +using Altinn.App.Core.Helpers; using Altinn.App.Core.Internal.Expressions; using Altinn.App.Core.Models.Validation; using FluentAssertions; @@ -54,7 +55,7 @@ public async Task RemoveWholeGroup() data.Some.Data[0].Binding2.Should().Be(0); // binding is not nullable, but will be reset to zero data.Some.Data[1].Binding.Should().Be("binding"); data.Some.Data[1].Binding2.Should().Be(2); - LayoutEvaluator.RemoveHiddenData(state); + LayoutEvaluator.RemoveHiddenData(state, RowRemovalOption.SetToNull); // Verify data was removed data.Some.Data[0].Binding.Should().BeNull(); @@ -111,4 +112,4 @@ public class Data [JsonPropertyName("binding3")] public string Binding3 { get; set; } -} \ No newline at end of file +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/Test3/RunTest3.cs b/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/Test3/RunTest3.cs index 43e3a233f..0c17439ed 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/Test3/RunTest3.cs +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/Test3/RunTest3.cs @@ -1,4 +1,5 @@ using System.Text.Json.Serialization; +using Altinn.App.Core.Helpers; using Altinn.App.Core.Internal.Expressions; using FluentAssertions; using Xunit; @@ -61,7 +62,7 @@ public async Task RemoveRowDataFromGroup() data.Some.Data[2].Binding.Should().Be("hideRow"); data.Some.Data[2].Binding2.Should().Be(3); data.Some.Data[2].Binding3.Should().Be("text"); - LayoutEvaluator.RemoveHiddenData(state); + LayoutEvaluator.RemoveHiddenData(state, RowRemovalOption.SetToNull); // Verify row not deleted but fields null data.Some.Data.Should().HaveCount(3); @@ -71,7 +72,7 @@ public async Task RemoveRowDataFromGroup() data.Some.Data[1].Binding2.Should().Be(2); data.Some.Data[2].Should().BeNull(); } - + [Fact] public async Task RemoveRowFromGroup() { @@ -118,9 +119,9 @@ public async Task RemoveRowFromGroup() data.Some.Data[2].Binding.Should().Be("hideRow"); data.Some.Data[2].Binding2.Should().Be(3); data.Some.Data[2].Binding3.Should().Be("text"); - + // Verify rows deleted - LayoutEvaluator.RemoveHiddenData(state, true); + LayoutEvaluator.RemoveHiddenData(state, RowRemovalOption.DeleteRow); data.Some.Data.Should().HaveCount(2); data.Some.Data[0].Binding.Should().BeNull(); data.Some.Data[0].Binding2.Should().Be(0); // binding is not nullable, but will be reset to zero @@ -154,4 +155,4 @@ public class Data [JsonPropertyName("binding3")] public string Binding3 { get; set; } -} \ No newline at end of file +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/TestDataModel.cs b/test/Altinn.App.Core.Tests/LayoutExpressions/TestDataModel.cs index 3a9afbf14..f0b1c87ac 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/TestDataModel.cs +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/TestDataModel.cs @@ -226,22 +226,22 @@ public void TestRemoveFields() }; IDataModelAccessor modelHelper = new DataModel(model); model.Id.Should().Be(2); - modelHelper.RemoveField("id"); + modelHelper.RemoveField("id", RowRemovalOption.SetToNull); model.Id.Should().Be(default); model.Name.Value.Should().Be("Ivar"); - modelHelper.RemoveField("name"); + modelHelper.RemoveField("name", RowRemovalOption.SetToNull); model.Name.Should().BeNull(); model.Friends.First().Name!.Value.Should().Be("Første venn"); - modelHelper.RemoveField("friends[0].name.value"); + modelHelper.RemoveField("friends[0].name.value", RowRemovalOption.SetToNull); model.Friends.First().Name!.Value.Should().BeNull(); - modelHelper.RemoveField("friends[0].name"); + modelHelper.RemoveField("friends[0].name", RowRemovalOption.SetToNull); model.Friends.First().Name.Should().BeNull(); model.Friends.First().Age.Should().Be(1235); model.Friends.First().Friends!.First().Age.Should().Be(233); - modelHelper.RemoveField("friends[0].friends"); + modelHelper.RemoveField("friends[0].friends", RowRemovalOption.SetToNull); model.Friends.First().Friends.Should().BeNull(); } @@ -338,12 +338,12 @@ public void TestRemoveRows() var model1 = System.Text.Json.JsonSerializer.Deserialize(serializedModel)!; IDataModelAccessor modelHelper1 = new DataModel(model1); - modelHelper1.RemoveField("friends[0].friends[0]"); + modelHelper1.RemoveField("friends[0].friends[0]", RowRemovalOption.SetToNull); model1.Friends![0].Friends![0].Should().BeNull(); model1.Friends![0].Friends!.Count.Should().Be(3); model1.Friends[0].Friends![1].Name!.Value.Should().Be("Første venn sin andre venn"); - modelHelper1.RemoveField("friends[1]"); + modelHelper1.RemoveField("friends[1]", RowRemovalOption.SetToNull); model1.Friends[1].Should().BeNull(); model1.Friends.Count.Should().Be(3); model1.Friends[2].Name!.Value.Should().Be("Tredje venn"); @@ -352,11 +352,11 @@ public void TestRemoveRows() var model2 = System.Text.Json.JsonSerializer.Deserialize(serializedModel)!; IDataModelAccessor modelHelper2 = new DataModel(model2); - modelHelper2.RemoveField("friends[0].friends[0]", true); + modelHelper2.RemoveField("friends[0].friends[0]", RowRemovalOption.DeleteRow); model2.Friends![0].Friends!.Count.Should().Be(2); model2.Friends[0].Friends![0].Name!.Value.Should().Be("Første venn sin andre venn"); - modelHelper2.RemoveField("friends[1]", true); + modelHelper2.RemoveField("friends[1]", RowRemovalOption.DeleteRow); model2.Friends.Count.Should().Be(2); model2.Friends[1].Name!.Value.Should().Be("Tredje venn"); } @@ -454,16 +454,16 @@ public void RemoveField_WhenValueDoesNotExist_DoNothing() var modelHelper = new DataModel(new Model()); // real fields works, no error - modelHelper.RemoveField("id"); + modelHelper.RemoveField("id", RowRemovalOption.SetToNull); // non-existant-fields works, no error - modelHelper.RemoveField("doesNotExist"); + modelHelper.RemoveField("doesNotExist", RowRemovalOption.SetToNull); // non-existant-fields in subfield works, no error - modelHelper.RemoveField("friends.doesNotExist"); + modelHelper.RemoveField("friends.doesNotExist", RowRemovalOption.SetToNull); // non-existant-fields in subfield works, no error - modelHelper.RemoveField("friends[0].doesNotExist"); + modelHelper.RemoveField("friends[0].doesNotExist", RowRemovalOption.SetToNull); } }