Merge branch 'v14/dev' into contrib
This commit is contained in:
@@ -63,15 +63,14 @@ variables:
|
||||
DOTNET_GENERATE_ASPNET_CERTIFICATE: false
|
||||
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: true
|
||||
npm_config_cache: $(Pipeline.Workspace)/.npm_client
|
||||
NODE_OPTIONS: --max_old_space_size=16384
|
||||
|
||||
stages:
|
||||
###############################################
|
||||
## Build
|
||||
###############################################
|
||||
- stage: Build
|
||||
variables:
|
||||
npm_config_cache: $(Pipeline.Workspace)/.npm_client
|
||||
NODE_OPTIONS: --max_old_space_size=16384
|
||||
jobs:
|
||||
- job: A
|
||||
displayName: Build Umbraco CMS
|
||||
@@ -80,18 +79,11 @@ stages:
|
||||
steps:
|
||||
- checkout: self
|
||||
submodules: true
|
||||
- task: NodeTool@0
|
||||
displayName: Use Node.js $(nodeVersion)
|
||||
retryCountOnTaskFailure: 3
|
||||
- task: UseDotNet@2
|
||||
displayName: Use .NET SDK from global.json
|
||||
inputs:
|
||||
versionSpec: $(nodeVersion)
|
||||
- script: npm ci --no-fund --no-audit --prefer-offline
|
||||
displayName: Run npm ci (Bellissima)
|
||||
workingDirectory: src/Umbraco.Web.UI.Client
|
||||
- script: npm run generate:api-local
|
||||
displayName: Generate API models (Bellissima)
|
||||
workingDirectory: src/Umbraco.Web.UI.Client
|
||||
enabled: false
|
||||
useGlobalJson: true
|
||||
- template: templates/backoffice-install.yml
|
||||
- script: npm run build:for:cms
|
||||
displayName: Run build (Bellissima)
|
||||
workingDirectory: src/Umbraco.Web.UI.Client
|
||||
@@ -101,10 +93,6 @@ stages:
|
||||
- script: npm run build
|
||||
displayName: Run npm build (Login)
|
||||
workingDirectory: src/Umbraco.Web.UI.Login
|
||||
- task: UseDotNet@2
|
||||
displayName: Use .NET SDK from global.json
|
||||
inputs:
|
||||
useGlobalJson: true
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: Run dotnet restore
|
||||
inputs:
|
||||
@@ -127,18 +115,25 @@ stages:
|
||||
inputs:
|
||||
targetPath: $(Build.SourcesDirectory)
|
||||
artifactName: build_output
|
||||
|
||||
- job: B
|
||||
displayName: Build Bellissima Package
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- checkout: self
|
||||
submodules: true
|
||||
- template: templates/backoffice-install.yml
|
||||
- script: npm run build:for:npm
|
||||
displayName: Run build:for:npm
|
||||
workingDirectory: src/Umbraco.Web.UI.Client
|
||||
- bash: |
|
||||
echo "##[command]Running npm version"
|
||||
echo "##[debug]Version: $PACKAGE_VERSION"
|
||||
echo "##[command]Running npm pack"
|
||||
echo "##[debug]Output directory: $(Build.ArtifactStagingDirectory)"
|
||||
npm version $PACKAGE_VERSION --allow-same-version --no-git-tag-version
|
||||
mkdir $(Build.ArtifactStagingDirectory)/npm
|
||||
npm pack --pack-destination $(Build.ArtifactStagingDirectory)/npm
|
||||
mv .npmrc $(Build.ArtifactStagingDirectory)/npm/
|
||||
displayName: Prepare Bellissima npm package
|
||||
env:
|
||||
PACKAGE_VERSION: $(build.NBGV_NpmPackageVersion)
|
||||
displayName: Run npm pack
|
||||
workingDirectory: src/Umbraco.Web.UI.Client
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: Publish Bellissima npm artifact
|
||||
@@ -208,28 +203,11 @@ stages:
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
variables:
|
||||
npm_config_cache: $(Pipeline.Workspace)/.npm_client
|
||||
NODE_OPTIONS: --max_old_space_size=16384
|
||||
BASE_PATH: /v$(umbracoMajorVersion)/ui
|
||||
steps:
|
||||
- checkout: self
|
||||
submodules: true
|
||||
- task: NodeTool@0
|
||||
displayName: Use Node.js $(nodeVersion)
|
||||
retryCountOnTaskFailure: 3
|
||||
inputs:
|
||||
versionSpec: $(nodeVersion)
|
||||
- task: Cache@2
|
||||
displayName: Cache node_modules
|
||||
inputs:
|
||||
key: '"npm_client" | "$(Agent.OS)"| $(Build.SourcesDirectory)/src/Umbraco.Web.UI.Client/package-lock.json'
|
||||
restoreKeys: |
|
||||
"npm_client" | "$(Agent.OS)"
|
||||
"npm_client"
|
||||
path: $(npm_config_cache)
|
||||
- script: npm ci --no-fund --no-audit --prefer-offline
|
||||
workingDirectory: src/Umbraco.Web.UI.Client
|
||||
displayName: Run npm ci
|
||||
- template: templates/backoffice-install.yml
|
||||
- script: npm run storybook:build
|
||||
displayName: Build Storybook
|
||||
env:
|
||||
@@ -239,16 +217,30 @@ stages:
|
||||
displayName: Replace BASE_PATH on assets
|
||||
workingDirectory: $(Build.SourcesDirectory)/src/Umbraco.Web.UI.Client/storybook-static
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Archive js Docs
|
||||
displayName: Archive Storybook
|
||||
inputs:
|
||||
rootFolderOrFile: $(Build.SourcesDirectory)/src/Umbraco.Web.UI.Client/storybook-static
|
||||
includeRootFolder: false
|
||||
archiveFile: $(Build.ArtifactStagingDirectory)/ui-docs.zip
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: Publish js Docs
|
||||
displayName: Publish Storybook
|
||||
inputs:
|
||||
targetPath: $(Build.ArtifactStagingDirectory)/ui-docs.zip
|
||||
artifact: ui-docs
|
||||
- script: npm run generate:ui-api-docs
|
||||
displayName: Generate API Docs
|
||||
workingDirectory: $(Build.SourcesDirectory)/src/Umbraco.Web.UI.Client
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Archive UI API Docs
|
||||
inputs:
|
||||
rootFolderOrFile: $(Build.SourcesDirectory)/src/Umbraco.Web.UI.Client/ui-api
|
||||
includeRootFolder: false
|
||||
archiveFile: $(Build.ArtifactStagingDirectory)/ui-api-docs.zip
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: Publish UI API Docs
|
||||
inputs:
|
||||
targetPath: $(Build.ArtifactStagingDirectory)/ui-api-docs.zip
|
||||
artifact: ui-api-docs
|
||||
|
||||
###############################################
|
||||
## Test
|
||||
@@ -528,7 +520,7 @@ stages:
|
||||
- ${{ if eq(parameters.isNightly, true) }}:
|
||||
pwsh: npm run test --ignore-certificate-errors
|
||||
${{ else }}:
|
||||
pwsh: npm run smokeTest --ignore-certificate-errors
|
||||
pwsh: npm run smokeTest --ignore-certificate-errors
|
||||
displayName: Run Playwright tests
|
||||
continueOnError: true
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
@@ -868,7 +860,7 @@ stages:
|
||||
BlobPrefix: v$(umbracoMajorVersion)/csharp
|
||||
CleanTargetBeforeCopy: true
|
||||
- job:
|
||||
displayName: Upload js Docs
|
||||
displayName: Upload Storybook
|
||||
steps:
|
||||
- checkout: none
|
||||
- task: DownloadPipelineArtifact@2
|
||||
@@ -881,7 +873,7 @@ stages:
|
||||
archiveFilePatterns: $(Build.SourcesDirectory)/ui-docs.zip
|
||||
destinationFolder: $(Build.ArtifactStagingDirectory)/ui-docs
|
||||
- task: AzureFileCopy@4
|
||||
displayName: 'Copy UI Docs to blob storage'
|
||||
displayName: 'Copy Storybook to blob storage'
|
||||
inputs:
|
||||
SourcePath: '$(Build.ArtifactStagingDirectory)/ui-docs/*'
|
||||
azureSubscription: umbraco-storage
|
||||
@@ -890,3 +882,26 @@ stages:
|
||||
ContainerName: '$web'
|
||||
BlobPrefix: v$(umbracoMajorVersion)/ui
|
||||
CleanTargetBeforeCopy: true
|
||||
- job:
|
||||
displayName: Upload UI API Docs
|
||||
steps:
|
||||
- checkout: none
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download artifact
|
||||
inputs:
|
||||
artifact: ui-api-docs
|
||||
path: $(Build.SourcesDirectory)
|
||||
- task: ExtractFiles@1
|
||||
inputs:
|
||||
archiveFilePatterns: $(Build.SourcesDirectory)/ui-api-docs.zip
|
||||
destinationFolder: $(Build.ArtifactStagingDirectory)/ui-api-docs
|
||||
- task: AzureFileCopy@4
|
||||
displayName: 'Copy UI API Docs to blob storage'
|
||||
inputs:
|
||||
SourcePath: '$(Build.ArtifactStagingDirectory)/ui-api-docs/*'
|
||||
azureSubscription: umbraco-storage
|
||||
Destination: AzureBlob
|
||||
storage: umbracoapidocs
|
||||
ContainerName: '$web'
|
||||
BlobPrefix: v$(umbracoMajorVersion)/ui-api
|
||||
CleanTargetBeforeCopy: true
|
||||
|
||||
31
build/templates/backoffice-install.yml
Normal file
31
build/templates/backoffice-install.yml
Normal file
@@ -0,0 +1,31 @@
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
displayName: Use Node.js
|
||||
retryCountOnTaskFailure: 3
|
||||
inputs:
|
||||
versionSource: 'fromFile'
|
||||
versionFilePath: src/Umbraco.Web.UI.Client/.nvmrc
|
||||
|
||||
- bash: |
|
||||
echo "##[command]Install nbgv"
|
||||
dotnet tool install --tool-path . nbgv
|
||||
echo "##[command]Running nbgv get-version"
|
||||
PACKAGE_VERSION=$(nbgv get-version -v NpmPackageVersion)
|
||||
echo "##[command]Running npm version"
|
||||
echo "##[debug]Version: $PACKAGE_VERSION"
|
||||
cd src/Umbraco.Web.UI.Client
|
||||
npm version $PACKAGE_VERSION --allow-same-version --no-git-tag-version
|
||||
displayName: Set NPM Version
|
||||
|
||||
- task: Cache@2
|
||||
displayName: Cache node_modules
|
||||
inputs:
|
||||
key: '"npm_client" | "$(Agent.OS)"| $(Build.SourcesDirectory)/src/Umbraco.Web.UI.Client/package-lock.json'
|
||||
restoreKeys: |
|
||||
"npm_client" | "$(Agent.OS)"
|
||||
"npm_client"
|
||||
path: $(npm_config_cache)
|
||||
|
||||
- script: npm ci --no-fund --no-audit --prefer-offline
|
||||
displayName: Run npm ci (Bellissima)
|
||||
workingDirectory: src/Umbraco.Web.UI.Client
|
||||
@@ -86,6 +86,18 @@ public abstract class DocumentTypeControllerBase : ManagementApiControllerBase
|
||||
.WithTitle("Operation not permitted")
|
||||
.WithDetail("The attempted operation was not permitted, likely due to a permission/configuration mismatch with the operation.")
|
||||
.Build()),
|
||||
ContentTypeOperationStatus.CancelledByNotification => new BadRequestObjectResult(problemDetailsBuilder
|
||||
.WithTitle("Cancelled by notification")
|
||||
.WithDetail("The attempted operation was cancelled by a notification.")
|
||||
.Build()),
|
||||
ContentTypeOperationStatus.NameCannotBeEmpty => new BadRequestObjectResult(problemDetailsBuilder
|
||||
.WithTitle("Name cannot be empty")
|
||||
.WithDetail("The name of the content type cannot be empty")
|
||||
.Build()),
|
||||
ContentTypeOperationStatus.NameTooLong => new BadRequestObjectResult(problemDetailsBuilder
|
||||
.WithTitle("Name was too long")
|
||||
.WithDetail("Name cannot be more than 255 characters in length.")
|
||||
.Build()),
|
||||
_ => new ObjectResult("Unknown content type operation status") { StatusCode = StatusCodes.Status500InternalServerError },
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
@@ -29,7 +29,7 @@ public class ByRelationTypeKeyRelationController : RelationControllerBase
|
||||
/// <remarks>
|
||||
/// Use case: On a relation type page you can see all created relations of this type.
|
||||
/// </remarks>
|
||||
[HttpGet("type/{id:guid}")]
|
||||
[HttpGet("type/{id:guid}", Name = "GetRelationByRelationTypeId")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<RelationResponseModel>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(PagedViewModel<ProblemDetails>), StatusCodes.Status404NotFound)]
|
||||
|
||||
@@ -34,7 +34,7 @@ internal static class BackOfficeCorsPolicyBuilderExtensions
|
||||
{
|
||||
policy
|
||||
.WithOrigins(customOrigin)
|
||||
.WithExposedHeaders(Constants.Headers.Location, Constants.Headers.GeneratedResource)
|
||||
.WithExposedHeaders(Constants.Headers.Location, Constants.Headers.GeneratedResource, Constants.Headers.Notifications)
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod()
|
||||
.AllowCredentials();
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Hosting;
|
||||
using Umbraco.Cms.Core.Routing;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.Security;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Middleware;
|
||||
|
||||
@@ -16,15 +21,40 @@ public class BackOfficeAuthorizationInitializationMiddleware : IMiddleware
|
||||
private readonly UmbracoRequestPaths _umbracoRequestPaths;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly IRuntimeState _runtimeState;
|
||||
private readonly IOptions<GlobalSettings> _globalSettings;
|
||||
private readonly IOptions<WebRoutingSettings> _webRoutingSettings;
|
||||
private readonly IHostingEnvironment _hostingEnvironment;
|
||||
|
||||
[Obsolete("Use the non-obsolete constructor. This will be removed in Umbraco 16.")]
|
||||
public BackOfficeAuthorizationInitializationMiddleware(
|
||||
UmbracoRequestPaths umbracoRequestPaths,
|
||||
IServiceProvider serviceProvider,
|
||||
IRuntimeState runtimeState)
|
||||
: this(
|
||||
umbracoRequestPaths,
|
||||
serviceProvider,
|
||||
runtimeState,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IOptions<GlobalSettings>>(),
|
||||
StaticServiceProvider.Instance.GetRequiredService<IOptions<WebRoutingSettings>>(),
|
||||
StaticServiceProvider.Instance.GetRequiredService<IHostingEnvironment>()
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public BackOfficeAuthorizationInitializationMiddleware(
|
||||
UmbracoRequestPaths umbracoRequestPaths,
|
||||
IServiceProvider serviceProvider,
|
||||
IRuntimeState runtimeState,
|
||||
IOptions<GlobalSettings> globalSettings,
|
||||
IOptions<WebRoutingSettings> webRoutingSettings,
|
||||
IHostingEnvironment hostingEnvironment)
|
||||
{
|
||||
_umbracoRequestPaths = umbracoRequestPaths;
|
||||
_serviceProvider = serviceProvider;
|
||||
_runtimeState = runtimeState;
|
||||
_globalSettings = globalSettings;
|
||||
_webRoutingSettings = webRoutingSettings;
|
||||
_hostingEnvironment = hostingEnvironment;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
|
||||
@@ -47,6 +77,7 @@ public class BackOfficeAuthorizationInitializationMiddleware : IMiddleware
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (_umbracoRequestPaths.IsBackOfficeRequest(context.Request.Path) == false)
|
||||
{
|
||||
return;
|
||||
@@ -55,9 +86,13 @@ public class BackOfficeAuthorizationInitializationMiddleware : IMiddleware
|
||||
await _firstBackOfficeRequestLocker.WaitAsync();
|
||||
if (_firstBackOfficeRequest == false)
|
||||
{
|
||||
Uri? backOfficeUrl = string.IsNullOrWhiteSpace(_webRoutingSettings.Value.UmbracoApplicationUrl) is false
|
||||
? new Uri($"{_webRoutingSettings.Value.UmbracoApplicationUrl.TrimEnd('/')}{_globalSettings.Value.GetBackOfficePath(_hostingEnvironment).EnsureStartsWith('/')}")
|
||||
: null;
|
||||
|
||||
using IServiceScope scope = _serviceProvider.CreateScope();
|
||||
IBackOfficeApplicationManager backOfficeApplicationManager = scope.ServiceProvider.GetRequiredService<IBackOfficeApplicationManager>();
|
||||
await backOfficeApplicationManager.EnsureBackOfficeApplicationAsync(new Uri(context.Request.GetDisplayUrl()));
|
||||
await backOfficeApplicationManager.EnsureBackOfficeApplicationAsync(backOfficeUrl ?? new Uri(context.Request.GetDisplayUrl()));
|
||||
_firstBackOfficeRequest = true;
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,6 @@ using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Hosting;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Web.Mvc;
|
||||
using Umbraco.Cms.Web.Common.Controllers;
|
||||
using Umbraco.Cms.Web.Common.Routing;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
@@ -54,8 +52,8 @@ public sealed class BackOfficeAreaRoutes : IAreaRoutes
|
||||
case RuntimeLevel.Install:
|
||||
case RuntimeLevel.Upgrade:
|
||||
case RuntimeLevel.Run:
|
||||
|
||||
MapMinimalBackOffice(endpoints);
|
||||
endpoints.MapHub<BackofficeHub>(_umbracoPathSegment + Constants.Web.BackofficeSignalRHub);
|
||||
break;
|
||||
case RuntimeLevel.BootFailed:
|
||||
case RuntimeLevel.Unknown:
|
||||
|
||||
8
src/Umbraco.Cms.Api.Management/Routing/BackofficeHub.cs
Normal file
8
src/Umbraco.Cms.Api.Management/Routing/BackofficeHub.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Routing;
|
||||
|
||||
public class BackofficeHub : Hub
|
||||
{
|
||||
public async Task SendPayload(object payload) => await Clients.All.SendAsync("payloadReceived", payload);
|
||||
}
|
||||
@@ -56,6 +56,7 @@ public static partial class Constants
|
||||
/// The "base" path to the Management API
|
||||
/// </summary>
|
||||
public const string ManagementApiPath = "/management/api/";
|
||||
public const string BackofficeSignalRHub = "/backofficeHub";
|
||||
|
||||
public static class Routing
|
||||
{
|
||||
|
||||
@@ -40,7 +40,12 @@ internal sealed class ContentTypeEditingService : ContentTypeEditingServiceBase<
|
||||
UpdateTemplates(contentType, model);
|
||||
|
||||
// save content type
|
||||
await SaveAsync(contentType, userKey);
|
||||
Attempt<ContentTypeOperationStatus> creationAttempt = await _contentTypeService.CreateAsync(contentType, userKey);
|
||||
|
||||
if(creationAttempt.Success is false)
|
||||
{
|
||||
return Attempt.FailWithStatus<IContentType?, ContentTypeOperationStatus>(creationAttempt.Result, contentType);
|
||||
}
|
||||
|
||||
return Attempt.SucceedWithStatus<IContentType?, ContentTypeOperationStatus>(ContentTypeOperationStatus.Success, contentType);
|
||||
}
|
||||
@@ -58,9 +63,11 @@ internal sealed class ContentTypeEditingService : ContentTypeEditingServiceBase<
|
||||
UpdateHistoryCleanup(contentType, model);
|
||||
UpdateTemplates(contentType, model);
|
||||
|
||||
await SaveAsync(contentType, userKey);
|
||||
Attempt<ContentTypeOperationStatus> attempt = await _contentTypeService.UpdateAsync(contentType, userKey);
|
||||
|
||||
return Attempt.SucceedWithStatus<IContentType?, ContentTypeOperationStatus>(ContentTypeOperationStatus.Success, contentType);
|
||||
return attempt.Success
|
||||
? Attempt.SucceedWithStatus<IContentType?, ContentTypeOperationStatus>(ContentTypeOperationStatus.Success, contentType)
|
||||
: Attempt.FailWithStatus<IContentType?, ContentTypeOperationStatus>(attempt.Result, null);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ContentTypeAvailableCompositionsResult>> GetAvailableCompositionsAsync(
|
||||
@@ -93,9 +100,6 @@ internal sealed class ContentTypeEditingService : ContentTypeEditingServiceBase<
|
||||
contentType.SetDefaultTemplate(allowedTemplates.FirstOrDefault(t => t.Key == model.DefaultTemplateKey));
|
||||
}
|
||||
|
||||
private async Task SaveAsync(IContentType contentType, Guid userKey)
|
||||
=> await _contentTypeService.SaveAsync(contentType, userKey);
|
||||
|
||||
protected override IContentType CreateContentType(IShortStringHelper shortStringHelper, int parentId)
|
||||
=> new ContentType(shortStringHelper, parentId);
|
||||
|
||||
|
||||
@@ -603,6 +603,75 @@ public abstract class ContentTypeServiceBase<TRepository, TItem> : ContentTypeSe
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Attempt<ContentTypeOperationStatus>> CreateAsync(TItem item, Guid performingUserKey) => await InternalSaveAsync(item, performingUserKey);
|
||||
|
||||
public async Task<Attempt<ContentTypeOperationStatus>> UpdateAsync(TItem item, Guid performingUserKey) => await InternalSaveAsync(item, performingUserKey);
|
||||
|
||||
private async Task<Attempt<ContentTypeOperationStatus>> InternalSaveAsync(TItem item, Guid performingUserKey)
|
||||
{
|
||||
using ICoreScope scope = ScopeProvider.CreateCoreScope();
|
||||
EventMessages eventMessages = EventMessagesFactory.Get();
|
||||
|
||||
Attempt<ContentTypeOperationStatus> validationAttempt = ValidateCommon(item);
|
||||
if (validationAttempt.Success is false)
|
||||
{
|
||||
return Attempt.Fail(validationAttempt.Result);
|
||||
}
|
||||
|
||||
SavingNotification<TItem> savingNotification = GetSavingNotification(item, eventMessages);
|
||||
if (await scope.Notifications.PublishCancelableAsync(savingNotification))
|
||||
{
|
||||
scope.Complete();
|
||||
return Attempt.Fail(ContentTypeOperationStatus.CancelledByNotification);
|
||||
}
|
||||
|
||||
scope.WriteLock(WriteLockIds);
|
||||
|
||||
// validate the DAG transform, within the lock
|
||||
ValidateLocked(item); // throws if invalid
|
||||
|
||||
int userId = await _userIdKeyResolver.GetAsync(performingUserKey);
|
||||
item.CreatorId = userId;
|
||||
if (item.Description == string.Empty)
|
||||
{
|
||||
item.Description = null;
|
||||
}
|
||||
|
||||
Repository.Save(item); // also updates content/media/member items
|
||||
|
||||
// figure out impacted content types
|
||||
ContentTypeChange<TItem>[] changes = ComposeContentTypeChanges(item).ToArray();
|
||||
|
||||
// Publish this in scope, see comment at GetContentTypeRefreshedNotification for more info.
|
||||
await _eventAggregator.PublishAsync(GetContentTypeRefreshedNotification(changes, eventMessages));
|
||||
|
||||
scope.Notifications.Publish(GetContentTypeChangedNotification(changes, eventMessages));
|
||||
|
||||
SavedNotification<TItem> savedNotification = GetSavedNotification(item, eventMessages);
|
||||
savedNotification.WithStateFrom(savingNotification);
|
||||
scope.Notifications.Publish(savedNotification);
|
||||
|
||||
Audit(AuditType.Save, userId, item.Id);
|
||||
scope.Complete();
|
||||
|
||||
return Attempt.Succeed(ContentTypeOperationStatus.Success);
|
||||
}
|
||||
|
||||
private Attempt<ContentTypeOperationStatus> ValidateCommon(TItem item)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(item.Name))
|
||||
{
|
||||
return Attempt.Fail(ContentTypeOperationStatus.NameCannotBeEmpty);
|
||||
}
|
||||
|
||||
if (item.Name.Length > 255)
|
||||
{
|
||||
return Attempt.Fail(ContentTypeOperationStatus.NameTooLong);
|
||||
}
|
||||
|
||||
return Attempt.Succeed(ContentTypeOperationStatus.Success);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Delete
|
||||
|
||||
@@ -67,16 +67,31 @@ public interface IContentTypeBaseService<TItem> : IContentTypeBaseService, IServ
|
||||
|
||||
bool HasChildren(Guid id);
|
||||
|
||||
[Obsolete("Please use the respective Create or Update instead")]
|
||||
void Save(TItem? item, int userId = Constants.Security.SuperUserId);
|
||||
|
||||
[Obsolete("Please use the respective Create or Update instead")]
|
||||
Task SaveAsync(TItem item, Guid performingUserKey)
|
||||
{
|
||||
Save(item);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Obsolete("Please use the respective Create or Update instead")]
|
||||
void Save(IEnumerable<TItem> items, int userId = Constants.Security.SuperUserId);
|
||||
|
||||
Task<Attempt<ContentTypeOperationStatus>> CreateAsync(TItem item, Guid performingUserKey)
|
||||
{
|
||||
Save(item);
|
||||
return Task.FromResult(Attempt.Succeed(ContentTypeOperationStatus.Success));
|
||||
}
|
||||
|
||||
Task<Attempt<ContentTypeOperationStatus>> UpdateAsync(TItem item, Guid performingUserKey)
|
||||
{
|
||||
Save(item);
|
||||
return Task.FromResult(Attempt.Succeed(ContentTypeOperationStatus.Success));
|
||||
}
|
||||
|
||||
void Delete(TItem item, int userId = Constants.Security.SuperUserId);
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -5,6 +5,8 @@ public enum ContentTypeOperationStatus
|
||||
Success,
|
||||
DuplicateAlias,
|
||||
InvalidAlias,
|
||||
NameCannotBeEmpty,
|
||||
NameTooLong,
|
||||
InvalidPropertyTypeAlias,
|
||||
PropertyTypeAliasCannotEqualContentTypeAlias,
|
||||
DuplicatePropertyTypeAlias,
|
||||
@@ -17,5 +19,6 @@ public enum ContentTypeOperationStatus
|
||||
MissingContainer,
|
||||
DuplicateContainer,
|
||||
NotFound,
|
||||
NotAllowed
|
||||
NotAllowed,
|
||||
CancelledByNotification,
|
||||
}
|
||||
|
||||
@@ -10,6 +10,13 @@ namespace Umbraco.Cms.Core.Templates;
|
||||
/// </summary>
|
||||
public sealed class HtmlLocalLinkParser
|
||||
{
|
||||
// needs to support media and document links, order of attributes should not matter nor should other attributes mess with things
|
||||
// <a type="media" href="/{localLink:7e21a725-b905-4c5f-86dc-8c41ec116e39}" title="media">media</a>
|
||||
// <a type="document" href="/{localLink:eed5fc6b-96fd-45a5-a0f1-b1adfb483c2f}" title="other page">other page</a>
|
||||
internal static readonly Regex LocalLinkTagPattern = new(
|
||||
@"<a\s+(?:(?:(?:type=['""](?<type>document|media)['""].*?(?<locallink>href=[""']/{localLink:(?<guid>[a-fA-F0-9-]+)})[""'])|((?<locallink>href=[""']/{localLink:(?<guid>[a-fA-F0-9-]+)})[""'].*?type=(['""])(?<type>document|media)(?:['""])))|(?:(?:type=['""](?<type>document|media)['""])|(?:(?<locallink>href=[""']/{localLink:[a-fA-F0-9-]+})[""'])))[^>]*>",
|
||||
RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
|
||||
|
||||
internal static readonly Regex LocalLinkPattern = new(
|
||||
@"href=""[/]?(?:\{|\%7B)localLink:([a-zA-Z0-9-://]+)(?:\}|\%7D)",
|
||||
RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
|
||||
@@ -105,6 +112,32 @@ public sealed class HtmlLocalLinkParser
|
||||
}
|
||||
|
||||
private IEnumerable<(int? intId, GuidUdi? udi, string tagValue)> FindLocalLinkIds(string text)
|
||||
{
|
||||
MatchCollection localLinkTagMatches = LocalLinkTagPattern.Matches(text);
|
||||
foreach (Match linkTag in localLinkTagMatches)
|
||||
{
|
||||
if (linkTag.Groups.Count < 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Guid.TryParse(linkTag.Groups["guid"].Value, out Guid guid) is false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
yield return (null, new GuidUdi(linkTag.Groups["type"].Value, guid), linkTag.Groups["locallink"].Value);
|
||||
}
|
||||
|
||||
// also return legacy results for values that have not been migrated
|
||||
foreach ((int? intId, GuidUdi? udi, string tagValue) legacyResult in FindLegacyLocalLinkIds(text))
|
||||
{
|
||||
yield return legacyResult;
|
||||
}
|
||||
}
|
||||
|
||||
// todo remove at some point?
|
||||
private IEnumerable<(int? intId, GuidUdi? udi, string tagValue)> FindLegacyLocalLinkIds(string text)
|
||||
{
|
||||
// Parse internal links
|
||||
MatchCollection tags = LocalLinkPattern.Matches(text);
|
||||
|
||||
@@ -132,9 +132,15 @@ internal sealed class ApiRichTextElementParser : ApiRichTextParserBase, IApiRich
|
||||
return;
|
||||
}
|
||||
|
||||
if (attributes.ContainsKey("type") is false || attributes["type"] is not string type)
|
||||
{
|
||||
type = "unknown";
|
||||
}
|
||||
|
||||
ReplaceLocalLinks(
|
||||
publishedSnapshot,
|
||||
href,
|
||||
type,
|
||||
route =>
|
||||
{
|
||||
attributes["route"] = route;
|
||||
|
||||
@@ -52,8 +52,9 @@ internal sealed class ApiRichTextMarkupParser : ApiRichTextParserBase, IApiRichT
|
||||
foreach (HtmlNode link in links)
|
||||
{
|
||||
ReplaceLocalLinks(
|
||||
publishedSnapshot,
|
||||
publishedSnapshot,
|
||||
link.GetAttributeValue("href", string.Empty),
|
||||
link.GetAttributeValue("type", "unknown"),
|
||||
route =>
|
||||
{
|
||||
link.SetAttributeValue("href", route.Path);
|
||||
|
||||
@@ -4,6 +4,7 @@ using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Templates;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.DeliveryApi;
|
||||
|
||||
@@ -18,20 +19,35 @@ internal abstract partial class ApiRichTextParserBase
|
||||
_apiMediaUrlProvider = apiMediaUrlProvider;
|
||||
}
|
||||
|
||||
protected void ReplaceLocalLinks(IPublishedSnapshot publishedSnapshot, string href, Action<IApiContentRoute> handleContentRoute, Action<string> handleMediaUrl, Action handleInvalidLink)
|
||||
protected void ReplaceLocalLinks(IPublishedSnapshot publishedSnapshot, string href, string type, Action<IApiContentRoute> handleContentRoute, Action<string> handleMediaUrl, Action handleInvalidLink)
|
||||
{
|
||||
ReplaceStatus replaceAttempt = ReplaceLocalLink(publishedSnapshot, href, type, handleContentRoute, handleMediaUrl);
|
||||
if (replaceAttempt == ReplaceStatus.Success)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (replaceAttempt == ReplaceStatus.InvalidEntityType || ReplaceLegacyLocalLink(publishedSnapshot, href, handleContentRoute, handleMediaUrl) == ReplaceStatus.InvalidEntityType)
|
||||
{
|
||||
handleInvalidLink();
|
||||
}
|
||||
}
|
||||
|
||||
private ReplaceStatus ReplaceLocalLink(IPublishedSnapshot publishedSnapshot, string href, string type, Action<IApiContentRoute> handleContentRoute, Action<string> handleMediaUrl)
|
||||
{
|
||||
Match match = LocalLinkRegex().Match(href);
|
||||
if (match.Success is false)
|
||||
{
|
||||
return;
|
||||
return ReplaceStatus.NoMatch;
|
||||
}
|
||||
|
||||
if (UdiParser.TryParse(match.Groups["udi"].Value, out Udi? udi) is false)
|
||||
if (Guid.TryParse(match.Groups["guid"].Value, out Guid guid) is false)
|
||||
{
|
||||
return;
|
||||
return ReplaceStatus.NoMatch;
|
||||
}
|
||||
|
||||
bool handled = false;
|
||||
var udi = new GuidUdi(type, guid);
|
||||
|
||||
switch (udi.EntityType)
|
||||
{
|
||||
case Constants.UdiEntityType.Document:
|
||||
@@ -41,8 +57,8 @@ internal abstract partial class ApiRichTextParserBase
|
||||
: null;
|
||||
if (route != null)
|
||||
{
|
||||
handled = true;
|
||||
handleContentRoute(route);
|
||||
return ReplaceStatus.Success;
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -50,17 +66,56 @@ internal abstract partial class ApiRichTextParserBase
|
||||
IPublishedContent? media = publishedSnapshot.Media?.GetById(udi);
|
||||
if (media != null)
|
||||
{
|
||||
handled = true;
|
||||
handleMediaUrl(_apiMediaUrlProvider.GetUrl(media));
|
||||
return ReplaceStatus.Success;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if(handled is false)
|
||||
return ReplaceStatus.InvalidEntityType;
|
||||
}
|
||||
|
||||
private ReplaceStatus ReplaceLegacyLocalLink(IPublishedSnapshot publishedSnapshot, string href, Action<IApiContentRoute> handleContentRoute, Action<string> handleMediaUrl)
|
||||
{
|
||||
Match match = LegacyLocalLinkRegex().Match(href);
|
||||
if (match.Success is false)
|
||||
{
|
||||
handleInvalidLink();
|
||||
return ReplaceStatus.NoMatch;
|
||||
}
|
||||
|
||||
if (UdiParser.TryParse(match.Groups["udi"].Value, out Udi? udi) is false)
|
||||
{
|
||||
return ReplaceStatus.NoMatch;
|
||||
}
|
||||
|
||||
|
||||
switch (udi.EntityType)
|
||||
{
|
||||
case Constants.UdiEntityType.Document:
|
||||
IPublishedContent? content = publishedSnapshot.Content?.GetById(udi);
|
||||
IApiContentRoute? route = content != null
|
||||
? _apiContentRouteBuilder.Build(content)
|
||||
: null;
|
||||
if (route != null)
|
||||
{
|
||||
handleContentRoute(route);
|
||||
return ReplaceStatus.Success;
|
||||
}
|
||||
|
||||
break;
|
||||
case Constants.UdiEntityType.Media:
|
||||
IPublishedContent? media = publishedSnapshot.Media?.GetById(udi);
|
||||
if (media != null)
|
||||
{
|
||||
handleMediaUrl(_apiMediaUrlProvider.GetUrl(media));
|
||||
return ReplaceStatus.Success;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return ReplaceStatus.InvalidEntityType;
|
||||
}
|
||||
|
||||
protected void ReplaceLocalImages(IPublishedSnapshot publishedSnapshot, string udi, Action<string> handleMediaUrl)
|
||||
@@ -80,5 +135,15 @@ internal abstract partial class ApiRichTextParserBase
|
||||
}
|
||||
|
||||
[GeneratedRegex("{localLink:(?<udi>umb:.+)}")]
|
||||
private static partial Regex LegacyLocalLinkRegex();
|
||||
|
||||
[GeneratedRegex("{localLink:(?<guid>.+)}")]
|
||||
private static partial Regex LocalLinkRegex();
|
||||
|
||||
private enum ReplaceStatus
|
||||
{
|
||||
NoMatch,
|
||||
Success,
|
||||
InvalidEntityType
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,9 @@ public class UmbracoPlan : MigrationPlan
|
||||
// we need to re-run this migration, as it was flawed for V14 RC3 (the migration can run twice without any issues)
|
||||
To<V_14_0_0.AddEditorUiToDataType>("{6FB5CA9E-C823-473B-A14C-FE760D75943C}");
|
||||
To<V_14_0_0.CleanUpDataTypeConfigurations>("{827360CA-0855-42A5-8F86-A51F168CB559}");
|
||||
To<V_14_0_0.MigrateRichTextConfiguration>("{FEF2DAF4-5408-4636-BB0E-B8798DF8F095}");
|
||||
|
||||
// To 14.1.0
|
||||
To<V_14_1_0.MigrateRichTextConfiguration>("{FEF2DAF4-5408-4636-BB0E-B8798DF8F095}");
|
||||
To<V_14_1_0.MigrateOldRichTextSeedConfiguration>("{A385C5DF-48DC-46B4-A742-D5BB846483BC}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
using NPoco;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_14_1_0;
|
||||
|
||||
public class MigrateOldRichTextSeedConfiguration : MigrationBase
|
||||
{
|
||||
private const string OldSeedValue =
|
||||
"{\"value\":\",code,undo,redo,cut,copy,mcepasteword,stylepicker,bold,italic,bullist,numlist,outdent,indent,mcelink,unlink,mceinsertanchor,mceimage,umbracomacro,mceinserttable,umbracoembed,mcecharmap,|1|1,2,3,|0|500,400|1049,|true|\"}";
|
||||
|
||||
private const string NewDefaultValue =
|
||||
"{\"toolbar\":[\"styles\",\"bold\",\"italic\",\"alignleft\",\"aligncenter\",\"alignright\",\"bullist\",\"numlist\",\"outdent\",\"indent\",\"sourcecode\",\"link\",\"umbmediapicker\",\"umbembeddialog\"],\"mode\":\"Classic\",\"maxImageSize\":500}";
|
||||
public MigrateOldRichTextSeedConfiguration(IMigrationContext context) : base(context)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Migrate()
|
||||
{
|
||||
Sql<ISqlContext> sql = Sql()
|
||||
.Select<DataTypeDto>()
|
||||
.From<DataTypeDto>()
|
||||
.Where<DataTypeDto>(x =>
|
||||
x.EditorAlias.Equals(Constants.PropertyEditors.Aliases.RichText)
|
||||
&& x.Configuration == OldSeedValue);
|
||||
|
||||
List<DataTypeDto> dataTypeDtos = Database.Fetch<DataTypeDto>(sql);
|
||||
|
||||
foreach (DataTypeDto dataTypeDto in dataTypeDtos)
|
||||
{
|
||||
// Update the configuration
|
||||
dataTypeDto.Configuration = NewDefaultValue;
|
||||
Database.Update(dataTypeDto);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ using Umbraco.Cms.Infrastructure.Persistence;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_14_0_0;
|
||||
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_14_1_0;
|
||||
|
||||
public class MigrateRichTextConfiguration : MigrationBase
|
||||
{
|
||||
@@ -101,8 +101,9 @@ public sealed class JsonObjectConverter : JsonConverter<object>
|
||||
JsonTokenType.Number when reader.TryGetInt32(out int i) => i,
|
||||
JsonTokenType.Number when reader.TryGetInt64(out long l) => l,
|
||||
JsonTokenType.Number => reader.GetDouble(),
|
||||
JsonTokenType.String when reader.TryGetDateTimeOffset(out DateTimeOffset datetime) => datetime,
|
||||
JsonTokenType.String when reader.TryGetDateTime(out DateTime datetime) => datetime,
|
||||
JsonTokenType.String => reader.GetString()!,
|
||||
JsonTokenType.String => reader.GetString(),
|
||||
_ => JsonDocument.ParseValue(ref reader).RootElement.Clone()
|
||||
};
|
||||
}
|
||||
|
||||
Submodule src/Umbraco.Web.UI.Client updated: 9077e80b32...9ec8c79227
1773
src/Umbraco.Web.UI.Login/package-lock.json
generated
1773
src/Umbraco.Web.UI.Login/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -15,8 +15,8 @@
|
||||
"dependencies": {
|
||||
},
|
||||
"devDependencies": {
|
||||
"@umbraco-cms/backoffice": "file:../Umbraco.Web.UI.Client",
|
||||
"@umbraco-ui/uui-css": "^1.8.0-rc.0",
|
||||
"@umbraco-cms/backoffice": "^14.0.0",
|
||||
"@umbraco-ui/uui-css": "^1.8.0",
|
||||
"msw": "^2.3.0",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.2.11",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* - Please do NOT serve this file on production.
|
||||
*/
|
||||
|
||||
const PACKAGE_VERSION = '2.3.0'
|
||||
const PACKAGE_VERSION = '2.3.1'
|
||||
const INTEGRITY_CHECKSUM = '26357c79639bfa20d64c0efca2a87423'
|
||||
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
|
||||
const activeClientIds = new Set()
|
||||
|
||||
26
tests/Umbraco.Tests.AcceptanceTest/package-lock.json
generated
26
tests/Umbraco.Tests.AcceptanceTest/package-lock.json
generated
@@ -7,8 +7,8 @@
|
||||
"name": "acceptancetest",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@umbraco/json-models-builders": "^2.0.7",
|
||||
"@umbraco/playwright-testhelpers": "^2.0.0-beta.63",
|
||||
"@umbraco/json-models-builders": "^2.0.9",
|
||||
"@umbraco/playwright-testhelpers": "^2.0.0-beta.65",
|
||||
"camelize": "^1.0.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"faker": "^4.1.0",
|
||||
@@ -132,25 +132,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@umbraco/json-models-builders": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-2.0.7.tgz",
|
||||
"integrity": "sha512-roR5A+jzIFN9z1BhogMGOEzSzoR8jOrIYIAevT7EnyS3H3OM0m0uREgvjYCQo0+QMfVws4zq4Ydjx2TIfGYvlQ==",
|
||||
"version": "2.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-2.0.9.tgz",
|
||||
"integrity": "sha512-p6LjcE38WsFCvLtRRRVOCuMvris3OXeoueFu0FZBOHk2r7PXiqYCBUls/KbKxqpixzVDAb48RBd1hV7sKPcm5A==",
|
||||
"dependencies": {
|
||||
"camelize": "^1.0.1",
|
||||
"faker": "^6.6.6"
|
||||
"camelize": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@umbraco/json-models-builders/node_modules/faker": {
|
||||
"version": "6.6.6",
|
||||
"resolved": "https://registry.npmjs.org/faker/-/faker-6.6.6.tgz",
|
||||
"integrity": "sha512-9tCqYEDHI5RYFQigXFwF1hnCwcWCOJl/hmll0lr5D2Ljjb0o4wphb69wikeJDz5qCEzXCoPvG6ss5SDP6IfOdg=="
|
||||
},
|
||||
"node_modules/@umbraco/playwright-testhelpers": {
|
||||
"version": "2.0.0-beta.63",
|
||||
"resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-2.0.0-beta.63.tgz",
|
||||
"integrity": "sha512-fLXUcWNJupfGKkD6zOGg6WcU5cmqQ6gQkyIyG+UsKSrkgCxK23+N5LrOz2OVp2NZ8GQ8kB5pJ4izvCp+yMMOnA==",
|
||||
"version": "2.0.0-beta.65",
|
||||
"resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-2.0.0-beta.65.tgz",
|
||||
"integrity": "sha512-plSD/4hhVaMl2TItAaBOUQyuy0Qo5rW3EGIF0TvL3a01s6hNoW1DrOCZhWsOOsMTkgf+oScLEsVIBMk0uDLQrg==",
|
||||
"dependencies": {
|
||||
"@umbraco/json-models-builders": "2.0.7",
|
||||
"@umbraco/json-models-builders": "2.0.9",
|
||||
"camelize": "^1.0.0",
|
||||
"faker": "^4.1.0",
|
||||
"form-data": "^4.0.0",
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
"wait-on": "^7.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@umbraco/json-models-builders": "^2.0.7",
|
||||
"@umbraco/playwright-testhelpers": "^2.0.0-beta.63",
|
||||
"@umbraco/json-models-builders": "^2.0.9",
|
||||
"@umbraco/playwright-testhelpers": "^2.0.0-beta.65",
|
||||
"camelize": "^1.0.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"faker": "^4.1.0",
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { ConstantHelper, test, AliasHelper } from '@umbraco/playwright-testhelpers';
|
||||
import {expect} from "@playwright/test";
|
||||
|
||||
const contentName = 'TestContent';
|
||||
const documentTypeName = 'TestDocumentTypeForContent';
|
||||
const dataTypeName = 'Checkbox list';
|
||||
|
||||
test.beforeEach(async ({umbracoApi, umbracoUi}) => {
|
||||
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
|
||||
await umbracoApi.document.ensureNameNotExists(contentName);
|
||||
await umbracoUi.goToBackOffice();
|
||||
});
|
||||
|
||||
test.afterEach(async ({umbracoApi}) => {
|
||||
await umbracoApi.document.ensureNameNotExists(contentName);
|
||||
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
|
||||
});
|
||||
|
||||
test('can create content with the checkbox list data type', async ({umbracoApi, umbracoUi}) => {
|
||||
// Arrange
|
||||
const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName);
|
||||
await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id);
|
||||
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
|
||||
|
||||
// Act
|
||||
await umbracoUi.content.clickActionsMenuAtRoot();
|
||||
await umbracoUi.content.clickCreateButton();
|
||||
await umbracoUi.content.chooseDocumentType(documentTypeName);
|
||||
await umbracoUi.content.enterContentName(contentName);
|
||||
await umbracoUi.content.clickSaveButton();
|
||||
|
||||
// Assert
|
||||
await umbracoUi.content.isSuccessNotificationVisible();
|
||||
expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy();
|
||||
const contentData = await umbracoApi.document.getByName(contentName);
|
||||
expect(contentData.values).toEqual([]);
|
||||
});
|
||||
|
||||
test('can publish content with the checkbox list data type', async ({umbracoApi, umbracoUi}) => {
|
||||
// Arrange
|
||||
const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName);
|
||||
await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id);
|
||||
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
|
||||
|
||||
// Act
|
||||
await umbracoUi.content.clickActionsMenuAtRoot();
|
||||
await umbracoUi.content.clickCreateButton();
|
||||
await umbracoUi.content.chooseDocumentType(documentTypeName);
|
||||
await umbracoUi.content.enterContentName(contentName);
|
||||
await umbracoUi.content.clickSaveAndPublishButton();
|
||||
|
||||
// Assert
|
||||
await umbracoUi.content.doesSuccessNotificationsHaveCount(2);
|
||||
expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy();
|
||||
const contentData = await umbracoApi.document.getByName(contentName);
|
||||
expect(contentData.values).toEqual([]);
|
||||
});
|
||||
|
||||
test('can create content with the custom approved color data type', async ({umbracoApi, umbracoUi}) => {
|
||||
// Arrange
|
||||
const customDataTypeName = 'CustomCheckboxList';
|
||||
const optionValues = ['testOption1', 'testOption2'];
|
||||
const customDataTypeId = await umbracoApi.dataType.createCheckboxListDataType(customDataTypeName, optionValues);
|
||||
await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId);
|
||||
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
|
||||
|
||||
// Act
|
||||
await umbracoUi.content.clickActionsMenuAtRoot();
|
||||
await umbracoUi.content.clickCreateButton();
|
||||
await umbracoUi.content.chooseDocumentType(documentTypeName);
|
||||
await umbracoUi.content.enterContentName(contentName);
|
||||
await umbracoUi.content.chooseCheckboxListOption(optionValues[0]);
|
||||
await umbracoUi.content.clickSaveAndPublishButton();
|
||||
|
||||
// Assert
|
||||
await umbracoUi.content.doesSuccessNotificationsHaveCount(2);
|
||||
expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy();
|
||||
const contentData = await umbracoApi.document.getByName(contentName);
|
||||
expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(customDataTypeName));
|
||||
expect(contentData.values[0].value).toEqual([optionValues[0]]);
|
||||
|
||||
// Clean
|
||||
await umbracoApi.dataType.ensureNameNotExists(customDataTypeName);
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ using Umbraco.Cms.Core.Routing;
|
||||
using Umbraco.Cms.Core.Templates;
|
||||
using Umbraco.Cms.Tests.Common;
|
||||
using Umbraco.Cms.Tests.UnitTests.TestHelpers.Objects;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Templates;
|
||||
|
||||
@@ -21,6 +22,32 @@ public class HtmlLocalLinkParserTests
|
||||
{
|
||||
[Test]
|
||||
public void Returns_Udis_From_LocalLinks()
|
||||
{
|
||||
var input = @"<p>
|
||||
<div>
|
||||
<img src='/media/12312.jpg' data-udi='umb://media/D4B18427A1544721B09AC7692F35C264' />
|
||||
<a type=""document"" href=""/{localLink:eed5fc6b-96fd-45a5-a0f1-b1adfb483c2f}"" title=""other page"">other page</a>
|
||||
</div>
|
||||
</p><p><img src='/media/234234.jpg' data-udi=""umb://media-type/B726D735E4C446D58F703F3FBCFC97A5"" />
|
||||
<a type=""media"" href=""/{localLink:7e21a725-b905-4c5f-86dc-8c41ec116e39}"" title=""media"">media</a>
|
||||
</p>";
|
||||
|
||||
var umbracoContextAccessor = new TestUmbracoContextAccessor();
|
||||
var parser = new HtmlLocalLinkParser(umbracoContextAccessor, Mock.Of<IPublishedUrlProvider>());
|
||||
|
||||
var result = parser.FindUdisFromLocalLinks(input).ToList();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.AreEqual(2, result.Count);
|
||||
Assert.Contains(UdiParser.Parse("umb://document/eed5fc6b-96fd-45a5-a0f1-b1adfb483c2f"), result);
|
||||
Assert.Contains(UdiParser.Parse("umb://media/7e21a725-b905-4c5f-86dc-8c41ec116e39"), result);
|
||||
});
|
||||
}
|
||||
|
||||
// todo remove at some point and the implementation.
|
||||
[Test]
|
||||
public void Returns_Udis_From_Legacy_LocalLinks()
|
||||
{
|
||||
var input = @"<p>
|
||||
<div>
|
||||
@@ -36,12 +63,59 @@ public class HtmlLocalLinkParserTests
|
||||
|
||||
var result = parser.FindUdisFromLocalLinks(input).ToList();
|
||||
|
||||
Assert.AreEqual(2, result.Count);
|
||||
Assert.AreEqual(UdiParser.Parse("umb://document/C093961595094900AAF9170DDE6AD442"), result[0]);
|
||||
Assert.AreEqual(UdiParser.Parse("umb://document-type/2D692FCB070B4CDA92FB6883FDBFD6E2"), result[1]);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.AreEqual(2, result.Count);
|
||||
Assert.Contains(UdiParser.Parse("umb://document/C093961595094900AAF9170DDE6AD442"), result);
|
||||
Assert.Contains(UdiParser.Parse("umb://document-type/2D692FCB070B4CDA92FB6883FDBFD6E2"), result);
|
||||
});
|
||||
}
|
||||
|
||||
// todo remove at some point and the implementation.
|
||||
[Test]
|
||||
public void Returns_Udis_From_Legacy_And_Current_LocalLinks()
|
||||
{
|
||||
var input = @"<p>
|
||||
<div>
|
||||
<img src='/media/12312.jpg' data-udi='umb://media/D4B18427A1544721B09AC7692F35C264' />
|
||||
<a href=""{locallink:umb://document/C093961595094900AAF9170DDE6AD442}"">hello</a>
|
||||
</div>
|
||||
</p><p><img src='/media/234234.jpg' data-udi=""umb://media-type/B726D735E4C446D58F703F3FBCFC97A5"" />
|
||||
<a href=""{locallink:umb://document-type/2D692FCB070B4CDA92FB6883FDBFD6E2}"">hello</a>
|
||||
</p>
|
||||
<p>
|
||||
<div>
|
||||
<img src='/media/12312.jpg' data-udi='umb://media/D4B18427A1544721B09AC7692F35C264' />
|
||||
<a type=""document"" href=""/{localLink:eed5fc6b-96fd-45a5-a0f1-b1adfb483c2f}"" title=""other page"">other page</a>
|
||||
</div>
|
||||
</p><p><img src='/media/234234.jpg' data-udi=""umb://media-type/B726D735E4C446D58F703F3FBCFC97A5"" />
|
||||
<a type=""media"" href=""/{localLink:7e21a725-b905-4c5f-86dc-8c41ec116e39}"" title=""media"">media</a>
|
||||
</p>";
|
||||
|
||||
var umbracoContextAccessor = new TestUmbracoContextAccessor();
|
||||
var parser = new HtmlLocalLinkParser(umbracoContextAccessor, Mock.Of<IPublishedUrlProvider>());
|
||||
|
||||
var result = parser.FindUdisFromLocalLinks(input).ToList();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.AreEqual(4, result.Count);
|
||||
Assert.Contains(UdiParser.Parse("umb://document/eed5fc6b-96fd-45a5-a0f1-b1adfb483c2f"), result);
|
||||
Assert.Contains(UdiParser.Parse("umb://media/7e21a725-b905-4c5f-86dc-8c41ec116e39"), result);
|
||||
Assert.Contains(UdiParser.Parse("umb://document/C093961595094900AAF9170DDE6AD442"), result);
|
||||
Assert.Contains(UdiParser.Parse("umb://document-type/2D692FCB070B4CDA92FB6883FDBFD6E2"), result);
|
||||
});
|
||||
}
|
||||
|
||||
[TestCase("", "")]
|
||||
// current
|
||||
[TestCase(
|
||||
"<a type=\"document\" href=\"/{localLink:9931BDE0-AAC3-4BAB-B838-909A7B47570E}\" title=\"world\">world</a>",
|
||||
"<a type=\"document\" href=\"/my-test-url\" title=\"world\">world</a>")]
|
||||
[TestCase(
|
||||
"<a type=\"media\" href=\"/{localLink:9931BDE0-AAC3-4BAB-B838-909A7B47570E}\" title=\"world\">world</a>",
|
||||
"<a type=\"media\" href=\"/media/1001/my-image.jpg\" title=\"world\">world</a>")]
|
||||
// legacy
|
||||
[TestCase(
|
||||
"hello href=\"{localLink:1234}\" world ",
|
||||
"hello href=\"/my-test-url\" world ")]
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Infrastructure.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.DeliveryApi;
|
||||
|
||||
[TestFixture]
|
||||
public class ApiRichTextMarkupParserTests
|
||||
{
|
||||
private Mock<IApiContentRouteBuilder> _apiContentRouteBuilder;
|
||||
private Mock<IApiMediaUrlProvider> _apiMediaUrlProvider;
|
||||
private Mock<IPublishedSnapshotAccessor> _publishedSnapshotAccessor;
|
||||
|
||||
[Test]
|
||||
public void Can_Parse_Legacy_LocalLinks()
|
||||
{
|
||||
var key1 = Guid.Parse("a1c5d649977f4ea59b1cb26055f3eed3");
|
||||
var data1 = new MockData()
|
||||
.WithKey(key1)
|
||||
.WithRoutePath("/inline/")
|
||||
.WithRouteStartPath("inline");
|
||||
|
||||
var mockData = new Dictionary<Guid, MockData>
|
||||
{
|
||||
{ key1, data1 },
|
||||
};
|
||||
|
||||
var parser = BuildDefaultSut(mockData);
|
||||
|
||||
var legacyHtml =
|
||||
"<p><a href=\"/{localLink:umb://document/a1c5d649977f4ea59b1cb26055f3eed3}\" title=\"Inline\">link </a>to another page</p>";
|
||||
|
||||
var expectedOutput =
|
||||
"<p><a href=\"/inline/\" title=\"Inline\" data-start-item-path=\"inline\" data-start-item-id=\"a1c5d649-977f-4ea5-9b1c-b26055f3eed3\">link </a>to another page</p>";
|
||||
|
||||
var parsedHtml = parser.Parse(legacyHtml);
|
||||
|
||||
Assert.AreEqual(expectedOutput, parsedHtml);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Can_Parse_LocalLinks()
|
||||
{
|
||||
var key1 = Guid.Parse("eed5fc6b-96fd-45a5-a0f1-b1adfb483c2f");
|
||||
var data1 = new MockData()
|
||||
.WithKey(key1)
|
||||
.WithRoutePath("/self/")
|
||||
.WithRouteStartPath("self");
|
||||
|
||||
var key2 = Guid.Parse("cc143afe-4cbf-46e5-b399-c9f451384373");
|
||||
var data2 = new MockData()
|
||||
.WithKey(key2)
|
||||
.WithRoutePath("/other/")
|
||||
.WithRouteStartPath("other");
|
||||
|
||||
var mockData = new Dictionary<Guid, MockData>
|
||||
{
|
||||
{ key1, data1 },
|
||||
{ key2, data2 },
|
||||
};
|
||||
|
||||
var parser = BuildDefaultSut(mockData);
|
||||
|
||||
var html =
|
||||
@"<p>Rich text outside of the blocks with a link to <a type=""document"" href=""/{localLink:eed5fc6b-96fd-45a5-a0f1-b1adfb483c2f}"" title=""itself"">itself</a><br><br></p>
|
||||
<p>and to the <a type=""document"" href=""/{localLink:cc143afe-4cbf-46e5-b399-c9f451384373}"" title=""other page"">other page</a></p>";
|
||||
|
||||
var expectedOutput =
|
||||
@"<p>Rich text outside of the blocks with a link to <a type=""document"" href=""/self/"" title=""itself"" data-start-item-path=""self"" data-start-item-id=""eed5fc6b-96fd-45a5-a0f1-b1adfb483c2f"">itself</a><br><br></p>
|
||||
<p>and to the <a type=""document"" href=""/other/"" title=""other page"" data-start-item-path=""other"" data-start-item-id=""cc143afe-4cbf-46e5-b399-c9f451384373"">other page</a></p>";
|
||||
|
||||
var parsedHtml = parser.Parse(html);
|
||||
|
||||
Assert.AreEqual(expectedOutput, parsedHtml);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Can_Parse_Legacy_LocalImages()
|
||||
{
|
||||
var key1 = Guid.Parse("395bdc0e8f4d4ad4af7f3a3f6265651e");
|
||||
var data1 = new MockData()
|
||||
.WithKey(key1)
|
||||
.WithMediaUrl("https://localhost:44331/media/bdofwokn/77gtp8fbrxmgkefatp10aw.webp");
|
||||
|
||||
var mockData = new Dictionary<Guid, MockData>
|
||||
{
|
||||
{ key1, data1 },
|
||||
};
|
||||
var parser = BuildDefaultSut(mockData);
|
||||
|
||||
var legacyHtml =
|
||||
@"<p>An image</p>\n<p><img src=""/media/bdofwokn/77gtp8fbrxmgkefatp10aw.webp?rmode=max&width=500&height=500"" alt="""" width=""500"" height=""500"" data-udi=""umb://media/395bdc0e8f4d4ad4af7f3a3f6265651e""></p>";
|
||||
|
||||
var expectedOutput =
|
||||
@"<p>An image</p>\n<p><img src=""https://localhost:44331/media/bdofwokn/77gtp8fbrxmgkefatp10aw.webp?rmode=max&width=500&height=500"" alt="""" width=""500"" height=""500""></p>";
|
||||
|
||||
var parsedHtml = parser.Parse(legacyHtml);
|
||||
|
||||
Assert.AreEqual(expectedOutput, parsedHtml);
|
||||
}
|
||||
|
||||
private ApiRichTextMarkupParser BuildDefaultSut(Dictionary<Guid, MockData> mockData)
|
||||
{
|
||||
var contentCacheMock = new Mock<IPublishedContentCache>();
|
||||
|
||||
contentCacheMock.Setup(cc => cc.GetById(It.IsAny<bool>(), It.IsAny<Guid>()))
|
||||
.Returns<bool, Guid>((preview, key) => mockData[key].PublishedContent);
|
||||
contentCacheMock.Setup(cc => cc.GetById(It.IsAny<Guid>()))
|
||||
.Returns<Guid>(key => mockData[key].PublishedContent);
|
||||
contentCacheMock.Setup(cc => cc.GetById(It.IsAny<bool>(), It.IsAny<Udi>()))
|
||||
.Returns<bool, Udi>((preview, udi) => mockData[((GuidUdi)udi).Guid].PublishedContent);
|
||||
contentCacheMock.Setup(cc => cc.GetById(It.IsAny<Udi>()))
|
||||
.Returns<Udi>(udi => mockData[((GuidUdi)udi).Guid].PublishedContent);
|
||||
|
||||
var mediaCacheMock = new Mock<IPublishedMediaCache>();
|
||||
mediaCacheMock.Setup(cc => cc.GetById(It.IsAny<bool>(), It.IsAny<Guid>()))
|
||||
.Returns<bool, Guid>((preview, key) => mockData[key].PublishedContent);
|
||||
mediaCacheMock.Setup(cc => cc.GetById(It.IsAny<Guid>()))
|
||||
.Returns<Guid>(key => mockData[key].PublishedContent);
|
||||
mediaCacheMock.Setup(cc => cc.GetById(It.IsAny<bool>(), It.IsAny<Udi>()))
|
||||
.Returns<bool, Udi>((preview, udi) => mockData[((GuidUdi)udi).Guid].PublishedContent);
|
||||
mediaCacheMock.Setup(cc => cc.GetById(It.IsAny<Udi>()))
|
||||
.Returns<Udi>(udi => mockData[((GuidUdi)udi).Guid].PublishedContent);
|
||||
|
||||
var snapshotMock = new Mock<IPublishedSnapshot>();
|
||||
snapshotMock.SetupGet(ss => ss.Content)
|
||||
.Returns(contentCacheMock.Object);
|
||||
snapshotMock.SetupGet(ss => ss.Media)
|
||||
.Returns(mediaCacheMock.Object);
|
||||
|
||||
var snapShot = snapshotMock.Object;
|
||||
|
||||
_publishedSnapshotAccessor = new Mock<IPublishedSnapshotAccessor>();
|
||||
_publishedSnapshotAccessor.Setup(psa => psa.TryGetPublishedSnapshot(out snapShot))
|
||||
.Returns(true);
|
||||
|
||||
_apiMediaUrlProvider = new Mock<IApiMediaUrlProvider>();
|
||||
_apiMediaUrlProvider.Setup(mup => mup.GetUrl(It.IsAny<IPublishedContent>()))
|
||||
.Returns<IPublishedContent>(ipc => mockData[ipc.Key].MediaUrl);
|
||||
|
||||
_apiContentRouteBuilder = new Mock<IApiContentRouteBuilder>();
|
||||
_apiContentRouteBuilder.Setup(acrb => acrb.Build(It.IsAny<IPublishedContent>(), It.IsAny<string>()))
|
||||
.Returns<IPublishedContent, string>((content, culture) => mockData[content.Key].ApiContentRoute);
|
||||
|
||||
return new ApiRichTextMarkupParser(
|
||||
_apiContentRouteBuilder.Object,
|
||||
_apiMediaUrlProvider.Object,
|
||||
_publishedSnapshotAccessor.Object,
|
||||
Mock.Of<ILogger<ApiRichTextMarkupParser>>());
|
||||
}
|
||||
|
||||
private class MockData
|
||||
{
|
||||
private Mock<IPublishedContent> _publishedContentMock = new Mock<IPublishedContent>();
|
||||
private Mock<IApiContentRoute> _apiContentRouteMock = new Mock<IApiContentRoute>();
|
||||
private Mock<IApiContentStartItem> _apiContentStartItem = new Mock<IApiContentStartItem>();
|
||||
|
||||
public IPublishedContent PublishedContent => _publishedContentMock.Object;
|
||||
|
||||
public IApiContentRoute ApiContentRoute => _apiContentRouteMock.Object;
|
||||
|
||||
public string MediaUrl { get; set; } = string.Empty;
|
||||
|
||||
public MockData()
|
||||
{
|
||||
_apiContentRouteMock.SetupGet(r => r.StartItem).Returns(_apiContentStartItem.Object);
|
||||
}
|
||||
|
||||
public MockData WithKey(Guid key)
|
||||
{
|
||||
_publishedContentMock.SetupGet(i => i.Key).Returns(key);
|
||||
_apiContentStartItem.SetupGet(rsi => rsi.Id).Returns(key);
|
||||
return this;
|
||||
}
|
||||
|
||||
public MockData WithRoutePath(string path)
|
||||
{
|
||||
_apiContentRouteMock.SetupGet(r => r.Path).Returns(path);
|
||||
return this;
|
||||
}
|
||||
|
||||
public MockData WithRouteStartPath(string path)
|
||||
{
|
||||
_apiContentStartItem.SetupGet(rsi => rsi.Path).Returns(path);
|
||||
return this;
|
||||
}
|
||||
|
||||
public MockData WithMediaUrl(string url)
|
||||
{
|
||||
MediaUrl = url;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ public class BackOfficeAreaRoutesTests
|
||||
var endpoints = new TestRouteBuilder();
|
||||
routes.CreateRoutes(endpoints);
|
||||
|
||||
Assert.AreEqual(1, endpoints.DataSources.Count);
|
||||
Assert.AreEqual(2, endpoints.DataSources.Count);
|
||||
var route = endpoints.DataSources.First();
|
||||
Assert.AreEqual(2, route.Endpoints.Count);
|
||||
|
||||
|
||||
@@ -140,6 +140,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{20CE9C97
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
build\azure-pipelines.yml = build\azure-pipelines.yml
|
||||
build\nightly-build-trigger.yml = build\nightly-build-trigger.yml
|
||||
build\templates\backoffice-install.yml = build\templates\backoffice-install.yml
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "csharp-docs", "csharp-docs", "{F2BF84D9-0A14-40AF-A0F3-B9BBBBC16A44}"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
|
||||
"version": "14.1.0-rc",
|
||||
"version": "14.1.0-rc2",
|
||||
"assemblyVersion": {
|
||||
"precision": "build"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user