Compare commits
63
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0331cd154b | ||
|
|
0f98b8342c | ||
|
|
b13f90046d | ||
|
|
304db8c28a | ||
|
|
723d3be905 | ||
|
|
5f37cd3d20 | ||
|
|
203cc0e380 | ||
|
|
442de9a803 | ||
|
|
eea6b9b2f1 | ||
|
|
b3b1ce2dcd | ||
|
|
69b69a5fc1 | ||
|
|
605cda9224 | ||
|
|
b65ad4e234 | ||
|
|
4e680c88bb | ||
|
|
e8f991f485 | ||
|
|
66891333a8 | ||
|
|
044c9d2c87 | ||
|
|
7b0c419ace | ||
|
|
6ec1b9f1e4 | ||
|
|
18069633b2 | ||
|
|
b24c29c647 | ||
|
|
f3f7fcc051 | ||
|
|
b6fa93edac | ||
|
|
b07a24ba55 | ||
|
|
42cb9b5c1e | ||
|
|
88657c1bb0 | ||
|
|
e91a25dcb6 | ||
|
|
81e63e7fa9 | ||
|
|
2f9d58fc68 | ||
|
|
e9ba06929f | ||
|
|
a5da305093 | ||
|
|
e1e5f527e4 | ||
|
|
f4e0e38824 | ||
|
|
fa09ce6214 | ||
|
|
dd8fb66300 | ||
|
|
a02db287ce | ||
|
|
e9c97f8c9b | ||
|
|
f016dbd223 | ||
|
|
da5669d9fc | ||
|
|
74aa3d9676 | ||
|
|
c671668e45 | ||
|
|
e337821d0e | ||
|
|
95fcf645dc | ||
|
|
b2aa04ae1a | ||
|
|
e270adc50f | ||
|
|
de282d0fbf | ||
|
|
fd5b8de533 | ||
|
|
e3d9b042c2 | ||
|
|
3586f137fc | ||
|
|
ced3db8542 | ||
|
|
1f2afbaa1d | ||
|
|
e8e1b6ef33 | ||
|
|
4803b4303d | ||
|
|
f4ae796b30 | ||
|
|
367d0386b6 | ||
|
|
5b54bed406 | ||
|
|
d3b60277d7 | ||
|
|
2ec6ff4ebc | ||
|
|
900938edd0 | ||
|
|
b9250c20e9 | ||
|
|
451b5f96e7 | ||
|
|
15ebae5025 | ||
|
|
ac3b17b390 |
@@ -6,8 +6,8 @@ body:
|
||||
- type: input
|
||||
id: "version"
|
||||
attributes:
|
||||
label: "Which Umbraco version are you using? (Please write the *exact* version, example: 10.1.0)"
|
||||
description: "Use the help icon in the Umbraco backoffice to find the version you're using"
|
||||
label: "Which Umbraco version are you using?"
|
||||
description: "Please write the *exact* version, example: `10.1.0`. Use the help icon in the Umbraco backoffice to find the version you're using"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
||||
+5
-4
@@ -1,11 +1,12 @@
|
||||
# [Umbraco CMS](https://umbraco.com)
|
||||
|
||||
[](../LICENSE.md)
|
||||
[](CONTRIBUTING.md)
|
||||

