V15/fix/sub variant block deletion (#18802)

* Fix

* Editors with access should be able to clear a blocklist value

* Writeup around block element level variation

* Dissallow values to be removed a limited language user does not have permissions to

* Remove commented out code

* improved comments

* Improve expose list for limited language access sub variant block lists
This commit is contained in:
Sven Geusens
2025-04-21 10:30:12 +02:00
committed by GitHub
parent 1ccb5cc09e
commit c86a6fa8e5
3 changed files with 757 additions and 33 deletions

View File

@@ -14,6 +14,10 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.PropertyEditors;
internal partial class BlockListElementLevelVariationTests
{
/// <summary>
/// Tests whether the user can update the variant values of existing blocks inside an invariant blocklist
/// </summary>
/// <param name="updateWithLimitedUserAccess">true => danish only which is not the default. false => admin which is all languages</param>
[TestCase(true)]
[TestCase(false)]
[ConfigureBuilder(ActionName = nameof(ConfigureAllowEditInvariantFromNonDefaultTrue))]
@@ -169,6 +173,10 @@ internal partial class BlockListElementLevelVariationTests
}
}
/// <summary>
/// Tests whether the user can add new variant blocks to an invariant blocklist
/// </summary>
/// <param name="updateWithLimitedUserAccess">true => danish only which is not the default. false => admin which is all languages</param>
[TestCase(true)]
[TestCase(false)]
[ConfigureBuilder(ActionName = nameof(ConfigureAllowEditInvariantFromNonDefaultTrue))]
@@ -299,6 +307,10 @@ internal partial class BlockListElementLevelVariationTests
}
}
/// <summary>
/// Tests whether the user can update the variant values of existing blocks inside an invariant blocklist
/// </summary>
/// <param name="updateWithLimitedUserAccess">true => danish only which is not the default. false => admin which is all languages</param>
[TestCase(true)]
[TestCase(false)]
public async Task Can_Handle_Limited_User_Access_To_Languages_Without_AllowEditInvariantFromNonDefault(bool updateWithLimitedUserAccess)
@@ -457,6 +469,10 @@ internal partial class BlockListElementLevelVariationTests
}
}
/// <summary>
/// Tests whether the user can add new variant blocks to an invariant blocklist
/// </summary>
/// <param name="updateWithLimitedUserAccess">true => danish only which is not the default. false => admin which is all languages</param>
[TestCase(true)]
[TestCase(false)]
public async Task Can_Handle_Limited_User_Access_To_Languages_Without_AllowEditInvariantFromNonDefault_WithoutInitialValue(bool updateWithLimitedUserAccess)
@@ -524,14 +540,14 @@ internal partial class BlockListElementLevelVariationTests
{
InvariantProperties = new[]
{
new PropertyValueModel { Alias = "blocks", Value = JsonSerializer.Serialize(blockListValue) }
new PropertyValueModel { Alias = "blocks", Value = JsonSerializer.Serialize(blockListValue) },
},
Variants = new[]
{
new VariantModel { Name = content.GetCultureName("en-US")!, Culture = "en-US", Properties = [] },
new VariantModel { Name = content.GetCultureName("da-DK")!, Culture = "da-DK", Properties = [] },
new VariantModel { Name = content.GetCultureName("de-DE")!, Culture = "de-DE", Properties = [] }
}
new VariantModel { Name = content.GetCultureName("de-DE")!, Culture = "de-DE", Properties = [] },
},
};
var result = await ContentEditingService.UpdateAsync(content.Key, updateModel, userKey);
@@ -542,24 +558,10 @@ internal partial class BlockListElementLevelVariationTests
Assert.NotNull(savedBlocksValue);
blockListValue = JsonSerializer.Deserialize<BlockListValue>(savedBlocksValue);
// the Danish values should be updated regardless of the executing user
Assert.Multiple(() =>
{
Assert.AreEqual("#1: The second content value in Danish", blockListValue.ContentData[0].Values.Single(v => v.Culture == "da-DK").Value);
Assert.AreEqual("#1: The second settings value in Danish", blockListValue.SettingsData[0].Values.Single(v => v.Culture == "da-DK").Value);
Assert.AreEqual("#2: The second content value in Danish", blockListValue.ContentData[1].Values.Single(v => v.Culture == "da-DK").Value);
Assert.AreEqual("#2: The second settings value in Danish", blockListValue.SettingsData[1].Values.Single(v => v.Culture == "da-DK").Value);
});
// limited user access means invariant, English and German should not have been updated - changes should be rolled back to the initial block values
// limited user access means invariant data is inaccessible since AllowEditInvariantFromNonDefault is disabled
if (updateWithLimitedUserAccess)
{
Assert.Multiple(() =>
{
Assert.AreEqual(1, blockListValue.ContentData[0].Values.Count);
Assert.AreEqual(1, blockListValue.ContentData[1].Values.Count);
});
Assert.IsNull(blockListValue);
}
else
{
@@ -581,6 +583,542 @@ internal partial class BlockListElementLevelVariationTests
Assert.AreEqual("#2: The second settings value in English", blockListValue.SettingsData[1].Values[1].Value);
Assert.AreEqual("#2: The second content value in German", blockListValue.ContentData[1].Values[3].Value);
Assert.AreEqual("#2: The second settings value in German", blockListValue.SettingsData[1].Values[3].Value);
Assert.AreEqual("#1: The second content value in Danish", blockListValue.ContentData[0].Values.Single(v => v.Culture == "da-DK").Value);
Assert.AreEqual("#1: The second settings value in Danish", blockListValue.SettingsData[0].Values.Single(v => v.Culture == "da-DK").Value);
Assert.AreEqual("#2: The second content value in Danish", blockListValue.ContentData[1].Values.Single(v => v.Culture == "da-DK").Value);
Assert.AreEqual("#2: The second settings value in Danish", blockListValue.SettingsData[1].Values.Single(v => v.Culture == "da-DK").Value);
});
}
}
/// <summary>
/// Tests whether the user can add/remove new variant blocks to an invariant blocklist
/// </summary>
/// <param name="updateWithLimitedUserAccess">true => danish only which is not the default. false => admin which is all languages</param>
[TestCase(true)]
[TestCase(false)]
public async Task Can_Handle_BlockStructureManipulation_For_Limited_Users_Without_AllowEditInvariantFromNonDefault(
bool updateWithLimitedUserAccess)
{
await LanguageService.CreateAsync(
new Language("de-DE", "German"), Constants.Security.SuperUserKey);
var userKey = updateWithLimitedUserAccess
? (await CreateLimitedUser()).Key
: Constants.Security.SuperUserKey;
var elementType = CreateElementType(ContentVariation.Culture);
var blockListDataType = await CreateBlockListDataType(elementType);
var contentType = CreateContentType(ContentVariation.Culture, blockListDataType);
var content = CreateContent(contentType, elementType, [], false);
content.SetCultureName("Home (de)", "de-DE");
ContentService.Save(content);
var firstContentElementKey = Guid.NewGuid();
var firstSettingsElementKey = Guid.NewGuid();
var secondContentElementKey = Guid.NewGuid();
var secondSettingsElementKey = Guid.NewGuid();
var blockListValue = BlockListPropertyValue(
elementType,
[
(
firstContentElementKey,
firstSettingsElementKey,
new BlockProperty(
new List<BlockPropertyValue> {
new() { Alias = "invariantText", Value = "#1: The first invariant content value" },
new() { Alias = "variantText", Value = "#1: The first content value in English", Culture = "en-US" },
new() { Alias = "variantText", Value = "#1: The first content value in Danish", Culture = "da-DK" },
new() { Alias = "variantText", Value = "#1: The first content value in German", Culture = "de-DE" },
},
new List<BlockPropertyValue> {
new() { Alias = "invariantText", Value = "#1: The first invariant settings value" },
new() { Alias = "variantText", Value = "#1: The first settings value in English", Culture = "en-US" },
new() { Alias = "variantText", Value = "#1: The first settings value in Danish", Culture = "da-DK" },
new() { Alias = "variantText", Value = "#1: The first settings value in German", Culture = "de-DE" },
},
null,
null)),
(
secondContentElementKey,
secondSettingsElementKey,
new BlockProperty(
new List<BlockPropertyValue> {
new() { Alias = "invariantText", Value = "#2: The first invariant content value" },
new() { Alias = "variantText", Value = "#2: The first content value in English", Culture = "en-US" },
new() { Alias = "variantText", Value = "#2: The first content value in Danish", Culture = "da-DK" },
new() { Alias = "variantText", Value = "#2: The first content value in German", Culture = "de-DE" },
},
new List<BlockPropertyValue> {
new() { Alias = "invariantText", Value = "#2: The first invariant settings value" },
new() { Alias = "variantText", Value = "#2: The first settings value in English", Culture = "en-US" },
new() { Alias = "variantText", Value = "#2: The first settings value in Danish", Culture = "da-DK" },
new() { Alias = "variantText", Value = "#2: The first settings value in German", Culture = "de-DE" },
},
null,
null))
]);
content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue));
ContentService.Save(content);
var newContentElementKey = Guid.NewGuid();
RemoveBlock(blockListValue, firstContentElementKey);
AddBlock(
blockListValue,
new BlockItemData
{
Key = newContentElementKey,
ContentTypeAlias = elementType.Alias,
ContentTypeKey = elementType.Key,
Values = new List<BlockPropertyValue> {
new() { Alias = "invariantText", Value = "#new: The new invariant settings value" },
new() { Alias = "variantText", Value = "#new: The new settings value in English", Culture = "en-US" },
new() { Alias = "variantText", Value = "#new: The new settings value in Danish", Culture = "da-DK" },
new() { Alias = "variantText", Value = "#new: The new settings value in German", Culture = "de-DE" },
},
},
null,
elementType);
var updateModel = new ContentUpdateModel
{
InvariantProperties = new[]
{
new PropertyValueModel { Alias = "blocks", Value = JsonSerializer.Serialize(blockListValue) }
},
Variants = new[]
{
new VariantModel { Name = content.GetCultureName("en-US")!, Culture = "en-US", Properties = [] },
new VariantModel { Name = content.GetCultureName("da-DK")!, Culture = "da-DK", Properties = [] },
new VariantModel { Name = content.GetCultureName("de-DE")!, Culture = "de-DE", Properties = [] }
},
};
var result = await ContentEditingService.UpdateAsync(content.Key, updateModel, userKey);
Assert.IsTrue(result.Success);
content = ContentService.GetById(content.Key);
var savedBlocksValue = content?.Properties["blocks"]?.GetValue()?.ToString();
Assert.NotNull(savedBlocksValue);
blockListValue = JsonSerializer.Deserialize<BlockListValue>(savedBlocksValue);
if (updateWithLimitedUserAccess)
{
Assert.Multiple(() =>
{
// new one can't be added
Assert.AreEqual(0, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.ContentKey == newContentElementKey));
Assert.AreEqual(0, blockListValue.ContentData.Sum(contentData => contentData.Values.Count(value => value.Value?.ToString()?.StartsWith("#new") == true)));
Assert.AreEqual(0, blockListValue.SettingsData.Sum(settingsData => settingsData.Values.Count(value => value.Value?.ToString()?.StartsWith("#new") == true)));
// can't remove first
Assert.AreEqual(1, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.ContentKey == firstContentElementKey));
Assert.AreEqual(1, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.SettingsKey == firstSettingsElementKey));
Assert.AreEqual(4, blockListValue.ContentData.Sum(contentData => contentData.Values.Count(value => value.Value?.ToString()?.StartsWith("#1") == true)));
Assert.AreEqual(4, blockListValue.SettingsData.Sum(settingsData => settingsData.Values.Count(value => value.Value?.ToString()?.StartsWith("#1") == true)));
// second wasn't touched
Assert.AreEqual(1, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.SettingsKey == secondSettingsElementKey));
Assert.AreEqual(1, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.ContentKey == secondContentElementKey));
Assert.AreEqual(4, blockListValue.ContentData.Sum(contentData => contentData.Values.Count(value => value.Value?.ToString()?.StartsWith("#2") == true)));
Assert.AreEqual(4, blockListValue.SettingsData.Sum(settingsData => settingsData.Values.Count(value => value.Value?.ToString()?.StartsWith("#2") == true)));
});
}
else
{
Assert.Multiple(() =>
{
// add new one, did not add settings
Assert.AreEqual(1, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.ContentKey == newContentElementKey));
Assert.AreEqual(4, blockListValue.ContentData.Sum(contentData => contentData.Values.Count(value => value.Value?.ToString()?.StartsWith("#new") == true)));
Assert.AreEqual(0, blockListValue.SettingsData.Sum(settingsData => settingsData.Values.Count(value => value.Value?.ToString()?.StartsWith("#new") == true)));
// first one removed
Assert.AreEqual(0, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.ContentKey == firstContentElementKey));
Assert.AreEqual(0, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.SettingsKey == firstSettingsElementKey));
Assert.AreEqual(0, blockListValue.ContentData.Sum(contentData => contentData.Values.Count(value => value.Value?.ToString()?.StartsWith("#1") == true)));
Assert.AreEqual(0, blockListValue.SettingsData.Sum(settingsData => settingsData.Values.Count(value => value.Value?.ToString()?.StartsWith("#1") == true)));
// second wasn't touched
Assert.AreEqual(1, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.SettingsKey == secondSettingsElementKey));
Assert.AreEqual(1, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.ContentKey == secondContentElementKey));
Assert.AreEqual(4, blockListValue.ContentData.Sum(contentData => contentData.Values.Count(value => value.Value?.ToString()?.StartsWith("#2") == true)));
Assert.AreEqual(4, blockListValue.SettingsData.Sum(settingsData => settingsData.Values.Count(value => value.Value?.ToString()?.StartsWith("#2") == true)));
});
}
}
/// <summary>
/// Tests whether the user can add/remove new variant blocks to an invariant blocklist
/// </summary>
/// <param name="updateWithLimitedUserAccess">true => danish only which is not the default. false => admin which is all languages</param>
[TestCase(true)]
[TestCase(false)]
[ConfigureBuilder(ActionName = nameof(ConfigureAllowEditInvariantFromNonDefaultTrue))]
public async Task Can_Handle_BlockStructureManipulation_For_Limited_Users_With_AllowEditInvariantFromNonDefault(
bool updateWithLimitedUserAccess)
{
await LanguageService.CreateAsync(
new Language("de-DE", "German"), Constants.Security.SuperUserKey);
var userKey = updateWithLimitedUserAccess
? (await CreateLimitedUser()).Key
: Constants.Security.SuperUserKey;
var elementType = CreateElementType(ContentVariation.Culture);
var blockListDataType = await CreateBlockListDataType(elementType);
var contentType = CreateContentType(ContentVariation.Culture, blockListDataType);
var content = CreateContent(contentType, elementType, [], false);
content.SetCultureName("Home (de)", "de-DE");
ContentService.Save(content);
var firstContentElementKey = Guid.NewGuid();
var firstSettingsElementKey = Guid.NewGuid();
var blockListValue = BlockListPropertyValue(
elementType,
[
(
firstContentElementKey,
firstSettingsElementKey,
new BlockProperty(
new List<BlockPropertyValue> {
new() { Alias = "invariantText", Value = "#1: The first invariant content value" },
new() { Alias = "variantText", Value = "#1: The first content value in English", Culture = "en-US" },
new() { Alias = "variantText", Value = "#1: The first content value in Danish", Culture = "da-DK" },
new() { Alias = "variantText", Value = "#1: The first content value in German", Culture = "de-DE" },
},
new List<BlockPropertyValue> {
new() { Alias = "invariantText", Value = "#1: The first invariant settings value" },
new() { Alias = "variantText", Value = "#1: The first settings value in English", Culture = "en-US" },
new() { Alias = "variantText", Value = "#1: The first settings value in Danish", Culture = "da-DK" },
new() { Alias = "variantText", Value = "#1: The first settings value in German", Culture = "de-DE" },
},
null,
null)),
(
Guid.NewGuid(),
Guid.NewGuid(),
new BlockProperty(
new List<BlockPropertyValue> {
new() { Alias = "invariantText", Value = "#2: The first invariant content value" },
new() { Alias = "variantText", Value = "#2: The first content value in English", Culture = "en-US" },
new() { Alias = "variantText", Value = "#2: The first content value in Danish", Culture = "da-DK" },
new() { Alias = "variantText", Value = "#2: The first content value in German", Culture = "de-DE" },
},
new List<BlockPropertyValue> {
new() { Alias = "invariantText", Value = "#2: The first invariant settings value" },
new() { Alias = "variantText", Value = "#2: The first settings value in English", Culture = "en-US" },
new() { Alias = "variantText", Value = "#2: The first settings value in Danish", Culture = "da-DK" },
new() { Alias = "variantText", Value = "#2: The first settings value in German", Culture = "de-DE" },
},
null,
null))
]);
content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue));
ContentService.Save(content);
var newContentElementKey = Guid.NewGuid();
RemoveBlock(blockListValue, firstContentElementKey);
AddBlock(
blockListValue,
new BlockItemData
{
Key = newContentElementKey,
ContentTypeAlias = elementType.Alias,
ContentTypeKey = elementType.Key,
Values = new List<BlockPropertyValue> {
new() { Alias = "invariantText", Value = "#new: The new invariant settings value" },
new() { Alias = "variantText", Value = "#new: The new settings value in English", Culture = "en-US" },
new() { Alias = "variantText", Value = "#new: The new settings value in Danish", Culture = "da-DK" },
new() { Alias = "variantText", Value = "#new: The new settings value in German", Culture = "de-DE" },
},
},
null,
elementType);
var updateModel = new ContentUpdateModel
{
InvariantProperties = new[]
{
new PropertyValueModel { Alias = "blocks", Value = JsonSerializer.Serialize(blockListValue) }
},
Variants = new[]
{
new VariantModel { Name = content.GetCultureName("en-US")!, Culture = "en-US", Properties = [] },
new VariantModel { Name = content.GetCultureName("da-DK")!, Culture = "da-DK", Properties = [] },
new VariantModel { Name = content.GetCultureName("de-DE")!, Culture = "de-DE", Properties = [] }
},
};
var result = await ContentEditingService.UpdateAsync(content.Key, updateModel, userKey);
Assert.IsTrue(result.Success);
content = ContentService.GetById(content.Key);
var savedBlocksValue = content?.Properties["blocks"]?.GetValue()?.ToString();
Assert.NotNull(savedBlocksValue);
blockListValue = JsonSerializer.Deserialize<BlockListValue>(savedBlocksValue);
// In both cases we are allowed to change the invariant structure
// But the amount of new cultured values we can add differs
Assert.Multiple(() =>
{
Assert.AreEqual(1, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.ContentKey == newContentElementKey));
Assert.AreEqual(0, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.ContentKey == firstContentElementKey));
Assert.AreEqual(0, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.SettingsKey == firstSettingsElementKey));
Assert.AreEqual(updateWithLimitedUserAccess ? 2 : 4, blockListValue.ContentData.Sum(contentData => contentData.Values.Count(value => value.Value?.ToString()?.StartsWith("#new") == true)));
});
}
/// <summary>
/// Tests whether the user can update the variant values of existing blocks inside an invariant blocklist
/// </summary>
/// <param name="updateWithLimitedUserAccess">true => danish only which is not the default. false => admin which is all languages</param>
[TestCase(true)]
[TestCase(false)]
public async Task Can_ClearBlocks_Limited_User_Access_To_Languages_Without_AllowEditInvariantFromNonDefault(bool updateWithLimitedUserAccess)
{
await LanguageService.CreateAsync(
new Language("de-DE", "German"), Constants.Security.SuperUserKey);
var userKey = updateWithLimitedUserAccess
? (await CreateLimitedUser()).Key
: Constants.Security.SuperUserKey;
var elementType = CreateElementType(ContentVariation.Culture);
var blockListDataType = await CreateBlockListDataType(elementType);
var contentType = CreateContentType(ContentVariation.Culture, blockListDataType);
var content = CreateContent(contentType, elementType, [], false);
content.SetCultureName("Home (de)", "de-DE");
ContentService.Save(content);
var blockListValue = BlockListPropertyValue(
elementType,
[
(
Guid.NewGuid(),
Guid.NewGuid(),
new BlockProperty(
new List<BlockPropertyValue> {
new() { Alias = "invariantText", Value = "#1: The first invariant content value" },
new() { Alias = "variantText", Value = "#1: The first content value in English", Culture = "en-US" },
new() { Alias = "variantText", Value = "#1: The first content value in Danish", Culture = "da-DK" },
new() { Alias = "variantText", Value = "#1: The first content value in German", Culture = "de-DE" }
},
new List<BlockPropertyValue> {
new() { Alias = "invariantText", Value = "#1: The first invariant settings value" },
new() { Alias = "variantText", Value = "#1: The first settings value in English", Culture = "en-US" },
new() { Alias = "variantText", Value = "#1: The first settings value in Danish", Culture = "da-DK" },
new() { Alias = "variantText", Value = "#1: The first settings value in German", Culture = "de-DE" }
},
null,
null
)
),
(
Guid.NewGuid(),
Guid.NewGuid(),
new BlockProperty(
new List<BlockPropertyValue> {
new() { Alias = "invariantText", Value = "#2: The first invariant content value" },
new() { Alias = "variantText", Value = "#2: The first content value in English", Culture = "en-US" },
new() { Alias = "variantText", Value = "#2: The first content value in Danish", Culture = "da-DK" },
new() { Alias = "variantText", Value = "#2: The first content value in German", Culture = "de-DE" }
},
new List<BlockPropertyValue> {
new() { Alias = "invariantText", Value = "#2: The first invariant settings value" },
new() { Alias = "variantText", Value = "#2: The first settings value in English", Culture = "en-US" },
new() { Alias = "variantText", Value = "#2: The first settings value in Danish", Culture = "da-DK" },
new() { Alias = "variantText", Value = "#2: The first settings value in German", Culture = "de-DE" }
},
null,
null
)
)
]
);
var serializedBlockListValue = JsonSerializer.Serialize(blockListValue);
content.Properties["blocks"]!.SetValue(serializedBlockListValue);
ContentService.Save(content);
var updateModel = new ContentUpdateModel
{
InvariantProperties = new[]
{
new PropertyValueModel { Alias = "blocks", Value = null },
},
Variants = new[]
{
new VariantModel { Name = content.GetCultureName("en-US")!, Culture = "en-US", Properties = [] },
new VariantModel { Name = content.GetCultureName("da-DK")!, Culture = "da-DK", Properties = [] },
new VariantModel { Name = content.GetCultureName("de-DE")!, Culture = "de-DE", Properties = [] },
},
};
var result = await ContentEditingService.UpdateAsync(content.Key, updateModel, userKey);
Assert.IsTrue(result.Success);
content = ContentService.GetById(content.Key);
var savedBlocksValue = content?.Properties["blocks"]?.GetValue()?.ToString();
// limited user access means English and German should not have been updated - changes should be rolled back to the initial block values
if (updateWithLimitedUserAccess)
{
Assert.NotNull(savedBlocksValue);
Assert.AreEqual(serializedBlockListValue, savedBlocksValue);
}
else
{
Assert.AreEqual("null", savedBlocksValue);
}
}
/// <summary>
/// Tests whether the user can add/remove a value for a given culture
/// </summary>
/// <param name="updateWithLimitedUserAccess">true => danish only which is not the default. false => admin which is all languages</param>
[TestCase(true)]
[TestCase(false)]
public async Task Can_Handle_ValueRemoval_For_Limited_Users(
bool updateWithLimitedUserAccess)
{
await LanguageService.CreateAsync(
new Language("de-DE", "German"), Constants.Security.SuperUserKey);
var userKey = updateWithLimitedUserAccess
? (await CreateLimitedUser()).Key
: Constants.Security.SuperUserKey;
var elementType = CreateElementType(ContentVariation.Culture);
var blockListDataType = await CreateBlockListDataType(elementType);
var contentType = CreateContentType(ContentVariation.Culture, blockListDataType);
var content = CreateContent(contentType, elementType, [], false);
content.SetCultureName("Home (de)", "de-DE");
ContentService.Save(content);
var firstContentElementKey = Guid.NewGuid();
var firstSettingsElementKey = Guid.NewGuid();
var secondContentElementKey = Guid.NewGuid();
var secondSettingsElementKey = Guid.NewGuid();
var blockListValue = BlockListPropertyValue(
elementType,
[
(
firstContentElementKey,
firstSettingsElementKey,
new BlockProperty(
new List<BlockPropertyValue> {
new() { Alias = "invariantText", Value = "#1: The first invariant content value" },
new() { Alias = "variantText", Value = "#1: The first content value in English", Culture = "en-US" },
new() { Alias = "variantText", Value = "#1: The first content value in Danish", Culture = "da-DK" },
new() { Alias = "variantText", Value = "#1: The first content value in German", Culture = "de-DE" },
},
new List<BlockPropertyValue> {
new() { Alias = "invariantText", Value = "#1: The first invariant settings value" },
new() { Alias = "variantText", Value = "#1: The first settings value in English", Culture = "en-US" },
new() { Alias = "variantText", Value = "#1: The first settings value in Danish", Culture = "da-DK" },
new() { Alias = "variantText", Value = "#1: The first settings value in German", Culture = "de-DE" },
},
null,
null)),
(
secondContentElementKey,
secondSettingsElementKey,
new BlockProperty(
new List<BlockPropertyValue> {
new() { Alias = "invariantText", Value = "#2: The first invariant content value" },
new() { Alias = "variantText", Value = "#2: The first content value in English", Culture = "en-US" },
new() { Alias = "variantText", Value = "#2: The first content value in Danish", Culture = "da-DK" },
new() { Alias = "variantText", Value = "#2: The first content value in German", Culture = "de-DE" },
},
new List<BlockPropertyValue> {
new() { Alias = "invariantText", Value = "#2: The first invariant settings value" },
new() { Alias = "variantText", Value = "#2: The first settings value in English", Culture = "en-US" },
new() { Alias = "variantText", Value = "#2: The first settings value in Danish", Culture = "da-DK" },
new() { Alias = "variantText", Value = "#2: The first settings value in German", Culture = "de-DE" },
},
null,
null))
]);
content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue));
ContentService.Save(content);
// remove a value the limited user can remove
blockListValue.ContentData.First().Values.RemoveAll(value => value.Culture == "da-DK");
blockListValue.SettingsData.First().Values.RemoveAll(value => value.Culture == "da-DK");
// remove a value the admin user can remove
blockListValue.ContentData.First().Values.RemoveAll(value => value.Culture == "en-US");
blockListValue.SettingsData.First().Values.RemoveAll(value => value.Culture == "en-US");
var updateModel = new ContentUpdateModel
{
InvariantProperties = new[]
{
new PropertyValueModel { Alias = "blocks", Value = JsonSerializer.Serialize(blockListValue) }
},
Variants = new[]
{
new VariantModel { Name = content.GetCultureName("en-US")!, Culture = "en-US", Properties = [] },
new VariantModel { Name = content.GetCultureName("da-DK")!, Culture = "da-DK", Properties = [] },
new VariantModel { Name = content.GetCultureName("de-DE")!, Culture = "de-DE", Properties = [] }
},
};
var result = await ContentEditingService.UpdateAsync(content.Key, updateModel, userKey);
Assert.IsTrue(result.Success);
content = ContentService.GetById(content.Key);
var savedBlocksValue = content?.Properties["blocks"]?.GetValue()?.ToString();
Assert.NotNull(savedBlocksValue);
blockListValue = JsonSerializer.Deserialize<BlockListValue>(savedBlocksValue);
if (updateWithLimitedUserAccess)
{
Assert.Multiple(() =>
{
// Should only have removed the danish value
Assert.AreEqual(1, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.ContentKey == firstContentElementKey));
Assert.AreEqual(1, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.SettingsKey == firstSettingsElementKey));
Assert.AreEqual(3, blockListValue.ContentData.Sum(contentData => contentData.Values.Count(value => value.Value?.ToString()?.StartsWith("#1") == true)));
Assert.AreEqual(3, blockListValue.SettingsData.Sum(settingsData => settingsData.Values.Count(value => value.Value?.ToString()?.StartsWith("#1") == true)));
Assert.AreEqual(0, blockListValue.ContentData.First().Values.Count(value => value.Culture == "da-DK"));
Assert.AreEqual(1, blockListValue.ContentData.First().Values.Count(value => value.Culture == "en-US"));
Assert.AreEqual(0, blockListValue.SettingsData.First().Values.Count(value => value.Culture == "da-DK"));
Assert.AreEqual(1, blockListValue.SettingsData.First().Values.Count(value => value.Culture == "en-US"));
// second wasn't touched
Assert.AreEqual(1, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.SettingsKey == secondSettingsElementKey));
Assert.AreEqual(1, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.ContentKey == secondContentElementKey));
Assert.AreEqual(4, blockListValue.ContentData.Sum(contentData => contentData.Values.Count(value => value.Value?.ToString()?.StartsWith("#2") == true)));
Assert.AreEqual(4, blockListValue.SettingsData.Sum(settingsData => settingsData.Values.Count(value => value.Value?.ToString()?.StartsWith("#2") == true)));
});
}
else
{
Assert.Multiple(() =>
{
// both danish and english should be removed
Assert.AreEqual(1, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.ContentKey == firstContentElementKey));
Assert.AreEqual(1, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.SettingsKey == firstSettingsElementKey));
Assert.AreEqual(2, blockListValue.ContentData.Sum(contentData => contentData.Values.Count(value => value.Value?.ToString()?.StartsWith("#1") == true)));
Assert.AreEqual(2, blockListValue.SettingsData.Sum(settingsData => settingsData.Values.Count(value => value.Value?.ToString()?.StartsWith("#1") == true)));
Assert.AreEqual(0, blockListValue.ContentData.First().Values.Count(value => value.Culture == "da-DK"));
Assert.AreEqual(0, blockListValue.ContentData.First().Values.Count(value => value.Culture == "en-US"));
Assert.AreEqual(0, blockListValue.SettingsData.First().Values.Count(value => value.Culture == "da-DK"));
Assert.AreEqual(0, blockListValue.SettingsData.First().Values.Count(value => value.Culture == "en-US"));
// second wasn't touched
Assert.AreEqual(1, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.SettingsKey == secondSettingsElementKey));
Assert.AreEqual(1, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.ContentKey == secondContentElementKey));
Assert.AreEqual(4, blockListValue.ContentData.Sum(contentData => contentData.Values.Count(value => value.Value?.ToString()?.StartsWith("#2") == true)));
Assert.AreEqual(4, blockListValue.SettingsData.Sum(settingsData => settingsData.Values.Count(value => value.Value?.ToString()?.StartsWith("#2") == true)));
});
}
}
@@ -940,4 +1478,55 @@ internal partial class BlockListElementLevelVariationTests
return user;
}
private void AddBlock(BlockListValue listValue, BlockItemData contentData, BlockItemData? settingsData, IContentType elementType)
{
listValue.ContentData.Add(contentData);
if (settingsData != null)
{
listValue.SettingsData.Add(settingsData);
}
var cultures = elementType.VariesByCulture()
? contentData.Values.Select(value => value.Culture)
.WhereNotNull()
.Distinct()
.ToArray()
: [null];
if (cultures.Any() is false)
{
cultures = [null];
}
var segments = elementType.VariesBySegment()
? contentData.Values.Select(value => value.Segment)
.Distinct()
.ToArray()
: [null];
foreach (var exposeItem in cultures.SelectMany(culture => segments.Select(segment =>
new BlockItemVariation(contentData.Key, culture, segment))))
{
listValue.Expose.Add(exposeItem);
}
listValue.Layout[Constants.PropertyEditors.Aliases.BlockList] = listValue
.Layout[Constants.PropertyEditors.Aliases.BlockList]
.Append(new BlockListLayoutItem { ContentKey = contentData.Key, SettingsKey = settingsData?.Key });
}
private void RemoveBlock(BlockListValue listValue, Guid blockKey)
{
// remove the item from the layout
var layoutItem = listValue.Layout[Constants.PropertyEditors.Aliases.BlockList].First(x => x.ContentKey == blockKey);
listValue.Layout[Constants.PropertyEditors.Aliases.BlockList] = listValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Where(layout => layout.ContentKey != blockKey);
listValue.ContentData.RemoveAll(contentData => contentData.Key == blockKey);
if (layoutItem.SettingsKey != null)
{
listValue.SettingsData.RemoveAll(settingsData => settingsData.Key == layoutItem.SettingsKey);
}
listValue.Expose.RemoveAll(exposeItem => exposeItem.ContentKey == blockKey);
}
}

