Merge branch 'main' into v17/dev

# Conflicts:
#	src/Umbraco.Core/Services/PropertyValidationService.cs
#	src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs
#	src/Umbraco.Infrastructure/PublishedContentQuery.cs
#	src/Umbraco.Web.UI.Client/package-lock.json
#	src/Umbraco.Web.UI.Client/package.json
#	src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts
#	src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-element-base.ts
#	templates/UmbracoProject/.template.config/template.json
#	version.json
This commit is contained in:
Andy Butland
2025-09-24 23:31:14 +02:00
265 changed files with 5961 additions and 1546 deletions
+21 -3
View File
@@ -7,9 +7,26 @@ We recommend you to [sync with our repository][sync fork] before you submit your
GitHub will have picked up on the new branch you've pushed and will offer to create a Pull Request. Click that green button and away you go.
![Create a pull request](img/createpullrequest.png)
We like to use [git flow][git flow] as much as possible, but don't worry if you are not familiar with it. The most important thing you need to know is that when you fork the Umbraco repository, the default branch is set to `contrib`. This is the branch you should be targeting.
We like to use [git flow][git flow] as much as possible, but don't worry if you are not familiar with it. The most important thing you need to know is that when you fork the Umbraco repository, the default branch is set to `main`. This is the branch you should be targeting.
Please note: we are no longer accepting features for v8 and below but will continue to merge security fixes as and when they arise.
We welcome PRs for features and bugfixes for different versions according to the [published support and EOL schedule][support-and-eol].
We don't have rules for naming PRs - so name them as you prefer. At HQ we do have a best practice on clear and concise PR naming, so if you would like to use the format feel free to do so.
Our convention of doing it is:
_Area: Description (closes #IssueID)_
1. Start by specifying the area. Fx the feature name(UFM, Tiptap etc.) or specific section (migrations, relations, segmentation).
2. In your description, where applicable, mention type of PR (Build, Bump, Fix, Refactor etc.).
4. Good practise is to make sure you describe specifically the change and/or impact of change.<br>
Example: Writing "Extension Insights: Fixes CSS alignment" instead of "Fixed issue".
6. Add (closes #IssueID) behind description, if your PR resolves an issue.
That's it!
## The review process
[review process]: #the-review-process
@@ -48,4 +65,5 @@ There will be times that we really like your proposed changes and well finish
[making larger changes]: contributing-before-you-start.md#making-large-changes
[pr or package]: contributing-before-you-start.md#pull-request-or-package
[Core collabs]: contributing-core-collabs-team.md
[Core collabs]: contributing-core-collabs-team.md
[support-and-eol]: https://umbraco.com/products/knowledge-center/long-term-support-and-end-of-life/
+4 -1
View File
@@ -1,5 +1,8 @@
{
"cSpell.words": ["unprovide"],
"cSpell.words": [
"unprovide",
"Unproviding"
],
"eslint.useFlatConfig": true,
"eslint.workingDirectories": [
"./src/Umbraco.Web.UI.Client/",
+71 -435
View File
@@ -522,160 +522,72 @@ stages:
# Connection string
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=Umbraco;Mode=Memory;Cache=Shared;Foreign Keys=True;Pooling=True
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.Sqlite
DatabaseType: SQLite
additionalEnvironmentVariables: false
strategy:
matrix:
LinuxPart1Of3:
vmImage: "ubuntu-latest"
testFolder: "DefaultConfig"
testCommand: "npm run smokeTestSqlite -- --shard=1/3"
LinuxPart2Of3:
vmImage: "ubuntu-latest"
testFolder: "DefaultConfig"
testCommand: "npm run smokeTestSqlite -- --shard=2/3"
LinuxPart3Of3:
vmImage: "ubuntu-latest"
testFolder: "DefaultConfig"
testCommand: "npm run smokeTestSqlite -- --shard=3/3"
WindowsPart1Of3:
vmImage: "windows-latest"
testFolder: "DefaultConfig"
testCommand: "npm run smokeTestSqlite -- --shard=1/3"
WindowsPart2Of3:
vmImage: "windows-latest"
testFolder: "DefaultConfig"
testCommand: "npm run smokeTestSqlite -- --shard=2/3"
WindowsPart3Of3:
vmImage: "windows-latest"
testFolder: "DefaultConfig"
testCommand: "npm run smokeTestSqlite -- --shard=3/3"
pool:
vmImage: $(vmImage)
steps:
# Setup test environment
- task: DownloadPipelineArtifact@2
displayName: Download NuGet artifacts
inputs:
artifact: nupkg
path: $(Agent.BuildDirectory)/app/nupkg
- task: NodeTool@0
displayName: Use Node.js $(nodeVersion)
retryCountOnTaskFailure: 3
inputs:
versionSpec: $(nodeVersion)
- task: UseDotNet@2
displayName: Use .NET SDK from global.json
inputs:
useGlobalJson: true
# Setup test environment Template
- template: nightly-E2E-setup-template.yml
parameters:
nodeVersion: ${{ variables.nodeVersion }}
PlaywrightUserEmail: ${{ variables.UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL }}
PlaywrightPassword: ${{ variables.UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD }}
ASPNETCORE_URLS: ${{ variables.ASPNETCORE_URLS }}
npm_config_cache: ${{ variables.npm_config_cache }}
- pwsh: |
"UMBRACO_USER_LOGIN=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL)
UMBRACO_USER_PASSWORD=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
URL=$(ASPNETCORE_URLS)
STORAGE_STAGE_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth/user.json
CONSOLE_ERRORS_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/console-errors.json" | Out-File .env
displayName: Generate .env
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
# Cache and restore NPM packages
- task: Cache@2
displayName: Cache NPM packages
inputs:
key: 'npm_e2e | "$(Agent.OS)" | $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/package-lock.json'
restoreKeys: |
npm_e2e | "$(Agent.OS)"
npm_e2e
path: $(npm_config_cache)
- script: npm ci --no-fund --no-audit --prefer-offline
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
displayName: Restore NPM packages
# Build application
- pwsh: |
$cmsVersion = "$(Build.BuildNumber)" -replace "\+",".g"
dotnet new nugetconfig
dotnet nuget add source ./nupkg --name Local
dotnet new install Umbraco.Templates::$cmsVersion
dotnet new umbraco --name UmbracoProject --version $cmsVersion --exclude-gitignore --no-restore --no-update-check
dotnet restore UmbracoProject
cp $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest.UmbracoProject/*.cs UmbracoProject
dotnet build UmbracoProject --configuration $(buildConfiguration) --no-restore
displayName: Restore project
workingDirectory: $(Agent.BuildDirectory)/app
- pwsh: |
dotnet build UmbracoProject --configuration ${{ variables.buildConfiguration }} --no-restore
dotnet dev-certs https
displayName: Build application
workingDirectory: $(Agent.BuildDirectory)/app
condition: succeeded()
# Run application
- bash: |
nohup dotnet run --project UmbracoProject --configuration $(buildConfiguration) --no-build --no-launch-profile > $(Build.ArtifactStagingDirectory)/playwright.log 2>&1 &
echo "##vso[task.setvariable variable=AcceptanceTestProcessId]$!"
displayName: Run application (Linux)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
workingDirectory: $(Agent.BuildDirectory)/app
# Run application Template
- template: nightly-E2E-run-application-template.yml
parameters:
DatabaseType: ${{ variables.DatabaseType }}
buildConfiguration: ${{ variables.buildConfiguration }}
additionalEnvironmentVariables: ${{ variables.additionalEnvironmentVariables }}
- pwsh: |
$process = Start-Process dotnet "run --project UmbracoProject --configuration $(buildConfiguration) --no-build --no-launch-profile 2>&1" -PassThru -NoNewWindow -RedirectStandardOutput $(Build.ArtifactStagingDirectory)/playwright.log
Write-Host "##vso[task.setvariable variable=AcceptanceTestProcessId]$($process.Id)"
displayName: Run application (Windows)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
workingDirectory: $(Agent.BuildDirectory)/app
# Wait for application to start responding to requests
- pwsh: npx wait-on -v --interval 1000 --timeout 120000 $(ASPNETCORE_URLS)
displayName: Wait for application
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Install Playwright and dependencies
- pwsh: npx playwright install chromium
displayName: Install Playwright only with Chromium browser
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Test
- pwsh: $(testCommand)
displayName: Run Playwright tests
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
env:
CI: true
CommitId: $(Build.SourceVersion)
AgentOs: $(Agent.OS)
# Stop application
- bash: kill -15 $(AcceptanceTestProcessId)
displayName: Stop application (Linux)
condition: and(ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Linux'))
- pwsh: Stop-Process -Id $(AcceptanceTestProcessId)
displayName: Stop application (Windows)
condition: and(ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Windows_NT'))
# Copy artifacts
- pwsh: |
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/results/*) {
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results/* $(Build.ArtifactStagingDirectory) -Recurse
}
displayName: Copy Playwright results
condition: succeededOrFailed()
# Copy console error log
- pwsh: |
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/console-errors.json) {
Copy-Item tests/Umbraco.Tests.AcceptanceTest/console-errors.json $(Build.ArtifactStagingDirectory)
}
displayName: Copy console error log
condition: succeededOrFailed()
# Publish test artifacts
- task: PublishPipelineArtifact@1
displayName: Publish test artifacts
condition: succeededOrFailed()
inputs:
targetPath: $(Build.ArtifactStagingDirectory)
artifact: "Acceptance Test Results - $(Agent.JobName) - Attempt #$(System.JobAttempt)"
# Publish test results
- task: PublishTestResults@2
displayName: "Publish test results"
condition: succeededOrFailed()
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '*.xml'
searchFolder: "tests/Umbraco.Tests.AcceptanceTest/results"
testRunTitle: "$(Agent.JobName)"
# Run tests Template
- template: nightly-E2E-run-tests-template.yml
parameters:
testCommand: $(testCommand)
ASPNETCORE_URLS: ${{ variables.ASPNETCORE_URLS }}
DatabaseType: ${{ variables.DatabaseType }}
- job:
displayName: E2E Smoke Tests (SQL Server)
@@ -683,354 +595,78 @@ stages:
# Connection string
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
DatabaseType: SQLServer
additionalEnvironmentVariables: false
strategy:
matrix:
${{ if eq(parameters.sqlServerLinuxAcceptanceTests, True) }}:
LinuxPart1Of3:
testCommand: "npm run smokeTest -- --shard=1/3"
vmImage: "ubuntu-latest"
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
testFolder: "DefaultConfig"
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
LinuxPart2Of3:
testCommand: "npm run smokeTest -- --shard=2/3"
vmImage: "ubuntu-latest"
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
testFolder: "DefaultConfig"
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
LinuxPart3Of3:
testCommand: "npm run smokeTest -- --shard=3/3"
vmImage: "ubuntu-latest"
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
testFolder: "DefaultConfig"
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
WindowsPart1Of3:
vmImage: "windows-latest"
testFolder: "DefaultConfig"
testCommand: "npm run smokeTest -- --shard=1/3"
WindowsPart2Of3:
vmImage: "windows-latest"
testFolder: "DefaultConfig"
testCommand: "npm run smokeTest -- --shard=2/3"
WindowsPart3Of3:
vmImage: "windows-latest"
testFolder: "DefaultConfig"
testCommand: "npm run smokeTest -- --shard=3/3"
pool:
vmImage: $(vmImage)
steps:
# Setup test environment
- task: DownloadPipelineArtifact@2
displayName: Download NuGet artifacts
inputs:
artifact: nupkg
path: $(Agent.BuildDirectory)/app/nupkg
- task: NodeTool@0
displayName: Use Node.js $(nodeVersion)
inputs:
versionSpec: $(nodeVersion)
- task: UseDotNet@2
displayName: Use .NET SDK from global.json
inputs:
useGlobalJson: true
# Setup test environment Template
- template: nightly-E2E-setup-template.yml
parameters:
nodeVersion: ${{ variables.nodeVersion }}
PlaywrightUserEmail: ${{ variables.UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL }}
PlaywrightPassword: ${{ variables.UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD }}
ASPNETCORE_URLS: ${{ variables.ASPNETCORE_URLS }}
npm_config_cache: ${{ variables.npm_config_cache }}
- pwsh: |
"UMBRACO_USER_LOGIN=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL)
UMBRACO_USER_PASSWORD=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
URL=$(ASPNETCORE_URLS)
STORAGE_STAGE_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth/user.json
CONSOLE_ERRORS_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/console-errors.json" | Out-File .env
displayName: Generate .env
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
# Cache and restore NPM packages
- task: Cache@2
displayName: Cache NPM packages
inputs:
key: 'npm_e2e | "$(Agent.OS)" | $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/package-lock.json'
restoreKeys: |
npm_e2e | "$(Agent.OS)"
npm_e2e
path: $(npm_config_cache)
- script: npm ci --no-fund --no-audit --prefer-offline
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
displayName: Restore NPM packages
# Build application
- pwsh: |
$cmsVersion = "$(Build.BuildNumber)" -replace "\+",".g"
dotnet new nugetconfig
dotnet nuget add source ./nupkg --name Local
dotnet new install Umbraco.Templates::$cmsVersion
dotnet new umbraco --name UmbracoProject --version $cmsVersion --exclude-gitignore --no-restore --no-update-check
dotnet restore UmbracoProject
cp $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest.UmbracoProject/*.cs UmbracoProject
dotnet build UmbracoProject --configuration $(buildConfiguration) --no-restore
displayName: Restore project
workingDirectory: $(Agent.BuildDirectory)/app
- pwsh: |
dotnet build UmbracoProject --configuration ${{ variables.buildConfiguration }} --no-restore
dotnet dev-certs https
displayName: Build application
workingDirectory: $(Agent.BuildDirectory)/app
condition: succeeded()
# Start SQL Server
- powershell: docker run --name mssql -d -p 1433:1433 -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=$(SA_PASSWORD)" mcr.microsoft.com/mssql/server:2022-latest
displayName: Start SQL Server Docker image (Linux)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
# Run application Template
- template: nightly-E2E-run-application-template.yml
parameters:
SA_PASSWORD: ${{ variables.SA_PASSWORD }}
buildConfiguration: ${{ variables.buildConfiguration }}
DatabaseType: ${{ variables.DatabaseType }}
additionalEnvironmentVariables: ${{ variables.additionalEnvironmentVariables }}
- pwsh: SqlLocalDB start MSSQLLocalDB
displayName: Start SQL Server LocalDB (Windows)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
# Run application
- bash: |
nohup dotnet run --project UmbracoProject --configuration $(buildConfiguration) --no-build --no-launch-profile > $(Build.ArtifactStagingDirectory)/playwright.log 2>&1 &
echo "##vso[task.setvariable variable=AcceptanceTestProcessId]$!"
displayName: Run application (Linux)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
workingDirectory: $(Agent.BuildDirectory)/app
- pwsh: |
$process = Start-Process dotnet "run --project UmbracoProject --configuration $(buildConfiguration) --no-build --no-launch-profile 2>&1" -PassThru -NoNewWindow -RedirectStandardOutput $(Build.ArtifactStagingDirectory)/playwright.log
Write-Host "##vso[task.setvariable variable=AcceptanceTestProcessId]$($process.Id)"
displayName: Run application (Windows)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
workingDirectory: $(Agent.BuildDirectory)/app
# Wait for application to start responding to requests
- pwsh: npx wait-on -v --interval 1000 --timeout 120000 $(ASPNETCORE_URLS)
displayName: Wait for application
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Install Playwright and dependencies
- pwsh: npx playwright install chromium
displayName: Install Playwright only with Chromium browser
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Test
- pwsh: $(testCommand)
displayName: Run Playwright tests
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
env:
CI: true
CommitId: $(Build.SourceVersion)
AgentOs: $(Agent.OS)
# Stop application
- bash: kill -15 $(AcceptanceTestProcessId)
displayName: Stop application (Linux)
condition: and(ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Linux'))
- pwsh: Stop-Process -Id $(AcceptanceTestProcessId)
displayName: Stop application (Windows)
condition: and(ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Windows_NT'))
# Stop SQL Server
- pwsh: docker stop mssql
displayName: Stop SQL Server Docker image (Linux)
condition: eq(variables['Agent.OS'], 'Linux')
- pwsh: SqlLocalDB stop MSSQLLocalDB
displayName: Stop SQL Server LocalDB (Windows)
condition: eq(variables['Agent.OS'], 'Windows_NT')
# Copy artifacts
- pwsh: |
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/results/*) {
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results/* $(Build.ArtifactStagingDirectory) -Recurse
}
displayName: Copy Playwright results
condition: succeededOrFailed()
# Copy console error log
- pwsh: |
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/console-errors.json) {
Copy-Item tests/Umbraco.Tests.AcceptanceTest/console-errors.json $(Build.ArtifactStagingDirectory)
}
displayName: Copy console error log
condition: succeededOrFailed()
# Publish test artifacts
- task: PublishPipelineArtifact@1
displayName: Publish test artifacts
condition: succeededOrFailed()
inputs:
targetPath: $(Build.ArtifactStagingDirectory)
artifact: "Acceptance Test Results - $(Agent.JobName) - Attempt #$(System.JobAttempt)"
# Publish test results
- task: PublishTestResults@2
displayName: "Publish test results"
condition: succeededOrFailed()
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '*.xml'
searchFolder: "tests/Umbraco.Tests.AcceptanceTest/results"
testRunTitle: "$(Agent.JobName)"
- job:
displayName: E2E Release Tests (SQL Server)
variables:
# Connection string
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
condition: eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True')
strategy:
matrix:
WindowsPart1Of3:
vmImage: "windows-latest"
testCommand: "npm run releaseTest -- --shard=1/3"
WindowsPart2Of3:
vmImage: "windows-latest"
testCommand: "npm run releaseTest -- --shard=2/3"
WindowsPart3Of3:
vmImage: "windows-latest"
testCommand: "npm run releaseTest -- --shard=3/3"
pool:
vmImage: $(vmImage)
steps:
# Setup test environment
- task: DownloadPipelineArtifact@2
displayName: Download NuGet artifacts
inputs:
artifact: nupkg
path: $(Agent.BuildDirectory)/app/nupkg
- task: NodeTool@0
displayName: Use Node.js $(nodeVersion)
inputs:
versionSpec: $(nodeVersion)
- task: UseDotNet@2
displayName: Use .NET SDK from global.json
inputs:
useGlobalJson: true
- pwsh: |
"UMBRACO_USER_LOGIN=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL)
UMBRACO_USER_PASSWORD=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
URL=$(ASPNETCORE_URLS)
STORAGE_STAGE_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth/user.json
CONSOLE_ERRORS_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/console-errors.json" | Out-File .env
displayName: Generate .env
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
# Cache and restore NPM packages
- task: Cache@2
displayName: Cache NPM packages
inputs:
key: 'npm_e2e | "$(Agent.OS)" | $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/package-lock.json'
restoreKeys: |
npm_e2e | "$(Agent.OS)"
npm_e2e
path: $(npm_config_cache)
- script: npm ci --no-fund --no-audit --prefer-offline
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
displayName: Restore NPM packages
# Build application
- pwsh: |
$cmsVersion = "$(Build.BuildNumber)" -replace "\+",".g"
dotnet new nugetconfig
dotnet nuget add source ./nupkg --name Local
dotnet new install Umbraco.Templates::$cmsVersion
dotnet new umbraco --name UmbracoProject --version $cmsVersion --exclude-gitignore --no-restore --no-update-check
dotnet restore UmbracoProject
cp $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest.UmbracoProject/*.cs UmbracoProject
dotnet build UmbracoProject --configuration $(buildConfiguration) --no-restore
dotnet dev-certs https
displayName: Build application
workingDirectory: $(Agent.BuildDirectory)/app
# Start SQL Server
- powershell: docker run --name mssql -d -p 1433:1433 -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=$(SA_PASSWORD)" mcr.microsoft.com/mssql/server:2022-latest
displayName: Start SQL Server Docker image (Linux)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
- pwsh: SqlLocalDB start MSSQLLocalDB
displayName: Start SQL Server LocalDB (Windows)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
# Run application
- bash: |
nohup dotnet run --project UmbracoProject --configuration $(buildConfiguration) --no-build --no-launch-profile > $(Build.ArtifactStagingDirectory)/playwright.log 2>&1 &
echo "##vso[task.setvariable variable=AcceptanceTestProcessId]$!"
displayName: Run application (Linux)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
workingDirectory: $(Agent.BuildDirectory)/app
- pwsh: |
$process = Start-Process dotnet "run --project UmbracoProject --configuration $(buildConfiguration) --no-build --no-launch-profile 2>&1" -PassThru -NoNewWindow -RedirectStandardOutput $(Build.ArtifactStagingDirectory)/playwright.log
Write-Host "##vso[task.setvariable variable=AcceptanceTestProcessId]$($process.Id)"
displayName: Run application (Windows)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
workingDirectory: $(Agent.BuildDirectory)/app
# Wait for application to start responding to requests
- pwsh: npx wait-on -v --interval 1000 --timeout 120000 $(ASPNETCORE_URLS)
displayName: Wait for application
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Install Playwright and dependencies
- pwsh: npx playwright install chromium
displayName: Install Playwright only with Chromium browser
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Test
- pwsh: $(testCommand)
displayName: Run Playwright tests
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
env:
CI: true
CommitId: $(Build.SourceVersion)
AgentOs: $(Agent.OS)
# Stop application
- bash: kill -15 $(AcceptanceTestProcessId)
displayName: Stop application (Linux)
condition: and(ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Linux'))
- pwsh: Stop-Process -Id $(AcceptanceTestProcessId)
displayName: Stop application (Windows)
condition: and(ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Windows_NT'))
# Stop SQL Server
- pwsh: docker stop mssql
displayName: Stop SQL Server Docker image (Linux)
condition: eq(variables['Agent.OS'], 'Linux')
- pwsh: SqlLocalDB stop MSSQLLocalDB
displayName: Stop SQL Server LocalDB (Windows)
condition: eq(variables['Agent.OS'], 'Windows_NT')
# Copy artifacts
- pwsh: |
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/results/*) {
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results/* $(Build.ArtifactStagingDirectory) -Recurse
}
displayName: Copy Playwright results
condition: succeededOrFailed()
# Copy console error log
- pwsh: |
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/console-errors.json) {
Copy-Item tests/Umbraco.Tests.AcceptanceTest/console-errors.json $(Build.ArtifactStagingDirectory)
}
displayName: Copy console error log
condition: succeededOrFailed()
# Publish test artifacts
- task: PublishPipelineArtifact@1
displayName: Publish test artifacts
condition: succeededOrFailed()
inputs:
targetPath: $(Build.ArtifactStagingDirectory)
artifact: "Acceptance Test Results - $(Agent.JobName) - Attempt #$(System.JobAttempt)"
# Publish test results
- task: PublishTestResults@2
displayName: "Publish test results"
condition: succeededOrFailed()
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '*.xml'
searchFolder: "tests/Umbraco.Tests.AcceptanceTest/results"
testRunTitle: "$(Agent.JobName)"
# Run tests Template
- template: nightly-E2E-run-tests-template.yml
parameters:
testCommand: $(testCommand)
ASPNETCORE_URLS: ${{ variables.ASPNETCORE_URLS }}
DatabaseType: ${{ variables.DatabaseType }}
###############################################
## Release
@@ -1237,4 +873,4 @@ stages:
storage: umbracoapidocs
ContainerName: "$web"
BlobPrefix: v$(umbracoMajorVersion)/ui-api
CleanTargetBeforeCopy: true
CleanTargetBeforeCopy: true
+74
View File
@@ -0,0 +1,74 @@
parameters:
- name: testFolder
type: string
default: ''
- name: buildConfiguration
type: string
default: ''
- name: additionalEnvironmentVariables
type: string
default: 'false'
steps:
- pwsh: |
dotnet restore UmbracoProject
cp $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest.UmbracoProject/*.cs UmbracoProject
displayName: Restore project
workingDirectory: $(Agent.BuildDirectory)/app
# Update application to use necessary app settings
- pwsh: |
$sourcePath = "$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/tests/${{ parameters.testFolder }}/AdditionalSetup"
$destinationPath = "UmbracoProject"
$jsonFiles = Get-ChildItem -Path $sourcePath -Filter "*.json"
if ($jsonFiles) {
$jsonFiles | ForEach-Object {
Write-Host "Copying: $($_.FullName)"
Copy-Item -Path $_.FullName -Destination $destinationPath -Force
}
} else {
Write-Host "No JSON files found."
}
displayName: Update application to use necessary app settings
workingDirectory: $(Agent.BuildDirectory)/app
# Update application to use necessary App_Plugins
- pwsh: |
$sourcePath = "$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/tests/${{ parameters.testFolder }}/AdditionalSetup"
$destinationPath = "UmbracoProject"
$appPluginsFolders = Get-ChildItem -Path $sourcePath -Directory -Filter "App_Plugins"
if ($appPluginsFolders) {
foreach ($folder in $appPluginsFolders) {
Write-Host "Copying folder: $($folder.FullName)"
Copy-Item -Path $folder.FullName -Destination $destinationPath -Recurse -Force
}
} else {
Write-Host "No App_Plugins found."
}
displayName: Update application to use necessary app plugins
workingDirectory: $(Agent.BuildDirectory)/app
# Update application to use necessary classes
- pwsh: |
$sourcePath = "$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/tests/${{ parameters.testFolder }}/AdditionalSetup"
$destinationPath = "UmbracoProject"
$csharpFiles = Get-ChildItem -Path $sourcePath -Filter "*.cs"
if ($csharpFiles) {
$csharpFiles | ForEach-Object {
Write-Host "Copying: $($_.FullName)"
Copy-Item -Path $_.FullName -Destination $destinationPath -Force
}
} else {
Write-Host "No C# files found."
}
displayName: Update application to use necessary classes
workingDirectory: $(Agent.BuildDirectory)/app
- pwsh: |
dotnet build UmbracoProject --configuration ${{ parameters.buildConfiguration }} --no-restore
dotnet dev-certs https
displayName: Build application
workingDirectory: $(Agent.BuildDirectory)/app
condition: and(succeeded(), eq(variables['additionalEnvironmentVariables'], 'false'))
@@ -0,0 +1,45 @@
parameters:
- name: SA_PASSWORD
type: string
default: ''
- name: buildConfiguration
type: string
default: ''
- name: additionalEnvironmentVariables
type: string
default: 'false'
- name: DatabaseType
type: string
default: ''
steps:
# Skips the SQLServer setup if the databaseType does not match
- ${{ if eq(parameters.DatabaseType, 'SQLServer') }}:
# Start SQL Server Linux
- powershell: docker run --name mssql -d -p 1433:1433 -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=${{ parameters.SA_PASSWORD }}" mcr.microsoft.com/mssql/server:2022-latest
displayName: Start SQL Server Docker image (Linux)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
# Start SQL Server LocalDB Windows
- pwsh: SqlLocalDB start MSSQLLocalDB
displayName: Start SQL Server LocalDB (Windows)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
# Run application for Linux
- bash: |
nohup dotnet run --project UmbracoProject --configuration ${{ parameters.buildConfiguration }} --no-build --no-launch-profile > $(Build.ArtifactStagingDirectory)/playwright.log 2>&1 &
echo "##vso[task.setvariable variable=AcceptanceTestProcessId]$!"
displayName: Run application (Linux)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'), eq(variables['additionalEnvironmentVariables'], 'false'))
workingDirectory: $(Agent.BuildDirectory)/app
# Run application for Windows
- pwsh: |
$process = Start-Process dotnet "run --project UmbracoProject --configuration ${{ parameters.buildConfiguration }} --no-build --no-launch-profile 2>&1" -PassThru -NoNewWindow -RedirectStandardOutput $(Build.ArtifactStagingDirectory)/playwright.log
Write-Host "##vso[task.setvariable variable=AcceptanceTestProcessId]$($process.Id)"
displayName: Run application (Windows)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'), eq(variables['additionalEnvironmentVariables'], 'false'))
workingDirectory: $(Agent.BuildDirectory)/app
+106
View File
@@ -0,0 +1,106 @@
parameters:
- name: ASPNETCORE_URLS
type: string
default: ''
- name: testCommand
type: string
default: ''
- name: port
type: string
default: ''
- name: AZUREB2CTESTUSEREMAIL
type: string
default: ''
- name: AZUREB2CTESTUSERPASSWORD
type: string
default: ''
- name: DatabaseType
type: string
default: ''
steps:
# Ensures we have the package wait-on installed
- pwsh: npm install wait-on
displayName: Install wait-on package
# Wait for either the port of the aspnetcore url
- pwsh: |
$Port = "${{ parameters.port }}"
$Url = "${{ parameters.ASPNETCORE_URLS }}"
if ($Port -ne "") {
Write-Host "Waiting on TCP port $Port"
npx wait-on -v --interval 1000 --timeout 120000 "tcp:$Port"
} else {
Write-Host "Waiting on URL $Url"
npx wait-on -v --interval 1000 --timeout 120000 "$Url"
}
displayName: Wait for application
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Install Playwright and dependencies
- pwsh: npx playwright install chromium
displayName: Install Playwright only with Chromium browser
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Test
- pwsh: ${{ parameters.testCommand }}
displayName: Run Playwright tests
continueOnError: true
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
env:
CI: true
CommitId: $(Build.SourceVersion)
AgentOs: $(Agent.OS)
AZUREADB2CTESTUSEREMAIL: ${{ parameters.AZUREB2CTESTUSEREMAIL }}
AZUREADB2CTESTUSERPASSWORD: ${{ parameters.AZUREB2CTESTUSERPASSWORD }}
# Stop application
- bash: kill -15 $(AcceptanceTestProcessId)
displayName: Stop application (Linux)
condition: and(succeededOrFailed(), ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Linux'))
- pwsh: Stop-Process -Id $(AcceptanceTestProcessId)
displayName: Stop application (Windows)
condition: and(succeededOrFailed(), ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Windows_NT'))
- ${{ if eq(parameters.DatabaseType, 'SQLServer') }}:
# Stop SQL Server
- pwsh: docker stop mssql
displayName: Stop SQL Server Docker image (Linux)
condition: and(succeededOrFailed(), eq(variables['Agent.OS'], 'Linux'))
- pwsh: SqlLocalDB stop MSSQLLocalDB
displayName: Stop SQL Server LocalDB (Windows)
condition: and(succeededOrFailed(), eq(variables['Agent.OS'], 'Windows_NT'))
# Copy artifacts
- pwsh: |
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/results/*) {
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results/* $(Build.ArtifactStagingDirectory) -Recurse
}
displayName: Copy Playwright results
condition: succeededOrFailed()
# Publish
- task: PublishPipelineArtifact@1
displayName: Publish test artifacts
condition: succeededOrFailed()
inputs:
targetPath: $(Build.ArtifactStagingDirectory)
artifact: "Acceptance Test Results - $(Agent.JobName) - Attempt #$(System.JobAttempt)"
# Publish test results
- task: PublishTestResults@2
displayName: "Publish test results"
condition: succeededOrFailed()
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '*.xml'
searchFolder: "tests/Umbraco.Tests.AcceptanceTest/results"
testRunTitle: "$(Agent.JobName)"
+70
View File
@@ -0,0 +1,70 @@
parameters:
- name: nodeVersion
type: string
default: ''
- name: PlaywrightUserEmail
type: string
default: ''
- name: PlaywrightPassword
type: string
default: ''
- name: ASPNETCORE_URLS
type: string
default: ''
- name: npm_config_cache
type: string
default: ''
steps:
- task: DownloadPipelineArtifact@2
displayName: Download NuGet artifacts
inputs:
artifact: nupkg
path: $(Agent.BuildDirectory)/app/nupkg
- task: NodeTool@0
displayName: Use Node.js $(nodeVersion)
inputs:
versionSpec: $(nodeVersion)
- task: UseDotNet@2
displayName: Use .NET SDK from global.json
inputs:
useGlobalJson: true
- pwsh: |
"UMBRACO_USER_LOGIN=${{ parameters.PlaywrightUserEmail }}
UMBRACO_USER_PASSWORD=${{ parameters.PlaywrightPassword }}
URL=${{ parameters.ASPNETCORE_URLS }}
STORAGE_STAGE_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth/user.json
CONSOLE_ERRORS_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/console-errors.json" | Out-File .env
displayName: Generate .env
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
# Cache and restore NPM packages
- task: Cache@2
displayName: Cache NPM packages
inputs:
key: 'npm_e2e | "$(Agent.OS)" | $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/package-lock.json'
restoreKeys: |
npm_e2e | "$(Agent.OS)"
npm_e2e
path: ${{ parameters.npm_config_cache }}
- script: npm ci --no-fund --no-audit --prefer-offline
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
displayName: Restore NPM packages
# Install Template
- pwsh: |
$cmsVersion = "$(Build.BuildNumber)" -replace "\+",".g"
dotnet new nugetconfig
dotnet nuget add source ./nupkg --name Local
dotnet new install Umbraco.Templates::$cmsVersion
dotnet new umbraco --name UmbracoProject --version $cmsVersion --exclude-gitignore --no-restore --no-update-check
displayName: Install Template
workingDirectory: $(Agent.BuildDirectory)/app
+402 -275
View File
@@ -12,9 +12,18 @@ schedules:
- main
parameters:
# Skipped due to DB locks
- name: sqliteAcceptanceTests
displayName: Run SQLite Acceptance Tests
- name: skipIntegrationTests
displayName: Skip integration tests
type: boolean
default: false
- name: differentAppSettingsAcceptanceTests
displayName: Run acceptance tests with different app settings
type: boolean
default: false
- name: skipDefaultConfigAcceptanceTests
displayName: Skip tests with DefaultConfig
type: boolean
default: false
@@ -100,8 +109,191 @@ stages:
targetPath: $(Build.ArtifactStagingDirectory)/npm
artifactName: npm
- stage: E2E
displayName: E2E Tests
- stage: Integration
displayName: Integration Tests
dependsOn: Build
condition: ${{ eq(parameters.skipIntegrationTests, false) }}
jobs:
# Integration Tests (SQLite)
- job:
timeoutInMinutes: 180
displayName: Integration Tests (SQLite)
strategy:
matrix:
# Windows:
# vmImage: 'windows-latest'
# We split the tests into 3 parts for each OS to reduce the time it takes to run them on the pipeline
LinuxPart1Of3:
vmImage: "ubuntu-latest"
# Filter tests that are part of the Umbraco.Infrastructure namespace but not part of the Umbraco.Infrastructure.Service namespace
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure) & (FullyQualifiedName!~Umbraco.Infrastructure.Service)"
LinuxPart2Of3:
vmImage: "ubuntu-latest"
# Filter tests that are part of the Umbraco.Infrastructure.Service namespace
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)"
LinuxPart3Of3:
vmImage: "ubuntu-latest"
# Filter tests that are not part of the Umbraco.Infrastructure namespace. So this will run all tests that are not part of the Umbraco.Infrastructure namespace
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure)"
macOSPart1Of3:
vmImage: "macOS-latest"
# Filter tests that are part of the Umbraco.Infrastructure namespace but not part of the Umbraco.Infrastructure.Service namespace
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure) & (FullyQualifiedName!~Umbraco.Infrastructure.Service)"
macOSPart2Of3:
vmImage: "macOS-latest"
# Filter tests that are part of the Umbraco.Infrastructure.Service namespace
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)"
macOSPart3Of3:
vmImage: "macOS-latest"
# Filter tests that are not part of the Umbraco.Infrastructure namespace.
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure)"
pool:
vmImage: $(vmImage)
variables:
Tests__Database__DatabaseType: "Sqlite"
steps:
- checkout: self
submodules: false
lfs: false,
fetchDepth: 1
fetchFilter: tree:0
# Setup test environment
- task: DownloadPipelineArtifact@2
displayName: Download build artifacts
inputs:
artifact: build_output
path: $(Build.SourcesDirectory)
- task: UseDotNet@2
displayName: Use .NET SDK from global.json
inputs:
useGlobalJson: true
# Test
- task: DotNetCoreCLI@2
displayName: Run dotnet test
inputs:
command: test
projects: "tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj"
testRunTitle: Integration Tests SQLite - $(Agent.OS)
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build'
# Integration Tests (SQL Server)
- job:
timeoutInMinutes: 180
displayName: Integration Tests (SQL Server)
variables:
SA_PASSWORD: UmbracoAcceptance123!
strategy:
matrix:
# We split the tests into 3 parts for each OS to reduce the time it takes to run them on the pipeline
WindowsPart1Of3:
vmImage: "windows-latest"
Tests__Database__DatabaseType: LocalDb
Tests__Database__SQLServerMasterConnectionString: N/A
# Filter tests that are part of the Umbraco.Infrastructure namespace but not part of the Umbraco.Infrastructure.Service namespace
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure) & (FullyQualifiedName!~Umbraco.Infrastructure.Service)"
WindowsPart2Of3:
vmImage: "windows-latest"
Tests__Database__DatabaseType: LocalDb
Tests__Database__SQLServerMasterConnectionString: N/A
# Filter tests that are part of the Umbraco.Infrastructure.Service namespace
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)"
WindowsPart3Of3:
vmImage: "windows-latest"
Tests__Database__DatabaseType: LocalDb
Tests__Database__SQLServerMasterConnectionString: N/A
# Filter tests that are not part of the Umbraco.Infrastructure namespace. So this will run all tests that are not part of the Umbraco.Infrastructure namespace
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure)"
LinuxPart1Of3:
vmImage: "ubuntu-latest"
Tests__Database__DatabaseType: SqlServer
Tests__Database__SQLServerMasterConnectionString: "Server=(local);User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
# Filter tests that are part of the Umbraco.Infrastructure namespace but not part of the Umbraco.Infrastructure.Service namespace
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure) & (FullyQualifiedName!~Umbraco.Infrastructure.Service)"
LinuxPart2Of3:
vmImage: "ubuntu-latest"
Tests__Database__DatabaseType: SqlServer
Tests__Database__SQLServerMasterConnectionString: "Server=(local);User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
# Filter tests that are part of the Umbraco.Infrastructure.Service namespace
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)"
LinuxPart3Of3:
vmImage: "ubuntu-latest"
Tests__Database__DatabaseType: SqlServer
Tests__Database__SQLServerMasterConnectionString: "Server=(local);User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
# Filter tests that are not part of the Umbraco.Infrastructure namespace. So this will run all tests that are not part of the Umbraco.Infrastructure namespace
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure)"
pool:
vmImage: $(vmImage)
steps:
# Setup test environment
- task: DownloadPipelineArtifact@2
displayName: Download build artifacts
inputs:
artifact: build_output
path: $(Build.SourcesDirectory)
- task: UseDotNet@2
displayName: Use .NET SDK from global.json
inputs:
useGlobalJson: true
# Start SQL Server
- powershell: docker run --name mssql -d -p 1433:1433 -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=$(SA_PASSWORD)" mcr.microsoft.com/mssql/server:2022-latest
displayName: Start SQL Server Docker image (Linux)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
- powershell: |
$maxAttempts = 12
$attempt = 0
$status = ""
while (($status -ne 'running') -and ($attempt -lt $maxAttempts)) {
Start-Sleep -Seconds 5
# We use the docker inspect command to check the status of the container. If the container is not running, we wait 5 seconds and try again. And if reaches 12 attempts, we fail the build.
$status = docker inspect -f '{{.State.Status}}' mssql
if ($status -ne 'running') {
Write-Host "Waiting for SQL Server to be ready... Attempt $($attempt + 1)"
$attempt++
}
}
if ($status -eq 'running') {
Write-Host "SQL Server container is running"
docker ps -a
} else {
Write-Host "SQL Server did not become ready in time. Last known status: $status"
docker logs mssql
exit 1
}
displayName: Wait for SQL Server to be ready (Linux)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
- pwsh: SqlLocalDB start MSSQLLocalDB
displayName: Start SQL Server LocalDB (Windows)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
# Test
- task: DotNetCoreCLI@2
displayName: Run dotnet test
inputs:
command: test
projects: "tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj"
testRunTitle: Integration Tests SQL Server - $(Agent.OS)
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build'
# Stop SQL Server
- pwsh: docker stop mssql
displayName: Stop SQL Server Docker image (Linux)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
- pwsh: SqlLocalDB stop MSSQLLocalDB
displayName: Stop SQL Server LocalDB (Windows)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
- stage: DefaultConfigE2E
displayName: Default Config E2E Tests
dependsOn: Build
variables:
npm_config_cache: $(Pipeline.Workspace)/.npm_e2e
@@ -128,369 +320,304 @@ stages:
# E2E Tests
- job:
displayName: E2E Tests (SQLite)
condition: eq(${{parameters.sqliteAcceptanceTests}}, True)
timeoutInMinutes: 180
condition: ${{ eq(parameters.skipDefaultConfigAcceptanceTests, false) }}
variables:
# Connection string
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=Umbraco;Mode=Memory;Cache=Shared;Foreign Keys=True;Pooling=True
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.Sqlite
DatabaseType: SQLite
additionalEnvironmentVariables: false
strategy:
matrix:
LinuxPart1Of3:
vmImage: "ubuntu-latest"
testFolder: "DefaultConfig"
testCommand: "npm run testSqlite -- --shard=1/3"
LinuxPart2Of3:
vmImage: "ubuntu-latest"
testFolder: "DefaultConfig"
testCommand: "npm run testSqlite -- --shard=2/3"
LinuxPart3Of3:
vmImage: "ubuntu-latest"
testFolder: "DefaultConfig"
testCommand: "npm run testSqlite -- --shard=3/3"
WindowsPart1Of3:
vmImage: "windows-latest"
testFolder: "DefaultConfig"
testCommand: "npm run testSqlite -- --shard=1/3"
WindowsPart2Of3:
vmImage: "windows-latest"
testFolder: "DefaultConfig"
testCommand: "npm run testSqlite -- --shard=2/3"
WindowsPart3Of3:
vmImage: "windows-latest"
testFolder: "DefaultConfig"
testCommand: "npm run testSqlite -- --shard=3/3"
pool:
vmImage: $(vmImage)
steps:
# Setup test environment
- task: DownloadPipelineArtifact@2
displayName: Download NuGet artifacts
inputs:
artifact: nupkg
path: $(Agent.BuildDirectory)/app/nupkg
- task: NodeTool@0
displayName: Use Node.js $(nodeVersion)
retryCountOnTaskFailure: 3
inputs:
versionSpec: $(nodeVersion)
- task: UseDotNet@2
displayName: Use .NET SDK from global.json
inputs:
useGlobalJson: true
# Setup test environment Template
- template: nightly-E2E-setup-template.yml
parameters:
nodeVersion: ${{ variables.nodeVersion }}
PlaywrightUserEmail: ${{ variables.UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL }}
PlaywrightPassword: ${{ variables.UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD }}
ASPNETCORE_URLS: ${{ variables.ASPNETCORE_URLS }}
npm_config_cache: ${{ variables.npm_config_cache }}
- pwsh: |
"UMBRACO_USER_LOGIN=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL)
UMBRACO_USER_PASSWORD=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
URL=$(ASPNETCORE_URLS)
STORAGE_STAGE_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth/user.json
CONSOLE_ERRORS_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/console-errors.json" | Out-File .env
displayName: Generate .env
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
# Cache and restore NPM packages
- task: Cache@2
displayName: Cache NPM packages
inputs:
key: 'npm_e2e | "$(Agent.OS)" | $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/package-lock.json'
restoreKeys: |
npm_e2e | "$(Agent.OS)"
npm_e2e
path: $(npm_config_cache)
- script: npm ci --no-fund --no-audit --prefer-offline
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
displayName: Restore NPM packages
# Build application
- pwsh: |
$cmsVersion = "$(Build.BuildNumber)" -replace "\+",".g"
dotnet new nugetconfig
dotnet nuget add source ./nupkg --name Local
dotnet new install Umbraco.Templates::$cmsVersion
dotnet new umbraco --name UmbracoProject --version $cmsVersion --exclude-gitignore --no-restore --no-update-check
dotnet restore UmbracoProject
cp $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest.UmbracoProject/*.cs UmbracoProject
dotnet build UmbracoProject --configuration $(buildConfiguration) --no-restore
displayName: Restore project
workingDirectory: $(Agent.BuildDirectory)/app
- pwsh: |
dotnet build UmbracoProject --configuration ${{ variables.buildConfiguration }} --no-restore
dotnet dev-certs https
displayName: Build application
workingDirectory: $(Agent.BuildDirectory)/app
condition: succeeded()
# Run application
- bash: |
nohup dotnet run --project UmbracoProject --configuration $(buildConfiguration) --no-build --no-launch-profile > $(Build.ArtifactStagingDirectory)/playwright.log 2>&1 &
echo "##vso[task.setvariable variable=AcceptanceTestProcessId]$!"
displayName: Run application (Linux)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
workingDirectory: $(Agent.BuildDirectory)/app
# Run application Template
- template: nightly-E2E-run-application-template.yml
parameters:
DatabaseType: ${{ variables.DatabaseType }}
buildConfiguration: ${{ variables.buildConfiguration }}
additionalEnvironmentVariables: ${{ variables.additionalEnvironmentVariables }}
- pwsh: |
$process = Start-Process dotnet "run --project UmbracoProject --configuration $(buildConfiguration) --no-build --no-launch-profile 2>&1" -PassThru -NoNewWindow -RedirectStandardOutput $(Build.ArtifactStagingDirectory)/playwright.log
Write-Host "##vso[task.setvariable variable=AcceptanceTestProcessId]$($process.Id)"
displayName: Run application (Windows)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
workingDirectory: $(Agent.BuildDirectory)/app
# Ensures we have the package wait-on installed
- pwsh: npm install wait-on
displayName: Install wait-on package
# Wait for application to start responding to requests
- pwsh: npx wait-on -v --interval 1000 --timeout 120000 $(ASPNETCORE_URLS)
displayName: Wait for application
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Install Playwright and dependencies
- pwsh: npx playwright install chromium
displayName: Install Playwright only with Chromium browser
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Test
- pwsh: $(testCommand)
displayName: Run Playwright tests
continueOnError: true
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
env:
CI: true
CommitId: $(Build.SourceVersion)
AgentOs: $(Agent.OS)
# Stop application
- bash: kill -15 $(AcceptanceTestProcessId)
displayName: Stop application (Linux)
condition: and(succeeded(), ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Linux'))
- pwsh: Stop-Process -Id $(AcceptanceTestProcessId)
displayName: Stop application (Windows)
condition: and(succeeded(), ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Windows_NT'))
# Copy artifacts
- pwsh: |
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/results/*) {
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results/* $(Build.ArtifactStagingDirectory) -Recurse
}
displayName: Copy Playwright results
condition: succeededOrFailed()
# Copy console error log
- pwsh: |
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/console-errors.json) {
Copy-Item tests/Umbraco.Tests.AcceptanceTest/console-errors.json $(Build.ArtifactStagingDirectory)
}
displayName: Copy console error log
condition: succeededOrFailed()
# Publish
- task: PublishPipelineArtifact@1
displayName: Publish test artifacts
condition: succeededOrFailed()
inputs:
targetPath: $(Build.ArtifactStagingDirectory)
artifact: "Acceptance Test Results - $(Agent.JobName) - Attempt #$(System.JobAttempt)"
# Publish test results
- task: PublishTestResults@2
displayName: "Publish test results"
condition: succeededOrFailed()
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '*.xml'
searchFolder: "tests/Umbraco.Tests.AcceptanceTest/results"
testRunTitle: "$(Agent.JobName)"
# Run tests Template
- template: nightly-E2E-run-tests-template.yml
parameters:
testCommand: $(testCommand)
ASPNETCORE_URLS: ${{ variables.ASPNETCORE_URLS }}
DatabaseType: ${{ variables.DatabaseType }}
- job:
displayName: E2E Tests (SQL Server)
timeoutInMinutes: 180
condition: ${{ eq(parameters.skipDefaultConfigAcceptanceTests, false) }}
variables:
# Connection string
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
DatabaseType: SQLServer
SA_PASSWORD: UmbracoAcceptance123!
additionalEnvironmentVariables: false
strategy:
matrix:
LinuxPart1Of3:
testCommand: "npm run test -- --shard=1/3"
testFolder: "DefaultConfig"
vmImage: "ubuntu-latest"
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
LinuxPart2Of3:
testCommand: "npm run test -- --shard=2/3"
testFolder: "DefaultConfig"
vmImage: "ubuntu-latest"
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
LinuxPart3Of3:
testCommand: "npm run test -- --shard=3/3"
testFolder: "DefaultConfig"
vmImage: "ubuntu-latest"
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
WindowsPart1Of3:
vmImage: "windows-latest"
testCommand: "npm run test -- --shard=1/3"
testFolder: "DefaultConfig"
vmImage: "windows-latest"
WindowsPart2Of3:
vmImage: "windows-latest"
testCommand: "npm run test -- --shard=2/3"
WindowsPart3Of3:
testFolder: "DefaultConfig"
vmImage: "windows-latest"
WindowsPart3Of3:
testCommand: "npm run test -- --shard=3/3"
testFolder: "DefaultConfig"
vmImage: "windows-latest"
pool:
vmImage: $(vmImage)
steps:
# Setup test environment
- task: DownloadPipelineArtifact@2
displayName: Download NuGet artifacts
inputs:
artifact: nupkg
path: $(Agent.BuildDirectory)/app/nupkg
- task: NodeTool@0
displayName: Use Node.js $(nodeVersion)
inputs:
versionSpec: $(nodeVersion)
- task: UseDotNet@2
displayName: Use .NET SDK from global.json
inputs:
useGlobalJson: true
# Setup test environment Template
- template: nightly-E2E-setup-template.yml
parameters:
nodeVersion: ${{ variables.nodeVersion }}
PlaywrightUserEmail: ${{ variables.UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL }}
PlaywrightPassword: ${{ variables.UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD }}
ASPNETCORE_URLS: ${{ variables.ASPNETCORE_URLS }}
npm_config_cache: ${{ variables.npm_config_cache }}
- pwsh: |
"UMBRACO_USER_LOGIN=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL)
UMBRACO_USER_PASSWORD=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
URL=$(ASPNETCORE_URLS)
STORAGE_STAGE_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth/user.json
CONSOLE_ERRORS_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/console-errors.json" | Out-File .env
displayName: Generate .env
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
# Cache and restore NPM packages
- task: Cache@2
displayName: Cache NPM packages
inputs:
key: 'npm_e2e | "$(Agent.OS)" | $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/package-lock.json'
restoreKeys: |
npm_e2e | "$(Agent.OS)"
npm_e2e
path: $(npm_config_cache)
- script: npm ci --no-fund --no-audit --prefer-offline
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
displayName: Restore NPM packages
# Build application
- pwsh: |
$cmsVersion = "$(Build.BuildNumber)" -replace "\+",".g"
dotnet new nugetconfig
dotnet nuget add source ./nupkg --name Local
dotnet new install Umbraco.Templates::$cmsVersion
dotnet new umbraco --name UmbracoProject --version $cmsVersion --exclude-gitignore --no-restore --no-update-check
dotnet restore UmbracoProject
cp $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest.UmbracoProject/*.cs UmbracoProject
dotnet build UmbracoProject --configuration $(buildConfiguration) --no-restore
displayName: Restore project
workingDirectory: $(Agent.BuildDirectory)/app
- pwsh: |
dotnet build UmbracoProject --configuration ${{ variables.buildConfiguration }} --no-restore
dotnet dev-certs https
displayName: Build application
workingDirectory: $(Agent.BuildDirectory)/app
condition: succeeded()
# Start SQL Server
- powershell: docker run --name mssql -d -p 1433:1433 -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=$(SA_PASSWORD)" mcr.microsoft.com/mssql/server:2022-latest
displayName: Start SQL Server Docker image (Linux)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
# Run application Template
- template: nightly-E2E-run-application-template.yml
parameters:
SA_PASSWORD: ${{ variables.SA_PASSWORD }}
buildConfiguration: ${{ variables.buildConfiguration }}
DatabaseType: ${{ variables.DatabaseType }}
additionalEnvironmentVariables: ${{ variables.additionalEnvironmentVariables }}
- pwsh: SqlLocalDB start MSSQLLocalDB
displayName: Start SQL Server LocalDB (Windows)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
# Run tests Template
- template: nightly-E2E-run-tests-template.yml
parameters:
testCommand: $(testCommand)
ASPNETCORE_URLS: ${{ variables.ASPNETCORE_URLS }}
DatabaseType: ${{ variables.DatabaseType }}
# Run application
- stage: AdditionalConfigE2E
displayName: Additional Config E2E Tests
dependsOn: Build
variables:
npm_config_cache: $(Pipeline.Workspace)/.npm_e2e
ASPNETCORE_URLS: https://localhost:44331
PlaywrightPassword: UmbracoAcceptance123!
PlaywrightUserEmail: playwright@umbraco.com
jobs:
- job:
displayName: E2E Tests with Different App settings (SQL Server)
condition: ${{ or(eq(parameters.differentAppSettingsAcceptanceTests, true), eq(parameters.skipDefaultConfigAcceptanceTests, true)) }}
timeoutInMinutes: 180
variables:
SA_PASSWORD: UmbracoAcceptance123!
DatabaseType: SQLServer
strategy:
matrix:
# UnattendedInstallConfig
WindowsUnattendedInstallConfig:
vmImage: "windows-latest"
testFolder: "UnattendedInstallConfig"
testCommand: "npx playwright test --project=unattendedInstallConfig --grep=InstallSQLServer"
port: 44331
additionalEnvironmentVariables: false
# DeliveryApiConfig
WindowsDeliveryApiConfig:
vmImage: "windows-latest"
testFolder: "DeliveryApi"
port: ''
testCommand: "npx playwright test --project=deliveryApi"
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
additionalEnvironmentVariables: false
LinuxDeliveryApiConfig:
vmImage: "ubuntu-latest"
testFolder: "DeliveryApi"
port: ''
testCommand: "npx playwright test --project=deliveryApi"
CONNECTIONSTRINGS__UMBRACODBDSN: Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
additionalEnvironmentVariables: false
# ExternalLogin AzureADB2C
WindowsExternalLoginAzureADB2C:
vmImage: "windows-latest"
testFolder: "ExternalLogin\\AzureADB2C"
testCommand: "npx playwright test --project=externalLoginAzureADB2C"
port: 44331
packageName: "Microsoft.AspNetCore.Authentication.OpenIdConnect"
packageVersion: "9.0.8"
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
additionalEnvironmentVariables: true
pool:
vmImage: $(vmImage)
steps:
# Setup test environment Template
- template: nightly-E2E-setup-template.yml
parameters:
nodeVersion: ${{ variables.nodeVersion }}
PlaywrightUserEmail: ${{ variables.PlaywrightUserEmail }}
PlaywrightPassword: ${{ variables.PlaywrightPassword }}
ASPNETCORE_URLS: ${{ variables.ASPNETCORE_URLS }}
npm_config_cache: ${{ variables.npm_config_cache }}
# Install NuGet package if specified in the matrix
- pwsh: |
Write-Host "Installing package $(packageName) version $(packageVersion)"
dotnet add package $(packageName) --version $(packageVersion)
displayName: "Install NuGet package: $(packageName)"
workingDirectory: $(Agent.BuildDirectory)/app/UmbracoProject
condition: and(succeeded(), ne(variables['packageName'], ''), ne(variables['packageVersion'], ''))
# Build application Template
- template: nightly-E2E-build-template.yml
parameters:
testFolder: $(testFolder)
buildConfiguration: ${{ variables.buildConfiguration }}
additionalEnvironmentVariables: $(additionalEnvironmentVariables)
# Build application for AzureADB2C
- pwsh: |
dotnet build UmbracoProject --configuration ${{ variables.buildConfiguration }} --no-restore
dotnet dev-certs https
displayName: Build application for AzureADB2C
workingDirectory: $(Agent.BuildDirectory)/app
env:
AZUREADB2CDOMAIN: $(AZUREB2CDOMAIN)
AZUREADB2CTENANT: $(AZUREB2CTENANT)
AZUREADB2CPOLICY: $(AZUREB2CPOLICY)
AZUREADB2CCLIENTID: $(AZUREB2CCLIENTID)
AZUREADB2CCLIENTSECRET: $(AZUREB2CCLIENTSECRET)
condition: and(succeeded(), eq(variables['testFolder'], 'ExternalLogin\AzureADB2C'))
# Run application Template
- template: nightly-E2E-run-application-template.yml
parameters:
SA_PASSWORD: ${{ variables.SA_PASSWORD }}
additionalEnvironmentVariables: $(additionalEnvironmentVariables)
buildConfiguration: ${{ variables.buildConfiguration }}
DatabaseType: ${{ variables.DatabaseType }}
# Run application for Linux with additional Environment Variables for Azure AD
- bash: |
nohup dotnet run --project UmbracoProject --configuration $(buildConfiguration) --no-build --no-launch-profile > $(Build.ArtifactStagingDirectory)/playwright.log 2>&1 &
nohup dotnet run --project UmbracoProject --configuration ${{ variables.buildConfiguration }} --no-build --no-launch-profile > $(Build.ArtifactStagingDirectory)/playwright.log 2>&1 &
echo "##vso[task.setvariable variable=AcceptanceTestProcessId]$!"
displayName: Run application (Linux)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'), eq(variables['testFolder'], 'ExternalLogin\AzureADB2C'))
workingDirectory: $(Agent.BuildDirectory)/app
env:
AZUREADB2CDOMAIN: $(AZUREB2CDOMAIN)
AZUREADB2CTENANT: $(AZUREB2CTENANT)
AZUREADB2CPOLICY: $(AZUREB2CPOLICY)
AZUREADB2CCLIENTID: $(AZUREB2CCLIENTID)
AZUREADB2CCLIENTSECRET: $(AZUREB2CCLIENTSECRET)
# Run application for Windows with additional Environment Variables for Azure AD
- pwsh: |
$process = Start-Process dotnet "run --project UmbracoProject --configuration $(buildConfiguration) --no-build --no-launch-profile 2>&1" -PassThru -NoNewWindow -RedirectStandardOutput $(Build.ArtifactStagingDirectory)/playwright.log
$process = Start-Process dotnet "run --project UmbracoProject --configuration ${{ variables.buildConfiguration }} --no-build --no-launch-profile 2>&1" -PassThru -NoNewWindow -RedirectStandardOutput $(Build.ArtifactStagingDirectory)/playwright.log
Write-Host "##vso[task.setvariable variable=AcceptanceTestProcessId]$($process.Id)"
displayName: Run application (Windows)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'), eq(variables['testFolder'], 'ExternalLogin\AzureADB2C'))
workingDirectory: $(Agent.BuildDirectory)/app
# Ensures we have the package wait-on installed
- pwsh: npm install wait-on
displayName: Install wait-on package
# Wait for application to start responding to requests
- pwsh: npx wait-on -v --interval 1000 --timeout 120000 $(ASPNETCORE_URLS)
displayName: Wait for application
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Install Playwright and dependencies
- pwsh: npx playwright install chromium
displayName: Install Playwright only with Chromium browser
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Test
- pwsh: $(testCommand)
displayName: Run Playwright tests
continueOnError: true
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
env:
CI: true
CommitId: $(Build.SourceVersion)
AgentOs: $(Agent.OS)
AZUREADB2CDOMAIN: $(AZUREB2CDOMAIN)
AZUREADB2CTENANT: $(AZUREB2CTENANT)
AZUREADB2CPOLICY: $(AZUREB2CPOLICY)
AZUREADB2CCLIENTID: $(AZUREB2CCLIENTID)
AZUREADB2CCLIENTSECRET: $(AZUREB2CCLIENTSECRET)
# Stop application
- bash: kill -15 $(AcceptanceTestProcessId)
displayName: Stop application (Linux)
condition: and(succeeded(), ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Linux'))
- pwsh: Stop-Process -Id $(AcceptanceTestProcessId)
displayName: Stop application (Windows)
condition: and(succeeded(), ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Windows_NT'))
# Stop SQL Server
- pwsh: docker stop mssql
displayName: Stop SQL Server Docker image (Linux)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
- pwsh: SqlLocalDB stop MSSQLLocalDB
displayName: Stop SQL Server LocalDB (Windows)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
# Copy artifacts
- pwsh: |
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/results/*) {
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results/* $(Build.ArtifactStagingDirectory) -Recurse
}
displayName: Copy Playwright results
condition: succeededOrFailed()
# Copy console error log
- pwsh: |
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/console-errors.json) {
Copy-Item tests/Umbraco.Tests.AcceptanceTest/console-errors.json $(Build.ArtifactStagingDirectory)
}
displayName: Copy console error log
condition: succeededOrFailed()
# Publish
- task: PublishPipelineArtifact@1
displayName: Publish test artifacts
condition: succeededOrFailed()
inputs:
targetPath: $(Build.ArtifactStagingDirectory)
artifact: "Acceptance Test Results - $(Agent.JobName) - Attempt #$(System.JobAttempt)"
# Publish test results
- task: PublishTestResults@2
displayName: "Publish test results"
condition: succeededOrFailed()
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '*.xml'
searchFolder: "tests/Umbraco.Tests.AcceptanceTest/results"
testRunTitle: "$(Agent.JobName)"
# Run tests Template
- template: nightly-E2E-run-tests-template.yml
parameters:
testCommand: $(testCommand)
ASPNETCORE_URLS: ${{ variables.ASPNETCORE_URLS }}
port: $(port)
AZUREB2CTESTUSEREMAIL: $(AZUREB2CTESTUSEREMAIL)
AZUREB2CTESTUSERPASSWORD: $(AZUREB2CTESTUSERPASSWORD)
DatabaseType: ${{ variables.DatabaseType }}
- stage: NotifySlackBot
displayName: Notify Slack on Failure
dependsOn: E2E
dependsOn: DefaultConfigE2E
# This stage will only run if the E2E tests fail or succeed with issues
condition: or(
eq(dependencies.E2E.result, 'failed'),
eq(dependencies.E2E.result, 'succeededWithIssues'))
condition: or(eq(dependencies.DefaultConfigE2E.result, 'failed'), eq(dependencies.DefaultConfigE2E.result, 'succeededWithIssues'))
jobs:
- job: PostToSlack
displayName: Send Slack Notification
@@ -0,0 +1,24 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.Document.RecycleBin;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.Document.RecycleBin;
[ApiVersion("1.0")]
public class SiblingsDocumentRecycleBinController : DocumentRecycleBinControllerBase
{
public SiblingsDocumentRecycleBinController(IEntityService entityService, IDocumentPresentationFactory documentPresentationFactory)
: base(entityService, documentPresentationFactory)
{
}
[HttpGet("siblings")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(SubsetViewModel<DocumentRecycleBinItemResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<SubsetViewModel<DocumentRecycleBinItemResponseModel>>> Siblings(CancellationToken cancellationToken, Guid target, int before, int after, Guid? dataTypeId = null)
=> await GetSiblings(target, before, after);
}
@@ -0,0 +1,24 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.Media.RecycleBin;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.Media.RecycleBin;
[ApiVersion("1.0")]
public class SiblingsMediaRecycleBinController : MediaRecycleBinControllerBase
{
public SiblingsMediaRecycleBinController(IEntityService entityService, IMediaPresentationFactory mediaPresentationFactory)
: base(entityService, mediaPresentationFactory)
{
}
[HttpGet("siblings")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(SubsetViewModel<MediaRecycleBinItemResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<SubsetViewModel<MediaRecycleBinItemResponseModel>>> Siblings(CancellationToken cancellationToken, Guid target, int before, int after, Guid? dataTypeId = null)
=> await GetSiblings(target, before, after);
}
@@ -0,0 +1,28 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Services.Flags;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.MemberType.Tree;
public class SiblingMemberTypeTreeController : MemberTypeTreeControllerBase
{
public SiblingMemberTypeTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
IMemberTypeService memberTypeService)
: base(entityService, flagProviders, memberTypeService)
{
}
[HttpGet("siblings")]
[ProducesResponseType(typeof(SubsetViewModel<MemberTypeTreeItemResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<SubsetViewModel<MemberTypeTreeItemResponseModel>>> Siblings(
CancellationToken cancellationToken,
Guid target,
int before,
int after)
=> await GetSiblings(target, before, after);
}
@@ -1,16 +1,34 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Services.FileSystem;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.PartialView.Tree;
[ApiVersion("1.0")]
public class AncestorsPartialViewTreeController : PartialViewTreeControllerBase
{
private readonly IPartialViewTreeService _partialViewTreeService;
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
public AncestorsPartialViewTreeController(IPartialViewTreeService partialViewTreeService)
: this(partialViewTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>())
=> _partialViewTreeService = partialViewTreeService;
[ActivatorUtilitiesConstructor]
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
public AncestorsPartialViewTreeController(IPartialViewTreeService partialViewTreeService, FileSystems fileSystems)
: base(partialViewTreeService, fileSystems) =>
_partialViewTreeService = partialViewTreeService;
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
public AncestorsPartialViewTreeController(FileSystems fileSystems)
: base(fileSystems)
: this(StaticServiceProvider.Instance.GetRequiredService<IPartialViewTreeService>(), fileSystems)
{
}
@@ -1,17 +1,34 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Services.FileSystem;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.DependencyInjection;
namespace Umbraco.Cms.Api.Management.Controllers.PartialView.Tree;
[ApiVersion("1.0")]
public class ChildrenPartialViewTreeController : PartialViewTreeControllerBase
{
private readonly IPartialViewTreeService _partialViewTreeService;
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
public ChildrenPartialViewTreeController(IPartialViewTreeService partialViewTreeService)
: this(partialViewTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>())
=> _partialViewTreeService = partialViewTreeService;
[ActivatorUtilitiesConstructor]
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
public ChildrenPartialViewTreeController(IPartialViewTreeService partialViewTreeService, FileSystems fileSystems)
: base(partialViewTreeService, fileSystems) =>
_partialViewTreeService = partialViewTreeService;
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
public ChildrenPartialViewTreeController(FileSystems fileSystems)
: base(fileSystems)
: this(StaticServiceProvider.Instance.GetRequiredService<IPartialViewTreeService>(), fileSystems)
{
}
@@ -1,8 +1,11 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Controllers.Tree;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Api.Management.Services.FileSystem;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Web.Common.Authorization;
@@ -13,9 +16,30 @@ namespace Umbraco.Cms.Api.Management.Controllers.PartialView.Tree;
[Authorize(Policy = AuthorizationPolicies.TreeAccessPartialViews)]
public class PartialViewTreeControllerBase : FileSystemTreeControllerBase
{
public PartialViewTreeControllerBase(FileSystems fileSystems)
=> FileSystem = fileSystems.PartialViewsFileSystem ??
throw new ArgumentException("Missing partial views file system", nameof(fileSystems));
private readonly IPartialViewTreeService _partialViewTreeService;
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
public PartialViewTreeControllerBase(IPartialViewTreeService partialViewTreeService)
: this(partialViewTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>()) =>
_partialViewTreeService = partialViewTreeService;
// FileSystem is required therefore, we can't remove it without some wizadry. When obsoletion is due, remove this.
[ActivatorUtilitiesConstructor]
[Obsolete("Scheduled for removal in Umbraco 19")]
public PartialViewTreeControllerBase(IPartialViewTreeService partialViewTreeService, FileSystems fileSystems)
: base(partialViewTreeService)
{
_partialViewTreeService = partialViewTreeService;
FileSystem = fileSystems.PartialViewsFileSystem ??
throw new ArgumentException("Missing scripts file system", nameof(fileSystems));
}
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
public PartialViewTreeControllerBase(FileSystems fileSystems)
: this(StaticServiceProvider.Instance.GetRequiredService<IPartialViewTreeService>())
=> FileSystem = fileSystems.PartialViewsFileSystem ??
throw new ArgumentException("Missing scripts file system", nameof(fileSystems));
[Obsolete("Included in the service class. Scheduled to be removed in Umbraco 19")]
protected override IFileSystem FileSystem { get; }
}
@@ -1,17 +1,34 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Services.FileSystem;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.DependencyInjection;
namespace Umbraco.Cms.Api.Management.Controllers.PartialView.Tree;
[ApiVersion("1.0")]
public class RootPartialViewTreeController : PartialViewTreeControllerBase
{
private readonly IPartialViewTreeService _partialViewTreeService;
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
public RootPartialViewTreeController(IPartialViewTreeService partialViewTreeService)
: this(partialViewTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>())
=> _partialViewTreeService = partialViewTreeService;
[ActivatorUtilitiesConstructor]
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
public RootPartialViewTreeController(IPartialViewTreeService partialViewTreeService, FileSystems fileSystems)
: base(partialViewTreeService, fileSystems) =>
_partialViewTreeService = partialViewTreeService;
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
public RootPartialViewTreeController(FileSystems fileSystems)
: base(fileSystems)
: this(StaticServiceProvider.Instance.GetRequiredService<IPartialViewTreeService>(), fileSystems)
{
}
@@ -0,0 +1,41 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Services.FileSystem;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.IO;
namespace Umbraco.Cms.Api.Management.Controllers.PartialView.Tree;
public class SiblingsPartialViewTreeController : PartialViewTreeControllerBase
{
private readonly IPartialViewTreeService _partialViewTreeService;
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
public SiblingsPartialViewTreeController(IPartialViewTreeService partialViewTreeService)
: this(partialViewTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>())
=> _partialViewTreeService = partialViewTreeService;
[ActivatorUtilitiesConstructor]
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
public SiblingsPartialViewTreeController(IPartialViewTreeService partialViewTreeService, FileSystems fileSystems)
: base(partialViewTreeService, fileSystems) =>
_partialViewTreeService = partialViewTreeService;
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
public SiblingsPartialViewTreeController(FileSystems fileSystems)
: this(StaticServiceProvider.Instance.GetRequiredService<IPartialViewTreeService>(), fileSystems)
{
}
[HttpGet("siblings")]
[ProducesResponseType(typeof(SubsetViewModel<FileSystemTreeItemPresentationModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<SubsetViewModel<FileSystemTreeItemPresentationModel>>> Siblings(
CancellationToken cancellationToken,
string path,
int before,
int after)
=> await GetSiblings(path, before, after);
}
@@ -4,6 +4,7 @@ using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Controllers.Content;
using Umbraco.Cms.Api.Management.ViewModels.Item;
using Umbraco.Cms.Api.Management.ViewModels.RecycleBin;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Services;
@@ -45,6 +46,24 @@ public abstract class RecycleBinControllerBase<TItem> : ContentControllerBase
return Task.FromResult<ActionResult<PagedViewModel<TItem>>>(Ok(result));
}
protected async Task<ActionResult<SubsetViewModel<TItem>>> GetSiblings(Guid target, int before, int after)
{
IEntitySlim[] siblings = GetSiblingEntities(target, before, after, out var totalBefore, out var totalAfter);
if (siblings.Length == 0)
{
return NotFound();
}
IEntitySlim entity = siblings.First();
Guid? parentKey = GetParentKey(entity);
TItem[] treeItemViewModels = MapRecycleBinViewModels(parentKey, siblings);
SubsetViewModel<TItem> result = SubsetViewModel(treeItemViewModels, totalBefore, totalAfter);
return Ok(result);
}
protected virtual TItem MapRecycleBinViewModel(Guid? parentKey, IEntitySlim entity)
{
if (entity == null)
@@ -136,4 +155,27 @@ public abstract class RecycleBinControllerBase<TItem> : ContentControllerBase
private PagedViewModel<TItem> PagedViewModel(IEnumerable<TItem> treeItemViewModels, long totalItems)
=> new() { Total = totalItems, Items = treeItemViewModels };
protected SubsetViewModel<TItem> SubsetViewModel(IEnumerable<TItem> treeItemViewModels, long totalBefore, long totalAfter)
=> new() { TotalBefore = totalBefore, TotalAfter = totalAfter, Items = treeItemViewModels };
protected virtual IEntitySlim[] GetSiblingEntities(Guid target, int before, int after, out long totalBefore, out long totalAfter) =>
_entityService
.GetTrashedSiblings(
target,
[ItemObjectType],
before,
after,
out totalBefore,
out totalAfter,
ordering: Ordering.By(nameof(Infrastructure.Persistence.Dtos.NodeDto.Text)))
.ToArray();
/// <summary>
/// Gets the parent key for an entity, or root if null or no parent.
/// </summary>
protected virtual Guid? GetParentKey(IEntitySlim entity) =>
entity.ParentId > 0
? _entityService.GetKey(entity.ParentId, ItemObjectType).Result
: Constants.System.RootKey;
}
@@ -1,7 +1,10 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Services.FileSystem;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.IO;
namespace Umbraco.Cms.Api.Management.Controllers.Script.Tree;
@@ -9,8 +12,22 @@ namespace Umbraco.Cms.Api.Management.Controllers.Script.Tree;
[ApiVersion("1.0")]
public class AncestorsScriptTreeController : ScriptTreeControllerBase
{
private readonly IScriptTreeService _scriptTreeService;
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
public AncestorsScriptTreeController(IScriptTreeService scriptTreeService)
: this(scriptTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>())
=> _scriptTreeService = scriptTreeService;
[ActivatorUtilitiesConstructor]
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
public AncestorsScriptTreeController(IScriptTreeService scriptTreeService, FileSystems fileSystems)
: base(scriptTreeService, fileSystems) =>
_scriptTreeService = scriptTreeService;
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
public AncestorsScriptTreeController(FileSystems fileSystems)
: base(fileSystems)
: this(StaticServiceProvider.Instance.GetRequiredService<IScriptTreeService>(), fileSystems)
{
}
@@ -1,17 +1,34 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Services.FileSystem;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.DependencyInjection;
namespace Umbraco.Cms.Api.Management.Controllers.Script.Tree;
[ApiVersion("1.0")]
public class ChildrenScriptTreeController : ScriptTreeControllerBase
{
private readonly IScriptTreeService _scriptTreeService;
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
public ChildrenScriptTreeController(IScriptTreeService scriptTreeService)
: this(scriptTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>())
=> _scriptTreeService = scriptTreeService;
[ActivatorUtilitiesConstructor]
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
public ChildrenScriptTreeController(IScriptTreeService scriptTreeService, FileSystems fileSystems)
: base(scriptTreeService, fileSystems) =>
_scriptTreeService = scriptTreeService;
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
public ChildrenScriptTreeController(FileSystems fileSystems)
: base(fileSystems)
: this(StaticServiceProvider.Instance.GetRequiredService<IScriptTreeService>(), fileSystems)
{
}
@@ -1,17 +1,34 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Services.FileSystem;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.DependencyInjection;
namespace Umbraco.Cms.Api.Management.Controllers.Script.Tree;
[ApiVersion("1.0")]
public class RootScriptTreeController : ScriptTreeControllerBase
{
private readonly IScriptTreeService _scriptTreeService;
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
public RootScriptTreeController(IScriptTreeService scriptTreeService)
: this(scriptTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>())
=> _scriptTreeService = scriptTreeService;
[ActivatorUtilitiesConstructor]
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
public RootScriptTreeController(IScriptTreeService scriptTreeService, FileSystems fileSystems)
: base(scriptTreeService, fileSystems) =>
_scriptTreeService = scriptTreeService;
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
public RootScriptTreeController(FileSystems fileSystems)
: base(fileSystems)
: this(StaticServiceProvider.Instance.GetRequiredService<IScriptTreeService>(), fileSystems)
{
}
@@ -1,8 +1,11 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Controllers.Tree;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Api.Management.Services.FileSystem;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Web.Common.Authorization;
@@ -13,9 +16,30 @@ namespace Umbraco.Cms.Api.Management.Controllers.Script.Tree;
[Authorize(Policy = AuthorizationPolicies.TreeAccessScripts)]
public class ScriptTreeControllerBase : FileSystemTreeControllerBase
{
private readonly IScriptTreeService _scriptTreeService;
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
public ScriptTreeControllerBase(IScriptTreeService scriptTreeService)
: this(scriptTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>()) =>
_scriptTreeService = scriptTreeService;
// FileSystem is required therefore, we can't remove it without some wizadry. When obsoletion is due, remove this.
[ActivatorUtilitiesConstructor]
[Obsolete("Scheduled for removal in Umbraco 19")]
public ScriptTreeControllerBase(IScriptTreeService scriptTreeService, FileSystems fileSystems)
: base(scriptTreeService)
{
_scriptTreeService = scriptTreeService;
FileSystem = fileSystems.ScriptsFileSystem ??
throw new ArgumentException("Missing scripts file system", nameof(fileSystems));
}
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
public ScriptTreeControllerBase(FileSystems fileSystems)
: this(StaticServiceProvider.Instance.GetRequiredService<IScriptTreeService>())
=> FileSystem = fileSystems.ScriptsFileSystem ??
throw new ArgumentException("Missing scripts file system", nameof(fileSystems));
[Obsolete("Included in the service class. Scheduled to be removed in Umbraco 19")]
protected override IFileSystem FileSystem { get; }
}
@@ -0,0 +1,41 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Services.FileSystem;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.IO;
namespace Umbraco.Cms.Api.Management.Controllers.Script.Tree;
public class SiblingsScriptTreeController : ScriptTreeControllerBase
{
private readonly IScriptTreeService _scriptTreeService;
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
public SiblingsScriptTreeController(IScriptTreeService scriptTreeService)
: this(scriptTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>())
=> _scriptTreeService = scriptTreeService;
[ActivatorUtilitiesConstructor]
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
public SiblingsScriptTreeController(IScriptTreeService scriptTreeService, FileSystems fileSystems)
: base(scriptTreeService, fileSystems) =>
_scriptTreeService = scriptTreeService;
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
public SiblingsScriptTreeController(FileSystems fileSystems)
: this(StaticServiceProvider.Instance.GetRequiredService<IScriptTreeService>(), fileSystems)
{
}
[HttpGet("siblings")]
[ProducesResponseType(typeof(SubsetViewModel<FileSystemTreeItemPresentationModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<SubsetViewModel<FileSystemTreeItemPresentationModel>>> Siblings(
CancellationToken cancellationToken,
string path,
int before,
int after)
=> await GetSiblings(path, before, after);
}
@@ -1,8 +1,11 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Controllers.Tree;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Api.Management.Services.FileSystem;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.IO;
namespace Umbraco.Cms.Api.Management.Controllers.StaticFile.Tree;
@@ -11,28 +14,39 @@ namespace Umbraco.Cms.Api.Management.Controllers.StaticFile.Tree;
[ApiExplorerSettings(GroupName = "Static File")]
public class StaticFileTreeControllerBase : FileSystemTreeControllerBase
{
private readonly IFileSystemTreeService _fileSystemTreeService;
private static readonly string[] _allowedRootFolders = { $"{Path.DirectorySeparatorChar}App_Plugins", $"{Path.DirectorySeparatorChar}wwwroot" };
public StaticFileTreeControllerBase(IPhysicalFileSystem physicalFileSystem, IFileSystemTreeService fileSystemTreeService)
: base (fileSystemTreeService)
{
FileSystem = physicalFileSystem;
_fileSystemTreeService = fileSystemTreeService;
}
[Obsolete("Please use the other constructor. Scheduled for removal in Umbraco 19")]
public StaticFileTreeControllerBase(IPhysicalFileSystem physicalFileSystem)
=> FileSystem = physicalFileSystem;
: this(physicalFileSystem, StaticServiceProvider.Instance.GetRequiredService<IFileSystemTreeService>())
{
}
protected override IFileSystem FileSystem { get; }
protected override string[] GetDirectories(string path) =>
protected string[] GetDirectories(string path) =>
IsTreeRootPath(path)
? _allowedRootFolders
: IsAllowedPath(path)
? base.GetDirectories(path)
? _fileSystemTreeService.GetDirectories(path)
: Array.Empty<string>();
protected override string[] GetFiles(string path)
protected string[] GetFiles(string path)
=> IsTreeRootPath(path) || IsAllowedPath(path) == false
? Array.Empty<string>()
: base.GetFiles(path);
: _fileSystemTreeService.GetFiles(path);
protected override FileSystemTreeItemPresentationModel[] GetAncestorModels(string path, bool includeSelf)
protected FileSystemTreeItemPresentationModel[] GetAncestorModels(string path, bool includeSelf)
=> IsAllowedPath(path)
? base.GetAncestorModels(path, includeSelf)
? _fileSystemTreeService.GetAncestorModels(path, includeSelf)
: Array.Empty<FileSystemTreeItemPresentationModel>();
private bool IsTreeRootPath(string path) => path == Path.DirectorySeparatorChar.ToString();
@@ -1,7 +1,10 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Services.FileSystem;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.IO;
namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Tree;
@@ -9,8 +12,22 @@ namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Tree;
[ApiVersion("1.0")]
public class AncestorsStylesheetTreeController : StylesheetTreeControllerBase
{
private readonly IStyleSheetTreeService _styleSheetTreeService;
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
public AncestorsStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService)
: this(styleSheetTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>())
=> _styleSheetTreeService = styleSheetTreeService;
[ActivatorUtilitiesConstructor]
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
public AncestorsStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService, FileSystems fileSystems)
: base(styleSheetTreeService, fileSystems) =>
_styleSheetTreeService = styleSheetTreeService;
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
public AncestorsStylesheetTreeController(FileSystems fileSystems)
: base(fileSystems)
: this(StaticServiceProvider.Instance.GetRequiredService<IStyleSheetTreeService>(), fileSystems)
{
}
@@ -1,20 +1,36 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Services.FileSystem;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.DependencyInjection;
namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Tree;
[ApiVersion("1.0")]
public class ChildrenStylesheetTreeController : StylesheetTreeControllerBase
{
private readonly IStyleSheetTreeService _styleSheetTreeService;
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
public ChildrenStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService)
: this(styleSheetTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>())
=> _styleSheetTreeService = styleSheetTreeService;
[ActivatorUtilitiesConstructor]
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
public ChildrenStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService, FileSystems fileSystems)
: base(styleSheetTreeService, fileSystems) =>
_styleSheetTreeService = styleSheetTreeService;
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
public ChildrenStylesheetTreeController(FileSystems fileSystems)
: base(fileSystems)
: this(StaticServiceProvider.Instance.GetRequiredService<IStyleSheetTreeService>(), fileSystems)
{
}
[HttpGet("children")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<FileSystemTreeItemPresentationModel>), StatusCodes.Status200OK)]
@@ -1,17 +1,34 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Services.FileSystem;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.DependencyInjection;
namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Tree;
[ApiVersion("1.0")]
public class RootStylesheetTreeController : StylesheetTreeControllerBase
{
private readonly IStyleSheetTreeService _styleSheetTreeService;
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
public RootStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService)
: this(styleSheetTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>())
=> _styleSheetTreeService = styleSheetTreeService;
[ActivatorUtilitiesConstructor]
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
public RootStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService, FileSystems fileSystems)
: base(styleSheetTreeService, fileSystems) =>
_styleSheetTreeService = styleSheetTreeService;
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
public RootStylesheetTreeController(FileSystems fileSystems)
: base(fileSystems)
: this(StaticServiceProvider.Instance.GetRequiredService<IStyleSheetTreeService>(), fileSystems)
{
}
@@ -0,0 +1,41 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Services.FileSystem;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.IO;
namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Tree;
public class SiblingsStylesheetTreeController : StylesheetTreeControllerBase
{
private readonly IStyleSheetTreeService _styleSheetTreeService;
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
public SiblingsStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService)
: this(styleSheetTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>())
=> _styleSheetTreeService = styleSheetTreeService;
[ActivatorUtilitiesConstructor]
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
public SiblingsStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService, FileSystems fileSystems)
: base(styleSheetTreeService, fileSystems) =>
_styleSheetTreeService = styleSheetTreeService;
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
public SiblingsStylesheetTreeController(FileSystems fileSystems)
: this(StaticServiceProvider.Instance.GetRequiredService<IStyleSheetTreeService>(), fileSystems)
{
}
[HttpGet("siblings")]
[ProducesResponseType(typeof(SubsetViewModel<FileSystemTreeItemPresentationModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<SubsetViewModel<FileSystemTreeItemPresentationModel>>> Siblings(
CancellationToken cancellationToken,
string path,
int before,
int after)
=> await GetSiblings(path, before, after);
}
@@ -1,8 +1,11 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Controllers.Tree;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Api.Management.Services.FileSystem;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Web.Common.Authorization;
@@ -13,9 +16,30 @@ namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Tree;
[Authorize(Policy = AuthorizationPolicies.TreeAccessStylesheets)]
public class StylesheetTreeControllerBase : FileSystemTreeControllerBase
{
public StylesheetTreeControllerBase(FileSystems fileSystems)
=> FileSystem = fileSystems.StylesheetsFileSystem ??
throw new ArgumentException("Missing stylesheets file system", nameof(fileSystems));
private readonly IStyleSheetTreeService _styleSheetTreeService;
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
public StylesheetTreeControllerBase(IStyleSheetTreeService styleSheetTreeService)
: this(styleSheetTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>()) =>
_styleSheetTreeService = styleSheetTreeService;
// FileSystem is required therefore, we can't remove it without some wizadry. When obsoletion is due, remove this.
[ActivatorUtilitiesConstructor]
[Obsolete("Scheduled for removal in Umbraco 19")]
public StylesheetTreeControllerBase(IStyleSheetTreeService styleSheetTreeService, FileSystems fileSystems)
: base(styleSheetTreeService)
{
_styleSheetTreeService = styleSheetTreeService;
FileSystem = fileSystems.ScriptsFileSystem ??
throw new ArgumentException("Missing scripts file system", nameof(fileSystems));
}
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
public StylesheetTreeControllerBase(FileSystems fileSystems)
: this(StaticServiceProvider.Instance.GetRequiredService<IStyleSheetTreeService>())
=> FileSystem = fileSystems.ScriptsFileSystem ??
throw new ArgumentException("Missing scripts file system", nameof(fileSystems));
[Obsolete("Included in the service class. Scheduled to be removed in Umbraco 19")]
protected override IFileSystem FileSystem { get; }
}
@@ -1,8 +1,11 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Extensions;
using Umbraco.Cms.Api.Management.Services.FileSystem;
using Umbraco.Cms.Api.Management.ViewModels.FileSystem;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.IO;
using Umbraco.Extensions;
@@ -10,11 +13,24 @@ namespace Umbraco.Cms.Api.Management.Controllers.Tree;
public abstract class FileSystemTreeControllerBase : ManagementApiControllerBase
{
private readonly IFileSystemTreeService _fileSystemTreeService;
[Obsolete("Has been moved to the individual services. Scheduled to be removed in Umbraco 19")]
protected abstract IFileSystem FileSystem { get; }
[ActivatorUtilitiesConstructor]
protected FileSystemTreeControllerBase(IFileSystemTreeService fileSystemTreeService) => _fileSystemTreeService = fileSystemTreeService;
[Obsolete("Use the other constructor. Scheduled for removal in Umbraco 19")]
protected FileSystemTreeControllerBase()
: this(StaticServiceProvider.Instance.GetRequiredService<IScriptTreeService>())
{
}
protected Task<ActionResult<PagedViewModel<FileSystemTreeItemPresentationModel>>> GetRoot(int skip, int take)
{
FileSystemTreeItemPresentationModel[] viewModels = GetPathViewModels(string.Empty, skip, take, out var totalItems);
FileSystemTreeItemPresentationModel[] viewModels = _fileSystemTreeService.GetPathViewModels(string.Empty, skip, take, out var totalItems);
PagedViewModel<FileSystemTreeItemPresentationModel> result = PagedViewModel(viewModels, totalItems);
return Task.FromResult<ActionResult<PagedViewModel<FileSystemTreeItemPresentationModel>>>(Ok(result));
@@ -22,20 +38,39 @@ public abstract class FileSystemTreeControllerBase : ManagementApiControllerBase
protected Task<ActionResult<PagedViewModel<FileSystemTreeItemPresentationModel>>> GetChildren(string path, int skip, int take)
{
FileSystemTreeItemPresentationModel[] viewModels = GetPathViewModels(path, skip, take, out var totalItems);
FileSystemTreeItemPresentationModel[] viewModels = _fileSystemTreeService.GetPathViewModels(path, skip, take, out var totalItems);
PagedViewModel<FileSystemTreeItemPresentationModel> result = PagedViewModel(viewModels, totalItems);
return Task.FromResult<ActionResult<PagedViewModel<FileSystemTreeItemPresentationModel>>>(Ok(result));
}
/// <summary>
/// Gets the sibling of the targeted item based on its path.
/// </summary>
/// <param name="path">The path to the item.</param>
/// <param name="before">The amount of siblings you want to fetch from before the items position in the array.</param>
/// <param name="after">The amount of siblings you want to fetch after the items position in the array.</param>
/// <returns>A SubsetViewModel of the siblings of the item and the item itself.</returns>
protected Task<ActionResult<SubsetViewModel<FileSystemTreeItemPresentationModel>>> GetSiblings(string path, int before, int after)
{
FileSystemTreeItemPresentationModel[] viewModels = _fileSystemTreeService.GetSiblingsViewModels(path, before, after, out var totalBefore, out var totalAfter);
SubsetViewModel<FileSystemTreeItemPresentationModel> result = new() { TotalBefore = totalBefore, TotalAfter = totalAfter, Items = viewModels };
return Task.FromResult<ActionResult<SubsetViewModel<FileSystemTreeItemPresentationModel>>>(Ok(result));
}
protected virtual Task<ActionResult<IEnumerable<FileSystemTreeItemPresentationModel>>> GetAncestors(string path, bool includeSelf = true)
{
path = path.VirtualPathToSystemPath();
FileSystemTreeItemPresentationModel[] models = GetAncestorModels(path, includeSelf);
FileSystemTreeItemPresentationModel[] models = _fileSystemTreeService.GetAncestorModels(path, includeSelf);
return Task.FromResult<ActionResult<IEnumerable<FileSystemTreeItemPresentationModel>>>(Ok(models));
}
private PagedViewModel<FileSystemTreeItemPresentationModel> PagedViewModel(IEnumerable<FileSystemTreeItemPresentationModel> viewModels, long totalItems)
=> new() { Total = totalItems, Items = viewModels };
[Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 19")]
protected virtual FileSystemTreeItemPresentationModel[] GetAncestorModels(string path, bool includeSelf)
{
var directories = path.Split(Path.DirectorySeparatorChar).Take(Range.EndAt(Index.FromEnd(1))).ToArray();
@@ -52,49 +87,28 @@ public abstract class FileSystemTreeControllerBase : ManagementApiControllerBase
return result.ToArray();
}
[Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 19")]
protected virtual string[] GetDirectories(string path) => FileSystem
.GetDirectories(path)
.OrderBy(directory => directory)
.ToArray();
[Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 19")]
protected virtual string[] GetFiles(string path) => FileSystem
.GetFiles(path)
.OrderBy(file => file)
.ToArray();
[Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 19")]
protected virtual bool DirectoryHasChildren(string path)
=> FileSystem.GetFiles(path).Any() || FileSystem.GetDirectories(path).Any();
private FileSystemTreeItemPresentationModel[] GetPathViewModels(string path, int skip, int take, out long totalItems)
{
path = path.VirtualPathToSystemPath();
var allItems = GetDirectories(path)
.Select(directory => new { Path = directory, IsFolder = true })
.Union(GetFiles(path).Select(file => new { Path = file, IsFolder = false }))
.ToArray();
totalItems = allItems.Length;
FileSystemTreeItemPresentationModel ViewModel(string itemPath, bool isFolder)
=> MapViewModel(
itemPath,
GetFileSystemItemName(isFolder, itemPath),
isFolder);
return allItems
.Skip(skip)
.Take(take)
.Select(item => ViewModel(item.Path, item.IsFolder))
.ToArray();
}
[Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 19")]
private string GetFileSystemItemName(bool isFolder, string itemPath) => isFolder
? Path.GetFileName(itemPath)
: FileSystem.GetFileName(itemPath);
private PagedViewModel<FileSystemTreeItemPresentationModel> PagedViewModel(IEnumerable<FileSystemTreeItemPresentationModel> viewModels, long totalItems)
=> new() { Total = totalItems, Items = viewModels };
[Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 19")]
private FileSystemTreeItemPresentationModel MapViewModel(string path, string name, bool isFolder)
{
var parentPath = Path.GetDirectoryName(path);
@@ -0,0 +1,52 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.UserData;
[ApiVersion("1.0")]
public class DeleteUserDataController : UserDataControllerBase
{
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IUserDataService _userDataService;
public DeleteUserDataController(
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IUserDataService userDataService)
{
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_userDataService = userDataService;
}
[HttpDelete("{id:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(UserDataOperationStatus), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(UserDataOperationStatus), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Delete(CancellationToken cancellationToken, Guid id)
{
IUserData? data = await _userDataService.GetAsync(id);
if (data is null)
{
return NotFound();
}
Guid currentUserKey = CurrentUserKey(_backOfficeSecurityAccessor);
if (data.UserKey != currentUserKey)
{
return Unauthorized();
}
Attempt<UserDataOperationStatus> attempt = await _userDataService.DeleteAsync(id);
return attempt.Success
? Ok()
: UserDataOperationStatusResult(attempt.Result);
}
}
@@ -1,6 +1,8 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Services.Entities;
using Umbraco.Cms.Api.Management.Services.FileSystem;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.DependencyInjection;
@@ -9,6 +11,9 @@ internal static class TreeBuilderExtensions
internal static IUmbracoBuilder AddTrees(this IUmbracoBuilder builder)
{
builder.Services.AddTransient<IUserStartNodeEntitiesService, UserStartNodeEntitiesService>();
builder.Services.AddUnique<IPartialViewTreeService, PartialViewTreeService>();
builder.Services.AddUnique<IScriptTreeService, ScriptTreeService>();
builder.Services.AddUnique<IStyleSheetTreeService, StyleSheetTreeService>();
return builder;
}
@@ -0,0 +1,102 @@
using Umbraco.Cms.Api.Management.Extensions;
using Umbraco.Cms.Api.Management.ViewModels.FileSystem;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.IO;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Services.FileSystem;
public abstract class FileSystemTreeServiceBase : IFileSystemTreeService
{
protected abstract IFileSystem FileSystem { get; }
public FileSystemTreeItemPresentationModel[] GetAncestorModels(string path, bool includeSelf)
{
var directories = path.Split(Path.DirectorySeparatorChar).Take(Range.EndAt(Index.FromEnd(1))).ToArray();
var result = directories
.Select((directory, index) => MapViewModel(string.Join(Path.DirectorySeparatorChar, directories.Take(index + 1)), directory, true))
.ToList();
if (includeSelf)
{
var selfIsFolder = FileSystem.FileExists(path) is false;
result.Add(MapViewModel(path, GetFileSystemItemName(selfIsFolder, path), selfIsFolder));
}
return result.ToArray();
}
public FileSystemTreeItemPresentationModel[] GetPathViewModels(string path, int skip, int take, out long totalItems)
{
path = path.VirtualPathToSystemPath();
var allItems = GetDirectories(path)
.Select(directory => new { Path = directory, IsFolder = true })
.Union(GetFiles(path).Select(file => new { Path = file, IsFolder = false }))
.ToArray();
totalItems = allItems.Length;
FileSystemTreeItemPresentationModel ViewModel(string itemPath, bool isFolder)
=> MapViewModel(
itemPath,
GetFileSystemItemName(isFolder, itemPath),
isFolder);
return allItems
.Skip(skip)
.Take(take)
.Select(item => ViewModel(item.Path, item.IsFolder))
.ToArray();
}
public FileSystemTreeItemPresentationModel[] GetSiblingsViewModels(string path, int before, int after, out long totalBefore, out long totalAfter)
{
var filePath = Path.GetDirectoryName(path);
var fileName = Path.GetFileName(path);
FileSystemTreeItemPresentationModel[] viewModels = GetPathViewModels(filePath!, 0, int.MaxValue, out totalBefore);
FileSystemTreeItemPresentationModel? target = viewModels.FirstOrDefault(item => item.Name == fileName);
var position = Array.IndexOf(viewModels, target);
totalBefore = position - before < 0 ? 0 : position - before;
totalAfter = (viewModels.Length - 1) - (position + after) < 0 ? 0 : (viewModels.Length - 1) - (position + after);
return viewModels
.Select((item, index) => new { item, index })
.Where(item => item.index >= position - before && item.index <= position + after)
.Select(item => item.item)
.ToArray();
}
public string[] GetDirectories(string path) => FileSystem
.GetDirectories(path)
.OrderBy(directory => directory)
.ToArray();
public string[] GetFiles(string path) => FileSystem
.GetFiles(path)
.OrderBy(file => file)
.ToArray();
public bool DirectoryHasChildren(string path)
=> FileSystem.GetFiles(path).Any() || FileSystem.GetDirectories(path).Any();
public string GetFileSystemItemName(bool isFolder, string itemPath) => isFolder
? Path.GetFileName(itemPath)
: FileSystem.GetFileName(itemPath);
private FileSystemTreeItemPresentationModel MapViewModel(string path, string name, bool isFolder)
{
var parentPath = Path.GetDirectoryName(path);
return new FileSystemTreeItemPresentationModel
{
Path = path.SystemPathToVirtualPath(),
Name = name,
HasChildren = isFolder && DirectoryHasChildren(path),
IsFolder = isFolder,
Parent = parentPath.IsNullOrWhiteSpace()
? null
: new FileSystemFolderModel { Path = parentPath.SystemPathToVirtualPath() }
};
}
}
@@ -0,0 +1,18 @@
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.IO;
namespace Umbraco.Cms.Api.Management.Services.FileSystem;
public interface IFileSystemTreeService
{
FileSystemTreeItemPresentationModel[] GetAncestorModels(string path, bool includeSelf);
FileSystemTreeItemPresentationModel[] GetPathViewModels(string path, int skip, int take, out long totalItems);
FileSystemTreeItemPresentationModel[] GetSiblingsViewModels(string path, int before, int after, out long totalBefore,
out long totalAfter);
string[] GetDirectories(string path);
string[] GetFiles(string path);
}
@@ -0,0 +1,5 @@
namespace Umbraco.Cms.Api.Management.Services.FileSystem;
public interface IPartialViewTreeService : IFileSystemTreeService
{
}
@@ -0,0 +1,5 @@
namespace Umbraco.Cms.Api.Management.Services.FileSystem;
public interface IScriptTreeService : IFileSystemTreeService
{
}
@@ -0,0 +1,7 @@
using Umbraco.Cms.Core.IO;
namespace Umbraco.Cms.Api.Management.Services.FileSystem;
public interface IStyleSheetTreeService : IFileSystemTreeService
{
}
@@ -0,0 +1,14 @@
using Umbraco.Cms.Core.IO;
namespace Umbraco.Cms.Api.Management.Services.FileSystem;
public class PartialViewTreeService : FileSystemTreeServiceBase, IPartialViewTreeService
{
private readonly IFileSystem _partialViewFileSystem;
protected override IFileSystem FileSystem => _partialViewFileSystem;
public PartialViewTreeService(FileSystems fileSystems) =>
_partialViewFileSystem = fileSystems.PartialViewsFileSystem ??
throw new ArgumentException("Missing partial views file system", nameof(fileSystems));
}
@@ -0,0 +1,14 @@
using Umbraco.Cms.Core.IO;
namespace Umbraco.Cms.Api.Management.Services.FileSystem;
public class ScriptTreeService : FileSystemTreeServiceBase, IScriptTreeService
{
private readonly IFileSystem _scriptFileSystem;
protected override IFileSystem FileSystem => _scriptFileSystem;
public ScriptTreeService(FileSystems fileSystems) =>
_scriptFileSystem = fileSystems.ScriptsFileSystem ??
throw new ArgumentException("Missing partial views file system", nameof(fileSystems));
}
@@ -0,0 +1,14 @@
using Umbraco.Cms.Core.IO;
namespace Umbraco.Cms.Api.Management.Services.FileSystem;
public class StyleSheetTreeService : FileSystemTreeServiceBase, IStyleSheetTreeService
{
private readonly IFileSystem _scriptFileSystem;
protected override IFileSystem FileSystem => _scriptFileSystem;
public StyleSheetTreeService(FileSystems fileSystems) =>
_scriptFileSystem = fileSystems.StylesheetsFileSystem ??
throw new ArgumentException("Missing partial views file system", nameof(fileSystems));
}
@@ -100,7 +100,7 @@ public interface IPublishedContent : IPublishedElement
/// Gets the parent of the content item.
/// </summary>
/// <remarks>The parent of root content is <c>null</c>.</remarks>
[Obsolete("Please use either the IPublishedContent.Parent<>() extension method in the Umbraco.Extensions namespace, or IDocumentNavigationQueryService if you only need keys. Scheduled for removal in V16.")]
[Obsolete("Please use either the IPublishedContent.Parent<>() extension method in the Umbraco.Extensions namespace, or IDocumentNavigationQueryService if you only need keys. Scheduled for removal in Umbraco 18.")]
IPublishedContent? Parent { get; }
/// <summary>
@@ -142,6 +142,6 @@ public interface IPublishedContent : IPublishedElement
/// <summary>
/// Gets the children of the content item that are available for the current culture.
/// </summary>
[Obsolete("Please use either the IPublishedContent.Children() extension method in the Umbraco.Extensions namespace, or IDocumentNavigationQueryService if you only need keys. Scheduled for removal in V16.")]
[Obsolete("Please use either the IPublishedContent.Children() extension method in the Umbraco.Extensions namespace, or IDocumentNavigationQueryService if you only need keys. Scheduled for removal in Umbraco 18.")]
IEnumerable<IPublishedContent> Children { get; }
}
@@ -46,6 +46,33 @@ public interface IEntityRepository : IRepository
return [];
}
/// <summary>
/// Gets trashed sibling entities of a specified target entity, within a given range before and after the target, ordered as specified.
/// </summary>
/// <param name="objectTypes">The object type keys of the entities.</param>
/// <param name="targetKey">The key of the target entity whose siblings are to be retrieved.</param>
/// <param name="before">The number of siblings to retrieve before the target entity.</param>
/// <param name="after">The number of siblings to retrieve after the target entity.</param>
/// <param name="filter">An optional filter to apply to the result set.</param>
/// <param name="ordering">The ordering to apply to the siblings.</param>
/// <param name="totalBefore">Outputs the total number of siblings before the target entity.</param>
/// <param name="totalAfter">Outputs the total number of siblings after the target entity.</param>
/// <returns>Enumerable of trashed sibling entities.</returns>
IEnumerable<IEntitySlim> GetTrashedSiblings(
ISet<Guid> objectTypes,
Guid targetKey,
int before,
int after,
IQuery<IUmbracoEntity>? filter,
Ordering ordering,
out long totalBefore,
out long totalAfter)
{
totalBefore = 0;
totalAfter = 0;
return [];
}
/// <summary>
/// Gets entities for a query
/// </summary>
@@ -64,11 +64,18 @@ public class DecimalPropertyEditor : DataEditor
=> TryParsePropertyValue(editorValue.Value);
private static decimal? TryParsePropertyValue(object? value)
=> value is decimal decimalValue
? decimalValue
: decimal.TryParse(value?.ToString(), CultureInfo.InvariantCulture, out var parsedDecimalValue)
? parsedDecimalValue
: null;
=> value switch
{
decimal d => d,
double db => (decimal)db,
float f => (decimal)f,
IFormattable f => decimal.TryParse(f.ToString(null, CultureInfo.InvariantCulture), NumberStyles.Any, CultureInfo.InvariantCulture, out var parsedDecimalValue)
? parsedDecimalValue
: null,
_ => decimal.TryParse(value?.ToString(), CultureInfo.CurrentCulture, out var parsedDecimalValue)
? parsedDecimalValue
: null,
};
/// <summary>
/// Base validator for the decimal property editor validation against data type configured values.
@@ -40,7 +40,7 @@ public class TextStringValueConverter : PropertyValueConverterBase, IDeliveryApi
var sourceString = source.ToString();
// ensures string is parsed for {localLink} and URLs are resolved correctly
sourceString = _linkParser.EnsureInternalLinks(sourceString!, preview);
sourceString = _linkParser.EnsureInternalLinks(sourceString!);
sourceString = _urlParser.EnsureUrls(sourceString);
return sourceString;
@@ -39,7 +39,7 @@ public class MultipleValueValidator : IValueValidator
}
var invalidValues = values
.Where(x => valueListConfiguration.Items.Contains(x) is false)
.Where(x => x.IsNullOrWhiteSpace() is false && valueListConfiguration.Items.Contains(x) is false)
.ToList();
if (invalidValues.Count == 1)
@@ -526,8 +526,12 @@ internal abstract class ContentEditingServiceBase<TContent, TContentType, TConte
// this should already have been validated by now, so it's OK to throw exceptions here
if (_propertyEditorCollection.TryGet(propertyType.PropertyEditorAlias, out IDataEditor? dataEditor) == false)
{
_logger.LogWarning("Unable to retrieve property value - no data editor found for property editor: {PropertyEditorAlias}", propertyType.PropertyEditorAlias);
return null;
_logger.LogWarning(
"Unable to find property editor {PropertyEditorAlias}, for property {PropertyAlias}. Leaving property value unchanged.",
propertyType.PropertyEditorAlias,
propertyType.Alias);
return content.GetValue(propertyType.Alias, culture, segment);
}
IDataValueEditor dataValueEditor = dataEditor.GetValueEditor();
@@ -359,6 +359,47 @@ public class EntityService : RepositoryService, IEntityService
return siblings;
}
/// <inheritdoc />
public IEnumerable<IEntitySlim> GetTrashedSiblings(
Guid key,
IEnumerable<UmbracoObjectTypes> objectTypes,
int before,
int after,
out long totalBefore,
out long totalAfter,
IQuery<IUmbracoEntity>? filter = null,
Ordering? ordering = null)
{
if (before < 0)
{
throw new ArgumentOutOfRangeException(nameof(before), "The 'before' parameter must be greater than or equal to 0.");
}
if (after < 0)
{
throw new ArgumentOutOfRangeException(nameof(after), "The 'after' parameter must be greater than or equal to 0.");
}
ordering ??= new Ordering("sortOrder");
using ICoreScope scope = ScopeProvider.CreateCoreScope();
var objectTypeGuids = objectTypes.Select(x => x.GetGuid()).ToHashSet();
IEnumerable<IEntitySlim> siblings = _entityRepository.GetTrashedSiblings(
objectTypeGuids,
key,
before,
after,
filter,
ordering,
out totalBefore,
out totalAfter);
scope.Complete();
return siblings;
}
/// <inheritdoc />
public virtual IEnumerable<IEntitySlim> GetDescendants(int id)
{
+29 -2
View File
@@ -171,7 +171,7 @@ public interface IEntityService
IEntitySlim? GetParent(int id, UmbracoObjectTypes objectType);
/// <summary>
/// Gets sibling entities of a specified target entity, within a given range before and after the target, ordered as specified.
/// Gets non-trashed sibling entities of a specified target entity, within a given range before and after the target, ordered as specified.
/// </summary>
/// <param name="key">The key of the target entity whose siblings are to be retrieved.</param>
/// <param name="objectTypes">The object types of the entities.</param>
@@ -181,7 +181,7 @@ public interface IEntityService
/// <param name="ordering">The ordering to apply to the siblings.</param>
/// <param name="totalBefore">Outputs the total number of siblings before the target entity.</param>
/// <param name="totalAfter">Outputs the total number of siblings after the target entity.</param>
/// <returns>Enumerable of sibling entities.</returns>
/// <returns>Enumerable of non-trashed sibling entities.</returns>
IEnumerable<IEntitySlim> GetSiblings(
Guid key,
IEnumerable<UmbracoObjectTypes> objectTypes,
@@ -197,6 +197,33 @@ public interface IEntityService
return [];
}
/// <summary>
/// Gets trashed sibling entities of a specified target entity, within a given range before and after the target, ordered as specified.
/// </summary>
/// <param name="key">The key of the target entity whose siblings are to be retrieved.</param>
/// <param name="objectTypes">The object types of the entities.</param>
/// <param name="before">The number of siblings to retrieve before the target entity. Needs to be greater or equal to 0.</param>
/// <param name="after">The number of siblings to retrieve after the target entity. Needs to be greater or equal to 0.</param>
/// <param name="filter">An optional filter to apply to the result set.</param>
/// <param name="ordering">The ordering to apply to the siblings.</param>
/// <param name="totalBefore">Outputs the total number of siblings before the target entity.</param>
/// <param name="totalAfter">Outputs the total number of siblings after the target entity.</param>
/// <returns>Enumerable of trashed sibling entities.</returns>
IEnumerable<IEntitySlim> GetTrashedSiblings(
Guid key,
IEnumerable<UmbracoObjectTypes> objectTypes,
int before,
int after,
out long totalBefore,
out long totalAfter,
IQuery<IUmbracoEntity>? filter = null,
Ordering? ordering = null)
{
totalBefore = 0;
totalAfter = 0;
return [];
}
/// <summary>
/// Gets the children of an entity.
/// </summary>
@@ -58,10 +58,9 @@ public class PropertyValidationService : IPropertyValidationService
}
IDataEditor? dataEditor = GetDataEditor(propertyType);
if (dataEditor == null)
if (dataEditor is null)
{
throw new InvalidOperationException("No property editor found by alias " +
propertyType.PropertyEditorAlias);
return [];
}
// only validate culture invariant properties if
@@ -1,5 +1,6 @@
using System.Globalization;
using System.Text.RegularExpressions;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Routing;
namespace Umbraco.Cms.Core.Templates;
@@ -45,17 +46,18 @@ public sealed class HtmlLocalLinkParser
/// <summary>
/// Parses the string looking for the {localLink} syntax and updates them to their correct links.
/// </summary>
/// <param name="text"></param>
/// <param name="preview"></param>
/// <returns></returns>
[Obsolete("This method overload is no longer used in Umbraco and delegates to the overload without the preview parameter. Scheduled for removal in Umbraco 18.")]
public string EnsureInternalLinks(string text, bool preview) => EnsureInternalLinks(text);
/// <summary>
/// Parses the string looking for the {localLink} syntax and updates them to their correct links.
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
public string EnsureInternalLinks(string text)
public string EnsureInternalLinks(string text) => EnsureInternalLinks(text, UrlMode.Default);
/// <summary>
/// Parses the string looking for the {localLink} syntax and updates them to their correct links.
/// </summary>
public string EnsureInternalLinks(string text, UrlMode urlMode)
{
foreach (LocalLinkTag tagData in FindLocalLinkIds(text))
{
@@ -63,8 +65,8 @@ public sealed class HtmlLocalLinkParser
{
var newLink = tagData.Udi?.EntityType switch
{
Constants.UdiEntityType.Document => _publishedUrlProvider.GetUrl(tagData.Udi.Guid),
Constants.UdiEntityType.Media => _publishedUrlProvider.GetMediaUrl(tagData.Udi.Guid),
Constants.UdiEntityType.Document => _publishedUrlProvider.GetUrl(tagData.Udi.Guid, urlMode),
Constants.UdiEntityType.Media => _publishedUrlProvider.GetMediaUrl(tagData.Udi.Guid, urlMode),
_ => string.Empty,
};
@@ -73,7 +75,7 @@ public sealed class HtmlLocalLinkParser
}
else if (tagData.IntId.HasValue)
{
var newLink = _publishedUrlProvider.GetUrl(tagData.IntId.Value);
var newLink = _publishedUrlProvider.GetUrl(tagData.IntId.Value, urlMode);
text = text.Replace(tagData.TagHref, newLink);
}
}
@@ -193,6 +193,72 @@ internal sealed class EntityRepository : RepositoryBase, IEntityRepositoryExtend
Ordering ordering,
out long totalBefore,
out long totalAfter)
{
Sql<ISqlContext> mainSql = SiblingsSql(
false,
objectTypes,
targetKey,
before,
after,
filter,
ordering,
out totalBefore,
out totalAfter);
List<Guid>? keys = Database.Fetch<Guid>(mainSql);
if (keys is null || keys.Count == 0)
{
return [];
}
// To re-use this method we need to provide a single object type. By convention for folder based trees, we provide the primary object type last.
return PerformGetAll(objectTypes.ToArray(), ordering, sql => sql.WhereIn<NodeDto>(x => x.UniqueId, keys));
}
/// <inheritdoc/>
public IEnumerable<IEntitySlim> GetTrashedSiblings(
ISet<Guid> objectTypes,
Guid targetKey,
int before,
int after,
IQuery<IUmbracoEntity>? filter,
Ordering ordering,
out long totalBefore,
out long totalAfter)
{
Sql<ISqlContext>? mainSql = SiblingsSql(
true,
objectTypes,
targetKey,
before,
after,
filter,
ordering,
out totalBefore,
out totalAfter);
List<Guid>? keys = Database.Fetch<Guid>(mainSql);
if (keys is null || keys.Count == 0)
{
return [];
}
// To re-use this method we need to provide a single object type. By convention for folder based trees, we provide the primary object type last.
return PerformGetAll(objectTypes.ToArray(), ordering, sql => sql.WhereIn<NodeDto>(x => x.UniqueId, keys));
}
private Sql<ISqlContext> SiblingsSql(
bool isTrashed,
ISet<Guid> objectTypes,
Guid targetKey,
int before,
int after,
IQuery<IUmbracoEntity>? filter,
Ordering ordering,
out long totalBefore,
out long totalAfter)
{
// Ideally we don't want to have to do a second query for the parent ID, but the siblings query is already messy enough
// without us also having to do a nested query for the parent ID too.
@@ -212,7 +278,7 @@ internal sealed class EntityRepository : RepositoryBase, IEntityRepositoryExtend
.Select($"ROW_NUMBER() OVER ({orderingSql.SQL}) AS rn")
.AndSelect<NodeDto>(n => n.UniqueId)
.From<NodeDto>()
.Where<NodeDto>(x => x.ParentId == parentId && x.Trashed == false)
.Where<NodeDto>(x => x.ParentId == parentId && x.Trashed == isTrashed)
.WhereIn<NodeDto>(x => x.NodeObjectType, objectTypes);
// Apply the filter if provided.
@@ -245,25 +311,16 @@ internal sealed class EntityRepository : RepositoryBase, IEntityRepositoryExtend
var beforeAfterParameterIndex = BeforeAfterParameterIndex + beforeAfterParameterIndexOffset;
var beforeArgumentsArray = beforeArguments.ToArray();
var afterArgumentsArray = afterArguments.ToArray();
Sql<ISqlContext>? mainSql = Sql()
totalBefore = GetNumberOfSiblingsOutsideSiblingRange(rowNumberSql, targetRowSql, beforeAfterParameterIndex, beforeArgumentsArray, true);
totalAfter = GetNumberOfSiblingsOutsideSiblingRange(rowNumberSql, targetRowSql, beforeAfterParameterIndex, afterArgumentsArray, false);
return Sql()
.Select("UniqueId")
.From().AppendSubQuery(rowNumberSql, "NumberedNodes")
.Where($"rn >= ({targetRowSql.SQL}) - @{beforeAfterParameterIndex}", beforeArgumentsArray)
.Where($"rn <= ({targetRowSql.SQL}) + @{beforeAfterParameterIndex}", afterArgumentsArray)
.OrderBy("rn");
List<Guid>? keys = Database.Fetch<Guid>(mainSql);
totalBefore = GetNumberOfSiblingsOutsideSiblingRange(rowNumberSql, targetRowSql, beforeAfterParameterIndex, beforeArgumentsArray, true);
totalAfter = GetNumberOfSiblingsOutsideSiblingRange(rowNumberSql, targetRowSql, beforeAfterParameterIndex, afterArgumentsArray, false);
if (keys is null || keys.Count == 0)
{
return [];
}
// To re-use this method we need to provide a single object type. By convention for folder based trees, we provide the primary object type last.
return PerformGetAll(objectTypes.ToArray(), ordering, sql => sql.WhereIn<NodeDto>(x => x.UniqueId, keys));
}
private static int GetBeforeAfterParameterOffset(ISet<Guid> objectTypes, IQuery<IUmbracoEntity>? filter)
@@ -681,6 +681,15 @@ SELECT 4 AS {keyAlias}, COUNT(id) AS {valueAlias} FROM {userTableName}
protected override void PersistDeletedItem(IUser entity)
{
// Clear user group caches for any user groups associated with the deleted user.
// We need to do this because the count of the number of users in the user group is cached
// along with the user group, and if we've made changes to the user groups assigned to the user,
// the count for the groups need to be refreshed.
foreach (IReadOnlyUserGroup group in entity.Groups)
{
ClearRepositoryCacheForUserGroup(group.Id);
}
IEnumerable<string> deletes = GetDeleteClauses();
foreach (var delete in deletes)
{
@@ -722,20 +731,31 @@ SELECT 4 AS {keyAlias}, COUNT(id) AS {valueAlias} FROM {userTableName}
if (entity.IsPropertyDirty("Groups"))
{
// lookup all assigned
Sql<ISqlContext> sql = SqlContext.Sql()
.SelectAll()
.From<UserGroupDto>()
.WhereIn<UserGroupDto>(x => x.Alias, entity.Groups.Select(x => x.Alias).ToArray());
List<UserGroupDto>? assigned = entity.Groups == null || entity.Groups.Any() == false
? new List<UserGroupDto>()
: Database.Fetch<UserGroupDto>(sql);
// Lookup all assigned groups.
List<UserGroupDto> assigned = [];
if (entity.Groups.Any())
{
Sql<ISqlContext> sql = SqlContext.Sql()
.SelectAll()
.From<UserGroupDto>()
.WhereIn<UserGroupDto>(x => x.Alias, entity.Groups.Select(x => x.Alias).ToArray());
assigned = Database.Fetch<UserGroupDto>(sql);
}
foreach (UserGroupDto? groupDto in assigned)
{
var dto = new User2UserGroupDto { UserGroupId = groupDto.Id, UserId = entity.Id };
Database.Insert(dto);
}
// Clear user group caches for the user groups associated with the new user.
// We need to do this because the count of the number of users in the user group is cached
// along with the user group, and if we've made changes to the user groups assigned to the user,
// the count for the groups need to be refreshed.
foreach (IReadOnlyUserGroup group in entity.Groups)
{
ClearRepositoryCacheForUserGroup(group.Id);
}
}
entity.ResetDirtyProperties();
@@ -849,34 +869,68 @@ SELECT 4 AS {keyAlias}, COUNT(id) AS {valueAlias} FROM {userTableName}
if (entity.IsPropertyDirty("Groups"))
{
// lookup all assigned
// Get all user groups Ids currently assigned to the user.
Sql<ISqlContext> sql = SqlContext.Sql()
.SelectAll()
.From<UserGroupDto>()
.WhereIn<UserGroupDto>(
x => x.Alias,
entity.Groups.Select(x => x.Alias).ToArray());
List<UserGroupDto>? assigned = entity.Groups == null || entity.Groups.Any() == false
? []
: Database.Fetch<UserGroupDto>(sql);
// first delete all
sql = SqlContext.Sql()
.Delete<User2UserGroupDto>()
.Select<User2UserGroupDto>(x => x.UserGroupId)
.From<User2UserGroupDto>()
.Where<User2UserGroupDto>(c => c.UserId == entity.Id);
Database.Execute(sql);
foreach (UserGroupDto? groupDto in assigned)
List<int> existingUserGroupIds = Database.Fetch<int>(sql);
// Get the user groups Ids that need to be removed and added.
var userGroupsIdsToRemove = existingUserGroupIds
.Except(entity.Groups.Select(x => x.Id))
.ToList();
var userGroupIdsToAdd = entity.Groups
.Select(x => x.Id)
.Except(existingUserGroupIds)
.ToList();
// Remove user groups that are no longer assigned to the user.
if (userGroupsIdsToRemove.Count > 0)
{
var dto = new User2UserGroupDto { UserGroupId = groupDto.Id, UserId = entity.Id };
Database.Insert(dto);
Database.Delete<User2UserGroupDto>(
Sql()
.Where<User2UserGroupDto>(x => x.UserId == entity.Id)
.WhereIn<User2UserGroupDto>(x => x.UserGroupId, userGroupsIdsToRemove));
}
// Add user groups that are newly assigned to the user.
if (userGroupIdsToAdd.Count > 0)
{
IEnumerable<User2UserGroupDto> user2UserGroupDtos = userGroupIdsToAdd
.Select(userGroupId => new User2UserGroupDto
{
UserGroupId = userGroupId,
UserId = entity.Id,
});
Database.InsertBulk(user2UserGroupDtos);
}
// Clear user group caches for any user group that have been removed or added.
// We need to do this because the count of the number of users in the user group is cached
// along with the user group, and if we've made changes to the user groups assigned to the user,
// the count for the groups need to be refreshed.
var userGroupIdsToRefresh = userGroupsIdsToRemove
.Union(userGroupIdsToAdd)
.ToList();
foreach (int userGroupIdToRefresh in userGroupIdsToRefresh)
{
ClearRepositoryCacheForUserGroup(userGroupIdToRefresh);
}
}
entity.ResetDirtyProperties();
}
private void ClearRepositoryCacheForUserGroup(int id)
{
IAppPolicyCache userGroupCache = AppCaches.IsolatedCaches.GetOrCreate<IUserGroup>();
string cacheKey = RepositoryCacheKeys.GetKey<IUserGroup, int>(id);
userGroupCache.Clear(cacheKey);
}
private void AddingOrUpdateStartNodes(IEntity entity, IEnumerable<UserStartNodeDto> current,
UserStartNodeDto.StartNodeTypeValue startNodeType, int[]? entityStartIds)
{
@@ -41,7 +41,7 @@ public class MarkdownEditorValueConverter : PropertyValueConverterBase, IDeliver
var sourceString = source.ToString()!;
// ensures string is parsed for {localLink} and URLs are resolved correctly
sourceString = _localLinkParser.EnsureInternalLinks(sourceString, preview);
sourceString = _localLinkParser.EnsureInternalLinks(sourceString);
sourceString = _urlParser.EnsureUrls(sourceString);
return sourceString;
@@ -135,7 +135,7 @@ public class RteBlockRenderingValueConverter : SimpleRichTextValueConverter, IDe
var sourceString = intermediateValue.Markup;
// ensures string is parsed for {localLink} and URLs and media are resolved correctly
sourceString = _linkParser.EnsureInternalLinks(sourceString, preview);
sourceString = _linkParser.EnsureInternalLinks(sourceString);
sourceString = _urlParser.EnsureUrls(sourceString);
sourceString = _imageSourceParser.EnsureImageSources(sourceString);
@@ -132,10 +132,8 @@ internal sealed class DocumentCacheService : IDocumentCacheService
GetEntryOptions(key, preview),
GenerateTags(key));
// We don't want to cache removed items, this may cause issues if the L2 serializer changes.
if (contentCacheNode is null)
{
await _hybridCache.RemoveAsync(cacheKey);
return null;
}
@@ -347,7 +347,7 @@ public static class FriendlyPublishedContentExtensions
/// </param>
/// <returns></returns>
/// <remarks>
/// This can be useful in order to return all nodes in an entire site by a type when combined with TypedContentAtRoot
/// This can be useful in order to return all nodes in an entire site by a type when combined with ContentAtRoot.
/// </remarks>
public static IEnumerable<IPublishedContent> DescendantsOrSelfOfType(
this IEnumerable<IPublishedContent> parentNodes, string docTypeAlias, string? culture = null)
@@ -375,7 +375,7 @@ public static class FriendlyPublishedContentExtensions
/// </param>
/// <returns></returns>
/// <remarks>
/// This can be useful in order to return all nodes in an entire site by a type when combined with TypedContentAtRoot
/// This can be useful in order to return all nodes in an entire site by a type when combined with ContentAtRoot.
/// </remarks>
public static IEnumerable<T> DescendantsOrSelf<T>(
this IEnumerable<IPublishedContent> parentNodes,
+4
View File
@@ -309,6 +309,10 @@ public class UmbracoHelper
/// <remarks>If an identifier does not match an existing content, it will be missing in the returned value.</remarks>
public IEnumerable<IPublishedContent> Content(IEnumerable<int> ids) => _publishedContentQuery.Content(ids);
/// <summary>
/// Gets the documents at root.
/// </summary>
/// <returns>A collection of <see cref="IPublishedContent"/> found at the root.</returns>
public IEnumerable<IPublishedContent> ContentAtRoot() => _publishedContentQuery.ContentAtRoot();
#endregion
@@ -41,10 +41,10 @@ export class ExampleHintWorkspaceView extends UmbElementMixin(LitElement) {
throw new Error('Could not find the workspace');
}
if (workspace.hints.has('exampleHintFromToggleAction')) {
workspace.hints.removeOne('exampleHintFromToggleAction');
if (workspace.view.hints.has('exampleHintFromToggleAction')) {
workspace.view.hints.removeOne('exampleHintFromToggleAction');
} else {
workspace.hints.addOne({
workspace.view.hints.addOne({
unique: 'exampleHintFromToggleAction',
path: ['Umb.WorkspaceView.Document.Edit'],
text: 'Hi',
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@umbraco-cms/backoffice",
"version": "16.2.0-rc",
"version": "17.0.0-rc",
"lockfileVersion": 3,
"requires": true,
"packages": {
+6 -3
View File
@@ -31,6 +31,7 @@
"./collection": "./dist-cms/packages/core/collection/index.js",
"./components": "./dist-cms/packages/core/components/index.js",
"./const": "./dist-cms/packages/core/const/index.js",
"./content-picker": "./dist-cms/packages/property-editors/content-picker/index.js",
"./content-type": "./dist-cms/packages/content/content-type/index.js",
"./content": "./dist-cms/packages/content/content/index.js",
"./culture": "./dist-cms/packages/core/culture/index.js",
@@ -46,8 +47,8 @@
"./entity-action": "./dist-cms/packages/core/entity-action/index.js",
"./entity-bulk-action": "./dist-cms/packages/core/entity-bulk-action/index.js",
"./entity-create-option-action": "./dist-cms/packages/core/entity-create-option-action/index.js",
"./entity": "./dist-cms/packages/core/entity/index.js",
"./entity-item": "./dist-cms/packages/core/entity-item/index.js",
"./entity": "./dist-cms/packages/core/entity/index.js",
"./event": "./dist-cms/packages/core/event/index.js",
"./extension-registry": "./dist-cms/packages/core/extension-registry/index.js",
"./health-check": "./dist-cms/packages/health-check/index.js",
@@ -57,6 +58,7 @@
"./icon": "./dist-cms/packages/core/icon-registry/index.js",
"./id": "./dist-cms/packages/core/id/index.js",
"./imaging": "./dist-cms/packages/media/imaging/index.js",
"./interaction-memory": "./dist-cms/packages/core/interaction-memory/index.js",
"./language": "./dist-cms/packages/language/index.js",
"./lit-element": "./dist-cms/packages/core/lit-element/index.js",
"./localization": "./dist-cms/packages/core/localization/index.js",
@@ -66,9 +68,9 @@
"./media-type": "./dist-cms/packages/media/media-types/index.js",
"./media": "./dist-cms/packages/media/media/index.js",
"./member-group": "./dist-cms/packages/members/member-group/index.js",
"./member-public-access": "./dist-cms/packages/members/member-public-access/index.js",
"./member-type": "./dist-cms/packages/members/member-type/index.js",
"./member": "./dist-cms/packages/members/member/index.js",
"./member-public-access": "./dist-cms/packages/members/member-public-access/index.js",
"./menu": "./dist-cms/packages/core/menu/index.js",
"./modal": "./dist-cms/packages/core/modal/index.js",
"./models": "./dist-cms/packages/core/models/index.js",
@@ -94,9 +96,10 @@
"./search": "./dist-cms/packages/search/index.js",
"./section": "./dist-cms/packages/core/section/index.js",
"./segment": "./dist-cms/packages/segment/index.js",
"./server": "./dist-cms/packages/core/server/index.js",
"./server-file-system": "./dist-cms/packages/core/server-file-system/index.js",
"./server": "./dist-cms/packages/core/server/index.js",
"./settings": "./dist-cms/packages/settings/index.js",
"./shortcut": "./dist-cms/packages/core/shortcut/index.js",
"./sorter": "./dist-cms/packages/core/sorter/index.js",
"./static-file": "./dist-cms/packages/static-file/index.js",
"./store": "./dist-cms/packages/core/store/index.js",
@@ -22,6 +22,7 @@ import { filter, first, firstValueFrom } from '@umbraco-cms/backoffice/external/
import { hasOwnOpener, redirectToStoredPath } from '@umbraco-cms/backoffice/utils';
import { UmbApiInterceptorController } from '@umbraco-cms/backoffice/resources';
import { umbHttpClient } from '@umbraco-cms/backoffice/http-client';
import { UmbViewContext } from '@umbraco-cms/backoffice/view';
import './app-logo.element.js';
import './app-oauth.element.js';
@@ -159,6 +160,8 @@ export class UmbAppElement extends UmbLitElement {
new UmbContextDebugController(this);
new UmbNetworkConnectionStatusManager(this);
new UmbViewContext(this, null);
}
override connectedCallback(): void {
@@ -54,6 +54,10 @@ export class UmbBackofficeHeaderSectionsElement extends UmbLitElement {
);
}
#getSectionName(section: UmbExtensionManifestInitializer<ManifestSection>) {
return section.manifest?.meta.label ? this.localize.string(section.manifest?.meta.label) : section.manifest?.name;
}
#getSectionPath(manifest: ManifestSection | undefined) {
return `section/${manifest?.meta.pathname}`;
}
@@ -108,12 +112,10 @@ export class UmbBackofficeHeaderSectionsElement extends UmbLitElement {
?active="${this._currentSectionAlias === section.alias}"
@click=${(event: PointerEvent) => this.#onSectionClick(event, section.manifest)}
href="${this.#getSectionPath(section.manifest)}"
label="${ifDefined(
section.manifest?.meta.label
? this.localize.string(section.manifest?.meta.label)
: section.manifest?.name,
)}"
data-mark="section-link:${section.alias}"></uui-tab>
label="${ifDefined(this.#getSectionName(section))}"
data-mark="section-link:${section.alias}"
>${this.#getSectionName(section)}</uui-tab
>
`,
)}
</uui-tab-group>
@@ -2211,6 +2211,7 @@ export default {
searchContentTree: "Chwilio'r coeden cynnwys",
maxAmount: 'Uchafswm',
expandChildItems: 'Ehangu eitemau plentyn ar gyfer',
collapseChildItems: 'Cuddio eitemau plant ar gyfer',
openContextNode: 'Agor nod cyd-destun ar gyfer',
},
references: {
@@ -2350,6 +2350,7 @@ export default {
maxAmount: 'Maximum antal',
contextDialogDescription: 'Perform action %0% on the %1% node',
expandChildItems: 'Udvid underordnede elementer for',
collapseChildItems: 'Skjul underordnede elementer for',
openContextNode: 'Åbn kontekstnode for',
},
references: {
@@ -357,6 +357,9 @@ export default {
saveAndPublishModalTitle: 'Save and publish',
publishModalTitle: 'Publish',
openSplitViewForVariant: (variant: string) => `Open ${variant} in split view`,
sharedAcrossCultures: 'Shared across cultures',
sharedAcrossSegments: 'Shared across segments',
shared: 'Shared',
},
blueprints: {
createBlueprintFrom: "Create a new Document Blueprint from '%0%'",
@@ -2408,6 +2411,7 @@ export default {
searchContentTree: 'Search content tree',
maxAmount: 'Maximum amount',
expandChildItems: 'Expand child items for',
collapseChildItems: 'Collapse child items for',
openContextNode: 'Open context node for',
},
references: {
@@ -2834,10 +2838,17 @@ export default {
resetUrlLabel: 'Reset',
},
missingEditor: {
title: 'This property type is no longer available.',
description:
'<p><strong>Error!</strong> This property type is no longer available. Please reach out to your administrator.</p>',
"Don't worry, your content is safe and publishing this document won't overwrite it or remove it.<br/>Please contact your site administrator to resolve this issue.",
detailsTitle: 'Additional details',
detailsDescription:
'<p>This property type is no longer available.<br/>Please contact your administrator so they can either delete this property or restore the property type.</p><p><strong>Data:</strong></p>',
"To resolve this you should either restore the property editor, change the property to use a supported data type or remove the property if it's no longer needed.",
detailsDataType: 'Data type',
detailsPropertyEditor: 'Property editor',
detailsData: 'Data',
detailsHide: 'Hide details',
detailsShow: 'Show details',
},
uiCulture: {
ar: 'العربية',
@@ -1873,6 +1873,7 @@ export default {
searchContentTree: "Chercher dans l'arborescence de contenu",
maxAmount: 'Quantité maximum',
expandChildItems: 'Afficher les éléments enfant pour',
collapseChildItems: 'Cacher les éléments enfant pour',
openContextNode: 'Ouvrir le noeud de contexte pour',
},
references: {
@@ -2407,6 +2407,7 @@ export default {
searchContentTree: 'Pesquisar Árvore de Conteúdo',
maxAmount: 'Quantidade máxima',
expandChildItems: 'Expandir itens filhos para',
collapseChildItems: 'Fechar itens filhos para',
openContextNode: 'Abrir nó de contexto para',
},
references: {
@@ -2832,9 +2833,16 @@ export default {
resetUrlLabel: 'Redefinir',
},
missingEditor: {
title: 'Este tipo de propriedade já não se encontra disponível.',
description:
'<p><strong>Erro!</strong> Este tipo de propriedade já não se encontra disponível. Por favor, contacte o administrador.</p>',
'Não se preocupe, o seu conteúdo está seguro e a publicação deste documento não o substituirá nem removerá.<br/>Entre em contacto com o administrador do site para resolver o problema.',
detailsTitle: 'Detalhes adicionais',
detailsDescription:
'<p>Este tipo de propriedade já não se encontra disponível.<br/>Por favor, contacte o administrador para que ele possa apagar a propriedade ou restaurar o tipo de propriedade.</p><p><strong>Dados:</strong></p>',
'Para resolver o problema, deverá ou restaurar o editor de propriedades, ou alterar a propriedade para usar um tipo de dados compatível ou remover a propriedade se ela não for mais necessária.',
detailsDataType: 'Tipo de dados',
detailsPropertyEditor: 'Editor de propriedades',
detailsData: 'Dados',
detailsHide: 'Esconder detalhes',
detailsShow: 'Mostrar detalhes',
},
} as UmbLocalizationDictionary;
@@ -318,6 +318,7 @@ export default {
searchContentTree: 'Sök i innehållsträdet',
maxAmount: 'Maximalt värde',
expandChildItems: 'Visa underliggande noder för',
collapseChildItems: 'Dölj underliggande noder för',
openContextNode: 'Öppna kontext för',
},
prompt: {
@@ -2410,6 +2410,7 @@ export default {
searchContentTree: 'Tìm kiếm cây nội dung',
maxAmount: 'Số lượng tối đa',
expandChildItems: 'Mở rộng các mục con cho',
collapseChildItems: 'Thu gọn các mục con cho',
openContextNode: 'Mở nút ngữ cảnh cho %0%',
},
references: {
@@ -84,7 +84,12 @@ export class UmbBlockElementManager<LayoutDataType extends UmbBlockLayoutBaseMod
// Ugly, but we just inherit these from the workspace context: [NL]
this.name = host.name;
this.getName = host.getName;
this.getName = () => {
const contentTypeLabel = this.structure.getOwnerContentType()?.name;
const blockLabel = host.getName();
return contentTypeLabel ? `${contentTypeLabel} ${blockLabel}` : blockLabel;
};
this.propertyViewGuard.fallbackToPermitted();
this.propertyWriteGuard.fallbackToPermitted();
@@ -94,6 +99,7 @@ export class UmbBlockElementManager<LayoutDataType extends UmbBlockLayoutBaseMod
this.structure.loadType(id);
}
});
this.observe(this.unique, (key) => {
if (key) {
this.validation.setDataPath('$.' + dataPathPropertyName + `[?(@.key == '${key}')]`);
@@ -26,6 +26,7 @@ import { UmbVariantId } from '@umbraco-cms/backoffice/variant';
import type { UUIModalSidebarSize } from '@umbraco-cms/backoffice/external/uui';
export type UmbBlockWorkspaceElementManagerNames = 'content' | 'settings';
export class UmbBlockWorkspaceContext<LayoutDataType extends UmbBlockLayoutBaseModel = UmbBlockLayoutBaseModel>
extends UmbSubmittableWorkspaceContextBase<LayoutDataType>
implements UmbRoutableWorkspaceContext
@@ -470,7 +471,7 @@ export class UmbBlockWorkspaceContext<LayoutDataType extends UmbBlockLayoutBaseM
}
getName() {
return 'block name content element type here...';
return '';
}
/**
@@ -70,4 +70,5 @@ export class UmbCopyToClipboardPropertyAction extends UmbPropertyActionBase<Meta
});
}
}
export { UmbCopyToClipboardPropertyAction as api };
@@ -70,7 +70,9 @@ export abstract class UmbContentTypeWorkspaceContextBase<
this.collection = this.structure.ownerContentTypeObservablePart((data) => data?.collection);
// Keep current data in sync with the owner content type - This is used for the discard changes feature
this.observe(this.structure.ownerContentType, (data) => this._data.setCurrent(data));
this.observe(this.structure.ownerContentType, (data) => this._data.setCurrent(data), null);
this.observe(this.name, (name) => this.view.setTitle(name), null);
// TODO: sometimes the browserTitle for a parent view is set later than the child is updating. We need to fix this as well enable a parent browser title to be updating on the go. [NL]
}
/**
@@ -1,13 +1,13 @@
import type { UmbContentTypeCompositionModel, UmbContentTypeModel, UmbContentTypeSortModel } from '../types.js';
import type { UmbContentTypeStructureManager } from '../structure/index.js';
import type { Observable } from '@umbraco-cms/backoffice/external/rxjs';
import type { UmbSubmittableWorkspaceContext } from '@umbraco-cms/backoffice/workspace';
import type { UmbNamableWorkspaceContext, UmbSubmittableWorkspaceContext } from '@umbraco-cms/backoffice/workspace';
export interface UmbContentTypeWorkspaceContext<ContentTypeType extends UmbContentTypeModel = UmbContentTypeModel>
extends UmbSubmittableWorkspaceContext {
extends UmbSubmittableWorkspaceContext,
UmbNamableWorkspaceContext {
readonly IS_CONTENT_TYPE_WORKSPACE_CONTEXT: true;
readonly name: Observable<string | undefined>;
readonly alias: Observable<string | undefined>;
readonly description: Observable<string | undefined>;
readonly icon: Observable<string | undefined>;
@@ -32,7 +32,4 @@ export interface UmbContentTypeWorkspaceContext<ContentTypeType extends UmbConte
getIcon(): string | undefined;
setIcon(icon: string): void;
getName(): string | undefined;
setName(name: string): void;
}
@@ -310,6 +310,7 @@ export class UmbContentTypeDesignEditorPropertyElement extends UmbLitElement {
${this.#renderVariantTags()}
${this.property.appearance?.labelOnTop == true
? html`<uui-tag look="default">
<uui-icon name="icon-stretch-horizontal"></uui-icon>
<span>${this.localize.term('contentTypeEditor_displaySettingsLabelOnTop')}</span>
</uui-tag>`
: nothing}
@@ -348,7 +349,7 @@ export class UmbContentTypeDesignEditorPropertyElement extends UmbLitElement {
) {
return html`
<uui-tag look="default">
<uui-icon name="icon-shared-value"></uui-icon> ${this.localize.term(
<uui-icon name="icon-trending-up-down"></uui-icon> ${this.localize.term(
'contentTypeEditor_cultureAndVariantInvariantLabel',
)}
</uui-tag>
@@ -357,13 +358,17 @@ export class UmbContentTypeDesignEditorPropertyElement extends UmbLitElement {
if (this.ownerVariesByCulture && !this.property.variesByCulture) {
return html`<uui-tag look="default">
<uui-icon name="icon-shuffle"></uui-icon> ${this.localize.term('contentTypeEditor_cultureInvariantLabel')}
<uui-icon name="icon-trending-up-down"></uui-icon> ${this.localize.term(
'contentTypeEditor_cultureInvariantLabel',
)}
</uui-tag>`;
}
if (this.ownerVariesBySegment && !this.property.variesBySegment) {
return html`<uui-tag look="default">
<uui-icon name="icon-shuffle"></uui-icon> ${this.localize.term('contentTypeEditor_segmentInvariantLabel')}
<uui-icon name="icon-trending-up-down"></uui-icon> ${this.localize.term(
'contentTypeEditor_segmentInvariantLabel',
)}
</uui-tag>`;
}
@@ -9,7 +9,12 @@ import type { UmbContentCollectionWorkspaceContext } from '../collection/content
import type { UmbContentWorkspaceContext } from './content-workspace-context.interface.js';
import { UmbContentDetailValidationPathTranslator } from './content-detail-validation-path-translator.js';
import { UmbContentValidationToHintsManager } from './content-validation-to-hints.manager.js';
import { appendToFrozenArray, mergeObservables, UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
import {
appendToFrozenArray,
mergeObservables,
observeMultiple,
UmbArrayState,
} from '@umbraco-cms/backoffice/observable-api';
import { firstValueFrom, map } from '@umbraco-cms/backoffice/external/rxjs';
import { umbOpenModal } from '@umbraco-cms/backoffice/modal';
import { UmbContentTypeStructureManager } from '@umbraco-cms/backoffice/content-type';
@@ -21,7 +26,6 @@ import {
UmbRequestReloadChildrenOfEntityEvent,
UmbRequestReloadStructureForEntityEvent,
} from '@umbraco-cms/backoffice/entity-action';
import { UmbHintContext } from '@umbraco-cms/backoffice/hint';
import { UmbLanguageCollectionRepository } from '@umbraco-cms/backoffice/language';
import {
UmbPropertyValuePresetVariantBuilderController,
@@ -52,7 +56,6 @@ import type { UmbLanguageDetailModel } from '@umbraco-cms/backoffice/language';
import type { UmbPropertyTypePresetModel, UmbPropertyTypePresetModelTypeModel } from '@umbraco-cms/backoffice/property';
import type { UmbModalToken } from '@umbraco-cms/backoffice/modal';
import type { UmbSegmentCollectionItemModel } from '@umbraco-cms/backoffice/segment';
import type { UmbVariantHint } from '@umbraco-cms/backoffice/hint';
export interface UmbContentDetailWorkspaceContextArgs<
DetailModelType extends UmbContentDetailModel<VariantModelType>,
@@ -141,9 +144,6 @@ export abstract class UmbContentDetailWorkspaceContextBase<
readonly collection: UmbContentCollectionManager;
/* Hints */
readonly hints = new UmbHintContext<UmbVariantHint>(this);
/* Variant Options */
// TODO: Optimize this so it uses either a App Language Context? [NL]
#languageRepository = new UmbLanguageCollectionRepository(this);
@@ -221,7 +221,7 @@ export abstract class UmbContentDetailWorkspaceContextBase<
this,
this.structure,
this.validationContext,
this.hints,
this.view.hints,
);
this.variantOptions = mergeObservables(
@@ -334,6 +334,17 @@ export abstract class UmbContentDetailWorkspaceContextBase<
null,
);
this.observe(
observeMultiple([this.splitView.activeVariantByIndex(0), this.variants]),
([activeVariant, variants]) => {
const variantName = variants.find(
(v) => v.culture === activeVariant?.culture && v.segment === activeVariant?.segment,
)?.name;
this.view.setTitle(variantName);
},
null,
);
this.observe(
this.varies,
(varies) => {
@@ -810,7 +821,7 @@ export abstract class UmbContentDetailWorkspaceContextBase<
* Request a submit of the workspace, in the case of Document Workspaces the validation does not need to be valid for this to be submitted.
* @returns {Promise<void>} a promise which resolves once it has been completed.
*/
public override requestSubmit() {
public override requestSubmit(): Promise<void> {
return this._handleSubmit();
}
@@ -820,7 +831,7 @@ export abstract class UmbContentDetailWorkspaceContextBase<
/**
* Request a save of the workspace, in the case of Document Workspaces the validation does not need to be valid for this to be saved.
* @returns {Promise<void>} a promise which resolves once it has been completed.
* @returns {Promise<void>} A promise which resolves once it has been completed.
*/
public requestSave() {
return this._handleSave();
@@ -836,11 +847,11 @@ export abstract class UmbContentDetailWorkspaceContextBase<
return this._data.constructData(variantIds);
}
protected async _handleSubmit() {
protected async _handleSubmit(): Promise<void> {
await this._handleSave();
this._closeModal();
}
protected async _handleSave() {
protected async _handleSave(): Promise<void> {
const data = this.getData();
if (!data) {
throw new Error('Data is missing');
@@ -866,7 +877,9 @@ export abstract class UmbContentDetailWorkspaceContextBase<
value: { selection: selected },
}).catch(() => undefined);
if (!result?.selection.length) return;
if (!result?.selection.length) {
return Promise.reject('Cannot save without selecting at least one variant.');
}
variantIds = result?.selection.map((x) => UmbVariantId.FromString(x)) ?? [];
} else {
@@ -886,7 +899,9 @@ export abstract class UmbContentDetailWorkspaceContextBase<
() => false,
);
if (valid || this.#ignoreValidationResultOnSubmit) {
return this.performCreateOrUpdate(variantIds, saveData);
await this.performCreateOrUpdate(variantIds, saveData);
} else {
return Promise.reject('Validation issues prevent saving');
}
} else {
await this.performCreateOrUpdate(variantIds, saveData);
@@ -1,22 +1,28 @@
import type { UmbContentWorkspaceViewEditTabElement } from './content-editor-tab.element.js';
import { css, html, customElement, state, repeat, nothing } from '@umbraco-cms/backoffice/external/lit';
import { encodeFolderName } from '@umbraco-cms/backoffice/router';
import {
UmbContentTypeContainerStructureHelper,
UMB_PROPERTY_STRUCTURE_WORKSPACE_CONTEXT,
} from '@umbraco-cms/backoffice/content-type';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UMB_VIEW_CONTEXT, UmbViewController } from '@umbraco-cms/backoffice/view';
import type {
PageComponent,
UmbRoute,
UmbRouterSlotChangeEvent,
UmbRouterSlotInitEvent,
} from '@umbraco-cms/backoffice/router';
import type {
UmbContentTypeModel,
UmbContentTypeStructureManager,
UmbPropertyTypeContainerMergedModel,
} from '@umbraco-cms/backoffice/content-type';
import {
UmbContentTypeContainerStructureHelper,
UMB_PROPERTY_STRUCTURE_WORKSPACE_CONTEXT,
} from '@umbraco-cms/backoffice/content-type';
import type { UmbRoute, UmbRouterSlotChangeEvent, UmbRouterSlotInitEvent } from '@umbraco-cms/backoffice/router';
import { encodeFolderName } from '@umbraco-cms/backoffice/router';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/workspace';
import './content-editor-tab.element.js';
import type { UmbVariantHint } from '@umbraco-cms/backoffice/hint';
import { UMB_VIEW_CONTEXT, UmbViewContext } from '@umbraco-cms/backoffice/view';
import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/workspace';
import './content-editor-tab.element.js';
@customElement('umb-content-workspace-view-edit')
export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements UmbWorkspaceViewElement {
@@ -25,7 +31,7 @@ export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements
@state()
private _hasRootProperties = false;
*/
#viewContext?: UmbViewContext;
#viewContext?: typeof UMB_VIEW_CONTEXT.TYPE;
@state()
private _hasRootGroups = false;
@@ -43,9 +49,9 @@ export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements
private _activePath = '';
@state()
private _hintMap: Map<string, UmbVariantHint> = new Map();
private _hintMap: Map<string | null, UmbVariantHint> = new Map();
#tabViewContexts: Array<UmbViewContext> = [];
#tabViewContexts: Array<UmbViewController> = [];
#structureManager?: UmbContentTypeStructureManager<UmbContentTypeModel>;
@@ -104,9 +110,10 @@ export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements
component: () => import('./content-editor-tab.element.js'),
setup: (component) => {
(component as UmbContentWorkspaceViewEditTabElement).containerId = null;
this.#provideViewContext(null, component);
},
});
this.#createViewContext('root');
this.#createViewContext(null, '#general_generic');
}
if (this._tabs.length > 0) {
@@ -118,9 +125,10 @@ export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements
component: () => import('./content-editor-tab.element.js'),
setup: (component) => {
(component as UmbContentWorkspaceViewEditTabElement).containerId = tab.ownerId ?? tab.ids[0];
this.#provideViewContext(path, component);
},
});
this.#createViewContext(path);
this.#createViewContext(path, tabName);
});
}
@@ -140,11 +148,17 @@ export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements
this._routes = routes;
}
#createViewContext(viewAlias: string) {
#createViewContext(viewAlias: string | null, tabName: string) {
if (!this.#tabViewContexts.find((context) => context.viewAlias === viewAlias)) {
const view = new UmbViewContext(this, viewAlias);
const view = new UmbViewController(this, viewAlias);
this.#tabViewContexts.push(view);
if (viewAlias === null) {
// for the root tab, we need to filter hints, so in this case we do accept everything that is not in a tab: [NL]
view.hints.setPathFilter((paths) => paths[0].includes('tab/') === false);
}
view.setTitle(tabName);
view.inheritFrom(this.#viewContext);
this.observe(
@@ -162,13 +176,33 @@ export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements
}
}
#currentProvidedView?: UmbViewController;
#provideViewContext(viewAlias: string | null, component: PageComponent) {
const view = this.#tabViewContexts.find((context) => context.viewAlias === viewAlias);
if (this.#currentProvidedView === view) {
return;
}
this.#currentProvidedView?.unprovide();
if (!view) {
throw new Error(`View context with alias ${viewAlias} not found`);
}
this.#currentProvidedView = view;
// ViewAlias null is only for the root tab, therefor we can implement this hack.
if (viewAlias === null) {
// Specific hack for the Generic tab to only show its name if there are other tabs.
view.setTitle(this._tabs && this._tabs?.length > 0 ? '#general_generic' : undefined);
}
view.provideAt(component as any);
}
override render() {
if (!this._routes || !this._tabs) return;
return html`
<umb-body-layout header-fit-height>
${this._routerPath && (this._tabs.length > 1 || (this._tabs.length === 1 && this._hasRootGroups))
? html` <uui-tab-group slot="header">
${this._hasRootGroups && this._tabs.length > 0 ? this.#renderTab('root', '#general_generic') : nothing}
${this._hasRootGroups && this._tabs.length > 0 ? this.#renderTab(null, '#general_generic') : nothing}
${repeat(
this._tabs,
(tab) => tab.name,
@@ -194,20 +228,21 @@ export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements
`;
}
#renderTab(path: string, name: string, index = 0) {
#renderTab(path: string | null, name: string, index = 0) {
const hint = this._hintMap.get(path);
const fullPath = this._routerPath + '/' + path;
const fullPath = this._routerPath + '/' + (path ? path : 'root');
const active =
fullPath === this._activePath ||
(!this._hasRootGroups && index === 0 && this._routerPath + '/' === this._activePath);
(!this._hasRootGroups && index === 0 && this._routerPath + '/' === this._activePath) ||
(this._hasRootGroups && index === 0 && path === null && this._routerPath + '/' === this._activePath);
return html`<uui-tab
label=${this.localize.string(name ?? '#general_unnamed')}
.active=${active}
href=${fullPath}
data-mark="content-tab:${path}"
data-mark="content-tab:${path ?? 'root'}"
>${hint && !active
? html`<uui-badge slot="extra" .color=${hint.color ?? 'default'} ?attention=${hint.color === 'invalid'}
>${hint.text}</uui-badge
? html`<umb-badge slot="extra" .color=${hint.color ?? 'default'} ?attention=${hint.color === 'invalid'}
>${hint.text}</umb-badge
>`
: nothing}</uui-tab
>`;
@@ -6,6 +6,7 @@ import type {
UmbInvariantDatasetWorkspaceContext,
UmbRoutableWorkspaceContext,
ManifestWorkspace,
UmbNamableWorkspaceContext,
} from '@umbraco-cms/backoffice/workspace';
import {
UmbSubmittableWorkspaceContextBase,
@@ -26,7 +27,7 @@ type PropertyTypeDataModel = UmbPropertyTypeScaffoldModel;
export class UmbPropertyTypeWorkspaceContext
extends UmbSubmittableWorkspaceContextBase<PropertyTypeDataModel>
implements UmbInvariantDatasetWorkspaceContext, UmbRoutableWorkspaceContext
implements UmbInvariantDatasetWorkspaceContext, UmbRoutableWorkspaceContext, UmbNamableWorkspaceContext
{
// Just for context token safety:
public readonly IS_PROPERTY_TYPE_WORKSPACE_CONTEXT = true;
@@ -62,11 +63,22 @@ export class UmbPropertyTypeWorkspaceContext
this.validationContext = new UmbValidationContext(this);
this.addValidationContext(this.validationContext);
this.observe(this.unique, (unique) => {
if (unique) {
this.validationContext.setDataPath(UmbDataPathPropertyTypeQuery({ id: unique }));
}
});
this.observe(
this.unique,
(unique) => {
if (unique) {
this.validationContext.setDataPath(UmbDataPathPropertyTypeQuery({ id: unique }));
}
},
null,
);
this.observe(
this.name,
(name) => {
this.view.setTitle(name);
},
null,
);
this.#init = this.consumeContext(UMB_CONTENT_TYPE_WORKSPACE_CONTEXT, (context) => {
this.#contentTypeContext = context;
@@ -33,6 +33,10 @@ export class UmbCollectionFilterFieldElement extends UmbLitElement {
static override readonly styles = [
css`
:host {
display: flex;
}
uui-input {
width: 100%;
}
@@ -0,0 +1,72 @@
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { html, customElement, property, css, LitElement, ifDefined } from '@umbraco-cms/backoffice/external/lit';
import type { UUIInterfaceColor, UUIInterfaceLook } from '@umbraco-cms/backoffice/external/uui';
/**
* @element umb-badge
* @description A wrapper for the uui-badge component with position fixed support to go on top of other elements.
* @augments {LitElement}
*/
@customElement('umb-badge')
export class UmbBadgeElement extends LitElement {
/**
* Changes the look of the button to one of the predefined, symbolic looks.
* @type {"default" | "positive" | "warning" | "danger"}
* @attr
* @default "default"
*/
@property({ type: String })
color?: UUIInterfaceColor;
/**
* Changes the look of the button to one of the predefined, symbolic looks.
* @type {"default" | "primary" | "secondary" | "outline" | "placeholder"}
* @attr
* @default "default"
*/
@property({ type: String })
look?: UUIInterfaceLook;
/**
* Bring attention to this badge by applying a bounce animation.
* @type boolean
* @attr
* @default false
*/
@property({ type: Boolean })
attention?: boolean;
override render() {
return html`<uui-badge color=${ifDefined(this.color)} look=${ifDefined(this.look)} ?attention=${this.attention}
><slot></slot
></uui-badge>`;
}
static override styles = [
UmbTextStyles,
css`
:host {
position: absolute;
anchor-name: --umb-badge-anchor;
/** because inset has no effect on uui-badge in this case, we then apply it here: */
inset: var(--uui-badge-inset, -8px -8px auto auto);
}
@supports (position-anchor: --my-name) {
uui-badge {
position: fixed;
position-anchor: --umb-badge-anchor;
z-index: 1;
top: anchor(top);
right: anchor(right);
}
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
'umb-badge': UmbBadgeElement;
}
}
@@ -0,0 +1 @@
export * from './badge.element.js';
@@ -2,6 +2,7 @@
// TODO: we need to move these files into their respective folders/silos. We then need a way for a silo to globally register a component
export * from './backoffice-modal-container/backoffice-modal-container.element.js';
export * from './backoffice-notification-container/backoffice-notification-container.element.js';
export * from './badge/index.js';
export * from './body-layout/body-layout.element.js';
export * from './code-block/index.js';
export * from './dropdown/index.js';
@@ -25,6 +26,6 @@ export * from './multiple-color-picker-input/index.js';
export * from './multiple-text-string-input/index.js';
export * from './popover-layout/index.js';
export * from './ref-item/index.js';
export * from './stack/index.js';
export * from './split-panel/index.js';
export * from './stack/index.js';
export * from './table/index.js';
@@ -189,7 +189,7 @@ export class UmbMultipleColorPickerItemInputElement extends UUIFormControlMixin(
<uui-button
compact
label=${this.localize.term('actions_delete')}
look="primary"
look="outline"
?disabled=${this.disabled}
@click=${this.#onDelete}>
<uui-icon name="icon-trash"></uui-icon>
@@ -120,6 +120,12 @@ export class UmbEntityItemRefElement extends UmbLitElement {
}
}
@property({ type: Boolean })
error?: boolean;
@property({ type: String, attribute: 'error-message', reflect: false })
errorMessage?: string;
#pathAddendum = new UmbRoutePathAddendumContext(this);
#onSelected(event: UmbSelectedEvent) {
@@ -155,6 +161,7 @@ export class UmbEntityItemRefElement extends UmbLitElement {
this._component?.remove();
const component = extensionControllers[0]?.component || document.createElement('umb-default-item-ref');
// TODO: I would say this code can use feature of the UmbExtensionsElementInitializer, to set properties and get a fallback element. [NL]
// assign the properties to the component
component.item = this.#item;
component.readonly = this.readonly;
@@ -182,7 +189,25 @@ export class UmbEntityItemRefElement extends UmbLitElement {
}
override render() {
return html`${this._component}`;
if (this._component) {
return html`${this._component}`;
}
// Error:
if (this.error) {
return html`<uui-ref-node
style="color: var(--uui-color-danger);"
.name=${this.localize.string(this.errorMessage ?? '#general_notFound')}
.readonly=${this.readonly}
.standalone=${this.standalone}
.selectOnly=${this.selectOnly}
.selected=${this.selected}
.disabled=${this.disabled}>
<uui-icon slot="icon" name="icon-alert" style="color: var(--uui-color-danger);"></uui-icon>
<slot name="actions"></slot>
</uui-ref-node>`;
}
// Loading:
return html`<uui-loader-bar style="margin-top:10px;"></uui-loader-bar>`;
}
override destroy(): void {
@@ -211,7 +236,7 @@ export class UmbEntityItemRefElement extends UmbLitElement {
}
:host([drag-placeholder]) {
--uui-color-focus:transparent;
--uui-color-focus: transparent;
}
:host([drag-placeholder])::after {
@@ -233,7 +258,6 @@ export class UmbEntityItemRefElement extends UmbLitElement {
transition: opacity 50ms 16ms;
opacity: 0;
}
`,
];
}
@@ -1,10 +1,12 @@
import { UMB_AUTH_CONTEXT } from './auth/auth.context.token.js';
import { UmbBackofficeNotificationContainerElement, UmbBackofficeModalContainerElement } from './components/index.js';
import { UmbActionEventContext } from './action/action-event.context.js';
import { manifests as coreManifests } from './manifests.js';
import { UmbNotificationContext } from '@umbraco-cms/backoffice/notification';
import { UMB_AUTH_CONTEXT } from './auth/auth.context.token.js';
import { UmbActionEventContext } from './action/action-event.context.js';
import { UmbBackofficeNotificationContainerElement, UmbBackofficeModalContainerElement } from './components/index.js';
import { UmbInteractionMemoryContext } from './interaction-memory/index.js';
import { UmbExtensionsApiInitializer } from '@umbraco-cms/backoffice/extension-api';
import { UmbModalManagerContext } from '@umbraco-cms/backoffice/modal';
import { UmbExtensionsApiInitializer, type UmbEntryPointOnInit } from '@umbraco-cms/backoffice/extension-api';
import { UmbNotificationContext } from '@umbraco-cms/backoffice/notification';
import type { UmbEntryPointOnInit } from '@umbraco-cms/backoffice/extension-api';
import './property-action/components/index.js';
import './menu/components/index.js';
@@ -31,6 +33,7 @@ export const onInit: UmbEntryPointOnInit = (host, extensionRegistry) => {
new UmbNotificationContext(host);
new UmbModalManagerContext(host);
new UmbActionEventContext(host);
new UmbInteractionMemoryContext(host);
host.consumeContext(UMB_AUTH_CONTEXT, (authContext) => {
// Initialize the auth context to let the app context know that the core module is ready
@@ -1,4 +1,4 @@
import type { UmbHintController } from './hints.controller.js';
import type { UmbHintController } from './hint.controller.js';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
export const UMB_HINT_CONTEXT = new UmbContextToken<UmbHintController>('UmbHintContext');
@@ -1,6 +1,6 @@
import type { UmbHint, UmbIncomingHintBase } from '../types.js';
import { UMB_HINT_CONTEXT } from './hint.context-token.js';
import { UmbHintController, type UmbHintControllerArgs } from './hints.controller.js';
import { UmbHintController, type UmbHintControllerArgs } from './hint.controller.js';
import type { UmbPartialSome } from '@umbraco-cms/backoffice/utils';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
@@ -7,7 +7,7 @@ import { UmbArrayState, UmbObjectState, type Observable } from '@umbraco-cms/bac
import type { UmbContextProviderController } from '@umbraco-cms/backoffice/context-api';
export interface UmbHintControllerArgs<HintType extends UmbHint = UmbHint> {
viewAlias?: string;
viewAlias?: string | null;
scaffold?: Partial<HintType>;
}
@@ -16,10 +16,15 @@ export class UmbHintController<
IncomingHintType extends UmbIncomingHintBase = UmbPartialSome<HintType, 'unique' | 'weight' | 'path'>,
> extends UmbControllerBase {
//
#viewAlias?: string;
getViewAlias(): string | undefined {
#viewAlias: string | null;
getViewAlias(): string | null {
return this.#viewAlias;
}
#pathFilter?: (path: Array<string>) => boolean;
setPathFilter(filter: (path: Array<string>) => boolean) {
this.#pathFilter = filter;
}
#scaffold = new UmbObjectState<Partial<HintType>>({});
readonly scaffold = this.#scaffold.asObservable();
#inUnprovidingState?: boolean;
@@ -43,7 +48,7 @@ export class UmbHintController<
constructor(host: UmbControllerHost, args?: UmbHintControllerArgs<HintType>) {
super(host);
this.#viewAlias = args?.viewAlias;
this.#viewAlias = args?.viewAlias ?? null;
if (args?.scaffold) {
this.#scaffold.setValue(args?.scaffold);
}
@@ -82,7 +87,7 @@ export class UmbHintController<
return this.#hints.asObservablePart(fn);
}
descendingHints(viewAlias?: string): Observable<Array<UmbHint> | undefined> {
descendingHints(viewAlias?: string | null): Observable<Array<UmbHint> | undefined> {
if (viewAlias) {
return this.#hints.asObservablePart((hints) => {
return hints.filter((hint) => hint.path[0] === viewAlias);
@@ -92,7 +97,22 @@ export class UmbHintController<
}
}
/**
* @internal
* @param {(path: Array<string>) => boolean} filter - A filter function to filter the hints by their path.
* @returns {Observable<Array<UmbHint> | undefined>} An observable of an array of hints that match the filter.
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
_internal_descendingHintsByFilter(filter: (path: Array<string>) => boolean): Observable<Array<UmbHint> | undefined> {
return this.#hints.asObservablePart((hints) => {
return hints.filter((hint) => filter(hint.path));
});
}
inherit(): void {
if (this.#viewAlias === null && this.#pathFilter === undefined) {
throw new Error('A Hint Controller needs a view alias or pathFilter to be able to inherit from a parent.');
}
this.consumeContext(UMB_HINT_CONTEXT, (parent) => {
this.inheritFrom(parent);
}).skipHost();
@@ -101,13 +121,24 @@ export class UmbHintController<
inheritFrom(parent: UmbHintController | undefined): void {
if (this.#parent === parent) return;
if (this.#viewAlias === null && this.#pathFilter === undefined) {
throw new Error('A Hint Controller needs a view alias or pathFilter to be able to inherit from a parent.');
}
this.#parent = parent;
this.observe(this.#parent?.scaffold, (scaffold) => {
if (scaffold) {
this.#scaffold.update(scaffold as any);
}
});
this.observe(parent?.descendingHints(this.#viewAlias), this.#receiveHints, 'observeParentHints');
if (this.#viewAlias) {
this.observe(parent?.descendingHints(this.#viewAlias), this.#receiveHints, 'observeParentHints');
} else if (this.#pathFilter) {
this.observe(
parent?._internal_descendingHintsByFilter(this.#pathFilter),
this.#receiveHints,
'observeParentHints',
);
}
this.observe(this.hints, this.#propagateHints, 'observeLocalMessages');
}
@@ -1,3 +1,3 @@
export * from './hint.context-token.js';
export * from './hints.context.js';
export * from './hints.controller.js';
export * from './hint.context.js';
export * from './hint.controller.js';
@@ -100,6 +100,10 @@
"name": "icon-backspace",
"file": "delete.svg"
},
{
"name": "icon-badge",
"file": "badge.svg"
},
{
"name": "icon-badge-add",
"file": "circle-plus.svg"
@@ -974,6 +978,10 @@
"file": "hard-drive.svg",
"legacy": true
},
{
"name": "icon-heading",
"file": "heading.svg"
},
{
"name": "icon-heading-1",
"file": "heading-1.svg"
@@ -990,6 +998,14 @@
"name": "icon-heading-4",
"file": "heading-4.svg"
},
{
"name": "icon-heading-5",
"file": "heading-5.svg"
},
{
"name": "icon-heading-6",
"file": "heading-6.svg"
},
{
"name": "icon-headphones",
"file": "headphones.svg"
@@ -2210,6 +2226,10 @@
"name": "icon-tree",
"file": "tree-deciduous.svg"
},
{
"name": "icon-trending-up-down",
"file": "trending-up-down.svg"
},
{
"name": "icon-trophy",
"file": "trophy.svg"
@@ -2411,6 +2431,10 @@
"name": "icon-star",
"file": "star.svg"
},
{
"name": "icon-stretch-horizontal",
"file": "stretch-horizontal.svg"
},
{
"name": "icon-database",
"file": "database.svg"
@@ -74,6 +74,9 @@ path: () => import("./icons/icon-axis-rotation.js"),
name: "icon-backspace",
path: () => import("./icons/icon-backspace.js"),
},{
name: "icon-badge",
path: () => import("./icons/icon-badge.js"),
},{
name: "icon-badge-add",
path: () => import("./icons/icon-badge-add.js"),
},{
@@ -766,6 +769,9 @@ legacy: true,
hidden: true,
path: () => import("./icons/icon-hard-drive.js"),
},{
name: "icon-heading",
path: () => import("./icons/icon-heading.js"),
},{
name: "icon-heading-1",
path: () => import("./icons/icon-heading-1.js"),
},{
@@ -778,6 +784,12 @@ path: () => import("./icons/icon-heading-3.js"),
name: "icon-heading-4",
path: () => import("./icons/icon-heading-4.js"),
},{
name: "icon-heading-5",
path: () => import("./icons/icon-heading-5.js"),
},{
name: "icon-heading-6",
path: () => import("./icons/icon-heading-6.js"),
},{
name: "icon-headphones",
path: () => import("./icons/icon-headphones.js"),
},{
@@ -1798,6 +1810,9 @@ path: () => import("./icons/icon-trash.js"),
name: "icon-tree",
path: () => import("./icons/icon-tree.js"),
},{
name: "icon-trending-up-down",
path: () => import("./icons/icon-trending-up-down.js"),
},{
name: "icon-trophy",
path: () => import("./icons/icon-trophy.js"),
},{
@@ -1965,6 +1980,9 @@ path: () => import("./icons/icon-zoom-out.js"),
name: "icon-star",
path: () => import("./icons/icon-star.js"),
},{
name: "icon-stretch-horizontal",
path: () => import("./icons/icon-stretch-horizontal.js"),
},{
name: "icon-database",
path: () => import("./icons/icon-database.js"),
},{
@@ -0,0 +1 @@
export default `<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.75" class="lucide lucide-badge" viewBox="0 0 24 24"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76"/></svg>`;
@@ -0,0 +1 @@
export default `<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.75" class="lucide lucide-heading-5" viewBox="0 0 24 24"><path d="M4 12h8M4 18V6M12 18V6M17 13v-3h4M17 17.7c.4.2.8.3 1.3.3 1.5 0 2.7-1.1 2.7-2.5S19.8 13 18.3 13H17"/></svg>`;
@@ -0,0 +1 @@
export default `<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.75" class="lucide lucide-heading-6" viewBox="0 0 24 24"><path d="M4 12h8M4 18V6M12 18V6"/><circle cx="19" cy="16" r="2"/><path d="M20 10c-2 2-3 3.5-3 6"/></svg>`;
@@ -0,0 +1 @@
export default `<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.75" class="lucide lucide-heading" viewBox="0 0 24 24"><path d="M6 12h12M6 20V4M18 20V4"/></svg>`;
@@ -0,0 +1 @@
export default `<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.75" class="lucide lucide-stretch-horizontal" viewBox="0 0 24 24"><rect width="20" height="6" x="2" y="4" rx="2"/><rect width="20" height="6" x="2" y="14" rx="2"/></svg>`;
@@ -0,0 +1 @@
export default `<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.75" class="lucide lucide-trending-up-down" viewBox="0 0 24 24"><path d="M14.828 14.828 21 21M21 16v5h-5M21 3l-9 9-4-4-6 6"/><path d="M21 8V3h-5"/></svg>`;
@@ -0,0 +1 @@
export * from './interaction-memory.context.token.js';
@@ -0,0 +1,8 @@
export class UmbInteractionMemoriesChangeEvent extends Event {
public static readonly TYPE = 'interaction-memories-change';
public constructor() {
// mimics the native change event
super(UmbInteractionMemoriesChangeEvent.TYPE, { bubbles: true, composed: false, cancelable: false });
}
}
@@ -0,0 +1,6 @@
export * from './constants.js';
export * from './event/interaction-memories-change.event.js';
export * from './interaction-memory.context.js';
export * from './interaction-memory.manager.js';
export type * from './types.js';
@@ -0,0 +1,6 @@
import type { UmbInteractionMemoryContext } from './interaction-memory.context.js';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
export const UMB_INTERACTION_MEMORY_CONTEXT = new UmbContextToken<UmbInteractionMemoryContext>(
'UmbInteractionMemoryContext',
);

Some files were not shown because too many files have changed in this diff Show More