|
||||
[](https://discord.gg/umbraco)
|
||||
[](https://discord-chats.umbraco.com)
|
||||
[](https://www.nuget.org/packages/Umbraco.Cms)
|
||||
[](https://umbraco.visualstudio.com/Umbraco%20Cms/_build?definitionId=301)
|
||||
[](CONTRIBUTING.md)
|
||||
[](https://discord.gg/umbraco)
|
||||
[](https://discord-chats.umbraco.com)
|
||||

|
||||
|
||||
|
||||
### Umbraco is a free and open source .NET content management system. Our mission is to help you deliver delightful digital experiences by making Umbraco friendly, simpler and social.
|
||||
|
||||
@@ -45,8 +45,8 @@
|
||||
<PackageVersion Include="Asp.Versioning.Mvc" Version="8.1.0" />
|
||||
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
|
||||
<PackageVersion Include="Dazinator.Extensions.FileProviders" Version="2.0.0" />
|
||||
<PackageVersion Include="Examine" Version="3.6.0" />
|
||||
<PackageVersion Include="Examine.Core" Version="3.6.0" />
|
||||
<PackageVersion Include="Examine" Version="3.7.0" />
|
||||
<PackageVersion Include="Examine.Core" Version="3.7.0" />
|
||||
<PackageVersion Include="HtmlAgilityPack" Version="1.11.74" />
|
||||
<PackageVersion Include="JsonPatch.Net" Version="3.1.1" />
|
||||
<PackageVersion Include="K4os.Compression.LZ4" Version="1.3.8" />
|
||||
|
||||
@@ -9,9 +9,10 @@ schedules:
|
||||
branches:
|
||||
include:
|
||||
- v10/dev
|
||||
- v12/dev
|
||||
- v13/dev
|
||||
- v14/dev
|
||||
- v15/dev
|
||||
- v16/dev
|
||||
|
||||
steps:
|
||||
- checkout: none
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Services;
|
||||
@@ -11,5 +11,5 @@ internal sealed class RequestPreviewService : RequestHeaderHandler, IRequestPrev
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsPreview() => GetHeaderValue("Preview") == "true";
|
||||
public bool IsPreview() => string.Equals(GetHeaderValue("Preview"), "true", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
+22
-3
@@ -30,14 +30,33 @@ public class SearchDocumentItemController : DocumentItemControllerBase
|
||||
[NonAction]
|
||||
[Obsolete("Scheduled to be removed in v16, use the non obsoleted method instead")]
|
||||
public async Task<IActionResult> SearchFromParent(CancellationToken cancellationToken, string query, int skip = 0, int take = 100, Guid? parentId = null)
|
||||
=> await SearchFromParentWithAllowedTypes(cancellationToken, query, skip, take, parentId);
|
||||
=> await SearchWithTrashed(cancellationToken, query, null, skip, take, parentId);
|
||||
|
||||
[NonAction]
|
||||
[Obsolete("Scheduled to be removed in v16, use the non obsoleted method instead")]
|
||||
[ProducesResponseType(typeof(PagedModel<DocumentItemResponseModel>), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> SearchFromParentWithAllowedTypes(
|
||||
CancellationToken cancellationToken,
|
||||
string query,
|
||||
int skip = 0,
|
||||
int take = 100,
|
||||
Guid? parentId = null,
|
||||
[FromQuery] IEnumerable<Guid>? allowedDocumentTypes = null) =>
|
||||
await SearchWithTrashed(cancellationToken, query, null, skip, take, parentId, allowedDocumentTypes);
|
||||
|
||||
[HttpGet("search")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedModel<DocumentItemResponseModel>), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> SearchFromParentWithAllowedTypes(CancellationToken cancellationToken, string query, int skip = 0, int take = 100, Guid? parentId = null, [FromQuery]IEnumerable<Guid>? allowedDocumentTypes = null)
|
||||
public async Task<IActionResult> SearchWithTrashed(
|
||||
CancellationToken cancellationToken,
|
||||
string query,
|
||||
bool? trashed = null,
|
||||
int skip = 0,
|
||||
int take = 100,
|
||||
Guid? parentId = null,
|
||||
[FromQuery] IEnumerable<Guid>? allowedDocumentTypes = null)
|
||||
{
|
||||
PagedModel<IEntitySlim> searchResult = _indexedEntitySearchService.Search(UmbracoObjectTypes.Document, query, parentId, allowedDocumentTypes, skip, take);
|
||||
PagedModel<IEntitySlim> searchResult = _indexedEntitySearchService.Search(UmbracoObjectTypes.Document, query, parentId, allowedDocumentTypes, trashed, skip, take);
|
||||
var result = new PagedModel<DocumentItemResponseModel>
|
||||
{
|
||||
Items = searchResult.Items.OfType<IDocumentEntitySlim>().Select(_documentPresentationFactory.CreateItemResponseModel),
|
||||
|
||||
+32
-17
@@ -1,45 +1,60 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.DocumentType.Item;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Entities;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DocumentType.Item;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class SearchDocumentTypeItemController : DocumentTypeItemControllerBase
|
||||
{
|
||||
private readonly IEntitySearchService _entitySearchService;
|
||||
private readonly IContentTypeService _contentTypeService;
|
||||
private readonly IUmbracoMapper _mapper;
|
||||
private readonly IContentTypeSearchService _contentTypeSearchService;
|
||||
|
||||
[Obsolete("Please use ctor that only accepts IUmbracoMapper & IContentTypeSearchService, scheduled for removal in v17")]
|
||||
public SearchDocumentTypeItemController(IEntitySearchService entitySearchService, IContentTypeService contentTypeService, IUmbracoMapper mapper)
|
||||
: this(mapper, StaticServiceProvider.Instance.GetRequiredService<IContentTypeSearchService>())
|
||||
{
|
||||
_entitySearchService = entitySearchService;
|
||||
_contentTypeService = contentTypeService;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
[Obsolete("Please use ctor that only accepts IUmbracoMapper & IContentTypeSearchService, scheduled for removal in v17")]
|
||||
// We need to have this constructor, or else we get ambiguous constructor error
|
||||
public SearchDocumentTypeItemController(
|
||||
IEntitySearchService entitySearchService,
|
||||
IContentTypeService contentTypeService,
|
||||
IUmbracoMapper mapper,
|
||||
IContentTypeSearchService contentTypeSearchService)
|
||||
: this(mapper, contentTypeSearchService)
|
||||
{
|
||||
}
|
||||
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public SearchDocumentTypeItemController(IUmbracoMapper mapper, IContentTypeSearchService contentTypeSearchService)
|
||||
{
|
||||
_mapper = mapper;
|
||||
_contentTypeSearchService = contentTypeSearchService;
|
||||
}
|
||||
|
||||
[NonAction]
|
||||
[Obsolete("Scheduled to be removed in v16, use the non obsoleted method instead")]
|
||||
public async Task<IActionResult> Search(CancellationToken cancellationToken, string query, int skip = 0, int take = 100)
|
||||
=> await SearchDocumentType(cancellationToken, query, null, skip, take);
|
||||
|
||||
[HttpGet("search")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedModel<DocumentTypeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> Search(CancellationToken cancellationToken, string query, int skip = 0, int take = 100)
|
||||
public async Task<IActionResult> SearchDocumentType(CancellationToken cancellationToken, string query, bool? isElement = null, int skip = 0, int take = 100)
|
||||
{
|
||||
PagedModel<IEntitySlim> searchResult = _entitySearchService.Search(UmbracoObjectTypes.DocumentType, query, skip, take);
|
||||
if (searchResult.Items.Any() is false)
|
||||
{
|
||||
return await Task.FromResult(Ok(new PagedModel<DocumentTypeItemResponseModel> { Total = searchResult.Total }));
|
||||
}
|
||||
|
||||
IEnumerable<IContentType> contentTypes = _contentTypeService.GetMany(searchResult.Items.Select(item => item.Key).ToArray().EmptyNull());
|
||||
PagedModel<IContentType> contentTypes = await _contentTypeSearchService.SearchAsync(query, isElement, cancellationToken, skip, take);
|
||||
var result = new PagedModel<DocumentTypeItemResponseModel>
|
||||
{
|
||||
Items = _mapper.MapEnumerable<IContentType, DocumentTypeItemResponseModel>(contentTypes),
|
||||
Total = searchResult.Total
|
||||
Items = _mapper.MapEnumerable<IContentType, DocumentTypeItemResponseModel>(contentTypes.Items),
|
||||
Total = contentTypes.Total
|
||||
};
|
||||
|
||||
return Ok(result);
|
||||
|
||||
@@ -32,12 +32,17 @@ public class SearchMediaItemController : MediaItemControllerBase
|
||||
public async Task<IActionResult> SearchFromParent(CancellationToken cancellationToken, string query, int skip = 0, int take = 100, Guid? parentId = null)
|
||||
=> await SearchFromParentWithAllowedTypes(cancellationToken, query, skip, take, parentId);
|
||||
|
||||
[NonAction]
|
||||
[Obsolete("Scheduled to be removed in v16, use the non obsoleted method instead")]
|
||||
public async Task<IActionResult> SearchFromParentWithAllowedTypes(CancellationToken cancellationToken, string query, int skip = 0, int take = 100, Guid? parentId = null, [FromQuery]IEnumerable<Guid>? allowedMediaTypes = null)
|
||||
=> await SearchFromParentWithAllowedTypes(cancellationToken, query, null, skip, take, parentId);
|
||||
|
||||
[HttpGet("search")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedModel<MediaItemResponseModel>), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> SearchFromParentWithAllowedTypes(CancellationToken cancellationToken, string query, int skip = 0, int take = 100, Guid? parentId = null, [FromQuery]IEnumerable<Guid>? allowedMediaTypes = null)
|
||||
public async Task<IActionResult> SearchFromParentWithAllowedTypes(CancellationToken cancellationToken, string query, bool? trashed = null, int skip = 0, int take = 100, Guid? parentId = null, [FromQuery]IEnumerable<Guid>? allowedMediaTypes = null)
|
||||
{
|
||||
PagedModel<IEntitySlim> searchResult = _indexedEntitySearchService.Search(UmbracoObjectTypes.Media, query, parentId, allowedMediaTypes, skip, take);
|
||||
PagedModel<IEntitySlim> searchResult = _indexedEntitySearchService.Search(UmbracoObjectTypes.Media, query, parentId, allowedMediaTypes, trashed, skip, take);
|
||||
var result = new PagedModel<MediaItemResponseModel>
|
||||
{
|
||||
Items = searchResult.Items.OfType<IMediaEntitySlim>().Select(_mediaPresentationFactory.CreateItemResponseModel),
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Core.Security.OperationStatus;
|
||||
|
||||
@@ -20,7 +20,7 @@ public abstract class ClientCredentialsUserControllerBase : UserControllerBase
|
||||
.Build()),
|
||||
BackOfficeUserClientCredentialsOperationStatus.InvalidClientId => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Invalid client ID")
|
||||
.WithDetail("The specified client ID is invalid. A valid client ID can only contain [a-z], [A-Z], [0-9], and [-._~]. Furthermore, including the prefix it cannot be longer than 255 characters.")
|
||||
.WithDetail("The specified client ID is invalid. A valid client ID can only contain [a-z], [A-Z], [0-9], and [-._~]. Furthermore, including the prefix it cannot be longer than 100 characters.")
|
||||
.Build()),
|
||||
_ => StatusCode(StatusCodes.Status500InternalServerError, problemDetailsBuilder
|
||||
.WithTitle("Unknown client credentials operation status.")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<language alias="da" intName="Danish" localName="dansk" lcid="6" culture="da-DK">
|
||||
<creator>
|
||||
<name>The Umbraco community</name>
|
||||
@@ -125,7 +125,7 @@ Mange hilsner fra Umbraco robotten
|
||||
<key alias="outOfRangeMultipleItemsMaximum">De %0% enheder givet er større end det tilladte minimum af %1%.</key>
|
||||
<key alias="invalidStep">Værdien %0% passer ikke med den konfigureret trin værdi af %1% og mindste værdi af %2%.</key>
|
||||
<key alias="unexpectedRange">Værdien %0% forventes ikke at indeholde et spænd.</key>
|
||||
<key alias="stringLengthExceeded">Tekststrengen er længere end den tilladte længde på %0% tegn.</key>
|
||||
<key alias="stringLengthExceeded">Tekststrengen er længere end den tilladte længde på %0% tegn, %1% for mange.</key>
|
||||
<key alias="invalidRange">Værdien %0% forventes at have en værdi der er større end fra værdien.</key>
|
||||
<key alias="invalidObjectType">Det valgte indhold er af den forkerte type.</key>
|
||||
<key alias="notOneOfOptions">"Værdien '%0%' er ikke en af de tilgængelige valgmuligheder.</key>
|
||||
|
||||
@@ -378,7 +378,7 @@
|
||||
<key alias="invalidPattern">Value is invalid, it does not match the correct pattern</key>
|
||||
<key alias="entriesShort"><![CDATA[Minimum %0% entries, requires <strong>%1%</strong> more.]]></key>
|
||||
<key alias="entriesExceed"><![CDATA[Maximum %0% entries, <strong>%1%</strong> too many.]]></key>
|
||||
<key alias="stringLengthExceeded">The string length exceeds the maximum length of %0% characters.</key>
|
||||
<key alias="stringLengthExceeded">The string length exceeds the maximum length of %0% characters, %1% too many.</key>
|
||||
<key alias="entriesAreasMismatch">The content amount requirements are not met for one or more areas.</key>
|
||||
<key alias="invalidMemberGroupName">Invalid member group name</key>
|
||||
<key alias="invalidUserGroupName">Invalid user group name</key>
|
||||
|
||||
@@ -395,7 +395,7 @@
|
||||
<key alias="invalidRange">The value %0% is not expected to have a to value less than the from value</key>
|
||||
<key alias="entriesShort"><![CDATA[Minimum %0% entries, requires <strong>%1%</strong> more.]]></key>
|
||||
<key alias="entriesExceed"><![CDATA[Maximum %0% entries, <strong>%1%</strong> too many.]]></key>
|
||||
<key alias="stringLengthExceeded">The string length exceeds the maximum length of %0% characters.</key>
|
||||
<key alias="stringLengthExceeded">The string length exceeds the maximum length of %0% characters, %1% too many.</key>
|
||||
<key alias="entriesAreasMismatch">The content amount requirements are not met for one or more areas.</key>
|
||||
<key alias="invalidMediaType">The chosen media type is invalid.</key>
|
||||
<key alias="invalidContentType">The chosen content is of invalid type.</key>
|
||||
|
||||
@@ -11,7 +11,7 @@ public static class ContentBaseExtensions
|
||||
private static DefaultUrlSegmentProvider? _defaultUrlSegmentProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the URL segment for a specified content and culture.
|
||||
/// Gets a single URL segment for a specified content and culture.
|
||||
/// </summary>
|
||||
/// <param name="content">The content.</param>
|
||||
/// <param name="shortStringHelper"></param>
|
||||
@@ -19,29 +19,73 @@ public static class ContentBaseExtensions
|
||||
/// <param name="culture">The culture.</param>
|
||||
/// <param name="published">Whether to get the published or draft.</param>
|
||||
/// <returns>The URL segment.</returns>
|
||||
/// <remarks>
|
||||
/// If more than one URL segment provider is available, the first one that returns a non-null value will be returned.
|
||||
/// </remarks>
|
||||
public static string? GetUrlSegment(this IContentBase content, IShortStringHelper shortStringHelper, IEnumerable<IUrlSegmentProvider> urlSegmentProviders, string? culture = null, bool published = true)
|
||||
{
|
||||
if (content == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(content));
|
||||
}
|
||||
var urlSegment = GetUrlSegments(content, urlSegmentProviders, culture, published).FirstOrDefault();
|
||||
|
||||
if (urlSegmentProviders == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(urlSegmentProviders));
|
||||
}
|
||||
// Ensure we have at least the segment from the default URL provider returned.
|
||||
urlSegment ??= GetDefaultUrlSegment(shortStringHelper, content, culture, published);
|
||||
|
||||
var url = urlSegmentProviders.Select(p => p.GetUrlSegment(content, published, culture)).FirstOrDefault(u => u != null);
|
||||
if (url == null)
|
||||
return urlSegment;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all URL segments for a specified content and culture.
|
||||
/// </summary>
|
||||
/// <param name="content">The content.</param>
|
||||
/// <param name="shortStringHelper"></param>
|
||||
/// <param name="urlSegmentProviders"></param>
|
||||
/// <param name="culture">The culture.</param>
|
||||
/// <param name="published">Whether to get the published or draft.</param>
|
||||
/// <returns>The collection of URL segments.</returns>
|
||||
public static IEnumerable<string> GetUrlSegments(this IContentBase content, IShortStringHelper shortStringHelper, IEnumerable<IUrlSegmentProvider> urlSegmentProviders, string? culture = null, bool published = true)
|
||||
{
|
||||
var urlSegments = GetUrlSegments(content, urlSegmentProviders, culture, published).Distinct().ToList();
|
||||
|
||||
// Ensure we have at least the segment from the default URL provider returned.
|
||||
if (urlSegments.Count == 0)
|
||||
{
|
||||
if (_defaultUrlSegmentProvider == null)
|
||||
var defaultUrlSegment = GetDefaultUrlSegment(shortStringHelper, content, culture, published);
|
||||
if (defaultUrlSegment is not null)
|
||||
{
|
||||
_defaultUrlSegmentProvider = new DefaultUrlSegmentProvider(shortStringHelper);
|
||||
urlSegments.Add(defaultUrlSegment);
|
||||
}
|
||||
|
||||
url = _defaultUrlSegmentProvider.GetUrlSegment(content, culture); // be safe
|
||||
}
|
||||
|
||||
return url;
|
||||
return urlSegments;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> GetUrlSegments(
|
||||
IContentBase content,
|
||||
IEnumerable<IUrlSegmentProvider> urlSegmentProviders,
|
||||
string? culture,
|
||||
bool published)
|
||||
{
|
||||
foreach (IUrlSegmentProvider urlSegmentProvider in urlSegmentProviders)
|
||||
{
|
||||
var segment = urlSegmentProvider.GetUrlSegment(content, published, culture);
|
||||
if (string.IsNullOrEmpty(segment) == false)
|
||||
{
|
||||
yield return segment;
|
||||
|
||||
if (urlSegmentProvider.AllowAdditionalSegments is false)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string? GetDefaultUrlSegment(
|
||||
IShortStringHelper shortStringHelper,
|
||||
IContentBase content,
|
||||
string? culture,
|
||||
bool published)
|
||||
{
|
||||
_defaultUrlSegmentProvider ??= new DefaultUrlSegmentProvider(shortStringHelper);
|
||||
return _defaultUrlSegmentProvider.GetUrlSegment(content, published, culture);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,32 @@
|
||||
namespace Umbraco.Cms.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a URL segment for a published document.
|
||||
/// </summary>
|
||||
public class PublishedDocumentUrlSegment
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the document key.
|
||||
/// </summary>
|
||||
public required Guid DocumentKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the language Id.
|
||||
/// </summary>
|
||||
public required int LanguageId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the URL segment string.
|
||||
/// </summary>
|
||||
public required string UrlSegment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the URL segment is for a draft.
|
||||
/// </summary>
|
||||
public required bool IsDraft { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the URL segment is the primary one (first resolved from the collection of URL providers).
|
||||
/// </summary>
|
||||
public required bool IsPrimary { get; set; }
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ public class TextOnlyValueEditor : DataValueEditor
|
||||
_localizedTextService.Localize(
|
||||
"validation",
|
||||
"stringLengthExceeded",
|
||||
[maxCharacters.ToString()]),
|
||||
[maxCharacters.ToString(), (typedValue.Length - maxCharacters).ToString() ]),
|
||||
["value'"])
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1267,7 +1267,7 @@ public class ContentService : RepositoryService, IContentService
|
||||
// we need to guard against unsaved changes before proceeding; the content will be saved, but we're not firing any saved notifications
|
||||
if (HasUnsavedChanges(content))
|
||||
{
|
||||
return new PublishResult(PublishResultType.FailedPublishUnsavedChanges, EventMessagesFactory.Get(), content);
|
||||
return new PublishResult(PublishResultType.FailedPublishUnsavedChanges, evtMsgs, content);
|
||||
}
|
||||
|
||||
if (content.Name != null && content.Name.Length > 255)
|
||||
@@ -1324,13 +1324,8 @@ public class ContentService : RepositoryService, IContentService
|
||||
|
||||
// Change state to publishing
|
||||
content.PublishedState = PublishedState.Publishing;
|
||||
var publishingNotification = new ContentPublishingNotification(content, evtMsgs);
|
||||
if (scope.Notifications.PublishCancelable(publishingNotification))
|
||||
{
|
||||
return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
|
||||
}
|
||||
|
||||
PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, publishingNotification.State, userId);
|
||||
PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, new Dictionary<string, object?>(), userId);
|
||||
scope.Complete();
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.Persistence.Querying;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Services.Changes;
|
||||
@@ -147,6 +148,16 @@ public class ContentTypeService : ContentTypeServiceBase<IContentTypeRepository,
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<IContentType>> GetByQueryAsync(IQuery<IContentType> query, CancellationToken cancellationToken)
|
||||
{
|
||||
using ICoreScope scope = ScopeProvider.CreateCoreScope();
|
||||
// that one is special because it works across content, media and member types
|
||||
scope.ReadLock(Constants.Locks.ContentTypes);
|
||||
IEnumerable<IContentType> contentTypes = Repository.Get(query);
|
||||
scope.Complete();
|
||||
return contentTypes;
|
||||
}
|
||||
|
||||
protected override void DeleteItemsOfTypes(IEnumerable<int> typeIds)
|
||||
{
|
||||
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services;
|
||||
|
||||
public interface IContentTypeSearchService
|
||||
{
|
||||
Task<PagedModel<IContentType>> SearchAsync(string query, bool? isElement, CancellationToken cancellationToken, int skip = 0, int take = 100);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Persistence.Querying;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services;
|
||||
@@ -30,4 +31,6 @@ public interface IContentTypeService : IContentTypeBaseService<IContentType>
|
||||
/// <param name="aliases"></param>
|
||||
/// <returns></returns>
|
||||
IEnumerable<int> GetAllContentTypeIds(string[] aliases);
|
||||
|
||||
Task<IEnumerable<IContentType>> GetByQueryAsync(IQuery<IContentType> query, CancellationToken cancellationToken) => Task.FromResult(Enumerable.Empty<IContentType>());
|
||||
}
|
||||
|
||||
@@ -1,38 +1,97 @@
|
||||
using Umbraco.Cms.Core.Media.EmbedProviders;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Routing;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Defines operations for handling document URLs.
|
||||
/// </summary>
|
||||
public interface IDocumentUrlService
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes the service and ensure the content in the database is correct with the current configuration.
|
||||
/// </summary>
|
||||
/// <param name="forceEmpty"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <param name="forceEmpty">Forces an early return when we know there are no routes (i.e. on install).</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
Task InitAsync(bool forceEmpty, CancellationToken cancellationToken);
|
||||
|
||||
Task RebuildAllUrlsAsync();
|
||||
/// <summary>
|
||||
/// Gets the Url from a document key, culture and segment. Preview urls are returned if isPreview is true.
|
||||
/// Rebuilds all document URLs.
|
||||
/// </summary>
|
||||
Task RebuildAllUrlsAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a single URL segment from a document key and culture. Preview urls are returned if isDraft is true.
|
||||
/// </summary>
|
||||
/// <param name="documentKey">The key of the document.</param>
|
||||
/// <param name="culture">The culture code.</param>
|
||||
/// <param name="isDraft">Whether to get the url of the draft or published document.</param>
|
||||
/// <returns>The url of the document.</returns>
|
||||
/// <returns>A URL segment for the document.</returns>
|
||||
/// <remarks>If more than one segment is available, the first retrieved and indicated as primary will be returned.</remarks>
|
||||
string? GetUrlSegment(Guid documentKey, string culture, bool isDraft);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the URL segments from a document key and culture. Preview urls are returned if isDraft is true.
|
||||
/// </summary>
|
||||
/// <param name="documentKey">The key of the document.</param>
|
||||
/// <param name="culture">The culture code.</param>
|
||||
/// <param name="isDraft">Whether to get the url of the draft or published document.</param>
|
||||
/// <returns>The URL segments for the document.</returns>
|
||||
IEnumerable<string> GetUrlSegments(Guid documentKey, string culture, bool isDraft)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
/// <summary>
|
||||
/// Creates or updates the URL segments for a single document.
|
||||
/// </summary>
|
||||
/// <param name="key">The document key.</param>
|
||||
Task CreateOrUpdateUrlSegmentsAsync(Guid key);
|
||||
|
||||
/// <summary>
|
||||
/// Creates or updates the URL segments for a document and it's descendants.
|
||||
/// </summary>
|
||||
/// <param name="key">The document key.</param>
|
||||
Task CreateOrUpdateUrlSegmentsWithDescendantsAsync(Guid key);
|
||||
|
||||
/// <summary>
|
||||
/// Creates or updates the URL segments for a collection of documents.
|
||||
/// </summary>
|
||||
/// <param name="documents">The document collection.</param>
|
||||
Task CreateOrUpdateUrlSegmentsAsync(IEnumerable<IContent> documents);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes all URLs from the cache for a collection of document keys.
|
||||
/// </summary>
|
||||
/// <param name="documentKeys">The collection of document keys.</param>
|
||||
Task DeleteUrlsFromCacheAsync(IEnumerable<Guid> documentKeys);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a document key by route.
|
||||
/// </summary>
|
||||
/// <param name="route">The route.</param>
|
||||
/// <param name="culture">The culture code.</param>
|
||||
/// <param name="documentStartNodeId">The document start node Id.</param>
|
||||
/// <param name="isDraft">Whether to get the url of the draft or published document.</param>
|
||||
/// <returns>The document key, or null if not found.</returns>
|
||||
Guid? GetDocumentKeyByRoute(string route, string? culture, int? documentStartNodeId, bool isDraft);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all the URLs for a given content key.
|
||||
/// </summary>
|
||||
/// <param name="contentKey">The content key.</param>
|
||||
[Obsolete("This method is obsolete and will be removed in future versions. Use IPublishedUrlInfoProvider.GetAllAsync instead. Scheduled for removal in Umbraco 17.")]
|
||||
Task<IEnumerable<UrlInfo>> ListUrlsAsync(Guid contentKey);
|
||||
Task CreateOrUpdateUrlSegmentsWithDescendantsAsync(Guid key);
|
||||
Task CreateOrUpdateUrlSegmentsAsync(Guid key);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the legacy route format for a document key and culture.
|
||||
/// </summary>
|
||||
/// <param name="key">The key of the document.</param>
|
||||
/// <param name="culture">The culture code.</param>
|
||||
/// <param name="isDraft">Whether to get the url of the draft or published document.</param>
|
||||
/// <returns></returns>
|
||||
string GetLegacyRouteFormat(Guid key, string? culture, bool isDraft);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether any URLs have been cached.
|
||||
/// </summary>
|
||||
bool HasAny();
|
||||
}
|
||||
|
||||
@@ -20,7 +20,19 @@ public interface IIndexedEntitySearchService
|
||||
PagedModel<IEntitySlim> Search(UmbracoObjectTypes objectType, string query, Guid? parentId, int skip = 0, int take = 100, bool ignoreUserStartNodes = false)
|
||||
=> Search(objectType,query, skip, take, ignoreUserStartNodes);
|
||||
|
||||
// default implementation to avoid breaking changes falls back to old behaviour
|
||||
[Obsolete("Please use the method that accepts all parameters. Will be removed in V17.")]
|
||||
PagedModel<IEntitySlim> Search(UmbracoObjectTypes objectType, string query, Guid? parentId, IEnumerable<Guid>? contentTypeIds, int skip = 0, int take = 100, bool ignoreUserStartNodes = false)
|
||||
=> Search(objectType,query, skip, take, ignoreUserStartNodes);
|
||||
|
||||
// default implementation to avoid breaking changes falls back to old behaviour
|
||||
PagedModel<IEntitySlim> Search(
|
||||
UmbracoObjectTypes objectType,
|
||||
string query,
|
||||
Guid? parentId,
|
||||
IEnumerable<Guid>? contentTypeIds,
|
||||
bool? trashed,
|
||||
int skip = 0,
|
||||
int take = 100,
|
||||
bool ignoreUserStartNodes = false)
|
||||
=> Search(objectType,query, skip, take, ignoreUserStartNodes);
|
||||
}
|
||||
|
||||
@@ -2680,7 +2680,7 @@ internal partial class UserService : RepositoryService, IUserService
|
||||
}
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"^[\w\d\-\._~]{1,255}$")]
|
||||
[GeneratedRegex(@"^[\w\d\-\._~]{1,100}$")]
|
||||
private static partial Regex ValidClientId();
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -8,6 +8,16 @@ namespace Umbraco.Cms.Core.Strings;
|
||||
/// <remarks>Url segments should comply with IETF RFCs regarding content, encoding, etc.</remarks>
|
||||
public interface IUrlSegmentProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the URL segment provider allows additional segments after providing one.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If set to true, when more than one URL segment provider is available, futher providers after this one in the collection will be called
|
||||
/// even if the current provider provides a segment.
|
||||
/// If false, the provider will terminate the chain of URL segment providers if it provides a segment.
|
||||
/// </remarks>
|
||||
bool AllowAdditionalSegments => false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the URL segment for a specified content and culture.
|
||||
/// </summary>
|
||||
@@ -20,6 +30,18 @@ public interface IUrlSegmentProvider
|
||||
/// URL per culture.
|
||||
/// </remarks>
|
||||
string? GetUrlSegment(IContentBase content, string? culture = null);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the URL segment for a specified content, published status and and culture.
|
||||
/// </summary>
|
||||
/// <param name="content">The content.</param>
|
||||
/// <param name="culture">The culture.</param>
|
||||
/// <returns>The URL segment.</returns>
|
||||
/// <remarks>
|
||||
/// This is for when Umbraco is capable of managing more than one URL
|
||||
/// per content, in 1-to-1 multilingual configurations. Then there would be one
|
||||
/// URL per culture.
|
||||
/// </remarks>
|
||||
string? GetUrlSegment(IContentBase content, bool published, string? culture = null) => GetUrlSegment(content, culture);
|
||||
|
||||
// TODO: For the 301 tracking, we need to add another extended interface to this so that
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
@@ -61,7 +62,19 @@ public class BackOfficeExamineSearcher : IBackOfficeExamineSearcher
|
||||
out long totalFound,
|
||||
string? searchFrom = null,
|
||||
bool ignoreUserStartNodes = false)
|
||||
=> Search(query, entityType, pageSize, pageIndex, out totalFound, null, searchFrom, ignoreUserStartNodes);
|
||||
=> Search(query, entityType, pageSize, pageIndex, out totalFound, null, null, searchFrom, ignoreUserStartNodes);
|
||||
|
||||
[Obsolete("Please use the method that accepts all parameters. Will be removed in V17.")]
|
||||
public IEnumerable<ISearchResult> Search(
|
||||
string query,
|
||||
UmbracoEntityTypes entityType,
|
||||
int pageSize,
|
||||
long pageIndex,
|
||||
out long totalFound,
|
||||
string[]? contentTypeAliases,
|
||||
string? searchFrom = null,
|
||||
bool ignoreUserStartNodes = false)
|
||||
=> Search(query, entityType, pageSize, pageIndex, out totalFound, contentTypeAliases, null, searchFrom, ignoreUserStartNodes);
|
||||
|
||||
public IEnumerable<ISearchResult> Search(
|
||||
string query,
|
||||
@@ -70,6 +83,7 @@ public class BackOfficeExamineSearcher : IBackOfficeExamineSearcher
|
||||
long pageIndex,
|
||||
out long totalFound,
|
||||
string[]? contentTypeAliases,
|
||||
bool? trashed,
|
||||
string? searchFrom = null,
|
||||
bool ignoreUserStartNodes = false)
|
||||
{
|
||||
@@ -133,6 +147,12 @@ public class BackOfficeExamineSearcher : IBackOfficeExamineSearcher
|
||||
? currentUser.CalculateMediaStartNodeIds(_entityService, _appCaches)
|
||||
: Array.Empty<int>();
|
||||
AppendPath(sb, UmbracoObjectTypes.Media, allMediaStartNodes, searchFrom, ignoreUserStartNodes, _entityService);
|
||||
|
||||
if (trashed.HasValue)
|
||||
{
|
||||
AppendRequiredTrashPath(trashed.Value, sb, Constants.System.RecycleBinMedia);
|
||||
}
|
||||
|
||||
break;
|
||||
case UmbracoEntityTypes.Document:
|
||||
type = "content";
|
||||
@@ -146,6 +166,12 @@ public class BackOfficeExamineSearcher : IBackOfficeExamineSearcher
|
||||
? currentUser.CalculateContentStartNodeIds(_entityService, _appCaches)
|
||||
: Array.Empty<int>();
|
||||
AppendPath(sb, UmbracoObjectTypes.Document, allContentStartNodes, searchFrom, ignoreUserStartNodes, _entityService);
|
||||
|
||||
if (trashed.HasValue)
|
||||
{
|
||||
AppendRequiredTrashPath(trashed.Value, sb, Constants.System.RecycleBinContent);
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException("The " + typeof(BackOfficeExamineSearcher) +
|
||||
@@ -181,6 +207,14 @@ public class BackOfficeExamineSearcher : IBackOfficeExamineSearcher
|
||||
return result;
|
||||
}
|
||||
|
||||
private void AppendRequiredTrashPath(bool trashed, StringBuilder sb, int recycleBinId)
|
||||
{
|
||||
var requiredOrNotString = trashed ? "+" : "!";
|
||||
var trashPath = $"-1,{recycleBinId}";
|
||||
trashPath = trashPath.Replace("-", "\\-").Replace(",", "\\,");
|
||||
sb.Append($"{requiredOrNotString}__Path:{trashPath}\\,* ");
|
||||
}
|
||||
|
||||
private bool BuildQuery(StringBuilder sb, string query, string? searchFrom, List<string> fields, string type)
|
||||
{
|
||||
//build a lucene query:
|
||||
@@ -394,6 +428,7 @@ public class BackOfficeExamineSearcher : IBackOfficeExamineSearcher
|
||||
searchFromId > 0
|
||||
? entityService.GetAllPaths(objectType, searchFromId).FirstOrDefault()
|
||||
: null;
|
||||
|
||||
if (entityPath != null)
|
||||
{
|
||||
// find... only what's underneath
|
||||
|
||||
@@ -78,6 +78,7 @@ public static partial class UmbracoBuilderExtensions
|
||||
builder.Services.AddUnique<IContentListViewService, ContentListViewService>();
|
||||
builder.Services.AddUnique<IMediaListViewService, MediaListViewService>();
|
||||
builder.Services.AddUnique<IEntitySearchService, EntitySearchService>();
|
||||
builder.Services.AddUnique<IContentTypeSearchService, ContentTypeSearchService>();
|
||||
builder.Services.AddUnique<IIndexedEntitySearchService, IndexedEntitySearchService>();
|
||||
builder.Services.TryAddTransient<IReservedFieldNamesService, ReservedFieldNamesService>();
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ public interface IBackOfficeExamineSearcher
|
||||
string? searchFrom = null,
|
||||
bool ignoreUserStartNodes = false);
|
||||
|
||||
// default implementation to avoid breaking changes falls back to old behaviour
|
||||
[Obsolete("Please use the method that accepts all parameters. Will be removed in V17.")]
|
||||
IEnumerable<ISearchResult> Search(
|
||||
string query,
|
||||
UmbracoEntityTypes entityType,
|
||||
@@ -28,5 +28,18 @@ public interface IBackOfficeExamineSearcher
|
||||
string[]? contentTypeAliases,
|
||||
string? searchFrom = null,
|
||||
bool ignoreUserStartNodes = false)
|
||||
=> Search(query, entityType, pageSize, pageIndex, out totalFound, searchFrom, ignoreUserStartNodes);
|
||||
|
||||
// default implementation to avoid breaking changes falls back to old behaviour
|
||||
IEnumerable<ISearchResult> Search(
|
||||
string query,
|
||||
UmbracoEntityTypes entityType,
|
||||
int pageSize,
|
||||
long pageIndex,
|
||||
out long totalFound,
|
||||
string[]? contentTypeAliases,
|
||||
bool? trashed,
|
||||
string? searchFrom = null,
|
||||
bool ignoreUserStartNodes = false)
|
||||
=> Search(query, entityType, pageSize, pageIndex, out totalFound, null, searchFrom, ignoreUserStartNodes);
|
||||
}
|
||||
|
||||
@@ -111,5 +111,8 @@ public class UmbracoPlan : MigrationPlan
|
||||
|
||||
// To 15.3.0
|
||||
To<V_15_3_0.AddNameAndDescriptionToWebhooks>("{7B11F01E-EE33-4B0B-81A1-F78F834CA45B}");
|
||||
|
||||
// To 15.4.0
|
||||
To<V_15_4_0.UpdateDocumentUrlToPersistMultipleSegmentsPerDocument>("{A9E72794-4036-4563-B543-1717C73B8879}");
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ public class AddNameAndDescriptionToWebhooks : MigrationBase
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning($"Table {Constants.DatabaseSchema.Tables.Webhook} does not exist so the addition of the name and description by columnss in migration {nameof(AddNameAndDescriptionToWebhooks)} cannot be completed.");
|
||||
Logger.LogWarning($"Table {Constants.DatabaseSchema.Tables.Webhook} does not exist so the addition of the name and description by columns in migration {nameof(AddNameAndDescriptionToWebhooks)} cannot be completed.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_15_4_0;
|
||||
|
||||
/// <summary>
|
||||
/// Migration to make necessary schema updates to support multiple segments per document.
|
||||
/// </summary>
|
||||
public class UpdateDocumentUrlToPersistMultipleSegmentsPerDocument : MigrationBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UpdateDocumentUrlToPersistMultipleSegmentsPerDocument"/> class.
|
||||
/// </summary>
|
||||
public UpdateDocumentUrlToPersistMultipleSegmentsPerDocument(IMigrationContext context)
|
||||
: base(context)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void Migrate()
|
||||
{
|
||||
Logger.LogDebug("Schema updates to {TableName} for support of multiple segments per document", Constants.DatabaseSchema.Tables.DocumentUrl);
|
||||
|
||||
if (TableExists(Constants.DatabaseSchema.Tables.DocumentUrl))
|
||||
{
|
||||
ExtendUniqueIndexAcrossSegmentField();
|
||||
AddAndPopulateIsPrimaryColumn();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Table {Constants.DatabaseSchema.Tables.DocumentUrl} does not exist so the migration {nameof(UpdateDocumentUrlToPersistMultipleSegmentsPerDocument)} could not be completed.");
|
||||
}
|
||||
}
|
||||
|
||||
private void ExtendUniqueIndexAcrossSegmentField()
|
||||
{
|
||||
Logger.LogDebug("Extending the unique index on {TableName} to include the urlSegment column", Constants.DatabaseSchema.Tables.DocumentUrl);
|
||||
|
||||
var indexName = "IX_" + Constants.DatabaseSchema.Tables.DocumentUrl;
|
||||
if (IndexExists(indexName))
|
||||
{
|
||||
DeleteIndex<DocumentUrlDto>(indexName);
|
||||
}
|
||||
|
||||
CreateIndex<DocumentUrlDto>(indexName);
|
||||
}
|
||||
|
||||
private void AddAndPopulateIsPrimaryColumn()
|
||||
{
|
||||
const string IsPrimaryColumnName = "isPrimary";
|
||||
|
||||
Logger.LogDebug("Adding the {Column} column {TableName} to include the urlSegment column", IsPrimaryColumnName, Constants.DatabaseSchema.Tables.DocumentUrl);
|
||||
|
||||
var columns = Context.SqlContext.SqlSyntax.GetColumnsInSchema(Context.Database).ToList();
|
||||
if (columns
|
||||
.SingleOrDefault(x => x.TableName == Constants.DatabaseSchema.Tables.DocumentUrl && x.ColumnName == IsPrimaryColumnName) is null)
|
||||
{
|
||||
AddColumn<DocumentUrlDto>(Constants.DatabaseSchema.Tables.DocumentUrl, IsPrimaryColumnName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ public class DocumentUrlDto
|
||||
[PrimaryKeyColumn(Clustered = false, AutoIncrement = true)]
|
||||
public int NodeId { get; set; }
|
||||
|
||||
[Index(IndexTypes.UniqueClustered, ForColumns = "uniqueId, languageId, isDraft", Name = "IX_" + TableName)]
|
||||
[Index(IndexTypes.UniqueClustered, ForColumns = "uniqueId, languageId, isDraft, urlSegment", Name = "IX_" + TableName)]
|
||||
[Column("uniqueId")]
|
||||
[ForeignKey(typeof(NodeDto), Column = "uniqueId")]
|
||||
public Guid UniqueId { get; set; }
|
||||
@@ -28,14 +28,11 @@ public class DocumentUrlDto
|
||||
[ForeignKey(typeof(LanguageDto))]
|
||||
public int LanguageId { get; set; }
|
||||
|
||||
//
|
||||
// [Column("segment")]
|
||||
// [NullSetting(NullSetting = NullSettings.Null)]
|
||||
// [Length(PropertyDataDto.SegmentLength)]
|
||||
// public string Segment { get; set; } = string.Empty;
|
||||
|
||||
[Column("urlSegment")]
|
||||
[NullSetting(NullSetting = NullSettings.NotNull)]
|
||||
public string UrlSegment { get; set; } = string.Empty;
|
||||
|
||||
[Column("isPrimary")]
|
||||
[Constraint(Default = 1)]
|
||||
public bool IsPrimary { get; set; }
|
||||
}
|
||||
|
||||
+11
-29
@@ -1,18 +1,11 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NPoco;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Entities;
|
||||
using Umbraco.Cms.Core.Persistence.Querying;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Factories;
|
||||
using Umbraco.Cms.Infrastructure.Scoping;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
|
||||
|
||||
public class DocumentUrlRepository : IDocumentUrlRepository
|
||||
@@ -36,14 +29,15 @@ public class DocumentUrlRepository : IDocumentUrlRepository
|
||||
|
||||
public void Save(IEnumerable<PublishedDocumentUrlSegment> publishedDocumentUrlSegments)
|
||||
{
|
||||
//TODO avoid this is called as first thing on first restart after install
|
||||
// TODO: avoid this is called as first thing on first restart after install
|
||||
IEnumerable<Guid> documentKeys = publishedDocumentUrlSegments.Select(x => x.DocumentKey).Distinct();
|
||||
|
||||
Dictionary<(Guid UniqueId, int LanguageId, bool isDraft), DocumentUrlDto> dtoDictionary = publishedDocumentUrlSegments.Select(BuildDto).ToDictionary(x=> (x.UniqueId, x.LanguageId, x.IsDraft));
|
||||
Dictionary<(Guid UniqueId, int LanguageId, bool isDraft, string urlSegment), DocumentUrlDto> dtoDictionary = publishedDocumentUrlSegments
|
||||
.Select(BuildDto)
|
||||
.ToDictionary(x => (x.UniqueId, x.LanguageId, x.IsDraft, x.UrlSegment));
|
||||
|
||||
var toUpdate = new List<DocumentUrlDto>();
|
||||
var toDelete = new List<int>();
|
||||
var toInsert = dtoDictionary.Values.ToDictionary(x => (x.UniqueId, x.LanguageId, x.IsDraft));
|
||||
var toInsert = dtoDictionary.Values.ToDictionary(x => (x.UniqueId, x.LanguageId, x.IsDraft, x.UrlSegment));
|
||||
|
||||
foreach (IEnumerable<Guid> group in documentKeys.InGroupsOf(Constants.Sql.MaxParameterCount))
|
||||
{
|
||||
@@ -57,18 +51,12 @@ public class DocumentUrlRepository : IDocumentUrlRepository
|
||||
|
||||
foreach (DocumentUrlDto existing in existingUrlsInBatch)
|
||||
{
|
||||
|
||||
if (dtoDictionary.TryGetValue((existing.UniqueId, existing.LanguageId, existing.IsDraft), out DocumentUrlDto? found))
|
||||
if (dtoDictionary.TryGetValue((existing.UniqueId, existing.LanguageId, existing.IsDraft, existing.UrlSegment), out DocumentUrlDto? found))
|
||||
{
|
||||
found.NodeId = existing.NodeId;
|
||||
|
||||
// Only update if the url segment is different
|
||||
if (found.UrlSegment != existing.UrlSegment)
|
||||
{
|
||||
toUpdate.Add(found);
|
||||
}
|
||||
// if we found it, we know we should not insert it as a new
|
||||
toInsert.Remove((found.UniqueId, found.LanguageId, found.IsDraft));
|
||||
// If we found it, we know we should not insert it as a new record.
|
||||
toInsert.Remove((found.UniqueId, found.LanguageId, found.IsDraft, found.UrlSegment));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -83,14 +71,6 @@ public class DocumentUrlRepository : IDocumentUrlRepository
|
||||
Database.DeleteMany<DocumentUrlDto>().Where(x => toDelete.Contains(x.NodeId)).Execute();
|
||||
}
|
||||
|
||||
if (toUpdate.Any())
|
||||
{
|
||||
foreach (DocumentUrlDto updated in toUpdate)
|
||||
{
|
||||
Database.Update(updated);
|
||||
}
|
||||
}
|
||||
|
||||
Database.InsertBulk(toInsert.Values);
|
||||
}
|
||||
|
||||
@@ -115,7 +95,8 @@ public class DocumentUrlRepository : IDocumentUrlRepository
|
||||
UrlSegment = dto.UrlSegment,
|
||||
DocumentKey = dto.UniqueId,
|
||||
LanguageId = dto.LanguageId,
|
||||
IsDraft = dto.IsDraft
|
||||
IsDraft = dto.IsDraft,
|
||||
IsPrimary = dto.IsPrimary
|
||||
};
|
||||
|
||||
private DocumentUrlDto BuildDto(PublishedDocumentUrlSegment model)
|
||||
@@ -126,6 +107,7 @@ public class DocumentUrlRepository : IDocumentUrlRepository
|
||||
UniqueId = model.DocumentKey,
|
||||
LanguageId = model.LanguageId,
|
||||
IsDraft = model.IsDraft,
|
||||
IsPrimary = model.IsPrimary,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ public class UmbracoTreeSearcherFields : IUmbracoTreeSearcherFields
|
||||
UmbracoExamineFieldNames.CategoryFieldName,
|
||||
"parentID",
|
||||
UmbracoExamineFieldNames.ItemTypeFieldName,
|
||||
UmbracoExamineFieldNames.IndexPathFieldName,
|
||||
};
|
||||
|
||||
private readonly ISet<string> _backOfficeMediaFieldsToLoad =
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Persistence.Querying;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Services;
|
||||
|
||||
internal sealed class ContentTypeSearchService : IContentTypeSearchService
|
||||
{
|
||||
private readonly ISqlContext _sqlContext;
|
||||
private readonly IContentTypeService _contentTypeService;
|
||||
|
||||
public ContentTypeSearchService(ISqlContext sqlContext, IContentTypeService contentTypeService)
|
||||
{
|
||||
_sqlContext = sqlContext;
|
||||
_contentTypeService = contentTypeService;
|
||||
}
|
||||
|
||||
public async Task<PagedModel<IContentType>> SearchAsync(string query, bool? isElement, CancellationToken cancellationToken, int skip = 0, int take = 100)
|
||||
{
|
||||
// if the query is a GUID, search for that explicitly
|
||||
Guid.TryParse(query, out Guid guidQuery);
|
||||
|
||||
IQuery<IContentType> nameQuery = isElement is not null ?
|
||||
_sqlContext.Query<IContentType>().Where(x => (x.Name!.Contains(query) || x.Key == guidQuery) && x.IsElement == isElement) :
|
||||
_sqlContext.Query<IContentType>().Where(x => x.Name!.Contains(query) || x.Key == guidQuery);
|
||||
|
||||
IContentType[] contentTypes = (await _contentTypeService.GetByQueryAsync(nameQuery, cancellationToken)).ToArray();
|
||||
|
||||
return new PagedModel<IContentType>
|
||||
{
|
||||
Items = contentTypes.Skip(skip).Take(take),
|
||||
Total = contentTypes.Count()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,7 @@ internal sealed class IndexedEntitySearchService : IIndexedEntitySearchService
|
||||
int skip = 0,
|
||||
int take = 100,
|
||||
bool ignoreUserStartNodes = false)
|
||||
=> Search(objectType, query, parentId, null, skip, take, ignoreUserStartNodes);
|
||||
=> Search(objectType, query, parentId, null, null, skip, take, ignoreUserStartNodes);
|
||||
|
||||
public PagedModel<IEntitySlim> Search(
|
||||
UmbracoObjectTypes objectType,
|
||||
@@ -62,6 +62,17 @@ internal sealed class IndexedEntitySearchService : IIndexedEntitySearchService
|
||||
int skip = 0,
|
||||
int take = 100,
|
||||
bool ignoreUserStartNodes = false)
|
||||
=> Search(objectType, query, parentId, contentTypeIds, null, skip, take, ignoreUserStartNodes);
|
||||
|
||||
public PagedModel<IEntitySlim> Search(
|
||||
UmbracoObjectTypes objectType,
|
||||
string query,
|
||||
Guid? parentId,
|
||||
IEnumerable<Guid>? contentTypeIds,
|
||||
bool? trashed,
|
||||
int skip = 0,
|
||||
int take = 100,
|
||||
bool ignoreUserStartNodes = false)
|
||||
{
|
||||
UmbracoEntityTypes entityType = objectType switch
|
||||
{
|
||||
@@ -91,6 +102,7 @@ internal sealed class IndexedEntitySearchService : IIndexedEntitySearchService
|
||||
pageNumber,
|
||||
out var totalFound,
|
||||
contentTypeAliases,
|
||||
trashed,
|
||||
ignoreUserStartNodes: ignoreUserStartNodes,
|
||||
searchFrom: parentId?.ToString());
|
||||
|
||||
|
||||
@@ -291,8 +291,8 @@ internal class MediaCacheService : IMediaCacheService
|
||||
|
||||
private HybridCacheEntryOptions GetSeedEntryOptions() => new()
|
||||
{
|
||||
Expiration = _cacheSettings.SeedCacheDuration,
|
||||
LocalCacheExpiration = _cacheSettings.SeedCacheDuration,
|
||||
Expiration = _cacheSettings.Entry.Media.SeedCacheDuration,
|
||||
LocalCacheExpiration = _cacheSettings.Entry.Media.SeedCacheDuration,
|
||||
};
|
||||
|
||||
private string GetCacheKey(Guid key, bool preview) => preview ? $"{key}+draft" : $"{key}";
|
||||
|
||||
@@ -35,7 +35,9 @@ const config: StorybookConfig = {
|
||||
typescript: {
|
||||
check: true,
|
||||
},
|
||||
docs: {},
|
||||
docs: {
|
||||
autodocs: true
|
||||
},
|
||||
managerHead(head, { configType }) {
|
||||
const base = process.env.VITE_BASE_PATH || '/';
|
||||
const injections = [
|
||||
|
||||
+740
-1219
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@umbraco-cms/backoffice",
|
||||
"license": "MIT",
|
||||
"version": "15.3.0-rc",
|
||||
"version": "15.4.0-rc",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": null,
|
||||
@@ -216,8 +216,8 @@
|
||||
"@tiptap/pm": "2.11.5",
|
||||
"@tiptap/starter-kit": "2.11.5",
|
||||
"@types/diff": "^7.0.1",
|
||||
"@umbraco-ui/uui": "^1.13.0-rc.1",
|
||||
"@umbraco-ui/uui-css": "^1.13.0-rc.0",
|
||||
"@umbraco-ui/uui": "1.13.0-rc.2",
|
||||
"@umbraco-ui/uui-css": "1.13.0-rc.2",
|
||||
"diff": "^7.0.0",
|
||||
"dompurify": "^3.2.4",
|
||||
"element-internals-polyfill": "^1.3.13",
|
||||
@@ -238,12 +238,13 @@
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^16.0.0",
|
||||
"@rollup/plugin-replace": "^6.0.2",
|
||||
"@storybook/addon-a11y": "^8.5.7",
|
||||
"@storybook/addon-actions": "^8.5.7",
|
||||
"@storybook/addon-essentials": "^8.5.7",
|
||||
"@storybook/addon-links": "^8.5.7",
|
||||
"@storybook/web-components": "^8.5.7",
|
||||
"@storybook/web-components-vite": "^8.5.7",
|
||||
"@storybook/addon-a11y": "^8.6.7",
|
||||
"@storybook/addon-actions": "^8.6.7",
|
||||
"@storybook/addon-essentials": "^8.6.7",
|
||||
"@storybook/addon-links": "^8.6.7",
|
||||
"@storybook/manager-api": "^8.6.7",
|
||||
"@storybook/web-components": "^8.6.7",
|
||||
"@storybook/web-components-vite": "^8.6.7",
|
||||
"@types/chai": "^5.0.1",
|
||||
"@types/eslint__js": "^8.42.3",
|
||||
"@types/mocha": "^10.0.10",
|
||||
@@ -268,14 +269,12 @@
|
||||
"msw": "^1.3.2",
|
||||
"playwright-msw": "^3.0.1",
|
||||
"prettier": "3.5.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"rollup": "^4.34.8",
|
||||
"rollup-plugin-esbuild": "^6.2.0",
|
||||
"rollup-plugin-import-css": "^3.5.8",
|
||||
"simple-icons": "^14.7.0",
|
||||
"storybook": "^8.5.7",
|
||||
"storybook": "^8.6.7",
|
||||
"svgo": "^3.3.2",
|
||||
"tiny-glob": "^0.2.9",
|
||||
"tsc-alias": "^1.8.10",
|
||||
@@ -283,7 +282,7 @@
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.24.1",
|
||||
"typescript-json-schema": "^0.65.1",
|
||||
"vite": "^6.1.0",
|
||||
"vite": "^6.2.2",
|
||||
"vite-plugin-static-copy": "^2.2.0",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"web-component-analyzer": "^2.0.0"
|
||||
|
||||
@@ -1278,6 +1278,11 @@ export default {
|
||||
defineRootNode: 'Pick root node',
|
||||
defineXPathOrigin: 'Specify via XPath',
|
||||
defineDynamicRoot: 'Specify a Dynamic Root',
|
||||
unsupportedHeadline: (type?: string) =>
|
||||
`<strong>Unsupported ${type ?? 'content'} items</strong><br>The following content is no longer supported in this Editor.`,
|
||||
unsupportedMessage:
|
||||
'If you still require this content, please contact your administrator. Otherwise you can remove it.',
|
||||
unsupportedRemove: 'Remove unsupported items?',
|
||||
},
|
||||
dynamicRoot: {
|
||||
configurationTitle: 'Dynamic Root Query',
|
||||
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
/* This Source Code has been derived from Tiptiz.
|
||||
* https://github.com/tiptiz/editor/blob/main/packages/tiptiz-extension-indent/src/indent.ts
|
||||
* SPDX-License-Identifier: MIT
|
||||
* Copyright © 2024 Owen Kriz.
|
||||
* Modifications are licensed under the MIT License.
|
||||
*/
|
||||
|
||||
import type { Dispatch } from '@tiptap/core';
|
||||
import type { EditorState, Transaction } from '@tiptap/pm/state';
|
||||
|
||||
import { Extension } from '@tiptap/core';
|
||||
import { AllSelection, TextSelection } from '@tiptap/pm/state';
|
||||
|
||||
export interface TextIndentOptions {
|
||||
minLevel: number;
|
||||
maxLevel: number;
|
||||
types: Array<string>;
|
||||
}
|
||||
|
||||
export const TextIndent = Extension.create<TextIndentOptions>({
|
||||
name: 'textIndent',
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
minLevel: 0,
|
||||
maxLevel: 5,
|
||||
types: ['heading', 'paragraph', 'listItem', 'taskItem'],
|
||||
};
|
||||
},
|
||||
|
||||
addGlobalAttributes() {
|
||||
return [
|
||||
{
|
||||
types: this.options.types,
|
||||
attributes: {
|
||||
indent: {
|
||||
default: null,
|
||||
parseHTML: (element) => {
|
||||
const minLevel = this.options.minLevel;
|
||||
const maxLevel = this.options.maxLevel;
|
||||
const indent = element.style.textIndent;
|
||||
return indent ? Math.max(minLevel, Math.min(maxLevel, parseInt(indent, 10))) : null;
|
||||
},
|
||||
renderHTML: (attributes) => {
|
||||
if (!attributes.indent) return {};
|
||||
return {
|
||||
style: `text-indent: ${attributes.indent}rem;`,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
const updateNodeIndentMarkup = (tr: Transaction, pos: number, delta: number) => {
|
||||
const node = tr.doc.nodeAt(pos);
|
||||
if (!node) return tr;
|
||||
|
||||
const minLevel = this.options.minLevel;
|
||||
const maxLevel = this.options.maxLevel;
|
||||
|
||||
let level = (node.attrs.indent || 0) + delta;
|
||||
level = Math.max(minLevel, Math.min(maxLevel, parseInt(level, 10)));
|
||||
|
||||
if (level === node.attrs.indent) return tr;
|
||||
|
||||
return tr.setNodeMarkup(pos, node.type, { ...node.attrs, indent: level }, node.marks);
|
||||
};
|
||||
|
||||
const updateIndentLevel = (tr: Transaction, delta: number) => {
|
||||
if (tr.selection instanceof TextSelection || tr.selection instanceof AllSelection) {
|
||||
const { from, to } = tr.selection;
|
||||
tr.doc.nodesBetween(from, to, (node, pos) => {
|
||||
if (this.options.types.includes(node.type.name)) {
|
||||
tr = updateNodeIndentMarkup(tr, pos, delta);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
return tr;
|
||||
};
|
||||
|
||||
type CommanderArgs = {
|
||||
tr: Transaction;
|
||||
state: EditorState;
|
||||
dispatch: Dispatch;
|
||||
};
|
||||
|
||||
const commanderFactory = (direction: number) => () =>
|
||||
function chainHandler({ tr, state, dispatch }: CommanderArgs) {
|
||||
const { selection } = state;
|
||||
tr.setSelection(selection);
|
||||
tr = updateIndentLevel(tr, direction);
|
||||
if (tr.docChanged) {
|
||||
if (dispatch instanceof Function) dispatch(tr);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
return {
|
||||
textIndent: commanderFactory(1),
|
||||
textOutdent: commanderFactory(-1),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
textIndent: {
|
||||
textIndent: () => ReturnType;
|
||||
textOutdent: () => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
Vendored
+109
@@ -0,0 +1,109 @@
|
||||
import type { UUIPopoverContainerElement } from '../../uui/index.js';
|
||||
import { Extension } from '@tiptap/core';
|
||||
import { Editor } from '@tiptap/core';
|
||||
import { EditorState, Plugin, PluginKey } from '@tiptap/pm/state';
|
||||
import { EditorView } from '@tiptap/pm/view';
|
||||
import type { PluginView } from '@tiptap/pm/state';
|
||||
|
||||
export interface UmbTiptapBubbleMenuElement extends HTMLElement {
|
||||
editor?: Editor;
|
||||
}
|
||||
|
||||
export type UmbBubbleMenuPluginProps = {
|
||||
unique: string;
|
||||
placement?: UUIPopoverContainerElement['placement'];
|
||||
elementName?: string | null;
|
||||
shouldShow?:
|
||||
| ((props: { editor: Editor; view: EditorView; state: EditorState; from: number; to: number }) => boolean)
|
||||
| null;
|
||||
};
|
||||
|
||||
export type UmbBubbleMenuOptions = UmbBubbleMenuPluginProps;
|
||||
|
||||
export const UmbBubbleMenu = Extension.create<UmbBubbleMenuOptions>({
|
||||
name: 'umbBubbleMenu',
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
unique: 'umb-tiptap-menu',
|
||||
placement: 'top',
|
||||
elementName: null,
|
||||
shouldShow: null,
|
||||
};
|
||||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
if (!this.options.unique || !this.options.elementName) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
UmbBubbleMenuPlugin(this.editor, {
|
||||
unique: this.options.unique,
|
||||
placement: this.options.placement,
|
||||
elementName: this.options.elementName,
|
||||
shouldShow: this.options.shouldShow,
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
class UmbBubbleMenuPluginView implements PluginView {
|
||||
#editor: Editor;
|
||||
|
||||
#popover: UUIPopoverContainerElement;
|
||||
|
||||
#shouldShow: UmbBubbleMenuPluginProps['shouldShow'];
|
||||
|
||||
constructor(editor: Editor, view: EditorView, props: UmbBubbleMenuPluginProps) {
|
||||
this.#editor = editor;
|
||||
|
||||
this.#shouldShow = props.shouldShow ?? null;
|
||||
|
||||
this.#popover = document.createElement('uui-popover-container') as UUIPopoverContainerElement;
|
||||
this.#popover.id = props.unique;
|
||||
this.#popover.setAttribute('placement', props.placement ?? 'top');
|
||||
this.#popover.setAttribute('popover', 'manual');
|
||||
|
||||
if (props.elementName) {
|
||||
const menu = document.createElement(props.elementName) as UmbTiptapBubbleMenuElement;
|
||||
menu.editor = editor;
|
||||
this.#popover.appendChild(menu);
|
||||
}
|
||||
|
||||
view.dom.parentNode?.appendChild(this.#popover);
|
||||
|
||||
this.update(view, null);
|
||||
}
|
||||
|
||||
update(view: EditorView, prevState: EditorState | null) {
|
||||
const editor = this.#editor;
|
||||
|
||||
const { state } = view;
|
||||
const { selection } = state;
|
||||
|
||||
const { ranges } = selection;
|
||||
const from = Math.min(...ranges.map((range) => range.$from.pos));
|
||||
const to = Math.max(...ranges.map((range) => range.$to.pos));
|
||||
|
||||
const shouldShow = this.#shouldShow?.({ editor, view, state, from, to });
|
||||
|
||||
if (!shouldShow) {
|
||||
this.#popover.hidePopover();
|
||||
} else {
|
||||
this.#popover.showPopover();
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.#popover.remove();
|
||||
}
|
||||
}
|
||||
|
||||
export const UmbBubbleMenuPlugin = (editor: Editor, props: UmbBubbleMenuPluginProps) => {
|
||||
return new Plugin({
|
||||
view(editorView) {
|
||||
return new UmbBubbleMenuPluginView(editor, editorView, props);
|
||||
},
|
||||
});
|
||||
};
|
||||
+29
-12
@@ -1,11 +1,9 @@
|
||||
import { UmbBubbleMenuPlugin } from './tiptap-umb-bubble-menu.extension.js';
|
||||
import { CellSelection, TableMap } from '@tiptap/pm/tables';
|
||||
import { Decoration, DecorationSet } from '@tiptap/pm/view';
|
||||
import { EditorState } from '@tiptap/pm/state';
|
||||
import { EditorView } from '@tiptap/pm/view';
|
||||
import { findParentNode, mergeAttributes, Editor, Node } from '@tiptap/core';
|
||||
import { Decoration, DecorationSet, EditorView } from '@tiptap/pm/view';
|
||||
import { EditorState, Plugin, Selection, Transaction } from '@tiptap/pm/state';
|
||||
import { findParentNode, Editor } from '@tiptap/core';
|
||||
import { Node as PMNode, ResolvedPos } from '@tiptap/pm/model';
|
||||
import { Plugin } from '@tiptap/pm/state';
|
||||
import { Selection, Transaction } from '@tiptap/pm/state';
|
||||
import { Table } from '@tiptap/extension-table';
|
||||
import { TableCell } from '@tiptap/extension-table-cell';
|
||||
import { TableHeader } from '@tiptap/extension-table-header';
|
||||
@@ -43,7 +41,16 @@ export const UmbTableHeader = TableHeader.extend({
|
||||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
const { editor } = this;
|
||||
return [
|
||||
UmbBubbleMenuPlugin(this.editor, {
|
||||
unique: 'table-column-menu',
|
||||
placement: 'top',
|
||||
elementName: 'umb-tiptap-table-column-menu',
|
||||
shouldShow(props) {
|
||||
return isColumnGripSelected(props);
|
||||
},
|
||||
}),
|
||||
new Plugin({
|
||||
props: {
|
||||
decorations: (state) => {
|
||||
@@ -62,16 +69,16 @@ export const UmbTableHeader = TableHeader.extend({
|
||||
decorations.push(
|
||||
Decoration.widget(pos + 1, () => {
|
||||
const colSelected = isColumnSelected(index)(selection);
|
||||
const className = colSelected ? 'grip-column selected' : 'grip-column';
|
||||
|
||||
const grip = document.createElement('a');
|
||||
grip.appendChild(document.createElement('uui-symbol-more'));
|
||||
|
||||
grip.className = className;
|
||||
grip.className = colSelected ? 'grip-column selected' : 'grip-column';
|
||||
grip.setAttribute('popovertarget', colSelected ? 'table-column-menu' : '');
|
||||
|
||||
grip.addEventListener('mousedown', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
|
||||
this.editor.view.dispatch(selectColumn(index)(this.editor.state.tr));
|
||||
});
|
||||
|
||||
@@ -126,7 +133,16 @@ export const UmbTableCell = TableCell.extend({
|
||||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
const { editor } = this;
|
||||
return [
|
||||
UmbBubbleMenuPlugin(this.editor, {
|
||||
unique: 'table-row-menu',
|
||||
placement: 'left',
|
||||
elementName: 'umb-tiptap-table-row-menu',
|
||||
shouldShow(props) {
|
||||
return isRowGripSelected(props);
|
||||
},
|
||||
}),
|
||||
new Plugin({
|
||||
props: {
|
||||
decorations: (state) => {
|
||||
@@ -145,12 +161,13 @@ export const UmbTableCell = TableCell.extend({
|
||||
decorations.push(
|
||||
Decoration.widget(pos + 1, () => {
|
||||
const rowSelected = isRowSelected(index)(selection);
|
||||
const className = rowSelected ? 'grip-row selected' : 'grip-row';
|
||||
|
||||
const grip = document.createElement('a');
|
||||
grip.appendChild(document.createElement('uui-symbol-more'));
|
||||
|
||||
grip.className = className;
|
||||
grip.className = rowSelected ? 'grip-row selected' : 'grip-row';
|
||||
grip.setAttribute('popovertarget', rowSelected ? 'table-row-menu' : '');
|
||||
|
||||
grip.addEventListener('mousedown', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
@@ -449,7 +466,7 @@ const isColumnGripSelected = ({
|
||||
return !!gripColumn;
|
||||
};
|
||||
|
||||
export const isRowGripSelected = ({
|
||||
const isRowGripSelected = ({
|
||||
editor,
|
||||
view,
|
||||
state,
|
||||
|
||||
@@ -35,7 +35,9 @@ export * from './extensions/tiptap-figure.extension.js';
|
||||
export * from './extensions/tiptap-span.extension.js';
|
||||
export * from './extensions/tiptap-html-global-attributes.extension.js';
|
||||
export * from './extensions/tiptap-text-direction-extension.js';
|
||||
export * from './extensions/tiptap-text-indent-extension.js';
|
||||
export * from './extensions/tiptap-trailing-node.extension.js';
|
||||
export * from './extensions/tiptap-umb-bubble-menu.extension.js';
|
||||
export * from './extensions/tiptap-umb-embedded-media.extension.js';
|
||||
export * from './extensions/tiptap-umb-image.extension.js';
|
||||
export * from './extensions/tiptap-umb-link.extension.js';
|
||||
|
||||
@@ -270,10 +270,6 @@ export const data: Array<UmbMockDataTypeModel> = [
|
||||
alias: 'ignoreUserStartNodes',
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
alias: 'showOpenButton',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
alias: 'filter',
|
||||
value: '',
|
||||
@@ -1037,6 +1033,7 @@ export const data: Array<UmbMockDataTypeModel> = [
|
||||
'Umb.Tiptap.Table',
|
||||
'Umb.Tiptap.TextAlign',
|
||||
'Umb.Tiptap.TextDirection',
|
||||
'Umb.Tiptap.TextIndent',
|
||||
'Umb.Tiptap.Underline',
|
||||
],
|
||||
},
|
||||
@@ -1069,6 +1066,7 @@ export const data: Array<UmbMockDataTypeModel> = [
|
||||
'Umb.Tiptap.Toolbar.TextDirectionRtl',
|
||||
'Umb.Tiptap.Toolbar.TextDirectionLtr',
|
||||
],
|
||||
['Umb.Tiptap.Toolbar.TextIndent', 'Umb.Tiptap.Toolbar.TextOutdent'],
|
||||
[
|
||||
'Umb.Tiptap.Toolbar.BulletList',
|
||||
'Umb.Tiptap.Toolbar.OrderedList',
|
||||
|
||||
@@ -2,10 +2,7 @@ import { UMB_BLOCK_GRID_PROPERTY_EDITOR_UI_ALIAS } from '../property-editors/con
|
||||
import { manifests as blockManifests } from './block/manifests.js';
|
||||
import { manifests as gridBlockManifests } from './grid-block/manifests.js';
|
||||
import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry';
|
||||
import {
|
||||
UMB_PROPERTY_HAS_VALUE_CONDITION_ALIAS,
|
||||
UMB_WRITABLE_PROPERTY_CONDITION_ALIAS,
|
||||
} from '@umbraco-cms/backoffice/property';
|
||||
import { UMB_PROPERTY_HAS_VALUE_CONDITION_ALIAS } from '@umbraco-cms/backoffice/property';
|
||||
|
||||
const forPropertyEditorUis = [UMB_BLOCK_GRID_PROPERTY_EDITOR_UI_ALIAS];
|
||||
|
||||
@@ -24,9 +21,6 @@ export const manifests: Array<UmbExtensionManifest | UmbExtensionManifestKind> =
|
||||
name: 'Block Grid Copy To Clipboard Property Action',
|
||||
forPropertyEditorUis,
|
||||
conditions: [
|
||||
{
|
||||
alias: UMB_WRITABLE_PROPERTY_CONDITION_ALIAS,
|
||||
},
|
||||
{
|
||||
alias: UMB_PROPERTY_HAS_VALUE_CONDITION_ALIAS,
|
||||
},
|
||||
|
||||
@@ -23,9 +23,6 @@ export const manifests: Array<UmbExtensionManifest> = [
|
||||
name: 'Block List Copy To Clipboard Property Action',
|
||||
forPropertyEditorUis,
|
||||
conditions: [
|
||||
{
|
||||
alias: UMB_WRITABLE_PROPERTY_CONDITION_ALIAS,
|
||||
},
|
||||
{
|
||||
alias: UMB_PROPERTY_HAS_VALUE_CONDITION_ALIAS,
|
||||
},
|
||||
|
||||
+2
-1
@@ -68,6 +68,7 @@ export class UmbBlockListEntryElement extends UmbLitElement implements UmbProper
|
||||
|
||||
@state()
|
||||
_showContentEdit = false;
|
||||
|
||||
@state()
|
||||
_hasSettings = false;
|
||||
|
||||
@@ -426,7 +427,7 @@ export class UmbBlockListEntryElement extends UmbLitElement implements UmbProper
|
||||
look="secondary"
|
||||
color=${this._contentInvalid ? 'danger' : ''}
|
||||
href=${this._workspaceEditContentPath}>
|
||||
<uui-icon name=${this._exposed === false ? 'icon-add' : 'icon-edit'}></uui-icon>
|
||||
<uui-icon name=${this._exposed === false && this._isReadOnly === false ? 'icon-add' : 'icon-edit'}></uui-icon>
|
||||
${this._contentInvalid
|
||||
? html`<uui-badge attention color="danger" label="Invalid content">!</uui-badge>`
|
||||
: nothing}
|
||||
|
||||
+1
-1
@@ -195,7 +195,7 @@ export class UmbBlockElementManager<LayoutDataType extends UmbBlockLayoutBaseMod
|
||||
throw new Error(`Editor Alias of "${property.dataType.unique}" not found.`);
|
||||
}
|
||||
|
||||
const entry = { ...variantId.toObject(), alias, editorAlias, value } as UmbBlockDataValueModel<ValueType>;
|
||||
const entry = { editorAlias, ...variantId.toObject(), alias, value } as UmbBlockDataValueModel<ValueType>;
|
||||
|
||||
const currentData = this.getData();
|
||||
if (currentData) {
|
||||
|
||||
+12
@@ -1,4 +1,5 @@
|
||||
import type { UmbBlockElementManager } from './block-element-manager.js';
|
||||
import { UMB_BLOCK_WORKSPACE_CONTEXT } from './block-workspace.context-token.js';
|
||||
import type { UmbPropertyDatasetContext } from '@umbraco-cms/backoffice/property';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import type { UmbVariantId } from '@umbraco-cms/backoffice/variant';
|
||||
@@ -23,5 +24,16 @@ export class UmbBlockElementPropertyDatasetContext
|
||||
this.getName = elementManager.getName;
|
||||
this.culture = createObservablePart(elementManager.variantId, (v) => v?.culture);
|
||||
this.segment = createObservablePart(elementManager.variantId, (v) => v?.segment);
|
||||
|
||||
this.consumeContext(UMB_BLOCK_WORKSPACE_CONTEXT, (workspace) => {
|
||||
this.observe(
|
||||
workspace.readOnlyState.states,
|
||||
(states) => {
|
||||
const isReadOnly = states.some((state) => state.variantId.equal(elementManager.getVariantId()));
|
||||
this._readOnly.setValue(isReadOnly);
|
||||
},
|
||||
'umbObserveReadOnlyStates',
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+6
-4
@@ -78,10 +78,12 @@ export class UmbBlockWorkspaceViewEditTabElement extends UmbLitElement {
|
||||
override render() {
|
||||
return html`
|
||||
${this._hasProperties
|
||||
? html` <umb-block-workspace-view-edit-properties
|
||||
.managerName=${this.#managerName}
|
||||
data-mark="property-group:root"
|
||||
.containerId=${this._containerId}></umb-block-workspace-view-edit-properties>`
|
||||
? html`<uui-box>
|
||||
<umb-block-workspace-view-edit-properties
|
||||
.managerName=${this.#managerName}
|
||||
data-mark="property-group:root"
|
||||
.containerId=${this._containerId}></umb-block-workspace-view-edit-properties>
|
||||
</uui-box>`
|
||||
: ''}
|
||||
${this.hideSingleGroup && this._groups.length === 1
|
||||
? this.renderGroup(this._groups[0])
|
||||
|
||||
+1
-1
@@ -149,7 +149,7 @@ export class UmbMultipleColorPickerItemInputElement extends UUIFormControlMixin(
|
||||
return html`
|
||||
<umb-form-validation-message id="validation-message" @invalid=${this.#onInvalid} @valid=${this.#onValid}>
|
||||
<div id="item">
|
||||
${this.disabled || this.readonly ? nothing : html`<uui-icon name="icon-navigation"></uui-icon>`}
|
||||
${this.disabled || this.readonly ? nothing : html`<uui-icon name="icon-grib"></uui-icon>`}
|
||||
<div class="color-wrapper">
|
||||
<uui-input
|
||||
id="input"
|
||||
|
||||
+6
-2
@@ -85,7 +85,7 @@ export class UmbInputMultipleTextStringItemElement extends UUIFormControlMixin(U
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
${this.disabled || this.readonly ? nothing : html`<uui-icon name="icon-navigation" class="handle"></uui-icon>`}
|
||||
${this.disabled || this.readonly ? nothing : html`<uui-icon name="icon-grib" class="handle"></uui-icon>`}
|
||||
|
||||
<umb-form-validation-message id="validation-message" @invalid=${this.#onInvalid} @valid=${this.#onValid}>
|
||||
<uui-input
|
||||
@@ -136,7 +136,11 @@ export class UmbInputMultipleTextStringItemElement extends UUIFormControlMixin(U
|
||||
}
|
||||
|
||||
.handle {
|
||||
cursor: move;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
+1
-4
@@ -160,10 +160,7 @@ export class UmbContentTypeWorkspaceViewEditGroupElement extends UmbLitElement {
|
||||
return html`
|
||||
<div slot="header" class="drag-handle">
|
||||
<div>
|
||||
${when(
|
||||
this.sortModeActive && this._hasOwnerContainer,
|
||||
() => html`<uui-icon name="icon-navigation"></uui-icon>`,
|
||||
)}
|
||||
${when(this.sortModeActive && this._hasOwnerContainer, () => html`<uui-icon name="icon-grib"></uui-icon>`)}
|
||||
<uui-input
|
||||
id="group-name"
|
||||
label=${this.localize.term('contentTypeEditor_group')}
|
||||
|
||||
+1
-1
@@ -260,7 +260,7 @@ export class UmbContentTypeDesignEditorPropertyElement extends UmbLitElement {
|
||||
if (!this.property) return;
|
||||
return html`
|
||||
<div class="sortable">
|
||||
<uui-icon name="icon-navigation"></uui-icon>
|
||||
<uui-icon name="icon-grib"></uui-icon>
|
||||
<span>${this.property.name}</span>
|
||||
<span style="color: var(--uui-color-disabled-contrast)">(${this.property.alias})</span>
|
||||
</div>
|
||||
|
||||
+2
-1
@@ -173,7 +173,8 @@ export class UmbContentTypeDesignEditorTabElement extends UmbLitElement {
|
||||
? html`
|
||||
<uui-box class="${this._hasProperties ? '' : 'opaque'}">
|
||||
<umb-content-type-design-editor-properties
|
||||
.containerId=${this.containerId}></umb-content-type-design-editor-properties>
|
||||
.containerId=${this.containerId}
|
||||
.editContentTypePath=${this._editContentTypePath}></umb-content-type-design-editor-properties>
|
||||
</uui-box>
|
||||
`
|
||||
: nothing
|
||||
|
||||
+2
-2
@@ -433,7 +433,7 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements
|
||||
`
|
||||
: ''}
|
||||
<uui-button look="outline" label=${sortButtonText} compact @click=${this.#toggleSortMode}>
|
||||
<uui-icon name="icon-navigation"></uui-icon>
|
||||
<uui-icon name="icon-height"></uui-icon>
|
||||
${sortButtonText}
|
||||
</uui-button>
|
||||
</div>
|
||||
@@ -498,7 +498,7 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements
|
||||
if (this._sortModeActive) {
|
||||
return html`<div class="tab">
|
||||
${ownedTab
|
||||
? html`<uui-icon name="icon-navigation" class="drag-${tab.id}"> </uui-icon>${tabName}
|
||||
? html`<uui-icon name="icon-grib" class="drag-${tab.id}"> </uui-icon>${tabName}
|
||||
<uui-input
|
||||
label="sort order"
|
||||
type="number"
|
||||
|
||||
+4
-4
@@ -44,8 +44,8 @@ export abstract class UmbElementPropertyDatasetContext<
|
||||
#propertyVariantIdMap = new UmbBasicState<UmbPropertyVariantIdMapType>([]);
|
||||
private readonly _propertyVariantIdMap = this.#propertyVariantIdMap.asObservable();
|
||||
|
||||
#readOnly = new UmbBooleanState(false);
|
||||
public readOnly = this.#readOnly.asObservable();
|
||||
protected _readOnly = new UmbBooleanState(false);
|
||||
public readOnly = this._readOnly.asObservable();
|
||||
|
||||
getEntityType(): string {
|
||||
return this._dataOwner.getEntityType();
|
||||
@@ -56,7 +56,7 @@ export abstract class UmbElementPropertyDatasetContext<
|
||||
abstract getName(): string | undefined;
|
||||
|
||||
getReadOnly() {
|
||||
return this.#readOnly.getValue();
|
||||
return this._readOnly.getValue();
|
||||
}
|
||||
|
||||
constructor(host: UmbControllerHost, dataOwner: DataOwnerType, variantId?: UmbVariantId) {
|
||||
@@ -73,7 +73,7 @@ export abstract class UmbElementPropertyDatasetContext<
|
||||
this._dataOwner.readOnlyState.states,
|
||||
(states) => {
|
||||
const isReadOnly = states.some((state) => state.variantId.equal(this.#variantId));
|
||||
this.#readOnly.setValue(isReadOnly);
|
||||
this._readOnly.setValue(isReadOnly);
|
||||
},
|
||||
null,
|
||||
);
|
||||
|
||||
+1
-1
@@ -479,7 +479,7 @@ export abstract class UmbContentDetailWorkspaceContextBase<
|
||||
}
|
||||
|
||||
// Notice the order of the properties is important for our JSON String Compare function. [NL]
|
||||
const entry = { editorAlias, alias, ...variantId.toObject(), value } as UmbElementValueModel;
|
||||
const entry = { editorAlias, ...variantId.toObject(), alias, value } as UmbElementValueModel;
|
||||
|
||||
const currentData = this.getData();
|
||||
if (currentData) {
|
||||
|
||||
@@ -892,6 +892,11 @@
|
||||
"name": "icon-grid",
|
||||
"file": "grid-3x3.svg"
|
||||
},
|
||||
{
|
||||
"name": "icon-drag",
|
||||
"file": "grip-vertical.svg",
|
||||
"internal": true
|
||||
},
|
||||
{
|
||||
"name": "icon-hammer",
|
||||
"file": "hammer.svg"
|
||||
|
||||
@@ -698,6 +698,10 @@ path: () => import("./icons/icon-graduate.js"),
|
||||
name: "icon-grid",
|
||||
path: () => import("./icons/icon-grid.js"),
|
||||
},{
|
||||
name: "icon-drag",
|
||||
hidden: true,
|
||||
path: () => import("./icons/icon-drag.js"),
|
||||
},{
|
||||
name: "icon-hammer",
|
||||
path: () => import("./icons/icon-hammer.js"),
|
||||
},{
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export default `<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.75" class="lucide lucide-grip-vertical" viewBox="0 0 24 24"><circle cx="9" cy="12" r="1"/><circle cx="9" cy="5" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="15" cy="19" r="1"/></svg>`;
|
||||
@@ -115,7 +115,7 @@ export class UmbPickerInputContext<
|
||||
color: 'danger',
|
||||
headline: `Remove ${item.name}?`,
|
||||
content: 'Are you sure you want to remove this item',
|
||||
confirmLabel: 'Remove',
|
||||
confirmLabel: '#actions_remove',
|
||||
});
|
||||
|
||||
this.#removeItem(unique);
|
||||
|
||||
+7
-3
@@ -5,8 +5,7 @@ import { html, customElement, state, nothing, css } from '@umbraco-cms/backoffic
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||
|
||||
const elementName = 'umb-picker-search-field';
|
||||
@customElement(elementName)
|
||||
@customElement('umb-picker-search-field')
|
||||
export class UmbPickerSearchFieldElement extends UmbLitElement {
|
||||
@state()
|
||||
_query: string = '';
|
||||
@@ -66,6 +65,11 @@ export class UmbPickerSearchFieldElement extends UmbLitElement {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
uui-input [slot='prepend'] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#divider {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
@@ -84,6 +88,6 @@ export class UmbPickerSearchFieldElement extends UmbLitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
[elementName]: UmbPickerSearchFieldElement;
|
||||
'umb-picker-search-field': UmbPickerSearchFieldElement;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-3
@@ -6,8 +6,7 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import type { UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search';
|
||||
import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity';
|
||||
|
||||
const elementName = 'umb-picker-search-result';
|
||||
@customElement(elementName)
|
||||
@customElement('umb-picker-search-result')
|
||||
export class UmbPickerSearchResultElement extends UmbLitElement {
|
||||
@state()
|
||||
_query?: UmbSearchRequestArgs;
|
||||
@@ -67,6 +66,6 @@ export class UmbPickerSearchResultElement extends UmbLitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
[elementName]: UmbPickerSearchResultElement;
|
||||
'umb-picker-search-result': UmbPickerSearchResultElement;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ export class UmbTemporaryFileConfigRepository extends UmbRepositoryBase implemen
|
||||
/**
|
||||
* Promise that resolves when the repository has been initialized, i.e. when the configuration has been fetched from the server.
|
||||
*/
|
||||
initialized: Promise<void>;
|
||||
initialized;
|
||||
|
||||
#dataStore?: typeof UMB_TEMPORARY_FILE_CONFIG_STORE_CONTEXT.TYPE;
|
||||
#dataSource = new UmbTemporaryFileConfigServerDataSource(this);
|
||||
|
||||
+11
-1
@@ -22,6 +22,15 @@ export class UmbTemporaryFileManager<
|
||||
readonly #queue = new UmbArrayState<UploadableItem>([], (item) => item.temporaryUnique);
|
||||
public readonly queue = this.#queue.asObservable();
|
||||
|
||||
/**
|
||||
* Gets the temporary file configuration.
|
||||
* @returns {Promise<UmbTemporaryFileConfigRepository>} The temporary file configuration.
|
||||
*/
|
||||
async getConfiguration(): Promise<UmbTemporaryFileConfigRepository> {
|
||||
await this.#temporaryFileConfigRepository.initialized;
|
||||
return this.#temporaryFileConfigRepository;
|
||||
}
|
||||
|
||||
async uploadOne(uploadableItem: UploadableItem, options?: UmbUploadOptions<UploadableItem>): Promise<UploadableItem> {
|
||||
this.#queue.setValue([]);
|
||||
|
||||
@@ -78,7 +87,8 @@ export class UmbTemporaryFileManager<
|
||||
}
|
||||
|
||||
async #validateItem(item: UploadableItem): Promise<boolean> {
|
||||
let maxFileSize = await this.observe(this.#temporaryFileConfigRepository.part('maxFileSize')).asPromise();
|
||||
const config = await this.getConfiguration();
|
||||
let maxFileSize = await this.observe(config.part('maxFileSize')).asPromise();
|
||||
if (maxFileSize) {
|
||||
// Convert from kilobytes to bytes
|
||||
maxFileSize *= 1024;
|
||||
|
||||
+3
-3
@@ -289,7 +289,7 @@ export class UmbSortChildrenOfModalElement extends UmbModalBaseElement<
|
||||
#renderChild(item: UmbTreeItemModel) {
|
||||
// TODO: find a way to get the icon for the item. We do not have the icon in the tree item model.
|
||||
return html` <uui-table-row id="content-node" data-unique=${item.unique} class="${this._isSorting ? 'hidden' : ''}">
|
||||
<uui-table-cell><umb-icon name="icon-navigation"></umb-icon></uui-table-cell>
|
||||
<uui-table-cell class="handle"><umb-icon name="icon-grib"></umb-icon></uui-table-cell>
|
||||
<uui-table-cell>${item.name}</uui-table-cell>
|
||||
<uui-table-cell>${this.#renderCreateDate(item)}</uui-table-cell>
|
||||
</uui-table-row>`;
|
||||
@@ -341,8 +341,8 @@ export class UmbSortChildrenOfModalElement extends UmbModalBaseElement<
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
uui-icon[name='icon-navigation'] {
|
||||
cursor: hand;
|
||||
uui-table-row[id='content-node']:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
uui-box {
|
||||
|
||||
+1
-1
@@ -93,9 +93,9 @@ export class UmbDocumentBlueprintServerDataSource implements UmbDetailDataSource
|
||||
values: data.values.map((value) => {
|
||||
return {
|
||||
editorAlias: value.editorAlias,
|
||||
alias: value.alias,
|
||||
culture: value.culture || null,
|
||||
segment: value.segment || null,
|
||||
alias: value.alias,
|
||||
value: value.value,
|
||||
};
|
||||
}),
|
||||
|
||||
-6
@@ -11,12 +11,6 @@ export const manifest: ManifestPropertyEditorUi = {
|
||||
group: 'advanced',
|
||||
settings: {
|
||||
properties: [
|
||||
{
|
||||
alias: 'showOpenButton',
|
||||
label: 'Show open button',
|
||||
description: 'Opens the node in a dialog',
|
||||
propertyEditorUiAlias: 'Umb.PropertyEditorUi.Toggle',
|
||||
},
|
||||
{
|
||||
alias: 'onlyPickElementTypes',
|
||||
label: 'Only Element Types',
|
||||
|
||||
-5
@@ -21,7 +21,6 @@ export class UmbPropertyEditorUIDocumentTypePickerElement extends UmbLitElement
|
||||
this.max = minMax?.max ?? Infinity;
|
||||
|
||||
this.onlyElementTypes = config.getValueByAlias('onlyPickElementTypes') ?? false;
|
||||
this.showOpenButton = config?.getValueByAlias('showOpenButton') ?? false;
|
||||
}
|
||||
|
||||
@state()
|
||||
@@ -30,9 +29,6 @@ export class UmbPropertyEditorUIDocumentTypePickerElement extends UmbLitElement
|
||||
@state()
|
||||
max = Infinity;
|
||||
|
||||
@state()
|
||||
showOpenButton?: boolean;
|
||||
|
||||
@state()
|
||||
onlyElementTypes?: boolean;
|
||||
|
||||
@@ -48,7 +44,6 @@ export class UmbPropertyEditorUIDocumentTypePickerElement extends UmbLitElement
|
||||
.max=${this.max}
|
||||
.value=${this.value}
|
||||
.elementTypesOnly=${this.onlyElementTypes ?? false}
|
||||
?showOpenButton=${this.showOpenButton}
|
||||
@change=${this.#onChange}>
|
||||
</umb-input-document-type>
|
||||
`;
|
||||
|
||||
+1
@@ -183,6 +183,7 @@ export class UmbDocumentHistoryWorkspaceInfoAppElement extends UmbLitElement {
|
||||
}
|
||||
|
||||
.log-type uui-tag {
|
||||
justify-self: center;
|
||||
height: fit-content;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ export class UmbDocumentPickerInputContext extends UmbPickerInputContext<
|
||||
...pickerData?.search?.queryParams,
|
||||
};
|
||||
|
||||
super.openPicker(combinedPickerData);
|
||||
await super.openPicker(combinedPickerData);
|
||||
}
|
||||
|
||||
#pickableFilter = (
|
||||
|
||||
-3
@@ -89,9 +89,6 @@ export class UmbInputDocumentElement extends UmbFormControlMixin<string | undefi
|
||||
@property({ type: Array })
|
||||
allowedContentTypeIds?: string[] | undefined;
|
||||
|
||||
@property({ type: Boolean })
|
||||
showOpenButton?: boolean;
|
||||
|
||||
@property({ type: String })
|
||||
public override set value(selectionString: string | undefined) {
|
||||
this.selection = splitStringToArray(selectionString);
|
||||
|
||||
-6
@@ -26,12 +26,6 @@ export const manifests: Array<UmbExtensionManifest> = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
alias: 'showOpenButton',
|
||||
label: 'Show open button',
|
||||
description: 'Opens the node in a dialog',
|
||||
propertyEditorUiAlias: 'Umb.PropertyEditorUi.Toggle',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
-5
@@ -25,7 +25,6 @@ export class UmbPropertyEditorUIDocumentPickerElement extends UmbLitElement impl
|
||||
}
|
||||
|
||||
this._startNodeId = config.getValueByAlias('startNodeId');
|
||||
this._showOpenButton = config.getValueByAlias('showOpenButton') ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,9 +47,6 @@ export class UmbPropertyEditorUIDocumentPickerElement extends UmbLitElement impl
|
||||
@state()
|
||||
private _startNodeId?: string;
|
||||
|
||||
@state()
|
||||
private _showOpenButton?: boolean;
|
||||
|
||||
#onChange(event: CustomEvent & { target: UmbInputDocumentElement }) {
|
||||
this.value = event.target.value;
|
||||
this.dispatchEvent(new UmbChangeEvent());
|
||||
@@ -67,7 +63,6 @@ export class UmbPropertyEditorUIDocumentPickerElement extends UmbLitElement impl
|
||||
.max=${this._max}
|
||||
.startNode=${startNode}
|
||||
.value=${this.value}
|
||||
?showOpenButton=${this._showOpenButton}
|
||||
@change=${this.#onChange}
|
||||
?readonly=${this.readonly}>
|
||||
</umb-input-document>
|
||||
|
||||
+1
-1
@@ -137,9 +137,9 @@ export class UmbDocumentPublishingServerDataSource {
|
||||
values: data.values.map((value) => {
|
||||
return {
|
||||
editorAlias: value.editorAlias,
|
||||
alias: value.alias,
|
||||
culture: value.culture || null,
|
||||
segment: value.segment || null,
|
||||
alias: value.alias,
|
||||
value: value.value,
|
||||
};
|
||||
}),
|
||||
|
||||
+1
-1
@@ -94,9 +94,9 @@ export class UmbDocumentServerDataSource implements UmbDetailDataSource<UmbDocum
|
||||
values: data.values.map((value) => {
|
||||
return {
|
||||
editorAlias: value.editorAlias,
|
||||
alias: value.alias,
|
||||
culture: value.culture || null,
|
||||
segment: value.segment || null,
|
||||
alias: value.alias,
|
||||
value: value.value,
|
||||
};
|
||||
}),
|
||||
|
||||
+27
-7
@@ -2,7 +2,7 @@ import type { UmbDocumentUserPermissionModel } from '../types.js';
|
||||
import { UmbDocumentItemRepository } from '../../item/index.js';
|
||||
import type { UmbDocumentItemModel } from '../../item/types.js';
|
||||
import { UMB_DOCUMENT_PICKER_MODAL } from '../../constants.js';
|
||||
import { css, customElement, html, repeat, state } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { css, customElement, html, property, repeat, state } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import type { UmbModalManagerContext } from '@umbraco-cms/backoffice/modal';
|
||||
import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
|
||||
@@ -27,6 +27,9 @@ export class UmbInputDocumentGranularUserPermissionElement extends UUIFormContro
|
||||
this.#observePickedDocuments(uniques);
|
||||
}
|
||||
|
||||
@property({ type: Array, attribute: false })
|
||||
fallbackPermissions: Array<string> = [];
|
||||
|
||||
@state()
|
||||
private _items?: Array<UmbDocumentItemModel>;
|
||||
|
||||
@@ -122,15 +125,18 @@ export class UmbInputDocumentGranularUserPermissionElement extends UUIFormContro
|
||||
// TODO: get correct variant name
|
||||
const name = item.variants[0]?.name;
|
||||
const headline = name ? `Permissions for ${name}` : 'Permissions';
|
||||
|
||||
const fallbackVerbs = this.#getFallbackPermissionVerbsForEntityType(item.entityType);
|
||||
this.#entityUserPermissionModalContext = this.#modalManagerContext?.open(this, UMB_ENTITY_USER_PERMISSION_MODAL, {
|
||||
data: {
|
||||
unique: item.unique,
|
||||
entityType: item.entityType,
|
||||
headline,
|
||||
preset: {
|
||||
allowedVerbs: fallbackVerbs,
|
||||
},
|
||||
},
|
||||
value: {
|
||||
allowedVerbs,
|
||||
allowedVerbs: allowedVerbs,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -224,10 +230,8 @@ export class UmbInputDocumentGranularUserPermissionElement extends UUIFormContro
|
||||
if (!permission) return;
|
||||
|
||||
return umbExtensionsRegistry
|
||||
.getAllExtensions()
|
||||
.filter((manifest) => manifest.type === 'entityUserPermission')
|
||||
.filter((manifest) =>
|
||||
(manifest as ManifestEntityUserPermission).meta.verbs.every((verb) => permission.verbs.includes(verb)),
|
||||
.getByTypeAndFilter('entityUserPermission', (manifest) =>
|
||||
manifest.meta.verbs.every((verb) => permission.verbs.includes(verb)),
|
||||
)
|
||||
.map((m) => {
|
||||
const manifest = m as ManifestEntityUserPermission;
|
||||
@@ -236,6 +240,22 @@ export class UmbInputDocumentGranularUserPermissionElement extends UUIFormContro
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
#getFallbackPermissionVerbsForEntityType(entityType: string) {
|
||||
// get all permissions that are allowed for the entity type and have at least one of the fallback permissions
|
||||
// this is used to determine the default permissions for a document
|
||||
const verbs = umbExtensionsRegistry
|
||||
.getByTypeAndFilter(
|
||||
'entityUserPermission',
|
||||
(manifest) =>
|
||||
manifest.forEntityTypes.includes(entityType) &&
|
||||
this.fallbackPermissions.map((verb) => manifest.meta.verbs.includes(verb)).includes(true),
|
||||
)
|
||||
.flatMap((permission) => permission.meta.verbs);
|
||||
|
||||
// ensure that the verbs are unique
|
||||
return [...new Set([...verbs])];
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
css`
|
||||
#btn-add {
|
||||
|
||||
+41
-16
@@ -1,6 +1,6 @@
|
||||
import type { UmbLanguageItemModel } from '../../types.js';
|
||||
import { UmbLanguagePickerInputContext } from './input-language.context.js';
|
||||
import { css, html, customElement, property, state, repeat, nothing } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { css, html, customElement, property, state, repeat, nothing, when } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { splitStringToArray } from '@umbraco-cms/backoffice/utils';
|
||||
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
@@ -91,6 +91,27 @@ export class UmbInputLanguageElement extends UUIFormControlMixin(UmbLitElement,
|
||||
return this.selection.join(',');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the input to readonly mode, meaning value cannot be changed but still able to read and select its content.
|
||||
* @type {boolean}
|
||||
* @attr
|
||||
* @default
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public get readonly() {
|
||||
return this.#readonly;
|
||||
}
|
||||
public set readonly(value) {
|
||||
this.#readonly = value;
|
||||
|
||||
if (this.#readonly) {
|
||||
this.#sorter.disable();
|
||||
} else {
|
||||
this.#sorter.enable();
|
||||
}
|
||||
}
|
||||
#readonly = false;
|
||||
|
||||
@state()
|
||||
private _items: Array<UmbLanguageItemModel> = [];
|
||||
|
||||
@@ -126,7 +147,7 @@ export class UmbInputLanguageElement extends UUIFormControlMixin(UmbLitElement,
|
||||
});
|
||||
}
|
||||
|
||||
#removeItem(item: UmbLanguageItemModel) {
|
||||
#onRemove(item: UmbLanguageItemModel) {
|
||||
this.#pickerContext.requestRemoveItem(item.unique);
|
||||
}
|
||||
|
||||
@@ -146,30 +167,34 @@ export class UmbInputLanguageElement extends UUIFormControlMixin(UmbLitElement,
|
||||
}
|
||||
|
||||
#renderItems() {
|
||||
if (!this._items) return nothing;
|
||||
if (!this._items) return;
|
||||
return html`
|
||||
<uui-ref-list>
|
||||
${repeat(
|
||||
this._items,
|
||||
(item) => item.unique,
|
||||
(item) => this.#renderItem(item),
|
||||
(item) =>
|
||||
html`<umb-entity-item-ref
|
||||
id=${item.unique}
|
||||
.item=${item}
|
||||
?readonly=${this.readonly}
|
||||
?standalone=${this.max === 1}>
|
||||
${when(
|
||||
!this.readonly,
|
||||
() => html`
|
||||
<uui-action-bar slot="actions">
|
||||
<uui-button
|
||||
label=${this.localize.term('general_remove')}
|
||||
@click=${() => this.#onRemove(item)}></uui-button>
|
||||
</uui-action-bar>
|
||||
`,
|
||||
)}
|
||||
</umb-entity-item-ref>`,
|
||||
)}
|
||||
</uui-ref-list>
|
||||
`;
|
||||
}
|
||||
|
||||
#renderItem(item: UmbLanguageItemModel) {
|
||||
if (!item.unique) return;
|
||||
return html`
|
||||
<!-- TODO: add language ref element -->
|
||||
<uui-ref-node name=${item.name} id=${item.unique} detail=${item.unique}>
|
||||
<uui-action-bar slot="actions">
|
||||
<uui-button @click=${() => this.#removeItem(item)} label=${this.localize.term('general_remove')}></uui-button>
|
||||
</uui-action-bar>
|
||||
</uui-ref-node>
|
||||
`;
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
css`
|
||||
#btn-add {
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { UmbLanguageItemModel } from '../types.js';
|
||||
import { UMB_LANGUAGE_ENTITY_TYPE } from '../entity.js';
|
||||
import { UMB_EDIT_LANGUAGE_WORKSPACE_PATH_PATTERN } from '../paths.js';
|
||||
import { customElement, html, ifDefined, nothing, property, state } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router';
|
||||
import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/workspace';
|
||||
|
||||
@customElement('umb-language-item-ref')
|
||||
export class UmbLanguageItemRefElement extends UmbLitElement {
|
||||
@property({ type: Object })
|
||||
item?: UmbLanguageItemModel;
|
||||
|
||||
@property({ type: Boolean })
|
||||
readonly = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
standalone = false;
|
||||
|
||||
@state()
|
||||
_editPath = '';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
new UmbModalRouteRegistrationController(this, UMB_WORKSPACE_MODAL)
|
||||
.addUniquePaths(['unique'])
|
||||
.onSetup(() => {
|
||||
return { data: { entityType: UMB_LANGUAGE_ENTITY_TYPE, preset: {} } };
|
||||
})
|
||||
.observeRouteBuilder((routeBuilder) => {
|
||||
this._editPath = routeBuilder({});
|
||||
});
|
||||
}
|
||||
|
||||
#getHref() {
|
||||
if (!this.item?.unique) return;
|
||||
const path = UMB_EDIT_LANGUAGE_WORKSPACE_PATH_PATTERN.generateLocal({ unique: this.item.unique });
|
||||
return `${this._editPath}/${path}`;
|
||||
}
|
||||
|
||||
override render() {
|
||||
if (!this.item) return nothing;
|
||||
|
||||
return html`
|
||||
<uui-ref-node
|
||||
name=${this.item.name}
|
||||
href=${ifDefined(this.#getHref())}
|
||||
?readonly=${this.readonly}
|
||||
?standalone=${this.standalone}>
|
||||
<slot name="actions" slot="actions"></slot>
|
||||
<umb-icon slot="icon" name="icon-globe"></umb-icon>
|
||||
</uui-ref-node>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export { UmbLanguageItemRefElement as element };
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-language-item-ref': UmbLanguageItemRefElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { UMB_LANGUAGE_ENTITY_TYPE } from '../entity.js';
|
||||
|
||||
export const manifests: Array<UmbExtensionManifest> = [
|
||||
{
|
||||
type: 'entityItemRef',
|
||||
alias: 'Umb.EntityItemRef.Language',
|
||||
name: 'Language Entity Item Reference',
|
||||
element: () => import('./langauge-item-ref.element.js'),
|
||||
forEntityTypes: [UMB_LANGUAGE_ENTITY_TYPE],
|
||||
},
|
||||
];
|
||||
@@ -1,23 +1,25 @@
|
||||
import { manifests as repositoryManifests } from './repository/manifests.js';
|
||||
import { manifests as menuManifests } from './menu/manifests.js';
|
||||
import { manifests as entityActions } from './entity-actions/manifests.js';
|
||||
import { manifests as workspaceManifests } from './workspace/manifests.js';
|
||||
import { manifests as appLanguageSelect } from './app-language-select/manifests.js';
|
||||
import { manifests as modalManifests } from './modals/manifests.js';
|
||||
import { manifests as collectionManifests } from './collection/manifests.js';
|
||||
import { manifests as globalContextManifests } from './global-contexts/manifests.js';
|
||||
import { manifest as conditionsManifest } from './conditions/multiple-app-languages.condition.js';
|
||||
import { manifests as appLanguageSelect } from './app-language-select/manifests.js';
|
||||
import { manifests as collectionManifests } from './collection/manifests.js';
|
||||
import { manifests as entityActions } from './entity-actions/manifests.js';
|
||||
import { manifests as globalContextManifests } from './global-contexts/manifests.js';
|
||||
import { manifests as itemManifests } from './item/manifests.js';
|
||||
import { manifests as menuManifests } from './menu/manifests.js';
|
||||
import { manifests as modalManifests } from './modals/manifests.js';
|
||||
import { manifests as repositoryManifests } from './repository/manifests.js';
|
||||
import { manifests as workspaceManifests } from './workspace/manifests.js';
|
||||
import { UMB_WORKSPACE_CONDITION_ALIAS } from '@umbraco-cms/backoffice/workspace';
|
||||
|
||||
export const manifests: Array<UmbExtensionManifest> = [
|
||||
...repositoryManifests,
|
||||
...entityActions,
|
||||
...menuManifests,
|
||||
...workspaceManifests,
|
||||
...appLanguageSelect,
|
||||
...modalManifests,
|
||||
...collectionManifests,
|
||||
...entityActions,
|
||||
...globalContextManifests,
|
||||
...itemManifests,
|
||||
...menuManifests,
|
||||
...modalManifests,
|
||||
...repositoryManifests,
|
||||
...workspaceManifests,
|
||||
conditionsManifest,
|
||||
{
|
||||
type: 'workspaceContext',
|
||||
|
||||
+2
@@ -1,3 +1,4 @@
|
||||
import { UMB_LANGUAGE_ENTITY_TYPE } from '../../entity.js';
|
||||
import type { UmbLanguageItemModel } from './types.js';
|
||||
import { UmbItemServerDataSourceBase } from '@umbraco-cms/backoffice/repository';
|
||||
import type { LanguageItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
@@ -33,5 +34,6 @@ const mapper = (item: LanguageItemResponseModel): UmbLanguageItemModel => {
|
||||
return {
|
||||
unique: item.isoCode,
|
||||
name: item.name,
|
||||
entityType: UMB_LANGUAGE_ENTITY_TYPE,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { UmbLanguageEntityType } from '../../entity.js';
|
||||
|
||||
export interface UmbLanguageItemModel {
|
||||
entityType: UmbLanguageEntityType;
|
||||
unique: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
-1
@@ -237,7 +237,6 @@ export class UmbLogViewerMessageElement extends UmbLitElement {
|
||||
|
||||
pre {
|
||||
border-left: 4px solid #d42054;
|
||||
color: #303033;
|
||||
display: block;
|
||||
font-family:
|
||||
Lato,
|
||||
|
||||
-10
@@ -9,15 +9,5 @@ export const manifest: ManifestPropertyEditorUi = {
|
||||
label: 'Media Type Picker',
|
||||
icon: 'icon-media-dashed-line',
|
||||
group: 'advanced',
|
||||
settings: {
|
||||
properties: [
|
||||
{
|
||||
alias: 'showOpenButton',
|
||||
label: 'Show open button',
|
||||
description: 'Opens the node in a dialog',
|
||||
propertyEditorUiAlias: 'Umb.PropertyEditorUi.Toggle',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
+34
-1
@@ -148,6 +148,10 @@ export class UmbImageCropperFocusSetterElement extends UmbLitElement {
|
||||
|
||||
#handleGridDrag(event: PointerEvent) {
|
||||
if (this.disabled || this.hideFocalPoint) return;
|
||||
if (event.button !== 0) {
|
||||
// This is not a primary mouse button click, so lets not do anything.
|
||||
return;
|
||||
}
|
||||
|
||||
const grid = this.wrapperElement;
|
||||
const handle = this.focalPointElement;
|
||||
@@ -177,6 +181,31 @@ export class UmbImageCropperFocusSetterElement extends UmbLitElement {
|
||||
});
|
||||
}
|
||||
|
||||
#changeFocalPoint(event: PointerEvent) {
|
||||
if (this.disabled || this.hideFocalPoint) return;
|
||||
if (event.button !== 0) {
|
||||
// This is not a primary mouse button click, so lets not do anything.
|
||||
return;
|
||||
}
|
||||
|
||||
const grid = this.wrapperElement;
|
||||
const handle = this.focalPointElement;
|
||||
|
||||
if (!grid) return;
|
||||
|
||||
handle?.focus();
|
||||
|
||||
const dims = grid.getBoundingClientRect();
|
||||
const defaultView = grid.ownerDocument.defaultView!;
|
||||
const { width, height } = grid.getBoundingClientRect();
|
||||
const offsetX = dims.left + defaultView.scrollX;
|
||||
const offsetY = dims.top + defaultView.scrollY;
|
||||
|
||||
const x = event.pageX - offsetX;
|
||||
const y = event.pageY - offsetY;
|
||||
this.#setFocalPoint(x, y, width, height);
|
||||
}
|
||||
|
||||
#handleGridKeyDown(event: KeyboardEvent) {
|
||||
if (this.disabled || this.hideFocalPoint) return;
|
||||
|
||||
@@ -215,7 +244,11 @@ export class UmbImageCropperFocusSetterElement extends UmbLitElement {
|
||||
override render() {
|
||||
if (!this.src) return nothing;
|
||||
return html`
|
||||
<div id="wrapper" @mousedown=${this.#handleGridDrag} @touchstart=${this.#handleGridDrag}>
|
||||
<div
|
||||
id="wrapper"
|
||||
@click=${this.#changeFocalPoint}
|
||||
@mousedown=${this.#handleGridDrag}
|
||||
@touchstart=${this.#handleGridDrag}>
|
||||
<img id="image" @keydown=${() => nothing} src=${this.src} alt="" />
|
||||
<span
|
||||
id="focal-point"
|
||||
|
||||
+37
-5
@@ -1,18 +1,18 @@
|
||||
import type { UmbImageCropperPropertyEditorValue } from './types.js';
|
||||
import type { UmbInputImageCropperFieldElement } from './image-cropper-field.element.js';
|
||||
import { html, customElement, property, query, state, css } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { html, customElement, property, query, state, css, ifDefined } from '@umbraco-cms/backoffice/external/lit';
|
||||
import type { UUIFileDropzoneElement, UUIFileDropzoneEvent } from '@umbraco-cms/backoffice/external/uui';
|
||||
import { UmbId } from '@umbraco-cms/backoffice/id';
|
||||
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { UmbTemporaryFileManager } from '@umbraco-cms/backoffice/temporary-file';
|
||||
import { assignToFrozenObject } from '@umbraco-cms/backoffice/observable-api';
|
||||
import { UMB_VALIDATION_EMPTY_LOCALIZATION_KEY, UmbFormControlMixin } from '@umbraco-cms/backoffice/validation';
|
||||
|
||||
import './image-cropper.element.js';
|
||||
import './image-cropper-focus-setter.element.js';
|
||||
import './image-cropper-preview.element.js';
|
||||
import './image-cropper-field.element.js';
|
||||
import { UMB_VALIDATION_EMPTY_LOCALIZATION_KEY, UmbFormControlMixin } from '@umbraco-cms/backoffice/validation';
|
||||
|
||||
const DefaultFocalPoint = { left: 0.5, top: 0.5 };
|
||||
const DefaultValue = {
|
||||
@@ -50,11 +50,16 @@ export class UmbInputImageCropperElement extends UmbFormControlMixin<
|
||||
@state()
|
||||
fileUnique?: string;
|
||||
|
||||
#manager?: UmbTemporaryFileManager;
|
||||
@state()
|
||||
private _accept?: string;
|
||||
|
||||
@state()
|
||||
private _loading = true;
|
||||
|
||||
#manager = new UmbTemporaryFileManager(this);
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.#manager = new UmbTemporaryFileManager(this);
|
||||
|
||||
this.addValidator(
|
||||
'valueMissing',
|
||||
@@ -67,6 +72,19 @@ export class UmbInputImageCropperElement extends UmbFormControlMixin<
|
||||
|
||||
protected override firstUpdated(): void {
|
||||
this.#mergeCrops();
|
||||
this.#observeAcceptedFileTypes();
|
||||
}
|
||||
|
||||
async #observeAcceptedFileTypes() {
|
||||
const config = await this.#manager.getConfiguration();
|
||||
this.observe(
|
||||
config.part('imageFileTypes'),
|
||||
(imageFileTypes) => {
|
||||
this._accept = imageFileTypes.join(',');
|
||||
this._loading = false;
|
||||
},
|
||||
'_observeFileTypes',
|
||||
);
|
||||
}
|
||||
|
||||
#onUpload(e: UUIFileDropzoneEvent) {
|
||||
@@ -122,6 +140,10 @@ export class UmbInputImageCropperElement extends UmbFormControlMixin<
|
||||
}
|
||||
|
||||
override render() {
|
||||
if (this._loading) {
|
||||
return html`<div id="loader"><uui-loader></uui-loader></div>`;
|
||||
}
|
||||
|
||||
if (this.value?.src || this.file) {
|
||||
return this.#renderImageCropper();
|
||||
}
|
||||
@@ -131,7 +153,12 @@ export class UmbInputImageCropperElement extends UmbFormControlMixin<
|
||||
|
||||
#renderDropzone() {
|
||||
return html`
|
||||
<uui-file-dropzone id="dropzone" label="dropzone" @change="${this.#onUpload}" @click=${this.#onBrowse}>
|
||||
<uui-file-dropzone
|
||||
id="dropzone"
|
||||
label="dropzone"
|
||||
accept=${ifDefined(this._accept)}
|
||||
@change="${this.#onUpload}"
|
||||
@click=${this.#onBrowse}>
|
||||
<uui-button label=${this.localize.term('media_clickToUpload')} @click="${this.#onBrowse}"></uui-button>
|
||||
</uui-file-dropzone>
|
||||
`;
|
||||
@@ -166,6 +193,11 @@ export class UmbInputImageCropperElement extends UmbFormControlMixin<
|
||||
|
||||
static override styles = [
|
||||
css`
|
||||
#loader {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
uui-file-dropzone {
|
||||
position: relative;
|
||||
display: block;
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ export class UmbMediaPickerInputContext extends UmbPickerInputContext<
|
||||
...pickerData?.search?.queryParams,
|
||||
};
|
||||
|
||||
super.openPicker(combinedPickerData);
|
||||
await super.openPicker(combinedPickerData);
|
||||
}
|
||||
|
||||
#pickableFilter = (
|
||||
|
||||
-3
@@ -110,9 +110,6 @@ export class UmbInputMediaElement extends UmbFormControlMixin<string | undefined
|
||||
@property({ type: Array })
|
||||
allowedContentTypeIds?: Array<string> | undefined;
|
||||
|
||||
@property({ type: Boolean })
|
||||
showOpenButton?: boolean;
|
||||
|
||||
@property({ type: Object, attribute: false })
|
||||
startNode?: UmbTreeStartNode;
|
||||
|
||||
|
||||
+4
@@ -424,6 +424,10 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement<UmbMediaPick
|
||||
width: 100%;
|
||||
margin-bottom: var(--uui-size-3);
|
||||
}
|
||||
#search uui-input [slot='prepend'] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#searching-indicator {
|
||||
margin-left: 7px;
|
||||
|
||||
+6
-1
@@ -193,7 +193,7 @@ export class UmbPropertyEditorUIImageCropsElement extends UmbLitElement implemen
|
||||
(item) => item.alias,
|
||||
(item) => html`
|
||||
<div class="crop" data-alias="${item.alias}">
|
||||
<span class="crop-drag">+</span>
|
||||
<uui-icon name="icon-grib" class="crop-drag"></uui-icon>
|
||||
<span><strong>${item.label}</strong> <em>(${item.alias})</em></span>
|
||||
<span class="crop-size">(${item.width} x ${item.height}px)</span>
|
||||
<div class="crop-actions">
|
||||
@@ -236,6 +236,11 @@ export class UmbPropertyEditorUIImageCropsElement extends UmbLitElement implemen
|
||||
color: var(--uui-color-disabled-contrast);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.crop-drag:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.crop-size {
|
||||
font-size: 0.9em;
|
||||
padding-inline: var(--uui-size-space-4);
|
||||
|
||||
-3
@@ -83,9 +83,6 @@ export class UmbInputMemberGroupElement extends UmbFormControlMixin<string | und
|
||||
return this.#pickerContext.getSelection();
|
||||
}
|
||||
|
||||
@property({ type: Boolean })
|
||||
showOpenButton?: boolean;
|
||||
|
||||
@property({ type: Array })
|
||||
allowedContentTypeIds?: string[] | undefined;
|
||||
|
||||
|
||||
-1
@@ -50,7 +50,6 @@ export class UmbPropertyEditorUIMemberGroupPickerElement extends UmbLitElement i
|
||||
.min=${this._min}
|
||||
.max=${this._max}
|
||||
.value=${this.value}
|
||||
?showOpenButton=${true}
|
||||
@change=${this.#onChange}
|
||||
?readonly=${this.readonly}></umb-input-member-group>
|
||||
`;
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ export class UmbMemberPickerInputContext extends UmbPickerInputContext<
|
||||
...pickerData?.search?.queryParams,
|
||||
};
|
||||
|
||||
super.openPicker(combinedPickerData);
|
||||
await super.openPicker(combinedPickerData);
|
||||
}
|
||||
|
||||
#pickableFilter = (
|
||||
|
||||
+7
-15
@@ -56,17 +56,15 @@ export class UmbModelsBuilderDashboardElement extends UmbLitElement {
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<uui-box class="uui-text">
|
||||
<div class="headline">
|
||||
<h1 class="uui-h2">Models Builder</h1>
|
||||
<uui-button
|
||||
.state="${this._buttonStateReload}"
|
||||
look="secondary"
|
||||
label="Reload"
|
||||
@click="${this._onDashboardReload}">
|
||||
<uui-box headline="Models Builder" class="overview">
|
||||
<uui-button
|
||||
slot="header-actions"
|
||||
.state="${this._buttonStateReload}"
|
||||
look="secondary"
|
||||
label="Reload"
|
||||
@click="${this._onDashboardReload}">
|
||||
Reload
|
||||
</uui-button>
|
||||
</div>
|
||||
<p>Version: ${this._modelsBuilder?.version}</p>
|
||||
<div class="models-description">
|
||||
<p>ModelsBuilder is enabled with the following configuration:</p>
|
||||
@@ -138,12 +136,6 @@ export class UmbModelsBuilderDashboardElement extends UmbLitElement {
|
||||
padding: var(--uui-size-layout-1);
|
||||
}
|
||||
|
||||
.headline {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.models-description ul {
|
||||
list-style-type: square;
|
||||
margin: 0;
|
||||
|
||||
-1
@@ -263,7 +263,6 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement<UmbLinkPicker
|
||||
<umb-input-document
|
||||
?hidden=${!this.value.link.unique || this.value.link.type !== 'document'}
|
||||
.max=${1}
|
||||
.showOpenButton=${true}
|
||||
.value=${this.value.link.unique && this.value.link.type === 'document' ? this.value.link.unique : ''}
|
||||
@change=${(e: UmbInputPickerEvent) => this.#onPickerSelection(e, 'document')}>
|
||||
</umb-input-document>
|
||||
|
||||
+9
-1
@@ -132,7 +132,7 @@ export class UmbPropertyEditorUICollectionColumnConfigurationElement
|
||||
#renderColumn(column: UmbCollectionColumnConfiguration) {
|
||||
return html`
|
||||
<div class="layout-item" id=${column.alias}>
|
||||
<uui-icon name="icon-navigation"></uui-icon>
|
||||
<uui-icon class="drag-handle" name="icon-grib"></uui-icon>
|
||||
|
||||
<uui-input
|
||||
required
|
||||
@@ -193,6 +193,14 @@ export class UmbPropertyEditorUICollectionColumnConfigurationElement
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.drag-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
+9
-1
@@ -158,7 +158,7 @@ export class UmbPropertyEditorUICollectionLayoutConfigurationElement
|
||||
const varName = icon.color ? extractUmbColorVariable(icon.color) : undefined;
|
||||
return html`
|
||||
<div class="layout-item" id=${this.#getUnique(layout)}>
|
||||
<uui-icon name="icon-navigation"></uui-icon>
|
||||
<uui-icon class="drag-handle" name="icon-grib"></uui-icon>
|
||||
|
||||
<uui-button compact look="outline" label="pick icon" @click=${() => this.#onIconChange(icon, index)}>
|
||||
${when(
|
||||
@@ -224,6 +224,14 @@ export class UmbPropertyEditorUICollectionLayoutConfigurationElement
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.drag-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user