Merge remote-tracking branch 'origin/release/17.0'
This commit is contained in:
3
.github/BUILD.md
vendored
3
.github/BUILD.md
vendored
@@ -79,13 +79,12 @@ Conversely, if you are working on front-end only, you want to build the back-end
|
||||
"AuthorizeCallbackLogoutPathName": "/logout",
|
||||
"AuthorizeCallbackErrorPathName": "/error",
|
||||
"BackOfficeTokenCookie": {
|
||||
"Enabled": true,
|
||||
"SameSite": "None"
|
||||
}
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> If you get stuck in a login loop, try clearing your browser cookies for localhost, and make sure that the `BackOfficeTokenCookie` settings are correct. Namely, that `SameSite` should be set to `None` when running the front-end server separately.
|
||||
> If you get stuck in a login loop, try clearing your browser cookies for localhost, and make sure that the `Umbraco:Cms:Security:BackOfficeTokenCookie:SameSite` setting is set to `None`.
|
||||
|
||||
Then run Umbraco from the command line.
|
||||
|
||||
|
||||
24
.github/copilot-instructions.md
vendored
24
.github/copilot-instructions.md
vendored
@@ -31,6 +31,8 @@ Bootstrap, build, and test the repository:
|
||||
- `cd src/Umbraco.Web.UI`
|
||||
- `dotnet run --no-build` -- Application runs on https://localhost:44339 and http://localhost:11000
|
||||
|
||||
Check out [BUILD.md](./BUILD.md) for more detailed instructions.
|
||||
|
||||
## Validation
|
||||
|
||||
- ALWAYS run through at least one complete end-to-end scenario after making changes.
|
||||
@@ -44,22 +46,26 @@ Bootstrap, build, and test the repository:
|
||||
## Testing
|
||||
|
||||
### Unit Tests (.NET)
|
||||
|
||||
- Location: tests/Umbraco.Tests.UnitTests/
|
||||
- Run: `dotnet test tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj --configuration Release --verbosity minimal`
|
||||
- Duration: ~1 minute with 3,343 tests
|
||||
- NEVER CANCEL: Set timeout to 5+ minutes
|
||||
|
||||
### Integration Tests (.NET)
|
||||
|
||||
- Location: tests/Umbraco.Tests.Integration/
|
||||
- Run: `dotnet test tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj --configuration Release --verbosity minimal`
|
||||
- NEVER CANCEL: Set timeout to 10+ minutes
|
||||
|
||||
### Frontend Tests
|
||||
|
||||
- Location: src/Umbraco.Web.UI.Client/
|
||||
- Run: `npm test` (requires `npx playwright install` first)
|
||||
- Frontend tests use Web Test Runner with Playwright
|
||||
|
||||
### Acceptance Tests (E2E)
|
||||
|
||||
- Location: tests/Umbraco.Tests.AcceptanceTest/
|
||||
- Requires running Umbraco application and configuration
|
||||
- See tests/Umbraco.Tests.AcceptanceTest/README.md for detailed setup (requires `npx playwright install` first)
|
||||
@@ -69,6 +75,7 @@ Bootstrap, build, and test the repository:
|
||||
The solution contains 30 C# projects organized as follows:
|
||||
|
||||
### Main Application Projects
|
||||
|
||||
- **Umbraco.Web.UI**: Main web application project (startup project)
|
||||
- **Umbraco.Web.UI.Client**: TypeScript frontend (backoffice)
|
||||
- **Umbraco.Web.UI.Login**: Separate login screen frontend
|
||||
@@ -77,16 +84,19 @@ The solution contains 30 C# projects organized as follows:
|
||||
- **Umbraco.Cms**: Main CMS package
|
||||
|
||||
### API Projects
|
||||
|
||||
- **Umbraco.Cms.Api.Management**: Management API
|
||||
- **Umbraco.Cms.Api.Delivery**: Content Delivery API
|
||||
- **Umbraco.Cms.Api.Common**: Shared API components
|
||||
|
||||
### Persistence Projects
|
||||
|
||||
- **Umbraco.Cms.Persistence.SqlServer**: SQL Server support
|
||||
- **Umbraco.Cms.Persistence.Sqlite**: SQLite support
|
||||
- **Umbraco.Cms.Persistence.EFCore**: Entity Framework Core abstractions
|
||||
|
||||
### Test Projects
|
||||
|
||||
- **Umbraco.Tests.UnitTests**: Unit tests
|
||||
- **Umbraco.Tests.Integration**: Integration tests
|
||||
- **Umbraco.Tests.AcceptanceTest**: End-to-end tests with Playwright
|
||||
@@ -98,6 +108,7 @@ The solution contains 30 C# projects organized as follows:
|
||||
|
||||
**Production Mode (Standard Development)**
|
||||
Use this for backend development, testing full builds, or when you don't need hot reloading:
|
||||
|
||||
1. Build frontend assets: `cd src/Umbraco.Web.UI.Client && npm run build:for:cms`
|
||||
2. Run backend: `cd src/Umbraco.Web.UI && dotnet run --no-build`
|
||||
3. Access backoffice: `https://localhost:44339/umbraco`
|
||||
@@ -105,6 +116,7 @@ Use this for backend development, testing full builds, or when you don't need ho
|
||||
|
||||
**Vite Dev Server Mode (Frontend Development with Hot Reload)**
|
||||
Use this for frontend-only development with hot module reloading:
|
||||
|
||||
1. Configure backend for frontend development - Add to `src/Umbraco.Web.UI/appsettings.json` under `Umbraco:CMS:Security`:
|
||||
```json
|
||||
"BackOfficeHost": "http://localhost:5173",
|
||||
@@ -112,7 +124,6 @@ Use this for frontend-only development with hot module reloading:
|
||||
"AuthorizeCallbackLogoutPathName": "/logout",
|
||||
"AuthorizeCallbackErrorPathName": "/error",
|
||||
"BackOfficeTokenCookie": {
|
||||
"Enabled": true,
|
||||
"SameSite": "None"
|
||||
}
|
||||
```
|
||||
@@ -124,7 +135,9 @@ Use this for frontend-only development with hot module reloading:
|
||||
**Important:** Remove the `BackOfficeHost` configuration before committing or switching back to production mode.
|
||||
|
||||
### Backend-Only Development
|
||||
|
||||
For backend-only changes, disable frontend builds:
|
||||
|
||||
- Comment out the target named "BuildStaticAssetsPreconditions" in src/Umbraco.Cms.StaticAssets.csproj:
|
||||
```
|
||||
<!--<Target Name="BuildStaticAssetsPreconditions" BeforeTargets="AssignTargetPaths">
|
||||
@@ -134,29 +147,36 @@ For backend-only changes, disable frontend builds:
|
||||
- Remember to uncomment before committing
|
||||
|
||||
### Building NuGet Packages
|
||||
|
||||
To build custom NuGet packages for testing:
|
||||
|
||||
```bash
|
||||
dotnet pack -c Release -o Build.Out
|
||||
dotnet nuget add source [Path to Build.Out folder] -n MyLocalFeed
|
||||
```
|
||||
|
||||
### Regenerating Frontend API Types
|
||||
|
||||
When changing Management API:
|
||||
|
||||
```bash
|
||||
cd src/Umbraco.Web.UI.Client
|
||||
npm run generate:server-api-dev
|
||||
```
|
||||
|
||||
Also update OpenApi.json from /umbraco/swagger/management/swagger.json
|
||||
|
||||
## Database Setup
|
||||
|
||||
Default configuration supports SQLite for development. For production-like testing:
|
||||
|
||||
- Use SQL Server/LocalDb for better performance
|
||||
- Configure connection string in src/Umbraco.Web.UI/appsettings.json
|
||||
|
||||
## Clean Up / Reset
|
||||
|
||||
To reset development environment:
|
||||
|
||||
```bash
|
||||
# Remove configuration and database
|
||||
rm src/Umbraco.Web.UI/appsettings.json
|
||||
@@ -185,7 +205,7 @@ git clean -xdf .
|
||||
**NEVER CANCEL** these operations - they are expected to take time:
|
||||
|
||||
| Operation | Expected Time | Timeout Setting |
|
||||
|-----------|--------------|-----------------|
|
||||
| ----------------------- | ------------- | --------------- |
|
||||
| `dotnet restore` | 50 seconds | 90+ seconds |
|
||||
| `dotnet build` | 4.5 minutes | 10+ minutes |
|
||||
| `npm ci` | 11 seconds | 30+ seconds |
|
||||
|
||||
1
.vscode/launch.json
vendored
1
.vscode/launch.json
vendored
@@ -107,7 +107,6 @@
|
||||
"UMBRACO__CMS__SECURITY__AUTHORIZECALLBACKLOGOUTPATHNAME": "/logout",
|
||||
"UMBRACO__CMS__SECURITY__AUTHORIZECALLBACKERRORPATHNAME": "/error",
|
||||
"UMBRACO__CMS__SECURITY__KEEPUSERLOGGEDIN": "true",
|
||||
"UMBRACO__CMS__SECURITY__BACKOFFICETOKENCOOKIE__ENABLED": "true",
|
||||
"UMBRACO__CMS__SECURITY__BACKOFFICETOKENCOOKIE__SAMESITE": "None"
|
||||
},
|
||||
"sourceFileMap": {
|
||||
|
||||
@@ -12,27 +12,27 @@
|
||||
</ItemGroup>
|
||||
<!-- Microsoft packages -->
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="9.9.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="10.0.0" />
|
||||
<PackageVersion Include="System.Linq.Async" Version="6.0.3" />
|
||||
</ItemGroup>
|
||||
<!-- Umbraco packages -->
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace Umbraco.Cms.Api.Common.DependencyInjection;
|
||||
|
||||
internal sealed class HideBackOfficeTokensHandler
|
||||
: IOpenIddictServerHandler<OpenIddictServerEvents.ApplyTokenResponseContext>,
|
||||
IOpenIddictServerHandler<OpenIddictServerEvents.ApplyAuthorizationResponseContext>,
|
||||
IOpenIddictServerHandler<OpenIddictServerEvents.ExtractTokenRequestContext>,
|
||||
IOpenIddictValidationHandler<OpenIddictValidationEvents.ProcessAuthenticationContext>,
|
||||
INotificationHandler<UserLogoutSuccessNotification>
|
||||
@@ -22,6 +23,7 @@ internal sealed class HideBackOfficeTokensHandler
|
||||
private const string RedactedTokenValue = "[redacted]";
|
||||
private const string AccessTokenCookieKey = "__Host-umbAccessToken";
|
||||
private const string RefreshTokenCookieKey = "__Host-umbRefreshToken";
|
||||
private const string PkceCodeCookieKey = "__Host-umbPkceCode";
|
||||
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly IDataProtectionProvider _dataProtectionProvider;
|
||||
@@ -70,6 +72,28 @@ internal sealed class HideBackOfficeTokensHandler
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is invoked when a PKCE code is issued to the client. For the back-office client, we will intercept the
|
||||
/// response, write the PKCE code from the response into a HTTP-only cookie, and redact the code from the response,
|
||||
/// so it's not exposed to the client.
|
||||
/// </summary>
|
||||
public ValueTask HandleAsync(OpenIddictServerEvents.ApplyAuthorizationResponseContext context)
|
||||
{
|
||||
if (context.Request?.ClientId is not Constants.OAuthClientIds.BackOffice)
|
||||
{
|
||||
// Only ever handle the back-office client.
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
if (context.Response.Code is not null)
|
||||
{
|
||||
SetCookie(GetHttpContext(), PkceCodeCookieKey, context.Response.Code);
|
||||
context.Response.Code = RedactedTokenValue;
|
||||
}
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is invoked when requesting new tokens.
|
||||
/// </summary>
|
||||
@@ -81,7 +105,23 @@ internal sealed class HideBackOfficeTokensHandler
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
// For the back-office client, this only happens when a refresh token is being exchanged for a new access token.
|
||||
// Handle when the PKCE code is being exchanged for an access token.
|
||||
if (context.Request.Code == RedactedTokenValue
|
||||
&& TryGetCookie(PkceCodeCookieKey, out var code))
|
||||
{
|
||||
context.Request.Code = code;
|
||||
|
||||
// We won't need the PKCE cookie after this, let's remove it.
|
||||
RemoveCookie(GetHttpContext(), PkceCodeCookieKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
// PCKE codes should always be redacted. If we got here, someone might be trying to pass another PKCE
|
||||
// code. For security reasons, explicitly discard the code (if any) to be on the safe side.
|
||||
context.Request.Code = null;
|
||||
}
|
||||
|
||||
// Handle when a refresh token is being exchanged for a new access token.
|
||||
if (context.Request.RefreshToken == RedactedTokenValue
|
||||
&& TryGetCookie(RefreshTokenCookieKey, out var refreshToken))
|
||||
{
|
||||
@@ -95,7 +135,6 @@ internal sealed class HideBackOfficeTokensHandler
|
||||
context.Request.RefreshToken = null;
|
||||
}
|
||||
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -140,7 +179,15 @@ internal sealed class HideBackOfficeTokensHandler
|
||||
{
|
||||
var cookieValue = EncryptionHelper.Encrypt(value, _dataProtectionProvider);
|
||||
|
||||
var cookieOptions = new CookieOptions
|
||||
RemoveCookie(httpContext, key);
|
||||
httpContext.Response.Cookies.Append(key, cookieValue, GetCookieOptions(httpContext));
|
||||
}
|
||||
|
||||
private void RemoveCookie(HttpContext httpContext, string key)
|
||||
=> httpContext.Response.Cookies.Delete(key, GetCookieOptions(httpContext));
|
||||
|
||||
private CookieOptions GetCookieOptions(HttpContext httpContext) =>
|
||||
new()
|
||||
{
|
||||
// Prevent the client-side scripts from accessing the cookie.
|
||||
HttpOnly = true,
|
||||
@@ -164,10 +211,6 @@ internal sealed class HideBackOfficeTokensHandler
|
||||
SameSite = ParseSameSiteMode(_backOfficeTokenCookieSettings.SameSite),
|
||||
};
|
||||
|
||||
httpContext.Response.Cookies.Delete(key, cookieOptions);
|
||||
httpContext.Response.Cookies.Append(key, cookieValue, cookieOptions);
|
||||
}
|
||||
|
||||
private bool TryGetCookie(string key, [NotNullWhen(true)] out string? value)
|
||||
{
|
||||
if (GetHttpContext().Request.Cookies.TryGetValue(key, out var cookieValue))
|
||||
|
||||
@@ -120,21 +120,24 @@ public static class UmbracoBuilderAuthExtensions
|
||||
configuration.UseSingletonHandler<ProcessRequestContextHandler>().SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.ResolveRequestUri.Descriptor.Order - 1);
|
||||
});
|
||||
|
||||
if (hideBackOfficeTokens)
|
||||
{
|
||||
options.AddEventHandler<OpenIddictServerEvents.ApplyTokenResponseContext>(configuration =>
|
||||
{
|
||||
configuration
|
||||
.UseSingletonHandler<HideBackOfficeTokensHandler>()
|
||||
.SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.ProcessJsonResponse<OpenIddictServerEvents.ApplyTokenResponseContext>.Descriptor.Order - 1);
|
||||
});
|
||||
options.AddEventHandler<OpenIddictServerEvents.ApplyAuthorizationResponseContext>(configuration =>
|
||||
{
|
||||
configuration
|
||||
.UseSingletonHandler<HideBackOfficeTokensHandler>()
|
||||
.SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.Authentication.ProcessQueryResponse.Descriptor.Order - 1);
|
||||
});
|
||||
options.AddEventHandler<OpenIddictServerEvents.ExtractTokenRequestContext>(configuration =>
|
||||
{
|
||||
configuration
|
||||
.UseSingletonHandler<HideBackOfficeTokensHandler>()
|
||||
.SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.ExtractPostRequest<OpenIddictServerEvents.ExtractTokenRequestContext>.Descriptor.Order + 1);
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
// Register the OpenIddict validation components.
|
||||
@@ -160,8 +163,6 @@ public static class UmbracoBuilderAuthExtensions
|
||||
configuration.UseSingletonHandler<ProcessRequestContextHandler>().SetOrder(OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandlers.ResolveRequestUri.Descriptor.Order - 1);
|
||||
});
|
||||
|
||||
if (hideBackOfficeTokens)
|
||||
{
|
||||
options.AddEventHandler<OpenIddictValidationEvents.ProcessAuthenticationContext>(configuration =>
|
||||
{
|
||||
configuration
|
||||
@@ -169,15 +170,11 @@ public static class UmbracoBuilderAuthExtensions
|
||||
// IMPORTANT: the handler must be AFTER the built-in query string handler, because the client-side SignalR library sometimes appends access tokens to the query string.
|
||||
.SetOrder(OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandlers.ExtractAccessTokenFromQueryString.Descriptor.Order + 1);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
builder.Services.AddSingleton<IDistributedBackgroundJob, OpenIddictCleanupJob>();
|
||||
builder.Services.ConfigureOptions<ConfigureOpenIddict>();
|
||||
|
||||
if (hideBackOfficeTokens)
|
||||
{
|
||||
builder.AddNotificationHandler<UserLogoutSuccessNotification, HideBackOfficeTokensHandler>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ public class NewsDashboardService : INewsDashboardService
|
||||
/// <inheritdoc />
|
||||
public async Task<NewsDashboardResponseModel> GetItemsAsync()
|
||||
{
|
||||
const string BaseUrl = "https://umbraco-dashboard-news.euwest01.umbraco.io";
|
||||
const string BaseUrl = "https://news-dashboard.umbraco.com";
|
||||
const string Path = "/api/News";
|
||||
|
||||
var version = _umbracoVersion.SemanticVersion.ToSemanticStringWithoutBuild();
|
||||
|
||||
@@ -20,7 +20,14 @@ public interface IRepositoryCacheVersionAccessor
|
||||
/// <returns>
|
||||
/// The cache version if found, or <see langword="null"/> if the version doesn't exist or the request is a client-side request.
|
||||
/// </returns>
|
||||
public Task<RepositoryCacheVersion?> GetAsync(string cacheKey);
|
||||
Task<RepositoryCacheVersion?> GetAsync(string cacheKey);
|
||||
|
||||
/// <summary>
|
||||
/// Notifies of a version change on a given cache key.
|
||||
/// </summary>
|
||||
/// <param name="cacheKey">Key of the changed version.</param>
|
||||
void VersionChanged(string cacheKey)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Notifies the accessor that caches have been synchronized.
|
||||
@@ -29,5 +36,5 @@ public interface IRepositoryCacheVersionAccessor
|
||||
/// This method is called after cache synchronization to temporarily bypass version checking,
|
||||
/// preventing recursive sync attempts while repositories reload data from the database.
|
||||
/// </remarks>
|
||||
public void CachesSynced();
|
||||
void CachesSynced();
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ internal class RepositoryCacheVersionService : IRepositoryCacheVersionService
|
||||
_logger.LogDebug("Setting cache for {EntityType} to version {Version}", typeof(TEntity).Name, newVersion);
|
||||
await _repositoryCacheVersionRepository.SaveAsync(new RepositoryCacheVersion { Identifier = cacheKey, Version = newVersion.ToString() });
|
||||
_cacheVersions[cacheKey] = newVersion;
|
||||
_repositoryCacheVersionAccessor.VersionChanged(cacheKey);
|
||||
|
||||
scope.Complete();
|
||||
}
|
||||
|
||||
@@ -9,17 +9,8 @@ namespace Umbraco.Cms.Core.Configuration.Models;
|
||||
[Obsolete("This will be replaced with a different authentication scheme. Scheduled for removal in Umbraco 18.")]
|
||||
public class BackOfficeTokenCookieSettings
|
||||
{
|
||||
private const bool StaticEnabled = false;
|
||||
|
||||
private const string StaticSameSite = "Strict";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to enable access and refresh tokens in cookies.
|
||||
/// </summary>
|
||||
[DefaultValue(StaticEnabled)]
|
||||
[Obsolete("This is only configurable in Umbraco 16. Scheduled for removal in Umbraco 17.")]
|
||||
public bool Enabled { get; set; } = StaticEnabled;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the cookie SameSite configuration.
|
||||
/// </summary>
|
||||
|
||||
@@ -64,6 +64,10 @@ public static class UdiGetterExtensions
|
||||
{
|
||||
entityType = Constants.UdiEntityType.DataTypeContainer;
|
||||
}
|
||||
else if (entity.ContainedObjectType == Constants.ObjectTypes.DocumentBlueprint)
|
||||
{
|
||||
entityType = Constants.UdiEntityType.DocumentBlueprintContainer;
|
||||
}
|
||||
else if (entity.ContainedObjectType == Constants.ObjectTypes.DocumentType)
|
||||
{
|
||||
entityType = Constants.UdiEntityType.DocumentTypeContainer;
|
||||
@@ -72,9 +76,9 @@ public static class UdiGetterExtensions
|
||||
{
|
||||
entityType = Constants.UdiEntityType.MediaTypeContainer;
|
||||
}
|
||||
else if (entity.ContainedObjectType == Constants.ObjectTypes.DocumentBlueprint)
|
||||
else if (entity.ContainedObjectType == Constants.ObjectTypes.MemberType)
|
||||
{
|
||||
entityType = Constants.UdiEntityType.DocumentBlueprintContainer;
|
||||
entityType = Constants.UdiEntityType.MemberTypeContainer;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -10,10 +10,10 @@ public sealed class EntityContainer : TreeEntityBase, IUmbracoEntity
|
||||
private static readonly Dictionary<Guid, Guid> ObjectTypeMap = new()
|
||||
{
|
||||
{ Constants.ObjectTypes.DataType, Constants.ObjectTypes.DataTypeContainer },
|
||||
{ Constants.ObjectTypes.DocumentBlueprint, Constants.ObjectTypes.DocumentBlueprintContainer },
|
||||
{ Constants.ObjectTypes.DocumentType, Constants.ObjectTypes.DocumentTypeContainer },
|
||||
{ Constants.ObjectTypes.MediaType, Constants.ObjectTypes.MediaTypeContainer },
|
||||
{ Constants.ObjectTypes.MemberType, Constants.ObjectTypes.MemberTypeContainer },
|
||||
{ Constants.ObjectTypes.DocumentBlueprint, Constants.ObjectTypes.DocumentBlueprintContainer },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
@@ -83,7 +83,7 @@ public sealed class EntityContainer : TreeEntityBase, IUmbracoEntity
|
||||
public static Guid GetContainedObjectType(Guid containerObjectType)
|
||||
{
|
||||
Guid contained = ObjectTypeMap.FirstOrDefault(x => x.Value == containerObjectType).Key;
|
||||
if (contained == null)
|
||||
if (contained == default)
|
||||
{
|
||||
throw new ArgumentException("Not a container object type.", nameof(containerObjectType));
|
||||
}
|
||||
|
||||
@@ -139,6 +139,7 @@ public class UmbracoPlan : MigrationPlan
|
||||
To<V_17_0_0.AddDistributedJobLock>("{263075BF-F18A-480D-92B4-4947D2EAB772}");
|
||||
To<V_17_0_0.AddLastSyncedTable>("26179D88-58CE-4C92-B4A4-3CBA6E7188AC");
|
||||
To<V_17_0_0.EnsureDefaultMediaFolderHasDefaultCollection>("{8B2C830A-4FFB-4433-8337-8649B0BF52C8}");
|
||||
To<V_17_0_0.InvalidateBackofficeUserAccess>("{1C38D589-26BB-4A46-9ABE-E4A0DF548A87}");
|
||||
|
||||
// To 18.0.0
|
||||
// TODO (V18): Enable on 18 branch
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_17_0_0;
|
||||
|
||||
public class InvalidateBackofficeUserAccess : AsyncMigrationBase
|
||||
{
|
||||
public InvalidateBackofficeUserAccess(IMigrationContext context)
|
||||
: base(context)
|
||||
{
|
||||
}
|
||||
|
||||
protected override Task MigrateAsync()
|
||||
{
|
||||
InvalidateBackofficeUserAccess = true;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -77,5 +77,17 @@ public class RepositoryCacheVersionAccessor : IRepositoryCacheVersionAccessor
|
||||
return databaseVersion;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public void VersionChanged(string cacheKey)
|
||||
{
|
||||
var removed = _requestCache.Remove(cacheKey);
|
||||
if (removed is false)
|
||||
{
|
||||
_logger.LogDebug("Cache version for key {CacheKey} wasn't removed from request cache, possibly missing HTTP context", cacheKey);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void CachesSynced() => _requestCache.ClearOfType<RepositoryCacheVersion>();
|
||||
}
|
||||
|
||||
3
src/Umbraco.Web.UI.Client/.github/README.md
vendored
3
src/Umbraco.Web.UI.Client/.github/README.md
vendored
@@ -41,7 +41,6 @@ Open this file in an editor: `/src/Umbraco.Web.UI/appsettings.Development.json`
|
||||
"AuthorizeCallbackLogoutPathName": "/logout",
|
||||
"AuthorizeCallbackErrorPathName": "/error",,
|
||||
"BackOfficeTokenCookie": {
|
||||
"Enabled": true,
|
||||
"SameSite": "None"
|
||||
}
|
||||
},
|
||||
@@ -52,7 +51,7 @@ Open this file in an editor: `/src/Umbraco.Web.UI/appsettings.Development.json`
|
||||
This will override the backoffice host URL, enabling the Client to run from a different origin.
|
||||
|
||||
> [!NOTE]
|
||||
> If you get stuck in a login loop, try clearing your browser cookies for localhost, and make sure that the `BackOfficeTokenCookie` settings are correct. Namely, that `SameSite` should be set to `None` when running the front-end server separately.
|
||||
> If you get stuck in a login loop, try clearing your browser cookies for localhost, and make sure that the `Umbraco:Cms:Security:BackOfficeTokenCookie:SameSite` setting is set to `None`.
|
||||
|
||||
#### 2. Start Umbraco
|
||||
|
||||
|
||||
@@ -53,35 +53,35 @@ const customItems: Array<ExampleCollectionItemModel> = [
|
||||
unique: '1',
|
||||
entityType: 'example',
|
||||
name: 'Example 1',
|
||||
icon: 'icon-shape-triangle',
|
||||
icon: 'icon-shape-triangle yellow',
|
||||
isPickable: true,
|
||||
},
|
||||
{
|
||||
unique: '2',
|
||||
entityType: 'example',
|
||||
name: 'Example 2',
|
||||
icon: 'icon-shape-triangle',
|
||||
icon: 'icon-shape-triangle yellow',
|
||||
isPickable: true,
|
||||
},
|
||||
{
|
||||
unique: '3',
|
||||
entityType: 'example',
|
||||
name: 'Example 3',
|
||||
icon: 'icon-shape-triangle',
|
||||
icon: 'icon-shape-triangle yellow',
|
||||
isPickable: true,
|
||||
},
|
||||
{
|
||||
unique: '4',
|
||||
entityType: 'example',
|
||||
name: 'Example 4',
|
||||
icon: 'icon-shape-triangle',
|
||||
icon: 'icon-shape-triangle yellow',
|
||||
isPickable: false,
|
||||
},
|
||||
{
|
||||
unique: '5',
|
||||
entityType: 'example',
|
||||
name: 'Example 5',
|
||||
icon: 'icon-shape-triangle',
|
||||
icon: 'icon-shape-triangle yellow',
|
||||
isPickable: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -19,7 +19,9 @@ import type {
|
||||
UmbTreeChildrenOfRequestArgs,
|
||||
UmbTreeRootItemsRequestArgs,
|
||||
} from '@umbraco-cms/backoffice/tree';
|
||||
import { getConfigValue, type UmbConfigCollectionModel } from '@umbraco-cms/backoffice/utils';
|
||||
import { getConfigValue } from '@umbraco-cms/backoffice/utils';
|
||||
|
||||
type ExampleDocumentPickerConfigCollectionModel = Array<{ alias: 'filter'; value: string }>;
|
||||
|
||||
export class ExampleDocumentPickerPropertyEditorDataSource
|
||||
extends UmbControllerBase
|
||||
@@ -30,17 +32,17 @@ export class ExampleDocumentPickerPropertyEditorDataSource
|
||||
#tree = new UmbDocumentTreeRepository(this);
|
||||
#item = new UmbDocumentItemRepository(this);
|
||||
#search = new UmbDocumentSearchRepository(this);
|
||||
#config: UmbConfigCollectionModel = [];
|
||||
#config: ExampleDocumentPickerConfigCollectionModel = [];
|
||||
|
||||
treePickableFilter: (treeItem: UmbDocumentTreeItemModel) => boolean = (treeItem) => !!treeItem.unique;
|
||||
|
||||
setConfig(config: UmbConfigCollectionModel) {
|
||||
setConfig(config: ExampleDocumentPickerConfigCollectionModel) {
|
||||
// TODO: add examples for all config options
|
||||
this.#config = config;
|
||||
this.#applyPickableFilterFromConfig();
|
||||
}
|
||||
|
||||
getConfig(): UmbConfigCollectionModel {
|
||||
getConfig(): ExampleDocumentPickerConfigCollectionModel {
|
||||
return this.#config;
|
||||
}
|
||||
|
||||
@@ -72,7 +74,7 @@ export class ExampleDocumentPickerPropertyEditorDataSource
|
||||
}
|
||||
|
||||
#getAllowedDocumentTypesConfig() {
|
||||
const filterString = getConfigValue<string>(this.#config, 'filter');
|
||||
const filterString = getConfigValue(this.#config, 'filter');
|
||||
const filterArray = filterString ? filterString.split(',') : [];
|
||||
const allowedContentTypes: UmbDocumentSearchRequestArgs['allowedContentTypes'] = filterArray.map(
|
||||
(unique: string) => ({
|
||||
|
||||
@@ -61,9 +61,7 @@ export class UmbDefaultCollectionMenuItemElement extends UmbLitElement {
|
||||
?selected=${this._isSelected}
|
||||
@selected=${() => this.#api?.select()}
|
||||
@deselected=${() => this.#api?.deselect()}>
|
||||
${item.icon
|
||||
? html`<uui-icon slot="icon" name=${item.icon}></uui-icon>`
|
||||
: html`<uui-icon slot="icon" name=${getItemFallbackIcon()}></uui-icon>`}
|
||||
<umb-icon slot="icon" name=${item.icon ?? getItemFallbackIcon()}></umb-icon>
|
||||
</uui-menu-item>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -82,6 +82,12 @@ export class UmbPickerInputContext<
|
||||
getSelection() {
|
||||
return this.#itemManager.getUniques();
|
||||
}
|
||||
getSelectedItems() {
|
||||
return this.#itemManager.getItems();
|
||||
}
|
||||
getSelectedItemByUnique(unique: string) {
|
||||
return this.#itemManager.getItems().find((item) => item.unique === unique);
|
||||
}
|
||||
|
||||
setSelection(selection: Array<string | null>) {
|
||||
// Note: Currently we do not support picking root item. So we filter out null values:
|
||||
@@ -111,21 +117,12 @@ export class UmbPickerInputContext<
|
||||
this.getHostElement().dispatchEvent(new UmbChangeEvent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the display name for an item to show in the remove confirmation dialog.
|
||||
* Subclasses can override this to provide custom formatting for missing items.
|
||||
* @param item - The item to get the display name for, or undefined if not found
|
||||
* @param unique - The unique identifier of the item
|
||||
* @returns The display name to show in the dialog
|
||||
*/
|
||||
protected getItemDisplayName(item: PickedItemType | undefined, unique: string): string {
|
||||
return item?.name ?? unique;
|
||||
protected async _requestItemName(unique: string) {
|
||||
return this.getSelectedItemByUnique(unique)?.name ?? '#general_notFound';
|
||||
}
|
||||
|
||||
async requestRemoveItem(unique: string) {
|
||||
const item = this.#itemManager.getItems().find((item) => item.unique === unique);
|
||||
const name = this.getItemDisplayName(item, unique);
|
||||
|
||||
const name = await this._requestItemName(unique);
|
||||
await umbConfirmModal(this, {
|
||||
color: 'danger',
|
||||
headline: `#actions_remove?`,
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { expect } from '@open-wc/testing';
|
||||
import { getConfigValue } from './index.js';
|
||||
|
||||
describe('getConfigValue', () => {
|
||||
it('should return the value for a matching alias', () => {
|
||||
const config = [
|
||||
{ alias: 'foo', value: 123 },
|
||||
{ alias: 'bar', value: 'hello' },
|
||||
];
|
||||
const result = getConfigValue(config, 'foo');
|
||||
expect(result).to.equal(123);
|
||||
});
|
||||
|
||||
it('should return undefined if alias is not found', () => {
|
||||
const config = [
|
||||
{ alias: 'foo', value: 123 },
|
||||
{ alias: 'bar', value: 'hello' },
|
||||
];
|
||||
const result = getConfigValue(config, 'baz');
|
||||
expect(result).to.be.undefined;
|
||||
});
|
||||
|
||||
it('should return undefined if config is undefined', () => {
|
||||
const result = getConfigValue(undefined, 'foo');
|
||||
expect(result).to.be.undefined;
|
||||
});
|
||||
|
||||
it('should work with different value types', () => {
|
||||
const config = [
|
||||
{ alias: 'num', value: 42 },
|
||||
{ alias: 'str', value: 'test' },
|
||||
{ alias: 'obj', value: { a: 1 } },
|
||||
];
|
||||
expect(getConfigValue(config, 'num')).to.equal(42);
|
||||
expect(getConfigValue(config, 'str')).to.equal('test');
|
||||
expect(getConfigValue(config, 'obj')).to.deep.equal({ a: 1 });
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,14 @@
|
||||
import type { UmbConfigCollectionModel } from './types.js';
|
||||
import type { UmbConfigCollectionEntryModel } from './types.js';
|
||||
|
||||
/**
|
||||
* Get a value from a config collection by its alias.
|
||||
* @param {UmbConfigCollectionModel | undefined} config - The config collection to get the value from.
|
||||
* @param {string} alias - The alias of the value to get.
|
||||
* @returns {T | undefined} The value with the specified alias, or undefined if not found or if the config is undefined.
|
||||
* @param config - The config collection to get the value from.
|
||||
* @param alias - The alias of the config entry to get the value for.
|
||||
* @returns The value of the config entry with the specified alias, or undefined if not found.
|
||||
*/
|
||||
export function getConfigValue<T>(config: UmbConfigCollectionModel | undefined, alias: string): T | undefined {
|
||||
const entry = config?.find((entry) => entry.alias === alias);
|
||||
return entry?.value as T | undefined;
|
||||
export function getConfigValue<T extends UmbConfigCollectionEntryModel, K extends T['alias']>(
|
||||
config: T[] | undefined,
|
||||
alias: K,
|
||||
) {
|
||||
return config?.find((entry) => entry.alias === alias)?.value as Extract<T, { alias: K }>['value'] | undefined;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import type { UmbDocumentTypeEntityType } from '@umbraco-cms/backoffice/document-type';
|
||||
import { UMB_VARIANT_CONTEXT } from '@umbraco-cms/backoffice/variant';
|
||||
import { UmbDocumentItemDataResolver } from '../../item/index.js';
|
||||
|
||||
interface UmbDocumentPickerInputContextOpenArgs {
|
||||
allowedContentTypes?: Array<{ unique: string; entityType: UmbDocumentTypeEntityType }>;
|
||||
@@ -56,6 +57,15 @@ export class UmbDocumentPickerInputContext extends UmbPickerInputContext<
|
||||
await super.openPicker(combinedPickerData);
|
||||
}
|
||||
|
||||
protected override async _requestItemName(unique: string): Promise<string> {
|
||||
const item = this.getSelectedItemByUnique(unique);
|
||||
const resolver = new UmbDocumentItemDataResolver(this);
|
||||
resolver.setData(item);
|
||||
const name = await resolver.getName();
|
||||
this.removeUmbController(resolver);
|
||||
return name ?? '#general_notFound';
|
||||
}
|
||||
|
||||
#pickableFilter = (
|
||||
item: UmbDocumentItemModel,
|
||||
allowedContentTypes?: Array<{ unique: string; entityType: UmbDocumentTypeEntityType }>,
|
||||
|
||||
@@ -26,8 +26,6 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ClientAssetsInputs Include="Client\**" Exclude="$(DefaultItemExcludes)" />
|
||||
|
||||
<!-- Dont include the client folder as part of packaging nuget build -->
|
||||
<Content Remove="Client\**" />
|
||||
|
||||
@@ -39,19 +37,4 @@
|
||||
<Folder Include="wwwroot\" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Restore and build Client files -->
|
||||
<Target Name="RestoreClient" Inputs="Client\package.json;Client\package-lock.json" Outputs="Client\node_modules\.package-lock.json">
|
||||
<Message Importance="high" Text="Restoring Client NPM packages..." />
|
||||
<Exec Command="npm i" WorkingDirectory="Client" />
|
||||
</Target>
|
||||
|
||||
<Target Name="BuildClient" BeforeTargets="AssignTargetPaths" DependsOnTargets="RestoreClient" Inputs="@(ClientAssetsInputs)" Outputs="$(IntermediateOutputPath)client.complete.txt">
|
||||
<Message Importance="high" Text="Executing Client NPM build script..." />
|
||||
<Exec Command="npm run build" WorkingDirectory="Client" />
|
||||
<ItemGroup>
|
||||
<_ClientAssetsBuildOutput Include="wwwroot\App_Plugins\**" />
|
||||
</ItemGroup>
|
||||
<WriteLinesToFile File="$(IntermediateOutputPath)client.complete.txt" Lines="@(_ClientAssetsBuildOutput)" Overwrite="true" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@umbraco/json-models-builders": "^2.0.42",
|
||||
"@umbraco/playwright-testhelpers": "^17.0.6",
|
||||
"@umbraco/playwright-testhelpers": "^17.0.8",
|
||||
"camelize": "^1.0.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"node-fetch": "^2.6.7"
|
||||
@@ -67,9 +67,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@umbraco/playwright-testhelpers": {
|
||||
"version": "17.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-17.0.6.tgz",
|
||||
"integrity": "sha512-M0e5HJCqSTDxORFhebaNNGzBB4v6+77MerK6ctG1f+bU3JHfmbGZr4A4HDkD9eAeU7WGu5q7xoASdI0J1wqb1w==",
|
||||
"version": "17.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-17.0.8.tgz",
|
||||
"integrity": "sha512-LUVBdsweiS0WpE1F9YTQejmSxdtgEvbcmLHX57e2S2AbNkdVuR8cJ0rYd9TqSKtNU8ckwnk6YRtVikegU0D64w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@umbraco/json-models-builders": "2.0.42",
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@umbraco/json-models-builders": "^2.0.42",
|
||||
"@umbraco/playwright-testhelpers": "^17.0.6",
|
||||
"@umbraco/playwright-testhelpers": "^17.0.8",
|
||||
"camelize": "^1.0.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"node-fetch": "^2.6.7"
|
||||
|
||||
@@ -194,7 +194,8 @@ test('can add multiple content start nodes for a user', async ({umbracoApi, umbr
|
||||
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
|
||||
});
|
||||
|
||||
test('can remove a content start node from a user', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => {
|
||||
// TODO: Look into flaky test
|
||||
test.fixme('can remove a content start node from a user', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => {
|
||||
// Arrange
|
||||
const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName);
|
||||
const userId = await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]);
|
||||
|
||||
@@ -16,9 +16,10 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Extensions;
|
||||
public class UdiGetterExtensionsTests
|
||||
{
|
||||
[TestCase(Constants.ObjectTypes.Strings.DataType, "6ad82c70-685c-4e04-9b36-d81bd779d16f", "umb://data-type-container/6ad82c70685c4e049b36d81bd779d16f")]
|
||||
[TestCase(Constants.ObjectTypes.Strings.DocumentBlueprint, "6ad82c70-685c-4e04-9b36-d81bd779d16f", "umb://document-blueprint-container/6ad82c70685c4e049b36d81bd779d16f")]
|
||||
[TestCase(Constants.ObjectTypes.Strings.DocumentType, "6ad82c70-685c-4e04-9b36-d81bd779d16f", "umb://document-type-container/6ad82c70685c4e049b36d81bd779d16f")]
|
||||
[TestCase(Constants.ObjectTypes.Strings.MediaType, "6ad82c70-685c-4e04-9b36-d81bd779d16f", "umb://media-type-container/6ad82c70685c4e049b36d81bd779d16f")]
|
||||
[TestCase(Constants.ObjectTypes.Strings.DocumentBlueprint, "6ad82c70-685c-4e04-9b36-d81bd779d16f", "umb://document-blueprint-container/6ad82c70685c4e049b36d81bd779d16f")]
|
||||
[TestCase(Constants.ObjectTypes.Strings.MemberType, "6ad82c70-685c-4e04-9b36-d81bd779d16f", "umb://member-type-container/6ad82c70685c4e049b36d81bd779d16f")]
|
||||
public void GetUdiForEntityContainer(Guid containedObjectType, Guid key, string expected)
|
||||
{
|
||||
EntityContainer entity = new EntityContainer(containedObjectType)
|
||||
|
||||
Reference in New Issue
Block a user