Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
007d6cdf43 | ||
|
|
1e6900951e | ||
|
|
ab054cb23a | ||
|
|
9b4c617804 | ||
|
|
2766d52803 | ||
|
|
b57867ab70 | ||
|
|
f365493f0b |
@@ -64,4 +64,8 @@ public class BlockGridLayoutItem : BlockLayoutItemBase
|
||||
/// <inheritdoc />
|
||||
public override bool ReferencesSetting(Guid key)
|
||||
=> SettingsKey == key || Areas.Any(area => area.ContainsSetting(key));
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IEnumerable<IBlockLayoutItem> GetContainedLayouts()
|
||||
=> Areas.SelectMany(area => area.Items);
|
||||
}
|
||||
|
||||
@@ -5,12 +5,18 @@ namespace Umbraco.Cms.Core.Models.Blocks;
|
||||
/// </summary>
|
||||
public abstract class BlockLayoutItemBase : IBlockLayoutItem
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Guid Key { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid ContentKey { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid? SettingsKey { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsExternalContent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BlockLayoutItemBase" /> class.
|
||||
/// </summary>
|
||||
@@ -44,4 +50,7 @@ public abstract class BlockLayoutItemBase : IBlockLayoutItem
|
||||
/// <inheritdoc />
|
||||
public virtual bool ReferencesSetting(Guid key)
|
||||
=> SettingsKey == key;
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual IEnumerable<IBlockLayoutItem> GetContainedLayouts() => [];
|
||||
}
|
||||
|
||||
@@ -8,6 +8,18 @@ namespace Umbraco.Cms.Core.Models.Blocks;
|
||||
/// </summary>
|
||||
public interface IBlockLayoutItem
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the layout item key.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The layout item key.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// Uniquely identifies a layout item. Previously the <see cref="ContentKey"/> could be used for this, but
|
||||
/// with reusable elements, the same <see cref="ContentKey"/> can appear multiple times in one layout.
|
||||
/// </remarks>
|
||||
public Guid Key { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the content key.
|
||||
/// </summary>
|
||||
@@ -24,6 +36,11 @@ public interface IBlockLayoutItem
|
||||
/// </value>
|
||||
public Guid? SettingsKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if the content source is local or originates from the element service.
|
||||
/// </summary>
|
||||
public bool IsExternalContent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this layout item references the specified content key.
|
||||
/// </summary>
|
||||
@@ -41,4 +58,10 @@ public interface IBlockLayoutItem
|
||||
/// <c>true</c> if this layout item references the specified settings key; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public bool ReferencesSetting(Guid key) => SettingsKey == key;
|
||||
|
||||
/// <summary>
|
||||
/// Returns any nested layouts for this layout (e.g. area layouts for the Block Grid).
|
||||
/// </summary>
|
||||
/// <returns>The nested layouts.</returns>
|
||||
public IEnumerable<IBlockLayoutItem> GetContainedLayouts();
|
||||
}
|
||||
|
||||
+3
-4
@@ -1,12 +1,11 @@
|
||||
namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a property index value factory specifically for block-based property values.
|
||||
/// Represents a property index value factory specifically for block grid properties.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This marker interface allows for specialized indexing of block content,
|
||||
/// such as Block List, Block Grid, and Rich Text block values.
|
||||
/// This marker interface allows for specialized indexing of block grid content.
|
||||
/// </remarks>
|
||||
public interface IBlockValuePropertyIndexValueFactory : IPropertyIndexValueFactory
|
||||
public interface IBlockGridPropertyIndexValueFactory : IPropertyIndexValueFactory
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a property index value factory specifically for block list properties.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This marker interface allows for specialized indexing of block list content.
|
||||
/// </remarks>
|
||||
public interface IBlockListPropertyIndexValueFactory : IPropertyIndexValueFactory
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a property index value factory specifically for single block properties.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This marker interface allows for specialized indexing of single block content.
|
||||
/// </remarks>
|
||||
public interface ISingleBlockPropertyIndexValueFactory : IPropertyIndexValueFactory
|
||||
{
|
||||
}
|
||||
@@ -3,7 +3,17 @@ using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Cms.Core.PublishedCache;
|
||||
|
||||
/// <summary>
|
||||
/// A service for converting <see cref="BlockItemData"/> into <see cref="IPublishedElement"/>.
|
||||
/// </summary>
|
||||
public interface IBlockElementService
|
||||
{
|
||||
Task<IPublishedElement?> BuildElementAsync(BlockItemData blockItemData, bool? preview = null);
|
||||
/// <summary>
|
||||
/// Creates an <see cref="IPublishedElement"/> instance from <see cref="BlockItemData"/>.
|
||||
/// </summary>
|
||||
/// <param name="owner">The <see cref="IPublishedElement"/> that contains the block property which is the origin to the <see cref="BlockItemData"/>.</param>
|
||||
/// <param name="blockItemData">The <see cref="BlockItemData"/> containing the data to convert into an <see cref="IPublishedElement"/>.</param>
|
||||
/// <param name="preview">Whether to perform the conversion for preview.</param>
|
||||
/// <returns>The created <see cref="IPublishedElement"/>, or null if an element could not be created from the <see cref="BlockItemData"/>.</returns>
|
||||
Task<IPublishedElement?> BuildElementAsync(IPublishedElement owner, BlockItemData blockItemData, bool? preview = null);
|
||||
}
|
||||
|
||||
@@ -285,7 +285,9 @@ public static partial class UmbracoBuilderExtensions
|
||||
/// <returns>The same <see cref="Umbraco.Cms.Core.DependencyInjection.IUmbracoBuilder"/> instance so that multiple calls can be chained.</returns>
|
||||
public static IUmbracoBuilder AddPropertyIndexValueFactories(this IUmbracoBuilder builder)
|
||||
{
|
||||
builder.Services.AddSingleton<IBlockValuePropertyIndexValueFactory, BlockValuePropertyIndexValueFactory>();
|
||||
builder.Services.AddSingleton<IBlockListPropertyIndexValueFactory, BlockListPropertyIndexValueFactory>();
|
||||
builder.Services.AddSingleton<IBlockGridPropertyIndexValueFactory, BlockGridPropertyIndexValueFactory>();
|
||||
builder.Services.AddSingleton<ISingleBlockPropertyIndexValueFactory, SingleBlockPropertyIndexValueFactory>();
|
||||
builder.Services.AddSingleton<ITagPropertyIndexValueFactory, TagPropertyIndexValueFactory>();
|
||||
builder.Services.AddSingleton<IRichTextPropertyIndexValueFactory, RichTextPropertyIndexValueFactory>();
|
||||
builder.Services.AddSingleton<IDateOnlyPropertyIndexValueFactory, DateOnlyPropertyIndexValueFactory>();
|
||||
|
||||
@@ -76,7 +76,7 @@ public class MigrateSingleBlockList : AsyncMigrationBase
|
||||
SingleBlockListConfigurationCache blockListConfigurationCache,
|
||||
IDataValueEditorFactory dataValueEditorFactory,
|
||||
IIOHelper ioHelper,
|
||||
IBlockValuePropertyIndexValueFactory blockValuePropertyIndexValueFactory,
|
||||
ISingleBlockPropertyIndexValueFactory blockValuePropertyIndexValueFactory,
|
||||
IBlockEditorElementTypeCache elementTypeCache,
|
||||
AppCaches appCaches)
|
||||
: base(context)
|
||||
|
||||
+42
-8
@@ -110,10 +110,10 @@ public abstract class BlockEditorPropertyNotificationHandlerBase<TBlockLayoutIte
|
||||
|
||||
private void TraverseObject(JsonObject obj)
|
||||
{
|
||||
// we'll assume that the object is a data representation of a block based editor if it contains "contentData" and "settingsData".
|
||||
if (obj["contentData"] is JsonArray contentData && obj["settingsData"] is JsonArray settingsData)
|
||||
// we'll assume that the object is a data representation of a block based editor if it contains "contentData", "settingsData" and "layout".
|
||||
if (obj["contentData"] is JsonArray contentData && obj["settingsData"] is JsonArray settingsData && obj["layout"] is JsonObject layoutData)
|
||||
{
|
||||
ParseKeys(contentData, settingsData);
|
||||
ParseKeys(contentData, settingsData, layoutData);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -123,12 +123,46 @@ public abstract class BlockEditorPropertyNotificationHandlerBase<TBlockLayoutIte
|
||||
}
|
||||
}
|
||||
|
||||
private void ParseKeys(JsonArray contentData, JsonArray settingsData)
|
||||
private void ParseKeys(JsonArray contentData, JsonArray settingsData, JsonObject layoutData)
|
||||
{
|
||||
// grab all keys from the objects of contentData and settingsData
|
||||
var keys = contentData.Select(c => c?["key"])
|
||||
.Union(settingsData.Select(s => s?["key"]))
|
||||
.Select(keyToken => keyToken?.GetValue<string>().NullOrWhiteSpaceAsNull())
|
||||
// recurse a JSON object to find all contained block editor layouts
|
||||
List<JsonObject> GetLayoutItemsRecursively(JsonObject jsonObject)
|
||||
{
|
||||
var layoutItems = new List<JsonObject>();
|
||||
if (jsonObject.ContainsKey("key") && jsonObject.ContainsKey("contentKey"))
|
||||
{
|
||||
// assume it's a layout if it has "key" and "contentKey"
|
||||
layoutItems.Add(jsonObject);
|
||||
}
|
||||
|
||||
foreach (JsonNode property in jsonObject.Select(v => v.Value).WhereNotNull())
|
||||
{
|
||||
IEnumerable<JsonObject> childrenToRecurse = property is JsonObject jsonObjectChild
|
||||
? [jsonObjectChild]
|
||||
: property is JsonArray jsonArrayChild
|
||||
? jsonArrayChild.OfType<JsonObject>()
|
||||
: [];
|
||||
layoutItems.AddRange(childrenToRecurse.SelectMany(GetLayoutItemsRecursively));
|
||||
}
|
||||
|
||||
return layoutItems;
|
||||
}
|
||||
|
||||
// grab keys applicable for replacement from all the layouts - that is:
|
||||
// - the key of the layout itself ("key").
|
||||
// - the key of the content item ("contentKey").
|
||||
// - ONLY for local content; do NOT replace content item keys for shared content.
|
||||
// - the key of the settings item ("settingsKey") if present.
|
||||
List<JsonObject> layoutItems = GetLayoutItemsRecursively(layoutData);
|
||||
var keys = layoutItems.SelectMany(layoutItem => new[]
|
||||
{
|
||||
layoutItem["key"]?.GetValue<string>(),
|
||||
layoutItem["isExternalContent"]?.GetValue<bool>() is not true
|
||||
? layoutItem["contentKey"]?.GetValue<string>()
|
||||
: null,
|
||||
layoutItem["settingsKey"]?.GetValue<string>(),
|
||||
})
|
||||
.WhereNotNull()
|
||||
.ToArray();
|
||||
|
||||
// the following is solely for avoiding functionality wise breakage. we should consider removing it eventually, but for the time being it's harmless.
|
||||
|
||||
@@ -127,7 +127,7 @@ public abstract class BlockEditorPropertyValueEditor<TValue, TLayout> : BlockVal
|
||||
}
|
||||
|
||||
private static bool IsBlockEditorDataEmpty([NotNullWhen(false)] BlockEditorData<TValue, TLayout>? editorData)
|
||||
=> editorData is null || editorData.BlockValue.ContentData.Count == 0;
|
||||
=> editorData is null || editorData.BlockValue.Layout.Count == 0;
|
||||
|
||||
// We don't throw on error here because we want to be able to parse what we can, even if some of the data is invalid. In cases where migrating
|
||||
// from nested content to blocks, we don't want to trigger a fatal error for retrieving references, as this isn't vital to the operation.
|
||||
|
||||
@@ -63,13 +63,20 @@ public class BlockEditorValues<TValue, TLayout>
|
||||
|
||||
private BlockEditorData<TValue, TLayout>? Clean(BlockEditorData<TValue, TLayout> blockEditorData)
|
||||
{
|
||||
if (blockEditorData.BlockValue.ContentData.Count == 0)
|
||||
if (blockEditorData.BlockValue.Layout.Count == 0)
|
||||
{
|
||||
// if there's no content ensure there's no settings too
|
||||
blockEditorData.BlockValue.SettingsData.Clear();
|
||||
return null;
|
||||
}
|
||||
|
||||
if (blockEditorData.BlockValue.ContentData.Count == 0
|
||||
&& blockEditorData.BlockValue.SettingsData.Count == 0)
|
||||
{
|
||||
// no local content or settings; the block editor must contain only global elements
|
||||
return blockEditorData;
|
||||
}
|
||||
|
||||
var contentTypePropertyTypes = new Dictionary<string, Dictionary<string, IPropertyType>>();
|
||||
|
||||
// filter out any content that isn't referenced in the layout references
|
||||
|
||||
@@ -26,7 +26,7 @@ public class BlockGridPropertyEditor : BlockGridPropertyEditorBase
|
||||
public BlockGridPropertyEditor(
|
||||
IDataValueEditorFactory dataValueEditorFactory,
|
||||
IIOHelper ioHelper,
|
||||
IBlockValuePropertyIndexValueFactory blockValuePropertyIndexValueFactory)
|
||||
IBlockGridPropertyIndexValueFactory blockValuePropertyIndexValueFactory)
|
||||
: base(dataValueEditorFactory, blockValuePropertyIndexValueFactory)
|
||||
=> _ioHelper = ioHelper;
|
||||
|
||||
|
||||
@@ -25,9 +25,9 @@ namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
/// </summary>
|
||||
public abstract class BlockGridPropertyEditorBase : DataEditor, IValueSchemaProvider
|
||||
{
|
||||
private readonly IBlockValuePropertyIndexValueFactory _blockValuePropertyIndexValueFactory;
|
||||
private readonly IBlockGridPropertyIndexValueFactory _blockValuePropertyIndexValueFactory;
|
||||
|
||||
protected BlockGridPropertyEditorBase(IDataValueEditorFactory dataValueEditorFactory, IBlockValuePropertyIndexValueFactory blockValuePropertyIndexValueFactory)
|
||||
protected BlockGridPropertyEditorBase(IDataValueEditorFactory dataValueEditorFactory, IBlockGridPropertyIndexValueFactory blockValuePropertyIndexValueFactory)
|
||||
: base(dataValueEditorFactory)
|
||||
{
|
||||
_blockValuePropertyIndexValueFactory = blockValuePropertyIndexValueFactory;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
internal sealed class BlockGridPropertyIndexValueFactory
|
||||
: BlockValuePropertyIndexValueFactoryBase<BlockGridValue>, IBlockGridPropertyIndexValueFactory
|
||||
{
|
||||
public BlockGridPropertyIndexValueFactory(
|
||||
PropertyEditorCollection propertyEditorCollection,
|
||||
IElementService elementService,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IOptionsMonitor<IndexingSettings> indexingSettings)
|
||||
: base(propertyEditorCollection, elementService, jsonSerializer, indexingSettings)
|
||||
{
|
||||
}
|
||||
|
||||
protected override IEnumerable<RawDataItem> GetDataItems(BlockGridValue input, bool published)
|
||||
=> GetDataItems(input.GetLayouts() ?? [], input.ContentData, input.Expose, published);
|
||||
}
|
||||
@@ -25,7 +25,7 @@ public class BlockListPropertyEditor : BlockListPropertyEditorBase
|
||||
public BlockListPropertyEditor(
|
||||
IDataValueEditorFactory dataValueEditorFactory,
|
||||
IIOHelper ioHelper,
|
||||
IBlockValuePropertyIndexValueFactory blockValuePropertyIndexValueFactory,
|
||||
IBlockListPropertyIndexValueFactory blockValuePropertyIndexValueFactory,
|
||||
IJsonSerializer jsonSerializer)
|
||||
: base(dataValueEditorFactory, blockValuePropertyIndexValueFactory, jsonSerializer)
|
||||
=> _ioHelper = ioHelper;
|
||||
|
||||
@@ -21,13 +21,13 @@ namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
/// </summary>
|
||||
public abstract class BlockListPropertyEditorBase : DataEditor, IValueSchemaProvider
|
||||
{
|
||||
private readonly IBlockValuePropertyIndexValueFactory _blockValuePropertyIndexValueFactory;
|
||||
private readonly IBlockListPropertyIndexValueFactory _blockValuePropertyIndexValueFactory;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BlockListPropertyEditorBase"/> class.
|
||||
/// </summary>
|
||||
protected BlockListPropertyEditorBase(IDataValueEditorFactory dataValueEditorFactory, IBlockValuePropertyIndexValueFactory blockValuePropertyIndexValueFactory, IJsonSerializer jsonSerializer)
|
||||
protected BlockListPropertyEditorBase(IDataValueEditorFactory dataValueEditorFactory, IBlockListPropertyIndexValueFactory blockValuePropertyIndexValueFactory, IJsonSerializer jsonSerializer)
|
||||
: base(dataValueEditorFactory)
|
||||
{
|
||||
_blockValuePropertyIndexValueFactory = blockValuePropertyIndexValueFactory;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
internal sealed class BlockListPropertyIndexValueFactory
|
||||
: BlockValuePropertyIndexValueFactoryBase<BlockListValue>, IBlockListPropertyIndexValueFactory
|
||||
{
|
||||
public BlockListPropertyIndexValueFactory(
|
||||
PropertyEditorCollection propertyEditorCollection,
|
||||
IElementService elementService,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IOptionsMonitor<IndexingSettings> indexingSettings)
|
||||
: base(propertyEditorCollection, elementService, jsonSerializer, indexingSettings)
|
||||
{
|
||||
}
|
||||
|
||||
protected override IEnumerable<RawDataItem> GetDataItems(BlockListValue input, bool published)
|
||||
=> GetDataItems(input.GetLayouts() ?? [], input.ContentData, input.Expose, published);
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
|
||||
namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
internal sealed class BlockValuePropertyIndexValueFactory :
|
||||
BlockValuePropertyIndexValueFactoryBase<BlockValuePropertyIndexValueFactory.IndexValueFactoryBlockValue>,
|
||||
IBlockValuePropertyIndexValueFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BlockValuePropertyIndexValueFactory"/> class.
|
||||
/// </summary>
|
||||
/// <param name="propertyEditorCollection">The <see cref="PropertyEditorCollection"/> containing available property editors.</param>
|
||||
/// <param name="jsonSerializer">The <see cref="IJsonSerializer"/> used for serializing and deserializing JSON values.</param>
|
||||
/// <param name="indexingSettings">The <see cref="IOptionsMonitor{IndexingSettings}"/> providing access to indexing configuration settings.</param>
|
||||
public BlockValuePropertyIndexValueFactory(
|
||||
PropertyEditorCollection propertyEditorCollection,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IOptionsMonitor<IndexingSettings> indexingSettings)
|
||||
: base(propertyEditorCollection, jsonSerializer, indexingSettings)
|
||||
{
|
||||
}
|
||||
|
||||
protected override IEnumerable<RawDataItem> GetDataItems(IndexValueFactoryBlockValue input, bool published)
|
||||
=> GetDataItems(input.ContentData, input.Expose, published);
|
||||
|
||||
// we only care about the content data when extracting values for indexing - not the layouts nor the settings
|
||||
internal sealed class IndexValueFactoryBlockValue
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the list of content block item data.
|
||||
/// </summary>
|
||||
public List<BlockItemData> ContentData { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the collection of <see cref="BlockItemVariation"/> instances that should be exposed by the index value factory.
|
||||
/// </summary>
|
||||
public List<BlockItemVariation> Expose { get; set; } = new();
|
||||
}
|
||||
}
|
||||
+66
-25
@@ -4,6 +4,7 @@ using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.Examine;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
@@ -12,14 +13,17 @@ namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
internal abstract class BlockValuePropertyIndexValueFactoryBase<TSerialized> : JsonPropertyIndexValueFactoryBase<TSerialized>
|
||||
{
|
||||
private readonly PropertyEditorCollection _propertyEditorCollection;
|
||||
private readonly IElementService _elementService;
|
||||
|
||||
protected BlockValuePropertyIndexValueFactoryBase(
|
||||
PropertyEditorCollection propertyEditorCollection,
|
||||
IElementService elementService,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IOptionsMonitor<IndexingSettings> indexingSettings)
|
||||
: base(jsonSerializer, indexingSettings)
|
||||
{
|
||||
_propertyEditorCollection = propertyEditorCollection;
|
||||
_elementService = elementService;
|
||||
}
|
||||
|
||||
protected override IEnumerable<IndexValue> Handle(
|
||||
@@ -106,37 +110,74 @@ internal abstract class BlockValuePropertyIndexValueFactoryBase<TSerialized> : J
|
||||
/// <summary>
|
||||
/// Unwraps block item data as data items.
|
||||
/// </summary>
|
||||
protected IEnumerable<RawDataItem> GetDataItems(IList<BlockItemData> contentData, IList<BlockItemVariation> expose, bool published)
|
||||
protected IEnumerable<RawDataItem> GetDataItems(IEnumerable<IBlockLayoutItem> layouts, IList<BlockItemData> contentData, IList<BlockItemVariation> expose, bool published)
|
||||
{
|
||||
List<RawDataItem> indexData;
|
||||
if (published is false)
|
||||
{
|
||||
return contentData.Select(ToRawData);
|
||||
indexData = contentData.Select(ToRawData).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
indexData = new();
|
||||
foreach (BlockItemData blockItemData in contentData)
|
||||
{
|
||||
var exposedCultures = expose
|
||||
.Where(e => e.ContentKey == blockItemData.Key)
|
||||
.Select(e => e.Culture)
|
||||
.ToArray();
|
||||
|
||||
if (exposedCultures.Any() is false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (exposedCultures.Contains(null)
|
||||
|| exposedCultures.ContainsAll(blockItemData.Values.Select(v => v.Culture)))
|
||||
{
|
||||
indexData.Add(ToRawData(blockItemData));
|
||||
continue;
|
||||
}
|
||||
|
||||
indexData.Add(
|
||||
ToRawData(
|
||||
blockItemData.ContentTypeKey,
|
||||
blockItemData.Values.Where(value => value.Culture is null || exposedCultures.Contains(value.Culture))));
|
||||
}
|
||||
}
|
||||
|
||||
var indexData = new List<RawDataItem>();
|
||||
foreach (BlockItemData blockItemData in contentData)
|
||||
IBlockLayoutItem[] layoutsAsArray = layouts as IBlockLayoutItem[] ?? layouts.ToArray();
|
||||
|
||||
// Get the shared element keys from all layouts.
|
||||
// NOTE: While the Grid areas are modeled to contain areas within areas, in reality it cannot be configured as
|
||||
// such, so this "top-level aggregation" of shared content keys works in effect.
|
||||
Guid[] sharedElementKeys = layoutsAsArray
|
||||
.Union(layoutsAsArray.SelectMany(l => l.GetContainedLayouts()))
|
||||
.Where(l => l.IsExternalContent)
|
||||
.Select(l => l.ContentKey)
|
||||
.ToArray();
|
||||
|
||||
if (sharedElementKeys.Length > 0)
|
||||
{
|
||||
var exposedCultures = expose
|
||||
.Where(e => e.ContentKey == blockItemData.Key)
|
||||
.Select(e => e.Culture)
|
||||
.ToArray();
|
||||
|
||||
if (exposedCultures.Any() is false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (exposedCultures.Contains(null)
|
||||
|| exposedCultures.ContainsAll(blockItemData.Values.Select(v => v.Culture)))
|
||||
{
|
||||
indexData.Add(ToRawData(blockItemData));
|
||||
continue;
|
||||
}
|
||||
|
||||
indexData.Add(
|
||||
ToRawData(
|
||||
blockItemData.ContentTypeKey,
|
||||
blockItemData.Values.Where(value => value.Culture is null || exposedCultures.Contains(value.Culture))));
|
||||
IEnumerable<IElement> elements = _elementService.GetByIds(sharedElementKeys);
|
||||
indexData.AddRange(
|
||||
elements.Select(element => new RawDataItem
|
||||
{
|
||||
ContentTypeKey = element.ContentType.Key,
|
||||
Properties = element
|
||||
.Properties
|
||||
.SelectMany(property => property
|
||||
.Values
|
||||
.Select(value => new RawPropertyData
|
||||
{
|
||||
Alias = property.Alias,
|
||||
Culture = value.Culture,
|
||||
Value = published
|
||||
? value.PublishedValue
|
||||
: value.EditedValue,
|
||||
}))
|
||||
.ToArray(),
|
||||
}));
|
||||
}
|
||||
|
||||
return indexData;
|
||||
|
||||
@@ -287,11 +287,29 @@ public abstract class BlockValuePropertyValueEditorBase<TValue, TLayout> : DataV
|
||||
|
||||
protected void MapBlockValueToEditor(IProperty property, TValue blockValue, string? culture, string? segment)
|
||||
{
|
||||
EnsureLayoutItemKeys(blockValue);
|
||||
|
||||
MapBlockItemDataToEditor(property, blockValue.ContentData, culture, segment);
|
||||
MapBlockItemDataToEditor(property, blockValue.SettingsData, culture, segment);
|
||||
_blockEditorVarianceHandler.AlignExposeVariance(blockValue);
|
||||
}
|
||||
|
||||
// Ensures that all layout items have a key (for backwards data format compatibility).
|
||||
private static void EnsureLayoutItemKeys(TValue blockValue)
|
||||
{
|
||||
if (!blockValue.Layout.TryGetValue(blockValue.PropertyEditorAlias, out IEnumerable<IBlockLayoutItem>? layout))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// All layout items with an empty key will be assigned the content key of the layout item.
|
||||
// This ensures data consistency across multiple sessions.
|
||||
foreach (IBlockLayoutItem layoutItem in layout.Where(layoutItem => layoutItem.Key == Guid.Empty))
|
||||
{
|
||||
layoutItem.Key = layoutItem.ContentKey;
|
||||
}
|
||||
}
|
||||
|
||||
protected IEnumerable<Guid> ConfiguredElementTypeKeys(IBlockConfiguration configuration)
|
||||
{
|
||||
yield return configuration.ContentElementTypeKey;
|
||||
|
||||
@@ -3,6 +3,7 @@ using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.Examine;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
@@ -19,14 +20,16 @@ internal sealed class RichTextPropertyIndexValueFactory : BlockValuePropertyInde
|
||||
/// </summary>
|
||||
/// <param name="propertyEditorCollection">A collection containing all available property editors.</param>
|
||||
/// <param name="jsonSerializer">The serializer used for handling JSON data.</param>
|
||||
/// <param name="elementService">Service for accessing elements.</param>
|
||||
/// <param name="indexingSettings">The monitor providing current indexing settings.</param>
|
||||
/// <param name="logger">The logger used for logging diagnostic information.</param>
|
||||
public RichTextPropertyIndexValueFactory(
|
||||
PropertyEditorCollection propertyEditorCollection,
|
||||
IElementService elementService,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IOptionsMonitor<IndexingSettings> indexingSettings,
|
||||
ILogger<RichTextPropertyIndexValueFactory> logger)
|
||||
: base(propertyEditorCollection, jsonSerializer, indexingSettings)
|
||||
: base(propertyEditorCollection, elementService, jsonSerializer, indexingSettings)
|
||||
{
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_logger = logger;
|
||||
@@ -156,7 +159,7 @@ internal sealed class RichTextPropertyIndexValueFactory : BlockValuePropertyInde
|
||||
}
|
||||
|
||||
protected override IEnumerable<RawDataItem> GetDataItems(RichTextEditorValue input, bool published)
|
||||
=> GetDataItems(input.Blocks?.ContentData ?? [], input.Blocks?.Expose ?? [], published);
|
||||
=> GetDataItems(input.Blocks?.GetLayouts() ?? [], input.Blocks?.ContentData ?? [], input.Blocks?.Expose ?? [], published);
|
||||
|
||||
/// <summary>
|
||||
/// Strips HTML tags from content, replacing them with spaces to preserve word boundaries for indexing.
|
||||
|
||||
@@ -27,7 +27,7 @@ public class SingleBlockPropertyEditor : DataEditor
|
||||
{
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IIOHelper _ioHelper;
|
||||
private readonly IBlockValuePropertyIndexValueFactory _blockValuePropertyIndexValueFactory;
|
||||
private readonly ISingleBlockPropertyIndexValueFactory _blockValuePropertyIndexValueFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SingleBlockPropertyEditor"/> class.
|
||||
@@ -40,7 +40,7 @@ public class SingleBlockPropertyEditor : DataEditor
|
||||
IDataValueEditorFactory dataValueEditorFactory,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IIOHelper ioHelper,
|
||||
IBlockValuePropertyIndexValueFactory blockValuePropertyIndexValueFactory)
|
||||
ISingleBlockPropertyIndexValueFactory blockValuePropertyIndexValueFactory)
|
||||
: base(dataValueEditorFactory)
|
||||
{
|
||||
_jsonSerializer = jsonSerializer;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
internal sealed class SingleBlockPropertyIndexValueFactory
|
||||
: BlockValuePropertyIndexValueFactoryBase<SingleBlockValue>, ISingleBlockPropertyIndexValueFactory
|
||||
{
|
||||
public SingleBlockPropertyIndexValueFactory(
|
||||
PropertyEditorCollection propertyEditorCollection,
|
||||
IElementService elementService,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IOptionsMonitor<IndexingSettings> indexingSettings)
|
||||
: base(propertyEditorCollection, elementService, jsonSerializer, indexingSettings)
|
||||
{
|
||||
}
|
||||
|
||||
protected override IEnumerable<RawDataItem> GetDataItems(SingleBlockValue input, bool published)
|
||||
=> GetDataItems(input.GetLayouts() ?? [], input.ContentData, input.Expose, published);
|
||||
}
|
||||
@@ -94,7 +94,7 @@ public sealed class BlockEditorConverter
|
||||
Key = data.Key,
|
||||
};
|
||||
|
||||
return _blockElementService.BuildElementAsync(alignedData, preview).GetAwaiter().GetResult();
|
||||
return _blockElementService.BuildElementAsync(owner, alignedData, preview).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+43
-4
@@ -9,6 +9,7 @@ using Umbraco.Cms.Core.Models.Blocks;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PropertyEditors.DeliveryApi;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Extensions;
|
||||
@@ -30,10 +31,21 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters
|
||||
private readonly BlockEditorVarianceHandler _blockEditorVarianceHandler;
|
||||
private readonly ILanguageService _languageService;
|
||||
private readonly IPropertyRenderingContextAccessor _propertyRenderingContextAccessor;
|
||||
private readonly IElementCacheService _elementCacheService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BlockGridPropertyValueConverter"/> class.
|
||||
/// </summary>
|
||||
/// <param name="proflog">The logger used for profiling and diagnostics.</param>
|
||||
/// <param name="blockConverter">The converter responsible for handling block editor values.</param>
|
||||
/// <param name="jsonSerializer">The serializer used for JSON serialization and deserialization.</param>
|
||||
/// <param name="apiElementBuilder">The builder for creating API elements from block data.</param>
|
||||
/// <param name="constructorCache">The cache for block grid property value constructors.</param>
|
||||
/// <param name="variationContextAccessor">Provides access to the current variation context.</param>
|
||||
/// <param name="blockEditorVarianceHandler">Handles variance logic for block editors.</param>
|
||||
/// <param name="languageService">Service for accessing all languages.</param>
|
||||
/// <param name="propertyRenderingContextAccessor">Provides access to the current rendering context.</param>
|
||||
/// <param name="elementCacheService">The cache for elements.</param>
|
||||
public BlockGridPropertyValueConverter(
|
||||
IProfilingLogger proflog,
|
||||
BlockEditorConverter blockConverter,
|
||||
@@ -43,7 +55,8 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters
|
||||
IVariationContextAccessor variationContextAccessor,
|
||||
BlockEditorVarianceHandler blockEditorVarianceHandler,
|
||||
ILanguageService languageService,
|
||||
IPropertyRenderingContextAccessor propertyRenderingContextAccessor)
|
||||
IPropertyRenderingContextAccessor propertyRenderingContextAccessor,
|
||||
IElementCacheService elementCacheService)
|
||||
{
|
||||
_proflog = proflog;
|
||||
_blockConverter = blockConverter;
|
||||
@@ -54,10 +67,11 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters
|
||||
_blockEditorVarianceHandler = blockEditorVarianceHandler;
|
||||
_languageService = languageService;
|
||||
_propertyRenderingContextAccessor = propertyRenderingContextAccessor;
|
||||
_elementCacheService = elementCacheService;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="BlockGridPropertyValueConverter(IProfilingLogger, BlockEditorConverter, IJsonSerializer, IApiElementBuilder, BlockGridPropertyValueConstructorCache, IVariationContextAccessor, BlockEditorVarianceHandler, ILanguageService, IPropertyRenderingContextAccessor)"/>
|
||||
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
|
||||
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 20.")]
|
||||
public BlockGridPropertyValueConverter(
|
||||
IProfilingLogger proflog,
|
||||
BlockEditorConverter blockConverter,
|
||||
@@ -70,6 +84,31 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the non-obsolete constructor. Scheduled for removal in V20.")]
|
||||
public BlockGridPropertyValueConverter(
|
||||
IProfilingLogger proflog,
|
||||
BlockEditorConverter blockConverter,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IApiElementBuilder apiElementBuilder,
|
||||
BlockGridPropertyValueConstructorCache constructorCache,
|
||||
IVariationContextAccessor variationContextAccessor,
|
||||
BlockEditorVarianceHandler blockEditorVarianceHandler,
|
||||
ILanguageService languageService,
|
||||
IPropertyRenderingContextAccessor propertyRenderingContextAccessor)
|
||||
: this(
|
||||
proflog,
|
||||
blockConverter,
|
||||
jsonSerializer,
|
||||
apiElementBuilder,
|
||||
constructorCache,
|
||||
variationContextAccessor,
|
||||
blockEditorVarianceHandler,
|
||||
languageService,
|
||||
propertyRenderingContextAccessor,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IElementCacheService>())
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool IsConverter(IPublishedPropertyType propertyType)
|
||||
=> propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.BlockGrid);
|
||||
@@ -80,7 +119,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters
|
||||
|
||||
/// <inheritdoc />
|
||||
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
|
||||
=> PropertyCacheLevel.Element;
|
||||
=> PropertyCacheLevel.Elements;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview)
|
||||
@@ -155,7 +194,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters
|
||||
return null;
|
||||
}
|
||||
|
||||
var creator = new BlockGridPropertyValueCreator(_blockConverter, _variationContextAccessor, _propertyRenderingContextAccessor, _blockEditorVarianceHandler, _jsonSerializer, _constructorCache, _languageService);
|
||||
var creator = new BlockGridPropertyValueCreator(_blockConverter, _variationContextAccessor, _propertyRenderingContextAccessor, _blockEditorVarianceHandler, _elementCacheService, _jsonSerializer, _constructorCache, _languageService);
|
||||
return creator.CreateBlockModelAsync(owner, referenceCacheLevel, intermediateBlockModelValue, preview, configuration.Blocks, configuration.GridColumns).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -1,5 +1,6 @@
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Extensions;
|
||||
@@ -16,19 +17,22 @@ internal sealed class BlockGridPropertyValueCreator : BlockPropertyValueCreatorB
|
||||
/// </summary>
|
||||
/// <param name="blockEditorConverter">The service used to convert block editor data into strongly typed objects.</param>
|
||||
/// <param name="variationContextAccessor">Provides access to the current variation context for content.</param>
|
||||
/// <param name="propertyRenderingContextAccessor">Provides access to the current rendering context.</param>
|
||||
/// <param name="blockEditorVarianceHandler">Handles variance logic for block editors, such as culture or segment variations.</param>
|
||||
/// <param name="jsonSerializer">The serializer used to handle JSON data for block grid properties.</param>
|
||||
/// <param name="constructorCache">A cache for constructors used when creating block grid property values, improving performance.</param>
|
||||
/// <param name="languageService">Service used to retrieve language information for fallback resolution.</param>
|
||||
/// <param name="elementCacheService">The cache for elements.</param>
|
||||
public BlockGridPropertyValueCreator(
|
||||
BlockEditorConverter blockEditorConverter,
|
||||
IVariationContextAccessor variationContextAccessor,
|
||||
IPropertyRenderingContextAccessor propertyRenderingContextAccessor,
|
||||
BlockEditorVarianceHandler blockEditorVarianceHandler,
|
||||
IElementCacheService elementCacheService,
|
||||
IJsonSerializer jsonSerializer,
|
||||
BlockGridPropertyValueConstructorCache constructorCache,
|
||||
ILanguageService languageService)
|
||||
: base(blockEditorConverter, variationContextAccessor, propertyRenderingContextAccessor, blockEditorVarianceHandler, languageService)
|
||||
: base(blockEditorConverter, variationContextAccessor, propertyRenderingContextAccessor, blockEditorVarianceHandler, languageService, elementCacheService)
|
||||
{
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_constructorCache = constructorCache;
|
||||
|
||||
+35
-3
@@ -10,6 +10,7 @@ using Umbraco.Cms.Core.Models.Blocks;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PropertyEditors.DeliveryApi;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.Extensions;
|
||||
@@ -34,6 +35,7 @@ public class BlockListPropertyValueConverter : PropertyValueConverterBase, IDeli
|
||||
private readonly BlockEditorVarianceHandler _blockEditorVarianceHandler;
|
||||
private readonly ILanguageService _languageService;
|
||||
private readonly IPropertyRenderingContextAccessor _propertyRenderingContextAccessor;
|
||||
private readonly IElementCacheService _elementCacheService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BlockListPropertyValueConverter"/> class.
|
||||
@@ -48,6 +50,7 @@ public class BlockListPropertyValueConverter : PropertyValueConverterBase, IDeli
|
||||
/// <param name="blockEditorVarianceHandler">Handles variance for block editors.</param>
|
||||
/// <param name="languageService">Service used to retrieve language information for fallback resolution.</param>
|
||||
/// <param name="propertyRenderingContextAccessor">Accessor for the current property rendering context.</param>
|
||||
/// <param name="elementCacheService">The cache for elements.</param>
|
||||
public BlockListPropertyValueConverter(
|
||||
IProfilingLogger proflog,
|
||||
BlockEditorConverter blockConverter,
|
||||
@@ -58,7 +61,8 @@ public class BlockListPropertyValueConverter : PropertyValueConverterBase, IDeli
|
||||
IVariationContextAccessor variationContextAccessor,
|
||||
BlockEditorVarianceHandler blockEditorVarianceHandler,
|
||||
ILanguageService languageService,
|
||||
IPropertyRenderingContextAccessor propertyRenderingContextAccessor)
|
||||
IPropertyRenderingContextAccessor propertyRenderingContextAccessor,
|
||||
IElementCacheService elementCacheService)
|
||||
{
|
||||
_proflog = proflog;
|
||||
_blockConverter = blockConverter;
|
||||
@@ -70,6 +74,7 @@ public class BlockListPropertyValueConverter : PropertyValueConverterBase, IDeli
|
||||
_blockEditorVarianceHandler = blockEditorVarianceHandler;
|
||||
_languageService = languageService;
|
||||
_propertyRenderingContextAccessor = propertyRenderingContextAccessor;
|
||||
_elementCacheService = elementCacheService;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="BlockListPropertyValueConverter(IProfilingLogger, BlockEditorConverter, IContentTypeService, IApiElementBuilder, IJsonSerializer, BlockListPropertyValueConstructorCache, IVariationContextAccessor, BlockEditorVarianceHandler, ILanguageService, IPropertyRenderingContextAccessor)"/>
|
||||
@@ -87,6 +92,33 @@ public class BlockListPropertyValueConverter : PropertyValueConverterBase, IDeli
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the non-obsolete constructor. Scheduled for removal in V20.")]
|
||||
public BlockListPropertyValueConverter(
|
||||
IProfilingLogger proflog,
|
||||
BlockEditorConverter blockConverter,
|
||||
IContentTypeService contentTypeService,
|
||||
IApiElementBuilder apiElementBuilder,
|
||||
IJsonSerializer jsonSerializer,
|
||||
BlockListPropertyValueConstructorCache constructorCache,
|
||||
IVariationContextAccessor variationContextAccessor,
|
||||
BlockEditorVarianceHandler blockEditorVarianceHandler,
|
||||
ILanguageService languageService,
|
||||
IPropertyRenderingContextAccessor propertyRenderingContextAccessor)
|
||||
: this(
|
||||
proflog,
|
||||
blockConverter,
|
||||
contentTypeService,
|
||||
apiElementBuilder,
|
||||
jsonSerializer,
|
||||
constructorCache,
|
||||
variationContextAccessor,
|
||||
blockEditorVarianceHandler,
|
||||
languageService,
|
||||
propertyRenderingContextAccessor,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IElementCacheService>())
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool IsConverter(IPublishedPropertyType propertyType)
|
||||
=> propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.BlockList);
|
||||
@@ -128,7 +160,7 @@ public class BlockListPropertyValueConverter : PropertyValueConverterBase, IDeli
|
||||
|
||||
/// <inheritdoc />
|
||||
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
|
||||
=> PropertyCacheLevel.Element;
|
||||
=> PropertyCacheLevel.Elements;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview)
|
||||
@@ -196,7 +228,7 @@ public class BlockListPropertyValueConverter : PropertyValueConverterBase, IDeli
|
||||
return null;
|
||||
}
|
||||
|
||||
var creator = new BlockListPropertyValueCreator(_blockConverter, _variationContextAccessor, _propertyRenderingContextAccessor, _blockEditorVarianceHandler, _jsonSerializer, _constructorCache, _languageService);
|
||||
var creator = new BlockListPropertyValueCreator(_blockConverter, _variationContextAccessor, _propertyRenderingContextAccessor, _blockEditorVarianceHandler, _elementCacheService, _jsonSerializer, _constructorCache, _languageService);
|
||||
return creator.CreateBlockModelAsync(owner, referenceCacheLevel, intermediateBlockModelValue, preview, configuration.Blocks).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -1,5 +1,6 @@
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
@@ -15,19 +16,22 @@ internal sealed class BlockListPropertyValueCreator : BlockPropertyValueCreatorB
|
||||
/// </summary>
|
||||
/// <param name="blockEditorConverter">The service used to convert block editor data into strongly typed objects.</param>
|
||||
/// <param name="variationContextAccessor">Provides access to the current variation context, used for handling content variations.</param>
|
||||
/// <param name="propertyRenderingContextAccessor">Provides access to the current rendering context.</param>
|
||||
/// <param name="blockEditorVarianceHandler">Handles variance logic for block editors, determining how values vary by culture or segment.</param>
|
||||
/// <param name="jsonSerializer">The serializer used for serializing and deserializing JSON data related to block list properties.</param>
|
||||
/// <param name="constructorCache">A cache that stores constructors for block list property values to improve performance.</param>
|
||||
/// <param name="languageService">Service used to retrieve language information for fallback resolution.</param>
|
||||
/// <param name="elementCacheService">The cache for elements.</param>
|
||||
public BlockListPropertyValueCreator(
|
||||
BlockEditorConverter blockEditorConverter,
|
||||
IVariationContextAccessor variationContextAccessor,
|
||||
IPropertyRenderingContextAccessor propertyRenderingContextAccessor,
|
||||
BlockEditorVarianceHandler blockEditorVarianceHandler,
|
||||
IElementCacheService elementCacheService,
|
||||
IJsonSerializer jsonSerializer,
|
||||
BlockListPropertyValueConstructorCache constructorCache,
|
||||
ILanguageService languageService)
|
||||
: base(blockEditorConverter, variationContextAccessor, propertyRenderingContextAccessor, blockEditorVarianceHandler, languageService)
|
||||
: base(blockEditorConverter, variationContextAccessor, propertyRenderingContextAccessor, blockEditorVarianceHandler, languageService, elementCacheService)
|
||||
{
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_constructorCache = constructorCache;
|
||||
|
||||
+39
-17
@@ -6,6 +6,7 @@ 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.Core.PublishedCache;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters;
|
||||
@@ -21,6 +22,7 @@ internal abstract class BlockPropertyValueCreatorBase<TBlockModel, TBlockItemMod
|
||||
private readonly IPropertyRenderingContextAccessor _propertyRenderingContextAccessor;
|
||||
private readonly BlockEditorVarianceHandler _blockEditorVarianceHandler;
|
||||
private readonly ILanguageService _languageService;
|
||||
private readonly IElementCacheService _elementCacheService;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a specific data converter for the block property implementation.
|
||||
@@ -64,13 +66,14 @@ internal abstract class BlockPropertyValueCreatorBase<TBlockModel, TBlockItemMod
|
||||
/// <returns></returns>
|
||||
protected delegate TBlockItemModel? EnrichBlockItemModelFromConfiguration(TBlockItemModel item, TBlockLayoutItem layoutItem, TBlockConfiguration configuration, CreateBlockItemModelFromLayout blockItemModelCreator);
|
||||
|
||||
protected BlockPropertyValueCreatorBase(BlockEditorConverter blockEditorConverter, IVariationContextAccessor variationContextAccessor, IPropertyRenderingContextAccessor propertyRenderingContextAccessor, BlockEditorVarianceHandler blockEditorVarianceHandler, ILanguageService languageService)
|
||||
protected BlockPropertyValueCreatorBase(BlockEditorConverter blockEditorConverter, IVariationContextAccessor variationContextAccessor, IPropertyRenderingContextAccessor propertyRenderingContextAccessor, BlockEditorVarianceHandler blockEditorVarianceHandler, ILanguageService languageService, IElementCacheService elementCacheService)
|
||||
{
|
||||
BlockEditorConverter = blockEditorConverter;
|
||||
_variationContextAccessor = variationContextAccessor;
|
||||
_propertyRenderingContextAccessor = propertyRenderingContextAccessor;
|
||||
_blockEditorVarianceHandler = blockEditorVarianceHandler;
|
||||
_languageService = languageService;
|
||||
_elementCacheService = elementCacheService;
|
||||
}
|
||||
|
||||
protected BlockEditorConverter BlockEditorConverter { get; }
|
||||
@@ -121,17 +124,14 @@ internal abstract class BlockPropertyValueCreatorBase<TBlockModel, TBlockItemMod
|
||||
CreateBlockModelFromItems createModelFromItems,
|
||||
EnrichBlockItemModelFromConfiguration? enrichBlockItem = null)
|
||||
{
|
||||
if (converted.BlockValue.ContentData.Count == 0)
|
||||
if (converted.Layout is null || converted.Layout.Any() is false)
|
||||
{
|
||||
return createEmptyModel();
|
||||
}
|
||||
|
||||
if (converted.Layout is null)
|
||||
{
|
||||
return createEmptyModel();
|
||||
}
|
||||
|
||||
var blockConfigMap = blockConfigurations.ToDictionary(bc => bc.ContentElementTypeKey);
|
||||
TBlockConfiguration[] blockConfigurationsAsArray = blockConfigurations as TBlockConfiguration[] ?? blockConfigurations.ToArray();
|
||||
var blockConfigMap = blockConfigurationsAsArray.ToDictionary(bc => bc.ContentElementTypeKey);
|
||||
var blockContentDataMap = converted.BlockValue.ContentData.ToDictionary(b => b.Key);
|
||||
VariationContext variationContext = _variationContextAccessor.VariationContext ?? new VariationContext();
|
||||
var languagesByIsoCode = (await _languageService.GetAllAsync())
|
||||
.ToDictionary(l => l.IsoCode, StringComparer.OrdinalIgnoreCase);
|
||||
@@ -139,15 +139,33 @@ internal abstract class BlockPropertyValueCreatorBase<TBlockModel, TBlockItemMod
|
||||
|
||||
// Convert the content data
|
||||
var contentPublishedElements = new Dictionary<Guid, IPublishedElement>();
|
||||
foreach (BlockItemData data in converted.BlockValue.ContentData)
|
||||
|
||||
// Get all layouts.
|
||||
// NOTE: While the Grid areas are modeled to contain areas within areas, in reality it cannot be configured as
|
||||
// such, so this "top-level aggregation" of layouts works in effect.
|
||||
IBlockLayoutItem[] allLayouts = converted
|
||||
.Layout
|
||||
.SelectMany(layout => new[] { layout }.Union(layout.GetContainedLayouts()))
|
||||
.ToArray();
|
||||
foreach (var layout in allLayouts)
|
||||
{
|
||||
if (!blockConfigMap.ContainsKey(data.ContentTypeKey))
|
||||
IPublishedElement? element = null;
|
||||
BlockItemData? data = null;
|
||||
if (layout.IsExternalContent)
|
||||
{
|
||||
continue;
|
||||
element = await _elementCacheService.GetByKeyAsync(layout.ContentKey, preview);
|
||||
|
||||
if (preview is false && element?.IsPublished(variationContext.Culture) is false)
|
||||
{
|
||||
element = null;
|
||||
}
|
||||
}
|
||||
else if (blockContentDataMap.TryGetValue(layout.ContentKey, out data))
|
||||
{
|
||||
element = BlockEditorConverter.ConvertToElement(owner, data, referenceCacheLevel, preview);
|
||||
}
|
||||
|
||||
IPublishedElement? element = BlockEditorConverter.ConvertToElement(owner, data, referenceCacheLevel, preview);
|
||||
if (element == null)
|
||||
if (element is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -162,15 +180,19 @@ internal abstract class BlockPropertyValueCreatorBase<TBlockModel, TBlockItemMod
|
||||
? variationContext.Segment.NullOrWhiteSpaceAsNull()
|
||||
: null;
|
||||
|
||||
Fallback fallback = _propertyRenderingContextAccessor.PropertyRenderingContext?.Fallback ?? default;
|
||||
if (BlockExposeFallbackHelper.IsBlockExposed(expose, element.Key, expectedBlockVariationCulture, expectedBlockVariationSegment, fallback, languagesByIsoCode, defaultIsoCode, out var resolvedCulture) is false)
|
||||
string? resolvedCulture = null;
|
||||
if (layout.IsExternalContent is false)
|
||||
{
|
||||
continue;
|
||||
Fallback fallback = _propertyRenderingContextAccessor.PropertyRenderingContext?.Fallback ?? default;
|
||||
if (BlockExposeFallbackHelper.IsBlockExposed(expose, element.Key, expectedBlockVariationCulture, expectedBlockVariationSegment, fallback, languagesByIsoCode, defaultIsoCode, out resolvedCulture) is false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// If the block was exposed via fallback to a different culture, recreate the element
|
||||
// with that culture's variation context so its property values come from the resolved culture.
|
||||
if (resolvedCulture is not null && resolvedCulture.InvariantEquals(expectedBlockVariationCulture) is false)
|
||||
if (resolvedCulture is not null && resolvedCulture.InvariantEquals(expectedBlockVariationCulture) is false && data is not null)
|
||||
{
|
||||
VariationContext? originalContext = _variationContextAccessor.VariationContext;
|
||||
try
|
||||
|
||||
+5
-1
@@ -3,6 +3,7 @@
|
||||
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
@@ -18,7 +19,9 @@ internal sealed class RichTextBlockPropertyValueCreator : BlockPropertyValueCrea
|
||||
/// </summary>
|
||||
/// <param name="blockEditorConverter">The <see cref="BlockEditorConverter"/> used to convert block editor values.</param>
|
||||
/// <param name="variationContextAccessor">The <see cref="IVariationContextAccessor"/> providing access to the variation context.</param>
|
||||
/// <param name="propertyRenderingContextAccessor">Provides access to the current rendering context.</param>
|
||||
/// <param name="blockEditorVarianceHandler">The <see cref="BlockEditorVarianceHandler"/> that handles block editor variance.</param>
|
||||
/// <param name="elementCacheService">The cache for elements.</param>
|
||||
/// <param name="jsonSerializer">The <see cref="IJsonSerializer"/> used for JSON serialization and deserialization.</param>
|
||||
/// <param name="constructorCache">The <see cref="RichTextBlockPropertyValueConstructorCache"/> used to cache rich text block property value constructors.</param>
|
||||
/// <param name="languageService">Service used to retrieve language information for fallback resolution.</param>
|
||||
@@ -27,10 +30,11 @@ internal sealed class RichTextBlockPropertyValueCreator : BlockPropertyValueCrea
|
||||
IVariationContextAccessor variationContextAccessor,
|
||||
IPropertyRenderingContextAccessor propertyRenderingContextAccessor,
|
||||
BlockEditorVarianceHandler blockEditorVarianceHandler,
|
||||
IElementCacheService elementCacheService,
|
||||
IJsonSerializer jsonSerializer,
|
||||
RichTextBlockPropertyValueConstructorCache constructorCache,
|
||||
ILanguageService languageService)
|
||||
: base(blockEditorConverter, variationContextAccessor, propertyRenderingContextAccessor, blockEditorVarianceHandler, languageService)
|
||||
: base(blockEditorConverter, variationContextAccessor, propertyRenderingContextAccessor, blockEditorVarianceHandler, languageService, elementCacheService)
|
||||
{
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_constructorCache = constructorCache;
|
||||
|
||||
+46
-2
@@ -15,6 +15,7 @@ using Umbraco.Cms.Core.Models.Blocks;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PropertyEditors.DeliveryApi;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Strings;
|
||||
@@ -46,6 +47,7 @@ public class RteBlockRenderingValueConverter : SimpleRichTextValueConverter, IDe
|
||||
private readonly BlockEditorVarianceHandler _blockEditorVarianceHandler;
|
||||
private readonly ILanguageService _languageService;
|
||||
private readonly IPropertyRenderingContextAccessor _propertyRenderingContextAccessor;
|
||||
private readonly IElementCacheService _elementCacheService;
|
||||
|
||||
private DeliveryApiSettings _deliveryApiSettings;
|
||||
private readonly IDisposable? _deliveryApiSettingsChangeSubscription;
|
||||
@@ -69,6 +71,7 @@ public class RteBlockRenderingValueConverter : SimpleRichTextValueConverter, IDe
|
||||
/// <param name="deliveryApiSettingsMonitor">Monitors settings for the Delivery API.</param>
|
||||
/// <param name="languageService">Service used to retrieve language information for fallback resolution.</param>
|
||||
/// <param name="propertyRenderingContextAccessor">Accessor for the current property rendering context.</param>
|
||||
/// <param name="elementCacheService">The cache for elements.</param>
|
||||
public RteBlockRenderingValueConverter(
|
||||
HtmlLocalLinkParser linkParser,
|
||||
HtmlUrlParser urlParser,
|
||||
@@ -85,7 +88,8 @@ public class RteBlockRenderingValueConverter : SimpleRichTextValueConverter, IDe
|
||||
BlockEditorVarianceHandler blockEditorVarianceHandler,
|
||||
IOptionsMonitor<DeliveryApiSettings> deliveryApiSettingsMonitor,
|
||||
ILanguageService languageService,
|
||||
IPropertyRenderingContextAccessor propertyRenderingContextAccessor)
|
||||
IPropertyRenderingContextAccessor propertyRenderingContextAccessor,
|
||||
IElementCacheService elementCacheService)
|
||||
{
|
||||
_linkParser = linkParser;
|
||||
_urlParser = urlParser;
|
||||
@@ -102,6 +106,7 @@ public class RteBlockRenderingValueConverter : SimpleRichTextValueConverter, IDe
|
||||
_blockEditorVarianceHandler = blockEditorVarianceHandler;
|
||||
_languageService = languageService;
|
||||
_propertyRenderingContextAccessor = propertyRenderingContextAccessor;
|
||||
_elementCacheService = elementCacheService;
|
||||
|
||||
_deliveryApiSettings = deliveryApiSettingsMonitor.CurrentValue;
|
||||
_deliveryApiSettingsChangeSubscription = deliveryApiSettingsMonitor.OnChange(settings => _deliveryApiSettings = settings);
|
||||
@@ -128,6 +133,45 @@ public class RteBlockRenderingValueConverter : SimpleRichTextValueConverter, IDe
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the non-obsolete constructor. Scheduled for removal in V20.")]
|
||||
public RteBlockRenderingValueConverter(
|
||||
HtmlLocalLinkParser linkParser,
|
||||
HtmlUrlParser urlParser,
|
||||
HtmlImageSourceParser imageSourceParser,
|
||||
IApiRichTextElementParser apiRichTextElementParser,
|
||||
IApiRichTextMarkupParser apiRichTextMarkupParser,
|
||||
IPartialViewBlockEngine partialViewBlockEngine,
|
||||
BlockEditorConverter blockEditorConverter,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IApiElementBuilder apiElementBuilder,
|
||||
RichTextBlockPropertyValueConstructorCache constructorCache,
|
||||
ILogger<RteBlockRenderingValueConverter> logger,
|
||||
IVariationContextAccessor variationContextAccessor,
|
||||
BlockEditorVarianceHandler blockEditorVarianceHandler,
|
||||
IOptionsMonitor<DeliveryApiSettings> deliveryApiSettingsMonitor,
|
||||
ILanguageService languageService,
|
||||
IPropertyRenderingContextAccessor propertyRenderingContextAccessor)
|
||||
: this(
|
||||
linkParser,
|
||||
urlParser,
|
||||
imageSourceParser,
|
||||
apiRichTextElementParser,
|
||||
apiRichTextMarkupParser,
|
||||
partialViewBlockEngine,
|
||||
blockEditorConverter,
|
||||
jsonSerializer,
|
||||
apiElementBuilder,
|
||||
constructorCache,
|
||||
logger,
|
||||
variationContextAccessor,
|
||||
blockEditorVarianceHandler,
|
||||
deliveryApiSettingsMonitor,
|
||||
languageService,
|
||||
propertyRenderingContextAccessor,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IElementCacheService>())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the cache level for the property.
|
||||
/// </summary>
|
||||
@@ -328,7 +372,7 @@ public class RteBlockRenderingValueConverter : SimpleRichTextValueConverter, IDe
|
||||
return null;
|
||||
}
|
||||
|
||||
var creator = new RichTextBlockPropertyValueCreator(_blockEditorConverter, _variationContextAccessor, _propertyRenderingContextAccessor, _blockEditorVarianceHandler, _jsonSerializer, _constructorCache, _languageService);
|
||||
var creator = new RichTextBlockPropertyValueCreator(_blockEditorConverter, _variationContextAccessor, _propertyRenderingContextAccessor, _blockEditorVarianceHandler, _elementCacheService, _jsonSerializer, _constructorCache, _languageService);
|
||||
return creator.CreateBlockModelAsync(owner, referenceCacheLevel, blocks, preview, configuration.Blocks).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
|
||||
+23
-3
@@ -12,6 +12,7 @@ using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
using Umbraco.Cms.Core.PropertyEditors.DeliveryApi;
|
||||
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.Extensions;
|
||||
@@ -36,6 +37,7 @@ public class SingleBlockPropertyValueConverter : PropertyValueConverterBase, IDe
|
||||
private readonly BlockEditorVarianceHandler _blockEditorVarianceHandler;
|
||||
private readonly ILanguageService _languageService;
|
||||
private readonly IPropertyRenderingContextAccessor _propertyRenderingContextAccessor;
|
||||
private readonly IElementCacheService _elementCacheService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SingleBlockPropertyValueConverter"/> class.
|
||||
@@ -49,6 +51,7 @@ public class SingleBlockPropertyValueConverter : PropertyValueConverterBase, IDe
|
||||
/// <param name="blockEditorVarianceHandler">Handles variance logic for block editors.</param>
|
||||
/// <param name="languageService">Service used to retrieve language information for fallback resolution.</param>
|
||||
/// <param name="propertyRenderingContextAccessor">Accessor for the current property rendering context.</param>
|
||||
/// <param name="elementCacheService">The cache for elements.</param>
|
||||
public SingleBlockPropertyValueConverter(
|
||||
IProfilingLogger proflog,
|
||||
BlockEditorConverter blockConverter,
|
||||
@@ -58,7 +61,8 @@ public class SingleBlockPropertyValueConverter : PropertyValueConverterBase, IDe
|
||||
IVariationContextAccessor variationContextAccessor,
|
||||
BlockEditorVarianceHandler blockEditorVarianceHandler,
|
||||
ILanguageService languageService,
|
||||
IPropertyRenderingContextAccessor propertyRenderingContextAccessor)
|
||||
IPropertyRenderingContextAccessor propertyRenderingContextAccessor,
|
||||
IElementCacheService elementCacheService)
|
||||
{
|
||||
_proflog = proflog;
|
||||
_blockConverter = blockConverter;
|
||||
@@ -69,6 +73,7 @@ public class SingleBlockPropertyValueConverter : PropertyValueConverterBase, IDe
|
||||
_blockEditorVarianceHandler = blockEditorVarianceHandler;
|
||||
_languageService = languageService;
|
||||
_propertyRenderingContextAccessor = propertyRenderingContextAccessor;
|
||||
_elementCacheService = elementCacheService;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SingleBlockPropertyValueConverter(IProfilingLogger, BlockEditorConverter, IApiElementBuilder, IJsonSerializer, BlockListPropertyValueConstructorCache, IVariationContextAccessor, BlockEditorVarianceHandler, ILanguageService, IPropertyRenderingContextAccessor)"/>
|
||||
@@ -85,6 +90,21 @@ public class SingleBlockPropertyValueConverter : PropertyValueConverterBase, IDe
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the non-obsolete constructor. Scheduled for removal in V20.")]
|
||||
public SingleBlockPropertyValueConverter(
|
||||
IProfilingLogger proflog,
|
||||
BlockEditorConverter blockConverter,
|
||||
IApiElementBuilder apiElementBuilder,
|
||||
IJsonSerializer jsonSerializer,
|
||||
BlockListPropertyValueConstructorCache constructorCache,
|
||||
IVariationContextAccessor variationContextAccessor,
|
||||
BlockEditorVarianceHandler blockEditorVarianceHandler,
|
||||
ILanguageService languageService,
|
||||
IPropertyRenderingContextAccessor propertyRenderingContextAccessor)
|
||||
: this(proflog, blockConverter, apiElementBuilder, jsonSerializer, constructorCache, variationContextAccessor, blockEditorVarianceHandler, languageService, propertyRenderingContextAccessor, StaticServiceProvider.Instance.GetRequiredService<IElementCacheService>())
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool IsConverter(IPublishedPropertyType propertyType)
|
||||
=> propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.SingleBlock);
|
||||
@@ -94,7 +114,7 @@ public class SingleBlockPropertyValueConverter : PropertyValueConverterBase, IDe
|
||||
|
||||
/// <inheritdoc />
|
||||
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
|
||||
=> PropertyCacheLevel.Element;
|
||||
=> PropertyCacheLevel.Elements;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview)
|
||||
@@ -149,7 +169,7 @@ public class SingleBlockPropertyValueConverter : PropertyValueConverterBase, IDe
|
||||
}
|
||||
|
||||
|
||||
var creator = new SingleBlockPropertyValueCreator(_blockConverter, _variationContextAccessor, _propertyRenderingContextAccessor, _blockEditorVarianceHandler, _jsonSerializer, _constructorCache, _languageService);
|
||||
var creator = new SingleBlockPropertyValueCreator(_blockConverter, _variationContextAccessor, _propertyRenderingContextAccessor, _blockEditorVarianceHandler, _elementCacheService, _jsonSerializer, _constructorCache, _languageService);
|
||||
return creator.CreateBlockModelAsync(owner, referenceCacheLevel, intermediateBlockModelValue, preview, configuration.Blocks).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -1,5 +1,6 @@
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
@@ -16,7 +17,9 @@ internal sealed class SingleBlockPropertyValueCreator : BlockPropertyValueCreato
|
||||
/// </summary>
|
||||
/// <param name="blockEditorConverter">The service used to convert block editor data.</param>
|
||||
/// <param name="variationContextAccessor">Provides access to the current variation context.</param>
|
||||
/// <param name="propertyRenderingContextAccessor">Provides access to the current rendering context.</param>
|
||||
/// <param name="blockEditorVarianceHandler">Handles variance logic for block editors.</param>
|
||||
/// <param name="elementCacheService">The cache for elements.</param>
|
||||
/// <param name="jsonSerializer">The serializer used for JSON serialization and deserialization.</param>
|
||||
/// <param name="constructorCache">A cache for constructors used in block list property value creation.</param>
|
||||
/// <param name="languageService">Service used to retrieve language information for fallback resolution.</param>
|
||||
@@ -25,10 +28,11 @@ internal sealed class SingleBlockPropertyValueCreator : BlockPropertyValueCreato
|
||||
IVariationContextAccessor variationContextAccessor,
|
||||
IPropertyRenderingContextAccessor propertyRenderingContextAccessor,
|
||||
BlockEditorVarianceHandler blockEditorVarianceHandler,
|
||||
IElementCacheService elementCacheService,
|
||||
IJsonSerializer jsonSerializer,
|
||||
BlockListPropertyValueConstructorCache constructorCache,
|
||||
ILanguageService languageService)
|
||||
: base(blockEditorConverter, variationContextAccessor, propertyRenderingContextAccessor, blockEditorVarianceHandler, languageService)
|
||||
: base(blockEditorConverter, variationContextAccessor, propertyRenderingContextAccessor, blockEditorVarianceHandler, languageService, elementCacheService)
|
||||
{
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_constructorCache = constructorCache;
|
||||
|
||||
@@ -1,45 +1,84 @@
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.HybridCache.Factories;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache.Services;
|
||||
|
||||
/// <inheritdoc/>
|
||||
internal class BlockElementService : IBlockElementService
|
||||
{
|
||||
private readonly IPublishedContentTypeCache _publishedContentTypeCache;
|
||||
private readonly IPublishedContentFactory _publishedContentFactory;
|
||||
private readonly IPublishedModelFactory _publishedModelFactory;
|
||||
private readonly ILanguageService _languageService;
|
||||
|
||||
public BlockElementService(
|
||||
IPublishedContentTypeCache publishedContentTypeCache,
|
||||
IPublishedContentFactory publishedContentFactory,
|
||||
IPublishedModelFactory publishedModelFactory)
|
||||
IPublishedModelFactory publishedModelFactory,
|
||||
ILanguageService languageService)
|
||||
{
|
||||
_publishedContentTypeCache = publishedContentTypeCache;
|
||||
_publishedContentFactory = publishedContentFactory;
|
||||
_publishedModelFactory = publishedModelFactory;
|
||||
_languageService = languageService;
|
||||
}
|
||||
|
||||
public Task<IPublishedElement?> BuildElementAsync(BlockItemData blockItemData, bool? preview = null)
|
||||
/// <inheritdoc/>
|
||||
public async Task<IPublishedElement?> BuildElementAsync(IPublishedElement owner, BlockItemData blockItemData, bool? preview = null)
|
||||
{
|
||||
ILanguage[]? allLanguages = null;
|
||||
ILanguage? defaultLanguage = null;
|
||||
|
||||
// Only convert element types - content types will cause an exception when PublishedModelFactory creates the model
|
||||
IPublishedContentType? publishedContentType = _publishedContentTypeCache.Get(PublishedItemType.Element, blockItemData.ContentTypeKey);
|
||||
if (publishedContentType is null || publishedContentType.IsElement is false)
|
||||
{
|
||||
return Task.FromResult<IPublishedElement?>(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
var propertyData = new Dictionary<string, PropertyData[]>();
|
||||
foreach (IGrouping<string, BlockPropertyValue> properties in blockItemData.Values.GroupBy(value => value.Alias))
|
||||
{
|
||||
propertyData[properties.Key] = properties.Select(property => new PropertyData
|
||||
IPublishedPropertyType? propertyType = publishedContentType.GetPropertyType(properties.Key);
|
||||
if (propertyType is null)
|
||||
{
|
||||
Culture = property.Culture ?? string.Empty, // throws an error if there is no value
|
||||
Segment = property.Segment ?? string.Empty, // throws an error if there is no value
|
||||
Value = property.Value,
|
||||
}).ToArray();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (propertyType.VariesByCulture() && owner.ContentType.VariesByCulture() is false)
|
||||
{
|
||||
// Special case:
|
||||
// The element property type varies by culture, but the owner element (e.g. the page) content type does not
|
||||
// vary by culture. Since the created element is fully culture aware at render time, we need to replicate
|
||||
// property values across all available languages, to make them available for rendering.
|
||||
|
||||
allLanguages ??= (await _languageService.GetAllAsync()).ToArray();
|
||||
defaultLanguage ??= allLanguages.SingleOrDefault(l => l.IsDefault)
|
||||
?? throw new InvalidOperationException("Could not find the default language.");
|
||||
|
||||
BlockPropertyValue property = properties.FirstOrDefault(p => p.Culture.InvariantEquals(defaultLanguage.IsoCode))
|
||||
?? properties.First();
|
||||
propertyData[properties.Key] = allLanguages.Select(language => new PropertyData
|
||||
{
|
||||
Culture = language.IsoCode,
|
||||
Segment = property.Segment ?? string.Empty, // throws an error if there is no value
|
||||
Value = property.Value,
|
||||
}).ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
propertyData[properties.Key] = properties.Select(property => new PropertyData
|
||||
{
|
||||
Culture = property.Culture ?? string.Empty, // throws an error if there is no value
|
||||
Segment = property.Segment ?? string.Empty, // throws an error if there is no value
|
||||
Value = property.Value,
|
||||
}).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
var published = preview is not true;
|
||||
@@ -47,9 +86,15 @@ internal class BlockElementService : IBlockElementService
|
||||
|
||||
const string name = "n/a";
|
||||
|
||||
var cultureInfos = (publishedContentType.VariesByCulture()
|
||||
? blockItemData.Values.Select(value => value.Culture).WhereNotNull().Distinct()
|
||||
: []).ToDictionary(
|
||||
IEnumerable<string> cultures = publishedContentType.VariesByCulture()
|
||||
? propertyData
|
||||
.SelectMany(p => p.Value.Select(v => v.Culture))
|
||||
.Where(c => c.IsNullOrWhiteSpace() is false)
|
||||
.OfType<string>()
|
||||
.Distinct()
|
||||
: [];
|
||||
|
||||
var cultureInfos = cultures.ToDictionary(
|
||||
culture => culture,
|
||||
_ => new CultureVariation
|
||||
{
|
||||
@@ -83,6 +128,6 @@ internal class BlockElementService : IBlockElementService
|
||||
};
|
||||
|
||||
var result = _publishedContentFactory.ToIPublishedElement(contentCacheNode, draft);
|
||||
return Task.FromResult(result.CreateModel(_publishedModelFactory));
|
||||
return result.CreateModel(_publishedModelFactory);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -670,7 +670,7 @@ export const data: Array<UmbMockDataTypeModel> = [
|
||||
alias: 'blocks',
|
||||
value: [
|
||||
{
|
||||
label: 'Mocked Block Type for Block List',
|
||||
label: 'Mocked Block Type for Block List: ${elementProperty}',
|
||||
contentElementTypeKey: '4f68ba66-6fb2-4778-83b8-6ab4ca3a7c5c',
|
||||
settingsElementTypeKey: 'all-property-editors-document-type-id',
|
||||
iconColor: '#F5C1BC',
|
||||
|
||||
@@ -1062,13 +1062,21 @@ export const data: Array<UmbMockDocumentModel> = [
|
||||
layout: {
|
||||
'Umbraco.BlockList': [
|
||||
{
|
||||
key: '1234',
|
||||
contentKey: '1234',
|
||||
settingsKey: '5678',
|
||||
},
|
||||
{
|
||||
key: '1234-headline',
|
||||
contentKey: '1234-headline',
|
||||
settingsKey: '1234-headline-settings',
|
||||
},
|
||||
{
|
||||
key: '45110b54-764e-4198-858d-6bf8f51589b6',
|
||||
contentKey: 'simple-element-id',
|
||||
settingsKey: null,
|
||||
isExternalContent: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
contentData: [
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
const { http, HttpResponse } = window.MockServiceWorker;
|
||||
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
|
||||
|
||||
export const blockReferenceHandlers = [
|
||||
// GET /document/:id/referenced-elements-with-pending-changes
|
||||
// Returns library elements referenced by a document that have unpublished draft changes.
|
||||
http.get(umbracoPath('/document/:id/referenced-elements-with-pending-changes'), () => {
|
||||
return HttpResponse.json({
|
||||
total: 2,
|
||||
items: [
|
||||
{
|
||||
id: 'simple-element-id',
|
||||
name: 'Simple Element',
|
||||
documentType: { id: '4f68ba66-6fb2-4778-83b8-6ab4ca3a7c5c', icon: 'icon-lab' },
|
||||
state: 'PublishedPendingChanges',
|
||||
publishDate: '2024-02-01T10:00:00.000Z',
|
||||
scheduledPublishDate: null,
|
||||
},
|
||||
{
|
||||
id: 'element-in-folder-id',
|
||||
name: 'Element In Folder',
|
||||
documentType: { id: '4f68ba66-6fb2-4778-83b8-6ab4ca3a7c5c', icon: 'icon-lab' },
|
||||
state: 'PublishedPendingChanges',
|
||||
publishDate: '2024-01-17T08:00:00.000Z',
|
||||
scheduledPublishDate: '2026-05-01T00:00:00.000Z',
|
||||
},
|
||||
],
|
||||
});
|
||||
}),
|
||||
];
|
||||
@@ -5,6 +5,7 @@ import { publishingHandlers } from './publishing.handlers.js';
|
||||
import { detailHandlers } from './detail.handlers.js';
|
||||
import { folderHandlers } from './folder.handlers.js';
|
||||
import { moveCopyHandlers } from './move-copy.handlers.js';
|
||||
import { blockReferenceHandlers } from './block-reference.handlers.js';
|
||||
|
||||
export const handlers = [
|
||||
...recycleBinHandlers,
|
||||
@@ -14,4 +15,5 @@ export const handlers = [
|
||||
...detailHandlers,
|
||||
...folderHandlers,
|
||||
...moveCopyHandlers,
|
||||
...blockReferenceHandlers,
|
||||
];
|
||||
|
||||
@@ -9,7 +9,12 @@ export const treeHandlers = [
|
||||
const url = new URL(request.url);
|
||||
const skip = Number(url.searchParams.get('skip'));
|
||||
const take = Number(url.searchParams.get('take'));
|
||||
const foldersOnly = url.searchParams.get('foldersOnly') === 'true';
|
||||
const response = umbElementMockDb.tree.getRoot({ skip, take });
|
||||
if (foldersOnly) {
|
||||
response.items = response.items.filter((item: any) => item.isFolder);
|
||||
response.total = response.items.length;
|
||||
}
|
||||
return HttpResponse.json(response);
|
||||
}),
|
||||
|
||||
@@ -19,7 +24,12 @@ export const treeHandlers = [
|
||||
if (!parentId) return;
|
||||
const skip = Number(url.searchParams.get('skip'));
|
||||
const take = Number(url.searchParams.get('take'));
|
||||
const foldersOnly = url.searchParams.get('foldersOnly') === 'true';
|
||||
const response = umbElementMockDb.tree.getChildrenOf({ parentId, skip, take });
|
||||
if (foldersOnly) {
|
||||
response.items = response.items.filter((item: any) => item.isFolder);
|
||||
response.total = response.items.length;
|
||||
}
|
||||
return HttpResponse.json(response);
|
||||
}),
|
||||
|
||||
|
||||
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@umbraco-cms/backoffice",
|
||||
"version": "18.1.0-rc",
|
||||
"version": "19.0.0-beta1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@umbraco-cms/backoffice",
|
||||
"version": "18.1.0-rc",
|
||||
"version": "19.0.0-beta1",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"./src/libs/*",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@umbraco-cms/backoffice",
|
||||
"license": "MIT",
|
||||
"version": "18.1.0-rc",
|
||||
"version": "19.0.0-beta1",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": null,
|
||||
|
||||
@@ -2936,6 +2936,12 @@ export default {
|
||||
unsupportedBlockName: 'Unsupported',
|
||||
unsupportedBlockDescription:
|
||||
'This content is no longer supported in this Editor. If you are missing this content, please contact your administrator. Otherwise delete it.',
|
||||
tabLibrary: 'Library',
|
||||
transferToElementLibrary: 'Transfer to Library',
|
||||
disconnectFromElementLibrary: 'Disconnect from Library',
|
||||
disconnectFromElementLibraryConfirm:
|
||||
'This will create a local copy of the Element content. The Library Element will not be affected.',
|
||||
elementUsedByCount: (count: number) => `This Element is referenced by ${count} item(s).`,
|
||||
blockVariantConfigurationNotSupported:
|
||||
'One or more Block Types of this Block Editor is using a Element-Type that is configured to Vary By Culture or Vary By Segment. This is not supported on a Content item that does not vary by Culture or Segment.',
|
||||
},
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
--umb-section-sidebar-width: 300px;
|
||||
--umb-card-medium-min-width: 160px;
|
||||
--umb-card-large-min-width: 250px;
|
||||
--umb-color-reference: #7532c8;
|
||||
--umb-color-reference-contrast: var(--uui-color-surface, #fff);
|
||||
}
|
||||
|
||||
@font-face {
|
||||
|
||||
+4
-4
@@ -154,14 +154,14 @@ export class UmbBlockGridManagerContext<
|
||||
while (i--) {
|
||||
const currentEntry = entries[i];
|
||||
// Lets check if we found the right parent layout entry:
|
||||
if (currentEntry.contentKey === parentId) {
|
||||
if (currentEntry.key === parentId) {
|
||||
// Append the layout entry to be inserted and unfreeze the rest of the data:
|
||||
const areas =
|
||||
currentEntry.areas?.map((x) =>
|
||||
x.key === areaKey
|
||||
? {
|
||||
...x,
|
||||
items: pushAtToUniqueArray([...x.items], insert, (x) => x.contentKey === insert.contentKey, index),
|
||||
items: pushAtToUniqueArray([...x.items], insert, (x) => x.key === insert.key, index),
|
||||
}
|
||||
: x,
|
||||
) ?? [];
|
||||
@@ -171,7 +171,7 @@ export class UmbBlockGridManagerContext<
|
||||
...currentEntry,
|
||||
areas,
|
||||
},
|
||||
(x) => x.contentKey === currentEntry.contentKey,
|
||||
(x) => x.key === currentEntry.key,
|
||||
);
|
||||
}
|
||||
// Otherwise check if any items of the areas are the parent layout entry we are looking for. We do so based on parentId, recursively:
|
||||
@@ -199,7 +199,7 @@ export class UmbBlockGridManagerContext<
|
||||
(z) => z.key === area.key,
|
||||
),
|
||||
},
|
||||
(x) => x.contentKey === currentEntry.contentKey,
|
||||
(x) => x.key === currentEntry.key,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -32,6 +32,7 @@ describe('UmbBlockListToBlockClipboardCopyPropertyValueTranslator', () => {
|
||||
layout: {
|
||||
[UMB_BLOCK_GRID_PROPERTY_EDITOR_SCHEMA_ALIAS]: [
|
||||
{
|
||||
key: 'contentKey',
|
||||
columnSpan: 12,
|
||||
rowSpan: 1,
|
||||
areas: [],
|
||||
@@ -54,6 +55,7 @@ describe('UmbBlockListToBlockClipboardCopyPropertyValueTranslator', () => {
|
||||
contentData: blockGridPropertyValue.contentData,
|
||||
layout: [
|
||||
{
|
||||
key: 'contentKey',
|
||||
contentKey: 'contentKey',
|
||||
settingsKey: null,
|
||||
},
|
||||
|
||||
+1
@@ -62,6 +62,7 @@ export class UmbBlockGridToBlockClipboardCopyPropertyValueTranslator
|
||||
}
|
||||
|
||||
return {
|
||||
key: gridLayout.key,
|
||||
contentKey: gridLayout.contentKey,
|
||||
settingsKey: gridLayout.settingsKey,
|
||||
};
|
||||
|
||||
+2
@@ -36,6 +36,7 @@ describe('UmbBlockToBlockGridClipboardPastePropertyValueTranslator', () => {
|
||||
columnSpan: 12,
|
||||
rowSpan: 1,
|
||||
areas: [],
|
||||
key: 'contentKey',
|
||||
contentKey: 'contentKey',
|
||||
settingsKey: null,
|
||||
},
|
||||
@@ -49,6 +50,7 @@ describe('UmbBlockToBlockGridClipboardPastePropertyValueTranslator', () => {
|
||||
contentData: blockGridPropertyValue.contentData,
|
||||
layout: [
|
||||
{
|
||||
key: 'contentKey',
|
||||
contentKey: 'contentKey',
|
||||
settingsKey: null,
|
||||
},
|
||||
|
||||
+1
@@ -40,6 +40,7 @@ describe('UmbBlockListToBlockClipboardCopyPropertyValueTranslator', () => {
|
||||
columnSpan: 12,
|
||||
rowSpan: 1,
|
||||
areas: [],
|
||||
key: 'contentKey',
|
||||
contentKey: 'contentKey',
|
||||
settingsKey: null,
|
||||
},
|
||||
|
||||
+1
@@ -34,6 +34,7 @@ describe('UmbGridBlockToBlockGridClipboardPastePropertyValueTranslator', () => {
|
||||
columnSpan: 12,
|
||||
rowSpan: 1,
|
||||
areas: [],
|
||||
key: 'contentKey',
|
||||
contentKey: 'contentKey',
|
||||
settingsKey: null,
|
||||
},
|
||||
|
||||
+12
-8
@@ -183,6 +183,7 @@ export class UmbBlockGridEntriesContext
|
||||
const valueResolver = new UmbClipboardPastePropertyValueTranslatorValueResolver(this);
|
||||
|
||||
const blockTypes = this.#allowedBlockTypes.getValue();
|
||||
const libraryAllowedElementTypeKeys = await this._getLibraryAllowedElementTypeKeys(blockTypes);
|
||||
|
||||
const configuredSize = this._manager
|
||||
.getEditorConfiguration()
|
||||
@@ -200,6 +201,7 @@ export class UmbBlockGridEntriesContext
|
||||
blocks: blockTypes,
|
||||
blockGroups: this._manager.getBlockGroups() ?? [],
|
||||
openClipboard: routingInfo.view === 'clipboard',
|
||||
libraryAllowedElementTypeKeys,
|
||||
clipboardFilter: async (clipboardEntryDetail) => {
|
||||
const hasSupportedPasteTranslator = clipboardContext.hasSupportedPasteTranslator(
|
||||
pasteTranslatorManifests,
|
||||
@@ -237,7 +239,7 @@ export class UmbBlockGridEntriesContext
|
||||
};
|
||||
})
|
||||
.onSubmit(async (value, data) => {
|
||||
if (value?.create && data) {
|
||||
if (value && 'create' in value && data) {
|
||||
const created = await this.create(
|
||||
value.create.contentElementTypeKey,
|
||||
// We can parse an empty object, cause the rest will be filled in by others.
|
||||
@@ -254,7 +256,9 @@ export class UmbBlockGridEntriesContext
|
||||
} else {
|
||||
throw new Error('Failed to create block');
|
||||
}
|
||||
} else if (value?.clipboard && value.clipboard.selection?.length && data) {
|
||||
} else if (value && 'library' in value) {
|
||||
this._manager?.insertExternalContent(value.library.elementKey);
|
||||
} else if (value && 'clipboard' in value && value.clipboard.selection?.length && data) {
|
||||
const clipboardContext = await this.getContext(UMB_CLIPBOARD_PROPERTY_CONTEXT);
|
||||
if (!clipboardContext) {
|
||||
throw new Error('Clipboard context not available');
|
||||
@@ -498,23 +502,23 @@ export class UmbBlockGridEntriesContext
|
||||
}
|
||||
|
||||
// create Block?
|
||||
override async delete(contentKey: string) {
|
||||
override async delete(key: string) {
|
||||
// TODO: Loop through children and delete them as well?
|
||||
// Find layout entry:
|
||||
const layout = this._layoutEntries.getValue().find((x) => x.contentKey === contentKey);
|
||||
const layout = this._layoutEntries.getValue().find((x) => x.key === key);
|
||||
if (!layout) {
|
||||
throw new Error(`Cannot delete block, missing layout for ${contentKey}`);
|
||||
throw new Error(`Cannot delete block, missing layout for ${key}`);
|
||||
}
|
||||
// The following loop will only delete the referenced data of sub Layout Entries, as the Layout entry is part of the main Layout Entry they will go away when that is removed. [NL]
|
||||
forEachBlockLayoutEntryOf(layout, async (entry) => {
|
||||
if (entry.settingsKey) {
|
||||
this._manager!.removeOneSettings(entry.settingsKey);
|
||||
}
|
||||
this._manager!.removeOneContent(contentKey);
|
||||
this._manager!.removeExposesOf(contentKey);
|
||||
this._manager!.removeOneContent(entry.contentKey);
|
||||
this._manager!.removeExposesOf(entry.contentKey);
|
||||
});
|
||||
|
||||
await super.delete(contentKey);
|
||||
await super.delete(key);
|
||||
}
|
||||
|
||||
protected async _insertFromPropertyValue(value: UmbBlockGridValueModel, originData: UmbBlockGridWorkspaceOriginData) {
|
||||
|
||||
+3
-4
@@ -112,10 +112,10 @@ function resolvePlacementAsBlockGrid(
|
||||
|
||||
const SORTER_CONFIG: UmbSorterConfig<UmbBlockGridLayoutModel, UmbBlockGridEntryElement> = {
|
||||
getUniqueOfElement: (element) => {
|
||||
return element.contentKey!;
|
||||
return element.key!;
|
||||
},
|
||||
getUniqueOfModel: (modelEntry) => {
|
||||
return modelEntry.contentKey;
|
||||
return modelEntry.key;
|
||||
},
|
||||
resolvePlacement: resolvePlacementAsBlockGrid,
|
||||
identifier: 'block-grid-editor',
|
||||
@@ -400,12 +400,11 @@ export class UmbBlockGridEntriesElement extends UmbFormControlMixin(UmbLitElemen
|
||||
<div class="umb-block-grid__layout-container" data-area-length=${this._layoutEntries.length}>
|
||||
${repeat(
|
||||
this._layoutEntries,
|
||||
(layout) => layout.contentKey,
|
||||
(layout) => layout.key,
|
||||
(layout, index) =>
|
||||
html`<umb-block-grid-entry
|
||||
class="umb-block-grid__layout-item"
|
||||
index=${index}
|
||||
.contentKey=${layout.contentKey}
|
||||
.layout=${layout}>
|
||||
</umb-block-grid-entry>
|
||||
`,
|
||||
|
||||
+109
-12
@@ -2,9 +2,10 @@ import type { UmbBlockGridLayoutModel } from '../../types.js';
|
||||
import { UMB_BLOCK_GRID } from '../../constants.js';
|
||||
import { UmbBlockGridEntryContext } from './block-grid-entry.context.js';
|
||||
import { css, customElement, html, nothing, property, state, when } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { stringOrStringArrayContains } from '@umbraco-cms/backoffice/utils';
|
||||
import { stringOrStringArrayContains, UmbDeprecation } from '@umbraco-cms/backoffice/utils';
|
||||
import { UmbDataPathBlockElementDataQuery } from '@umbraco-cms/backoffice/block';
|
||||
import { umbDestroyOnDisconnect, UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { UmbElementVariantState } from '@umbraco-cms/backoffice/element';
|
||||
import { UmbObserveValidationStateController } from '@umbraco-cms/backoffice/validation';
|
||||
import { UUIBlinkAnimationValue, UUIBlinkKeyframes } from '@umbraco-cms/backoffice/external/uui';
|
||||
import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit';
|
||||
@@ -30,15 +31,61 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
|
||||
this.#context.setIndex(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the layout entry for this block.
|
||||
*/
|
||||
public set layout(value: UmbBlockGridLayoutModel | undefined) {
|
||||
if (!value) return;
|
||||
const key = value.key;
|
||||
const contentKey = value.contentKey;
|
||||
|
||||
if (key && key !== this._key) {
|
||||
this._key = key;
|
||||
this.#context.setKey(key);
|
||||
}
|
||||
|
||||
if (contentKey && contentKey !== this._contentKey) {
|
||||
this._contentKey = contentKey;
|
||||
this._blockViewProps.contentKey = contentKey;
|
||||
this.setAttribute('data-element-key', contentKey);
|
||||
|
||||
new UmbObserveValidationStateController(
|
||||
this,
|
||||
`$.contentData[${UmbDataPathBlockElementDataQuery({ key: contentKey })}]`,
|
||||
(hasMessages) => {
|
||||
this._contentInvalid = hasMessages;
|
||||
this._blockViewProps.contentInvalid = hasMessages;
|
||||
},
|
||||
'observeMessagesForContent',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public get key(): string | undefined {
|
||||
return this._key;
|
||||
}
|
||||
private _key?: string | undefined;
|
||||
|
||||
/**
|
||||
* @deprecated Use the `layout` property instead. Will be removed in Umbraco 20.
|
||||
*/
|
||||
@property({ attribute: false })
|
||||
public get contentKey(): string | undefined {
|
||||
return this._contentKey;
|
||||
}
|
||||
public set contentKey(key: string | undefined) {
|
||||
if (!key || key === this._contentKey) return;
|
||||
new UmbDeprecation({
|
||||
deprecated: 'umb-block-grid-entry.contentKey property',
|
||||
solution: 'Use the `layout` property instead.',
|
||||
removeInVersion: '20.0.0',
|
||||
}).warn();
|
||||
this._contentKey = key;
|
||||
this._blockViewProps.contentKey = key;
|
||||
this.setAttribute('data-element-key', key);
|
||||
if (!this._key) {
|
||||
this.#context.setKey(key);
|
||||
}
|
||||
this.#context.setContentKey(key);
|
||||
|
||||
new UmbObserveValidationStateController(
|
||||
@@ -88,6 +135,8 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
|
||||
@state()
|
||||
private _exposed?: boolean;
|
||||
|
||||
private _localExpose?: boolean;
|
||||
|
||||
// Unsupported is triggered if the Block Type is not recognized, it can also be triggered by the Content Element Type not existing any longer. [NL]
|
||||
@state()
|
||||
private _unsupported?: boolean;
|
||||
@@ -134,10 +183,11 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
|
||||
config: { showContentEdit: false, showSettingsEdit: false },
|
||||
}; // Set to undefined cause it will be set before we render.
|
||||
|
||||
#updateBlockViewProps(incoming: Partial<UmbBlockEditorCustomViewProperties<UmbBlockGridLayoutModel>>) {
|
||||
this._blockViewProps = { ...this._blockViewProps, ...incoming };
|
||||
this.requestUpdate('_blockViewProps');
|
||||
}
|
||||
@property({ type: Boolean, attribute: 'is-reference', reflect: true })
|
||||
private _isExternalContent = false;
|
||||
|
||||
@state()
|
||||
private _externalContentVariantState: string | null | undefined;
|
||||
|
||||
@state()
|
||||
private _isReadOnly = false;
|
||||
@@ -193,8 +243,8 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
|
||||
this.observe(
|
||||
this.#context.hasExpose,
|
||||
(exposed) => {
|
||||
this.#updateBlockViewProps({ unpublished: !exposed });
|
||||
this._exposed = exposed;
|
||||
this._localExpose = exposed;
|
||||
this.#updateExposedState();
|
||||
},
|
||||
null,
|
||||
);
|
||||
@@ -211,6 +261,22 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
|
||||
this.observe(this.#context.actionsVisibility, (showActions) => (this._showActions = showActions), null);
|
||||
this.observe(this.#context.inlineEditingMode, (mode) => (this._inlineEditingMode = mode), null);
|
||||
this.observe(this.#context.isSortMode, (isSortMode) => (this._isSortMode = isSortMode), null);
|
||||
this.observe(
|
||||
this.#context.isExternalContent,
|
||||
(isExternalContent) => {
|
||||
this._isExternalContent = isExternalContent;
|
||||
this.#updateExposedState();
|
||||
},
|
||||
null,
|
||||
);
|
||||
this.observe(
|
||||
this.#context.externalContentVariantState,
|
||||
(state) => {
|
||||
this._externalContentVariantState = state;
|
||||
this.#updateExposedState();
|
||||
},
|
||||
null,
|
||||
);
|
||||
|
||||
// Data:
|
||||
this.observe(
|
||||
@@ -299,6 +365,11 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
|
||||
);
|
||||
}
|
||||
|
||||
#updateBlockViewProps(incoming: Partial<UmbBlockEditorCustomViewProperties<UmbBlockGridLayoutModel>>) {
|
||||
this._blockViewProps = { ...this._blockViewProps, ...incoming };
|
||||
this.requestUpdate('_blockViewProps');
|
||||
}
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
// element styling:
|
||||
@@ -374,6 +445,16 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
|
||||
this.#context.expose();
|
||||
};
|
||||
|
||||
#updateExposedState() {
|
||||
// External content blocks use the element's variant state; local blocks use the expose entry
|
||||
const isExposed = this._isExternalContent
|
||||
? this._externalContentVariantState === UmbElementVariantState.PUBLISHED ||
|
||||
this._externalContentVariantState === UmbElementVariantState.PUBLISHED_PENDING_CHANGES
|
||||
: this._localExpose;
|
||||
this.#updateBlockViewProps({ unpublished: !isExposed });
|
||||
this._exposed = isExposed;
|
||||
}
|
||||
|
||||
#callUpdateInlineCreateButtons() {
|
||||
clearTimeout(this.#renderTimeout);
|
||||
this.#renderTimeout = setTimeout(this.#updateInlineCreateButtons, 100) as unknown as number;
|
||||
@@ -458,6 +539,9 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
|
||||
this._isSortMode,
|
||||
() => this.#renderRefBlock(),
|
||||
() => html`
|
||||
<umb-entity-frame>
|
||||
${when(this._isExternalContent, () => html`<uui-icon name="link"></uui-icon>`)} ${this._label}
|
||||
</umb-entity-frame>
|
||||
<umb-extension-slot
|
||||
single
|
||||
type="blockEditorCustomView"
|
||||
@@ -580,7 +664,7 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
|
||||
#renderActionBar() {
|
||||
if (this._isSortMode) return nothing;
|
||||
if (!this._showActions) return nothing;
|
||||
return html`<umb-block-action-list id="actions" block-editor=${UMB_BLOCK_GRID}></umb-block-action-list>`;
|
||||
return html`<umb-block-action-list id="actions" .blockEditor=${UMB_BLOCK_GRID}></umb-block-action-list>`;
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
@@ -659,10 +743,6 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
|
||||
right: calc(1px - (var(--umb-block-grid--column-gap, 0px) * 0.5));
|
||||
}
|
||||
|
||||
.umb-block-grid__block {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:host(:hover):not(:drop)::after {
|
||||
display: block;
|
||||
border-color: var(--uui-color-interactive-emphasis);
|
||||
@@ -696,6 +776,23 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
|
||||
uui-badge {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
:host([is-reference]) .umb-block-grid__block {
|
||||
--umb-entity-frame-color: var(--umb-color-reference, #7532c8);
|
||||
--umb-entity-frame-contrast-color: var(--umb-color-reference-contrast, #ffffff);
|
||||
}
|
||||
|
||||
.umb-block-grid__block {
|
||||
--umb-entity-frame-opacity: 0;
|
||||
--umb-entity-frame-color: var(--uui-color-interactive-emphasis);
|
||||
|
||||
height: 100%;
|
||||
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
--umb-entity-frame-opacity: 1;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ export async function forEachBlockLayoutEntryOf(
|
||||
callback: (entry: UmbBlockGridLayoutModel, parentUnique: string, areaKey: string) => PromiseLike<void>,
|
||||
): Promise<void> {
|
||||
if (entry.areas) {
|
||||
const parentUnique = entry.contentKey;
|
||||
const parentUnique = entry.key;
|
||||
await Promise.all(
|
||||
entry.areas.map(async (area) => {
|
||||
const areaKey = area.key;
|
||||
|
||||
+2
@@ -32,6 +32,7 @@ describe('UmbBlockListToBlockClipboardCopyPropertyValueTranslator', () => {
|
||||
layout: {
|
||||
[UMB_BLOCK_LIST_PROPERTY_EDITOR_SCHEMA_ALIAS]: [
|
||||
{
|
||||
key: 'contentKey',
|
||||
contentKey: 'contentKey',
|
||||
settingsKey: null,
|
||||
},
|
||||
@@ -51,6 +52,7 @@ describe('UmbBlockListToBlockClipboardCopyPropertyValueTranslator', () => {
|
||||
contentData: blockListPropertyValue.contentData,
|
||||
layout: [
|
||||
{
|
||||
key: 'contentKey',
|
||||
contentKey: 'contentKey',
|
||||
settingsKey: null,
|
||||
},
|
||||
|
||||
+2
@@ -32,6 +32,7 @@ describe('UmbBlockToBlockListClipboardPastePropertyValueTranslator', () => {
|
||||
layout: {
|
||||
[UMB_BLOCK_LIST_PROPERTY_EDITOR_SCHEMA_ALIAS]: [
|
||||
{
|
||||
key: 'contentKey',
|
||||
contentKey: 'contentKey',
|
||||
settingsKey: null,
|
||||
},
|
||||
@@ -45,6 +46,7 @@ describe('UmbBlockToBlockListClipboardPastePropertyValueTranslator', () => {
|
||||
contentData: blockListPropertyValue.contentData,
|
||||
layout: [
|
||||
{
|
||||
key: 'contentKey',
|
||||
contentKey: 'contentKey',
|
||||
settingsKey: null,
|
||||
},
|
||||
|
||||
+134
-29
@@ -3,8 +3,9 @@ import type { UmbBlockListLayoutModel } from '../../types.js';
|
||||
import { UMB_BLOCK_LIST } from '../../constants.js';
|
||||
import { css, customElement, html, nothing, property, state, when } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement, umbDestroyOnDisconnect } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { stringOrStringArrayContains } from '@umbraco-cms/backoffice/utils';
|
||||
import { stringOrStringArrayContains, UmbDeprecation } from '@umbraco-cms/backoffice/utils';
|
||||
import { UmbDataPathBlockElementDataQuery } from '@umbraco-cms/backoffice/block';
|
||||
import { UmbElementVariantState } from '@umbraco-cms/backoffice/element';
|
||||
import { UmbObserveValidationStateController } from '@umbraco-cms/backoffice/validation';
|
||||
import { UUIBlinkAnimationValue, UUIBlinkKeyframes } from '@umbraco-cms/backoffice/external/uui';
|
||||
import type {
|
||||
@@ -32,10 +33,54 @@ export class UmbBlockListEntryElement extends UmbLitElement implements UmbProper
|
||||
return this.#context.getIndex();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the layout entry for this block.
|
||||
*/
|
||||
public set layout(value: UmbBlockListLayoutModel | undefined) {
|
||||
if (!value) return;
|
||||
const key = value.key;
|
||||
const contentKey = value.contentKey;
|
||||
|
||||
if (key && key !== this._key) {
|
||||
this._key = key;
|
||||
this.#context.setKey(key);
|
||||
}
|
||||
|
||||
if (contentKey && contentKey !== this._contentKey) {
|
||||
this._contentKey = contentKey;
|
||||
|
||||
new UmbObserveValidationStateController(
|
||||
this,
|
||||
`$.contentData[${UmbDataPathBlockElementDataQuery({ key: contentKey })}]`,
|
||||
(hasMessages) => {
|
||||
this._contentInvalid = hasMessages;
|
||||
this._blockViewProps.contentInvalid = hasMessages;
|
||||
},
|
||||
'observeMessagesForContent',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public get key(): string | undefined {
|
||||
return this._key;
|
||||
}
|
||||
private _key?: string | undefined;
|
||||
|
||||
/**
|
||||
* @deprecated Use the `layout` property instead. Will be removed in Umbraco 20.
|
||||
*/
|
||||
@property({ attribute: false })
|
||||
public set contentKey(value: string | undefined) {
|
||||
if (!value) return;
|
||||
new UmbDeprecation({
|
||||
deprecated: 'umb-block-list-entry.contentKey property',
|
||||
solution: 'Use the `layout` property instead.',
|
||||
removeInVersion: '20.0.0',
|
||||
}).warn();
|
||||
this._contentKey = value;
|
||||
if (!this._key) {
|
||||
this.#context.setKey(value);
|
||||
}
|
||||
this.#context.setContentKey(value);
|
||||
|
||||
new UmbObserveValidationStateController(
|
||||
@@ -73,6 +118,8 @@ export class UmbBlockListEntryElement extends UmbLitElement implements UmbProper
|
||||
@state()
|
||||
private _exposed?: boolean;
|
||||
|
||||
private _localExpose?: boolean;
|
||||
|
||||
@state()
|
||||
private _unsupported?: boolean;
|
||||
|
||||
@@ -99,10 +146,11 @@ export class UmbBlockListEntryElement extends UmbLitElement implements UmbProper
|
||||
config: { showContentEdit: false, showSettingsEdit: false },
|
||||
}; // Set to undefined cause it will be set before we render.
|
||||
|
||||
#updateBlockViewProps(incoming: Partial<UmbBlockEditorCustomViewProperties<UmbBlockListLayoutModel>>) {
|
||||
this._blockViewProps = { ...this._blockViewProps, ...incoming };
|
||||
this.requestUpdate('_blockViewProps');
|
||||
}
|
||||
@property({ type: Boolean, attribute: 'is-reference', reflect: true })
|
||||
private _isExternalContent = false;
|
||||
|
||||
@state()
|
||||
private _externalContentVariantState: string | null | undefined;
|
||||
|
||||
@state()
|
||||
private _isReadOnly = false;
|
||||
@@ -155,8 +203,8 @@ export class UmbBlockListEntryElement extends UmbLitElement implements UmbProper
|
||||
this.observe(
|
||||
this.#context.hasExpose,
|
||||
(exposed) => {
|
||||
this.#updateBlockViewProps({ unpublished: !exposed });
|
||||
this._exposed = exposed;
|
||||
this._localExpose = exposed;
|
||||
this.#updateExposedState();
|
||||
},
|
||||
null,
|
||||
);
|
||||
@@ -173,6 +221,22 @@ export class UmbBlockListEntryElement extends UmbLitElement implements UmbProper
|
||||
this.observe(this.#context.actionsVisibility, (showActions) => (this._showActions = showActions), null);
|
||||
this.observe(this.#context.inlineEditingMode, (mode) => (this._inlineEditingMode = mode), null);
|
||||
this.observe(this.#context.isSortMode, (isSortMode) => (this._isSortMode = isSortMode), null);
|
||||
this.observe(
|
||||
this.#context.isExternalContent,
|
||||
(isExternalContent) => {
|
||||
this._isExternalContent = isExternalContent;
|
||||
this.#updateExposedState();
|
||||
},
|
||||
null,
|
||||
);
|
||||
this.observe(
|
||||
this.#context.externalContentVariantState,
|
||||
(state) => {
|
||||
this._externalContentVariantState = state;
|
||||
this.#updateExposedState();
|
||||
},
|
||||
null,
|
||||
);
|
||||
|
||||
// Data props:
|
||||
this.observe(
|
||||
@@ -244,6 +308,11 @@ export class UmbBlockListEntryElement extends UmbLitElement implements UmbProper
|
||||
);
|
||||
}
|
||||
|
||||
#updateBlockViewProps(incoming: Partial<UmbBlockEditorCustomViewProperties<UmbBlockListLayoutModel>>) {
|
||||
this._blockViewProps = { ...this._blockViewProps, ...incoming };
|
||||
this.requestUpdate('_blockViewProps');
|
||||
}
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
// element styling:
|
||||
@@ -279,6 +348,16 @@ export class UmbBlockListEntryElement extends UmbLitElement implements UmbProper
|
||||
this.#context.expose();
|
||||
};
|
||||
|
||||
#updateExposedState() {
|
||||
// External content blocks use the element's variant state; local blocks use the expose entry
|
||||
const isExposed = this._isExternalContent
|
||||
? this._externalContentVariantState === UmbElementVariantState.PUBLISHED ||
|
||||
this._externalContentVariantState === UmbElementVariantState.PUBLISHED_PENDING_CHANGES
|
||||
: this._localExpose;
|
||||
this.#updateBlockViewProps({ unpublished: !isExposed });
|
||||
this._exposed = isExposed;
|
||||
}
|
||||
|
||||
#extensionSlotFilterMethod = (manifest: ManifestBlockEditorCustomView) => {
|
||||
if (this._unsupported) {
|
||||
// If the block is unsupported, we should not allow any custom views to render.
|
||||
@@ -302,12 +381,14 @@ export class UmbBlockListEntryElement extends UmbLitElement implements UmbProper
|
||||
if (this._exposed || this._isReadOnly) {
|
||||
return ext.component;
|
||||
} else {
|
||||
return html`<div style="min-height: var(--uui-size-16);">
|
||||
${ext.component}
|
||||
<umb-block-overlay-expose-button
|
||||
.contentTypeName=${this._contentTypeName}
|
||||
@click=${this.#expose}></umb-block-overlay-expose-button>
|
||||
</div>`;
|
||||
return html`
|
||||
<div style="min-height: var(--uui-size-16);">
|
||||
${ext.component}
|
||||
<umb-block-overlay-expose-button
|
||||
.contentTypeName=${this._contentTypeName}
|
||||
@click=${this.#expose}></umb-block-overlay-expose-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -328,23 +409,29 @@ export class UmbBlockListEntryElement extends UmbLitElement implements UmbProper
|
||||
}
|
||||
|
||||
#renderInlineBlock() {
|
||||
return html`<umb-inline-list-block
|
||||
.label=${this._label}
|
||||
.icon=${this._icon}
|
||||
.index=${this._blockViewProps.index}
|
||||
.unpublished=${!this._exposed}
|
||||
.config=${this._blockViewProps.config}
|
||||
.content=${this._blockViewProps.content}
|
||||
.settings=${this._blockViewProps.settings}
|
||||
${umbDestroyOnDisconnect()}></umb-inline-list-block>`;
|
||||
return html`
|
||||
<umb-inline-list-block
|
||||
.label=${this._label}
|
||||
.icon=${this._icon}
|
||||
.index=${this._blockViewProps.index}
|
||||
.unpublished=${!this._exposed}
|
||||
.config=${this._blockViewProps.config}
|
||||
.content=${this._blockViewProps.content}
|
||||
.settings=${this._blockViewProps.settings}
|
||||
${umbDestroyOnDisconnect()}>
|
||||
</umb-inline-list-block>
|
||||
`;
|
||||
}
|
||||
|
||||
#renderUnsupportedBlock() {
|
||||
return html`<umb-unsupported-list-block
|
||||
.config=${this._blockViewProps.config}
|
||||
.content=${this._blockViewProps.content}
|
||||
.settings=${this._blockViewProps.settings}
|
||||
${umbDestroyOnDisconnect()}></umb-unsupported-list-block>`;
|
||||
return html`
|
||||
<umb-unsupported-list-block
|
||||
.config=${this._blockViewProps.config}
|
||||
.content=${this._blockViewProps.content}
|
||||
.settings=${this._blockViewProps.settings}
|
||||
${umbDestroyOnDisconnect()}>
|
||||
</umb-unsupported-list-block>
|
||||
`;
|
||||
}
|
||||
|
||||
#renderBuiltinBlockView = () => {
|
||||
@@ -366,6 +453,9 @@ export class UmbBlockListEntryElement extends UmbLitElement implements UmbProper
|
||||
this._isSortMode,
|
||||
() => this.#renderRefBlock(),
|
||||
() => html`
|
||||
<umb-entity-frame>
|
||||
${when(this._isExternalContent, () => html`<uui-icon name="link"></uui-icon>`)} ${this._label}
|
||||
</umb-entity-frame>
|
||||
<umb-extension-slot
|
||||
single
|
||||
type="blockEditorCustomView"
|
||||
@@ -388,7 +478,7 @@ export class UmbBlockListEntryElement extends UmbLitElement implements UmbProper
|
||||
#renderActionBar() {
|
||||
if (this._isSortMode) return nothing;
|
||||
if (!this._showActions) return nothing;
|
||||
return html`<umb-block-action-list id="actions" block-editor=${UMB_BLOCK_LIST}></umb-block-action-list>`;
|
||||
return html`<umb-block-action-list id="actions" .blockEditor=${UMB_BLOCK_LIST}></umb-block-action-list>`;
|
||||
}
|
||||
|
||||
override render() {
|
||||
@@ -484,6 +574,21 @@ export class UmbBlockListEntryElement extends UmbLitElement implements UmbProper
|
||||
transition: opacity 50ms 16ms;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
:host([is-reference]) .umb-block-list__block {
|
||||
--umb-entity-frame-color: var(--umb-color-reference, #7532c8);
|
||||
--umb-entity-frame-contrast-color: var(--umb-color-reference-contrast, #ffffff);
|
||||
}
|
||||
|
||||
.umb-block-list__block {
|
||||
--umb-entity-frame-opacity: 0;
|
||||
--umb-entity-frame-color: var(--uui-color-interactive-emphasis);
|
||||
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
--umb-entity-frame-opacity: 1;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
@@ -494,4 +599,4 @@ declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-block-list-entry': UmbBlockListEntryElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
-3
@@ -56,6 +56,7 @@ export class UmbBlockListEntriesContext extends UmbBlockEntriesContext<
|
||||
const valueResolver = new UmbClipboardPastePropertyValueTranslatorValueResolver(this);
|
||||
|
||||
const blockTypes = this._manager.getBlockTypes();
|
||||
const libraryAllowedElementTypeKeys = await this._getLibraryAllowedElementTypeKeys(blockTypes);
|
||||
|
||||
const configuredSize = this._manager
|
||||
.getEditorConfiguration()
|
||||
@@ -73,6 +74,7 @@ export class UmbBlockListEntriesContext extends UmbBlockEntriesContext<
|
||||
blocks: blockTypes,
|
||||
blockGroups: [],
|
||||
openClipboard: routingInfo.view === 'clipboard',
|
||||
libraryAllowedElementTypeKeys,
|
||||
clipboardFilter: async (clipboardEntryDetail) => {
|
||||
const hasSupportedPasteTranslator = clipboardContext.hasSupportedPasteTranslator(
|
||||
pasteTranslatorManifests,
|
||||
@@ -104,7 +106,7 @@ export class UmbBlockListEntriesContext extends UmbBlockEntriesContext<
|
||||
};
|
||||
})
|
||||
.onSubmit(async (value, data) => {
|
||||
if (value?.create && data) {
|
||||
if (value && 'create' in value && data) {
|
||||
const created = await this.create(
|
||||
value.create.contentElementTypeKey,
|
||||
{},
|
||||
@@ -120,7 +122,9 @@ export class UmbBlockListEntriesContext extends UmbBlockEntriesContext<
|
||||
} else {
|
||||
throw new Error('Failed to create block');
|
||||
}
|
||||
} else if (value?.clipboard && value.clipboard.selection?.length && data) {
|
||||
} else if (value && 'library' in value) {
|
||||
this._manager?.insertExternalContent(value.library.elementKey);
|
||||
} else if (value && 'clipboard' in value && value.clipboard.selection?.length && data) {
|
||||
const clipboardContext = await this.getContext(UMB_CLIPBOARD_PROPERTY_CONTEXT);
|
||||
if (!clipboardContext) {
|
||||
throw new Error('Clipboard context not found');
|
||||
@@ -196,7 +200,7 @@ export class UmbBlockListEntriesContext extends UmbBlockEntriesContext<
|
||||
|
||||
async create(
|
||||
contentElementTypeKey: string,
|
||||
partialLayoutEntry?: Omit<UmbBlockListLayoutModel, 'contentKey'>,
|
||||
partialLayoutEntry?: Omit<UmbBlockListLayoutModel, 'contentKey' | 'key'>,
|
||||
originData?: UmbBlockListWorkspaceOriginData,
|
||||
) {
|
||||
await this._retrieveManager;
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ export class UmbBlockListManagerContext<
|
||||
*/
|
||||
async createWithPresets(
|
||||
contentElementTypeKey: string,
|
||||
partialLayoutEntry?: Omit<BlockLayoutType, 'contentKey'>,
|
||||
partialLayoutEntry?: Omit<BlockLayoutType, 'contentKey' | 'key'>,
|
||||
// This property is used by some implementations, but not used in this. Do not remove. [NL]
|
||||
|
||||
_originData?: UmbBlockListWorkspaceOriginData,
|
||||
|
||||
+4
-9
@@ -40,10 +40,10 @@ import '../../components/block-list-entry/index.js';
|
||||
|
||||
const SORTER_CONFIG: UmbSorterConfig<UmbBlockListLayoutModel, UmbBlockListEntryElement> = {
|
||||
getUniqueOfElement: (element) => {
|
||||
return element.contentKey!;
|
||||
return element.key!;
|
||||
},
|
||||
getUniqueOfModel: (modelEntry) => {
|
||||
return modelEntry.contentKey;
|
||||
return modelEntry.key;
|
||||
},
|
||||
//identifier: 'block-list-editor',
|
||||
itemSelector: 'umb-block-list-entry',
|
||||
@@ -406,15 +406,10 @@ export class UmbPropertyEditorUIBlockListElement
|
||||
${this.#renderSortModeToolbar()}
|
||||
${repeat(
|
||||
this._layouts,
|
||||
(layout) => layout.contentKey,
|
||||
(layout) => layout.key,
|
||||
(layout, index) => html`
|
||||
${this.#renderInlineCreateButton(index)}
|
||||
<umb-block-list-entry
|
||||
index=${index}
|
||||
.contentKey=${layout.contentKey}
|
||||
.layout=${layout}
|
||||
${umbDestroyOnDisconnect()}>
|
||||
</umb-block-list-entry>
|
||||
<umb-block-list-entry index=${index} .layout=${layout} ${umbDestroyOnDisconnect()}></umb-block-list-entry>
|
||||
`,
|
||||
)}
|
||||
${this.#renderCreateButtonGroup()}
|
||||
|
||||
+104
-19
@@ -2,7 +2,8 @@ import type { UmbBlockRteLayoutModel } from '../../types.js';
|
||||
import { UMB_BLOCK_RTE } from '../../constants.js';
|
||||
import { UmbBlockRteEntryContext } from '../../context/block-rte-entry.context.js';
|
||||
import { css, customElement, html, nothing, property, state } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { stringOrStringArrayContains } from '@umbraco-cms/backoffice/utils';
|
||||
import { stringOrStringArrayContains, UmbDeprecation } from '@umbraco-cms/backoffice/utils';
|
||||
import { UmbElementVariantState } from '@umbraco-cms/backoffice/element';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { UmbDataPathBlockElementDataQuery } from '@umbraco-cms/backoffice/block';
|
||||
import { UmbObserveValidationStateController } from '@umbraco-cms/backoffice/validation';
|
||||
@@ -22,10 +23,35 @@ import '../../../block/action/block-action-list.element.js';
|
||||
*/
|
||||
@customElement('umb-rte-block')
|
||||
export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropertyEditorUiElement {
|
||||
/**
|
||||
* The unique key of this block layout entry.
|
||||
*/
|
||||
@property({ type: String, attribute: 'data-key', reflect: true })
|
||||
public set key(value: string | undefined) {
|
||||
if (!value) return;
|
||||
this._key = value;
|
||||
this.#context.setKey(value);
|
||||
}
|
||||
public get key(): string | undefined {
|
||||
return this._key;
|
||||
}
|
||||
private _key?: string | undefined;
|
||||
|
||||
/**
|
||||
* @deprecated Use `key` instead. Will be removed in Umbraco 20.
|
||||
*/
|
||||
@property({ type: String, attribute: 'data-content-key', reflect: true })
|
||||
public set contentKey(value: string | undefined) {
|
||||
if (!value) return;
|
||||
new UmbDeprecation({
|
||||
deprecated: 'umb-rte-block.contentKey property',
|
||||
solution: 'Use the `key` property instead.',
|
||||
removeInVersion: '20.0.0',
|
||||
}).warn();
|
||||
this._contentKey = value;
|
||||
if (!this._key) {
|
||||
this.#context.setKey(value);
|
||||
}
|
||||
this.#context.setContentKey(value);
|
||||
|
||||
new UmbObserveValidationStateController(
|
||||
@@ -57,6 +83,11 @@ export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropert
|
||||
@state()
|
||||
private _exposed?: boolean;
|
||||
|
||||
private _localExpose?: boolean;
|
||||
|
||||
@state()
|
||||
private _externalContentVariantState: string | null | undefined;
|
||||
|
||||
@state()
|
||||
private _showActions?: boolean;
|
||||
|
||||
@@ -75,6 +106,10 @@ export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropert
|
||||
config: { showContentEdit: false, showSettingsEdit: false },
|
||||
}; // Set to undefined cause it will be set before we render.
|
||||
|
||||
// 'is-reference' attribute is used for styling purpose.
|
||||
@property({ type: Boolean, attribute: 'is-reference', reflect: true })
|
||||
private _isExternalContent = false;
|
||||
|
||||
// 'content-invalid' attribute is used for styling purpose.
|
||||
@property({ type: Boolean, attribute: 'content-invalid', reflect: true })
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
@@ -85,6 +120,16 @@ export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropert
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
_settingsInvalid?: boolean;
|
||||
|
||||
#updateExposedState() {
|
||||
// External content blocks use the element's variant state; local blocks use the expose entry
|
||||
const isExposed = this._isExternalContent
|
||||
? this._externalContentVariantState === UmbElementVariantState.PUBLISHED ||
|
||||
this._externalContentVariantState === UmbElementVariantState.PUBLISHED_PENDING_CHANGES
|
||||
: this._localExpose;
|
||||
this.#updateBlockViewProps({ unpublished: !isExposed });
|
||||
this._exposed = isExposed;
|
||||
}
|
||||
|
||||
#updateBlockViewProps(incoming: Partial<UmbBlockEditorCustomViewProperties<UmbBlockRteLayoutModel>>) {
|
||||
this._blockViewProps = { ...this._blockViewProps, ...incoming };
|
||||
this.requestUpdate('_blockViewProps');
|
||||
@@ -152,8 +197,26 @@ export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropert
|
||||
this.observe(
|
||||
this.#context.hasExpose,
|
||||
(exposed) => {
|
||||
this.#updateBlockViewProps({ unpublished: !exposed });
|
||||
this._exposed = exposed;
|
||||
this._localExpose = exposed;
|
||||
this.#updateExposedState();
|
||||
},
|
||||
null,
|
||||
);
|
||||
|
||||
this.observe(
|
||||
this.#context.isExternalContent,
|
||||
(isExternalContent) => {
|
||||
this._isExternalContent = isExternalContent ?? false;
|
||||
this.#updateExposedState();
|
||||
},
|
||||
null,
|
||||
);
|
||||
|
||||
this.observe(
|
||||
this.#context.externalContentVariantState,
|
||||
(state) => {
|
||||
this._externalContentVariantState = state;
|
||||
this.#updateExposedState();
|
||||
},
|
||||
null,
|
||||
);
|
||||
@@ -255,19 +318,22 @@ export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropert
|
||||
if (this._exposed || this._isReadOnly) {
|
||||
return ext.component;
|
||||
} else {
|
||||
return html`<div>
|
||||
${ext.component}
|
||||
<umb-block-overlay-expose-button
|
||||
.contentTypeName=${this._contentTypeName}
|
||||
@click=${this.#expose}></umb-block-overlay-expose-button>
|
||||
</div>`;
|
||||
return html`
|
||||
<div>
|
||||
${ext.component}
|
||||
<umb-block-overlay-expose-button
|
||||
.contentTypeName=${this._contentTypeName}
|
||||
@click=${this.#expose}></umb-block-overlay-expose-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
};
|
||||
|
||||
#renderBlock() {
|
||||
return this.contentKey && this._contentTypeAlias
|
||||
? html`
|
||||
<div class="uui-text uui-font">
|
||||
<div class="umb-block-rte__block uui-text uui-font">
|
||||
<umb-entity-frame .label=${this._label}></umb-entity-frame>
|
||||
<umb-extension-slot
|
||||
type="blockEditorCustomView"
|
||||
default-element="umb-ref-rte-block"
|
||||
@@ -287,7 +353,7 @@ export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropert
|
||||
|
||||
#renderActionBar() {
|
||||
if (!this._showActions) return nothing;
|
||||
return html`<umb-block-action-list id="actions" block-editor=${UMB_BLOCK_RTE}></umb-block-action-list>`;
|
||||
return html`<umb-block-action-list id="actions" .blockEditor=${UMB_BLOCK_RTE}></umb-block-action-list>`;
|
||||
}
|
||||
|
||||
#renderBuiltinBlockView = () => {
|
||||
@@ -299,14 +365,17 @@ export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropert
|
||||
};
|
||||
|
||||
#renderRefBlock() {
|
||||
return html`<umb-ref-rte-block
|
||||
.label=${this._label}
|
||||
.icon=${this._icon}
|
||||
.index=${this._blockViewProps.index}
|
||||
.unpublished=${!this._exposed}
|
||||
.content=${this._blockViewProps.content}
|
||||
.settings=${this._blockViewProps.settings}
|
||||
.config=${this._blockViewProps.config}></umb-ref-rte-block>`;
|
||||
return html`
|
||||
<umb-ref-rte-block
|
||||
.label=${this._label}
|
||||
.icon=${this._icon}
|
||||
.index=${this._blockViewProps.index}
|
||||
.unpublished=${!this._exposed}
|
||||
.content=${this._blockViewProps.content}
|
||||
.settings=${this._blockViewProps.settings}
|
||||
.config=${this._blockViewProps.config}>
|
||||
</umb-ref-rte-block>
|
||||
`;
|
||||
}
|
||||
|
||||
override render() {
|
||||
@@ -319,6 +388,7 @@ export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropert
|
||||
:host {
|
||||
position: relative;
|
||||
display: block;
|
||||
margin-top: var(--uui-size-3);
|
||||
user-select: all;
|
||||
user-drag: auto;
|
||||
white-space: nowrap;
|
||||
@@ -355,6 +425,21 @@ export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropert
|
||||
:host([drag-placeholder]) {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
:host([is-reference]) .umb-block-rte__block {
|
||||
--umb-entity-frame-color: var(--umb-color-reference, #7532c8);
|
||||
--umb-entity-frame-contrast-color: var(--umb-color-reference-contrast, #ffffff);
|
||||
}
|
||||
|
||||
.umb-block-rte__block {
|
||||
--umb-entity-frame-opacity: 0;
|
||||
--umb-entity-frame-color: var(--uui-color-interactive-emphasis);
|
||||
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
--umb-entity-frame-opacity: 1;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
+14
-6
@@ -77,12 +77,15 @@ export class UmbBlockRteEntriesContext extends UmbBlockEntriesContext<
|
||||
const config = propertyContext.getConfig();
|
||||
const valueResolver = new UmbClipboardPastePropertyValueTranslatorValueResolver(this);
|
||||
|
||||
const libraryAllowedElementTypeKeys = await this._getLibraryAllowedElementTypeKeys(blockTypes);
|
||||
|
||||
return {
|
||||
modal: { size: modalSize },
|
||||
data: {
|
||||
blocks: blockTypes,
|
||||
blockGroups: [],
|
||||
openClipboard: routingInfo.view === 'clipboard',
|
||||
libraryAllowedElementTypeKeys,
|
||||
clipboardFilter: async (clipboardEntryDetail) => {
|
||||
const hasSupportedPasteTranslator = clipboardContext.hasSupportedPasteTranslator(
|
||||
pasteTranslatorManifests,
|
||||
@@ -114,7 +117,7 @@ export class UmbBlockRteEntriesContext extends UmbBlockEntriesContext<
|
||||
};
|
||||
})
|
||||
.onSubmit(async (value, data) => {
|
||||
if (value?.create && data) {
|
||||
if (value && 'create' in value && data) {
|
||||
const created = await this.create(
|
||||
value.create.contentElementTypeKey,
|
||||
{},
|
||||
@@ -130,7 +133,12 @@ export class UmbBlockRteEntriesContext extends UmbBlockEntriesContext<
|
||||
} else {
|
||||
throw new Error('Failed to create block');
|
||||
}
|
||||
} else if (value?.clipboard && value.clipboard.selection?.length && data) {
|
||||
} else if (value && 'library' in value && data) {
|
||||
await this._manager?.insertExternalContent(
|
||||
value.library.elementKey,
|
||||
data.originData as UmbBlockRteWorkspaceOriginData,
|
||||
);
|
||||
} else if (value && 'clipboard' in value && value.clipboard.selection?.length && data) {
|
||||
const clipboardContext = await this.getContext(UMB_CLIPBOARD_PROPERTY_CONTEXT);
|
||||
if (!clipboardContext) {
|
||||
throw new Error('Clipboard context not found');
|
||||
@@ -193,7 +201,7 @@ export class UmbBlockRteEntriesContext extends UmbBlockEntriesContext<
|
||||
|
||||
async create(
|
||||
contentElementTypeKey: string,
|
||||
partialLayoutEntry?: Omit<UmbBlockRteLayoutModel, 'contentKey'>,
|
||||
partialLayoutEntry?: Omit<UmbBlockRteLayoutModel, 'contentKey' | 'key'>,
|
||||
originData?: UmbBlockRteWorkspaceOriginData,
|
||||
) {
|
||||
await this._retrieveManager;
|
||||
@@ -214,11 +222,11 @@ export class UmbBlockRteEntriesContext extends UmbBlockEntriesContext<
|
||||
* Delete a block by requesting its removal through the pending deletion mechanism.
|
||||
* This enables undo support by removing the HTML element first via Tiptap,
|
||||
* which triggers _filterUnusedBlocks to store block data before removal.
|
||||
* @param {string} contentKey - The content key of the block to delete.
|
||||
* @param {string} layoutKey - The layout key of the block to delete.
|
||||
*/
|
||||
override async delete(contentKey: string) {
|
||||
override async delete(layoutKey: string) {
|
||||
await this._retrieveManager;
|
||||
this._manager?.requestPendingDeletion(contentKey);
|
||||
this._manager?.requestPendingDeletion(layoutKey);
|
||||
}
|
||||
|
||||
async #insertFromRtePropertyValues(
|
||||
|
||||
+21
-12
@@ -3,6 +3,7 @@ import type { UmbBlockRteLayoutModel, UmbBlockRteTypeModel } from '../types.js';
|
||||
import type { UmbBlockDataModel } from '../../block/types.js';
|
||||
import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
|
||||
import { UmbBlockManagerContext } from '@umbraco-cms/backoffice/block';
|
||||
import { UmbId } from '@umbraco-cms/backoffice/id';
|
||||
|
||||
import '../components/block-rte-entry/index.js';
|
||||
|
||||
@@ -22,28 +23,28 @@ export class UmbBlockRteManagerContext<
|
||||
public readonly pendingDeletions = this.#pendingDeletions.asObservable();
|
||||
|
||||
/**
|
||||
* Request a block to be deleted. This adds the contentKey to pending deletions,
|
||||
* Request a block to be deleted. This adds the layout key to pending deletions,
|
||||
* which will be processed by the Tiptap API to remove the HTML element first,
|
||||
* enabling undo support.
|
||||
* @param {string} contentKey - The content key of the block to delete.
|
||||
* @param {string} layoutKey - The layout key of the block to delete.
|
||||
*/
|
||||
public requestPendingDeletion(contentKey: string) {
|
||||
this.#pendingDeletions.appendOne(contentKey);
|
||||
public requestPendingDeletion(layoutKey: string) {
|
||||
this.#pendingDeletions.appendOne(layoutKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear a pending deletion after it has been processed.
|
||||
* @param {string} contentKey - The content key to clear from pending deletions.
|
||||
* @param {string} layoutKey - The layout key to clear from pending deletions.
|
||||
*/
|
||||
public clearPendingDeletion(contentKey: string) {
|
||||
this.#pendingDeletions.removeOne(contentKey);
|
||||
public clearPendingDeletion(layoutKey: string) {
|
||||
this.#pendingDeletions.removeOne(layoutKey);
|
||||
}
|
||||
|
||||
removeOneLayout(contentKey: string) {
|
||||
this._layouts.removeOne(contentKey);
|
||||
removeOneLayout(layoutKey: string) {
|
||||
this._layouts.removeOne(layoutKey);
|
||||
}
|
||||
removeManyLayouts(contentKeys: Array<string>) {
|
||||
this._layouts.remove(contentKeys);
|
||||
removeManyLayouts(layoutKeys: Array<string>) {
|
||||
this._layouts.remove(layoutKeys);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,7 +54,7 @@ export class UmbBlockRteManagerContext<
|
||||
*/
|
||||
async createWithPresets(
|
||||
contentElementTypeKey: string,
|
||||
partialLayoutEntry?: Omit<BlockLayoutType, 'contentKey'>,
|
||||
partialLayoutEntry?: Omit<BlockLayoutType, 'contentKey' | 'key'>,
|
||||
// This property is used by some implementations, but not used in this, do not remove. [NL]
|
||||
|
||||
_originData?: UmbBlockRteWorkspaceOriginData,
|
||||
@@ -82,6 +83,14 @@ export class UmbBlockRteManagerContext<
|
||||
return true;
|
||||
}
|
||||
|
||||
override async insertExternalContent(elementKey: string, originData?: UmbBlockRteWorkspaceOriginData) {
|
||||
await super.insertExternalContent(elementKey, originData);
|
||||
if (originData) {
|
||||
const layout = { key: UmbId.new(), contentKey: elementKey, isExternalContent: true } as BlockLayoutType;
|
||||
this.notifyBlockInserted(layout, originData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param contentKey
|
||||
* @internal
|
||||
|
||||
+2
@@ -32,6 +32,7 @@ describe('UmbBlockSingleToBlockClipboardCopyPropertyValueTranslator', () => {
|
||||
layout: {
|
||||
[UMB_BLOCK_SINGLE_PROPERTY_EDITOR_SCHEMA_ALIAS]: [
|
||||
{
|
||||
key: 'contentKey',
|
||||
contentKey: 'contentKey',
|
||||
settingsKey: null,
|
||||
},
|
||||
@@ -51,6 +52,7 @@ describe('UmbBlockSingleToBlockClipboardCopyPropertyValueTranslator', () => {
|
||||
contentData: blockSinglePropertyValue.contentData,
|
||||
layout: [
|
||||
{
|
||||
key: 'contentKey',
|
||||
contentKey: 'contentKey',
|
||||
settingsKey: null,
|
||||
},
|
||||
|
||||
+2
@@ -32,6 +32,7 @@ describe('UmbBlockToBlockSingleClipboardPastePropertyValueTranslator', () => {
|
||||
layout: {
|
||||
[UMB_BLOCK_SINGLE_PROPERTY_EDITOR_SCHEMA_ALIAS]: [
|
||||
{
|
||||
key: 'contentKey',
|
||||
contentKey: 'contentKey',
|
||||
settingsKey: null,
|
||||
},
|
||||
@@ -45,6 +46,7 @@ describe('UmbBlockToBlockSingleClipboardPastePropertyValueTranslator', () => {
|
||||
contentData: blockSinglePropertyValue.contentData,
|
||||
layout: [
|
||||
{
|
||||
key: 'contentKey',
|
||||
contentKey: 'contentKey',
|
||||
settingsKey: null,
|
||||
},
|
||||
|
||||
+137
-30
@@ -1,10 +1,11 @@
|
||||
import { UmbBlockSingleEntryContext } from '../../context/block-single-entry.context.js';
|
||||
import type { UmbBlockSingleLayoutModel } from '../../types.js';
|
||||
import { UMB_BLOCK_SINGLE } from '../../constants.js';
|
||||
import { css, customElement, html, nothing, property, state } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { css, customElement, html, nothing, property, state, when } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement, umbDestroyOnDisconnect } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { stringOrStringArrayContains } from '@umbraco-cms/backoffice/utils';
|
||||
import { stringOrStringArrayContains, UmbDeprecation } from '@umbraco-cms/backoffice/utils';
|
||||
import { UmbDataPathBlockElementDataQuery } from '@umbraco-cms/backoffice/block';
|
||||
import { UmbElementVariantState } from '@umbraco-cms/backoffice/element';
|
||||
import { UmbObserveValidationStateController } from '@umbraco-cms/backoffice/validation';
|
||||
import { UUIBlinkAnimationValue, UUIBlinkKeyframes } from '@umbraco-cms/backoffice/external/uui';
|
||||
import type {
|
||||
@@ -33,13 +34,57 @@ export class UmbBlockSingleEntryElement extends UmbLitElement implements UmbProp
|
||||
this.#context.setIndex(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the layout entry for this block.
|
||||
*/
|
||||
public set layout(value: UmbBlockSingleLayoutModel | undefined) {
|
||||
if (!value) return;
|
||||
const key = value.key;
|
||||
const contentKey = value.contentKey;
|
||||
|
||||
if (key && key !== this._key) {
|
||||
this._key = key;
|
||||
this.#context.setKey(key);
|
||||
}
|
||||
|
||||
if (contentKey && contentKey !== this._contentKey) {
|
||||
this._contentKey = contentKey;
|
||||
|
||||
new UmbObserveValidationStateController(
|
||||
this,
|
||||
`$.contentData[${UmbDataPathBlockElementDataQuery({ key: contentKey })}]`,
|
||||
(hasMessages) => {
|
||||
this._contentInvalid = hasMessages;
|
||||
this._blockViewProps.contentInvalid = hasMessages;
|
||||
},
|
||||
'observeMessagesForContent',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public get key(): string | undefined {
|
||||
return this._key;
|
||||
}
|
||||
private _key?: string | undefined;
|
||||
|
||||
/**
|
||||
* @deprecated Use the `layout` property instead. Will be removed in Umbraco 20.
|
||||
*/
|
||||
@property({ attribute: false })
|
||||
public get contentKey(): string | undefined {
|
||||
return this._contentKey;
|
||||
}
|
||||
public set contentKey(value: string | undefined) {
|
||||
if (!value) return;
|
||||
new UmbDeprecation({
|
||||
deprecated: 'umb-block-single-entry.contentKey property',
|
||||
solution: 'Use the `layout` property instead.',
|
||||
removeInVersion: '20.0.0',
|
||||
}).warn();
|
||||
this._contentKey = value;
|
||||
if (!this._key) {
|
||||
this.#context.setKey(value);
|
||||
}
|
||||
this.#context.setContentKey(value);
|
||||
|
||||
new UmbObserveValidationStateController(
|
||||
@@ -74,6 +119,8 @@ export class UmbBlockSingleEntryElement extends UmbLitElement implements UmbProp
|
||||
@state()
|
||||
private _exposed?: boolean;
|
||||
|
||||
private _localExpose?: boolean;
|
||||
|
||||
@state()
|
||||
private _unsupported?: boolean;
|
||||
|
||||
@@ -97,10 +144,11 @@ export class UmbBlockSingleEntryElement extends UmbLitElement implements UmbProp
|
||||
config: { showContentEdit: false, showSettingsEdit: false },
|
||||
}; // Set to undefined cause it will be set before we render.
|
||||
|
||||
#updateBlockViewProps(incoming: Partial<UmbBlockEditorCustomViewProperties<UmbBlockSingleLayoutModel>>) {
|
||||
this._blockViewProps = { ...this._blockViewProps, ...incoming };
|
||||
this.requestUpdate('_blockViewProps');
|
||||
}
|
||||
@property({ type: Boolean, attribute: 'is-reference', reflect: true })
|
||||
private _isExternalContent = false;
|
||||
|
||||
@state()
|
||||
private _externalContentVariantState: string | null | undefined;
|
||||
|
||||
@state()
|
||||
private _isReadOnly = false;
|
||||
@@ -109,6 +157,7 @@ export class UmbBlockSingleEntryElement extends UmbLitElement implements UmbProp
|
||||
super();
|
||||
this.#init();
|
||||
}
|
||||
|
||||
#init() {
|
||||
this.observe(
|
||||
this.#context.showContentEdit,
|
||||
@@ -152,8 +201,24 @@ export class UmbBlockSingleEntryElement extends UmbLitElement implements UmbProp
|
||||
this.observe(
|
||||
this.#context.hasExpose,
|
||||
(exposed) => {
|
||||
this.#updateBlockViewProps({ unpublished: !exposed });
|
||||
this._exposed = exposed;
|
||||
this._localExpose = exposed;
|
||||
this.#updateExposedState();
|
||||
},
|
||||
null,
|
||||
);
|
||||
this.observe(
|
||||
this.#context.isExternalContent,
|
||||
(isExternalContent) => {
|
||||
this._isExternalContent = isExternalContent;
|
||||
this.#updateExposedState();
|
||||
},
|
||||
null,
|
||||
);
|
||||
this.observe(
|
||||
this.#context.externalContentVariantState,
|
||||
(state) => {
|
||||
this._externalContentVariantState = state;
|
||||
this.#updateExposedState();
|
||||
},
|
||||
null,
|
||||
);
|
||||
@@ -245,6 +310,21 @@ export class UmbBlockSingleEntryElement extends UmbLitElement implements UmbProp
|
||||
);
|
||||
}
|
||||
|
||||
#updateBlockViewProps(incoming: Partial<UmbBlockEditorCustomViewProperties<UmbBlockSingleLayoutModel>>) {
|
||||
this._blockViewProps = { ...this._blockViewProps, ...incoming };
|
||||
this.requestUpdate('_blockViewProps');
|
||||
}
|
||||
|
||||
#updateExposedState() {
|
||||
// External content blocks use the element's variant state; local blocks use the expose entry
|
||||
const isExposed = this._isExternalContent
|
||||
? this._externalContentVariantState === UmbElementVariantState.PUBLISHED ||
|
||||
this._externalContentVariantState === UmbElementVariantState.PUBLISHED_PENDING_CHANGES
|
||||
: this._localExpose;
|
||||
this.#updateBlockViewProps({ unpublished: !isExposed });
|
||||
this._exposed = isExposed;
|
||||
}
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
// element styling:
|
||||
@@ -313,33 +393,42 @@ export class UmbBlockSingleEntryElement extends UmbLitElement implements UmbProp
|
||||
};
|
||||
|
||||
#renderRefBlock() {
|
||||
return html`<umb-ref-single-block
|
||||
.label=${this._label}
|
||||
.icon=${this._icon}
|
||||
.unpublished=${!this._exposed}
|
||||
.config=${this._blockViewProps.config}
|
||||
.content=${this._blockViewProps.content}
|
||||
.settings=${this._blockViewProps.settings}
|
||||
${umbDestroyOnDisconnect()}></umb-ref-single-block>`;
|
||||
return html`
|
||||
<umb-ref-single-block
|
||||
.label=${this._label}
|
||||
.icon=${this._icon}
|
||||
.unpublished=${!this._exposed}
|
||||
.config=${this._blockViewProps.config}
|
||||
.content=${this._blockViewProps.content}
|
||||
.settings=${this._blockViewProps.settings}
|
||||
${umbDestroyOnDisconnect()}>
|
||||
</umb-ref-single-block>
|
||||
`;
|
||||
}
|
||||
|
||||
#renderInlineBlock() {
|
||||
return html`<umb-inline-single-block
|
||||
.label=${this._label}
|
||||
.icon=${this._icon}
|
||||
.unpublished=${!this._exposed}
|
||||
.config=${this._blockViewProps.config}
|
||||
.content=${this._blockViewProps.content}
|
||||
.settings=${this._blockViewProps.settings}
|
||||
${umbDestroyOnDisconnect()}></umb-inline-single-block>`;
|
||||
return html`
|
||||
<umb-inline-single-block
|
||||
.label=${this._label}
|
||||
.icon=${this._icon}
|
||||
.unpublished=${!this._exposed}
|
||||
.config=${this._blockViewProps.config}
|
||||
.content=${this._blockViewProps.content}
|
||||
.settings=${this._blockViewProps.settings}
|
||||
${umbDestroyOnDisconnect()}>
|
||||
</umb-inline-single-block>
|
||||
`;
|
||||
}
|
||||
|
||||
#renderUnsupportedBlock() {
|
||||
return html`<umb-unsupported-single-block
|
||||
.config=${this._blockViewProps.config}
|
||||
.content=${this._blockViewProps.content}
|
||||
.settings=${this._blockViewProps.settings}
|
||||
${umbDestroyOnDisconnect()}></umb-unsupported-single-block>`;
|
||||
return html`
|
||||
<umb-unsupported-single-block
|
||||
.config=${this._blockViewProps.config}
|
||||
.content=${this._blockViewProps.content}
|
||||
.settings=${this._blockViewProps.settings}
|
||||
${umbDestroyOnDisconnect()}>
|
||||
</umb-unsupported-single-block>
|
||||
`;
|
||||
}
|
||||
|
||||
#renderBuiltinBlockView = () => {
|
||||
@@ -356,6 +445,9 @@ export class UmbBlockSingleEntryElement extends UmbLitElement implements UmbProp
|
||||
return this.contentKey && (this._contentTypeAlias || this._unsupported)
|
||||
? html`
|
||||
<div class="umb-block-single__block">
|
||||
<umb-entity-frame>
|
||||
${when(this._isExternalContent, () => html`<uui-icon name="link"></uui-icon>`)} ${this._label}
|
||||
</umb-entity-frame>
|
||||
<umb-extension-slot
|
||||
type="blockEditorCustomView"
|
||||
default-element=${this._inlineEditingMode ? 'umb-inline-single-block' : 'umb-ref-single-block'}
|
||||
@@ -375,7 +467,7 @@ export class UmbBlockSingleEntryElement extends UmbLitElement implements UmbProp
|
||||
|
||||
#renderActionBar() {
|
||||
if (!this._showActions) return nothing;
|
||||
return html`<umb-block-action-list id="actions" block-editor=${UMB_BLOCK_SINGLE}></umb-block-action-list>`;
|
||||
return html`<umb-block-action-list id="actions" .blockEditor=${UMB_BLOCK_SINGLE}></umb-block-action-list>`;
|
||||
}
|
||||
|
||||
override render() {
|
||||
@@ -471,6 +563,21 @@ export class UmbBlockSingleEntryElement extends UmbLitElement implements UmbProp
|
||||
transition: opacity 50ms 16ms;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
:host([is-reference]) .umb-block-single__block {
|
||||
--umb-entity-frame-color: var(--umb-color-reference, #7532c8);
|
||||
--umb-entity-frame-contrast-color: var(--umb-color-reference-contrast, #ffffff);
|
||||
}
|
||||
|
||||
.umb-block-single__block {
|
||||
--umb-entity-frame-opacity: 0;
|
||||
--umb-entity-frame-color: var(--uui-color-interactive-emphasis);
|
||||
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
--umb-entity-frame-opacity: 1;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
+5
-4
@@ -37,10 +37,11 @@ export class UmbRefSingleBlockElement extends UmbLitElement {
|
||||
<umb-ufm-render slot="name" inline .markdown=${this.label} .value=${blockValue}></umb-ufm-render>
|
||||
${when(
|
||||
this.unpublished,
|
||||
() =>
|
||||
html`<uui-tag slot="name" look="secondary" title=${this.localize.term('blockEditor_notExposedDescription')}
|
||||
><umb-localize key="blockEditor_notExposedLabel"></umb-localize
|
||||
></uui-tag>`,
|
||||
() => html`
|
||||
<uui-tag slot="name" look="secondary" title=${this.localize.term('blockEditor_notExposedDescription')}>
|
||||
<umb-localize key="blockEditor_notExposedLabel"></umb-localize>
|
||||
</uui-tag>
|
||||
`,
|
||||
)}
|
||||
</uui-ref-node>
|
||||
`;
|
||||
|
||||
+7
-3
@@ -56,6 +56,7 @@ export class UmbBlockSingleEntriesContext extends UmbBlockEntriesContext<
|
||||
const valueResolver = new UmbClipboardPastePropertyValueTranslatorValueResolver(this);
|
||||
|
||||
const blockTypes = this._manager.getBlockTypes() ?? [];
|
||||
const libraryAllowedElementTypeKeys = await this._getLibraryAllowedElementTypeKeys(blockTypes);
|
||||
|
||||
const configuredSize = this._manager
|
||||
.getEditorConfiguration()
|
||||
@@ -73,6 +74,7 @@ export class UmbBlockSingleEntriesContext extends UmbBlockEntriesContext<
|
||||
blocks: blockTypes,
|
||||
blockGroups: [],
|
||||
openClipboard: routingInfo.view === 'clipboard',
|
||||
libraryAllowedElementTypeKeys,
|
||||
clipboardFilter: async (clipboardEntryDetail) => {
|
||||
const hasSupportedPasteTranslator = clipboardContext.hasSupportedPasteTranslator(
|
||||
pasteTranslatorManifests,
|
||||
@@ -104,7 +106,7 @@ export class UmbBlockSingleEntriesContext extends UmbBlockEntriesContext<
|
||||
};
|
||||
})
|
||||
.onSubmit(async (value, data) => {
|
||||
if (value?.create && data) {
|
||||
if (value && 'create' in value && data) {
|
||||
const created = await this.create(
|
||||
value.create.contentElementTypeKey,
|
||||
{},
|
||||
@@ -120,7 +122,9 @@ export class UmbBlockSingleEntriesContext extends UmbBlockEntriesContext<
|
||||
} else {
|
||||
throw new Error('Failed to create block');
|
||||
}
|
||||
} else if (value?.clipboard && value.clipboard.selection?.length && data) {
|
||||
} else if (value && 'library' in value) {
|
||||
this._manager?.insertExternalContent(value.library.elementKey);
|
||||
} else if (value && 'clipboard' in value && value.clipboard.selection?.length && data) {
|
||||
const clipboardContext = await this.getContext(UMB_CLIPBOARD_PROPERTY_CONTEXT);
|
||||
if (!clipboardContext) {
|
||||
throw new Error('Clipboard context not found');
|
||||
@@ -196,7 +200,7 @@ export class UmbBlockSingleEntriesContext extends UmbBlockEntriesContext<
|
||||
|
||||
async create(
|
||||
contentElementTypeKey: string,
|
||||
partialLayoutEntry?: Omit<UmbBlockSingleLayoutModel, 'contentKey'>,
|
||||
partialLayoutEntry?: Omit<UmbBlockSingleLayoutModel, 'contentKey' | 'key'>,
|
||||
originData?: UmbBlockSingleWorkspaceOriginData,
|
||||
) {
|
||||
await this._retrieveManager;
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ export class UmbBlockSingleManagerContext<
|
||||
*/
|
||||
async createWithPresets(
|
||||
contentElementTypeKey: string,
|
||||
partialLayoutEntry?: Omit<BlockLayoutType, 'contentKey'>,
|
||||
partialLayoutEntry?: Omit<BlockLayoutType, 'contentKey' | 'key'>,
|
||||
// This property is used by some implementations, but not used in this. Do not remove. [NL]
|
||||
|
||||
_originData?: UmbBlockSingleWorkspaceOriginData,
|
||||
|
||||
+4
-8
@@ -41,10 +41,10 @@ import { UMB_VARIANT_CONTEXT } from '@umbraco-cms/backoffice/variant';
|
||||
|
||||
const SORTER_CONFIG: UmbSorterConfig<UmbBlockSingleLayoutModel, UmbBlockSingleEntryElement> = {
|
||||
getUniqueOfElement: (element) => {
|
||||
return element.contentKey!;
|
||||
return element.key!;
|
||||
},
|
||||
getUniqueOfModel: (modelEntry) => {
|
||||
return modelEntry.contentKey;
|
||||
return modelEntry.key;
|
||||
},
|
||||
//identifier: 'block-single-editor',
|
||||
itemSelector: 'umb-block-single-entry',
|
||||
@@ -395,13 +395,9 @@ export class UmbPropertyEditorUIBlockSingleElement
|
||||
return html`
|
||||
${repeat(
|
||||
this._layouts,
|
||||
(x) => x.contentKey,
|
||||
(x) => x.key,
|
||||
(layoutEntry) => html`
|
||||
<umb-block-single-entry
|
||||
.contentKey=${layoutEntry.contentKey}
|
||||
.layout=${layoutEntry}
|
||||
${umbDestroyOnDisconnect()}>
|
||||
</umb-block-single-entry>
|
||||
<umb-block-single-entry .layout=${layoutEntry} ${umbDestroyOnDisconnect()}></umb-block-single-entry>
|
||||
`,
|
||||
)}
|
||||
${this.#renderCreateButtonGroup()}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { UmbBlockActionArgs } from './types.js';
|
||||
import type { UmbAction } from '@umbraco-cms/backoffice/action';
|
||||
import type { Observable } from '@umbraco-cms/backoffice/external/rxjs';
|
||||
|
||||
export interface UmbBlockAction<ArgsMetaType> extends UmbAction<UmbBlockActionArgs<ArgsMetaType>> {
|
||||
/**
|
||||
@@ -9,6 +10,13 @@ export interface UmbBlockAction<ArgsMetaType> extends UmbAction<UmbBlockActionAr
|
||||
*/
|
||||
getHref(): Promise<string | undefined>;
|
||||
|
||||
/**
|
||||
* An optional reactive observable for the href location.
|
||||
* When provided, the default kind element subscribes to it and updates the link reactively,
|
||||
* rather than resolving `getHref()` once at initialisation time.
|
||||
*/
|
||||
href?: Observable<string | undefined>;
|
||||
|
||||
/**
|
||||
* The `execute` method, the action will act as a button.
|
||||
* @returns {Promise<void>}
|
||||
@@ -22,4 +30,12 @@ export interface UmbBlockAction<ArgsMetaType> extends UmbAction<UmbBlockActionAr
|
||||
* @returns {Promise<string | undefined>}
|
||||
*/
|
||||
getValidationDataPath(): Promise<string | undefined>;
|
||||
|
||||
/**
|
||||
* An optional reactive observable for the validation data path.
|
||||
* When provided, the default kind element subscribes to it and updates the validation
|
||||
* state controller reactively, rather than resolving `getValidationDataPath()` once at
|
||||
* initialisation time.
|
||||
*/
|
||||
validationDataPath?: Observable<string | undefined>;
|
||||
}
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const UMB_BLOCK_ACTION_DISCONNECT_FROM_ELEMENT_LIBRARY_ALIAS = 'Umb.BlockAction.DisconnectFromElementLibrary';
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import type { MetaBlockActionDefaultKind } from '../../default/types.js';
|
||||
import { UmbBlockActionBase } from '../../block-action-base.js';
|
||||
import { UMB_BLOCK_ENTRY_CONTEXT } from '../../../context/block-entry.context-token.js';
|
||||
|
||||
export class UmbDisconnectFromElementLibraryBlockAction extends UmbBlockActionBase<MetaBlockActionDefaultKind> {
|
||||
override async execute() {
|
||||
const context = await this.getContext(UMB_BLOCK_ENTRY_CONTEXT);
|
||||
await context?.requestDisconnectFromExternalContent();
|
||||
}
|
||||
}
|
||||
|
||||
export { UmbDisconnectFromElementLibraryBlockAction as api };
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import {
|
||||
UMB_BLOCK_ENTRY_HAS_EXTERNAL_CONTENT_CONDITION_ALIAS,
|
||||
UMB_BLOCK_ENTRY_IS_READ_ONLY_CONDITION_ALIAS,
|
||||
} from '../../../conditions/constants.js';
|
||||
import { UMB_BLOCK_ACTION_DISCONNECT_FROM_ELEMENT_LIBRARY_ALIAS } from './constants.js';
|
||||
|
||||
export const manifests: Array<UmbExtensionManifest> = [
|
||||
{
|
||||
type: 'blockAction',
|
||||
kind: 'default',
|
||||
alias: UMB_BLOCK_ACTION_DISCONNECT_FROM_ELEMENT_LIBRARY_ALIAS,
|
||||
name: 'Disconnect Block From Element Library Action',
|
||||
weight: 250,
|
||||
api: () => import('./disconnect-from-element-library-block.action.js'),
|
||||
meta: {
|
||||
icon: 'icon-unlink',
|
||||
label: '#blockEditor_disconnectFromElementLibrary',
|
||||
},
|
||||
conditions: [
|
||||
{
|
||||
alias: UMB_BLOCK_ENTRY_IS_READ_ONLY_CONDITION_ALIAS,
|
||||
match: false,
|
||||
},
|
||||
{
|
||||
alias: UMB_BLOCK_ENTRY_HAS_EXTERNAL_CONTENT_CONDITION_ALIAS,
|
||||
match: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
+23
-11
@@ -4,16 +4,19 @@ import { UmbBlockActionBase } from '../../block-action-base.js';
|
||||
import { UmbDataPathBlockElementDataQuery } from '../../../validation/data-path-element-data-query.function.js';
|
||||
import { UMB_BLOCK_ENTRY_CONTEXT } from '../../../context/block-entry.context-token.js';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UmbStringState } from '@umbraco-cms/backoffice/observable-api';
|
||||
|
||||
/**
|
||||
* Block action that navigates to the block's content editor workspace.
|
||||
* Exposes the workspace edit path via `getHref()` and the content validation data path via `getValidationDataPath()`.
|
||||
*/
|
||||
/** Block action that navigates to the block's content editor workspace. */
|
||||
export class UmbEditContentBlockAction extends UmbBlockActionBase<MetaBlockActionDefaultKind> {
|
||||
#context?: typeof UMB_BLOCK_ENTRY_CONTEXT.TYPE;
|
||||
#contextReady: Promise<void>;
|
||||
#resolveContext!: () => void;
|
||||
|
||||
readonly #href = new UmbStringState(undefined);
|
||||
readonly href = this.#href.asObservable();
|
||||
|
||||
readonly #validationDataPath = new UmbStringState(undefined);
|
||||
readonly validationDataPath = this.#validationDataPath.asObservable();
|
||||
|
||||
constructor(host: UmbControllerHost, args: UmbBlockActionArgs<MetaBlockActionDefaultKind>) {
|
||||
super(host, args);
|
||||
|
||||
@@ -22,22 +25,31 @@ export class UmbEditContentBlockAction extends UmbBlockActionBase<MetaBlockActio
|
||||
});
|
||||
|
||||
this.consumeContext(UMB_BLOCK_ENTRY_CONTEXT, (context) => {
|
||||
this.#context = context;
|
||||
if (!context) return;
|
||||
this.#resolveContext();
|
||||
|
||||
this.observe(context.workspaceEditContentPath, (path) => this.#href.setValue(path || undefined), 'observeHref');
|
||||
|
||||
this.observe(
|
||||
context.contentKey,
|
||||
(contentKey) => {
|
||||
this.#validationDataPath.setValue(
|
||||
contentKey ? `$.contentData[${UmbDataPathBlockElementDataQuery({ key: contentKey })}]` : undefined,
|
||||
);
|
||||
},
|
||||
'observeValidationDataPath',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
override async getHref() {
|
||||
await this.#contextReady;
|
||||
const path = await this.observe(this.#context?.workspaceEditContentPath)?.asPromise();
|
||||
return path || undefined;
|
||||
return (await this.observe(this.href)?.asPromise()) || undefined;
|
||||
}
|
||||
|
||||
override async getValidationDataPath() {
|
||||
await this.#contextReady;
|
||||
const contentKey = await this.observe(this.#context?.contentKey)?.asPromise();
|
||||
if (!contentKey) return undefined;
|
||||
return `$.contentData[${UmbDataPathBlockElementDataQuery({ key: contentKey })}]`;
|
||||
return await this.observe(this.validationDataPath)?.asPromise();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+23
-11
@@ -3,17 +3,20 @@ import type { UmbBlockActionArgs } from '../../types.js';
|
||||
import { UmbBlockActionBase } from '../../block-action-base.js';
|
||||
import { UmbDataPathBlockElementDataQuery } from '../../../validation/data-path-element-data-query.function.js';
|
||||
import { UMB_BLOCK_ENTRY_CONTEXT } from '../../../context/block-entry.context-token.js';
|
||||
import { UmbStringState } from '@umbraco-cms/backoffice/observable-api';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
|
||||
/**
|
||||
* Block action that navigates to the block's settings editor workspace.
|
||||
* Exposes the workspace edit path via `getHref()` and the settings validation data path via `getValidationDataPath()`.
|
||||
*/
|
||||
/** Block action that navigates to the block's settings editor workspace. */
|
||||
export class UmbEditSettingsBlockAction extends UmbBlockActionBase<MetaBlockActionDefaultKind> {
|
||||
#context?: typeof UMB_BLOCK_ENTRY_CONTEXT.TYPE;
|
||||
#contextReady: Promise<void>;
|
||||
#resolveContext!: () => void;
|
||||
|
||||
readonly #href = new UmbStringState(undefined);
|
||||
readonly href = this.#href.asObservable();
|
||||
|
||||
readonly #validationDataPath = new UmbStringState(undefined);
|
||||
readonly validationDataPath = this.#validationDataPath.asObservable();
|
||||
|
||||
constructor(host: UmbControllerHost, args: UmbBlockActionArgs<MetaBlockActionDefaultKind>) {
|
||||
super(host, args);
|
||||
|
||||
@@ -22,22 +25,31 @@ export class UmbEditSettingsBlockAction extends UmbBlockActionBase<MetaBlockActi
|
||||
});
|
||||
|
||||
this.consumeContext(UMB_BLOCK_ENTRY_CONTEXT, (context) => {
|
||||
this.#context = context;
|
||||
if (!context) return;
|
||||
this.#resolveContext();
|
||||
|
||||
this.observe(context.workspaceEditSettingsPath, (path) => this.#href.setValue(path || undefined), 'observeHref');
|
||||
|
||||
this.observe(
|
||||
context.settingsKey,
|
||||
(settingsKey) => {
|
||||
this.#validationDataPath.setValue(
|
||||
settingsKey ? `$.settingsData[${UmbDataPathBlockElementDataQuery({ key: settingsKey })}]` : undefined,
|
||||
);
|
||||
},
|
||||
'observeValidationDataPath',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
override async getHref() {
|
||||
await this.#contextReady;
|
||||
const path = await this.observe(this.#context?.workspaceEditSettingsPath)?.asPromise();
|
||||
return path || undefined;
|
||||
return (await this.observe(this.href)?.asPromise()) || undefined;
|
||||
}
|
||||
|
||||
override async getValidationDataPath() {
|
||||
await this.#contextReady;
|
||||
const settingsKey = await this.observe(this.#context?.settingsKey)?.asPromise();
|
||||
if (!settingsKey) return undefined;
|
||||
return `$.settingsData[${UmbDataPathBlockElementDataQuery({ key: settingsKey })}]`;
|
||||
return await this.observe(this.validationDataPath)?.asPromise();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const UMB_BLOCK_ACTION_TRANSFER_TO_ELEMENT_LIBRARY_ALIAS = 'Umb.BlockAction.TransferToElementLibrary';
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import {
|
||||
UMB_BLOCK_ENTRY_HAS_EXTERNAL_CONTENT_CONDITION_ALIAS,
|
||||
UMB_BLOCK_ENTRY_IS_READ_ONLY_CONDITION_ALIAS,
|
||||
} from '../../../conditions/constants.js';
|
||||
import { UMB_BLOCK_ACTION_TRANSFER_TO_ELEMENT_LIBRARY_ALIAS } from './constants.js';
|
||||
|
||||
export const manifests: Array<UmbExtensionManifest> = [
|
||||
{
|
||||
type: 'blockAction',
|
||||
kind: 'default',
|
||||
alias: UMB_BLOCK_ACTION_TRANSFER_TO_ELEMENT_LIBRARY_ALIAS,
|
||||
name: 'Transfer Block To Element Library Action',
|
||||
weight: 250,
|
||||
api: () => import('./transfer-to-element-library-block.action.js'),
|
||||
meta: {
|
||||
icon: 'icon-link',
|
||||
label: '#blockEditor_transferToElementLibrary',
|
||||
},
|
||||
conditions: [
|
||||
{
|
||||
alias: UMB_BLOCK_ENTRY_IS_READ_ONLY_CONDITION_ALIAS,
|
||||
match: false,
|
||||
},
|
||||
{
|
||||
alias: UMB_BLOCK_ENTRY_HAS_EXTERNAL_CONTENT_CONDITION_ALIAS,
|
||||
match: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import type { MetaBlockActionDefaultKind } from '../../default/types.js';
|
||||
import { UmbBlockActionBase } from '../../block-action-base.js';
|
||||
import { UMB_BLOCK_ENTRY_CONTEXT } from '../../../context/block-entry.context-token.js';
|
||||
|
||||
export class UmbTransferToElementLibraryBlockAction extends UmbBlockActionBase<MetaBlockActionDefaultKind> {
|
||||
override async execute() {
|
||||
const context = await this.getContext(UMB_BLOCK_ENTRY_CONTEXT);
|
||||
await context?.requestTransferToExternalContent();
|
||||
}
|
||||
}
|
||||
|
||||
export { UmbTransferToElementLibraryBlockAction as api };
|
||||
+38
-16
@@ -29,23 +29,45 @@ export class UmbBlockActionDefaultElement<
|
||||
this.#api = api;
|
||||
this._href = undefined;
|
||||
|
||||
// TODO: getHref() and getValidationDataPath() resolve once. If the underlying observable values
|
||||
// change, the button won't update. Consider making these reactive in a future iteration. [LK]
|
||||
this.#api?.getHref?.().then((href) => {
|
||||
this._href = href;
|
||||
});
|
||||
if (api?.href) {
|
||||
this.observe(api.href, (href) => (this._href = href), 'observeHref');
|
||||
} else {
|
||||
this.removeUmbControllerByAlias('observeHref');
|
||||
api?.getHref?.().then((href) => {
|
||||
this._href = href;
|
||||
});
|
||||
}
|
||||
|
||||
this.#api?.getValidationDataPath?.().then((path) => {
|
||||
this.removeUmbControllerByAlias('observeValidation');
|
||||
if (path) {
|
||||
new UmbObserveValidationStateController(
|
||||
this,
|
||||
path,
|
||||
(hasMessages) => (this._invalid = hasMessages),
|
||||
'observeValidation',
|
||||
);
|
||||
}
|
||||
});
|
||||
if (api?.validationDataPath) {
|
||||
this.observe(
|
||||
api.validationDataPath,
|
||||
(path) => {
|
||||
this.removeUmbControllerByAlias('observeValidation');
|
||||
if (path) {
|
||||
new UmbObserveValidationStateController(
|
||||
this,
|
||||
path,
|
||||
(hasMessages) => (this._invalid = hasMessages),
|
||||
'observeValidation',
|
||||
);
|
||||
}
|
||||
},
|
||||
'observeValidationDataPath',
|
||||
);
|
||||
} else {
|
||||
this.removeUmbControllerByAlias('observeValidationDataPath');
|
||||
api?.getValidationDataPath?.().then((path) => {
|
||||
this.removeUmbControllerByAlias('observeValidation');
|
||||
if (path) {
|
||||
new UmbObserveValidationStateController(
|
||||
this,
|
||||
path,
|
||||
(hasMessages) => (this._invalid = hasMessages),
|
||||
'observeValidation',
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@state()
|
||||
|
||||
@@ -2,9 +2,11 @@ export * from './block-action-base.js';
|
||||
export * from './block-action-list.element.js';
|
||||
export * from './common/copy-to-clipboard/constants.js';
|
||||
export * from './common/delete/constants.js';
|
||||
export * from './common/disconnect-from-element-library/constants.js';
|
||||
export * from './common/edit-content/constants.js';
|
||||
export * from './common/edit-settings/constants.js';
|
||||
export * from './common/expose-content/constants.js';
|
||||
export * from './common/transfer-to-element-library/constants.js';
|
||||
export * from './default/default.action.kind.js';
|
||||
|
||||
export type * from './types.js';
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { manifests as copyToClipboardManifests } from './common/copy-to-clipboard/manifests.js';
|
||||
import { manifests as deleteManifests } from './common/delete/manifests.js';
|
||||
import { manifests as disconnectFromElementLibraryManifests } from './common/disconnect-from-element-library/manifests.js';
|
||||
import { manifests as editContentManifests } from './common/edit-content/manifests.js';
|
||||
import { manifests as editSettingsManifests } from './common/edit-settings/manifests.js';
|
||||
import { manifests as exposeContentManifests } from './common/expose-content/manifests.js';
|
||||
import { manifests as transferToElementLibraryManifests } from './common/transfer-to-element-library/manifests.js';
|
||||
import { manifests as defaultKindManifests } from './default/manifests.js';
|
||||
import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry';
|
||||
|
||||
@@ -10,7 +12,9 @@ export const manifests: Array<UmbExtensionManifest | UmbExtensionManifestKind> =
|
||||
...defaultKindManifests,
|
||||
...copyToClipboardManifests,
|
||||
...deleteManifests,
|
||||
...disconnectFromElementLibraryManifests,
|
||||
...editContentManifests,
|
||||
...editSettingsManifests,
|
||||
...exposeContentManifests,
|
||||
...transferToElementLibraryManifests,
|
||||
];
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { UMB_BLOCK_ENTRY_CONTEXT } from '../context/block-entry.context-token.js';
|
||||
import type { BlockEntryHasExternalContentConditionConfig } from './types.js';
|
||||
import { UmbConditionBase } from '@umbraco-cms/backoffice/extension-registry';
|
||||
import type { UmbConditionControllerArguments, UmbExtensionCondition } from '@umbraco-cms/backoffice/extension-api';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
|
||||
export class UmbBlockEntryHasExternalContentCondition
|
||||
extends UmbConditionBase<BlockEntryHasExternalContentConditionConfig>
|
||||
implements UmbExtensionCondition
|
||||
{
|
||||
constructor(
|
||||
host: UmbControllerHost,
|
||||
args: UmbConditionControllerArguments<BlockEntryHasExternalContentConditionConfig>,
|
||||
) {
|
||||
super(host, args);
|
||||
|
||||
this.consumeContext(UMB_BLOCK_ENTRY_CONTEXT, (context) => {
|
||||
if (!context) return;
|
||||
this.observe(
|
||||
context.isExternalContent,
|
||||
(isExternalContent) => {
|
||||
this.permitted = isExternalContent === (this.config.match !== undefined ? this.config.match : true);
|
||||
},
|
||||
'observeIsExternalContent',
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default UmbBlockEntryHasExternalContentCondition;
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { UMB_BLOCK_WORKSPACE_CONTEXT } from '../workspace/block-workspace.context-token.js';
|
||||
import type { BlockWorkspaceHasContentConditionConfig } from './types.js';
|
||||
import { UmbConditionBase } from '@umbraco-cms/backoffice/extension-registry';
|
||||
import type { UmbConditionControllerArguments, UmbExtensionCondition } from '@umbraco-cms/backoffice/extension-api';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
|
||||
export class UmbBlockWorkspaceHasContentCondition
|
||||
extends UmbConditionBase<BlockWorkspaceHasContentConditionConfig>
|
||||
implements UmbExtensionCondition
|
||||
{
|
||||
constructor(
|
||||
host: UmbControllerHost,
|
||||
args: UmbConditionControllerArguments<BlockWorkspaceHasContentConditionConfig>,
|
||||
) {
|
||||
super(host, args);
|
||||
|
||||
this.consumeContext(UMB_BLOCK_WORKSPACE_CONTEXT, (context) => {
|
||||
this.observe(
|
||||
context?.hasContent,
|
||||
(hasContent) => {
|
||||
this.permitted = hasContent === true;
|
||||
},
|
||||
'observeHasContent',
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default UmbBlockWorkspaceHasContentCondition;
|
||||
@@ -1,4 +1,6 @@
|
||||
export const UMB_BLOCK_ENTRY_HAS_SETTINGS_CONDITION_ALIAS = 'Umb.Condition.BlockEntryHasSettings';
|
||||
export const UMB_BLOCK_ENTRY_IS_EXPOSED_CONDITION_ALIAS = 'Umb.Condition.BlockEntryIsExposed';
|
||||
export const UMB_BLOCK_ENTRY_HAS_EXTERNAL_CONTENT_CONDITION_ALIAS = 'Umb.Condition.BlockEntryHasExternalContent';
|
||||
export const UMB_BLOCK_ENTRY_IS_READ_ONLY_CONDITION_ALIAS = 'Umb.Condition.BlockEntryIsReadOnly';
|
||||
export const UMB_BLOCK_ENTRY_SHOW_CONTENT_EDIT_CONDITION_ALIAS = 'Umb.Condition.BlockEntryShowContentEdit';
|
||||
export const UMB_BLOCK_WORKSPACE_HAS_CONTENT_CONDITION_ALIAS = 'Umb.Condition.BlockWorkspaceHasContent';
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import {
|
||||
UMB_BLOCK_ENTRY_HAS_SETTINGS_CONDITION_ALIAS,
|
||||
UMB_BLOCK_ENTRY_HAS_EXTERNAL_CONTENT_CONDITION_ALIAS,
|
||||
UMB_BLOCK_ENTRY_IS_EXPOSED_CONDITION_ALIAS,
|
||||
UMB_BLOCK_ENTRY_IS_READ_ONLY_CONDITION_ALIAS,
|
||||
UMB_BLOCK_WORKSPACE_HAS_CONTENT_CONDITION_ALIAS,
|
||||
} from './constants.js';
|
||||
import UmbBlockEntryHasSettingsCondition from './block-entry-has-settings.condition.js';
|
||||
import UmbBlockEntryHasExternalContentCondition from './block-entry-has-external-content.condition.js';
|
||||
import UmbBlockEntryIsExposedCondition from './block-entry-is-exposed.condition.js';
|
||||
import UmbBlockEntryIsReadOnlyCondition from './block-entry-is-read-only.condition.js';
|
||||
import UmbBlockEntryShowContentEditCondition from './block-entry-show-content-edit.condition.js';
|
||||
import UmbBlockWorkspaceHasContentCondition from './block-workspace-has-content.condition.js';
|
||||
import UmbBlockWorkspaceHasSettingsCondition from './block-workspace-has-settings.condition.js';
|
||||
import UmbBlockWorkspaceIsExposedCondition from './block-workspace-is-exposed.condition.js';
|
||||
import UmbBlockWorkspaceIsReadOnlyCondition from './block-workspace-is-readonly.condition.js';
|
||||
@@ -19,6 +23,12 @@ export const manifests: Array<ManifestCondition> = [
|
||||
alias: 'Umb.Condition.BlockWorkspaceHasSettings',
|
||||
api: UmbBlockWorkspaceHasSettingsCondition,
|
||||
},
|
||||
{
|
||||
type: 'condition',
|
||||
name: 'Block Workspace Has Content Condition',
|
||||
alias: UMB_BLOCK_WORKSPACE_HAS_CONTENT_CONDITION_ALIAS,
|
||||
api: UmbBlockWorkspaceHasContentCondition,
|
||||
},
|
||||
{
|
||||
type: 'condition',
|
||||
name: 'Block Show Content Edit Condition',
|
||||
@@ -49,6 +59,12 @@ export const manifests: Array<ManifestCondition> = [
|
||||
alias: UMB_BLOCK_ENTRY_IS_EXPOSED_CONDITION_ALIAS,
|
||||
api: UmbBlockEntryIsExposedCondition,
|
||||
},
|
||||
{
|
||||
type: 'condition',
|
||||
name: 'Block Entry Has External Content Condition',
|
||||
alias: UMB_BLOCK_ENTRY_HAS_EXTERNAL_CONTENT_CONDITION_ALIAS,
|
||||
api: UmbBlockEntryHasExternalContentCondition,
|
||||
},
|
||||
{
|
||||
type: 'condition',
|
||||
name: 'Block Entry Is ReadOnly Condition',
|
||||
|
||||
@@ -4,37 +4,40 @@ import type { UmbConditionConfigBase } from '@umbraco-cms/backoffice/extension-a
|
||||
export type BlockWorkspaceHasSettingsConditionConfig =
|
||||
UmbConditionConfigBase<'Umb.Condition.BlockWorkspaceHasSettings'>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-object-type
|
||||
export interface BlockWorkspaceHasContentConditionConfig extends UmbConditionConfigBase<'Umb.Condition.BlockWorkspaceHasContent'> {}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export interface BlockEntryShowContentEditConditionConfig
|
||||
extends UmbConditionConfigBase<'Umb.Condition.BlockEntryShowContentEdit'> {
|
||||
export interface BlockEntryShowContentEditConditionConfig extends UmbConditionConfigBase<'Umb.Condition.BlockEntryShowContentEdit'> {
|
||||
match?: boolean;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export interface BlockEntryIsExposedConditionConfig
|
||||
extends UmbConditionConfigBase<'Umb.Condition.BlockWorkspaceIsExposed'> {
|
||||
export interface BlockEntryIsExposedConditionConfig extends UmbConditionConfigBase<'Umb.Condition.BlockWorkspaceIsExposed'> {
|
||||
match?: boolean;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export interface BlockWorkspaceIsReadOnlyConditionConfig
|
||||
extends UmbConditionConfigBase<'Umb.Condition.BlockWorkspaceIsReadOnly'> {
|
||||
export interface BlockWorkspaceIsReadOnlyConditionConfig extends UmbConditionConfigBase<'Umb.Condition.BlockWorkspaceIsReadOnly'> {
|
||||
match?: boolean;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-object-type
|
||||
export interface BlockEntryHasSettingsConditionConfig extends UmbConditionConfigBase<'Umb.Condition.BlockEntryHasSettings'> {}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export interface BlockEntryHasExternalContentConditionConfig extends UmbConditionConfigBase<'Umb.Condition.BlockEntryHasExternalContent'> {
|
||||
match?: boolean;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export type BlockEntryHasSettingsConditionConfig = UmbConditionConfigBase<'Umb.Condition.BlockEntryHasSettings'>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export interface BlockEntryIsReadOnlyConditionConfig
|
||||
extends UmbConditionConfigBase<'Umb.Condition.BlockEntryIsReadOnly'> {
|
||||
export interface BlockEntryIsReadOnlyConditionConfig extends UmbConditionConfigBase<'Umb.Condition.BlockEntryIsReadOnly'> {
|
||||
match?: boolean;
|
||||
}
|
||||
|
||||
// NOTE: Named with a `Umb` prefix, as clashed with `BlockEntryIsExposedConditionConfig`,
|
||||
// but that one is a misnomer as the condition targets the block workspace. [LK]
|
||||
export interface UmbBlockEntryIsExposedConditionConfig
|
||||
extends UmbConditionConfigBase<'Umb.Condition.BlockEntryIsExposed'> {
|
||||
export interface UmbBlockEntryIsExposedConditionConfig extends UmbConditionConfigBase<'Umb.Condition.BlockEntryIsExposed'> {
|
||||
match?: boolean;
|
||||
}
|
||||
|
||||
@@ -43,7 +46,9 @@ declare global {
|
||||
umbBlock:
|
||||
| BlockEntryShowContentEditConditionConfig
|
||||
| BlockWorkspaceHasSettingsConditionConfig
|
||||
| BlockWorkspaceHasContentConditionConfig
|
||||
| BlockEntryIsExposedConditionConfig
|
||||
| BlockEntryHasExternalContentConditionConfig
|
||||
| BlockWorkspaceIsReadOnlyConditionConfig
|
||||
| BlockEntryIsReadOnlyConditionConfig
|
||||
| BlockEntryHasSettingsConditionConfig
|
||||
|
||||
+41
-10
@@ -4,6 +4,7 @@ import type { UmbBlockDataObjectModel, UmbBlockManagerContext } from './block-ma
|
||||
import { UMB_BLOCK_ENTRIES_CONTEXT } from './block-entries.context-token.js';
|
||||
import { type Observable, UmbArrayState, UmbBasicState, UmbStringState } from '@umbraco-cms/backoffice/observable-api';
|
||||
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
|
||||
import { UmbElementTypeStructureRepository } from '@umbraco-cms/backoffice/element';
|
||||
import type { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import type { UmbModalRouteBuilder } from '@umbraco-cms/backoffice/router';
|
||||
@@ -30,7 +31,7 @@ export abstract class UmbBlockEntriesContext<
|
||||
|
||||
public abstract readonly canCreate: Observable<boolean>;
|
||||
|
||||
protected _layoutEntries = new UmbArrayState<BlockLayoutType>([], (x) => x.contentKey);
|
||||
protected _layoutEntries = new UmbArrayState<BlockLayoutType>([], (x) => x.key);
|
||||
readonly layoutEntries = this._layoutEntries.asObservable();
|
||||
readonly layoutEntriesLength = this._layoutEntries.asObservablePart((x) => x.length);
|
||||
|
||||
@@ -72,9 +73,15 @@ export abstract class UmbBlockEntriesContext<
|
||||
layoutOf(contentKey: string) {
|
||||
return this._layoutEntries.asObservablePart((source) => source.find((x) => x.contentKey === contentKey));
|
||||
}
|
||||
byKey(key: string) {
|
||||
return this._layoutEntries.asObservablePart((source) => source.find((x) => x.key === key));
|
||||
}
|
||||
getLayoutOf(contentKey: string) {
|
||||
return this._layoutEntries.getValue().find((x) => x.contentKey === contentKey);
|
||||
}
|
||||
getByKey(key: string) {
|
||||
return this._layoutEntries.getValue().find((x) => x.key === key);
|
||||
}
|
||||
setLayouts(layouts: Array<BlockLayoutType>) {
|
||||
return this._layoutEntries.setValue(layouts);
|
||||
}
|
||||
@@ -85,9 +92,21 @@ export abstract class UmbBlockEntriesContext<
|
||||
public abstract getPathForCreateBlock(index: number): string | undefined;
|
||||
public abstract getPathForClipboard(index: number): string | undefined;
|
||||
|
||||
/**
|
||||
* Returns the element type uniques allowed at the library root that overlap
|
||||
* with the given block types — used to filter the library picker in the
|
||||
* block catalogue modal.
|
||||
*/
|
||||
protected async _getLibraryAllowedElementTypeKeys(blockTypes: Array<BlockType>): Promise<Array<string>> {
|
||||
const blockTypeKeys = new Set(blockTypes.map((bt) => bt.contentElementTypeKey));
|
||||
const repo = new UmbElementTypeStructureRepository(this);
|
||||
const { data: allowedTypes } = await repo.requestAllowedChildrenOf(null, null);
|
||||
return allowedTypes?.items.filter((t) => t.unique && blockTypeKeys.has(t.unique)).map((t) => t.unique!) ?? [];
|
||||
}
|
||||
|
||||
public abstract create(
|
||||
contentElementTypeKey: string,
|
||||
layoutEntry?: Omit<BlockLayoutType, 'contentKey'>,
|
||||
layoutEntry?: Omit<BlockLayoutType, 'contentKey' | 'key'>,
|
||||
originData?: BlockOriginData,
|
||||
): Promise<UmbBlockDataObjectModel<BlockLayoutType> | undefined>;
|
||||
|
||||
@@ -98,19 +117,25 @@ export abstract class UmbBlockEntriesContext<
|
||||
originData: BlockOriginData,
|
||||
): Promise<boolean>;
|
||||
|
||||
public async delete(contentKey: string) {
|
||||
public async delete(key: string) {
|
||||
await this._retrieveManager;
|
||||
const layout = this._layoutEntries.value.find((x) => x.contentKey === contentKey);
|
||||
const layout = this._layoutEntries.value.find((x) => x.key === key);
|
||||
if (!layout) {
|
||||
throw new Error(`Cannot delete block, missing layout for ${contentKey}`);
|
||||
throw new Error(`Cannot delete block, missing layout for ${key}`);
|
||||
}
|
||||
this._layoutEntries.removeOne(contentKey);
|
||||
this._layoutEntries.removeOne(key);
|
||||
|
||||
this._manager!.removeOneContent(contentKey);
|
||||
if (layout.settingsKey) {
|
||||
this._manager!.removeOneSettings(layout.settingsKey);
|
||||
// Only remove content/settings/exposes if no other layout references the same contentKey
|
||||
const hasOtherReference = this._layoutEntries.value.some(
|
||||
(x) => x.key !== key && x.contentKey === layout.contentKey,
|
||||
);
|
||||
if (!hasOtherReference) {
|
||||
this._manager!.removeOneContent(layout.contentKey);
|
||||
if (layout.settingsKey) {
|
||||
this._manager!.removeOneSettings(layout.settingsKey);
|
||||
}
|
||||
this._manager!.removeExposesOf(layout.contentKey);
|
||||
}
|
||||
this._manager!.removeExposesOf(contentKey);
|
||||
}
|
||||
|
||||
// insert/paste from property value methods:
|
||||
@@ -133,6 +158,12 @@ export abstract class UmbBlockEntriesContext<
|
||||
value: UmbBlockValueType,
|
||||
originData: BlockOriginData,
|
||||
) {
|
||||
// External-content references have no inline contentData — insert as a reference instead.
|
||||
if (layoutEntry.isExternalContent) {
|
||||
await this._manager?.insertExternalContent(layoutEntry.contentKey, originData);
|
||||
return;
|
||||
}
|
||||
|
||||
const content = value.contentData.find((x) => x.key === layoutEntry.contentKey);
|
||||
if (!content) {
|
||||
throw new Error('No content found for layout entry');
|
||||
|
||||
+117
-19
@@ -17,12 +17,14 @@ import {
|
||||
mergeObservables,
|
||||
observeMultiple,
|
||||
} from '@umbraco-cms/backoffice/observable-api';
|
||||
import { encodeFilePath, UmbReadOnlyVariantGuardManager } from '@umbraco-cms/backoffice/utils';
|
||||
import { encodeFilePath, UmbDeprecation, UmbReadOnlyVariantGuardManager } from '@umbraco-cms/backoffice/utils';
|
||||
import { umbConfirmModal } from '@umbraco-cms/backoffice/modal';
|
||||
import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api';
|
||||
import { UmbRoutePathAddendumContext } from '@umbraco-cms/backoffice/router';
|
||||
import { UmbModalRouteRegistrationController, UmbRoutePathAddendumContext } from '@umbraco-cms/backoffice/router';
|
||||
import { UmbVariantId } from '@umbraco-cms/backoffice/variant';
|
||||
import { UmbUfmVirtualRenderController } from '@umbraco-cms/backoffice/ufm';
|
||||
import { UMB_EDIT_ELEMENT_WORKSPACE_PATH_PATTERN, UMB_ELEMENT_ENTITY_TYPE } from '@umbraco-cms/backoffice/element';
|
||||
import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/workspace';
|
||||
import type { Observable } from '@umbraco-cms/backoffice/external/rxjs';
|
||||
import type { UmbBlockTypeBaseModel } from '@umbraco-cms/backoffice/block-type';
|
||||
import type { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
|
||||
@@ -52,12 +54,20 @@ export abstract class UmbBlockEntryContext<
|
||||
protected _manager?: BlockManagerContextType;
|
||||
protected _entries?: BlockEntriesContextType;
|
||||
|
||||
#key?: string;
|
||||
#contentKey?: string;
|
||||
#unsupported = new UmbBooleanState(undefined);
|
||||
readonly unsupported = this.#unsupported.asObservable();
|
||||
#structurallyUnsupported = false;
|
||||
|
||||
protected readonly localize = new UmbLocalizationController(this);
|
||||
|
||||
#isExternalContent = new UmbBooleanState(false);
|
||||
readonly isExternalContent = this.#isExternalContent.asObservable();
|
||||
|
||||
#externalContentVariantState = new UmbStringState(undefined);
|
||||
readonly externalContentVariantState = this.#externalContentVariantState.asObservable();
|
||||
|
||||
#pathAddendum = new UmbRoutePathAddendumContext(this);
|
||||
#variantId = new UmbClassState<UmbVariantId | undefined>(undefined);
|
||||
protected readonly _variantId = this.#variantId.asObservable();
|
||||
@@ -140,7 +150,7 @@ export abstract class UmbBlockEntryContext<
|
||||
public readonly layout = this._layout.asObservable();
|
||||
public readonly contentKey = this._layout.asObservablePart((x) => x?.contentKey);
|
||||
public readonly settingsKey = this._layout.asObservablePart((x) => (x ? (x.settingsKey ?? null) : undefined));
|
||||
public readonly unique = this._layout.asObservablePart((x) => x?.contentKey);
|
||||
public readonly unique = this._layout.asObservablePart((x) => x?.key);
|
||||
|
||||
/**
|
||||
* Get the layout of the block.
|
||||
@@ -166,9 +176,18 @@ export abstract class UmbBlockEntryContext<
|
||||
|
||||
#workspacePath = new UmbStringState(undefined);
|
||||
public readonly workspacePath = this.#workspacePath.asObservable();
|
||||
|
||||
#externalContentWorkspacePath = new UmbStringState(undefined);
|
||||
|
||||
public readonly workspaceEditContentPath = mergeObservables(
|
||||
[this.contentKey, this.workspacePath],
|
||||
([contentKey, path]) => this.#generateWorkspaceEditContentPath(path, contentKey),
|
||||
[this.contentKey, this.workspacePath, this.isExternalContent, this.#externalContentWorkspacePath.asObservable()],
|
||||
([contentKey, path, isExternalContent, externalContentPath]) => {
|
||||
if (!contentKey) return '';
|
||||
if (isExternalContent && externalContentPath) {
|
||||
return externalContentPath + UMB_EDIT_ELEMENT_WORKSPACE_PATH_PATTERN.generateLocal({ unique: contentKey });
|
||||
}
|
||||
return this.#generateWorkspaceEditContentPath(path, contentKey);
|
||||
},
|
||||
);
|
||||
public readonly workspaceEditSettingsPath = mergeObservables(
|
||||
[this.contentKey, this.workspacePath],
|
||||
@@ -360,6 +379,17 @@ export abstract class UmbBlockEntryContext<
|
||||
null,
|
||||
);
|
||||
|
||||
this.observe(
|
||||
this.contentKey,
|
||||
(contentKey) => {
|
||||
if (!contentKey) return;
|
||||
this.#contentKey = contentKey;
|
||||
this.#observeContentData();
|
||||
this.#gotVariantId();
|
||||
},
|
||||
null,
|
||||
);
|
||||
|
||||
// Observe contentElementTypeKey:
|
||||
this.observe(
|
||||
this.contentTypeKey,
|
||||
@@ -441,15 +471,38 @@ export abstract class UmbBlockEntryContext<
|
||||
return this._layout.value?.contentKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the key of this entry — the unique identity of the block layout item.
|
||||
* @param {string} key the block key.
|
||||
*/
|
||||
setKey(key: string) {
|
||||
this.#key = key;
|
||||
this.#observeLayout();
|
||||
}
|
||||
|
||||
getKey() {
|
||||
return this.#key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the contentKey of this entry.
|
||||
* @function setContentKey
|
||||
* @param {string} contentKey the entry content key.
|
||||
* @returns {void}
|
||||
* @deprecated Use `setKey` instead. Will be removed in Umbraco 20.
|
||||
*/
|
||||
setContentKey(contentKey: string) {
|
||||
new UmbDeprecation({
|
||||
deprecated: 'UmbBlockEntryContext.setContentKey',
|
||||
solution: 'Use setKey() with the block layout key instead.',
|
||||
removeInVersion: '20.0.0',
|
||||
}).warn();
|
||||
this.#contentKey = contentKey;
|
||||
this.#observeLayout();
|
||||
// Backwards compat: if no key set yet, use contentKey
|
||||
if (!this.#key) {
|
||||
this.#key = contentKey;
|
||||
this.#observeLayout();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -481,12 +534,16 @@ export abstract class UmbBlockEntryContext<
|
||||
}
|
||||
|
||||
#observeLayout() {
|
||||
if (!this._entries || !this.#contentKey) return;
|
||||
if (!this._entries || !this.#key) return;
|
||||
|
||||
this.observe(
|
||||
this._entries.layoutOf(this.#contentKey),
|
||||
this._entries.byKey(this.#key),
|
||||
(layout) => {
|
||||
this._layout.setValue(layout);
|
||||
// Derive contentKey from the layout so internal flows have it without external setters.
|
||||
if (layout?.contentKey) {
|
||||
this.#contentKey = layout.contentKey;
|
||||
}
|
||||
},
|
||||
'observeParentLayout',
|
||||
);
|
||||
@@ -522,25 +579,53 @@ export abstract class UmbBlockEntryContext<
|
||||
},
|
||||
'observeWorkspacePath',
|
||||
);
|
||||
|
||||
new UmbModalRouteRegistrationController(this, UMB_WORKSPACE_MODAL)
|
||||
.addAdditionalPath('element')
|
||||
.addUniquePaths(['unique'])
|
||||
.onSetup(() => {
|
||||
return {
|
||||
data: { entityType: UMB_ELEMENT_ENTITY_TYPE, preset: {} },
|
||||
modal: { size: 'large' },
|
||||
};
|
||||
})
|
||||
.observeRouteBuilder((routeBuilder) => {
|
||||
this.#externalContentWorkspacePath.setValue(routeBuilder({ entityType: UMB_ELEMENT_ENTITY_TYPE }));
|
||||
});
|
||||
}
|
||||
|
||||
protected abstract _gotEntries(): void;
|
||||
|
||||
#observeContentData() {
|
||||
if (!this._manager || !this.#contentKey) return;
|
||||
const contentKey = this.#contentKey ?? this._layout.value?.contentKey;
|
||||
if (!this._manager || !contentKey) return;
|
||||
|
||||
// observe content:
|
||||
// Observe content and external-content state together to avoid race conditions.
|
||||
// Both are evaluated in the same tick, preventing the unsupported flag
|
||||
// from flashing true while external content is being fetched.
|
||||
this.observe(
|
||||
this._manager.contentOf(this.#contentKey),
|
||||
(content) => {
|
||||
if (this.#unsupported.getValue() !== true) {
|
||||
// If we could not find content, then we do not know the contentTypeKey and then the content is broken. [NL]
|
||||
this.#unsupported.setValue(!content);
|
||||
mergeObservables(
|
||||
[this._manager.contentOf(contentKey), this._manager.isExternalContentOf(contentKey)],
|
||||
([content, isExternalContent]) => ({ content, isExternalContent: isExternalContent ?? false }),
|
||||
),
|
||||
({ content, isExternalContent }) => {
|
||||
this.#isExternalContent.setValue(isExternalContent);
|
||||
if (!this.#structurallyUnsupported) {
|
||||
this.#unsupported.setValue(!content && !isExternalContent);
|
||||
}
|
||||
this.#content.setValue(content);
|
||||
},
|
||||
'observeContent',
|
||||
);
|
||||
|
||||
// Observe the variant state of external content (published, draft, etc.)
|
||||
this.observe(
|
||||
this._manager.externalContentStateOf(contentKey),
|
||||
(state) => {
|
||||
this.#externalContentVariantState.setValue(state ?? undefined);
|
||||
},
|
||||
'observeExternalContentVariantState',
|
||||
);
|
||||
}
|
||||
#observeSettingsData() {
|
||||
// observe settings:
|
||||
@@ -630,7 +715,8 @@ export abstract class UmbBlockEntryContext<
|
||||
this.#contentStructurePromiseResolve?.();
|
||||
|
||||
if (!this.#contentStructure) {
|
||||
// If we got no content structure, then this is element type did not load and there for it is not supported any longer.
|
||||
// If we got no content structure, then this element type did not load and the block is not supported any longer.
|
||||
this.#structurallyUnsupported = true;
|
||||
this.#unsupported.setValue(true);
|
||||
}
|
||||
|
||||
@@ -676,6 +762,7 @@ export abstract class UmbBlockEntryContext<
|
||||
this._blockType.setValue(blockType as BlockType);
|
||||
if (!blockType) {
|
||||
// If the block type is undefined, then we do not have this Block Type and the Block is then unsupported. [NL]
|
||||
this.#structurallyUnsupported = true;
|
||||
this.#unsupported.setValue(true);
|
||||
}
|
||||
},
|
||||
@@ -747,11 +834,22 @@ export abstract class UmbBlockEntryContext<
|
||||
this.delete();
|
||||
}
|
||||
|
||||
async requestTransferToExternalContent() {
|
||||
if (!this.#key) return;
|
||||
const name = this.getName();
|
||||
await this._manager?.requestTransferToExternalContent(this.#key, name);
|
||||
}
|
||||
|
||||
async requestDisconnectFromExternalContent() {
|
||||
if (!this.#key) return;
|
||||
await this._manager?.requestDisconnectFromExternalContent(this.#key);
|
||||
}
|
||||
|
||||
public delete() {
|
||||
if (!this._entries) return;
|
||||
const contentKey = this._layout.value?.contentKey;
|
||||
if (!contentKey) return;
|
||||
this._entries.delete(contentKey);
|
||||
const key = this._layout.value?.key;
|
||||
if (!key) return;
|
||||
this._entries.delete(key);
|
||||
}
|
||||
|
||||
public expose() {
|
||||
|
||||
+236
-8
@@ -1,5 +1,10 @@
|
||||
import type { UmbBlockWorkspaceOriginData } from '../workspace/index.js';
|
||||
import type { UmbBlockLayoutBaseModel, UmbBlockDataModel, UmbBlockExposeModel } from '../types.js';
|
||||
import type {
|
||||
UmbBlockDataModel,
|
||||
UmbBlockDataValueModel,
|
||||
UmbBlockExposeModel,
|
||||
UmbBlockLayoutBaseModel,
|
||||
} from '../types.js';
|
||||
import { UmbBlockInsertedEvent } from '../events/block-inserted.event.js';
|
||||
import { UMB_BLOCK_MANAGER_CONTEXT } from './block-manager.context-token.js';
|
||||
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
|
||||
@@ -26,6 +31,9 @@ import {
|
||||
} from '@umbraco-cms/backoffice/property';
|
||||
import { UMB_APP_LANGUAGE_CONTEXT } from '@umbraco-cms/backoffice/language';
|
||||
import { UmbDataTypeDetailRepository } from '@umbraco-cms/backoffice/data-type';
|
||||
import { UmbElementDetailRepository } from '@umbraco-cms/backoffice/element';
|
||||
import { UMB_BLOCK_TRANSFER_TO_ELEMENT_LIBRARY_MODAL } from '../modals/transfer-to-element-library/transfer-to-element-library-modal.token.js';
|
||||
import { UMB_MODAL_MANAGER_CONTEXT, umbConfirmModal } from '@umbraco-cms/backoffice/modal';
|
||||
|
||||
export type UmbBlockDataObjectModel<LayoutEntryType extends UmbBlockLayoutBaseModel> = {
|
||||
layout: LayoutEntryType;
|
||||
@@ -69,12 +77,31 @@ export abstract class UmbBlockManagerContext<
|
||||
protected _liveEditingMode = new UmbBooleanState(undefined);
|
||||
public readonly liveEditingMode = this._liveEditingMode.asObservable();
|
||||
|
||||
protected _layouts = new UmbArrayState(<Array<BlockLayoutType>>[], (x) => x.contentKey);
|
||||
protected _layouts = new UmbArrayState(<Array<BlockLayoutType>>[], (x) => x.key);
|
||||
public readonly layouts = this._layouts.asObservable();
|
||||
|
||||
readonly #contents = new UmbArrayState(<Array<UmbBlockDataModel>>[], (x) => x.key);
|
||||
public readonly contents = this.#contents.asObservable();
|
||||
|
||||
readonly #externalContentValues = new UmbArrayState(<Array<UmbBlockDataModel>>[], (x) => x.key);
|
||||
|
||||
/**
|
||||
* Combined observable of local block content and resolved external (library element) content.
|
||||
* Use this alongside `contents` when you also need to react to library elements becoming available.
|
||||
*/
|
||||
public readonly allContents = mergeObservables(
|
||||
[this.#contents.asObservable(), this.#externalContentValues.asObservable()],
|
||||
([local, external]) => [...(local ?? []), ...(external ?? [])],
|
||||
);
|
||||
readonly #externalContentVariants = new UmbArrayState(
|
||||
<
|
||||
Array<{ key: string; variants: Array<{ culture: string | null; segment: string | null; state: string | null }> }>
|
||||
>[],
|
||||
(x) => x.key,
|
||||
);
|
||||
#elementRepository = new UmbElementDetailRepository(this);
|
||||
#pendingElementFetches = new Set<string>();
|
||||
|
||||
readonly #settings = new UmbArrayState(<Array<UmbBlockDataModel>>[], (x) => x.key);
|
||||
public readonly settings = this.#settings.asObservable();
|
||||
|
||||
@@ -112,7 +139,8 @@ export abstract class UmbBlockManagerContext<
|
||||
* @param {Array<BlockLayoutType>} layouts - All layouts.
|
||||
*/
|
||||
setLayouts(layouts: Array<BlockLayoutType>) {
|
||||
this._layouts.setValue(layouts);
|
||||
// Backwards compatibility: ensure all layouts have a key (persisted data may not have one)
|
||||
this._layouts.setValue(layouts.map((layout) => (layout.key ? layout : { ...layout, key: layout.contentKey })));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -181,6 +209,18 @@ export abstract class UmbBlockManagerContext<
|
||||
},
|
||||
null,
|
||||
);
|
||||
|
||||
// Auto-resolve content for any layout marked as external, whenever layouts change.
|
||||
this.observe(
|
||||
this._layouts.asObservable(),
|
||||
(layouts) => {
|
||||
const keys = layouts
|
||||
.filter((layout) => layout.isExternalContent && layout.contentKey)
|
||||
.map((layout) => layout.contentKey as string);
|
||||
if (keys.length) this.#fetchExternalContent(keys);
|
||||
},
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
#ensureContentTypes(blockTypes: Array<BlockType>) {
|
||||
@@ -293,8 +333,82 @@ export abstract class UmbBlockManagerContext<
|
||||
return this._layouts.asObservablePart((source) => source.find((x) => x.contentKey === contentKey));
|
||||
}
|
||||
contentOf(key: string) {
|
||||
return this.#contents.asObservablePart((source) => source.find((x) => x.key === key));
|
||||
return mergeObservables(
|
||||
[
|
||||
this.#contents.asObservablePart((source) => source.find((x) => x.key === key)),
|
||||
this.#externalContentValues.asObservablePart((source) => source.find((x) => x.key === key)),
|
||||
],
|
||||
([localContent, externalContent]) => localContent ?? externalContent ?? undefined,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable that emits true when the layout for the given contentKey
|
||||
* has `isExternalContent` set (i.e., the block references external content).
|
||||
*/
|
||||
isExternalContentOf(key: string) {
|
||||
return this._layouts.asObservablePart(
|
||||
(source) => source.find((x) => x.contentKey === key)?.isExternalContent === true,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable of the external content variant state.
|
||||
* Emits the state string (e.g., 'Published', 'Draft') or null if not resolved yet.
|
||||
*/
|
||||
externalContentStateOf(key: string) {
|
||||
return mergeObservables(
|
||||
[
|
||||
this.#externalContentVariants.asObservablePart((source) => source.find((x) => x.key === key)),
|
||||
this.variantId,
|
||||
],
|
||||
([entry, variantId]) => {
|
||||
if (!entry?.variants.length) return null;
|
||||
if (!variantId) return entry.variants[0]?.state ?? null;
|
||||
const match = entry.variants.find((v) => v.culture === variantId.culture && v.segment === variantId.segment);
|
||||
return match?.state ?? entry.variants[0]?.state ?? null;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: migrate to UmbRepositoryDetailsManager for true batched fetches [LK]
|
||||
async #fetchExternalContent(keys: Array<string>) {
|
||||
for (const key of keys) {
|
||||
if (this.#pendingElementFetches.has(key)) continue;
|
||||
if (this.#externalContentValues.getValue().some((x) => x.key === key)) continue;
|
||||
this.#pendingElementFetches.add(key);
|
||||
try {
|
||||
const { data } = await this.#elementRepository.requestByUnique(key);
|
||||
if (data) {
|
||||
const blockData: UmbBlockDataModel = {
|
||||
key: data.unique,
|
||||
contentTypeKey: data.documentType.unique,
|
||||
values: data.values.map(
|
||||
(v): UmbBlockDataValueModel => ({
|
||||
alias: v.alias,
|
||||
editorAlias: v.editorAlias,
|
||||
culture: v.culture,
|
||||
segment: v.segment,
|
||||
value: v.value,
|
||||
}),
|
||||
),
|
||||
};
|
||||
this.#externalContentValues.appendOne(blockData);
|
||||
this.#externalContentVariants.appendOne({
|
||||
key: data.unique,
|
||||
variants: data.variants.map((v) => ({
|
||||
culture: v.culture ?? null,
|
||||
segment: v.segment ?? null,
|
||||
state: v.state ?? null,
|
||||
})),
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
this.#pendingElementFetches.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
settingsOf(key: string) {
|
||||
return this.#settings.asObservablePart((source) => source.find((x) => x.key === key));
|
||||
}
|
||||
@@ -355,7 +469,10 @@ export abstract class UmbBlockManagerContext<
|
||||
return this.#blockTypes.value.find((x) => x.contentElementTypeKey === contentTypeKey);
|
||||
}
|
||||
getContentOf(contentKey: string) {
|
||||
return this.#contents.value.find((x) => x.key === contentKey);
|
||||
return (
|
||||
this.#contents.value.find((x) => x.key === contentKey) ??
|
||||
this.#externalContentValues.value.find((x) => x.key === contentKey)
|
||||
);
|
||||
}
|
||||
getSettingsOf(settingsKey: string) {
|
||||
return this.#settings.value.find((x) => x.key === settingsKey);
|
||||
@@ -399,6 +516,114 @@ export abstract class UmbBlockManagerContext<
|
||||
this.#exposes.filter((x) => !(x.contentKey === contentKey && variantId.compare(x)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a block whose content is an external content reference.
|
||||
* Only creates a layout entry — no contentData entry is added.
|
||||
* The layout observer handles fetching the element data.
|
||||
*/
|
||||
insertExternalContent(elementKey: string, _originData?: BlockOriginDataType) {
|
||||
const layout = { key: UmbId.new(), contentKey: elementKey, isExternalContent: true } as BlockLayoutType;
|
||||
this._layouts.appendOne(layout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request to transfer a local block's content to external content (Element Library).
|
||||
* @param {string} key the block layout key.
|
||||
*/
|
||||
async requestTransferToExternalContent(key: string, name?: string) {
|
||||
const layout = this._layouts.getValue().find((x) => x.key === key);
|
||||
if (!layout) return;
|
||||
const contentKey = layout.contentKey;
|
||||
const content = this.getContentOf(contentKey);
|
||||
if (!content) return;
|
||||
|
||||
const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT).catch(() => undefined);
|
||||
if (!modalManager) return;
|
||||
const result = await modalManager
|
||||
.open(this, UMB_BLOCK_TRANSFER_TO_ELEMENT_LIBRARY_MODAL, { data: { name } })
|
||||
.onSubmit()
|
||||
.catch(() => undefined);
|
||||
if (!result) return;
|
||||
|
||||
const { data: scaffold } = await this.#elementRepository.createScaffold({
|
||||
documentType: { unique: content.contentTypeKey, collection: null },
|
||||
values: content.values,
|
||||
variants: [
|
||||
{
|
||||
culture: null,
|
||||
segment: null,
|
||||
state: null,
|
||||
name: result.name,
|
||||
publishDate: null,
|
||||
createDate: null,
|
||||
updateDate: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
if (!scaffold) return;
|
||||
|
||||
const { data: created } = await this.#elementRepository.create(scaffold, result.parentUnique);
|
||||
if (!created) return;
|
||||
|
||||
this.#contents.removeOne(contentKey);
|
||||
this.removeExposesOf(contentKey);
|
||||
this._layouts.updateOne(key, {
|
||||
contentKey: created.unique,
|
||||
isExternalContent: true,
|
||||
} as Partial<BlockLayoutType>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request to disconnect a block from external content (Element Library).
|
||||
* @param {string} key the block layout key.
|
||||
*/
|
||||
async requestDisconnectFromExternalContent(key: string) {
|
||||
const layout = this._layouts.getValue().find((x) => x.key === key);
|
||||
if (!layout) return;
|
||||
const elementKey = layout.contentKey;
|
||||
|
||||
try {
|
||||
await umbConfirmModal(this, {
|
||||
headline: '#blockEditor_disconnectFromElementLibrary',
|
||||
content: '#blockEditor_disconnectFromElementLibraryConfirm',
|
||||
confirmLabel: '#blockEditor_disconnectFromElementLibrary',
|
||||
color: 'warning',
|
||||
});
|
||||
} catch {
|
||||
return; // user cancelled
|
||||
}
|
||||
|
||||
const { data: element } = await this.#elementRepository.requestByUnique(elementKey);
|
||||
if (!element) return;
|
||||
|
||||
const contentTypeKey = element.documentType.unique;
|
||||
const newContent: UmbBlockDataModel = {
|
||||
key: UmbId.new(),
|
||||
contentTypeKey,
|
||||
values: element.values.map(
|
||||
(v): UmbBlockDataValueModel => ({
|
||||
alias: v.alias,
|
||||
editorAlias: v.editorAlias,
|
||||
culture: v.culture,
|
||||
segment: v.segment,
|
||||
value: v.value,
|
||||
}),
|
||||
),
|
||||
};
|
||||
this.#contents.appendOne(newContent);
|
||||
this._layouts.updateOne(key, {
|
||||
contentKey: newContent.key,
|
||||
isExternalContent: undefined,
|
||||
} as Partial<BlockLayoutType>);
|
||||
this.#externalContentValues.removeOne(elementKey);
|
||||
this.#externalContentVariants.removeOne(elementKey);
|
||||
// Only set expose if the content type structure is loaded (it may not be for external content
|
||||
// whose type was not in the block type list)
|
||||
if (this.getStructure(contentTypeKey)) {
|
||||
this.#setInitialBlockExpose(newContent);
|
||||
}
|
||||
}
|
||||
|
||||
setOneContentProperty(key: string, propertyAlias: string, value: unknown) {
|
||||
this.#contents.updateOne(key, { [propertyAlias]: value });
|
||||
}
|
||||
@@ -419,7 +644,7 @@ export abstract class UmbBlockManagerContext<
|
||||
|
||||
abstract createWithPresets(
|
||||
contentElementTypeKey: string,
|
||||
partialLayoutEntry?: Omit<BlockLayoutType, 'contentKey'>,
|
||||
partialLayoutEntry?: Omit<BlockLayoutType, 'contentKey' | 'key'>,
|
||||
originData?: BlockOriginDataType,
|
||||
): Promise<UmbBlockDataObjectModel<BlockLayoutType> | undefined>;
|
||||
|
||||
@@ -520,7 +745,7 @@ export abstract class UmbBlockManagerContext<
|
||||
|
||||
protected async _createBlockData(
|
||||
contentElementTypeKey: string,
|
||||
partialLayoutEntry?: Omit<BlockLayoutType, 'contentKey'>,
|
||||
partialLayoutEntry?: Omit<BlockLayoutType, 'contentKey' | 'key'>,
|
||||
) {
|
||||
// Find block type.
|
||||
const blockType = this.#blockTypes.value.find((x) => x.contentElementTypeKey === contentElementTypeKey);
|
||||
@@ -528,9 +753,12 @@ export abstract class UmbBlockManagerContext<
|
||||
throw new Error(`Cannot create block, missing block type for ${contentElementTypeKey}`);
|
||||
}
|
||||
|
||||
const contentKey = UmbId.new();
|
||||
|
||||
// Create layout entry:
|
||||
const layout: BlockLayoutType = {
|
||||
contentKey: UmbId.new(),
|
||||
key: contentKey,
|
||||
contentKey,
|
||||
...(partialLayoutEntry as Partial<BlockLayoutType>),
|
||||
} as BlockLayoutType;
|
||||
|
||||
|
||||
+92
-10
@@ -13,13 +13,14 @@ import {
|
||||
} from '@umbraco-cms/backoffice/external/lit';
|
||||
import { transformServerPathToClientPath } from '@umbraco-cms/backoffice/utils';
|
||||
import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router';
|
||||
import { UmbPickerContext } from '@umbraco-cms/backoffice/picker';
|
||||
import { UmbRepositoryItemsManager } from '@umbraco-cms/backoffice/repository';
|
||||
import { UMB_DOCUMENT_TYPE_ITEM_REPOSITORY_ALIAS } from '@umbraco-cms/backoffice/document-type';
|
||||
import { UMB_MODAL_CONTEXT, UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
|
||||
import { UMB_SERVER_CONTEXT } from '@umbraco-cms/backoffice/server';
|
||||
import type { UmbBlockTypeGroup, UmbBlockTypeWithGroupKey } from '@umbraco-cms/backoffice/block-type';
|
||||
import type { UmbDocumentTypeItemModel } from '@umbraco-cms/backoffice/document-type';
|
||||
import type { UmbSelectionChangeEvent } from '@umbraco-cms/backoffice/event';
|
||||
import type { UmbDeselectedEvent, UmbSelectedEvent, UmbSelectionChangeEvent } from '@umbraco-cms/backoffice/event';
|
||||
import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui';
|
||||
|
||||
type UmbBlockTypeItemWithGroupKey = UmbBlockTypeWithGroupKey & UmbDocumentTypeItemModel;
|
||||
@@ -34,6 +35,8 @@ export class UmbBlockCatalogueModalElement extends UmbModalBaseElement<
|
||||
UMB_DOCUMENT_TYPE_ITEM_REPOSITORY_ALIAS,
|
||||
);
|
||||
|
||||
#pickerContext = new UmbPickerContext(this);
|
||||
|
||||
#search = '';
|
||||
|
||||
#serverUrl = '';
|
||||
@@ -41,7 +44,12 @@ export class UmbBlockCatalogueModalElement extends UmbModalBaseElement<
|
||||
private _groupedBlocks: Array<{ name?: string; blocks: Array<UmbBlockTypeItemWithGroupKey> }> = [];
|
||||
|
||||
@state()
|
||||
private _openClipboard?: boolean;
|
||||
private _activeView: 'create' | 'clipboard' | 'library' = 'create';
|
||||
|
||||
@state()
|
||||
private _searchQuery = '';
|
||||
|
||||
#hasLibraryElements = false;
|
||||
|
||||
@state()
|
||||
private _workspacePath?: string;
|
||||
@@ -88,13 +96,24 @@ export class UmbBlockCatalogueModalElement extends UmbModalBaseElement<
|
||||
this.observe(this.#itemManager.items, async (items) => {
|
||||
this.#observeBlockTypes(items);
|
||||
});
|
||||
|
||||
this.#pickerContext.search.updateConfig({ providerAlias: 'Umb.SearchProvider.Element' });
|
||||
this.#pickerContext.selection.setMultiple(false);
|
||||
this.observe(this.#pickerContext.search.query, (query) => {
|
||||
this._searchQuery = query?.query ?? '';
|
||||
});
|
||||
this.observe(this.#pickerContext.selection.selection, (selection) => {
|
||||
const elementKey = selection[0];
|
||||
this.value = elementKey ? { library: { elementKey } } : undefined;
|
||||
});
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (!this.data) return;
|
||||
|
||||
this._openClipboard = this.data.openClipboard ?? false;
|
||||
this._activeView = this.data.openClipboard ? 'clipboard' : 'create';
|
||||
this.#hasLibraryElements = (this.data.libraryAllowedElementTypeKeys?.length ?? 0) > 0;
|
||||
|
||||
this.#itemManager.setUniques(this.data.blocks.map((block) => block.contentElementTypeKey));
|
||||
}
|
||||
@@ -193,7 +212,15 @@ export class UmbBlockCatalogueModalElement extends UmbModalBaseElement<
|
||||
}
|
||||
|
||||
#renderMain() {
|
||||
return this._manager ? (this._openClipboard ? this.#renderClipboard() : this.#renderCreateEmpty()) : nothing;
|
||||
if (!this._manager) return nothing;
|
||||
switch (this._activeView) {
|
||||
case 'clipboard':
|
||||
return this.#renderClipboard();
|
||||
case 'library':
|
||||
return this.#renderLibrary();
|
||||
default:
|
||||
return this.#renderCreateEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
#renderClipboard() {
|
||||
@@ -204,6 +231,49 @@ export class UmbBlockCatalogueModalElement extends UmbModalBaseElement<
|
||||
`;
|
||||
}
|
||||
|
||||
#renderLibrary() {
|
||||
return html`
|
||||
<umb-picker-search-field></umb-picker-search-field>
|
||||
<umb-picker-search-result .pickableFilter=${this.#librarySelectableFilter}></umb-picker-search-result>
|
||||
${when(
|
||||
!this._searchQuery,
|
||||
() => html`
|
||||
<uui-box>
|
||||
<umb-tree
|
||||
alias="Umb.Tree.Element"
|
||||
.props=${this.#libraryTreeProps}
|
||||
@selected=${this.#onLibraryElementSelected}
|
||||
@deselected=${this.#onLibraryElementDeselected}></umb-tree>
|
||||
</uui-box>
|
||||
`,
|
||||
)}
|
||||
`;
|
||||
}
|
||||
|
||||
#librarySelectableFilter = (item: any) => {
|
||||
if (item.isFolder) return false;
|
||||
const allowedKeys = this.data?.libraryAllowedElementTypeKeys;
|
||||
if (!allowedKeys?.length) return true;
|
||||
return allowedKeys.includes(item.documentType?.unique ?? '');
|
||||
};
|
||||
|
||||
#libraryTreeProps = {
|
||||
hideTreeItemActions: true,
|
||||
hideTreeRoot: true,
|
||||
selectableFilter: this.#librarySelectableFilter,
|
||||
selectionManager: this.#pickerContext.selection,
|
||||
};
|
||||
|
||||
#onLibraryElementSelected(event: UmbSelectedEvent) {
|
||||
event.stopPropagation();
|
||||
if (event.unique) this.#pickerContext.selection.select(event.unique);
|
||||
}
|
||||
|
||||
#onLibraryElementDeselected(event: UmbDeselectedEvent) {
|
||||
event.stopPropagation();
|
||||
if (event.unique) this.#pickerContext.selection.deselect(event.unique);
|
||||
}
|
||||
|
||||
#renderCreateEmpty() {
|
||||
if (this._loading) return html`<div id="loader"><uui-loader></uui-loader></div>`;
|
||||
return html`
|
||||
@@ -267,16 +337,28 @@ export class UmbBlockCatalogueModalElement extends UmbModalBaseElement<
|
||||
<uui-tab-group slot="navigation">
|
||||
<uui-tab
|
||||
label=${this.localize.term('blockEditor_tabCreateEmpty')}
|
||||
?active=${!this._openClipboard}
|
||||
@click=${() => (this._openClipboard = false)}>
|
||||
<umb-localize key=${this.localize.term('blockEditor_tabCreateEmpty')}>Create Empty</umb-localize>
|
||||
?active=${this._activeView === 'create'}
|
||||
@click=${() => (this._activeView = 'create')}>
|
||||
<umb-localize key="blockEditor_tabCreateEmpty">Create Empty</umb-localize>
|
||||
<uui-icon slot="icon" name="icon-add"></uui-icon>
|
||||
</uui-tab>
|
||||
${when(
|
||||
this.#hasLibraryElements,
|
||||
() => html`
|
||||
<uui-tab
|
||||
label=${this.localize.term('blockEditor_tabLibrary')}
|
||||
?active=${this._activeView === 'library'}
|
||||
@click=${() => (this._activeView = 'library')}>
|
||||
<umb-localize key="blockEditor_tabLibrary">Library</umb-localize>
|
||||
<uui-icon slot="icon" name="icon-link"></uui-icon>
|
||||
</uui-tab>
|
||||
`,
|
||||
)}
|
||||
<uui-tab
|
||||
label=${this.localize.term('blockEditor_tabClipboard')}
|
||||
?active=${this._openClipboard}
|
||||
@click=${() => (this._openClipboard = true)}>
|
||||
<umb-localize key=${this.localize.term('blockEditor_tabClipboard')}>Clipboard</umb-localize>
|
||||
?active=${this._activeView === 'clipboard'}
|
||||
@click=${() => (this._activeView = 'clipboard')}>
|
||||
<umb-localize key="blockEditor_tabClipboard">Clipboard</umb-localize>
|
||||
<uui-icon slot="icon" name="icon-clipboard"></uui-icon>
|
||||
</uui-tab>
|
||||
</uui-tab-group>
|
||||
|
||||
+4
-8
@@ -10,17 +10,13 @@ export interface UmbBlockCatalogueModalData {
|
||||
openClipboard?: boolean;
|
||||
clipboardFilter?: (clipboardDetailEntryModel: UmbClipboardEntryDetailModel) => Promise<boolean>;
|
||||
originData: UmbBlockWorkspaceData['originData'];
|
||||
libraryAllowedElementTypeKeys?: Array<string>;
|
||||
}
|
||||
|
||||
export type UmbBlockCatalogueModalValue =
|
||||
| {
|
||||
create?: {
|
||||
contentElementTypeKey: string;
|
||||
};
|
||||
clipboard?: {
|
||||
selection: Array<string>;
|
||||
};
|
||||
}
|
||||
| { create: { contentElementTypeKey: string } }
|
||||
| { clipboard: { selection: Array<string> } }
|
||||
| { library: { elementKey: string } }
|
||||
| undefined;
|
||||
|
||||
export const UMB_BLOCK_CATALOGUE_MODAL = new UmbModalToken<UmbBlockCatalogueModalData, UmbBlockCatalogueModalValue>(
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './block-catalogue/index.js';
|
||||
export * from './transfer-to-element-library/index.js';
|
||||
|
||||
@@ -5,4 +5,10 @@ export const manifests: Array<UmbExtensionManifest> = [
|
||||
name: 'Block Catalogue Modal',
|
||||
element: () => import('./block-catalogue/block-catalogue-modal.element.js'),
|
||||
},
|
||||
{
|
||||
type: 'modal',
|
||||
alias: 'Umb.Modal.BlockTransferToElementLibrary',
|
||||
name: 'Transfer Block To Element Library Modal',
|
||||
element: () => import('./transfer-to-element-library/transfer-to-element-library-modal.element.js'),
|
||||
},
|
||||
];
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
export { UMB_BLOCK_TRANSFER_TO_ELEMENT_LIBRARY_MODAL } from './transfer-to-element-library-modal.token.js';
|
||||
export type {
|
||||
UmbBlockTransferToElementLibraryModalData,
|
||||
UmbBlockTransferToElementLibraryModalValue,
|
||||
} from './transfer-to-element-library-modal.token.js';
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
import type {
|
||||
UmbBlockTransferToElementLibraryModalData,
|
||||
UmbBlockTransferToElementLibraryModalValue,
|
||||
} from './transfer-to-element-library-modal.token.js';
|
||||
import { css, customElement, html, state } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
|
||||
import type { UmbSelectedEvent, UmbDeselectedEvent } from '@umbraco-cms/backoffice/event';
|
||||
import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui';
|
||||
import { umbFocus } from '@umbraco-cms/backoffice/lit-element';
|
||||
|
||||
@customElement('umb-block-transfer-to-element-library-modal')
|
||||
export class UmbBlockTransferToElementLibraryModalElement extends UmbModalBaseElement<
|
||||
UmbBlockTransferToElementLibraryModalData,
|
||||
UmbBlockTransferToElementLibraryModalValue
|
||||
> {
|
||||
@state()
|
||||
private _name = '';
|
||||
|
||||
@state()
|
||||
private _parentUnique: string | null = null;
|
||||
|
||||
@state()
|
||||
private _hasSelectedLocation = false;
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this.data?.name) {
|
||||
this._name = this.data.name;
|
||||
}
|
||||
}
|
||||
|
||||
#onNameInput(e: UUIInputEvent) {
|
||||
this._name = e.target.value as string;
|
||||
}
|
||||
|
||||
#onFolderSelected(event: UmbSelectedEvent) {
|
||||
event.stopPropagation();
|
||||
this._parentUnique = event.unique ?? null;
|
||||
this._hasSelectedLocation = true;
|
||||
}
|
||||
|
||||
#onFolderDeselected(event: UmbDeselectedEvent) {
|
||||
event.stopPropagation();
|
||||
this._parentUnique = null;
|
||||
this._hasSelectedLocation = false;
|
||||
}
|
||||
|
||||
#onTransfer() {
|
||||
this.value = { name: this._name, parentUnique: this._parentUnique };
|
||||
this.modalContext?.submit();
|
||||
}
|
||||
|
||||
override render() {
|
||||
const treeProps = { hideTreeItemActions: true, foldersOnly: true };
|
||||
return html`
|
||||
<umb-body-layout headline=${this.localize.term('blockEditor_transferToElementLibrary')}>
|
||||
<uui-box>
|
||||
<umb-property-layout label="#general_name" orientation="vertical" mandatory>
|
||||
<uui-input
|
||||
slot="editor"
|
||||
required
|
||||
label=${this.localize.term('general_name')}
|
||||
.value=${this._name}
|
||||
@input=${this.#onNameInput}
|
||||
${umbFocus()}>
|
||||
</uui-input>
|
||||
</umb-property-layout>
|
||||
<umb-property-layout label="#general_choose" orientation="vertical" mandatory>
|
||||
<umb-tree
|
||||
slot="editor"
|
||||
alias="Umb.Tree.Element"
|
||||
.props=${treeProps}
|
||||
@selected=${this.#onFolderSelected}
|
||||
@deselected=${this.#onFolderDeselected}>
|
||||
</umb-tree>
|
||||
</umb-property-layout>
|
||||
</uui-box>
|
||||
<uui-button
|
||||
slot="actions"
|
||||
label=${this.localize.term('general_cancel')}
|
||||
@click=${this._rejectModal}></uui-button>
|
||||
<uui-button
|
||||
slot="actions"
|
||||
label=${this.localize.term('blockEditor_transferToElementLibrary')}
|
||||
look="primary"
|
||||
color="positive"
|
||||
?disabled=${!this._name.trim() || !this._hasSelectedLocation}
|
||||
@click=${this.#onTransfer}></uui-button>
|
||||
</umb-body-layout>
|
||||
`;
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
css`
|
||||
uui-input {
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
export default UmbBlockTransferToElementLibraryModalElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-block-transfer-to-element-library-modal': UmbBlockTransferToElementLibraryModalElement;
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
|
||||
|
||||
export interface UmbBlockTransferToElementLibraryModalData {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface UmbBlockTransferToElementLibraryModalValue {
|
||||
name: string;
|
||||
parentUnique: string | null;
|
||||
}
|
||||
|
||||
export const UMB_BLOCK_TRANSFER_TO_ELEMENT_LIBRARY_MODAL = new UmbModalToken<
|
||||
UmbBlockTransferToElementLibraryModalData,
|
||||
UmbBlockTransferToElementLibraryModalValue
|
||||
>('Umb.Modal.BlockTransferToElementLibrary', {
|
||||
modal: {
|
||||
type: 'sidebar',
|
||||
size: 'small',
|
||||
},
|
||||
});
|
||||
+2
-1
@@ -56,7 +56,8 @@ export abstract class UmbBlockPropertyValueCloner<
|
||||
const contentKey = layoutEntry.contentKey;
|
||||
const settingsKey = layoutEntry.settingsKey;
|
||||
|
||||
// Generate new contentKey and settingsKey:
|
||||
// Generate new key and contentKey:
|
||||
clonedLayoutEntry.key = UmbId.new();
|
||||
const newContentKey = UmbId.new();
|
||||
clonedLayoutEntry.contentKey = newContentKey;
|
||||
|
||||
|
||||
@@ -4,8 +4,10 @@ export type * from './conditions/types.js';
|
||||
export type * from './clipboard/types.js';
|
||||
|
||||
export interface UmbBlockLayoutBaseModel {
|
||||
key: string;
|
||||
contentKey: string;
|
||||
settingsKey?: string | null;
|
||||
isExternalContent?: boolean;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
@@ -35,7 +37,8 @@ export interface UmbBlockValueDataPropertiesBaseType {
|
||||
expose: Array<UmbBlockExposeModel>;
|
||||
}
|
||||
|
||||
export interface UmbBlockValueType<BlockLayoutType extends UmbBlockLayoutBaseModel = UmbBlockLayoutBaseModel>
|
||||
extends UmbBlockValueDataPropertiesBaseType {
|
||||
export interface UmbBlockValueType<
|
||||
BlockLayoutType extends UmbBlockLayoutBaseModel = UmbBlockLayoutBaseModel,
|
||||
> extends UmbBlockValueDataPropertiesBaseType {
|
||||
layout: { [key: string]: Array<BlockLayoutType> | undefined };
|
||||
}
|
||||
|
||||
+45
-8
@@ -81,6 +81,9 @@ export class UmbBlockWorkspaceContext<LayoutDataType extends UmbBlockLayoutBaseM
|
||||
#exposed = new UmbBooleanState<undefined>(undefined);
|
||||
readonly exposed = this.#exposed.asObservable();
|
||||
|
||||
#hasContent = new UmbBooleanState<undefined>(undefined);
|
||||
readonly hasContent = this.#hasContent.asObservable();
|
||||
|
||||
public readonly readOnlyGuard = new UmbReadOnlyVariantGuardManager(this);
|
||||
|
||||
constructor(host: UmbControllerHost, workspaceArgs: { manifest: ManifestWorkspace }) {
|
||||
@@ -233,18 +236,35 @@ export class UmbBlockWorkspaceContext<LayoutDataType extends UmbBlockLayoutBaseM
|
||||
'observeVariantIds',
|
||||
);
|
||||
|
||||
this.removeUmbControllerByAlias('observeHasExpose');
|
||||
this.observe(
|
||||
observeMultiple([this.contentKey, this.variantId]),
|
||||
([contentKey, variantId]) => {
|
||||
this.removeUmbControllerByAlias('observeExposeShared');
|
||||
this.removeUmbControllerByAlias('observeHasExpose');
|
||||
if (!contentKey || !variantId) return;
|
||||
|
||||
this.observe(
|
||||
manager.hasExposeOf(contentKey, variantId),
|
||||
(exposed) => {
|
||||
this.#exposed.setValue(exposed ?? false);
|
||||
manager.isExternalContentOf(contentKey),
|
||||
(isExternalContent) => {
|
||||
if (isExternalContent) {
|
||||
// External content does not keep an exposed state, so we default to true.
|
||||
this.#exposed.setValue(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const exposeObs = manager.hasExposeOf(contentKey, variantId);
|
||||
this.#exposed.setValue(false);
|
||||
if (!exposeObs) return;
|
||||
|
||||
this.observe(
|
||||
exposeObs,
|
||||
(exposed) => {
|
||||
this.#exposed.setValue(exposed ?? false);
|
||||
},
|
||||
'observeHasExpose',
|
||||
);
|
||||
},
|
||||
'observeHasExpose',
|
||||
'observeExposeShared',
|
||||
);
|
||||
},
|
||||
'observeContentKeyAndVariantId',
|
||||
@@ -272,6 +292,23 @@ export class UmbBlockWorkspaceContext<LayoutDataType extends UmbBlockLayoutBaseM
|
||||
},
|
||||
'observeContentTypeId',
|
||||
);
|
||||
|
||||
this.observe(
|
||||
this.contentKey,
|
||||
(contentKey) => {
|
||||
this.removeUmbControllerByAlias('observeHasContent');
|
||||
if (!contentKey) {
|
||||
this.#hasContent.setValue(false);
|
||||
return;
|
||||
}
|
||||
this.observe(
|
||||
manager.isExternalContentOf(contentKey),
|
||||
(isExternalContent) => this.#hasContent.setValue(!isExternalContent),
|
||||
'observeHasContent',
|
||||
);
|
||||
},
|
||||
'observeContentKeyForHasContent',
|
||||
);
|
||||
}
|
||||
|
||||
#gotLabel(label: string | undefined) {
|
||||
@@ -679,9 +716,9 @@ export class UmbBlockWorkspaceContext<LayoutDataType extends UmbBlockLayoutBaseM
|
||||
// Did it exist before?
|
||||
if (this.getIsNew() === true) {
|
||||
// Remove the block?
|
||||
const contentKey = this.#layout.value?.contentKey;
|
||||
if (contentKey) {
|
||||
this.#blockEntries?.delete(contentKey);
|
||||
const key = this.#layout.value?.key;
|
||||
if (key) {
|
||||
this.#blockEntries?.delete(key);
|
||||
}
|
||||
} else {
|
||||
// Revert the layout, content & settings data to the original state: [NL]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user