View File

@@ -0,0 +1,67 @@
# Block Element Level Variant
### Notes
- When talking about variant data, we mean language variant
- Segment variants are not taken into account at this moment but can be expected to work in a similar manner
### What is it
When an element document type supports variant data (marked as variant and has a property marked as variant) is used
inside an invariant property on a variant document type, that property is considered to have element level variant data
which is a form of partial variant data.
When a property editor supports partial variant data (`IDataEditor.CanMergePartialPropertyValues()`) the
`ContentEditingService` will run the `IDataEditor.MergeVariantInvariantPropertyValue(...)` method to get a valid
value according to the rules defined for that propertyEditor
Block Element level variant data is this within all (core) property editors derived from blocks
- Umbraco.BlockList
- Umbraco.BlockGrid
- Umbraco.RichText
Most logic regarding this feature can be found in `BlockValuePropertyValueEditorBase`
### Axioms
1. A `null` value for a property, including element level variation based properties, is a valid value
2. The invariant value holds the structure/representation of the underlying variant values
3. The structure takes precedence over the underlying data
## Editing Data
### Access to invariant data
- All Languages: The user has access to all languages
- Default Language: The user has access to the language that is defined as the default
- AllowEditInvariantFromNonDefault: Configuration setting
| All Languages | Default Language | AllowEditInvariantFromNonDefault | Can Edit Invariant |
|---------------|------------------|----------------------------------|--------------------|
| True | Inherits True | N/A | True |
| False | True | N/A | True |
| False | False | True | True |
| False | False | False | False |
### Rules derived from the axioms
- A user with access to invariant data is allowed to add or remove blocks even if those blocks hold language variant
data they do not have access to.
- A user without access to invariant data is NOT allowed to add or remove blocks.
- A user can only edit element variant properties for the languages they have access to.
- A user is allowed to clear (set value to `null`) an element level variation as long as they have access to edit invariant data.
## Exposing
When a block is defined on invariant level but a language has not had its variant fields filled in yet,
the variant version of the block might be empty or considered not ready for publishing. The Expose feature allows
editors to define in which culture a block is ready to be consumed by the publishing process.
The client currently adds a blocks culture to the expose list when editing for that blocks starts in the culture,
more precisely when inline editor for the block is opened.
From an API perspective you are allowed to add and remove cultures from the expose list as long as you have the permissions to do so
### Axioms
- Expose data is linked to the same permissions as variant editing
- Variant blocks that are not exposed for a specific culture should not be processed by the publish feature
### Rules derived from the axioms
- Only a user with access to a language should be able to remove or add a block to the expose list for that language
- A block that is not exposed for a given language should not exist in the published value of the document for that language
- A block that is not exposed should not be processed when running validation during publishing or by running the validation separately.