Enable single block mode (#13216)
* Enable single block mode * Fixes tests, and adds test for single block mode output type * Update src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> * Update src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> * Fix breaking change Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>
This commit is contained in:
@@ -16,6 +16,13 @@ public class BlockListConfiguration
|
||||
[ConfigurationField("validationLimit", "Amount", "numberrange", Description = "Set a required range of blocks")]
|
||||
public NumberRange ValidationLimit { get; set; } = new();
|
||||
|
||||
[ConfigurationField("useSingleBlockMode", "Single block mode", "boolean",
|
||||
Description = @"When in Single block mode, the output will be BlockListItem<>, instead of BlockListModel.
|
||||
|
||||
**NOTE:**
|
||||
Single block mode requires a maximum of one available block, and an amount set to minimum 1 and maximum 1 blocks.")]
|
||||
public bool UseSingleBlockMode { get; set; }
|
||||
|
||||
[ConfigurationField("useLiveEditing", "Live editing mode", "boolean", Description = "Live editing in editor overlays for live updated custom views or labels using custom expression.")]
|
||||
public bool UseLiveEditing { get; set; }
|
||||
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System.Reflection;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Logging;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Web.Common.DependencyInjection;
|
||||
using Umbraco.Extensions;
|
||||
using static Umbraco.Cms.Core.PropertyEditors.BlockListConfiguration;
|
||||
|
||||
@@ -12,18 +17,70 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters;
|
||||
[DefaultPropertyValueConverter(typeof(JsonValueConverter))]
|
||||
public class BlockListPropertyValueConverter : BlockPropertyValueConverterBase<BlockListModel, BlockListItem, BlockListLayoutItem, BlockConfiguration>
|
||||
{
|
||||
private readonly IContentTypeService _contentTypeService;
|
||||
private readonly BlockEditorConverter _blockConverter;
|
||||
private readonly BlockListEditorDataConverter _blockListEditorDataConverter;
|
||||
private readonly IProfilingLogger _proflog;
|
||||
|
||||
public BlockListPropertyValueConverter(IProfilingLogger proflog, BlockEditorConverter blockConverter)
|
||||
[Obsolete("Use the constructor with the IContentTypeService")]
|
||||
public BlockListPropertyValueConverter(IProfilingLogger proflog, BlockEditorConverter blockConverter) : this(proflog, blockConverter, StaticServiceProvider.Instance.GetRequiredService<IContentTypeService>()) { }
|
||||
|
||||
public BlockListPropertyValueConverter(IProfilingLogger proflog, BlockEditorConverter blockConverter, IContentTypeService contentTypeService)
|
||||
: base(blockConverter)
|
||||
{
|
||||
_proflog = proflog;
|
||||
_blockConverter = blockConverter;
|
||||
_blockListEditorDataConverter = new BlockListEditorDataConverter();
|
||||
_contentTypeService = contentTypeService;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool IsConverter(IPublishedPropertyType propertyType)
|
||||
=> propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.BlockList);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
|
||||
{
|
||||
var isSingleBlockMode = IsSingleBlockMode(propertyType.DataType);
|
||||
if (isSingleBlockMode)
|
||||
{
|
||||
BlockListConfiguration.BlockConfiguration? block =
|
||||
ConfigurationEditor.ConfigurationAs<BlockListConfiguration>(propertyType.DataType.Configuration)?.Blocks.FirstOrDefault();
|
||||
|
||||
ModelType? contentElementType = block?.ContentElementTypeKey is Guid contentElementTypeKey && _contentTypeService.Get(contentElementTypeKey) is IContentType contentType ? ModelType.For(contentType.Alias) : null;
|
||||
ModelType? settingsElementType = block?.SettingsElementTypeKey is Guid settingsElementTypeKey && _contentTypeService.Get(settingsElementTypeKey) is IContentType settingsType ? ModelType.For(settingsType.Alias) : null;
|
||||
|
||||
if (contentElementType is not null)
|
||||
{
|
||||
if (settingsElementType is not null)
|
||||
{
|
||||
return typeof(BlockListItem<,>).MakeGenericType(contentElementType, settingsElementType);
|
||||
}
|
||||
|
||||
return typeof(BlockListItem<>).MakeGenericType(contentElementType);
|
||||
}
|
||||
|
||||
return typeof(BlockListItem);
|
||||
}
|
||||
|
||||
return typeof(BlockListModel);
|
||||
}
|
||||
|
||||
private bool IsSingleBlockMode(PublishedDataType dataType)
|
||||
{
|
||||
BlockListConfiguration? config =
|
||||
ConfigurationEditor.ConfigurationAs<BlockListConfiguration>(dataType.Configuration);
|
||||
return (config?.UseSingleBlockMode ?? false) && config?.Blocks.Length == 1 && config?.ValidationLimit?.Min == 1 && config?.ValidationLimit?.Max == 1;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
|
||||
=> PropertyCacheLevel.Element;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview)
|
||||
=> source?.ToString();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview)
|
||||
{
|
||||
@@ -44,7 +101,7 @@ public class BlockListPropertyValueConverter : BlockPropertyValueConverterBase<B
|
||||
|
||||
BlockListModel blockModel = UnwrapBlockModel(referenceCacheLevel, inter, preview, configuration.Blocks, CreateEmptyModel, CreateModel);
|
||||
|
||||
return blockModel;
|
||||
return IsSingleBlockMode(propertyType.DataType) ? blockModel.FirstOrDefault() : blockModel;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,12 +38,18 @@ angular.module("umbraco")
|
||||
"disabled": vm.model.clipboardItems.length === 0
|
||||
}];
|
||||
|
||||
if (vm.model.openClipboard === true) {
|
||||
if (vm.model.singleBlockMode === true && vm.model.openClipboard === true) {
|
||||
vm.navigation.splice(0,1);
|
||||
vm.activeTab = vm.navigation[0];
|
||||
}
|
||||
else if (vm.model.openClipboard === true) {
|
||||
vm.activeTab = vm.navigation[1];
|
||||
} else {
|
||||
vm.activeTab = vm.navigation[0];
|
||||
}
|
||||
|
||||
|
||||
|
||||
vm.activeTab.active = true;
|
||||
}
|
||||
);
|
||||
@@ -55,10 +61,16 @@ angular.module("umbraco")
|
||||
};
|
||||
|
||||
vm.clickClearClipboard = function () {
|
||||
vm.onNavigationChanged(vm.navigation[0]);
|
||||
vm.navigation[1].disabled = true;// disabled ws determined when creating the navigation, so we need to update it here.
|
||||
vm.model.clipboardItems = [];// This dialog is not connected via the clipboardService events, so we need to update manually.
|
||||
vm.model.clickClearClipboard();
|
||||
if (vm.model.singleBlockMode !== true && vm.model.openClipboard !== true)
|
||||
{
|
||||
vm.onNavigationChanged(vm.navigation[0]);
|
||||
vm.navigation[1].disabled = true;// disabled ws determined when creating the navigation, so we need to update it here.
|
||||
}
|
||||
else {
|
||||
vm.close();
|
||||
}
|
||||
};
|
||||
|
||||
vm.model = $scope.model;
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
ng-click="vm.requestShowCreate($index, $event)"
|
||||
ng-controller="Umbraco.PropertyEditors.BlockListPropertyEditor.CreateButtonController as inlineCreateButtonCtrl"
|
||||
ng-mousemove="inlineCreateButtonCtrl.onMouseMove($event)"
|
||||
ng-if="!vm.singleBlockMode"
|
||||
ng-show="!vm.readonly">
|
||||
<div class="__plus" ng-style="{'left':inlineCreateButtonCtrl.plusPosX}">
|
||||
<umb-icon icon="icon-add" class="icon"></umb-icon>
|
||||
@@ -28,7 +29,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="umb-block-list__actions" ng-if="vm.loading !== true">
|
||||
<div class="umb-block-list__actions" ng-if="vm.loading !== true && !vm.singleBlockMode">
|
||||
<button
|
||||
id="{{vm.model.alias}}"
|
||||
type="button"
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<localize key="general_copy">Copy</localize>
|
||||
</span>
|
||||
</button>
|
||||
<button ng-if="!vm.blockEditorApi.readonly" type="button" class="btn-reset umb-outline action --delete" localize="title" title="actions_delete"
|
||||
<button ng-if="!vm.blockEditorApi.readonly && (!vm.blockEditorApi.singleBlockMode || vm.index > 0)" type="button" class="btn-reset umb-outline action --delete" localize="title" title="actions_delete"
|
||||
ng-click="vm.blockEditorApi.requestDeleteBlock(vm.layout.$block);">
|
||||
<umb-icon icon="icon-trash" class="icon"></umb-icon>
|
||||
<span class="sr-only">
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
// Property actions:
|
||||
let copyAllBlocksAction = null;
|
||||
let deleteAllBlocksAction = null;
|
||||
let pasteSingleBlockAction = null;
|
||||
|
||||
var inlineEditing = false;
|
||||
var liveEditing = true;
|
||||
@@ -43,6 +44,7 @@
|
||||
var vm = this;
|
||||
|
||||
vm.readonly = false;
|
||||
vm.singleBlockMode = false;
|
||||
|
||||
$attrs.$observe('readonly', (value) => {
|
||||
vm.readonly = value !== undefined;
|
||||
@@ -105,6 +107,12 @@
|
||||
|
||||
inlineEditing = vm.model.config.useInlineEditingAsDefault;
|
||||
liveEditing = vm.model.config.useLiveEditing;
|
||||
vm.singleBlockMode =
|
||||
vm.model.config.validationLimit.min == 1 &&
|
||||
vm.model.config.validationLimit.max == 1 &&
|
||||
vm.model.config.blocks.length == 1 &&
|
||||
vm.model.config.useSingleBlockMode;
|
||||
vm.blockEditorApi.singleBlockMode = vm.singleBlockMode;
|
||||
|
||||
vm.validationLimit = vm.model.config.validationLimit;
|
||||
|
||||
@@ -144,13 +152,25 @@
|
||||
useLegacyIcon: false
|
||||
};
|
||||
|
||||
var propertyActions = [
|
||||
copyAllBlocksAction,
|
||||
deleteAllBlocksAction
|
||||
];
|
||||
pasteSingleBlockAction = {
|
||||
labelKey: "content_createFromClipboard",
|
||||
labelTokens: [],
|
||||
icon: "icon-paste-in",
|
||||
method: requestShowClipboard,
|
||||
isDisabled: false,
|
||||
useLegacyIcon: false
|
||||
};
|
||||
|
||||
var propertyActions = [copyAllBlocksAction, deleteAllBlocksAction];
|
||||
|
||||
var propertyActionsForSingleBlockMode = [pasteSingleBlockAction];
|
||||
|
||||
if (vm.umbProperty) {
|
||||
if (vm.singleBlockMode) {
|
||||
vm.umbProperty.setPropertyActions(propertyActionsForSingleBlockMode);
|
||||
} else {
|
||||
vm.umbProperty.setPropertyActions(propertyActions);
|
||||
}
|
||||
}
|
||||
|
||||
// Create Model Object, to manage our data for this Block Editor.
|
||||
@@ -218,6 +238,20 @@
|
||||
|
||||
updateClipboard(true);
|
||||
|
||||
if (vm.singleBlockMode && vm.layout.length == 0) {
|
||||
var wasAdded = false;
|
||||
var blockType = vm.availableBlockTypes[0];
|
||||
|
||||
wasAdded = addNewBlock(1, blockType.blockConfigModel.contentElementTypeKey);
|
||||
|
||||
if (wasAdded && inlineEditing === true) {
|
||||
var blockObject = vm.layout[0]?.$block;
|
||||
if (blockObject) {
|
||||
blockObject.activate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vm.loading = false;
|
||||
|
||||
$scope.$evalAsync();
|
||||
@@ -528,6 +562,7 @@
|
||||
availableItems: vm.availableBlockTypes,
|
||||
title: vm.labels.grid_addElement,
|
||||
openClipboard: openClipboard,
|
||||
singleBlockMode: vm.singleBlockMode,
|
||||
orderBy: "$index",
|
||||
view: "views/common/infiniteeditors/blockpicker/blockpicker.html",
|
||||
size: (amountOfAvailableTypes > 8 ? "medium" : "small"),
|
||||
@@ -644,6 +679,8 @@
|
||||
if(firstTime !== true && vm.clipboardItems.length > oldAmount) {
|
||||
jumpClipboard();
|
||||
}
|
||||
|
||||
pasteSingleBlockAction.isDisabled = vm.clipboardItems.length === 0;
|
||||
}
|
||||
|
||||
var jumpClipboardTimeout;
|
||||
@@ -706,6 +743,13 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
if (vm.singleBlockMode) {
|
||||
if (vm.layout.length > 0) {
|
||||
deleteBlock(vm.layout[0].$block);
|
||||
index = 1;
|
||||
}
|
||||
}
|
||||
|
||||
var layoutEntry;
|
||||
if (pasteType === clipboardService.TYPES.ELEMENT_TYPE) {
|
||||
layoutEntry = modelObject.createFromElementType(pasteEntry);
|
||||
@@ -787,7 +831,8 @@
|
||||
requestDeleteBlock: requestDeleteBlock,
|
||||
deleteBlock: deleteBlock,
|
||||
openSettingsForBlock: openSettingsForBlock,
|
||||
readonly: vm.readonly
|
||||
readonly: vm.readonly,
|
||||
singleBlockMode: vm.singleBlockMode
|
||||
};
|
||||
|
||||
vm.sortableOptions = {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core;
|
||||
@@ -11,6 +10,7 @@ using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors;
|
||||
|
||||
@@ -64,7 +64,8 @@ public class BlockListPropertyValueConverterTests
|
||||
var publishedModelFactory = new NoopPublishedModelFactory();
|
||||
var editor = new BlockListPropertyValueConverter(
|
||||
Mock.Of<IProfilingLogger>(),
|
||||
new BlockEditorConverter(publishedSnapshotAccessor, publishedModelFactory));
|
||||
new BlockEditorConverter(publishedSnapshotAccessor, publishedModelFactory),
|
||||
Mock.Of<IContentTypeService>());
|
||||
return editor;
|
||||
}
|
||||
|
||||
@@ -90,6 +91,13 @@ public class BlockListPropertyValueConverterTests
|
||||
Blocks = new[] { new BlockListConfiguration.BlockConfiguration { ContentElementTypeKey = _contentKey1 } },
|
||||
};
|
||||
|
||||
private BlockListConfiguration ConfigForSingleBlockMode() => new()
|
||||
{
|
||||
Blocks = new[] { new BlockListConfiguration.BlockConfiguration { ContentElementTypeKey = _contentKey1 } },
|
||||
ValidationLimit = new() { Min = 1, Max = 1 },
|
||||
UseSingleBlockMode = true,
|
||||
};
|
||||
|
||||
private IPublishedPropertyType GetPropertyType(BlockListConfiguration config)
|
||||
{
|
||||
var dataType = new PublishedDataType(1, "test", new Lazy<object>(() => config));
|
||||
@@ -139,6 +147,20 @@ public class BlockListPropertyValueConverterTests
|
||||
Assert.AreEqual(typeof(BlockListModel), valueType);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Get_Value_Type_SingleBlockMode()
|
||||
{
|
||||
var editor = CreateConverter();
|
||||
var config = ConfigForSingleBlockMode();
|
||||
|
||||
var dataType = new PublishedDataType(1, "test", new Lazy<object>(() => config));
|
||||
var propType = Mock.Of<IPublishedPropertyType>(x => x.DataType == dataType);
|
||||
|
||||
var valueType = editor.GetPropertyValueType(propType);
|
||||
|
||||
Assert.AreEqual(typeof(BlockListItem), valueType);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Convert_Null_Empty()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user