v11: Merge v10/feature/project-cleanup into v11/dev (#13112)

* New backoffice/add system text json configuration attribute (#12998)

* Add SystemTextJsonConfigurationAttribute

* Fix up formatting

* Rename classes for clearer purpose

Co-authored-by: Zeegaan <nge@umbraco.dk>

* UmbracoPath should no longer be configurable (#13032)

* UmbracoPath should no longer be configurable

* Remove UmbracoPath configuration from all tests

* Only contain style instead of full layout (#13033)

* Only contain style instead of full layout (#13033)

* Fix CodeQL duplicate "permissions"  node and reformat

* add an extra check to ensure the pips exist before adding a class to them

* improve pip classList add/remove with no intermediary variable

* Only contain style instead of full layout (#13033)

* Ensure consistent margin on headings in tree root (#12992)

* Ensure consistent margin on headings in tree root (#12992)

(cherry picked from commit 88bfef9e0d)

* Bump version to 10.2.1

* Translate "User permissions for languages" feature to dutch (#12971)

* Translate 'sectionsHelp' to Dutch

* Translate 'selectLanguages' to Dutch

* Transkate 'allowAccessToAllLanguages' to Dutch

* Translate "User permissions for languages" feature to español (#12975)

* Translate 'selectLanguages' to Español

* Translate 'languagesHelp' to español

* Translate 'allowAccessToAllLanguages' to spanish

* Updated project references for Forms and Deploy in the JsonSchema project. (#13047)

* Updated project references for Forms and Deploy in the JsonSchema project. (#13047)

* UmbracoPath has been removed from the official schema store, remove temporary workaround from our schema generator as well (#13043)

* add an extra check to ensure the pips exist before adding a class to them

* improve pip classList add/remove with no intermediary variable

* pass in parameters needed to member service (#13020)

* Missing methods from IMemberService (#13022)

* Add back methods to interface

* Add default implementations to avoid breaking changes

Co-authored-by: Zeegaan <nge@umbraco.dk>

* New endpoint for web profiling dashboard (#13038)

* Endpoint for web profiling dashboard

* Add profiling API contract

* New Backoffice: Published cache controller (#13034)

* Add published cache controller (endpoints for the Published Status dashboard)

* Update OpenAPI contract for published cache endpoints

* Fix OpenApi spec

Co-authored-by: Zeegaan <nge@umbraco.dk>

* Bug fix for datepicker with offset time (#12847)

* https://github.com/umbraco/Umbraco-CMS/issues/12844

* remove "X" from ng-attr

Doing the test I killed the ng-if attr. But forgot it was there doing the commit

Co-authored-by: Lucas Bach Bisgaard <lom@novicell.dk>

* Make sure swagger tags and operations are sorted alphabetically (#13055)

* Add spellcheck false to password inputs (#13059)

* Add null check for variants in Grid Layout (#13060)

This fixes a regression from 10.2.0 where the `variants` property was removed.

* Add null check for variants in Grid Layout (#13060)

This fixes a regression from 10.2.0 where the `variants` property was removed.

* Fixes #12972 for validating legacy member passwords (#12973)

* Fixes #12972 for validating legacy member passwords

* Removed unused variable

* removed unused variable

* Fix issue toggling boolean between true/false after Save without refreshing

* New backoffice: examine management controller (#12981)

* Add ExamineManagementControllerBase

* Add ExamineIndexModelFactory

* Add IndexesExamineManagementController

* Add proper attributes

* Implement ExamineIndexViewModel.cs

* formatting

* Add comment about it working in .NET 7

* Add SearchersExamineManagementController.cs

* Update comments about why it might throw errors

* Add SearchResultViewModel

* Add SearchExamineManagementController

* Add ExamineSearcherValidationService

* Rename ExamineSearcherValidationService.cs to ExamineSearcherFinderService.cs

* Rename interface aswell

* Add SearchExamineManagementController

* Refactor ExamineSearcherFinderService

* Add HasIndexRebuiltExamineManagementController.cs

* Fix up formatting

* Async actions

* Add RebuildIndexExamineManagementController.cs

* Rename IExamineIndexModelFactory to IExamineIndexViewModelFactory

* Refactor HasIndexRebuilt endpoint to Index endpoint

* Remove unused usings

* Fix up DetailsExplanation

* Create dedicated SearchersViewModel

* Rename action

* Rename RebuildIndex to Rebuild

* Dont have changes in ExamineIndexModel

* Make values strongly typed instead of generic lists of strings

* Rename to non-plural

* Rename to non-plural

* Rename controller

* Introduce ITemporaryIndexingService

* Introduce ITemporaryIndexingService

* Add SearcherName to view model

* Move to new ExamineManagementControllerBase.cs

* Refactor ExamineManagerService

* Use init instead of setters

* Make properties explicitly on models

* Add DI

* Apply suggestions from code review

Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>

* Rename to IndexExamineManagementController

* Return ViewModel instead of exception

* Make view models non-nullable

* Add examine management extension point

* Rename to IndexingRebuilderService

* Move rebuild logic to service

* Fix up usages in IIndexingRebuilderService

* Fix up DI

* Fix OpenApi contract

* Implement CanRebuild on IIndexingRebuilderService.cs

Co-authored-by: Zeegaan <nge@umbraco.dk>
Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>

* Update build script

* Add BuildProjectReferences=false to dotnet pack

* Internalize Umbraco.Cms.ManagementApi references

* Make Searchers endpoint return ActionResult (#13068)

* New backoffice - trees design (#12963)

* Refactor: Add default versioned back office route attribute

* Tree controller bases and first draft implementations for document, media and doctype

* Move tree item view models to appropriate location

* Fix missing parent

* Refactor user entity access for testability

* A bit of clean-up + handle user start nodes for items endpoint

* Implement foldersOnly for folder tree

* Items endpoint for document type tree

* Strongly typed action results

* Content + media recycle bin

* Correct return type for swagger

* Member type tree

* Rename user start node handling to make a little more sense

* Revert to faked admin start nodes in document tree

* Media type tree

* Data type tree

* Relation type tree

* Remove unused dependency from member type tree

* Correct documentation for member type tree endpoint response types

* Use icon constants

* Add templates tree

* Member group tree

* Document blueprint tree

* Partial views, scripts and stylesheets trees

* Static files tree

* Clarify "folders only" state

* Comments and improved readability

* Rename TreeControllerBase and TreeItemViewModel

* Move recycle bin controller base to its own namespace

* Moved tree base controllers to their own namespace

* Common base class for tree view models

* Remove ProblemDetails response type declaration from all actions

* Add OpenApiTag

* Various review comments

* Dictionary item tree

* Renamed all tree controllers to follow action/feature naming convention

* Handle client culture state for document tree

* Support "ignore user start nodes" for content and media + refactor how tree states work to make things more explicit

* Fix or postpone a few TODOs

* Make entity service able to paginate trashed children

* Handle sorting explicitly

* Re-apply VersionedApiBackOfficeRoute to install and upgrade controllers after merge

* Use PagedViewModel instead of PagedResult for all trees

* Explain the usage of UmbracoObjectTypes.Unknown

* Introduce and apply GetMany pattern for dictionary items

* Add a note about relation type caching

* Fix broken test build + add unit tests for new localization service methods

* Use new management API controller base

* Entity repository should build document entities for document blueprints when getting paged entities (same as it does when getting specific entities)

* Use Media type for Media recycle bin

Co-authored-by: Mole <nikolajlauridsen@protonmail.ch>

* Move shared relation service to concrete implementations

* Use inclusive language

* Add 401 response type documentation to applicable trees

* Refactor entity load for folder tree controller base + ensure that folders are only included in the first result page

* Add (in-memory) pagination to dictionary tree

* Make file system controller honor paging parameters

* Support pagination in relation type tree

* Clarify method name a bit for detecting tree root path requests

* Update Open API schema to match new trees

* Move from page number and page size to skip/take (with temporary workaround for lack of property skip/take pagination in current DB implementation)

* Update OpenAPI schema to match skip/take

* Update OpenAPI schema

* Don't return paginated view models from "items" endpoints

* Update OpenApi schema

Co-authored-by: Mole <nikolajlauridsen@protonmail.ch>

* Use pattern matching to check if items is not `JArray`

* Bump Smidge up to v4.1.1

* Removing X-XSS-Protection healthcheck

* Redirect to a return URL if one is present on the querystring when logging into the back office

* Fix issue 13023 - Cannot read properties of undefined (reading 'allowedActions')

* Implement playwright acceptance tests instead of cypress (#13069)

* fix up dependencies in package.json

* Change configs to playwright

* update types

* remove cypress

* add playwright

* Fix up imports

* Move up test files

* Fix up scripts

* Update user import

* Remove allowEditInvariantFromNonDefault=True.spec.ts

* Update docker container

* Run playwright on pipelines

* Install playwright

* change urls

* change to run playwright

* Update reporter

* create .env file when installing

* update pipelines

* Remove @in yml

* Update Yaml script to use New-Item

* Pipe object to Value

* Update yaml to use "" not {}

* Update localhost to proper port

* Push package-lock.json

* include dotenv in package.json

* Add back umbraco.config.ts

* Dont change launchSettings.json

* Fix up pipelines

* Change working directory

* Add logging

* Actually name the file

* Remove .env from path

* Add working directory

* Add working directory to script

* check env content

* Update more working dir

* Try making newline in YAML

* add quotes

* Try multiline infront of script

* Move top statement

* use https

* Update to localhost 44331

* ignore https in config

* Change linux to https

* add timeouts for tests

* Fix up url for linux

* Update docker to use port 44331 -> 5000

* increase timeout

* Update yaml

* Remove Cypress references and fix URL/port mapping

* Update umbraco-linux.docker

* Generate ASP.NET HTTPS development certificate

* Enable HTTPS development certificate in Docker

* Dont run failing tests

* Update HTTPS development certificate generation

* Copy nuget.config to correct location in Docker file

* do not run flaky test

* update outputdir

* Remove flaky tests

* Update to dot reporter

* Update to json-models-builders package

* Check if results folder exists

* Remove logging

* Use bash to find folder

* Dont use junit to report

* only publish if folder exists

* Add 5 retries for CI

* search correct folder

* Remove unused json file

* Use line reporter

* Remove umbraco.config.ts

* Remove more flaky test

* Add waits so we dont bombard SQLite

* Add page as parameter

* add page as parameter

* Remove flaky macro test

Co-authored-by: Zeegaan <nge@umbraco.dk>
Co-authored-by: Ronald Barendse <ronald@barend.se>

* Fix comment of view property in `ConfigurationFieldAttribute` (#13077)

* Fix comment of view property in `ConfigurationFieldAttribute`

* Update description of key property as well

* Use char instead of string for DetectJson (#13019)

* Use char instead of string for DetectJson

* Use faster method

* Change DetectIsJon method

* Update acceptance test readme & scripts (#13078)

* Update readme to reflect the change to playwright

* Update test scripts

* update README

* update pipelines to run new script

* update package.json scripts

* dont include demo test in package.json

* Add creation of blueprint test

* Implement create test script

Co-authored-by: Zeegaan <nge@umbraco.dk>

* Refactor event handler away from keyup to ng-change

* Don't use legacy icon for action delete

* Added nullability attribute to IsNullOrWhitespace

* Fixes umbraco/Umbraco-CMS.Accessibility.Issues #63 and #61

* Revert "Removing X-XSS-Protection healthcheck" (#13096)

This reverts commit 696475ebf2.

Co-authored-by: Zeegaan <nge@umbraco.dk>

* New backoffice: New Api controllers (#12983)

* Create migrate Language controller to Umbraco.Cms:ManagementApi

* Add proper language mapping

* Update mapping to handle if language name is null

* Uncomment code

* Add new language view model

* Add LanguageViewModelMapping

* Add mapper registration

* Fix up AddMappers extension method

* Implement mapping IEnumerable of languages

* Change action signatures to ViewModel instead of model

* Seperate logic from controller into service

* Move LanguageService.cs

* Register service

* Fix up mapping

* Add null check to mapping instead of controller

* Map to ILanguage instead of implementation

* Fix up null check

* Implement ProblemDetailsBuilder.cs

* Rename duplicate method

* Use builder in actions

* Implement new Paged models

* Create language controller base

* Use pagedModel for GetAllLanguages

* Create GetAllLanguagesPaged method

* Split language controller into single APIs

* Fix up controllers with API versions

* Map Total property

* Fix up route and naming for GetLanguageController.cs

* Fix up naming for language controllers

* New folder structure

* Add culture controllers

* Map CultureInfos to paged CultureViewModel

* Remove wierd include in csproj

* Refactor controller to return pagedViewModel instead of dictionary

* Fix up mapping to map single and enumerable

* Register mapping

* Add apiversion to controller

* Add inheritdoc

* Create DictionaryControllerBase.cs

* Add delete controller

* Only use HttpDelete for deletes

* Check also if language exists in service

* Split Save action into Create & Update actions

* Update Http attributes on Create and update controllers

* Proper routing for delete controller

* Add api version

* Make action async

* Implement CreateDictionaryController

* Create DictionaryViewModel.cs

* Use viewmodel instead of values

* Create get by int DictionaryController

* Add view models

* Rename controller

* Rename DictionaryViewModel to DictionaryItemViewModel

* use created instead of ID

* Apply DataContract/Datamember to view model

* change to guid instead of Guid

* Use proper responses instead of return models when creating/updating

* Implement new IDictionaryService

* Implement new MoveController

* Use new service in mapper

* Remove unused method

* Add DictionaryViewModelMapDefinition

* Create MoveOrCopyViewModel

* Proper Http action

* Create UpdateDictionary controller

* Map IDictionaryItem to DictionaryViewModel

* Add JsonPatching

* Add UpdateDictionaryController.cs

* Map DictionaryTranslationsDisplays properly

* ParentId should be nullable

* Add new DictionaryTranslationViewModel.cs

* Remove translationViewModel

* Add Id and Key to DictionaryTranslationDisplay.cs

* Implement IDictionaryFactory.cs

* Create DictionaryViewModels and do not use IEntity

* Map to new view models instead of displays

* Register the factiories in the service container

* Remove newtonsoft

* Add serializing to and from PatchDocs

* Use JsonPatchViewModel instead of object type

* Add JsonPatch.Net to csproj

* Implement JsonPatchService.cs

* Register JsonPatchService

* Make model non-nullable

* Update controllers to use new attributes

* Rename MoveViewModel.cs

* Remove NameIsDirty as that is legacy from how we used to handle updating

* Add GetAllDictionaryController

* Add todo to DictionaryControllerBase

* Add ExportDictionaryController

* Add ImportDictionaryController

* Remove unused umbraco mapper

* Add upload dictionary controller

* Create Dictionary import view models

* Update UploadDictionaryController with view models

* Remove unused using

* Implement pagedviewmodel for GetAllDictionaryController.cs

* Add dictionary overview viewmodels

* Add mapping for DictionaryOverViewModel

* Update Dictionary controller to use new viewmodel

* Fix up attributes for UploadDictionaryController

* Make actions async

* Make controller bases abstract

* Fix after merge

* New backoffice: Analytics controller (#12957)

* Add AnalyticsControllerBase

* Add AnalyticsViewModel

* Add GetAnalyticsController

* Update ViewModel to use System.Text.Json

* Add SetAnalyticsController

* Add AnalyticsLevelViewModel

* Add GetAllAnalyticsController

* Add viewmodel factory

* Register factory for DI

* Use factory for creation of ViewModel

* Fix up AnalyticsLevelViewModel.cs

* Use analyticsLevelViewModel

* Add Enum validation to controller

* Add OpenApi attributes

* Add routing to GetAllAnalyticsController

* Rename IPagedViewModelFactory

* use new renamed PagedViewModelFactory

* Make actions async

* Make controller base abstract

* Fix up after merge

Co-authored-by: Zeegaan <nge@umbraco.dk>

* New backoffice: Help controller (#12958)

* Add HelpControllerBase

* Add HelpPageViewModel

* Add GetHelpController

* Add viewmodel factory

* Register factory for DI

* Use PagedViewModelFactory for controller

* Update baseurl to be nullable

* Rename IPagedViewModelFactory

* Use new renamed IPagedViewModelFactory

* Dont use httpclient as field

Co-authored-by: Zeegaan <nge@umbraco.dk>

* New backoffice: Relation controller (#12961)

* Add relation controller base

* Add commen about auth

* Add GetRelationController

* Use mapping to viewmodel

* Add RelationViewModel

* Add RelationViewModelsMapDefinition.cs

* Add viewmodel factory

* Register factory for DI

* Rename IPagedViewModelFactory

* Add RelationViewModelFactory

* Remove unused service

* Add GetByChildRelationController

* Add relationViewModelFactory to DI

* Add MappingBuilderExtensions

* Add relationViewModelsMapDefinition to DI

* Use PagedViewModel for child relations

* Add CreateMultiple

* Update GetRelationController to use factory instead of direct mapping

* Update GetByChildRelationController to use relationViewModelFactory

* Fix up after merge

Co-authored-by: Zeegaan <nge@umbraco.dk>

* New backoffice: Tracked references controller (#12966)

* Add TrackedReferencesControllerBase.cs

* Add GetTrackedReferencesController

* Add relation model

* Add ITrackedReferencesRepository.cs

* Rename relation to RelationModel

* Add RelationMapDefinition.cs

* Add TrackedReferencesSkipTakeRepository

* Rename RelationModelMapDefinition

* Add new services to DI

* Rename RelationModel to RelationItemModel

* Implement TrackedReferencesSkipTakeService

* Formatting

* Add RelationItemViewModel.cs

* Add mapdefition

* Update TrackedReferencesController to use new PagedViewModel

* Add service to DI

* imlement proper routing

* Add async to GetTrackedReferencesController.cs

* Rename action to get

* Add DescendantTrackedReferencesController

* make filterMustBeIsDependency nullable

* Use count instead of capacity

* Rename controller

* Add MultipleTrackedReferencesController.cs

* Refactor TrackedReferencesService to not return pagedViewModel

* Remove TODO untill consensus on convention

* Formatting

* Delete old duplicate controller

* Fix up naming

* Fix up naming and fixed todo

* Fix up mapdefinition registrations

* Rename controllers

* Fix naming

* Fix nullable tree

* Fix up relation controller & action names

* Fix naming

* Fix up multiple to not be post

* Apply suggestions from code review

Co-authored-by: Mole <nikolajlauridsen@protonmail.ch>

* Remove [ApiVersion] from each individual controller and added to base instead

* Added missing semi-colon

* Update all "non-async async" endpoints to return Task.FromResult

* Fixed up LanguageViewModels namespace

* Return proper status code

* Update xml documentation

* Rename GetAllCultureController

* Change return type attribute to proper Model

* Change return type attribute to correct model

* Add clarifying comment

* Change return type attribute to proper model

* Rename ById to by key

* Update DictionaryOverviewViewModel to use Key instead of ID

* Implement factory method to create viewModel instead of using services in mapper

* Fix up DictionaryItemViewModel to use Parent GUID instead of ID

* Update return type attribute to correct model

* Update key to actual GUID instead of string

* Update route to not include "delete"

* Remove redundant IActionResult specification

* Update responseType attribute to correct models

* Update OpenApiTag for DictionaryControllerBase

* Update ResponseType attribute to correct models

* update variable name to not be "XD"

* Update ResponseType attribute to correct model

* Update route to not include "update" as it is redundant

* Update produces responsetype attributes to correct models

* Use IJsonSerializer abstraction instead of JsonConvert directly

* Remove unused field

* Change ResponseType attribute to correct model

* change TrackedReferencesSkipTakeService to return pagedModels directly

* Remove duplicate DI

* change to better variable names

* Move RelationItemModel to core

* Remove empty folder

* Remove ITrackedReferencesSkipTakeService and add it to TrackedReferencesService

* remove TrackedReferencesSkipTakeRepository and add to TrackedReferencesRepository.cs

* Fix up TrackedReferencesService to not use SkipTake repository

* Remove AddRepositories from ManagementApiComposer

* Transition to ManagementApiBaseController

* Fixes ResponseType attribute to correct model

* Add loading files to service instead of having logic in controller

* Add todo comment

* Fix up routing for delete language

* Use problem details builder

* Add SystemTextJsonSerializer

* Add SystemTextJsonSerializer

* Remove unused usings

* Remove obsolete MoveDictionaryController

* Remove CreateDate and UpdateDate from DictionaryViewModel

* Change response type to correct model

* Remove PagedViewModelFactory.cs

* Add obsolete message

* Fix installer view models

A setter is required for the modelbinder to be able to do its work

* Update src/Umbraco.Cms.ManagementApi/Controllers/Analytics/SetAnalyticsController.cs

Co-authored-by: Mole <nikolajlauridsen@protonmail.ch>

* Update ResponseType to correct Model

* Update comment

* Add FileUploadService

* Add DictionaryFactory.cs to handle creation of viewmodel

* Remove unused DI

* Rename actions & controllers to reflect eachother

* Update OpenApi.json

* Add dictionary to openapi

* Update in proper alphabetical order

* Add trackedReferences to openapi

* Fix open api doc

Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>
Co-authored-by: Zeegaan <nge@umbraco.dk>

* Backport project cleanup from #12907

* Remove empty Directory.Build.props

* Fix GenerateAppsettingsSchema target

* Re-add empty Directory.Build.props to prevent inheritance

* Re-add missing JsonPatch.Net dependency

* Fix merge issues (redundant TargetFramework property and appsettings-schema.json generation)

* Fix and improve OpenAPI test assertion

Co-authored-by: Sebastiaan Janssen <sebastiaan@umbraco.com>
Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>
Co-authored-by: Zeegaan <nge@umbraco.dk>
Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>
Co-authored-by: Mads Rasmussen <madsr@hey.com>
Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Co-authored-by: Bjarne Fyrstenborg <bjarne_fyrstenborg@hotmail.com>
Co-authored-by: Erik-Jan Westendorp <erikjanwestendorp@outlook.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
Co-authored-by: Sean <29239704+Bakersbakebread@users.noreply.github.com>
Co-authored-by: Lucas Bach Bisgaard <rammi@rammi.dk>
Co-authored-by: Lucas Bach Bisgaard <lom@novicell.dk>
Co-authored-by: Mole <nikolajlauridsen@protonmail.ch>
Co-authored-by: Busra Sengul <aysebusrasengul@gmail.com>
Co-authored-by: Justin Neville <justin@nevitech.co.uk>
Co-authored-by: Jeavon Leopold <jeavon@crumpled-dog.com>
Co-authored-by: Austin Gilbert <AGilbert@rbaconsulting.com>
Co-authored-by: patrickdemooij9 <patrickdemooij98@hotmail.com>
Co-authored-by: bakersbakebread <hello@seanthorne.co.uk>
Co-authored-by: Karl Butler <kbutler@carbonsix.digital>
This commit is contained in:
Ronald Barendse
2022-10-05 12:14:43 +02:00
committed by GitHub
parent c9f6d6059d
commit 6dc874147f
389 changed files with 16083 additions and 8015 deletions

View File

@@ -2,29 +2,38 @@ name: "Code scanning - action"
on:
push:
branches: ['*/dev','*/contrib']
branches:
- '*/dev'
- '*/contrib'
pull_request:
# The branches below must be a subset of the branches above
branches: ['*/dev','*/contrib']
branches:
- '*/dev'
- '*/contrib'
permissions:
contents: read
env:
dotnetVersion: 7.x
dotnetIncludePreviewVersions: true
solution: umbraco.sln
buildConfiguration: SkipTests
DOTNET_NOLOGO: true
DOTNET_GENERATE_ASPNET_CERTIFICATE: false
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
jobs:
CodeQL-Build:
permissions:
actions: read # for github/codeql-action/init to get workflow details
contents: read # for actions/checkout to fetch code
security-events: write # for github/codeql-action/analyze to upload SARIF results
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
steps:
- name: Checkout repository
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
@@ -35,14 +44,17 @@ jobs:
with:
config-file: ./.github/config/codeql-config.yml
- name: Setup dotnet
- name: Use .NET ${{ env.dotnetVersion }}
uses: actions/setup-dotnet@v2
with:
dotnet-version: '7.x'
include-prerelease: true
dotnet-version: ${{ env.dotnetVersion }}
include-prerelease: ${{ env.dotnetIncludePreviewVersions }}
- name: dotnet build
run: dotnet build umbraco.sln -c SkipTests
- name: Run dotnet restore
run: dotnet restore ${{ env.solution }}
- name: Run dotnet build
run: dotnet build ${{ env.solution }} --configuration ${{ env.buildConfiguration }} --no-restore -p:ContinuousIntegrationBuild=true
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project>
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Company>Umbraco HQ</Company>
<Authors>Umbraco</Authors>
<Copyright>Copyright © Umbraco $([System.DateTime]::Today.ToString('yyyy'))</Copyright>

View File

@@ -25,6 +25,7 @@ parameters:
variables:
nodeVersion: 16.17.0
dotnetVersion: 7.x
dotnetIncludePreviewVersions: true
solution: umbraco.sln
buildConfiguration: Release
UMBRACO__CMS__GLOBAL__ID: 00000000-0000-0000-0000-000000000042
@@ -72,7 +73,7 @@ stages:
inputs:
version: $(dotnetVersion)
performMultiLevelLookup: true
includePreviewVersions: true
includePreviewVersions: $(dotnetIncludePreviewVersions)
- task: DotNetCoreCLI@2
displayName: Run dotnet restore
inputs:
@@ -110,7 +111,7 @@ stages:
}
}
dotnet pack $(solution) --configuration $(buildConfiguration) --no-build --output $(Build.ArtifactStagingDirectory)/nupkg
dotnet pack $(solution) --configuration $(buildConfiguration) --no-build -p:BuildProjectReferences=false --output $(Build.ArtifactStagingDirectory)/nupkg
- script: |
sha="$(Build.SourceVersion)"
sha=${sha:0:7}
@@ -246,7 +247,7 @@ stages:
inputs:
version: $(dotnetVersion)
performMultiLevelLookup: true
includePreviewVersions: true
includePreviewVersions: $(dotnetIncludePreviewVersions)
- task: DotNetCoreCLI@2
displayName: Run dotnet test
inputs:
@@ -283,7 +284,7 @@ stages:
inputs:
version: $(dotnetVersion)
performMultiLevelLookup: true
includePreviewVersions: true
includePreviewVersions: $(dotnetIncludePreviewVersions)
- task: DotNetCoreCLI@2
displayName: Run dotnet test
inputs:
@@ -293,7 +294,7 @@ stages:
testRunTitle: Integration Tests SQLite - $(Agent.OS)
env:
Tests__Database__DatabaseType: 'Sqlite'
Umbraco__Cms__global__MainDomLock: 'FileSystemMainDomLock'
Umbraco__CMS__Global__MainDomLock: 'FileSystemMainDomLock'
# Integration Tests (SQL Server)
- job:
@@ -336,12 +337,11 @@ stages:
env:
Tests__Database__DatabaseType: $(testDb)
Tests__Database__SQLServerMasterConnectionString: $(connectionString)
Umbraco__Cms__global__MainDomLock: 'SqlMainDomLock'
Umbraco__CMS__Global__MainDomLock: 'SqlMainDomLock'
- stage: E2E
variables:
npm_config_cache: $(Pipeline.Workspace)/.npm_e2e
CYPRESS_CACHE_FOLDER: $(Pipeline.Workspace)/cypress_binaries
displayName: E2E Tests
dependsOn: Build
jobs:
@@ -352,9 +352,9 @@ stages:
- name: Umbraco__CMS__Unattended__InstallUnattended # Windows only
value: true
- name: Umbraco__CMS__Unattended__UnattendedUserName # Windows only
value: Cypress Test
value: Playwright Test
- name: Umbraco__CMS__Unattended__UnattendedUserEmail # Windows only
value: cypress@umbraco.com
value: playwright@umbraco.com
- name: Umbraco__CMS__Unattended__UnattendedUserPassword # Windows only
value: UmbracoAcceptance123!
- name: Umbraco__CMS__Global__InstallMissingDatabase # Windows only
@@ -362,11 +362,11 @@ stages:
- name: UmbracoDatabaseServer # Windows only
value: (LocalDB)\MSSQLLocalDB
- name: UmbracoDatabaseName # Windows only
value: Cypress
value: Playwright
- name: ConnectionStrings__umbracoDbDSN # Windows only
value: Server=$(UmbracoDatabaseServer);Database=$(UmbracoDatabaseName);Integrated Security=true;
- name: CYPRESS_BASE_URL
value: http://localhost:8080
- name: PLAYWRIGHT_BASE_URL
value: https://localhost:8443
strategy:
matrix:
Linux:
@@ -375,6 +375,7 @@ stages:
dockerImageName: umbraco-linux
Windows:
vmImage: 'windows-latest'
DOTNET_GENERATE_ASPNET_CERTIFICATE: true # Automatically generate HTTPS development certificate on Windows
pool:
vmImage: $(vmImage)
steps:
@@ -395,17 +396,15 @@ stages:
"npm_e2e" | "$(Agent.OS)"
"npm_e2e"
path: $(npm_config_cache)
- task: Cache@2
displayName: Cache cypress binaries
inputs:
key: '"cypress_binaries" | "$(Agent.OS)" | $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/package-lock.json'
path: $(CYPRESS_CACHE_FOLDER)
- task: PowerShell@2
displayName: Generate Cypress.env.json
displayName: Generate .env
inputs:
targetType: inline
script: >
@{ username = "$(Umbraco__CMS__Unattended__UnattendedUserEmail)"; password = "$(Umbraco__CMS__Unattended__UnattendedUserPassword)" } | ConvertTo-Json | Set-Content -Path "tests/Umbraco.Tests.AcceptanceTest/cypress.env.json"#
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/
script: |
New-Item -Path "." -Name ".env" -ItemType "file" -Value "UMBRACO_USER_LOGIN=$(Umbraco__CMS__Unattended__UnattendedUserEmail)
UMBRACO_USER_PASSWORD=$(Umbraco__CMS__Unattended__UnattendedUserPassword)
URL=$(PLAYWRIGHT_BASE_URL)"
- script: npm ci --no-fund --no-audit --prefer-offline
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/
displayName: Run npm ci
@@ -420,7 +419,7 @@ stages:
inputs:
version: $(dotnetVersion)
performMultiLevelLookup: true
includePreviewVersions: true
includePreviewVersions: $(dotnetIncludePreviewVersions)
# Linux containers smooth
- task: PowerShell@2
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
@@ -433,10 +432,13 @@ stages:
docker build -t $(dockerImageName):$sha -f $(dockerfile) .
mkdir -p $(Build.ArtifactStagingDirectory)/docker-images
docker save -o $(Build.ArtifactStagingDirectory)/docker-images/$(dockerImageName).$sha.tar $(dockerImageName):$sha
docker run --name $(dockerImageName) -dp 8080:5000 -e UMBRACO__CMS__GLOBAL__ID=$(UMBRACO__CMS__GLOBAL__ID) $(dockerImageName):$sha
# Manually generate HTTPS development certificate on Linux
dotnet dev-certs https -ep ${HOME}/.aspnet/https/aspnetapp.pfx -p UmbracoAcceptance123!
dotnet dev-certs https --trust
docker run --name $(dockerImageName) -dp 8080:5000 -dp 8443:5001 -e UMBRACO__CMS__GLOBAL__ID=$(UMBRACO__CMS__GLOBAL__ID) -e ASPNETCORE_Kestrel__Certificates__Default__Password="UmbracoAcceptance123!" -e ASPNETCORE_Kestrel__Certificates__Default__Path=/https/aspnetapp.pfx -v ${HOME}/.aspnet/https:/https/ $(dockerImageName):$sha
docker ps
# Windows containers take forever.
# --no-launch-profile stops ASPNETCORE_ENVIRONMENT=Development which breaks the users.ts tests (smtp config = invite user button)
# Urls matching docker setup.
- task: PowerShell@2
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
@@ -446,10 +448,10 @@ stages:
targetType: inline
script: |
dotnet new --install ./nupkg/Umbraco.Templates.*.nupkg
dotnet new umbraco --name Cypress -o . --no-restore
dotnet new umbraco --name Playwright --no-restore --output .
dotnet restore --configfile ./nuget.config
dotnet build --configuration $(buildConfiguration) --no-restore
Start-Process -FilePath "dotnet" -ArgumentList "run --configuration $(buildConfiguration) --no-build --no-launch-profile --urls $(CYPRESS_BASE_URL)"
Start-Process -FilePath "dotnet" -ArgumentList "run --configuration $(buildConfiguration) --no-build --no-launch-profile --urls $(PLAYWRIGHT_BASE_URL)"
- task: PowerShell@2
displayName: Wait for app
inputs:
@@ -457,31 +459,33 @@ stages:
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
script: |
npm i -g wait-on
wait-on -v --interval 1000 --timeout 120000 $(CYPRESS_BASE_URL)
wait-on -v --interval 1000 --timeout 120000 $(PLAYWRIGHT_BASE_URL)
- task: PowerShell@2
displayName: Run Cypress (Desktop)
displayName: Install Playwright
inputs:
targetType: inline
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
script: npx playwright install
- task: PowerShell@2
displayName: Run Playwright (Desktop)
continueOnError: true
inputs:
targetType: inline
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
script: 'npm run test -- --reporter junit --reporter-options "mochaFile=results/test-output-D-[hash].xml,toConsole=true" --config="viewportHeight=1600,viewportWidth=2560,screenshotsFolder=cypress/artifacts/desktop/screenshots,videosFolder=cypress/artifacts/desktop/videos,videoUploadOnPasses=false"'
- task: PublishTestResults@2
displayName: Publish test results
condition: always()
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: 'tests/Umbraco.Tests.AcceptanceTest/results/test-output-D-*.xml'
mergeTestResults: true
testRunTitle: "e2e - $(Agent.OS)"
script: 'npm run test --ignore-certificate-errors'
- bash: |
if [ -f $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/results/ ]; then
echo "##vso[task.setVariable variable=myfileexists]true"
fi
- task: CopyFiles@2
displayName: Prepare artifacts
condition: always()
condition: eq(variables.myfileexists, 'true')
inputs:
sourceFolder: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/cypress/artifacts
targetFolder: $(Build.ArtifactStagingDirectory)/cypresss
sourceFolder: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/results/
targetFolder: $(Build.ArtifactStagingDirectory)/playwright
- task: PublishPipelineArtifact@1
displayName: "Publish test artifacts"
condition: always()
condition: eq(variables.myfileexists, 'true')
inputs:
targetPath: $(Build.ArtifactStagingDirectory)
artifact: 'E2E artifacts - $(Agent.OS) - Attempt #$(System.JobAttempt)'
@@ -506,8 +510,8 @@ stages:
inputs:
artifact: nupkg
path: $(Build.ArtifactStagingDirectory)/nupkg
- task: DotNetCoreCLI@2
displayName: dotnet restore
- task: NuGetCommand@2
displayName: NuGet push
inputs:
command: restore
projects: $(solution)
@@ -533,8 +537,8 @@ stages:
inputs:
artifact: nupkg
path: $(Build.ArtifactStagingDirectory)/nupkg
- task: DotNetCoreCLI@2
displayName: dotnet restore
- task: NuGetCommand@2
displayName: NuGet push
inputs:
command: restore
projects: $(solution)

View File

@@ -1,6 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<OutputType>Exe</OutputType>
<IsPackable>false</IsPackable>
<EnablePackageValidation>false</EnablePackageValidation>
@@ -13,7 +12,7 @@
<ItemGroup>
<ProjectReference Include="..\Umbraco.Core\Umbraco.Core.csproj" />
<PackageReference Include="Umbraco.Deploy.Core" Version="10.0.0" />
<PackageReference Include="Umbraco.Forms.Core" Version="10.0.0" />
<PackageReference Include="Umbraco.Deploy.Core" Version="10.1.0" />
<PackageReference Include="Umbraco.Forms.Core" Version="10.1.0" />
</ItemGroup>
</Project>

View File

@@ -1,11 +1,10 @@
// Copyright (c) Umbraco.
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NJsonSchema.Generation;
using Umbraco.Cms.Core.Configuration.Models;
namespace JsonSchema
{
@@ -50,6 +49,9 @@ namespace JsonSchema
{
NJsonSchema.JsonSchema schema = _innerGenerator.Generate(typeof(AppSettings));
// TODO: when the "UmbracoPath" setter is removed from "GlobalSettings" (scheduled for V12), remove this line as well
schema.Definitions["UmbracoCmsCoreConfigurationModelsGlobalSettings"]?.Properties?.Remove(nameof(GlobalSettings.UmbracoPath));
return JsonConvert.DeserializeObject<JObject>(schema.ToJson())!;
}
}

View File

@@ -2,7 +2,6 @@
<PropertyGroup>
<Title>Umbraco CMS - Imaging - ImageSharp</Title>
<Description>Adds imaging support using ImageSharp/ImageSharp.Web to Umbraco CMS.</Description>
<TargetFramework>net7.0</TargetFramework>
<!-- TODO: Enable when final version is shipped (because there's currently no previous version) -->
<EnablePackageValidation>false</EnablePackageValidation>
</PropertyGroup>

View File

@@ -0,0 +1,45 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Umbraco.Cms.ManagementApi.Builders;
public class ProblemDetailsBuilder
{
private string? _title;
private string? _detail;
private int _status = StatusCodes.Status400BadRequest;
private string? _type;
public ProblemDetailsBuilder WithTitle(string title)
{
_title = title;
return this;
}
public ProblemDetailsBuilder WithDetail(string detail)
{
_detail = detail;
return this;
}
public ProblemDetailsBuilder WithStatus(int status)
{
_status = status;
return this;
}
public ProblemDetailsBuilder WithType(string type)
{
_type = type;
return this;
}
public ProblemDetails Build() =>
new()
{
Title = _title,
Detail = _detail,
Status = _status,
Type = _type ?? "Error",
};
}

View File

@@ -0,0 +1,22 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.ManagementApi.ViewModels.Pagination;
namespace Umbraco.Cms.ManagementApi.Controllers.Analytics;
public class AllAnalyticsController : AnalyticsControllerBase
{
[HttpGet("all")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<TelemetryLevel>), StatusCodes.Status200OK)]
public async Task<PagedViewModel<TelemetryLevel>> GetAll(int skip, int take)
{
TelemetryLevel[] levels = Enum.GetValues<TelemetryLevel>();
return await Task.FromResult(new PagedViewModel<TelemetryLevel>
{
Total = levels.Length,
Items = levels.Skip(skip).Take(take),
});
}
}

View File

@@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Umbraco.New.Cms.Web.Common.Routing;
namespace Umbraco.Cms.ManagementApi.Controllers.Analytics;
[ApiController]
[BackOfficeRoute("api/v{version:apiVersion}/analytics")]
[OpenApiTag("Analytics")]
[ApiVersion("1.0")]
public abstract class AnalyticsControllerBase : ManagementApiControllerBase
{
}

View File

@@ -0,0 +1,18 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.ViewModels.Analytics;
namespace Umbraco.Cms.ManagementApi.Controllers.Analytics;
public class GetAnalyticsController : AnalyticsControllerBase
{
private readonly IMetricsConsentService _metricsConsentService;
public GetAnalyticsController(IMetricsConsentService metricsConsentService) => _metricsConsentService = metricsConsentService;
[HttpGet]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(AnalyticsLevelViewModel), StatusCodes.Status200OK)]
public async Task<AnalyticsLevelViewModel> Get() => await Task.FromResult(new AnalyticsLevelViewModel { AnalyticsLevel = _metricsConsentService.GetConsentLevel() });
}

View File

@@ -0,0 +1,36 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.ViewModels.Analytics;
using Umbraco.Cms.ManagementApi.ViewModels.Server;
namespace Umbraco.Cms.ManagementApi.Controllers.Analytics;
public class SetAnalyticsController : AnalyticsControllerBase
{
private readonly IMetricsConsentService _metricsConsentService;
public SetAnalyticsController(IMetricsConsentService metricsConsentService) => _metricsConsentService = metricsConsentService;
[HttpPost]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> SetConsentLevel(AnalyticsLevelViewModel analyticsLevelViewModel)
{
if (!Enum.IsDefined(analyticsLevelViewModel.AnalyticsLevel))
{
var invalidModelProblem = new ProblemDetails
{
Title = "Invalid AnalyticsLevel value",
Detail = "The provided value for AnalyticsLevel is not valid",
Status = StatusCodes.Status400BadRequest,
Type = "Error",
};
return BadRequest(invalidModelProblem);
}
_metricsConsentService.SetConsentLevel(analyticsLevelViewModel.AnalyticsLevel);
return await Task.FromResult(Ok());
}
}

View File

@@ -0,0 +1,33 @@
using System.Globalization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.ManagementApi.ViewModels.Culture;
using Umbraco.Cms.ManagementApi.ViewModels.Pagination;
namespace Umbraco.Cms.ManagementApi.Controllers.Culture;
public class AllCultureController : CultureControllerBase
{
private readonly IUmbracoMapper _umbracoMapper;
public AllCultureController(IUmbracoMapper umbracoMapper) => _umbracoMapper = umbracoMapper;
/// <summary>
/// Returns all cultures available for creating languages.
/// </summary>
/// <returns></returns>
[HttpGet]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<CultureViewModel>), StatusCodes.Status200OK)]
public async Task<PagedViewModel<CultureViewModel>> GetAll(int skip, int take)
{
IEnumerable<CultureInfo> list = CultureInfo.GetCultures(CultureTypes.AllCultures)
.DistinctBy(x => x.Name)
.OrderBy(x => x.EnglishName)
.Skip(skip)
.Take(take);
return await Task.FromResult(_umbracoMapper.Map<PagedViewModel<CultureViewModel>>(list)!);
}
}

View File

@@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Umbraco.New.Cms.Web.Common.Routing;
namespace Umbraco.Cms.ManagementApi.Controllers.Culture;
[ApiController]
[BackOfficeRoute("api/v{version:apiVersion}/culture")]
[OpenApiTag("Culture")]
[ApiVersion("1.0")]
public abstract class CultureControllerBase : ManagementApiControllerBase
{
}

View File

@@ -0,0 +1,24 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.ViewModels.Pagination;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
namespace Umbraco.Cms.ManagementApi.Controllers.DataType.Tree;
public class ChildrenDataTypeTreeController : DataTypeTreeControllerBase
{
public ChildrenDataTypeTreeController(IEntityService entityService, IDataTypeService dataTypeService)
: base(entityService, dataTypeService)
{
}
[HttpGet("children")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<FolderTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<FolderTreeItemViewModel>>> Children(Guid parentKey, int skip = 0, int take = 100, bool foldersOnly = false)
{
RenderFoldersOnly(foldersOnly);
return await GetChildren(parentKey, skip, take);
}
}

View File

@@ -0,0 +1,46 @@
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.Controllers.Tree;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
using Umbraco.New.Cms.Web.Common.Routing;
namespace Umbraco.Cms.ManagementApi.Controllers.DataType.Tree;
[ApiVersion("1.0")]
[ApiController]
[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.DataType}/tree")]
[OpenApiTag(nameof(Constants.UdiEntityType.DataType))]
public class DataTypeTreeControllerBase : FolderTreeControllerBase<FolderTreeItemViewModel>
{
private readonly IDataTypeService _dataTypeService;
public DataTypeTreeControllerBase(IEntityService entityService, IDataTypeService dataTypeService)
: base(entityService) =>
_dataTypeService = dataTypeService;
protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.DataType;
protected override UmbracoObjectTypes FolderObjectType => UmbracoObjectTypes.DataTypeContainer;
protected override FolderTreeItemViewModel[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities)
{
var dataTypes = _dataTypeService
.GetAll(entities.Select(entity => entity.Id).ToArray())
.ToDictionary(contentType => contentType.Id);
return entities.Select(entity =>
{
FolderTreeItemViewModel viewModel = MapTreeItemViewModel(parentKey, entity);
if (dataTypes.TryGetValue(entity.Id, out IDataType? dataType))
{
viewModel.Icon = dataType.Editor?.Icon ?? viewModel.Icon;
}
return viewModel;
}).ToArray();
}
}

View File

@@ -0,0 +1,20 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
namespace Umbraco.Cms.ManagementApi.Controllers.DataType.Tree;
public class ItemsDataTypeTreeController : DataTypeTreeControllerBase
{
public ItemsDataTypeTreeController(IEntityService entityService, IDataTypeService dataTypeService)
: base(entityService, dataTypeService)
{
}
[HttpGet("items")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<FolderTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<FolderTreeItemViewModel>>> Items([FromQuery(Name = "key")] Guid[] keys)
=> await GetItems(keys);
}

View File

@@ -0,0 +1,24 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.ViewModels.Pagination;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
namespace Umbraco.Cms.ManagementApi.Controllers.DataType.Tree;
public class RootDataTypeTreeController : DataTypeTreeControllerBase
{
public RootDataTypeTreeController(IEntityService entityService, IDataTypeService dataTypeService)
: base(entityService, dataTypeService)
{
}
[HttpGet("root")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<FolderTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<FolderTreeItemViewModel>>> Root(int skip = 0, int take = 100, bool foldersOnly = false)
{
RenderFoldersOnly(foldersOnly);
return await GetRoot(skip, take);
}
}

View File

@@ -0,0 +1,69 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.ViewModels.Dictionary;
using Umbraco.Cms.ManagementApi.ViewModels.Pagination;
namespace Umbraco.Cms.ManagementApi.Controllers.Dictionary;
public class AllDictionaryController : DictionaryControllerBase
{
private readonly ILocalizationService _localizationService;
private readonly IUmbracoMapper _umbracoMapper;
public AllDictionaryController(ILocalizationService localizationService, IUmbracoMapper umbracoMapper)
{
_localizationService = localizationService;
_umbracoMapper = umbracoMapper;
}
/// <summary>
/// Retrieves a list with all dictionary items
/// </summary>
/// <returns>
/// The <see cref="IEnumerable{T}" />.
/// </returns>
[HttpGet]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<DictionaryOverviewViewModel>), StatusCodes.Status200OK)]
public async Task<PagedViewModel<DictionaryOverviewViewModel>> All(int skip, int take)
{
IDictionaryItem[] items = _localizationService.GetDictionaryItemDescendants(null).ToArray();
var list = new List<DictionaryOverviewViewModel>(items.Length);
// Build the proper tree structure, as we can have nested dictionary items
BuildTree(list, items);
var model = new PagedViewModel<DictionaryOverviewViewModel>
{
Total = list.Count,
Items = list.Skip(skip).Take(take),
};
return await Task.FromResult(model);
}
// recursive method to build a tree structure from the flat structure returned above
private void BuildTree(List<DictionaryOverviewViewModel> list, IDictionaryItem[] items, int level = 0, Guid? parentId = null)
{
IDictionaryItem[] children = items.Where(t => t.ParentId == parentId).ToArray();
if (children.Any() == false)
{
return;
}
foreach (IDictionaryItem child in children.OrderBy(item => item.ItemKey))
{
DictionaryOverviewViewModel? display = _umbracoMapper.Map<IDictionaryItem, DictionaryOverviewViewModel>(child);
if (display is not null)
{
display.Level = level;
list.Add(display);
}
BuildTree(list, items, level + 1, child.Key);
}
}
}

View File

@@ -0,0 +1,47 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.ViewModels.Dictionary;
using Umbraco.New.Cms.Core.Factories;
namespace Umbraco.Cms.ManagementApi.Controllers.Dictionary;
public class ByIdDictionaryController : DictionaryControllerBase
{
private readonly ILocalizationService _localizationService;
private readonly IDictionaryFactory _dictionaryFactory;
public ByIdDictionaryController(
ILocalizationService localizationService,
IDictionaryFactory dictionaryFactory)
{
_localizationService = localizationService;
_dictionaryFactory = dictionaryFactory;
}
/// <summary>
/// Gets a dictionary item by guid
/// </summary>
/// <param name="key">
/// The id.
/// </param>
/// <returns>
/// The <see cref="DictionaryDisplay" />. Returns a not found response when dictionary item does not exist
/// </returns>
[HttpGet("{key:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(DictionaryViewModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(NotFoundResult), StatusCodes.Status404NotFound)]
public async Task<ActionResult<DictionaryViewModel>> ByKey(Guid key)
{
IDictionaryItem? dictionary = _localizationService.GetDictionaryItemById(key);
if (dictionary == null)
{
return NotFound();
}
return await Task.FromResult(_dictionaryFactory.CreateDictionaryViewModel(dictionary));
}
}

View File

@@ -0,0 +1,90 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.ViewModels.Dictionary;
using Umbraco.Extensions;
namespace Umbraco.Cms.ManagementApi.Controllers.Dictionary;
public class CreateDictionaryController : DictionaryControllerBase
{
private readonly ILocalizationService _localizationService;
private readonly ILocalizedTextService _localizedTextService;
private readonly GlobalSettings _globalSettings;
private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor;
private readonly ILogger<CreateDictionaryController> _logger;
public CreateDictionaryController(
ILocalizationService localizationService,
ILocalizedTextService localizedTextService,
IOptionsSnapshot<GlobalSettings> globalSettings,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
ILogger<CreateDictionaryController> logger)
{
_localizationService = localizationService;
_localizedTextService = localizedTextService;
_globalSettings = globalSettings.Value;
_backofficeSecurityAccessor = backofficeSecurityAccessor;
_logger = logger;
}
/// <summary>
/// Creates a new dictionary item
/// </summary>
/// <param name="dictionaryViewModel">The viewmodel to pass to the action</param>
/// <returns>
/// The <see cref="HttpResponseMessage" />.
/// </returns>
[HttpPost("create")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(CreatedResult), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
public async Task<ActionResult<int>> Create(DictionaryItemViewModel dictionaryViewModel)
{
if (string.IsNullOrEmpty(dictionaryViewModel.Key.ToString()))
{
return ValidationProblem("Key can not be empty."); // TODO: translate
}
if (_localizationService.DictionaryItemExists(dictionaryViewModel.Key.ToString()))
{
var message = _localizedTextService.Localize(
"dictionaryItem",
"changeKeyError",
_backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.GetUserCulture(_localizedTextService, _globalSettings),
new Dictionary<string, string?>
{
{ "0", dictionaryViewModel.Key.ToString() },
});
return await Task.FromResult(ValidationProblem(message));
}
try
{
Guid? parentGuid = null;
if (dictionaryViewModel.ParentId.HasValue)
{
parentGuid = dictionaryViewModel.ParentId;
}
IDictionaryItem item = _localizationService.CreateDictionaryItemWithIdentity(
dictionaryViewModel.Key.ToString(),
parentGuid,
string.Empty);
return await Task.FromResult(Created($"api/v1.0/dictionary/{item.Key}", item.Key));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating dictionary with {Name} under {ParentId}", dictionaryViewModel.Key, dictionaryViewModel.ParentId);
return await Task.FromResult(ValidationProblem("Error creating dictionary item"));
}
}
}

View File

@@ -0,0 +1,51 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.ManagementApi.Controllers.Dictionary;
public class DeleteDictionaryController : DictionaryControllerBase
{
private readonly ILocalizationService _localizationService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
public DeleteDictionaryController(ILocalizationService localizationService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_localizationService = localizationService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}
/// <summary>
/// Deletes a data type with a given ID
/// </summary>
/// <param name="key">The key of the dictionary item to delete</param>
/// <returns>
/// <see cref="HttpResponseMessage" />
/// </returns>
[HttpDelete("{key}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(NotFoundResult), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Delete(Guid key)
{
IDictionaryItem? foundDictionary = _localizationService.GetDictionaryItemByKey(key.ToString());
if (foundDictionary == null)
{
return await Task.FromResult(NotFound());
}
IEnumerable<IDictionaryItem> foundDictionaryDescendants =
_localizationService.GetDictionaryItemDescendants(foundDictionary.Key);
foreach (IDictionaryItem dictionaryItem in foundDictionaryDescendants)
{
_localizationService.Delete(dictionaryItem, _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? -1);
}
_localizationService.Delete(foundDictionary, _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? -1);
return await Task.FromResult(Ok());
}
}

View File

@@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Umbraco.New.Cms.Web.Common.Routing;
namespace Umbraco.Cms.ManagementApi.Controllers.Dictionary;
[ApiController]
[BackOfficeRoute("api/v{version:apiVersion}/dictionary")]
[OpenApiTag("Dictionary")]
[ApiVersion("1.0")]
// TODO: Add authentication
public abstract class DictionaryControllerBase : ManagementApiControllerBase
{
}

View File

@@ -0,0 +1,44 @@
using System.Net.Mime;
using System.Text;
using System.Xml.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
namespace Umbraco.Cms.ManagementApi.Controllers.Dictionary;
public class ExportDictionaryController : DictionaryControllerBase
{
private readonly ILocalizationService _localizationService;
private readonly IEntityXmlSerializer _entityXmlSerializer;
public ExportDictionaryController(ILocalizationService localizationService, IEntityXmlSerializer entityXmlSerializer)
{
_localizationService = localizationService;
_entityXmlSerializer = entityXmlSerializer;
}
[HttpGet("export/{key:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(NotFoundObjectResult), StatusCodes.Status404NotFound)]
public async Task<IActionResult> ExportDictionary(Guid key, bool includeChildren = false)
{
IDictionaryItem? dictionaryItem = _localizationService.GetDictionaryItemById(key);
if (dictionaryItem is null)
{
return await Task.FromResult(NotFound("No dictionary item found with id "));
}
XElement xml = _entityXmlSerializer.Serialize(dictionaryItem, includeChildren);
var fileName = $"{dictionaryItem.ItemKey}.udt";
// Set custom header so umbRequestHelper.downloadFile can save the correct filename
HttpContext.Response.Headers.Add("x-filename", fileName);
return await Task.FromResult(File(Encoding.UTF8.GetBytes(xml.ToDataString()), MediaTypeNames.Application.Octet, fileName));
}
}

View File

@@ -0,0 +1,54 @@
using System.Net.Mime;
using System.Text;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.Services;
using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment;
namespace Umbraco.Cms.ManagementApi.Controllers.Dictionary;
public class ImportDictionaryController : DictionaryControllerBase
{
private readonly IHostingEnvironment _hostingEnvironment;
private readonly IDictionaryService _dictionaryService;
private readonly IWebHostEnvironment _webHostEnvironment;
private readonly ILoadDictionaryItemService _loadDictionaryItemService;
public ImportDictionaryController(
IHostingEnvironment hostingEnvironment,
IDictionaryService dictionaryService,
IWebHostEnvironment webHostEnvironment,
ILoadDictionaryItemService loadDictionaryItemService)
{
_hostingEnvironment = hostingEnvironment;
_dictionaryService = dictionaryService;
_webHostEnvironment = webHostEnvironment;
_loadDictionaryItemService = loadDictionaryItemService;
}
[HttpPost("import")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(ContentResult), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(NotFoundResult), StatusCodes.Status404NotFound)]
public async Task<IActionResult> ImportDictionary(string file, int? parentId)
{
if (string.IsNullOrWhiteSpace(file))
{
return NotFound();
}
var filePath = Path.Combine(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data), file);
if (_webHostEnvironment.ContentRootFileProvider.GetFileInfo(filePath) is null)
{
return await Task.FromResult(NotFound());
}
IDictionaryItem dictionaryItem = _loadDictionaryItemService.Load(filePath, parentId);
return await Task.FromResult(Content(_dictionaryService.CalculatePath(dictionaryItem.ParentId, dictionaryItem.Id), MediaTypeNames.Text.Plain, Encoding.UTF8));
}
}

View File

@@ -0,0 +1,75 @@
using System.Net.Mime;
using System.Text;
using System.Text.Json;
using Json.Patch;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.Serialization;
using Umbraco.Cms.ManagementApi.Services;
using Umbraco.Cms.ManagementApi.ViewModels.Dictionary;
using Umbraco.Cms.ManagementApi.ViewModels.JsonPatch;
using Umbraco.New.Cms.Core.Factories;
namespace Umbraco.Cms.ManagementApi.Controllers.Dictionary;
public class UpdateDictionaryController : DictionaryControllerBase
{
private readonly ILocalizationService _localizationService;
private readonly IUmbracoMapper _umbracoMapper;
private readonly IDictionaryService _dictionaryService;
private readonly IDictionaryFactory _dictionaryFactory;
private readonly IJsonPatchService _jsonPatchService;
private readonly ISystemTextJsonSerializer _systemTextJsonSerializer;
public UpdateDictionaryController(
ILocalizationService localizationService,
IUmbracoMapper umbracoMapper,
IDictionaryService dictionaryService,
IDictionaryFactory dictionaryFactory,
IJsonPatchService jsonPatchService,
ISystemTextJsonSerializer systemTextJsonSerializer)
{
_localizationService = localizationService;
_umbracoMapper = umbracoMapper;
_dictionaryService = dictionaryService;
_dictionaryFactory = dictionaryFactory;
_jsonPatchService = jsonPatchService;
_systemTextJsonSerializer = systemTextJsonSerializer;
}
[HttpPatch("{id:Guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(ContentResult), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(NotFoundResult), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Update(Guid id, JsonPatchViewModel[] updateViewModel)
{
IDictionaryItem? dictionaryItem = _localizationService.GetDictionaryItemById(id);
if (dictionaryItem is null)
{
return NotFound();
}
DictionaryViewModel dictionaryToPatch = _umbracoMapper.Map<DictionaryViewModel>(dictionaryItem)!;
PatchResult? result = _jsonPatchService.Patch(updateViewModel, dictionaryToPatch);
if (result?.Result is null)
{
throw new JsonException("Could not patch the JsonPatchViewModel");
}
DictionaryViewModel? updatedDictionaryItem = _systemTextJsonSerializer.Deserialize<DictionaryViewModel>(result.Result.ToJsonString());
if (updatedDictionaryItem is null)
{
throw new JsonException("Could not serialize from PatchResult to DictionaryViewModel");
}
IDictionaryItem dictionaryToSave = _dictionaryFactory.CreateDictionaryItem(updatedDictionaryItem!);
_localizationService.Save(dictionaryToSave);
return await Task.FromResult(Content(_dictionaryService.CalculatePath(dictionaryToSave.ParentId, dictionaryToSave.Id), MediaTypeNames.Text.Plain, Encoding.UTF8));
}
}

View File

@@ -0,0 +1,51 @@
using System.Xml;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.Models;
using Umbraco.Cms.ManagementApi.Services;
using Umbraco.Cms.ManagementApi.ViewModels.Dictionary;
using Umbraco.Extensions;
using Umbraco.New.Cms.Core.Factories;
namespace Umbraco.Cms.ManagementApi.Controllers.Dictionary;
public class UploadDictionaryController : DictionaryControllerBase
{
private readonly ILocalizedTextService _localizedTextService;
private readonly IUploadFileService _uploadFileService;
private readonly IDictionaryFactory _dictionaryFactory;
public UploadDictionaryController(ILocalizedTextService localizedTextService, IUploadFileService uploadFileService, IDictionaryFactory dictionaryFactory)
{
_localizedTextService = localizedTextService;
_uploadFileService = uploadFileService;
_dictionaryFactory = dictionaryFactory;
}
[HttpPost("upload")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(DictionaryImportViewModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
public async Task<ActionResult<DictionaryImportViewModel>> Upload(IFormFile file)
{
FormFileUploadResult formFileUploadResult = _uploadFileService.TryLoad(file);
if (formFileUploadResult.CouldLoad is false || formFileUploadResult.XmlDocument is null)
{
return await Task.FromResult(ValidationProblem(
_localizedTextService.Localize("media", "failedFileUpload"),
formFileUploadResult.ErrorMessage));
}
DictionaryImportViewModel model = _dictionaryFactory.CreateDictionaryImportViewModel(formFileUploadResult);
if (!model.DictionaryItems.Any())
{
return ValidationProblem(
_localizedTextService.Localize("media", "failedFileUpload"),
_localizedTextService.Localize("dictionary", "noItemsInFile"));
}
return await Task.FromResult(model);
}
}

View File

@@ -0,0 +1,39 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.Services.Paging;
using Umbraco.Cms.ManagementApi.ViewModels.Pagination;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
namespace Umbraco.Cms.ManagementApi.Controllers.DictionaryItem.Tree;
public class ChildrenDictionaryItemTreeController : DictionaryItemTreeControllerBase
{
public ChildrenDictionaryItemTreeController(IEntityService entityService, ILocalizationService localizationService)
: base(entityService, localizationService)
{
}
[HttpGet("children")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<EntityTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<EntityTreeItemViewModel>>> Children(Guid parentKey, int skip = 0, int take = 100)
{
if (PaginationService.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize, out ProblemDetails? error) == false)
{
return BadRequest(error);
}
IDictionaryItem[] dictionaryItems = PaginatedDictionaryItems(
pageNumber,
pageSize,
LocalizationService.GetDictionaryItemChildren(parentKey),
out var totalItems);
EntityTreeItemViewModel[] viewModels = MapTreeItemViewModels(null, dictionaryItems);
PagedViewModel<EntityTreeItemViewModel> result = PagedViewModel(viewModels, totalItems);
return await Task.FromResult(Ok(result));
}
}

View File

@@ -0,0 +1,53 @@
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.Controllers.Tree;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
using Umbraco.New.Cms.Web.Common.Routing;
namespace Umbraco.Cms.ManagementApi.Controllers.DictionaryItem.Tree;
[ApiVersion("1.0")]
[ApiController]
[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.DictionaryItem}/tree")]
[OpenApiTag(nameof(Constants.UdiEntityType.DictionaryItem))]
// NOTE: at the moment dictionary items aren't supported by EntityService, so we have little use of the
// tree controller base. We'll keep it though, in the hope that we can mend EntityService.
public class DictionaryItemTreeControllerBase : EntityTreeControllerBase<EntityTreeItemViewModel>
{
public DictionaryItemTreeControllerBase(IEntityService entityService, ILocalizationService localizationService)
: base(entityService) =>
LocalizationService = localizationService;
// dictionary items do not currently have a known UmbracoObjectType, so we'll settle with Unknown for now
protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.Unknown;
protected ILocalizationService LocalizationService { get; }
protected EntityTreeItemViewModel[] MapTreeItemViewModels(Guid? parentKey, IDictionaryItem[] dictionaryItems)
=> dictionaryItems.Select(dictionaryItem => new EntityTreeItemViewModel
{
Icon = Constants.Icons.RelationType,
Name = dictionaryItem.ItemKey,
Key = dictionaryItem.Key,
Type = Constants.UdiEntityType.DictionaryItem,
HasChildren = false,
IsContainer = LocalizationService.GetDictionaryItemChildren(dictionaryItem.Key).Any(),
ParentKey = parentKey
}).ToArray();
// localization service does not (yet) allow pagination of dictionary items, we have to do it in memory for now
protected IDictionaryItem[] PaginatedDictionaryItems(long pageNumber, int pageSize, IEnumerable<IDictionaryItem> allDictionaryItems, out long totalItems)
{
IDictionaryItem[] allDictionaryItemsAsArray = allDictionaryItems.ToArray();
totalItems = allDictionaryItemsAsArray.Length;
return allDictionaryItemsAsArray
.OrderBy(item => item.ItemKey)
.Skip((int)pageNumber * pageSize)
.Take(pageSize)
.ToArray();
}
}

View File

@@ -0,0 +1,27 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
namespace Umbraco.Cms.ManagementApi.Controllers.DictionaryItem.Tree;
public class ItemsDictionaryItemTreeController : DictionaryItemTreeControllerBase
{
public ItemsDictionaryItemTreeController(IEntityService entityService, ILocalizationService localizationService)
: base(entityService, localizationService)
{
}
[HttpGet("items")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<FolderTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<FolderTreeItemViewModel>>> Items([FromQuery(Name = "key")] Guid[] keys)
{
IDictionaryItem[] dictionaryItems = LocalizationService.GetDictionaryItemsByIds(keys).ToArray();
EntityTreeItemViewModel[] viewModels = MapTreeItemViewModels(null, dictionaryItems);
return await Task.FromResult(Ok(viewModels));
}
}

View File

@@ -0,0 +1,39 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.Services.Paging;
using Umbraco.Cms.ManagementApi.ViewModels.Pagination;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
namespace Umbraco.Cms.ManagementApi.Controllers.DictionaryItem.Tree;
public class RootDictionaryItemTreeController : DictionaryItemTreeControllerBase
{
public RootDictionaryItemTreeController(IEntityService entityService, ILocalizationService localizationService)
: base(entityService, localizationService)
{
}
[HttpGet("root")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<EntityTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<EntityTreeItemViewModel>>> Root(int skip = 0, int take = 100)
{
if (PaginationService.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize, out ProblemDetails? error) == false)
{
return BadRequest(error);
}
IDictionaryItem[] dictionaryItems = PaginatedDictionaryItems(
pageNumber,
pageSize,
LocalizationService.GetRootDictionaryItems(),
out var totalItems);
EntityTreeItemViewModel[] viewModels = MapTreeItemViewModels(null, dictionaryItems);
PagedViewModel<EntityTreeItemViewModel> result = PagedViewModel(viewModels, totalItems);
return await Task.FromResult(Ok(result));
}
}

View File

@@ -0,0 +1,21 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.ViewModels.Pagination;
using Umbraco.Cms.ManagementApi.ViewModels.RecycleBin;
namespace Umbraco.Cms.ManagementApi.Controllers.Document.RecycleBin;
public class ChildrenDocumentRecycleBinController : DocumentRecycleBinControllerBase
{
public ChildrenDocumentRecycleBinController(IEntityService entityService)
: base(entityService)
{
}
[HttpGet("children")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<RecycleBinItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<RecycleBinItemViewModel>>> Children(Guid parentKey, int skip = 0, int take = 100)
=> await GetChildren(parentKey, skip, take);
}

View File

@@ -0,0 +1,43 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.Controllers.RecycleBin;
using Umbraco.Cms.ManagementApi.Filters;
using Umbraco.Cms.ManagementApi.ViewModels.RecycleBin;
using Umbraco.New.Cms.Web.Common.Routing;
namespace Umbraco.Cms.ManagementApi.Controllers.Document.RecycleBin;
[ApiVersion("1.0")]
[ApiController]
[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.Document}/recycle-bin")]
[RequireDocumentTreeRootAccess]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[OpenApiTag(nameof(Constants.UdiEntityType.Document))]
public class DocumentRecycleBinControllerBase : RecycleBinControllerBase<RecycleBinItemViewModel>
{
public DocumentRecycleBinControllerBase(IEntityService entityService)
: base(entityService)
{
}
protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.Document;
protected override int RecycleBinRootId => Constants.System.RecycleBinContent;
protected override RecycleBinItemViewModel MapRecycleBinViewModel(Guid? parentKey, IEntitySlim entity)
{
RecycleBinItemViewModel viewModel = base.MapRecycleBinViewModel(parentKey, entity);
if (entity is IDocumentEntitySlim documentEntitySlim)
{
viewModel.Icon = documentEntitySlim.ContentTypeIcon ?? viewModel.Icon;
}
return viewModel;
}
}

View File

@@ -0,0 +1,21 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.ViewModels.Pagination;
using Umbraco.Cms.ManagementApi.ViewModels.RecycleBin;
namespace Umbraco.Cms.ManagementApi.Controllers.Document.RecycleBin;
public class RootDocumentRecycleBinController : DocumentRecycleBinControllerBase
{
public RootDocumentRecycleBinController(IEntityService entityService)
: base(entityService)
{
}
[HttpGet("root")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<RecycleBinItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<RecycleBinItemViewModel>>> Root(int skip = 0, int take = 100)
=> await GetRoot(skip, take);
}

View File

@@ -0,0 +1,34 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.Services.Entities;
using Umbraco.Cms.ManagementApi.ViewModels.Pagination;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
namespace Umbraco.Cms.ManagementApi.Controllers.Document.Tree;
public class ChildrenDocumentTreeController : DocumentTreeControllerBase
{
public ChildrenDocumentTreeController(
IEntityService entityService,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
IPublicAccessService publicAccessService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor)
: base(entityService, userStartNodeEntitiesService, dataTypeService, publicAccessService, appCaches, backofficeSecurityAccessor)
{
}
[HttpGet("children")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<DocumentTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<DocumentTreeItemViewModel>>> Children(Guid parentKey, int skip = 0, int take = 100, Guid? dataTypeKey = null, string? culture = null)
{
IgnoreUserStartNodesForDataType(dataTypeKey);
RenderForClientCulture(culture);
return await GetChildren(parentKey, skip, take);
}
}

View File

@@ -0,0 +1,94 @@
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.Controllers.Tree;
using Umbraco.Cms.ManagementApi.Services.Entities;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
using Umbraco.Extensions;
using Umbraco.New.Cms.Web.Common.Routing;
namespace Umbraco.Cms.ManagementApi.Controllers.Document.Tree;
[ApiVersion("1.0")]
[ApiController]
[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.Document}/tree")]
[OpenApiTag(nameof(Constants.UdiEntityType.Document))]
public abstract class DocumentTreeControllerBase : UserStartNodeTreeControllerBase<DocumentTreeItemViewModel>
{
private readonly IPublicAccessService _publicAccessService;
private readonly AppCaches _appCaches;
private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor;
private string? _culture;
protected DocumentTreeControllerBase(
IEntityService entityService,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
IPublicAccessService publicAccessService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor)
: base(entityService, userStartNodeEntitiesService, dataTypeService)
{
_publicAccessService = publicAccessService;
_appCaches = appCaches;
_backofficeSecurityAccessor = backofficeSecurityAccessor;
}
protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.Document;
protected override Ordering ItemOrdering => Ordering.By(nameof(Infrastructure.Persistence.Dtos.NodeDto.SortOrder));
protected void RenderForClientCulture(string? culture) => _culture = culture;
protected override DocumentTreeItemViewModel MapTreeItemViewModel(Guid? parentKey, IEntitySlim entity)
{
DocumentTreeItemViewModel viewModel = base.MapTreeItemViewModel(parentKey, entity);
if (entity is IDocumentEntitySlim documentEntitySlim)
{
viewModel.IsPublished = documentEntitySlim.Published;
viewModel.IsEdited = documentEntitySlim.Edited;
viewModel.Icon = documentEntitySlim.ContentTypeIcon ?? viewModel.Icon;
viewModel.IsProtected = _publicAccessService.IsProtected(entity.Path);
if (_culture != null && documentEntitySlim.Variations.VariesByCulture())
{
viewModel.Name = documentEntitySlim.CultureNames.TryGetValue(_culture, out var cultureName)
? cultureName
: $"({viewModel.Name})";
viewModel.IsPublished = documentEntitySlim.PublishedCultures.Contains(_culture);
viewModel.IsEdited = documentEntitySlim.EditedCultures.Contains(_culture);
}
viewModel.IsEdited &= viewModel.IsPublished;
}
return viewModel;
}
// TODO: delete these (faking start node setup for unlimited editor)
protected override int[] GetUserStartNodeIds() => new[] { -1 };
protected override string[] GetUserStartNodePaths() => Array.Empty<string>();
// TODO: use these implementations instead of the dummy ones above once we have backoffice auth in place
// protected override int[] GetUserStartNodeIds()
// => _backofficeSecurityAccessor
// .BackOfficeSecurity?
// .CurrentUser?
// .CalculateContentStartNodeIds(EntityService, _appCaches)
// ?? Array.Empty<int>();
//
// protected override string[] GetUserStartNodePaths()
// => _backofficeSecurityAccessor
// .BackOfficeSecurity?
// .CurrentUser?
// .GetContentStartNodePaths(EntityService, _appCaches)
// ?? Array.Empty<string>();
}

View File

@@ -0,0 +1,33 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.Services.Entities;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
namespace Umbraco.Cms.ManagementApi.Controllers.Document.Tree;
public class ItemsDocumentTreeController : DocumentTreeControllerBase
{
public ItemsDocumentTreeController(
IEntityService entityService,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
IPublicAccessService publicAccessService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor)
: base(entityService, userStartNodeEntitiesService, dataTypeService, publicAccessService, appCaches, backofficeSecurityAccessor)
{
}
[HttpGet("items")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<DocumentTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<DocumentTreeItemViewModel>>> Items([FromQuery(Name = "key")] Guid[] keys, Guid? dataTypeKey = null, string? culture = null)
{
IgnoreUserStartNodesForDataType(dataTypeKey);
RenderForClientCulture(culture);
return await GetItems(keys);
}
}

View File

@@ -0,0 +1,34 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.Services.Entities;
using Umbraco.Cms.ManagementApi.ViewModels.Pagination;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
namespace Umbraco.Cms.ManagementApi.Controllers.Document.Tree;
public class RootDocumentTreeController : DocumentTreeControllerBase
{
public RootDocumentTreeController(
IEntityService entityService,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
IPublicAccessService publicAccessService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor)
: base(entityService, userStartNodeEntitiesService, dataTypeService, publicAccessService, appCaches, backofficeSecurityAccessor)
{
}
[HttpGet("root")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<DocumentTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<DocumentTreeItemViewModel>>> Root(int skip = 0, int take = 100, Guid? dataTypeKey = null, string? culture = null)
{
IgnoreUserStartNodesForDataType(dataTypeKey);
RenderForClientCulture(culture);
return await GetRoot(skip, take);
}
}

View File

@@ -0,0 +1,56 @@
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.Controllers.Tree;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
using Umbraco.New.Cms.Web.Common.Routing;
namespace Umbraco.Cms.ManagementApi.Controllers.DocumentBlueprint.Tree;
[ApiVersion("1.0")]
[ApiController]
[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.DocumentBlueprint}/tree")]
[OpenApiTag(nameof(Constants.UdiEntityType.DocumentBlueprint))]
public class DocumentBlueprintTreeControllerBase : EntityTreeControllerBase<DocumentBlueprintTreeItemViewModel>
{
private readonly IContentTypeService _contentTypeService;
public DocumentBlueprintTreeControllerBase(IEntityService entityService, IContentTypeService contentTypeService)
: base(entityService) =>
_contentTypeService = contentTypeService;
protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.DocumentBlueprint;
protected override DocumentBlueprintTreeItemViewModel[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities)
{
var contentTypeAliases = entities
.OfType<IDocumentEntitySlim>()
.Select(entity => entity.ContentTypeAlias)
.ToArray();
var contentTypeIds = _contentTypeService.GetAllContentTypeIds(contentTypeAliases).ToArray();
var contentTypeByAlias = _contentTypeService
.GetAll(contentTypeIds)
.ToDictionary(contentType => contentType.Alias);
return entities.Select(entity =>
{
DocumentBlueprintTreeItemViewModel viewModel = base.MapTreeItemViewModel(parentKey, entity);
viewModel.Icon = Constants.Icons.Blueprint;
viewModel.HasChildren = false;
if (entity is IDocumentEntitySlim documentEntitySlim
&& contentTypeByAlias.TryGetValue(documentEntitySlim.ContentTypeAlias, out IContentType? contentType))
{
viewModel.DocumentTypeKey = contentType.Key;
viewModel.DocumentTypeAlias = contentType.Alias;
viewModel.DocumentTypeName = contentType.Name;
}
return viewModel;
}).ToArray();
}
}

View File

@@ -0,0 +1,21 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.ViewModels.Pagination;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
namespace Umbraco.Cms.ManagementApi.Controllers.DocumentBlueprint.Tree;
public class ItemsDocumentBlueprintTreeController : DocumentBlueprintTreeControllerBase
{
public ItemsDocumentBlueprintTreeController(IEntityService entityService, IContentTypeService contentTypeService)
: base(entityService, contentTypeService)
{
}
[HttpGet("items")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<DocumentBlueprintTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<DocumentBlueprintTreeItemViewModel>>> Items([FromQuery(Name = "key")] Guid[] keys)
=> await GetItems(keys);
}

View File

@@ -0,0 +1,21 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.ViewModels.Pagination;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
namespace Umbraco.Cms.ManagementApi.Controllers.DocumentBlueprint.Tree;
public class RootDocumentBlueprintTreeController : DocumentBlueprintTreeControllerBase
{
public RootDocumentBlueprintTreeController(IEntityService entityService, IContentTypeService contentTypeService)
: base(entityService, contentTypeService)
{
}
[HttpGet("root")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<DocumentBlueprintTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<DocumentBlueprintTreeItemViewModel>>> Root(int skip = 0, int take = 100)
=> await GetRoot(skip, take);
}

View File

@@ -0,0 +1,24 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.ViewModels.Pagination;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
namespace Umbraco.Cms.ManagementApi.Controllers.DocumentType.Tree;
public class ChildrenDocumentTypeTreeController : DocumentTypeTreeControllerBase
{
public ChildrenDocumentTypeTreeController(IEntityService entityService, IContentTypeService contentTypeService)
: base(entityService, contentTypeService)
{
}
[HttpGet("children")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<DocumentTypeTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<DocumentTypeTreeItemViewModel>>> Children(Guid parentKey, int skip = 0, int take = 100, bool foldersOnly = false)
{
RenderFoldersOnly(foldersOnly);
return await GetChildren(parentKey, skip, take);
}
}

View File

@@ -0,0 +1,47 @@
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.Controllers.Tree;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
using Umbraco.New.Cms.Web.Common.Routing;
namespace Umbraco.Cms.ManagementApi.Controllers.DocumentType.Tree;
[ApiVersion("1.0")]
[ApiController]
[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.DocumentType}/tree")]
[OpenApiTag(nameof(Constants.UdiEntityType.DocumentType))]
public class DocumentTypeTreeControllerBase : FolderTreeControllerBase<DocumentTypeTreeItemViewModel>
{
private readonly IContentTypeService _contentTypeService;
public DocumentTypeTreeControllerBase(IEntityService entityService, IContentTypeService contentTypeService)
: base(entityService) =>
_contentTypeService = contentTypeService;
protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.DocumentType;
protected override UmbracoObjectTypes FolderObjectType => UmbracoObjectTypes.DocumentTypeContainer;
protected override DocumentTypeTreeItemViewModel[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities)
{
var contentTypes = _contentTypeService
.GetAll(entities.Select(entity => entity.Id).ToArray())
.ToDictionary(contentType => contentType.Id);
return entities.Select(entity =>
{
DocumentTypeTreeItemViewModel viewModel = MapTreeItemViewModel(parentKey, entity);
if (contentTypes.TryGetValue(entity.Id, out IContentType? contentType))
{
viewModel.Icon = contentType.Icon ?? viewModel.Icon;
viewModel.IsElement = contentType.IsElement;
}
return viewModel;
}).ToArray();
}
}

View File

@@ -0,0 +1,20 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
namespace Umbraco.Cms.ManagementApi.Controllers.DocumentType.Tree;
public class ItemsDocumentTypeTreeController : DocumentTypeTreeControllerBase
{
public ItemsDocumentTypeTreeController(IEntityService entityService, IContentTypeService contentTypeService)
: base(entityService, contentTypeService)
{
}
[HttpGet("items")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<DocumentTypeTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<DocumentTypeTreeItemViewModel>>> Items([FromQuery(Name = "key")] Guid[] keys)
=> await GetItems(keys);
}

View File

@@ -0,0 +1,24 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.ViewModels.Pagination;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
namespace Umbraco.Cms.ManagementApi.Controllers.DocumentType.Tree;
public class RootDocumentTypeTreeController : DocumentTypeTreeControllerBase
{
public RootDocumentTypeTreeController(IEntityService entityService, IContentTypeService contentTypeService)
: base(entityService, contentTypeService)
{
}
[HttpGet("root")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<DocumentTypeTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<DocumentTypeTreeItemViewModel>>> Root(int skip = 0, int take = 100, bool foldersOnly = false)
{
RenderFoldersOnly(foldersOnly);
return await GetRoot(skip, take);
}
}

View File

@@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Umbraco.New.Cms.Web.Common.Routing;
namespace Umbraco.Cms.ManagementApi.Controllers.ExamineManagement;
[ApiController]
[BackOfficeRoute("api/v{version:apiVersion}/examineManagement")]
[OpenApiTag("ExamineManagement")]
public class ExamineManagementControllerBase : ManagementApiControllerBase
{
}

View File

@@ -0,0 +1,54 @@
using Examine;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.ManagementApi.Factories;
using Umbraco.Cms.ManagementApi.ViewModels.ExamineManagement;
namespace Umbraco.Cms.ManagementApi.Controllers.ExamineManagement;
[ApiVersion("1.0")]
public class IndexExamineManagementController : ExamineManagementControllerBase
{
private readonly IExamineIndexViewModelFactory _examineIndexViewModelFactory;
private readonly IExamineManager _examineManager;
public IndexExamineManagementController(
IExamineIndexViewModelFactory examineIndexViewModelFactory,
IExamineManager examineManager)
{
_examineIndexViewModelFactory = examineIndexViewModelFactory;
_examineManager = examineManager;
}
/// <summary>
/// Check if the index has been rebuilt
/// </summary>
/// <param name="indexName"></param>
/// <returns></returns>
/// <remarks>
/// This is kind of rudimentary since there's no way we can know that the index has rebuilt, we
/// have a listener for the index op complete so we'll just check if that key is no longer there in the runtime cache
/// </remarks>
[HttpGet("index")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ExamineIndexViewModel), StatusCodes.Status200OK)]
public async Task<ActionResult<ExamineIndexViewModel?>> Index(string indexName)
{
if (_examineManager.TryGetIndex(indexName, out IIndex? index))
{
return await Task.FromResult(_examineIndexViewModelFactory.Create(index!));
}
var invalidModelProblem = new ProblemDetails
{
Title = "Index Not Found",
Detail = $"No index found with name = {indexName}",
Status = StatusCodes.Status400BadRequest,
Type = "Error",
};
return await Task.FromResult(BadRequest(invalidModelProblem));
}
}

View File

@@ -0,0 +1,42 @@
using Examine;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Infrastructure.Examine;
using Umbraco.Cms.ManagementApi.Factories;
using Umbraco.Cms.ManagementApi.ViewModels.ExamineManagement;
using Umbraco.Cms.ManagementApi.ViewModels.Pagination;
using Umbraco.Extensions;
namespace Umbraco.Cms.ManagementApi.Controllers.ExamineManagement;
[ApiVersion("1.0")]
public class IndexesExamineManagementController : ExamineManagementControllerBase
{
private readonly IExamineManager _examineManager;
private readonly IExamineIndexViewModelFactory _examineIndexViewModelFactory;
public IndexesExamineManagementController(
IExamineManager examineManager,
IExamineIndexViewModelFactory examineIndexViewModelFactory)
{
_examineManager = examineManager;
_examineIndexViewModelFactory = examineIndexViewModelFactory;
}
/// <summary>
/// Get the details for indexers
/// </summary>
/// <returns></returns>
[HttpGet("indexes")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<ExamineIndexViewModel>), StatusCodes.Status200OK)]
public async Task<PagedViewModel<ExamineIndexViewModel>> Indexes(int skip, int take)
{
ExamineIndexViewModel[] indexes = _examineManager.Indexes
.Select(_examineIndexViewModelFactory.Create)
.OrderBy(examineIndexModel => examineIndexModel.Name?.TrimEnd("Indexer")).ToArray();
var viewModel = new PagedViewModel<ExamineIndexViewModel> { Items = indexes.Skip(skip).Take(take), Total = indexes.Length };
return viewModel;
}
}

View File

@@ -0,0 +1,82 @@
using Examine;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Infrastructure.Examine;
using Umbraco.New.Cms.Infrastructure.Services;
namespace Umbraco.Cms.ManagementApi.Controllers.ExamineManagement;
[ApiVersion("1.0")]
public class RebuildExamineManagementController : ExamineManagementControllerBase
{
private readonly ILogger<RebuildExamineManagementController> _logger;
private readonly IIndexingRebuilderService _indexingRebuilderService;
private readonly IExamineManager _examineManager;
public RebuildExamineManagementController(
ILogger<RebuildExamineManagementController> logger,
IIndexingRebuilderService indexingRebuilderService,
IExamineManager examineManager)
{
_logger = logger;
_indexingRebuilderService = indexingRebuilderService;
_examineManager = examineManager;
}
/// <summary>
/// Rebuilds the index
/// </summary>
/// <param name="indexName"></param>
/// <returns></returns>
[HttpPost("rebuild")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(OkResult), StatusCodes.Status200OK)]
public async Task<IActionResult> Rebuild(string indexName)
{
if (!_examineManager.TryGetIndex(indexName, out var index))
{
var invalidModelProblem = new ProblemDetails
{
Title = "Index Not Found",
Detail = $"No index found with name = {indexName}",
Status = StatusCodes.Status400BadRequest,
Type = "Error",
};
return await Task.FromResult(BadRequest(invalidModelProblem));
}
if (!_indexingRebuilderService.CanRebuild(index.Name))
{
var invalidModelProblem = new ProblemDetails
{
Title = "Could not validate the populator",
Detail =
$"The index {index?.Name} could not be rebuilt because we could not validate its associated {typeof(IIndexPopulator)}",
Status = StatusCodes.Status400BadRequest,
Type = "Error",
};
return await Task.FromResult(BadRequest(invalidModelProblem));
}
_logger.LogInformation("Rebuilding index '{IndexName}'", indexName);
if (_indexingRebuilderService.TryRebuild(index, indexName))
{
return await Task.FromResult(Ok());
}
var problemDetails = new ProblemDetails
{
Title = "Index could not be rebuilt",
Detail = $"The index {index.Name} could not be rebuild. Check the log for details on this error.",
Status = StatusCodes.Status400BadRequest,
Type = "Error",
};
return await Task.FromResult(Conflict(problemDetails));
}
}

View File

@@ -0,0 +1,80 @@
using Examine;
using Examine.Search;
using Lucene.Net.QueryParsers.Classic;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.ManagementApi.Services;
using Umbraco.Cms.ManagementApi.ViewModels.ExamineManagement;
using Umbraco.Cms.ManagementApi.ViewModels.Pagination;
using Umbraco.Extensions;
namespace Umbraco.Cms.ManagementApi.Controllers.ExamineManagement;
[ApiVersion("1.0")]
public class SearchExamineManagementController : ExamineManagementControllerBase
{
private readonly IExamineManagerService _examineManagerService;
public SearchExamineManagementController(IExamineManagerService examineManagerService) => _examineManagerService = examineManagerService;
[HttpGet("search")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<PagedViewModel<SearchResultViewModel>>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<ActionResult<PagedViewModel<SearchResultViewModel>>> GetSearchResults(string searcherName, string? query, int skip, int take)
{
query = query?.Trim();
if (query.IsNullOrWhiteSpace())
{
return new PagedViewModel<SearchResultViewModel>();
}
if (!_examineManagerService.TryFindSearcher(searcherName, out ISearcher searcher))
{
var invalidModelProblem = new ProblemDetails
{
Title = "Could not find a valid searcher",
Detail = "The provided searcher name did not match any of our registered searchers",
Status = StatusCodes.Status404NotFound,
Type = "Error",
};
return NotFound(invalidModelProblem);
}
ISearchResults results;
// NativeQuery will work for a single word/phrase too (but depends on the implementation) the lucene one will work.
try
{
results = searcher
.CreateQuery()
.NativeQuery(query)
.Execute(QueryOptions.SkipTake(skip, take));
}
catch (ParseException)
{
var invalidModelProblem = new ProblemDetails
{
Title = "Could not parse the query",
Detail = "Parser could not parse the query. Please double check if the query is valid. Sometimes this can also happen if your query starts with a wildcard (*)",
Status = StatusCodes.Status404NotFound,
Type = "Error",
};
return BadRequest(invalidModelProblem);
}
return await Task.FromResult(new PagedViewModel<SearchResultViewModel>
{
Total = results.TotalItemCount,
Items = results.Select(x => new SearchResultViewModel
{
Id = x.Id,
Score = x.Score,
Fields = x.AllValues.OrderBy(y => y.Key).Select(y => new FieldViewModel { Name = y.Key, Values = y.Value }),
}),
});
}
}

View File

@@ -0,0 +1,39 @@
using Examine;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Infrastructure.Examine;
using Umbraco.Cms.ManagementApi.ViewModels.ExamineManagement;
using Umbraco.Cms.ManagementApi.ViewModels.Pagination;
using Umbraco.Extensions;
namespace Umbraco.Cms.ManagementApi.Controllers.ExamineManagement;
[ApiVersion("1.0")]
public class SearchersExamineManagementController : ExamineManagementControllerBase
{
private readonly IExamineManager _examineManager;
public SearchersExamineManagementController(IExamineManager examineManager) => _examineManager = examineManager;
/// <summary>
/// Get the details for searchers
/// </summary>
/// <returns></returns>
[HttpGet("searchers")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<SearcherViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<SearcherViewModel>>> Searchers(int skip, int take)
{
var searchers = new List<SearcherViewModel>(
_examineManager.RegisteredSearchers.Select(searcher => new SearcherViewModel { Name = searcher.Name })
.OrderBy(x =>
x.Name.TrimEnd("Searcher"))); // order by name , but strip the "Searcher" from the end if it exists
var viewModel = new PagedViewModel<SearcherViewModel>
{
Items = searchers.Skip(skip).Take(take),
Total = searchers.Count,
};
return await Task.FromResult(Ok(viewModel));
}
}

View File

@@ -0,0 +1,80 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.ManagementApi.Builders;
using Umbraco.Cms.ManagementApi.Factories;
using Umbraco.Cms.ManagementApi.ViewModels.Help;
using Umbraco.Cms.ManagementApi.ViewModels.Pagination;
namespace Umbraco.Cms.ManagementApi.Controllers.Help;
public class GetHelpController : HelpControllerBase
{
private readonly ILogger<GetHelpController> _logger;
private readonly IJsonSerializer _jsonSerializer;
private HelpPageSettings _helpPageSettings;
public GetHelpController(
IOptionsMonitor<HelpPageSettings> helpPageSettings,
ILogger<GetHelpController> logger,
IJsonSerializer jsonSerializer)
{
_logger = logger;
_jsonSerializer = jsonSerializer;
_helpPageSettings = helpPageSettings.CurrentValue;
helpPageSettings.OnChange(UpdateHelpPageSettings);
}
private void UpdateHelpPageSettings(HelpPageSettings settings) => _helpPageSettings = settings;
[HttpGet]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(PagedViewModel<HelpPageViewModel>), StatusCodes.Status200OK)]
public async Task<IActionResult> Get(string section, string? tree, int skip, int take, string? baseUrl = "https://our.umbraco.com")
{
if (IsAllowedUrl(baseUrl) is false)
{
_logger.LogError($"The following URL is not listed in the allowlist for HelpPage in HelpPageSettings: {baseUrl}");
ProblemDetails invalidModelProblem =
new ProblemDetailsBuilder()
.WithTitle("Invalid database configuration")
.WithDetail("The provided database configuration is invalid")
.Build();
return BadRequest(invalidModelProblem);
}
var url = string.Format(baseUrl + "/Umbraco/Documentation/Lessons/GetContextHelpDocs?sectionAlias={0}&treeAlias={1}", section, tree);
try
{
var httpClient = new HttpClient();
// fetch dashboard json and parse to JObject
var json = await httpClient.GetStringAsync(url);
List<HelpPageViewModel>? result = _jsonSerializer.Deserialize<List<HelpPageViewModel>>(json);
if (result != null)
{
return Ok(new PagedViewModel<HelpPageViewModel>
{
Total = result.Count,
Items = result.Skip(skip).Take(take),
});
}
}
catch (HttpRequestException rex)
{
_logger.LogInformation($"Check your network connection, exception: {rex.Message}");
}
return Ok(PagedViewModel<HelpPageViewModel>.Empty());
}
private bool IsAllowedUrl(string? url) =>
_helpPageSettings.HelpPageUrlAllowList is null || _helpPageSettings.HelpPageUrlAllowList.Contains(url);
}

View File

@@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Umbraco.New.Cms.Web.Common.Routing;
namespace Umbraco.Cms.ManagementApi.Controllers.Help;
[ApiController]
[BackOfficeRoute("api/v{version:apiVersion}/help")]
[OpenApiTag("Help")]
[ApiVersion("1.0")]
public abstract class HelpControllerBase : ManagementApiControllerBase
{
}

View File

@@ -7,9 +7,9 @@ using Umbraco.New.Cms.Web.Common.Routing;
namespace Umbraco.Cms.ManagementApi.Controllers.Install;
[ApiController]
[BackOfficeRoute("api/v{version:apiVersion}/install")]
[VersionedApiBackOfficeRoute("install")]
[OpenApiTag("Install")]
[RequireRuntimeLevel(RuntimeLevel.Install)]
public abstract class InstallControllerBase : Controller
public abstract class InstallControllerBase : ManagementApiControllerBase
{
}

View File

@@ -27,16 +27,13 @@ public class ValidateDatabaseInstallController : InstallControllerBase
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> ValidateDatabase(DatabaseInstallViewModel viewModel)
{
// TODO: Async - We need to figure out what we want to do with async endpoints that doesn't do anything async
// We want these to be async for future use (Ideally we'll have more async things),
// But we need to figure out how we want to handle it in the meantime? use Task.FromResult or?
DatabaseModel databaseModel = _mapper.Map<DatabaseModel>(viewModel)!;
var success = _databaseBuilder.ConfigureDatabaseConnection(databaseModel, true);
if (success)
{
return Ok();
return await Task.FromResult(Ok());
}
var invalidModelProblem = new ProblemDetails
@@ -47,6 +44,6 @@ public class ValidateDatabaseInstallController : InstallControllerBase
Type = "Error",
};
return BadRequest(invalidModelProblem);
return await Task.FromResult(BadRequest(invalidModelProblem));
}
}

View File

@@ -0,0 +1,36 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.ViewModels.Language;
using Umbraco.Cms.ManagementApi.ViewModels.Pagination;
using Umbraco.New.Cms.Core.Models;
namespace Umbraco.Cms.ManagementApi.Controllers.Language;
public class AllLanguageController : LanguageControllerBase
{
private readonly ILocalizationService _localizationService;
private readonly IUmbracoMapper _umbracoMapper;
public AllLanguageController(ILocalizationService localizationService, IUmbracoMapper umbracoMapper)
{
_localizationService = localizationService;
_umbracoMapper = umbracoMapper;
}
/// <summary>1
/// Returns all currently configured languages.
/// </summary>
/// <returns></returns>
[HttpGet]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<LanguageViewModel>), StatusCodes.Status200OK)]
public async Task<PagedViewModel<LanguageViewModel>?> GetAll(int skip, int take)
{
PagedModel<ILanguage> allLanguages = _localizationService.GetAllLanguagesPaged(skip, take);
return await Task.FromResult(_umbracoMapper.Map<PagedModel<ILanguage>, PagedViewModel<LanguageViewModel>>(allLanguages)!);
}
}

View File

@@ -0,0 +1,35 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.ViewModels.Language;
namespace Umbraco.Cms.ManagementApi.Controllers.Language;
public class ByIdLanguageController : LanguageControllerBase
{
private readonly ILocalizationService _localizationService;
private readonly IUmbracoMapper _umbracoMapper;
public ByIdLanguageController(ILocalizationService localizationService, IUmbracoMapper umbracoMapper)
{
_localizationService = localizationService;
_umbracoMapper = umbracoMapper;
}
[HttpGet("{id:int}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(NotFoundResult), StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<LanguageViewModel?>> ById(int id)
{
ILanguage? lang = _localizationService.GetLanguageById(id);
if (lang is null)
{
return NotFound();
}
return await Task.FromResult(_umbracoMapper.Map<LanguageViewModel>(lang));
}
}

View File

@@ -0,0 +1,61 @@
using System.Globalization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.ViewModels.Language;
using Umbraco.New.Cms.Core.Services.Installer;
namespace Umbraco.Cms.ManagementApi.Controllers.Language;
public class CreateLanguageController : LanguageControllerBase
{
private readonly ILanguageService _languageService;
private readonly IUmbracoMapper _umbracoMapper;
private readonly ILocalizationService _localizationService;
public CreateLanguageController(ILanguageService languageService, IUmbracoMapper umbracoMapper, ILocalizationService localizationService)
{
_languageService = languageService;
_umbracoMapper = umbracoMapper;
_localizationService = localizationService;
}
/// <summary>
/// Creates or saves a language
/// </summary>
[HttpPost("create")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status201Created)]
// TODO: This needs to be an authorized endpoint.
public async Task<ActionResult> Create(LanguageViewModel language)
{
if (_languageService.LanguageAlreadyExists(language.Id, language.IsoCode))
{
// Someone is trying to create a language that already exist
ModelState.AddModelError("IsoCode", "The language " + language.IsoCode + " already exists");
return ValidationProblem(ModelState);
}
// Creating a new lang...
CultureInfo culture;
try
{
culture = CultureInfo.GetCultureInfo(language.IsoCode);
}
catch (CultureNotFoundException)
{
ModelState.AddModelError("IsoCode", "No Culture found with name " + language.IsoCode);
return ValidationProblem(ModelState);
}
language.Name ??= culture.EnglishName;
ILanguage newLang = _umbracoMapper.Map<ILanguage>(language)!;
_localizationService.Save(newLang);
return await Task.FromResult(Created($"api/v1.0/language/{newLang.Id}", null));
}
}

View File

@@ -0,0 +1,50 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.Builders;
namespace Umbraco.Cms.ManagementApi.Controllers.Language;
public class DeleteLanguageController : LanguageControllerBase
{
private readonly ILocalizationService _localizationService;
public DeleteLanguageController(ILocalizationService localizationService) => _localizationService = localizationService;
/// <summary>
/// Deletes a language with a given ID
/// </summary>
[HttpDelete("{id:int}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status200OK)]
// TODO: This needs to be an authorized endpoint.
public async Task<IActionResult> Delete(int id)
{
ILanguage? language = _localizationService.GetLanguageById(id);
if (language == null)
{
return await Task.FromResult(NotFound());
}
// the service would not let us do it, but test here nevertheless
if (language.IsDefault)
{
ProblemDetails invalidModelProblem =
new ProblemDetailsBuilder()
.WithTitle("Cannot delete default language")
.WithDetail($"Language '{language.IsoCode}' is currently set to 'default' and can not be deleted.")
.Build();
return BadRequest(invalidModelProblem);
}
// service is happy deleting a language that's fallback for another language,
// will just remove it - so no need to check here
_localizationService.Delete(language);
return await Task.FromResult(Ok());
}
}

View File

@@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Umbraco.New.Cms.Web.Common.Routing;
namespace Umbraco.Cms.ManagementApi.Controllers.Language;
[ApiController]
[BackOfficeRoute("api/v{version:apiVersion}/language")]
[OpenApiTag("Language")]
[ApiVersion("1.0")]
public abstract class LanguageControllerBase : ManagementApiControllerBase
{
}

View File

@@ -0,0 +1,66 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.ViewModels.Language;
using Umbraco.New.Cms.Core.Services.Installer;
namespace Umbraco.Cms.ManagementApi.Controllers.Language;
public class UpdateLanguageController : LanguageControllerBase
{
private readonly ILanguageService _languageService;
private readonly IUmbracoMapper _umbracoMapper;
private readonly ILocalizationService _localizationService;
public UpdateLanguageController(ILanguageService languageService, IUmbracoMapper umbracoMapper, ILocalizationService localizationService)
{
_languageService = languageService;
_umbracoMapper = umbracoMapper;
_localizationService = localizationService;
}
/// <summary>
/// Updates a language
/// </summary>
[HttpPut("update")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(NotFoundResult), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status200OK)]
// TODO: This needs to be an authorized endpoint.
public async Task<ActionResult> Update(LanguageViewModel language)
{
ILanguage? existingById = language.Id != default ? _localizationService.GetLanguageById(language.Id) : null;
if (existingById is null)
{
return await Task.FromResult(NotFound());
}
// note that the service will prevent the default language from being "un-defaulted"
// but does not hurt to test here - though the UI should prevent it too
if (existingById.IsDefault && !language.IsDefault)
{
ModelState.AddModelError("IsDefault", "Cannot un-default the default language.");
return await Task.FromResult(ValidationProblem(ModelState));
}
existingById = _umbracoMapper.Map(language, existingById);
if (!_languageService.CanUseLanguagesFallbackLanguage(existingById))
{
ModelState.AddModelError("FallbackLanguage", "The selected fall back language does not exist.");
return await Task.FromResult(ValidationProblem(ModelState));
}
if (!_languageService.CanGetProperFallbackLanguage(existingById))
{
ModelState.AddModelError("FallbackLanguage", $"The selected fall back language {_localizationService.GetLanguageById(existingById.FallbackLanguageId!.Value)} would create a circular path.");
return await Task.FromResult(ValidationProblem(ModelState));
}
_localizationService.Save(existingById);
return await Task.FromResult(Ok());
}
}

View File

@@ -0,0 +1,10 @@
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.ManagementApi.Filters;
namespace Umbraco.Cms.ManagementApi.Controllers;
[ManagementApiJsonConfiguration]
public class ManagementApiControllerBase : Controller
{
}

View File

@@ -0,0 +1,21 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.ViewModels.Pagination;
using Umbraco.Cms.ManagementApi.ViewModels.RecycleBin;
namespace Umbraco.Cms.ManagementApi.Controllers.Media.RecycleBin;
public class ChildrenMediaRecycleBinController : MediaRecycleBinControllerBase
{
public ChildrenMediaRecycleBinController(IEntityService entityService)
: base(entityService)
{
}
[HttpGet("children")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<RecycleBinItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<RecycleBinItemViewModel>>> Children(Guid parentKey, int skip = 0, int take = 100)
=> await GetChildren(parentKey, skip, take);
}

View File

@@ -0,0 +1,43 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.Controllers.RecycleBin;
using Umbraco.Cms.ManagementApi.Filters;
using Umbraco.Cms.ManagementApi.ViewModels.RecycleBin;
using Umbraco.New.Cms.Web.Common.Routing;
namespace Umbraco.Cms.ManagementApi.Controllers.Media.RecycleBin;
[ApiVersion("1.0")]
[ApiController]
[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.Media}/recycle-bin")]
[RequireMediaTreeRootAccess]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[OpenApiTag(nameof(Constants.UdiEntityType.Media))]
public class MediaRecycleBinControllerBase : RecycleBinControllerBase<RecycleBinItemViewModel>
{
public MediaRecycleBinControllerBase(IEntityService entityService)
: base(entityService)
{
}
protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.Media;
protected override int RecycleBinRootId => Constants.System.RecycleBinMedia;
protected override RecycleBinItemViewModel MapRecycleBinViewModel(Guid? parentKey, IEntitySlim entity)
{
RecycleBinItemViewModel viewModel = base.MapRecycleBinViewModel(parentKey, entity);
if (entity is IMediaEntitySlim mediaEntitySlim)
{
viewModel.Icon = mediaEntitySlim.ContentTypeIcon ?? viewModel.Icon;
}
return viewModel;
}
}

View File

@@ -0,0 +1,21 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.ViewModels.Pagination;
using Umbraco.Cms.ManagementApi.ViewModels.RecycleBin;
namespace Umbraco.Cms.ManagementApi.Controllers.Media.RecycleBin;
public class RootMediaRecycleBinController : MediaRecycleBinControllerBase
{
public RootMediaRecycleBinController(IEntityService entityService)
: base(entityService)
{
}
[HttpGet("root")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<RecycleBinItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<RecycleBinItemViewModel>>> Root(int skip = 0, int take = 100)
=> await GetRoot(skip, take);
}

View File

@@ -0,0 +1,32 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.Services.Entities;
using Umbraco.Cms.ManagementApi.ViewModels.Pagination;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
namespace Umbraco.Cms.ManagementApi.Controllers.Media.Tree;
public class ChildrenMediaTreeController : MediaTreeControllerBase
{
public ChildrenMediaTreeController(
IEntityService entityService,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor)
: base(entityService, userStartNodeEntitiesService, dataTypeService, appCaches, backofficeSecurityAccessor)
{
}
[HttpGet("children")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<ContentTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<ContentTreeItemViewModel>>> Children(Guid parentKey, int skip = 0, int take = 100, Guid? dataTypeKey = null)
{
IgnoreUserStartNodesForDataType(dataTypeKey);
return await GetChildren(parentKey, skip, take);
}
}

View File

@@ -0,0 +1,31 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.Services.Entities;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
namespace Umbraco.Cms.ManagementApi.Controllers.Media.Tree;
public class ItemsMediaTreeController : MediaTreeControllerBase
{
public ItemsMediaTreeController(
IEntityService entityService,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor)
: base(entityService, userStartNodeEntitiesService, dataTypeService, appCaches, backofficeSecurityAccessor)
{
}
[HttpGet("items")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<ContentTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<ContentTreeItemViewModel>>> Items([FromQuery(Name = "key")] Guid[] keys, Guid? dataTypeKey = null)
{
IgnoreUserStartNodesForDataType(dataTypeKey);
return await GetItems(keys);
}
}

View File

@@ -0,0 +1,72 @@
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.Controllers.Tree;
using Umbraco.Cms.ManagementApi.Services.Entities;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
using Umbraco.New.Cms.Web.Common.Routing;
namespace Umbraco.Cms.ManagementApi.Controllers.Media.Tree;
[ApiVersion("1.0")]
[ApiController]
[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.Media}/tree")]
[OpenApiTag(nameof(Constants.UdiEntityType.Media))]
public class MediaTreeControllerBase : UserStartNodeTreeControllerBase<ContentTreeItemViewModel>
{
private readonly AppCaches _appCaches;
private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor;
public MediaTreeControllerBase(
IEntityService entityService,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor)
: base(entityService, userStartNodeEntitiesService, dataTypeService)
{
_appCaches = appCaches;
_backofficeSecurityAccessor = backofficeSecurityAccessor;
}
protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.Media;
protected override Ordering ItemOrdering => Ordering.By(nameof(Infrastructure.Persistence.Dtos.NodeDto.SortOrder));
protected override ContentTreeItemViewModel MapTreeItemViewModel(Guid? parentKey, IEntitySlim entity)
{
ContentTreeItemViewModel viewModel = base.MapTreeItemViewModel(parentKey, entity);
if (entity is IMediaEntitySlim mediaEntitySlim)
{
viewModel.Icon = mediaEntitySlim.ContentTypeIcon ?? viewModel.Icon;
}
return viewModel;
}
// TODO: delete these (faking start node setup for unlimited editor)
protected override int[] GetUserStartNodeIds() => new[] { -1 };
protected override string[] GetUserStartNodePaths() => Array.Empty<string>();
// TODO: use these implementations instead of the dummy ones above once we have backoffice auth in place
// protected override int[] GetUserStartNodeIds()
// => _backofficeSecurityAccessor
// .BackOfficeSecurity?
// .CurrentUser?
// .CalculateMediaStartNodeIds(EntityService, _appCaches)
// ?? Array.Empty<int>();
//
// protected override string[] GetUserStartNodePaths()
// => _backofficeSecurityAccessor
// .BackOfficeSecurity?
// .CurrentUser?
// .GetMediaStartNodePaths(EntityService, _appCaches)
// ?? Array.Empty<string>();
}

View File

@@ -0,0 +1,32 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.Services.Entities;
using Umbraco.Cms.ManagementApi.ViewModels.Pagination;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
namespace Umbraco.Cms.ManagementApi.Controllers.Media.Tree;
public class RootMediaTreeController : MediaTreeControllerBase
{
public RootMediaTreeController(
IEntityService entityService,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor)
: base(entityService, userStartNodeEntitiesService, dataTypeService, appCaches, backofficeSecurityAccessor)
{
}
[HttpGet("root")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<ContentTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<ContentTreeItemViewModel>>> Root(int skip = 0, int take = 100, Guid? dataTypeKey = null)
{
IgnoreUserStartNodesForDataType(dataTypeKey);
return await GetRoot(skip, take);
}
}

View File

@@ -0,0 +1,24 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.ViewModels.Pagination;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
namespace Umbraco.Cms.ManagementApi.Controllers.MediaType.Tree;
public class ChildrenMediaTypeTreeController : MediaTypeTreeControllerBase
{
public ChildrenMediaTypeTreeController(IEntityService entityService, IMediaTypeService mediaTypeService)
: base(entityService, mediaTypeService)
{
}
[HttpGet("children")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<FolderTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<FolderTreeItemViewModel>>> Children(Guid parentKey, int skip = 0, int take = 100, bool foldersOnly = false)
{
RenderFoldersOnly(foldersOnly);
return await GetChildren(parentKey, skip, take);
}
}

View File

@@ -0,0 +1,20 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
namespace Umbraco.Cms.ManagementApi.Controllers.MediaType.Tree;
public class ItemsMediaTypeTreeController : MediaTypeTreeControllerBase
{
public ItemsMediaTypeTreeController(IEntityService entityService, IMediaTypeService mediaTypeService)
: base(entityService, mediaTypeService)
{
}
[HttpGet("items")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<FolderTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<FolderTreeItemViewModel>>> Items([FromQuery(Name = "key")] Guid[] keys)
=> await GetItems(keys);
}

View File

@@ -0,0 +1,46 @@
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.Controllers.Tree;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
using Umbraco.New.Cms.Web.Common.Routing;
namespace Umbraco.Cms.ManagementApi.Controllers.MediaType.Tree;
[ApiVersion("1.0")]
[ApiController]
[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.MediaType}/tree")]
[OpenApiTag(nameof(Constants.UdiEntityType.MediaType))]
public class MediaTypeTreeControllerBase : FolderTreeControllerBase<FolderTreeItemViewModel>
{
private readonly IMediaTypeService _mediaTypeService;
public MediaTypeTreeControllerBase(IEntityService entityService, IMediaTypeService mediaTypeService)
: base(entityService) =>
_mediaTypeService = mediaTypeService;
protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.MediaType;
protected override UmbracoObjectTypes FolderObjectType => UmbracoObjectTypes.MediaTypeContainer;
protected override FolderTreeItemViewModel[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities)
{
var mediaTypes = _mediaTypeService
.GetAll(entities.Select(entity => entity.Id).ToArray())
.ToDictionary(contentType => contentType.Id);
return entities.Select(entity =>
{
FolderTreeItemViewModel viewModel = MapTreeItemViewModel(parentKey, entity);
if (mediaTypes.TryGetValue(entity.Id, out IMediaType? mediaType))
{
viewModel.Icon = mediaType.Icon ?? viewModel.Icon;
}
return viewModel;
}).ToArray();
}
}

View File

@@ -0,0 +1,24 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.ViewModels.Pagination;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
namespace Umbraco.Cms.ManagementApi.Controllers.MediaType.Tree;
public class RootMediaTypeTreeController : MediaTypeTreeControllerBase
{
public RootMediaTypeTreeController(IEntityService entityService, IMediaTypeService mediaTypeService)
: base(entityService, mediaTypeService)
{
}
[HttpGet("root")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<FolderTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<FolderTreeItemViewModel>>> Root(int skip = 0, int take = 100, bool foldersOnly = false)
{
RenderFoldersOnly(foldersOnly);
return await GetRoot(skip, take);
}
}

View File

@@ -0,0 +1,20 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
namespace Umbraco.Cms.ManagementApi.Controllers.MemberGroup.Tree;
public class ItemsMemberGroupTreeController : MemberGroupTreeControllerBase
{
public ItemsMemberGroupTreeController(IEntityService entityService)
: base(entityService)
{
}
[HttpGet("items")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<EntityTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<EntityTreeItemViewModel>>> Items([FromQuery(Name = "key")] Guid[] keys)
=> await GetItems(keys);
}

View File

@@ -0,0 +1,32 @@
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.Controllers.Tree;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
using Umbraco.New.Cms.Web.Common.Routing;
namespace Umbraco.Cms.ManagementApi.Controllers.MemberGroup.Tree;
[ApiVersion("1.0")]
[ApiController]
[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.MemberGroup}/tree")]
[OpenApiTag(nameof(Constants.UdiEntityType.MemberGroup))]
public class MemberGroupTreeControllerBase : EntityTreeControllerBase<EntityTreeItemViewModel>
{
public MemberGroupTreeControllerBase(IEntityService entityService)
: base(entityService)
{
}
protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.MemberGroup;
protected override EntityTreeItemViewModel MapTreeItemViewModel(Guid? parentKey, IEntitySlim entity)
{
EntityTreeItemViewModel viewModel = base.MapTreeItemViewModel(parentKey, entity);
viewModel.Icon = Constants.Icons.MemberGroup;
return viewModel;
}
}

View File

@@ -0,0 +1,21 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.ViewModels.Pagination;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
namespace Umbraco.Cms.ManagementApi.Controllers.MemberGroup.Tree;
public class RootMemberGroupTreeController : MemberGroupTreeControllerBase
{
public RootMemberGroupTreeController(IEntityService entityService)
: base(entityService)
{
}
[HttpGet("root")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<EntityTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<EntityTreeItemViewModel>>> Root(int skip = 0, int take = 100)
=> await GetRoot(skip, take);
}

View File

@@ -0,0 +1,20 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
namespace Umbraco.Cms.ManagementApi.Controllers.MemberType.Tree;
public class ItemsMemberTypeTreeController : MemberTypeTreeControllerBase
{
public ItemsMemberTypeTreeController(IEntityService entityService)
: base(entityService)
{
}
[HttpGet("items")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<EntityTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<EntityTreeItemViewModel>>> Items([FromQuery(Name = "key")] Guid[] keys)
=> await GetItems(keys);
}

View File

@@ -0,0 +1,32 @@
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.Controllers.Tree;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
using Umbraco.New.Cms.Web.Common.Routing;
namespace Umbraco.Cms.ManagementApi.Controllers.MemberType.Tree;
[ApiVersion("1.0")]
[ApiController]
[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.MemberType}/tree")]
[OpenApiTag(nameof(Constants.UdiEntityType.MemberType))]
public class MemberTypeTreeControllerBase : EntityTreeControllerBase<EntityTreeItemViewModel>
{
public MemberTypeTreeControllerBase(IEntityService entityService)
: base(entityService)
{
}
protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.MemberType;
protected override EntityTreeItemViewModel MapTreeItemViewModel(Guid? parentKey, IEntitySlim entity)
{
EntityTreeItemViewModel viewModel = base.MapTreeItemViewModel(parentKey, entity);
viewModel.Icon = Constants.Icons.User;
return viewModel;
}
}

View File

@@ -0,0 +1,21 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.ViewModels.Pagination;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
namespace Umbraco.Cms.ManagementApi.Controllers.MemberType.Tree;
public class RootMemberTypeTreeController : MemberTypeTreeControllerBase
{
public RootMemberTypeTreeController(IEntityService entityService)
: base(entityService)
{
}
[HttpGet("root")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<EntityTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<EntityTreeItemViewModel>>> Root(int skip = 0, int take = 100)
=> await GetRoot(skip, take);
}

View File

@@ -0,0 +1,21 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.ManagementApi.ViewModels.Pagination;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
namespace Umbraco.Cms.ManagementApi.Controllers.PartialView.Tree;
public class ChildrenPartialViewTreeController : PartialViewTreeControllerBase
{
public ChildrenPartialViewTreeController(FileSystems fileSystems)
: base(fileSystems)
{
}
[HttpGet("children")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<FileSystemTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<FileSystemTreeItemViewModel>>> Children(string path, int skip = 0, int take = 100)
=> await GetChildren(path, skip, take);
}

View File

@@ -0,0 +1,20 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
namespace Umbraco.Cms.ManagementApi.Controllers.PartialView.Tree;
public class ItemsPartialViewTreeController : PartialViewTreeControllerBase
{
public ItemsPartialViewTreeController(FileSystems fileSystems)
: base(fileSystems)
{
}
[HttpGet("items")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<FileSystemTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<FileSystemTreeItemViewModel>>> Items([FromQuery(Name = "path")] string[] paths)
=> await GetItems(paths);
}

View File

@@ -0,0 +1,25 @@
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.ManagementApi.Controllers.Tree;
using Umbraco.New.Cms.Web.Common.Routing;
namespace Umbraco.Cms.ManagementApi.Controllers.PartialView.Tree;
[ApiVersion("1.0")]
[ApiController]
[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.PartialView}/tree")]
[OpenApiTag(nameof(Constants.UdiEntityType.PartialView))]
public class PartialViewTreeControllerBase : FileSystemTreeControllerBase
{
public PartialViewTreeControllerBase(FileSystems fileSystems)
=> FileSystem = fileSystems.PartialViewsFileSystem ??
throw new ArgumentException("Missing partial views file system", nameof(fileSystems));
protected override IFileSystem FileSystem { get; }
protected override string FileIcon(string path) => Constants.Icons.PartialView;
protected override string ItemType(string path) => Constants.UdiEntityType.PartialView;
}

View File

@@ -0,0 +1,21 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.ManagementApi.ViewModels.Pagination;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
namespace Umbraco.Cms.ManagementApi.Controllers.PartialView.Tree;
public class RootPartialViewTreeController : PartialViewTreeControllerBase
{
public RootPartialViewTreeController(FileSystems fileSystems)
: base(fileSystems)
{
}
[HttpGet("root")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<FileSystemTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<FileSystemTreeItemViewModel>>> Root(int skip = 0, int take = 100)
=> await GetRoot(skip, take);
}

View File

@@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Umbraco.New.Cms.Web.Common.Routing;
namespace Umbraco.Cms.ManagementApi.Controllers.Profiling;
[ApiVersion("1.0")]
[ApiController]
[BackOfficeRoute("api/v{version:apiVersion}/profiling")]
[OpenApiTag("Profiling")]
public class ProfilingControllerBase : ManagementApiControllerBase
{
}

View File

@@ -0,0 +1,19 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.ManagementApi.ViewModels.Profiling;
namespace Umbraco.Cms.ManagementApi.Controllers.Profiling;
public class StatusProfilingController : ProfilingControllerBase
{
private readonly IHostingEnvironment _hosting;
public StatusProfilingController(IHostingEnvironment hosting) => _hosting = hosting;
[HttpGet("status")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(ProfilingStatusViewModel), StatusCodes.Status200OK)]
public async Task<ActionResult<ProfilingStatusViewModel>> Status()
=> await Task.FromResult(Ok(new ProfilingStatusViewModel(_hosting.IsDebugMode)));
}

View File

@@ -0,0 +1,23 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.PublishedCache;
namespace Umbraco.Cms.ManagementApi.Controllers.PublishedCache;
public class CollectPublishedCacheController : PublishedCacheControllerBase
{
private readonly IPublishedSnapshotService _publishedSnapshotService;
public CollectPublishedCacheController(IPublishedSnapshotService publishedSnapshotService)
=> _publishedSnapshotService = publishedSnapshotService;
[HttpPost("collect")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult> Collect()
{
GC.Collect();
await _publishedSnapshotService.CollectAsync();
return Ok();
}
}

View File

@@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Umbraco.New.Cms.Web.Common.Routing;
namespace Umbraco.Cms.ManagementApi.Controllers.PublishedCache;
[ApiVersion("1.0")]
[ApiController]
[BackOfficeRoute("api/v{version:apiVersion}/published-cache")]
[OpenApiTag("PublishedCache")]
public class PublishedCacheControllerBase : ManagementApiControllerBase
{
}

View File

@@ -0,0 +1,22 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.PublishedCache;
namespace Umbraco.Cms.ManagementApi.Controllers.PublishedCache;
public class RebuildPublishedCacheController : PublishedCacheControllerBase
{
private readonly IPublishedSnapshotService _publishedSnapshotService;
public RebuildPublishedCacheController(IPublishedSnapshotService publishedSnapshotService)
=> _publishedSnapshotService = publishedSnapshotService;
[HttpPost("rebuild")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult> Collect()
{
_publishedSnapshotService.Rebuild();
return await Task.FromResult(Ok());
}
}

View File

@@ -0,0 +1,23 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Cache;
using Umbraco.Extensions;
namespace Umbraco.Cms.ManagementApi.Controllers.PublishedCache;
public class ReloadPublishedCacheController : PublishedCacheControllerBase
{
private readonly DistributedCache _distributedCache;
public ReloadPublishedCacheController(DistributedCache distributedCache) => _distributedCache = distributedCache;
[HttpPost("reload")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult> Reload()
{
_distributedCache.RefreshAllPublishedSnapshot();
return await Task.FromResult(Ok());
}
}

View File

@@ -0,0 +1,19 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.PublishedCache;
namespace Umbraco.Cms.ManagementApi.Controllers.PublishedCache;
public class StatusPublishedCacheController : PublishedCacheControllerBase
{
private readonly IPublishedSnapshotStatus _publishedSnapshotStatus;
public StatusPublishedCacheController(IPublishedSnapshotStatus publishedSnapshotStatus)
=> _publishedSnapshotStatus = publishedSnapshotStatus;
[HttpGet("status")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
public async Task<ActionResult<string>> Status()
=> await Task.FromResult(Ok(_publishedSnapshotStatus.GetStatus()));
}

View File

@@ -0,0 +1,111 @@
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.Services.Paging;
using Umbraco.Cms.ManagementApi.ViewModels.Pagination;
using Umbraco.Cms.ManagementApi.ViewModels.RecycleBin;
namespace Umbraco.Cms.ManagementApi.Controllers.RecycleBin;
public abstract class RecycleBinControllerBase<TItem> : Controller
where TItem : RecycleBinItemViewModel, new()
{
private readonly IEntityService _entityService;
private readonly string _itemUdiType;
protected RecycleBinControllerBase(IEntityService entityService)
{
_entityService = entityService;
// ReSharper disable once VirtualMemberCallInConstructor
_itemUdiType = ItemObjectType.GetUdiType();
}
protected abstract UmbracoObjectTypes ItemObjectType { get; }
protected abstract int RecycleBinRootId { get; }
protected async Task<ActionResult<PagedViewModel<TItem>>> GetRoot(int skip, int take)
{
if (PaginationService.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize, out ProblemDetails? error) == false)
{
return BadRequest(error);
}
IEntitySlim[] rootEntities = GetPagedRootEntities(pageNumber, pageSize, out var totalItems);
TItem[] treeItemViewModels = MapRecycleBinViewModels(null, rootEntities);
PagedViewModel<TItem> result = PagedViewModel(treeItemViewModels, totalItems);
return await Task.FromResult(Ok(result));
}
protected async Task<ActionResult<PagedViewModel<TItem>>> GetChildren(Guid parentKey, int skip, int take)
{
if (PaginationService.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize, out ProblemDetails? error) == false)
{
return BadRequest(error);
}
IEntitySlim[] children = GetPagedChildEntities(parentKey, pageNumber, pageSize, out var totalItems);
TItem[] treeItemViewModels = MapRecycleBinViewModels(parentKey, children);
PagedViewModel<TItem> result = PagedViewModel(treeItemViewModels, totalItems);
return await Task.FromResult(Ok(result));
}
protected virtual TItem MapRecycleBinViewModel(Guid? parentKey, IEntitySlim entity)
{
if (entity == null)
{
throw new ArgumentNullException(nameof(entity));
}
var viewModel = new TItem
{
Icon = _itemUdiType,
Name = entity.Name!,
Key = entity.Key,
Type = _itemUdiType,
HasChildren = entity.HasChildren,
IsContainer = entity.IsContainer,
ParentKey = parentKey
};
return viewModel;
}
private IEntitySlim[] GetPagedRootEntities(long pageNumber, int pageSize, out long totalItems)
{
IEntitySlim[] rootEntities = _entityService
.GetPagedTrashedChildren(RecycleBinRootId, ItemObjectType, pageNumber, pageSize, out totalItems)
.ToArray();
return rootEntities;
}
private IEntitySlim[] GetPagedChildEntities(Guid parentKey, long pageNumber, int pageSize, out long totalItems)
{
IEntitySlim? parent = _entityService.Get(parentKey, ItemObjectType);
if (parent == null || parent.Trashed == false)
{
// not much else we can do here but return nothing
totalItems = 0;
return Array.Empty<IEntitySlim>();
}
IEntitySlim[] children = _entityService
.GetPagedTrashedChildren(parent.Id, ItemObjectType, pageNumber, pageSize, out totalItems)
.ToArray();
return children;
}
private TItem[] MapRecycleBinViewModels(Guid? parentKey, IEntitySlim[] entities)
=> entities.Select(entity => MapRecycleBinViewModel(parentKey, entity)).ToArray();
private PagedViewModel<TItem> PagedViewModel(IEnumerable<TItem> treeItemViewModels, long totalItems)
=> new() { Total = totalItems, Items = treeItemViewModels };
}

View File

@@ -0,0 +1,52 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.Factories;
using Umbraco.Cms.ManagementApi.ViewModels.Pagination;
using Umbraco.Cms.ManagementApi.ViewModels.Relation;
using Umbraco.Extensions;
namespace Umbraco.Cms.ManagementApi.Controllers.Relation;
public class ByChildRelationController : RelationControllerBase
{
private readonly IRelationService _relationService;
private readonly IRelationViewModelFactory _relationViewModelFactory;
public ByChildRelationController(
IRelationService relationService,
IRelationViewModelFactory relationViewModelFactory)
{
_relationService = relationService;
_relationViewModelFactory = relationViewModelFactory;
}
[HttpGet("childRelations/{childId:int}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<RelationViewModel>), StatusCodes.Status200OK)]
public async Task<PagedViewModel<RelationViewModel>> ByChild(int childId, int skip, int take, string? relationTypeAlias = "")
{
IRelation[] relations = _relationService.GetByChildId(childId).ToArray();
RelationViewModel[] result = Array.Empty<RelationViewModel>();
if (relations.Any())
{
if (string.IsNullOrWhiteSpace(relationTypeAlias) == false)
{
result = _relationViewModelFactory.CreateMultiple(relations.Where(x =>
x.RelationType.Alias.InvariantEquals(relationTypeAlias))).ToArray();
}
else
{
result = _relationViewModelFactory.CreateMultiple(relations).ToArray();
}
}
return await Task.FromResult(new PagedViewModel<RelationViewModel>
{
Total = result.Length,
Items = result.Skip(skip).Take(take),
});
}
}

View File

@@ -0,0 +1,35 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.Factories;
using Umbraco.Cms.ManagementApi.ViewModels.Relation;
namespace Umbraco.Cms.ManagementApi.Controllers.Relation;
public class ByIdRelationController : RelationControllerBase
{
private readonly IRelationService _relationService;
private readonly IRelationViewModelFactory _relationViewModelFactory;
public ByIdRelationController(IRelationService relationService, IRelationViewModelFactory relationViewModelFactory)
{
_relationService = relationService;
_relationViewModelFactory = relationViewModelFactory;
}
[HttpGet("{id:int}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(RelationViewModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(NotFoundResult), StatusCodes.Status404NotFound)]
public async Task<ActionResult> ById(int id)
{
IRelation? relation = _relationService.GetById(id);
if (relation is null)
{
return NotFound();
}
return await Task.FromResult(Ok(_relationViewModelFactory.Create(relation)));
}
}

View File

@@ -0,0 +1,15 @@
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Umbraco.New.Cms.Web.Common.Routing;
namespace Umbraco.Cms.ManagementApi.Controllers.Relation;
[ApiController]
[BackOfficeRoute("api/v{version:apiVersion}/relation")]
[OpenApiTag("Relation")]
[ApiVersion("1.0")]
// TODO: Implement Authentication
public abstract class RelationControllerBase : ManagementApiControllerBase
{
}

View File

@@ -0,0 +1,32 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
namespace Umbraco.Cms.ManagementApi.Controllers.RelationType.Tree;
public class ItemsRelationTypeTreeController : RelationTypeTreeControllerBase
{
private readonly IRelationService _relationService;
public ItemsRelationTypeTreeController(IEntityService entityService, IRelationService relationService)
: base(entityService) =>
_relationService = relationService;
[HttpGet("items")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<FolderTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<FolderTreeItemViewModel>>> Items([FromQuery(Name = "key")] Guid[] keys)
{
// relation service does not allow fetching a collection of relation types by their ids; instead it relies
// heavily on caching, which means this is as fast as it gets - even if it looks less than performant
IRelationType[] relationTypes = _relationService
.GetAllRelationTypes()
.Where(relationType => keys.Contains(relationType.Key)).ToArray();
EntityTreeItemViewModel[] viewModels = MapTreeItemViewModels(null, relationTypes);
return await Task.FromResult(Ok(viewModels));
}
}

View File

@@ -0,0 +1,38 @@
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.Controllers.Tree;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
using Umbraco.New.Cms.Web.Common.Routing;
namespace Umbraco.Cms.ManagementApi.Controllers.RelationType.Tree;
[ApiVersion("1.0")]
[ApiController]
[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.RelationType}/tree")]
[OpenApiTag(nameof(Constants.UdiEntityType.RelationType))]
// NOTE: at the moment relation types aren't supported by EntityService, so we have little use of the
// tree controller base. We'll keep it though, in the hope that we can mend EntityService.
public class RelationTypeTreeControllerBase : EntityTreeControllerBase<EntityTreeItemViewModel>
{
public RelationTypeTreeControllerBase(IEntityService entityService)
: base(entityService)
{
}
protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.RelationType;
protected EntityTreeItemViewModel[] MapTreeItemViewModels(Guid? parentKey, IRelationType[] relationTypes)
=> relationTypes.Select(relationType => new EntityTreeItemViewModel
{
Icon = Constants.Icons.RelationType,
Name = relationType.Name!,
Key = relationType.Key,
Type = Constants.UdiEntityType.RelationType,
HasChildren = false,
IsContainer = false,
ParentKey = parentKey
}).ToArray();
}

View File

@@ -0,0 +1,44 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.ManagementApi.Services.Paging;
using Umbraco.Cms.ManagementApi.ViewModels.Pagination;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
namespace Umbraco.Cms.ManagementApi.Controllers.RelationType.Tree;
public class RootRelationTypeTreeController : RelationTypeTreeControllerBase
{
private readonly IRelationService _relationService;
public RootRelationTypeTreeController(IEntityService entityService, IRelationService relationService)
: base(entityService) =>
_relationService = relationService;
[HttpGet("root")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<EntityTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<EntityTreeItemViewModel>>> Root(int skip = 0, int take = 100)
{
if (PaginationService.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize, out ProblemDetails? error) == false)
{
return BadRequest(error);
}
// pagination is not supported (yet) by relation service, so we do it in memory for now
// - chances are we won't have many relation types, so it won't be an actual issue
IRelationType[] allRelationTypes = _relationService.GetAllRelationTypes().ToArray();
EntityTreeItemViewModel[] viewModels = MapTreeItemViewModels(
null,
allRelationTypes
.OrderBy(relationType => relationType.Name)
.Skip((int)(pageNumber * pageSize))
.Take(pageSize)
.ToArray());
PagedViewModel<EntityTreeItemViewModel> result = PagedViewModel(viewModels, allRelationTypes.Length);
return await Task.FromResult(Ok(result));
}
}

View File

@@ -0,0 +1,21 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.ManagementApi.ViewModels.Pagination;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
namespace Umbraco.Cms.ManagementApi.Controllers.Script.Tree;
public class ChildrenScriptTreeController : ScriptTreeControllerBase
{
public ChildrenScriptTreeController(FileSystems fileSystems)
: base(fileSystems)
{
}
[HttpGet("children")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<FileSystemTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<FileSystemTreeItemViewModel>>> Children(string path, int skip = 0, int take = 100)
=> await GetChildren(path, skip, take);
}

View File

@@ -0,0 +1,20 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
namespace Umbraco.Cms.ManagementApi.Controllers.Script.Tree;
public class ItemsScriptTreeController : ScriptTreeControllerBase
{
public ItemsScriptTreeController(FileSystems fileSystems)
: base(fileSystems)
{
}
[HttpGet("items")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<FileSystemTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<FileSystemTreeItemViewModel>>> Items([FromQuery(Name = "path")] string[] paths)
=> await GetItems(paths);
}

View File

@@ -0,0 +1,21 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.ManagementApi.ViewModels.Pagination;
using Umbraco.Cms.ManagementApi.ViewModels.Tree;
namespace Umbraco.Cms.ManagementApi.Controllers.Script.Tree;
public class RootScriptTreeController : ScriptTreeControllerBase
{
public RootScriptTreeController(FileSystems fileSystems)
: base(fileSystems)
{
}
[HttpGet("root")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<FileSystemTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<FileSystemTreeItemViewModel>>> Root(int skip = 0, int take = 100)
=> await GetRoot(skip, take);
}

View File

@@ -0,0 +1,25 @@
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.ManagementApi.Controllers.Tree;
using Umbraco.New.Cms.Web.Common.Routing;
namespace Umbraco.Cms.ManagementApi.Controllers.Script.Tree;
[ApiVersion("1.0")]
[ApiController]
[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.Script}/tree")]
[OpenApiTag(nameof(Constants.UdiEntityType.Script))]
public class ScriptTreeControllerBase : FileSystemTreeControllerBase
{
public ScriptTreeControllerBase(FileSystems fileSystems)
=> FileSystem = fileSystems.ScriptsFileSystem ??
throw new ArgumentException("Missing scripts file system", nameof(fileSystems));
protected override IFileSystem FileSystem { get; }
protected override string FileIcon(string path) => Constants.Icons.Script;
protected override string ItemType(string path) => Constants.UdiEntityType.Script;
}

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