├── .config └── dotnet-tools.json ├── .editorconfig ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug.yml │ ├── config.yml │ └── feature_request.yml └── workflows │ └── release.yml ├── .gitignore ├── Directory.Build.props ├── LICENSE ├── UmbracoDeliveryApiExtensions.sln ├── UmbracoDeliveryApiExtensions.sln.DotSettings ├── docs ├── README.md ├── icon.png └── screenshots │ ├── api-preview.png │ ├── screenshot1.png │ └── typed-swagger-schema.png ├── nuget.config ├── src ├── UmbracoDeliveryApiExtensions.UI │ ├── .eslintrc.json │ ├── .gitignore │ ├── .vscode │ │ ├── extensions.json │ │ ├── launch.json │ │ └── settings.json │ ├── UmbracoDeliveryApiExtensions.UI.esproj │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── lang │ │ │ └── en.xml │ │ └── umbraco-package.json │ ├── src │ │ ├── conditions │ │ │ └── api-preview.view.condition.ts │ │ ├── config │ │ │ └── api-preview.config.ts │ │ ├── contexts │ │ │ ├── api-preview.context.ts │ │ │ └── api-preview.repository.ts │ │ ├── data │ │ │ ├── content-all.json │ │ │ └── content-none.json │ │ ├── elements │ │ │ ├── api-preview-section.element.ts │ │ │ ├── api-preview-workspace-view.element.ts │ │ │ ├── api-preview.element.ts │ │ │ ├── index.ts │ │ │ └── json-preview.element.tsx │ │ ├── events │ │ │ └── api-preview-content-changed.ts │ │ ├── helpers │ │ │ └── define-react-element.ts │ │ ├── index.css │ │ ├── main.dev.js │ │ ├── main.ts │ │ ├── mixins │ │ │ └── kebab-case-attributes.mixin.ts │ │ └── workspace-views │ │ │ └── api-preview.ts │ ├── tsconfig.json │ └── vite.config.ts └── UmbracoDeliveryApiExtensions │ ├── .gitignore │ ├── Composers │ └── DeliveryApiExtensionsComposer.cs │ ├── Configuration │ ├── Options │ │ ├── DeliveryApiExtensionsOptions.cs │ │ ├── MediaOptions.cs │ │ ├── PreviewOptions.cs │ │ ├── SwaggerGenerationMode.cs │ │ └── TypedSwaggerOptions.cs │ ├── OptionsExtensions.cs │ └── UmbracoBuilderExtensions.cs │ ├── Constants.cs │ ├── Controllers │ ├── BaseController.cs │ ├── Models │ │ └── PreviewConfig.cs │ └── PreviewController.cs │ ├── Models │ ├── ContentTypeInfo.cs │ └── ContentTypePropertyInfo.cs │ ├── Services │ └── ContentTypeInfoService.cs │ ├── Swagger │ ├── DeliveryApiContentTypesSchemaFilter.cs │ ├── FixPropertyNullabilityFilter.cs │ └── SwaggerGenerationSettings.cs │ ├── UmbracoDeliveryApiExtensions.csproj │ ├── appsettings-schema.DeliveryApiExtensions.json │ └── buildTransitive │ ├── DeliveryApiExtensions.AppSettingsSchema.props │ └── Umbraco.Community.DeliveryApiExtensions.props ├── tests ├── UmbracoDeliveryApiExtensions.Playwright │ ├── .env │ ├── .eslintrc.json │ ├── .gitignore │ ├── .vscode │ │ ├── launch.json │ │ └── settings.json │ ├── UmbracoDeliveryApiExtensions.Playwright.esproj │ ├── fixtures │ │ └── mediaLibrary │ │ │ └── File.txt │ ├── package-lock.json │ ├── package.json │ ├── playwright.config.ts │ ├── tests │ │ ├── auth.setup.ts │ │ └── preview │ │ │ ├── preview-content.spec.ts │ │ │ └── preview-media.spec.ts │ └── tsconfig.json ├── UmbracoDeliveryApiExtensions.TestSite │ ├── .config │ │ └── dotnet-tools.json │ ├── .gitignore │ ├── Custom │ │ └── AlwaysEnabledSwaggerPipelineFilter.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── UmbracoDeliveryApiExtensions.TestSite.csproj │ ├── Views │ │ ├── Partials │ │ │ ├── blockgrid │ │ │ │ ├── area.cshtml │ │ │ │ ├── areas.cshtml │ │ │ │ ├── default.cshtml │ │ │ │ └── items.cshtml │ │ │ ├── blocklist │ │ │ │ └── default.cshtml │ │ │ └── grid │ │ │ │ ├── bootstrap3-fluid.cshtml │ │ │ │ ├── bootstrap3.cshtml │ │ │ │ └── editors │ │ │ │ ├── base.cshtml │ │ │ │ ├── embed.cshtml │ │ │ │ ├── macro.cshtml │ │ │ │ ├── media.cshtml │ │ │ │ ├── rte.cshtml │ │ │ │ └── textstring.cshtml │ │ └── _ViewImports.cshtml │ ├── appsettings.Development.json │ ├── appsettings.json │ ├── delivery-compat.swagger.g.json │ ├── delivery.swagger.g.json │ ├── uSync │ │ └── v15 │ │ │ ├── Content │ │ │ ├── test-invariant.config │ │ │ └── test.config │ │ │ ├── ContentTypes │ │ │ ├── blocksettings.config │ │ │ ├── testblock.config │ │ │ ├── testblock2.config │ │ │ ├── testcomposition.config │ │ │ ├── testcomposition2.config │ │ │ ├── testpage.config │ │ │ └── testpageinvariant.config │ │ │ ├── DataTypes │ │ │ ├── ApprovedColor.config │ │ │ ├── BlockGrid.config │ │ │ ├── BlockList.config │ │ │ ├── CheckboxList.config │ │ │ ├── ContentPicker.config │ │ │ ├── DatePicker.config │ │ │ ├── DatePickerWithTime.config │ │ │ ├── Decimal.config │ │ │ ├── Dropdown.config │ │ │ ├── DropdownMultiple.config │ │ │ ├── EmailAddress.config │ │ │ ├── EyeDropperColorPicker.config │ │ │ ├── ImageCropper.config │ │ │ ├── ImageMediaPicker.config │ │ │ ├── LabelBigint.config │ │ │ ├── LabelDatetime.config │ │ │ ├── LabelDecimal.config │ │ │ ├── LabelInteger.config │ │ │ ├── LabelString.config │ │ │ ├── LabelTime.config │ │ │ ├── ListViewContent.config │ │ │ ├── ListViewMedia.config │ │ │ ├── ListViewMembers.config │ │ │ ├── MarkdownEditor.config │ │ │ ├── MediaPicker.config │ │ │ ├── MemberGroupPicker.config │ │ │ ├── MemberPicker.config │ │ │ ├── MultiURLPicker.config │ │ │ ├── MultinodeTreepicker.config │ │ │ ├── MultipleImageMediaPicker.config │ │ │ ├── MultipleMediaPicker.config │ │ │ ├── Numeric.config │ │ │ ├── Radiobox.config │ │ │ ├── RepeatableTextstrings.config │ │ │ ├── RichtextEditor.config │ │ │ ├── Slider.config │ │ │ ├── Tags.config │ │ │ ├── Textarea.config │ │ │ ├── Textstring.config │ │ │ ├── Truefalse.config │ │ │ ├── UploadArticle.config │ │ │ ├── UploadAudio.config │ │ │ ├── UploadFile.config │ │ │ ├── UploadVectorGraphics.config │ │ │ ├── UploadVideo.config │ │ │ └── UserPicker.config │ │ │ ├── Domains │ │ │ ├── _en-us.config │ │ │ ├── invariant_en-us.config │ │ │ └── pt_pt-pt.config │ │ │ ├── Languages │ │ │ ├── en-us.config │ │ │ └── pt-pt.config │ │ │ ├── Media │ │ │ └── dark-blue.config │ │ │ ├── MediaTypes │ │ │ ├── file.config │ │ │ ├── folder.config │ │ │ ├── image.config │ │ │ ├── umbracomediaarticle.config │ │ │ ├── umbracomediaaudio.config │ │ │ ├── umbracomediavectorgraphics.config │ │ │ └── umbracomediavideo.config │ │ │ ├── MemberTypes │ │ │ └── member.config │ │ │ └── usync.config │ └── wwwroot │ │ ├── favicon.ico │ │ └── media │ │ ├── 0hljflgw │ │ └── pink.jpg │ │ ├── fw2bqwqg │ │ └── pink.jpg │ │ ├── mvvhicln │ │ └── dark-blue.jpg │ │ ├── otgga1kg │ │ └── dark-blue.jpg │ │ └── q0yb0acw │ │ └── dark-blue.jpg └── clients │ ├── nswag │ ├── ClientTemplateOverrides │ │ └── File.Header.liquid │ ├── Program.cs │ ├── UmbracoApi.g.cs │ └── nswag.csproj │ ├── openapi-typescript │ ├── .eslintrc.json │ ├── .gitignore │ ├── .vscode │ │ ├── launch.json │ │ └── settings.json │ ├── README.md │ ├── api │ │ └── umbraco-api.d.ts │ ├── app.ts │ ├── openapi-typescript.esproj │ ├── package-lock.json │ ├── package.json │ └── tsconfig.json │ └── orval │ ├── .eslintrc.json │ ├── .gitignore │ ├── .vscode │ ├── launch.json │ └── settings.json │ ├── README.md │ ├── api │ └── umbraco-api.ts │ ├── app.ts │ ├── orval.config.ts │ ├── orval.esproj │ ├── package-lock.json │ ├── package.json │ └── tsconfig.json ├── tools └── UmbracoDeliveryApiExtensions.JsonSchemaGenerator │ ├── CommandLineArguments.cs │ ├── PrefixedSchemaNameGenerator.cs │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ └── UmbracoDeliveryApiExtensions.JsonSchemaGenerator.csproj └── umbraco-marketplace.json /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": {} 5 | } -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 📝 2 | 3 | Contributions to this package are very welcome! 🙌 4 | 5 | ## Running locally 🧑‍💻 6 | 7 | > [!IMPORTANT] 8 | > **Requirements** 9 | > - .NET 7 10 | > - Node 20+ (using [Volta](https://volta.sh/) is recommended to ensure you always have the right version ✨) 11 | > 12 | 13 | **Visual Studio** 14 | When using VS everything should just work, running the test website (`tests\UmbracoDeliveryApiExtensions.TestSite`) should automaticaly build both the UI and the package. 15 | 16 | **Command line** 17 | Running `dotnet run` in the test website path (`tests\UmbracoDeliveryApiExtensions.TestSite`) should automaticaly build both the UI and the package. 18 | 19 | There are also other helpful npm scripts to just run the UI, watch for file changes, among others, so check the different projects `package.json` to see what is already set up. 20 | 21 | **Backoffice credentials** 22 | Username: `admin@umbraco` 23 | Password: `#Umbraco123!` 24 | 25 | ## Project structure 26 | 27 | ### Back-end 🦾 28 | 29 | **.NET Library**: `src\UmbracoDeliveryApiExtensions\UmbracoDeliveryApiExtensions.csproj` 30 | 31 | ### Front-end 🌻 32 | 33 | **Vite + Lit**: `src\UmbracoDeliveryApiExtensions.UI` 34 | When building the back-end library, the front-end is automatically built and its output is copied over to the back-end library `wwwroot` folder. 35 | If you want to make changes to the front-end it might be useful to run the test website and do `npm run watch` on the front-end folder so that you can see your changes in real time. 36 | 37 | ### Tests 🐞 38 | 39 | For end-to-end testing, we have a [Playwright](https://playwright.dev) project set up with a few tests in `tests\UmbracoDeliveryApiExtensions.Playwright`. 40 | 41 | #### Testing the typed swagger 🪄 42 | 43 | In order to test changes in the typed swagger feature, we have added different client projects in `tests\clients`. Each projects uses a different client generation tool. 44 | 45 | For the typescript projects, you can simply run `npm install` on each one of them and then `npm run start`. These are basically console apps, and the `start` command will both generate the client and run some sample code which is using it. If any step of the command fails, then something might have broken! 🤐 (or fixed, who knows 😅) 46 | 47 | For the `nswag` one, that is also a console app but built in .NET, so you can simply run it using `dotnet run` (or through Visual Studio). 🙌 48 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: "Bug report" 2 | description: "File a bug report to help improve this package." 3 | labels: ["bug"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking the time to report this issue as thoroughly as possible. 9 | - type: input 10 | id: "PackageVersion" 11 | attributes: 12 | label: "Which version of the package are you using?" 13 | description: "Leave blank if you're not sure: the latest version will be assumed." 14 | validations: 15 | required: false 16 | - type: input 17 | id: "umbracoVersion" 18 | attributes: 19 | label: "Which Umbraco version are you using? For example: 12.2.0 - don't just write v12" 20 | description: "Use the help icon in the Umbraco backoffice to find the version you're using." 21 | validations: 22 | required: true 23 | - type: textarea 24 | id: "summary" 25 | attributes: 26 | label: "Bug summary" 27 | description: "Write a summary of the bug." 28 | placeholder: > 29 | Try to pinpoint it as much as possible. 30 | 31 | Try to state the actual problem, and not just what you think the solution might be. 32 | 33 | (Remember that you can format code and logs nicely with the `<>` button) 34 | validations: 35 | required: true 36 | - type: textarea 37 | attributes: 38 | label: "Steps to reproduce" 39 | description: "How can we reproduce the problem on a clean AdminOnlyPackage + Umbraco install?" 40 | placeholder: > 41 | Please include any links, screenshots, stack-traces, etc. 42 | validations: 43 | required: true 44 | - type: textarea 45 | attributes: 46 | label: "Expected result / actual result" 47 | description: "What did you expect that would happen on your Umbraco site and what is the actual result of the above steps?" 48 | placeholder: > 49 | Describe the intended/desired outcome after you did the steps mentioned. 50 | 51 | Describe the behaviour of the bug -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: "Feature request" 2 | description: "Suggest an idea for this package." 3 | labels: ["enhancement"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking the time to suggest a feature! 9 | - type: textarea 10 | id: "summary" 11 | attributes: 12 | label: "Feature summary" 13 | description: "Write a brief summary of the feature" 14 | validations: 15 | required: true 16 | - type: textarea 17 | id: "details" 18 | attributes: 19 | label: "Additional details" 20 | description: "Provide any additional details or comments about the feature you are suggesting" 21 | validations: 22 | required: false -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build and release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v[0-9]+.[0-9]+.[0-9]+' 7 | - 'v[0-9]+.[0-9]+.[0-9]+-[a-z]+.[0-9]+' 8 | branches: [ '*/main', '*/dev' ] 9 | pull_request: 10 | branches: [ '*/main', '*/dev' ] 11 | 12 | env: 13 | PKG_VERSION: ${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || null }} 14 | NODE_VERSION: 22 15 | DOTNET_VERSION: 9.x 16 | 17 | jobs: 18 | build: 19 | name: Build 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v4 24 | 25 | - name: Set version 26 | if: ${{ !env.PKG_VERSION }} 27 | run: | 28 | calculatedSha=$(git rev-parse --short ${{ github.sha }}) 29 | echo "PKG_VERSION=0.0.0-preview.${{github.run_number}}.$calculatedSha" >> $GITHUB_ENV 30 | 31 | - name: Setup npm 32 | uses: actions/setup-node@v4 33 | with: 34 | node-version: ${{env.NODE_VERSION}} 35 | cache: 'npm' 36 | cache-dependency-path: '**/package-lock.json' 37 | 38 | - name: Setup .NET 39 | uses: actions/setup-dotnet@v4 40 | with: 41 | dotnet-version: ${{env.DOTNET_VERSION}} 42 | 43 | - name: Build project 44 | run: dotnet build src/UmbracoDeliveryApiExtensions 45 | -c Release 46 | /p:ContinuousIntegrationBuild=true 47 | /p:Version=${PKG_VERSION#v} 48 | 49 | - name: Upload package as artifact 50 | uses: actions/upload-artifact@v4 51 | with: 52 | name: artifact 53 | if-no-files-found: error 54 | path: | 55 | **/*.nupkg 56 | **/*.snupkg 57 | 58 | tests: 59 | name: Tests 60 | runs-on: ubuntu-latest 61 | needs: build 62 | steps: 63 | - name: Checkout repository 64 | uses: actions/checkout@v4 65 | 66 | - name: Setup npm 67 | uses: actions/setup-node@v4 68 | with: 69 | node-version: ${{env.NODE_VERSION}} 70 | cache: 'npm' 71 | cache-dependency-path: '**/package-lock.json' 72 | 73 | - name: Setup .NET 74 | uses: actions/setup-dotnet@v4 75 | with: 76 | dotnet-version: ${{env.DOTNET_VERSION}} 77 | 78 | - name: Run Test website 79 | env: 80 | LANG: en_US.UTF-8 81 | run: | 82 | timeout 300 grep -q 'uSync: Startup Complete' <(dotnet run --project tests/UmbracoDeliveryApiExtensions.TestSite --launch-profile CI | tee -p /dev/fd/2) 83 | 84 | - name: Install packages 85 | working-directory: tests 86 | run: | 87 | find . -maxdepth 3 -name package.json -execdir npm ci \; 88 | 89 | - name: Run Orval client 90 | working-directory: tests/clients/orval 91 | run: | 92 | npm run start 93 | 94 | - name: Run openapi-typescript client 95 | working-directory: tests/clients/openapi-typescript 96 | run: | 97 | npm run start 98 | 99 | - name: Run NSwag client 100 | working-directory: tests/clients/nswag 101 | run: dotnet run -c Release 102 | 103 | - name: Run Playwright tests 104 | working-directory: tests/UmbracoDeliveryApiExtensions.Playwright 105 | run: | 106 | npm run test 107 | 108 | release: 109 | name: Release 110 | runs-on: ubuntu-latest 111 | if: ${{ startsWith(github.ref, 'refs/tags/') }} 112 | needs: [build, tests] 113 | steps: 114 | 115 | - name: Download artifact from build 116 | uses: actions/download-artifact@v4 117 | with: 118 | name: artifact 119 | 120 | - name: Push to NuGet 121 | run: dotnet nuget push **/*.nupkg --api-key ${{secrets.NUGET_API_KEY}} --source https://api.nuget.org/v3/index.json 122 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | net9.0 4 | enable 5 | enable 6 | Nullable 7 | true 8 | 9 | 10 | -------------------------------------------------------------------------------- /UmbracoDeliveryApiExtensions.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | True -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | 4 | Delivery Api Extensions logo 5 | 6 | 7 | # Umbraco Delivery Api Extensions 8 | 9 | [![Downloads](https://img.shields.io/nuget/dt/Umbraco.Community.DeliveryApiExtensions?color=cc9900)](https://www.nuget.org/packages/Umbraco.Community.DeliveryApiExtensions/) 10 | [![NuGet](https://img.shields.io/nuget/vpre/Umbraco.Community.DeliveryApiExtensions?color=0273B3)](https://www.nuget.org/packages/Umbraco.Community.DeliveryApiExtensions) 11 | [![GitHub license](https://img.shields.io/github/license/ByteCrumb/Umbraco.Community.DeliveryApiExtensions?color=8AB803)](../LICENSE) 12 | 13 | Extensions for the Umbraco Delivery API. 14 | 15 | ## Features ✨ 16 | 17 | ### Backoffice preview 18 | Preview the Delivery API responses from the backoffice content/media nodes. 19 | 20 | ![Preview](https://raw.githubusercontent.com/ByteCrumb/Umbraco.Community.DeliveryApiExtensions/v13/main/docs/screenshots/api-preview.png) 21 | 22 | ### Typed swagger 23 | Adds types to the Umbraco swagger based on your document and data types (just like Models Builder), so that you can more seamlessly generate typed clients. 24 | 25 | Example of a Node console app using the types/functions generated by a typescript restful client generator using the typed swagger. 26 | ```ts 27 | import { getContentItemByPath } from './api/umbraco-api'; 28 | 29 | const content = (await getContentItemByPath('/')).data; 30 | 31 | // Content can be of any document type here 32 | 33 | if (content.contentType === 'home') { 34 | // By checking the contentType, Typescript knows this is a Home page 35 | // and properly validates the properties and their types 36 | console.log(`Name: ${content.name}`); 37 | console.log(`Title: ${content.properties?.title}`); 38 | console.log(`Text: ${content.properties?.text?.markup}`); 39 | } 40 | ``` 41 | 42 | ![Typed Swagger](https://raw.githubusercontent.com/ByteCrumb/Umbraco.Community.DeliveryApiExtensions/v13/main/docs/screenshots/typed-swagger-schema.png) 43 | 44 | ## Installation 🧑‍💻 45 | 46 | Add the package to an existing Umbraco website (v12.2+) from nuget: 47 | 48 | ```sh 49 | dotnet add package Umbraco.Community.DeliveryApiExtensions 50 | ``` 51 | 52 | ### Configuration (appsettings.json) 53 | 54 | The following represents the default configuration, which can optionally be overriden by defining it in your own app settings. 55 | ```jsonc 56 | { 57 | "DeliveryApiExtensions": { 58 | "Preview": { 59 | "Enabled": true, 60 | "Media": { 61 | "Enabled": true 62 | }, 63 | "AllowedUserGroupAliases": [], // All allowed by default 64 | }, 65 | "TypedSwagger": { 66 | "Enabled": true, 67 | "Mode": "Auto" 68 | } 69 | } 70 | } 71 | ``` 72 | 73 | **Typed swagger modes** 74 | 75 | - **Automatic (Auto)** - Swagger will be generated with the `UseOneOfForPolymorphism` and `UseAllOfForInheritance` options. 76 | Suitable for most generators like [openapi-typescript](https://openapi-ts.pages.dev) and [orval](https://orval.dev). 77 | 78 | - **Compatibility** - Swagger will be generated with only the `UseAllOfForInheritance` option enabled. 79 | Suitable for generators that don't support polymorphism using OneOf like [NSwag](https://github.com/RicoSuter/NSwag). 80 | 81 | - **Manual** - Swagger options will not be configured, allowing full customization. 82 | It can be configured from your codebase using: 83 | ```csharp 84 | services.Configure(options => ...) 85 | services.Configure(options => ...) 86 | ``` 87 | 88 | ## Contributing 🙌 89 | 90 | Contributions to this package are most welcome! Please read the [Contributing Guidelines](https://github.com/ByteCrumb/Umbraco.Community.DeliveryApiExtensions/blob/HEAD/.github/CONTRIBUTING.md). 91 | -------------------------------------------------------------------------------- /docs/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ByteCrumb/Umbraco.Community.DeliveryApiExtensions/c54639acb3f164da022f96cff3365c059f365d64/docs/icon.png -------------------------------------------------------------------------------- /docs/screenshots/api-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ByteCrumb/Umbraco.Community.DeliveryApiExtensions/c54639acb3f164da022f96cff3365c059f365d64/docs/screenshots/api-preview.png -------------------------------------------------------------------------------- /docs/screenshots/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ByteCrumb/Umbraco.Community.DeliveryApiExtensions/c54639acb3f164da022f96cff3365c059f365d64/docs/screenshots/screenshot1.png -------------------------------------------------------------------------------- /docs/screenshots/typed-swagger-schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ByteCrumb/Umbraco.Community.DeliveryApiExtensions/c54639acb3f164da022f96cff3365c059f365d64/docs/screenshots/typed-swagger-schema.png -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions.UI/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": "xo-space", 7 | "overrides": [ 8 | { 9 | "extends": [ 10 | "xo-typescript/space", 11 | "plugin:wc/recommended", 12 | "plugin:lit/recommended", 13 | "plugin:lit-a11y/recommended" 14 | ], 15 | "files": ["*.ts", "*.tsx"], 16 | "plugins": ["simple-import-sort"], 17 | "rules": { 18 | "@typescript-eslint/consistent-type-definitions": [ 19 | "warn", "interface" 20 | ], 21 | "@typescript-eslint/naming-convention": [ 22 | "error", 23 | { 24 | "selector": ["class", "interface", "typeAlias", "enum", "typeParameter"], 25 | "format": ["StrictPascalCase"], 26 | "filter": { 27 | "regex": "^(HTMLElementTagNameMap|HTMLElementEventMap|HTML[A-Za-z]{0,}Element|UIEvent|UIEventInit|DOMError|WebUIListenerBehavior|I([A-Z][a-z]+)+)$", 28 | "match": false 29 | } 30 | } 31 | ], 32 | "@typescript-eslint/no-namespace": "off", 33 | "capitalized-comments": "off", 34 | "new-cap": [ 35 | "error", 36 | { 37 | "capIsNewExceptionPattern": "Mixin$" 38 | } 39 | ], 40 | "simple-import-sort/imports": "error", 41 | "simple-import-sort/exports": "error" 42 | }, 43 | "overrides": [ 44 | { 45 | "files": ["*.controller.ts"], 46 | "rules": { 47 | "@typescript-eslint/no-this-alias": ["off"], 48 | "max-params": ["off"] 49 | } 50 | } 51 | ] 52 | } 53 | ], 54 | "parserOptions": { 55 | "ecmaVersion": "latest", 56 | "sourceType": "module" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions.UI/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | !.vscode/launch.json 19 | !.vscode/settings.json 20 | .idea 21 | .DS_Store 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions.UI/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "runem.lit-plugin", 4 | "dbaeumer.vscode-eslint", 5 | "editorconfig.editorconfig" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions.UI/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "runtimeExecutable": "npm", 8 | "runtimeArgs": [ "run", "watch" ], 9 | "cwd": "${workspaceFolder}", 10 | "console": "externalTerminal", 11 | "name": "Watch" 12 | }, 13 | { 14 | "type": "chrome", 15 | "request": "launch", 16 | "url": "http://localhost:5173", 17 | "webRoot": "${workspaceFolder}", 18 | "cwd": "${workspaceFolder}", 19 | "name": "Launch Chrome" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions.UI/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": "explicit" 4 | }, 5 | "typescript.tsdk": "node_modules\\typescript\\lib" 6 | } 7 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions.UI/UmbracoDeliveryApiExtensions.UI.esproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | npm run dev 4 | npm run clean 5 | false 6 | Umbraco.Community.DeliveryApiExtensions 7 | $(MSBuildProjectDirectory)\dist 8 | 9 | 10 | 11 | 12 | App_Plugins 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions.UI/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Test UI 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 22 |
23 | 34 |
35 |
36 | 37 | 38 | Content 39 | Info 40 | Actions 41 | 42 |
43 | 44 | 45 | 52 | 53 |
54 | Save and preview 55 | Save 56 | Save and publish 57 |
58 |
59 |
60 |
61 |
62 | 63 | 64 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions.UI/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "delivery-api-extensions", 3 | "private": true, 4 | "version": "1.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite --base /", 8 | "dev:clean": "vite --force --base /", 9 | "build": "tsc && vite build", 10 | "watch": "chokidar src public -c \"tsc && vite build\" --initial", 11 | "clean": "node -e \"['dist', 'obj', 'node_modules'].forEach(f => require('node:fs').rmSync(f, { recursive: true, force: true }))\"" 12 | }, 13 | "dependencies": { 14 | "@lit/context": "^1.1.3", 15 | "@lit/task": "^1.0.1", 16 | "@r2wc/react-to-web-component": "^2.0.3", 17 | "@uiw/react-json-view": "^2.0.0-alpha.30", 18 | "lit": "^3.2.1", 19 | "preact": "^10.24.3" 20 | }, 21 | "devDependencies": { 22 | "@types/js-cookie": "^3.0.6", 23 | "@types/react": "^18.3.12", 24 | "@typescript-eslint/eslint-plugin": "^8.14.0", 25 | "@typescript-eslint/parser": "^8.14.0", 26 | "@umbraco-cms/backoffice": "^15.0.0", 27 | "chokidar-cli": "^3.0.0", 28 | "eslint": "^9.14.0", 29 | "eslint-config-xo-space": "^0.35.0", 30 | "eslint-config-xo-typescript": "^7.0.0", 31 | "eslint-plugin-lit": "^1.15.0", 32 | "eslint-plugin-lit-a11y": "^4.1.4", 33 | "eslint-plugin-n": "^17.13.1", 34 | "eslint-plugin-promise": "^7.1.0", 35 | "eslint-plugin-simple-import-sort": "^12.1.1", 36 | "eslint-plugin-wc": "^2.2.0", 37 | "typescript": "^5.6.3", 38 | "vite": "^5.4.11" 39 | }, 40 | "volta": { 41 | "node": "20.18.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions.UI/public/lang/en.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions.UI/public/umbraco-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../node_modules/@umbraco-cms/backoffice/dist-cms/umbraco-package-schema.json", 3 | "name": "DeliveryApiExtensions", 4 | "version": "0.1.0", 5 | "extensions": [ 6 | { 7 | "type": "backofficeEntryPoint", 8 | "alias": "DeliveryApiExtensions.EntryPoint", 9 | "name": "Delivery Api Extensions", 10 | "js": "/App_Plugins/DeliveryApiExtensions/delivery-api-extensions.js" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions.UI/src/conditions/api-preview.view.condition.ts: -------------------------------------------------------------------------------- 1 | import { UmbControllerHost } from "@umbraco-cms/backoffice/controller-api"; 2 | import { 3 | ManifestCondition, 4 | UmbConditionConfigBase, 5 | UmbConditionControllerArguments, 6 | UmbExtensionCondition, 7 | } from "@umbraco-cms/backoffice/extension-api"; 8 | import { UmbConditionBase } from "@umbraco-cms/backoffice/extension-registry"; 9 | 10 | export type ApiPreviewViewConfig = UmbConditionConfigBase; 11 | 12 | export class ApiPreviewViewCondition 13 | extends UmbConditionBase 14 | implements UmbExtensionCondition 15 | { 16 | constructor( 17 | host: UmbControllerHost, 18 | args: UmbConditionControllerArguments 19 | ) { 20 | super(host, args); 21 | 22 | // TODO: Check if the document or media item is "previewable" 23 | this.permitted = true; 24 | args.onChange(); 25 | } 26 | } 27 | 28 | export const manifest: ManifestCondition = { 29 | type: "condition", 30 | name: "API Preview View Condition", 31 | alias: "DeliveryApiExtensions.ApiPreview.View", 32 | api: ApiPreviewViewCondition, 33 | }; 34 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions.UI/src/config/api-preview.config.ts: -------------------------------------------------------------------------------- 1 | export type ApiPreviewConfig = { 2 | enabled: boolean, 3 | media: { 4 | enabled: boolean, 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions.UI/src/contexts/api-preview.context.ts: -------------------------------------------------------------------------------- 1 | import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; 2 | import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; 3 | import { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; 4 | import { ApiPreviewRepository } from './api-preview.repository'; 5 | 6 | export class ApiPreviewContext extends UmbControllerBase { 7 | #repo: ApiPreviewRepository; 8 | 9 | #type: ApiPreviewContentType = ApiPreviewContentType.Document; 10 | #culture? : string | undefined; 11 | #uniqueId? : string | undefined; 12 | 13 | constructor( 14 | host: UmbControllerHost) { 15 | super(host); 16 | this.#repo = new ApiPreviewRepository(host); 17 | } 18 | 19 | setType(type: ApiPreviewContentType) { 20 | this.#type = type; 21 | } 22 | 23 | setUniqueId(uniqueId: string) { 24 | this.#uniqueId = uniqueId; 25 | } 26 | 27 | getCulture() { 28 | return this.#culture; 29 | } 30 | 31 | setCulture(culture: string | undefined) { 32 | this.#culture = culture; 33 | } 34 | 35 | async fetchData(preview: boolean, expand: boolean, signal: AbortSignal): Promise { 36 | if(!this.#uniqueId) return null; 37 | return this.#repo.fetchData(this.#type, this.#uniqueId, this.#culture, preview, expand, signal); 38 | } 39 | } 40 | 41 | export enum ApiPreviewContentType { 42 | Document = 'content', 43 | Media = 'media', 44 | } 45 | 46 | export const API_PREVIEW_CONTEXT = 47 | new UmbContextToken('api-preview-context'); 48 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions.UI/src/contexts/api-preview.repository.ts: -------------------------------------------------------------------------------- 1 | import { UMB_AUTH_CONTEXT } from "@umbraco-cms/backoffice/auth"; 2 | import { UmbControllerBase } from "@umbraco-cms/backoffice/class-api"; 3 | import { UmbControllerHost } from "@umbraco-cms/backoffice/controller-api"; 4 | import { ApiPreviewContentType } from "./api-preview.context"; 5 | import { ApiPreviewConfig } from "../config/api-preview.config"; 6 | import { UmbContextConsumerController } from "@umbraco-cms/backoffice/context-api"; 7 | 8 | export class ApiPreviewRepository extends UmbControllerBase { 9 | #apiPath: string = ''; 10 | #getToken: () => Promise = async () => ''; 11 | #init: Promise; 12 | #contextConsumer; 13 | 14 | constructor(host: UmbControllerHost) { 15 | super(host); 16 | 17 | this.#contextConsumer = new UmbContextConsumerController(this, UMB_AUTH_CONTEXT, (_auth) => { 18 | const umbOpenApi = _auth.getOpenApiConfiguration(); 19 | this.#getToken = umbOpenApi.token; 20 | this.#apiPath = `${umbOpenApi.base}/umbraco/delivery-api-extensions/preview`; 21 | }); 22 | this.#init = this.#contextConsumer.asPromise(); 23 | } 24 | 25 | async fetchData( 26 | type: ApiPreviewContentType, 27 | uniqueId: string, 28 | culture: string | undefined, 29 | preview: boolean, 30 | expand: boolean, 31 | signal: AbortSignal) 32 | : Promise { 33 | if (!this.#apiPath) { 34 | return null; 35 | } 36 | await this.#init; 37 | const params: RequestInit & {headers: Record} = { 38 | method: 'GET', 39 | headers: { 40 | 'Authorization': `Bearer ${await this.#getToken()}`, 41 | }, 42 | credentials: 'include', 43 | signal, 44 | }; 45 | 46 | if (culture) { 47 | params.headers['Accept-Language'] = culture; 48 | } 49 | 50 | if (preview) { 51 | params.headers.preview = 'true'; 52 | } 53 | 54 | const response = await fetch(`${this.#apiPath}/${type}/${uniqueId}${(expand ? '?expand=properties[$all]' : '')}`, params); 55 | if (!response.ok) { 56 | throw new Error(response.statusText); 57 | } 58 | 59 | return response.json(); 60 | } 61 | 62 | async fetchConfig() 63 | : Promise { 64 | await this.#init; 65 | if (!this.#apiPath) { 66 | return null; 67 | } 68 | 69 | const params: RequestInit & {headers: Record} = { 70 | method: 'GET', 71 | headers: { 72 | 'Authorization': `Bearer ${await this.#getToken()}`, 73 | }, 74 | credentials: 'include' 75 | }; 76 | 77 | const response = await fetch(`${this.#apiPath}/config`, params); 78 | return response.json(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions.UI/src/data/content-none.json: -------------------------------------------------------------------------------- 1 | )]}', 2 | {"Cultures":{"en-us":{"Path":"/","StartItem":{"Id":"3b6a13d3-df48-43f4-b9b3-0d27d3765be6","Path":"home"}}},"Name":"Home","CreateDate":"2023-10-28T11:35:35","UpdateDate":"2023-10-31T18:45:35.4442349","Route":{"Path":"/","StartItem":{"Id":"3b6a13d3-df48-43f4-b9b3-0d27d3765be6","Path":"home"}},"Id":"3b6a13d3-df48-43f4-b9b3-0d27d3765be6","ContentType":"pageHome","Properties":{"text":null}} 3 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions.UI/src/elements/api-preview-section.element.ts: -------------------------------------------------------------------------------- 1 | import { Task } from "@lit/task"; 2 | import { css, html, LitElement } from "lit"; 3 | import { customElement, property, query, state } from "lit/decorators.js"; 4 | import { cache } from "lit/directives/cache.js"; 5 | import { ifDefined } from "lit/directives/if-defined.js"; 6 | import { UmbElementMixin } from "@umbraco-cms/backoffice/element-api"; 7 | 8 | import { API_PREVIEW_CONTEXT } from "../contexts/api-preview.context"; 9 | import { KebabCaseAttributesMixin } from "../mixins/kebab-case-attributes.mixin"; 10 | import { ApiPreviewContentChangedEvent } from "../events/api-preview-content-changed"; 11 | import { UMB_THEME_CONTEXT } from "@umbraco-cms/backoffice/themes"; 12 | 13 | export * from "./json-preview.element"; 14 | 15 | /** 16 | * The Delivery Api Extensions Preview Tab element. 17 | */ 18 | @customElement("bc-api-preview-section") 19 | export class ApiPreviewElementSection extends UmbElementMixin( 20 | KebabCaseAttributesMixin(LitElement) 21 | ) { 22 | static styles = css` 23 | :host { 24 | display: flex; 25 | } 26 | 27 | uui-box { 28 | flex: 1; 29 | overflow: auto; 30 | display: grid; 31 | grid-template-rows: max-content minmax(150px, auto); 32 | } 33 | 34 | .centered { 35 | display: flex; 36 | justify-content: center; 37 | align-items: center; 38 | height: 100%; 39 | } 40 | `; 41 | 42 | #context?: typeof API_PREVIEW_CONTEXT.TYPE; 43 | 44 | @property({ type: String }) 45 | headline = ""; 46 | 47 | @property({ type: Boolean }) 48 | preview = false; 49 | 50 | @query("bc-json-preview") 51 | jsonPreviewElement?: HTMLElement; 52 | 53 | @state() 54 | private _expand = false; 55 | 56 | @state() 57 | private _theme = "light"; 58 | 59 | private readonly _dataTask = new Task(this, { 60 | task: async ([preview, expand], { signal }) => 61 | this.#context?.fetchData(preview, expand, signal), 62 | args: (): [boolean, boolean] => [this.preview, this._expand], 63 | }); 64 | 65 | constructor() { 66 | super(); 67 | 68 | this.consumeContext(API_PREVIEW_CONTEXT, (context) => { 69 | this.#context = context; 70 | 71 | this.#context?.removeEventListener( 72 | ApiPreviewContentChangedEvent.TYPE, 73 | this.#onContentChanged as EventListener 74 | ); 75 | 76 | this.#context?.addEventListener( 77 | ApiPreviewContentChangedEvent.TYPE, 78 | this.#onContentChanged as EventListener 79 | ); 80 | }); 81 | 82 | this.consumeContext(UMB_THEME_CONTEXT, (instance) => { 83 | this.observe( 84 | instance.theme, 85 | (themeAlias) => { 86 | this._theme = this.#translateTheme(themeAlias); 87 | }, 88 | "_observeTheme" 89 | ); 90 | }); 91 | } 92 | 93 | #onContentChanged = () => { 94 | this._dataTask.run(); 95 | }; 96 | 97 | #translateTheme(theme: string) { 98 | switch (theme) { 99 | case "umb-dark-theme": 100 | return "dark"; 101 | case "umb-light-theme": 102 | case "umb-high-contrast-theme": 103 | default: 104 | return "light"; 105 | } 106 | } 107 | 108 | render() { 109 | const renderLoader = (minHeight?: number) => html` 110 |
111 | 112 |
113 | `; 114 | 115 | const toggleExpand = () => { 116 | this._expand = !this._expand; 117 | }; 118 | 119 | const content = this._dataTask.render({ 120 | initial: () => renderLoader(), 121 | pending: () => renderLoader(this.jsonPreviewElement?.offsetHeight), 122 | complete: (data) => html` 123 | 124 | `, 125 | error: (error) => html` 126 |
127 | ❌ Error: 129 | ${error && typeof error === "object" && "message" in error 130 | ? error.message 131 | : error}! 133 |
134 | `, 135 | }); 136 | 137 | return html` 138 | 139 | ${this.headline} 140 | 147 | ${cache(content)} 148 | 149 | `; 150 | } 151 | } 152 | 153 | declare global { 154 | interface HTMLElementTagNameMap { 155 | "bc-api-preview-section": ApiPreviewElementSection; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions.UI/src/elements/api-preview-workspace-view.element.ts: -------------------------------------------------------------------------------- 1 | import {UmbElementMixin} from '@umbraco-cms/backoffice/element-api'; 2 | import { html, LitElement } from 'lit'; 3 | import {customElement} from 'lit/decorators.js'; 4 | 5 | import {KebabCaseAttributesMixin} from '../mixins/kebab-case-attributes.mixin'; 6 | /** 7 | * The Delivery Api Extensions Preview Workspace View element. 8 | */ 9 | @customElement('bc-api-preview-workspace-view') 10 | export default class ApiPreviewWorkspaceView extends UmbElementMixin(KebabCaseAttributesMixin(LitElement)) { 11 | 12 | 13 | render() { 14 | return html` 15 | 16 | 17 | 18 | `; 19 | } 20 | } 21 | 22 | declare global { 23 | interface HTMLElementTagNameMap { 24 | 'bc-api-preview-workspace-view': ApiPreviewWorkspaceView; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions.UI/src/elements/api-preview.element.ts: -------------------------------------------------------------------------------- 1 | import {UmbElementMixin} from '@umbraco-cms/backoffice/element-api'; 2 | import { 3 | css, html, LitElement, nothing, 4 | } from 'lit'; 5 | import {customElement, state} from 'lit/decorators.js'; 6 | 7 | import {KebabCaseAttributesMixin} from '../mixins/kebab-case-attributes.mixin'; 8 | import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/document'; 9 | import { UMB_MEDIA_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/media'; 10 | import { DocumentVariantStateModel } from '@umbraco-cms/backoffice/external/backend-api'; 11 | import { API_PREVIEW_CONTEXT, ApiPreviewContentType, ApiPreviewContext } from '../contexts/api-preview.context'; 12 | import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property'; 13 | import { UmbRequestReloadChildrenOfEntityEvent, UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/entity-action'; 14 | import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; 15 | import { ApiPreviewContentChangedEvent } from '../events/api-preview-content-changed'; 16 | 17 | /** 18 | * The Delivery Api Extensions Preview element. 19 | */ 20 | @customElement('bc-api-preview') 21 | export default class ApiPreviewElement extends UmbElementMixin(KebabCaseAttributesMixin(LitElement)) { 22 | static styles = css` 23 | :host { 24 | display: flex; 25 | flex-direction: column; 26 | gap: 1rem; 27 | } 28 | 29 | @media (min-width: 1024px) { 30 | :host { 31 | flex-direction: row; 32 | } 33 | 34 | :host > * { 35 | flex: 1; 36 | } 37 | } 38 | `; 39 | 40 | #apiPreviewContext = new ApiPreviewContext(this); 41 | 42 | @state() 43 | private _hasPreview = false; 44 | 45 | @state() 46 | private _isPublished = false; 47 | 48 | constructor(){ 49 | super(); 50 | this.provideContext(API_PREVIEW_CONTEXT, this.#apiPreviewContext); 51 | 52 | this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, (instance) => { 53 | const currentCulture = instance.getVariantId().culture ?? undefined; 54 | this.#apiPreviewContext?.setCulture(currentCulture); 55 | }); 56 | 57 | this.consumeContext(UMB_DOCUMENT_WORKSPACE_CONTEXT, (context) => { 58 | if(!context) return; 59 | this._hasPreview = true; 60 | this.#apiPreviewContext?.setType(ApiPreviewContentType.Document); 61 | 62 | this.observe( 63 | context.unique, 64 | (unique) => { 65 | this.#apiPreviewContext?.setUniqueId(unique!); 66 | } 67 | ); 68 | 69 | this.observe(context.variants, (options) => { 70 | const currentVariant = options.find((option) => option.culture === (this.#apiPreviewContext.getCulture() ?? null)); 71 | const state = currentVariant?.state; 72 | 73 | this._hasPreview = state && state !== DocumentVariantStateModel.NOT_CREATED ? true : false; 74 | this._isPublished = state === DocumentVariantStateModel.PUBLISHED || state === DocumentVariantStateModel.PUBLISHED_PENDING_CHANGES; 75 | }); 76 | }); 77 | 78 | // Media context 79 | this.consumeContext(UMB_MEDIA_WORKSPACE_CONTEXT, (context) => { 80 | if(!context) return; 81 | this.#apiPreviewContext?.setType(ApiPreviewContentType.Media); 82 | this._hasPreview = false; 83 | 84 | this.observe( 85 | context.unique, 86 | (unique) => { 87 | if(!unique) return; 88 | this.#apiPreviewContext?.setUniqueId(unique); 89 | } 90 | ); 91 | 92 | this.observe(context.isNew, (isNew) => { 93 | this._isPublished = !isNew; 94 | }); 95 | }); 96 | 97 | // Listen to Umbraco events triggered on save 98 | this.consumeContext(UMB_ACTION_EVENT_CONTEXT, (instance) => { 99 | instance?.removeEventListener( 100 | UmbRequestReloadChildrenOfEntityEvent.TYPE, 101 | this.#onContentChanged as EventListener, 102 | ); 103 | 104 | instance?.removeEventListener( 105 | UmbRequestReloadStructureForEntityEvent.TYPE, 106 | this.#onContentChanged as EventListener, 107 | ); 108 | 109 | instance.addEventListener( 110 | UmbRequestReloadChildrenOfEntityEvent.TYPE, 111 | this.#onContentChanged as EventListener, 112 | ); 113 | 114 | instance.addEventListener( 115 | UmbRequestReloadStructureForEntityEvent.TYPE, 116 | this.#onContentChanged as EventListener, 117 | ); 118 | }); 119 | } 120 | 121 | #onContentChanged = () => { 122 | this.#apiPreviewContext.dispatchEvent(new ApiPreviewContentChangedEvent()); 123 | }; 124 | 125 | render() { 126 | return html` 127 | ${this._hasPreview ? html` 128 | 129 | ` : nothing} 130 | ${this._isPublished ? html` 131 | 132 | ` : nothing} 133 | `; 134 | } 135 | } 136 | 137 | declare global { 138 | interface HTMLElementTagNameMap { 139 | 'bc-api-preview': ApiPreviewElement; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions.UI/src/elements/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api-preview.element'; 2 | export * from './api-preview-section.element'; 3 | export * from './api-preview-workspace-view.element'; 4 | export * from './json-preview.element'; 5 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions.UI/src/elements/json-preview.element.tsx: -------------------------------------------------------------------------------- 1 | import JsonView from '@uiw/react-json-view'; 2 | import {type JsonViewProps} from '@uiw/react-json-view'; 3 | import {type UUIIconElement} from '@umbraco-ui/uui'; 4 | 5 | import { lightTheme } from '@uiw/react-json-view/light'; 6 | import { vscodeTheme } from '@uiw/react-json-view/vscode'; 7 | 8 | import defineReactElement from '../helpers/define-react-element'; 9 | 10 | export interface JsonPreviewProps extends JsonViewProps { 11 | theme?: 'dark' | 'light'; 12 | } 13 | 14 | const WebReactJsonComponent = (props: JsonPreviewProps>) => 15 | 16 | type === 'value' ? null : }/> 17 | Array.isArray(value) ? undefined : }/> 18 | Object.keys(value ?? {}).length === 0 ?   : undefined }/> 19 | { 20 | const copied = 'data-copied' in props && Boolean(props['data-copied']); 21 | return }>; 22 | }}/> 23 | ; 24 | 25 | defineReactElement('bc-json-preview', WebReactJsonComponent, {props: {value: undefined, theme: 'string' }}); 26 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions.UI/src/events/api-preview-content-changed.ts: -------------------------------------------------------------------------------- 1 | export class ApiPreviewContentChangedEvent extends Event { 2 | static readonly TYPE = 'api-preview-content-changed'; 3 | 4 | constructor() { 5 | super(ApiPreviewContentChangedEvent.TYPE); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions.UI/src/helpers/define-react-element.ts: -------------------------------------------------------------------------------- 1 | import type {R2WCOptions} from '@r2wc/core'; 2 | import r2wc from '@r2wc/react-to-web-component'; 3 | import { customElement } from 'lit/decorators.js'; 4 | 5 | declare global { 6 | namespace JSX { 7 | // IntrinsicElementMap grabs all the standard HTML tags in the TS DOM lib. 8 | interface IntrinsicElements extends IntrinsicElementMap { } 9 | 10 | // The following are custom types, not part of TS's known JSX namespace: 11 | type IntrinsicElementMap = { 12 | [K in keyof HTMLElementTagNameMap]: HTMLElementTagNameMap[K] | React.DetailedHTMLProps, HTMLElementTagNameMap[K]> 13 | }; 14 | } 15 | } 16 | 17 | export default function defineReactElement>(name: string, ReactComponent: React.ComponentType, options?: R2WCOptions): void { 18 | options ??= {}; 19 | options.shadow ??= 'open'; 20 | 21 | const ReactWebComponentElement = r2wc(ReactComponent, options); 22 | customElement(name)(ReactWebComponentElement); 23 | } 24 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions.UI/src/index.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | line-height: 1.5; 3 | font-weight: 400; 4 | margin: 0; 5 | padding: 0; 6 | } 7 | 8 | #app { 9 | display: flex; 10 | flex-direction: column; 11 | height: 100vh; 12 | width: 100vw; 13 | color: var(--uui-color-text); 14 | font-size: 14px; 15 | box-sizing: border-box; 16 | } 17 | 18 | #nav-top-bar { 19 | display: flex; 20 | color: var(--uui-color-header-contrast); 21 | gap: 24px; 22 | padding: 0 var(--uui-size-4); 23 | align-items: center; 24 | background-color: var(--uui-color-header-surface); 25 | height: 48px; 26 | width: 100%; 27 | font-size: 1rem; 28 | box-sizing: border-box; 29 | --uui-tab-text: white; 30 | --uui-tab-text-active: var(--uui-color-current); 31 | --uui-tab-text-hover: var(--uui-color-current-emphasis); 32 | } 33 | 34 | #main { 35 | width: 100%; 36 | height: calc(100% - 48px); 37 | display: flex; 38 | box-sizing: border-box; 39 | } 40 | 41 | #nav-side-bar { 42 | width: 400px; 43 | background-color: var(--uui-color-surface); 44 | height: 100%; 45 | border-right: 1px solid var(--uui-color-border); 46 | font-weight: 500; 47 | display: flex; 48 | flex-direction: column; 49 | } 50 | 51 | #nav-side-bar b { 52 | padding: var(--uui-size-6) var(--uui-size-8); 53 | } 54 | 55 | #editor { 56 | background-color: var(--uui-color-background); 57 | width: 100%; 58 | height: 100%; 59 | display: flex; 60 | flex-direction: column; 61 | } 62 | 63 | #editor-top { 64 | background-color: var(--uui-color-surface); 65 | width: 100%; 66 | display: flex; 67 | gap: 16px; 68 | align-items: center; 69 | border-bottom: 1px solid var(--uui-color-border); 70 | } 71 | 72 | #editor-top uui-input { 73 | width: 100%; 74 | margin-left: 16px; 75 | } 76 | 77 | #editor-top uui-tab-group { 78 | --uui-tab-divider: var(--uui-color-border); 79 | border-left: 1px solid var(--uui-color-border); 80 | flex-wrap: nowrap; 81 | height: 60px; 82 | } 83 | 84 | #editor-content { 85 | padding: var(--uui-size-6); 86 | height: 100%; 87 | display: flex; 88 | flex-direction: column; 89 | gap: 16px; 90 | } 91 | 92 | .editor-property { 93 | display: grid; 94 | grid-template-columns: 200px 600px; 95 | gap: 32px; 96 | } 97 | 98 | .editor-property > .label > p { 99 | color: var(--uui-color-text-alt); 100 | } 101 | 102 | .editor-property uui-input, 103 | .editor-property uui-textarea { 104 | width: 100%; 105 | } 106 | 107 | uui-box hr { 108 | margin-bottom: var(--uui-size-6); 109 | } 110 | 111 | hr { 112 | border: 0; 113 | border-top: 1px solid var(--uui-color-border-alt); 114 | } 115 | 116 | uui-tab { 117 | font-size: 0.8rem; 118 | } 119 | 120 | #editor-bottom { 121 | display: flex; 122 | justify-content: end; 123 | align-items: center; 124 | height: 70px; 125 | width: 100%; 126 | gap: 16px; 127 | padding-right: 24px; 128 | border-top: 1px solid var(--uui-color-border); 129 | background-color: var(--uui-color-surface); 130 | box-sizing: border-box; 131 | } 132 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions.UI/src/main.dev.js: -------------------------------------------------------------------------------- 1 | import '@umbraco-ui/uui'; 2 | import './elements'; 3 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions.UI/src/main.ts: -------------------------------------------------------------------------------- 1 | import {type UmbEntryPointOnInit} from '@umbraco-cms/backoffice/extension-api'; 2 | 3 | import {manifest as apiPreviewViewCondition} from './conditions/api-preview.view.condition'; 4 | import {ApiPreviewRepository} from './contexts/api-preview.repository'; 5 | 6 | export const onInit: UmbEntryPointOnInit = (_host, extensionRegistry) => { 7 | const workspaceAlias = 'deliveryApiPreview'; 8 | const apiPreviewRepository = new ApiPreviewRepository(_host); 9 | 10 | apiPreviewRepository 11 | .fetchConfig() 12 | .then(config => { 13 | if (config?.enabled !== true) { 14 | return; 15 | } 16 | 17 | extensionRegistry.register(apiPreviewViewCondition); 18 | const enabledWorkspaces = ['Umb.Workspace.Document']; 19 | if (config.media.enabled) { 20 | enabledWorkspaces.push('Umb.Workspace.Media'); 21 | } 22 | 23 | const apiPreviewManifest: UmbExtensionManifest = { 24 | type: 'workspaceView', 25 | alias: workspaceAlias, 26 | name: 'Delivery API Preview', 27 | meta: { 28 | icon: 'icon-code', 29 | label: 'API', 30 | pathname: 'preview', 31 | }, 32 | element: async () => (import('./workspace-views/api-preview')), 33 | weight: 110, 34 | conditions: [ 35 | { 36 | alias: 'Umb.Condition.WorkspaceAlias', 37 | oneOf: enabledWorkspaces, 38 | }, 39 | { 40 | alias: 'DeliveryApiExtensions.ApiPreview.View', 41 | }, 42 | ], 43 | }; 44 | 45 | extensionRegistry.register(apiPreviewManifest); 46 | }); 47 | }; 48 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions.UI/src/mixins/kebab-case-attributes.mixin.ts: -------------------------------------------------------------------------------- 1 | import {type LitElement} from 'lit'; 2 | import type {PropertyDeclaration} from 'lit-element'; 3 | 4 | const camelCaseToKebabCase = (str: string) => str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase(); 5 | 6 | export const KebabCaseAttributesMixin = (superClass: T) => { 7 | class KebabCaseAttributesMixinClass extends (superClass as typeof LitElement) { 8 | static createProperty(name: PropertyKey, options?: PropertyDeclaration) { 9 | let customOptions = options; 10 | 11 | // Derive the attribute name if not already defined or disabled 12 | if (typeof options?.attribute === 'undefined' || options?.attribute === true) { 13 | customOptions = {...options, attribute: camelCaseToKebabCase(name.toString())}; 14 | } 15 | 16 | super.createProperty(name, customOptions); 17 | } 18 | } 19 | 20 | return KebabCaseAttributesMixinClass as T; 21 | }; 22 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions.UI/src/workspace-views/api-preview.ts: -------------------------------------------------------------------------------- 1 | import '../elements/api-preview-section.element'; 2 | import '../elements/api-preview.element'; 3 | import '../elements/json-preview.element'; 4 | 5 | import ApiPreviewWorkspaceView from '../elements/api-preview-workspace-view.element'; 6 | 7 | export default ApiPreviewWorkspaceView; 8 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions.UI/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "experimentalDecorators": true, 5 | "useDefineForClassFields": false, 6 | "module": "ESNext", 7 | "lib": [ "ES2020", "DOM", "DOM.Iterable" ], 8 | "skipLibCheck": true, 9 | 10 | /* JSX */ 11 | "jsx": "preserve", 12 | 13 | /* Bundler mode */ 14 | "moduleResolution": "bundler", 15 | "allowImportingTsExtensions": true, 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "noEmit": true, 19 | 20 | /* Linting */ 21 | "strict": true, 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true, 24 | "noUncheckedIndexedAccess": true, 25 | "noFallthroughCasesInSwitch": true 26 | }, 27 | "include": ["src", "vite.config.ts"] 28 | } 29 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions.UI/vite.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'vite'; 2 | 3 | export default defineConfig(({mode}) => ({ 4 | base: '/App_Plugins/DeliveryApiExtensions', 5 | build: { 6 | lib: { 7 | entry: 'src/main.ts', 8 | name: 'DeliveryApiExtensions', 9 | formats: ['es'], 10 | }, 11 | outDir: 'dist/DeliveryApiExtensions', 12 | emptyOutDir: true, 13 | sourcemap: true, 14 | rollupOptions: { 15 | external: [/^@umbraco/], 16 | }, 17 | }, 18 | define: { 19 | 'process.env.NODE_ENV': JSON.stringify(mode), 20 | }, 21 | esbuild: { 22 | jsxFactory: 'h', 23 | jsxFragment: 'Fragment', 24 | jsxInject: 'import { h, Fragment } from \'preact\'', 25 | legalComments: 'none', 26 | }, 27 | resolve: { 28 | alias: { 29 | 'react-dom/test-utils': 'preact/test-utils', 30 | 'react-dom': 'preact/compat', 31 | react: 'preact/compat', 32 | }, 33 | }, 34 | server: { 35 | port: 5173, 36 | strictPort: true, 37 | proxy: { 38 | // Add support for query param replacement in data files 39 | // Syntax: {queryParamName|fallbackValue} 40 | '^/src/data/.*%7B.+%7D': { 41 | target: 'http://localhost:5173', 42 | rewrite(path) { 43 | const url = new URL(path, 'http://localhost'); 44 | return path.replace(/%7B(.+?)(?:%7C(.+?))?%7D/gi, (_, paramName: string, defaultValue: string) => url.searchParams.get(paramName) ?? defaultValue ?? ''); 45 | }, 46 | }, 47 | }, 48 | }, 49 | })); 50 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions/.gitignore: -------------------------------------------------------------------------------- 1 | wwwroot/ 2 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions/Composers/DeliveryApiExtensionsComposer.cs: -------------------------------------------------------------------------------- 1 | using Umbraco.Cms.Core.Composing; 2 | using Umbraco.Cms.Core.DependencyInjection; 3 | using Umbraco.Community.DeliveryApiExtensions.Configuration; 4 | 5 | namespace Umbraco.Community.DeliveryApiExtensions.Composers; 6 | 7 | /// 8 | /// Default for configuring Delivery API Extensions. 9 | /// 10 | public sealed class DeliveryApiExtensionsComposer : IComposer 11 | { 12 | /// 13 | public void Compose(IUmbracoBuilder builder) 14 | { 15 | builder.AddDeliveryApiExtensions(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions/Configuration/Options/DeliveryApiExtensionsOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Umbraco.Community.DeliveryApiExtensions.Configuration.Options; 2 | 3 | /// 4 | /// Delivery API Extensions options. 5 | /// 6 | public class DeliveryApiExtensionsOptions 7 | { 8 | /// 9 | /// Preview options. 10 | /// 11 | public PreviewOptions Preview { get; set; } = new(); 12 | 13 | /// 14 | /// Typed swagger options. 15 | /// 16 | public TypedSwaggerOptions TypedSwagger { get; set; } = new(); 17 | } 18 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions/Configuration/Options/MediaOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Umbraco.Community.DeliveryApiExtensions.Configuration.Options; 2 | 3 | /// 4 | /// Media preview options. 5 | /// 6 | public class MediaOptions 7 | { 8 | /// 9 | /// Whether the preview content app is enabled for media. 10 | /// 11 | public bool Enabled { get; set; } = true; 12 | } 13 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions/Configuration/Options/PreviewOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Umbraco.Community.DeliveryApiExtensions.Configuration.Options; 2 | 3 | /// 4 | /// Preview options. 5 | /// 6 | public class PreviewOptions 7 | { 8 | /// 9 | /// Whether the preview content app is enabled. 10 | /// 11 | public bool Enabled { get; set; } = true; 12 | 13 | /// 14 | /// Preview options for media. 15 | /// 16 | public MediaOptions Media { get; set; } = new(); 17 | 18 | /// 19 | /// The aliases of the allowed user groups. 20 | /// Defaults to empty, which allows all user groups. 21 | /// 22 | public List AllowedUserGroupAliases { get; set; } = []; 23 | } 24 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions/Configuration/Options/SwaggerGenerationMode.cs: -------------------------------------------------------------------------------- 1 | namespace Umbraco.Community.DeliveryApiExtensions.Configuration.Options; 2 | 3 | /// 4 | /// The swagger generation mode to use. 5 | /// 6 | public enum SwaggerGenerationMode 7 | { 8 | /// 9 | /// Swagger will be generated with the 'UseOneOfForPolymorphism' and 'UseAllOfForInheritance' options. 10 | /// Suitable for most generators like openapi-typescript and orval. 11 | /// 12 | Auto, 13 | 14 | /// 15 | /// Swagger will be generated with only the 'UseAllOfForInheritance' option enabled. 16 | /// Suitable for generators that don't support polymorphism using OneOf like NSwag. 17 | /// 18 | Compatibility, 19 | 20 | /// 21 | /// Swagger options will not be configured, allowing full customization. 22 | /// It can be configured from your codebase using: 23 | /// 24 | /// services.Configure<SwaggerGenOptions>(options => ...) 25 | /// services.Configure<TypedSwaggerOptions>(options => ...) 26 | /// 27 | /// 28 | Manual, 29 | } 30 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions/Configuration/Options/TypedSwaggerOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using Umbraco.Community.DeliveryApiExtensions.Swagger; 3 | 4 | namespace Umbraco.Community.DeliveryApiExtensions.Configuration.Options; 5 | 6 | /// 7 | /// Typed swagger options 8 | /// 9 | public class TypedSwaggerOptions 10 | { 11 | /// 12 | /// Default implementation based on the configured . 13 | /// 14 | [JsonIgnore] 15 | public static readonly Func DefaultSettingsFactory = mode => new SwaggerGenerationSettings 16 | { 17 | UseOneOf = mode == SwaggerGenerationMode.Auto, 18 | UseAllOf = mode is SwaggerGenerationMode.Auto or SwaggerGenerationMode.Compatibility, 19 | }; 20 | 21 | /// 22 | /// Initializes a new instance of the class. 23 | /// 24 | public TypedSwaggerOptions() 25 | { 26 | SettingsFactory = () => DefaultSettingsFactory(Mode); 27 | } 28 | 29 | /// 30 | /// Whether the typed swagger feature is enabled 31 | /// 32 | public bool Enabled { get; set; } = true; 33 | 34 | /// 35 | /// The swagger generation mode to use. 36 | /// Defaults to 'Auto'. 37 | /// 38 | public SwaggerGenerationMode Mode { get; set; } = SwaggerGenerationMode.Auto; 39 | 40 | /// 41 | /// The factory for the settings to be used for swagger generation. 42 | /// Defaults to . 43 | /// 44 | [JsonIgnore] 45 | public Func SettingsFactory { get; set; } 46 | } 47 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions/Configuration/OptionsExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Options; 4 | using Umbraco.Extensions; 5 | 6 | namespace Umbraco.Community.DeliveryApiExtensions.Configuration; 7 | 8 | internal static class OptionsExtensions 9 | { 10 | /// 11 | /// Adds and binds the provided class to the provided section. 12 | /// 13 | public static OptionsBuilder AddOptions(this IServiceCollection services, IConfigurationSection configurationSection) 14 | where TOptions : class 15 | { 16 | return services.AddOptions().Bind(configurationSection).ValidateDataAnnotations(); 17 | } 18 | 19 | /// 20 | /// Gets the default configuration section for the provided . 21 | /// 22 | public static IConfigurationSection GetSection(this IConfiguration configuration) 23 | { 24 | return configuration.GetSection(typeof(TOptions).Name.TrimEnd("Options")); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions/Configuration/UmbracoBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Swashbuckle.AspNetCore.SwaggerGen; 4 | using Umbraco.Cms.Core.DependencyInjection; 5 | using Umbraco.Cms.Core.Models.DeliveryApi; 6 | using Umbraco.Community.DeliveryApiExtensions.Configuration.Options; 7 | using Umbraco.Community.DeliveryApiExtensions.Services; 8 | using Umbraco.Community.DeliveryApiExtensions.Swagger; 9 | using Umbraco.Extensions; 10 | 11 | namespace Umbraco.Community.DeliveryApiExtensions.Configuration; 12 | 13 | /// 14 | /// Extensions for to add DeliveryApiExtensions. 15 | /// 16 | public static class UmbracoBuilderExtensions 17 | { 18 | /// 19 | /// Registers the necessary services and configuration for DeliveryApiExtensions. 20 | /// 21 | public static void AddDeliveryApiExtensions(this IUmbracoBuilder builder) 22 | { 23 | IConfigurationSection configSection = builder.Config.GetSection(); 24 | _ = builder.Services.AddOptions(configSection); 25 | 26 | // Preview 27 | builder.AddPreview(configSection); 28 | 29 | // TypedSwagger 30 | builder.AddTypedSwagger(configSection); 31 | } 32 | 33 | internal static void AddPreview(this IUmbracoBuilder builder, IConfigurationSection configSection) 34 | { 35 | IConfigurationSection previewConfigSection = configSection.GetSection(); 36 | _ = builder.Services.AddOptions(previewConfigSection); 37 | 38 | IConfigurationSection mediaConfigSection = previewConfigSection.GetSection(); 39 | _ = builder.Services.AddOptions(mediaConfigSection); 40 | } 41 | 42 | internal static void AddTypedSwagger(this IUmbracoBuilder builder, IConfigurationSection configSection) 43 | { 44 | IConfigurationSection typedSwaggerConfigSection = configSection.GetSection(); 45 | TypedSwaggerOptions? typedSwaggerOptions = typedSwaggerConfigSection.Get(); 46 | if (typedSwaggerOptions?.Enabled == false) 47 | { 48 | return; 49 | } 50 | 51 | _ = builder.Services.AddSingleton(); 52 | _ = builder.Services.AddOptions(typedSwaggerConfigSection); 53 | 54 | _ = builder.Services.PostConfigure(options => 55 | { 56 | switch (typedSwaggerOptions?.Mode ?? SwaggerGenerationMode.Auto) 57 | { 58 | case SwaggerGenerationMode.Auto: 59 | options.UseOneOfForPolymorphism(); 60 | options.UseAllOfForInheritance(); 61 | break; 62 | 63 | case SwaggerGenerationMode.Compatibility: 64 | options.SchemaGeneratorOptions.UseOneOfForPolymorphism = false; 65 | options.UseAllOfForInheritance(); 66 | break; 67 | case SwaggerGenerationMode.Manual: 68 | default: 69 | break; 70 | } 71 | 72 | options.SupportNonNullableReferenceTypes(); 73 | 74 | options.SchemaFilterDescriptors.Insert(0, new FilterDescriptor 75 | { 76 | Type = typeof(FixPropertyNullabilityFilter), 77 | Arguments = [], 78 | }); 79 | 80 | options.SchemaFilter(); 81 | options.DocumentFilter(); 82 | 83 | Func> currentSubTypesSelector = options.SchemaGeneratorOptions.SubTypesSelector; 84 | options.SelectSubTypesUsing(baseType => 85 | { 86 | List handledTypes = [ 87 | typeof(IApiElement), 88 | typeof(IApiContent), 89 | typeof(IApiMediaWithCrops), 90 | typeof(IApiContentResponse), 91 | typeof(IApiMediaWithCropsResponse), 92 | ]; 93 | 94 | if (handledTypes.Contains(baseType)) 95 | { 96 | return []; 97 | } 98 | 99 | List result = currentSubTypesSelector(baseType).ToList(); 100 | 101 | if (result.Count == 1 && result[0] == baseType) 102 | { 103 | return baseType.Assembly.GetTypes().Where(type => type.IsSubclassOf(baseType)); 104 | } 105 | 106 | return result; 107 | }); 108 | }); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace Umbraco.Community.DeliveryApiExtensions; 2 | 3 | internal static class Constants 4 | { 5 | public static class Api 6 | { 7 | public const string ApiName = "DeliveryApiExtensions"; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions/Controllers/BaseController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Umbraco.Cms.Api.Common.Attributes; 4 | using Umbraco.Cms.Api.Common.Filters; 5 | using Umbraco.Cms.Web.Common.Authorization; 6 | using Umbraco.Cms.Web.Common.Routing; 7 | 8 | namespace Umbraco.Community.DeliveryApiExtensions.Controllers; 9 | 10 | /// 11 | /// Base Delivery API Extensions controller class. 12 | /// 13 | [ApiController] 14 | [MapToApi(Constants.Api.ApiName)] 15 | [Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)] 16 | [JsonOptionsName(Cms.Core.Constants.JsonOptionsNames.BackOffice)] 17 | [BackOfficeRoute("delivery-api-extensions/[controller]")] 18 | public abstract class BaseController : ControllerBase 19 | { 20 | } 21 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions/Controllers/Models/PreviewConfig.cs: -------------------------------------------------------------------------------- 1 | namespace Umbraco.Community.DeliveryApiExtensions.Controllers.Models; 2 | 3 | /// 4 | /// API Preview config. 5 | /// 6 | public class PreviewConfig 7 | { 8 | /// 9 | /// Whether the preview content app is enabled. 10 | /// 11 | public required bool Enabled { get; set; } 12 | 13 | /// 14 | /// Preview options for media. 15 | /// 16 | public PreviewMediaConfig? Media { get; set; } 17 | } 18 | 19 | /// 20 | /// API Preview Media config. 21 | /// 22 | public class PreviewMediaConfig 23 | { 24 | /// 25 | /// Whether the preview content app is enabled for media. 26 | /// 27 | public required bool Enabled { get; set; } 28 | } 29 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions/Models/ContentTypeInfo.cs: -------------------------------------------------------------------------------- 1 | namespace Umbraco.Community.DeliveryApiExtensions.Models; 2 | 3 | /// 4 | /// Represents the subset of content type information that is needed for typed swagger schema generation. 5 | /// 6 | public class ContentTypeInfo 7 | { 8 | /// 9 | /// Content type alias. 10 | /// 11 | public required string Alias { get; set; } 12 | 13 | /// 14 | /// Content type schema id. 15 | /// 16 | public required string SchemaId { get; set; } 17 | 18 | /// 19 | /// List of schema ids of the content type's compositions. 20 | /// 21 | public required List CompositionSchemaIds { get; set; } 22 | 23 | /// 24 | /// List of content type's properties. 25 | /// 26 | public required List Properties { get; set; } 27 | 28 | /// 29 | /// Whether the content type is an element type. 30 | /// 31 | public bool IsElement { get; set; } 32 | 33 | /// 34 | /// Whether the content type is used as a composition. 35 | /// 36 | public bool IsComposition { get; set; } 37 | } 38 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions/Models/ContentTypePropertyInfo.cs: -------------------------------------------------------------------------------- 1 | namespace Umbraco.Community.DeliveryApiExtensions.Models; 2 | 3 | /// 4 | /// Represents the subset of content type property information that is needed for typed swagger schema generation. 5 | /// 6 | public class ContentTypePropertyInfo 7 | { 8 | /// 9 | /// Property alias. 10 | /// 11 | public required string Alias { get; set; } 12 | 13 | /// 14 | /// Property Editor alias. 15 | /// 16 | public required string EditorAlias { get; set; } 17 | 18 | /// 19 | /// Property delivery api type. 20 | /// 21 | public required Type Type { get; set; } 22 | 23 | /// 24 | /// Whether this property is inherited from a composition. 25 | /// 26 | public bool Inherited { get; set; } 27 | } 28 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions/Services/ContentTypeInfoService.cs: -------------------------------------------------------------------------------- 1 | using Umbraco.Cms.Core.Models; 2 | using Umbraco.Cms.Core.Models.PublishedContent; 3 | using Umbraco.Cms.Core.Services; 4 | using Umbraco.Cms.Core.Strings; 5 | using Umbraco.Community.DeliveryApiExtensions.Models; 6 | using Umbraco.Extensions; 7 | 8 | namespace Umbraco.Community.DeliveryApiExtensions.Services; 9 | 10 | /// 11 | /// Service responsible for providing content type information. 12 | /// 13 | /// 14 | /// Used for typed swagger schema generation. 15 | /// 16 | public interface IContentTypeInfoService 17 | { 18 | /// 19 | /// Gets all the available content types. 20 | /// 21 | ICollection GetContentTypes(); 22 | } 23 | 24 | internal sealed class ContentTypeInfoService : IContentTypeInfoService 25 | { 26 | private readonly IContentTypeService _contentTypeService; 27 | private readonly IPublishedContentTypeFactory _publishedContentTypeFactory; 28 | private readonly IShortStringHelper _shortStringHelper; 29 | 30 | public ContentTypeInfoService(IContentTypeService contentTypeService, IPublishedContentTypeFactory publishedContentTypeFactory, IShortStringHelper shortStringHelper) 31 | { 32 | _contentTypeService = contentTypeService; 33 | _publishedContentTypeFactory = publishedContentTypeFactory; 34 | _shortStringHelper = shortStringHelper; 35 | } 36 | 37 | public ICollection GetContentTypes() 38 | { 39 | List result = []; 40 | HashSet compositionAliases = []; 41 | 42 | foreach (IContentType contentType in _contentTypeService.GetAll()) 43 | { 44 | HashSet ownPropertyAliases = contentType.PropertyTypes.Select(p => p.Alias).ToHashSet(); 45 | IPublishedContentType publishedContentType = _publishedContentTypeFactory.CreateContentType(contentType); 46 | 47 | result.Add(new ContentTypeInfo 48 | { 49 | Alias = contentType.Alias, 50 | SchemaId = GetContentTypeSchemaId(contentType), 51 | CompositionSchemaIds = contentType.ContentTypeComposition.Select(GetContentTypeSchemaId).ToList(), 52 | Properties = publishedContentType.PropertyTypes.Select(p => new ContentTypePropertyInfo { Alias = p.Alias, EditorAlias = p.EditorAlias, Type = p.DeliveryApiModelClrType, Inherited = !ownPropertyAliases.Contains(p.Alias) }).ToList(), 53 | IsElement = contentType.IsElement, 54 | IsComposition = false, 55 | }); 56 | 57 | compositionAliases.UnionWith(contentType.CompositionAliases()); 58 | } 59 | 60 | result.ForEach(c => c.IsComposition = compositionAliases.Contains(c.Alias)); 61 | 62 | return result; 63 | } 64 | 65 | private string GetContentTypeSchemaId(IContentTypeBase contentType) 66 | { 67 | // This is what ModelsBuilder currently also uses 68 | return contentType.Alias.ToCleanString(_shortStringHelper, CleanStringType.ConvertCase | CleanStringType.PascalCase); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions/Swagger/FixPropertyNullabilityFilter.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Text.Json.Serialization; 3 | using Microsoft.OpenApi.Models; 4 | using Swashbuckle.AspNetCore.SwaggerGen; 5 | 6 | namespace Umbraco.Community.DeliveryApiExtensions.Swagger; 7 | 8 | internal sealed class FixPropertyNullabilityFilter : ISchemaFilter 9 | { 10 | public void Apply(OpenApiSchema schema, SchemaFilterContext context) 11 | { 12 | if (schema.Properties is not { Count: > 0 }) 13 | { 14 | return; 15 | } 16 | 17 | Dictionary typeMembers = context.Type 18 | .GetMembers(BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Instance) 19 | .Where(t => t is FieldInfo or PropertyInfo) 20 | .ToDictionary(GetPropertyName, t => t, StringComparer.OrdinalIgnoreCase); 21 | 22 | foreach (KeyValuePair property in schema.Properties) 23 | { 24 | if (property.Value.Reference == null || property.Value.Nullable || typeMembers.GetValueOrDefault(property.Key) is not { } memberInfo) 25 | { 26 | continue; 27 | } 28 | 29 | Type fieldType = memberInfo switch 30 | { 31 | FieldInfo fieldInfo => fieldInfo.FieldType, 32 | PropertyInfo propertyInfo => propertyInfo.PropertyType, 33 | _ => throw new NotSupportedException(), 34 | }; 35 | 36 | property.Value.Nullable = fieldType.IsValueType ? Nullable.GetUnderlyingType(fieldType) != null : !memberInfo.IsNonNullableReferenceType(); 37 | } 38 | } 39 | 40 | private static string GetPropertyName(MemberInfo memberInfo) 41 | { 42 | if (memberInfo.GetCustomAttribute() is { } nameAttribute) 43 | { 44 | return nameAttribute.Name; 45 | } 46 | 47 | return memberInfo.Name; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions/Swagger/SwaggerGenerationSettings.cs: -------------------------------------------------------------------------------- 1 | namespace Umbraco.Community.DeliveryApiExtensions.Swagger; 2 | 3 | /// 4 | /// Swagger generation settings to be used by . 5 | /// 6 | public class SwaggerGenerationSettings 7 | { 8 | /// 9 | /// Controls whether to use the OneOf extension for polymorphism. 10 | /// 11 | public bool UseOneOf { get; set; } 12 | 13 | /// 14 | /// Controls whether to use the AllOf extension for polymorphism and/or inheritance. 15 | /// 16 | public bool UseAllOf { get; set; } 17 | } 18 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions/UmbracoDeliveryApiExtensions.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | App_Plugins 4 | . 5 | Umbraco.Community.DeliveryApiExtensions 6 | Umbraco.Community.DeliveryApiExtensions 7 | DeliveryApiExtensions 8 | Extensions for the Delivery API, including typed swagger and backoffice preview of the API responses. 9 | umbraco;umbraco-marketplace;Swagger;OpenAPI 10 | Umbraco.Community.DeliveryApiExtensions 11 | true 12 | true 13 | true 14 | snupkg 15 | true 16 | 0.0.0-dev 17 | ByteCrumb 18 | $([System.DateTime]::UtcNow.ToString(`yyyy`)) © ByteCrumb 19 | https://github.com/ByteCrumb/Umbraco.Community.DeliveryApiExtensions 20 | true 21 | icon.png 22 | README.md 23 | git 24 | MPL-2.0 25 | 26 | true 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | False 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | <!--IF NUGET\s*([\s\S]*?)(\s*<!--ELSE-->\s*([\s\S]*?))?\s*<!--END--> 57 | $1 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions/appsettings-schema.DeliveryApiExtensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "title": "DeliveryApiExtensionsSchema", 4 | "type": "object", 5 | "properties": { 6 | "DeliveryApiExtensions": { 7 | "$ref": "#/definitions/DeliveryApiExtensionsOptions" 8 | } 9 | }, 10 | "definitions": { 11 | "DeliveryApiExtensionsOptions": { 12 | "title": "DeliveryApiExtensionsOptions", 13 | "type": "object", 14 | "description": "Delivery API Extensions options.", 15 | "additionalProperties": false, 16 | "properties": { 17 | "Preview": { 18 | "description": "Preview options.", 19 | "$ref": "#/definitions/DeliveryApiExtensionsOptions/definitions/DeliveryApiExtensionsPreviewOptions" 20 | }, 21 | "TypedSwagger": { 22 | "description": "Typed swagger options.", 23 | "$ref": "#/definitions/DeliveryApiExtensionsOptions/definitions/DeliveryApiExtensionsTypedSwaggerOptions" 24 | } 25 | }, 26 | "definitions": { 27 | "DeliveryApiExtensionsPreviewOptions": { 28 | "type": "object", 29 | "description": "Preview options.", 30 | "additionalProperties": false, 31 | "properties": { 32 | "Enabled": { 33 | "type": "boolean", 34 | "description": "Whether the preview content app is enabled." 35 | }, 36 | "Media": { 37 | "description": "Preview options for media.", 38 | "$ref": "#/definitions/DeliveryApiExtensionsOptions/definitions/DeliveryApiExtensionsMediaOptions" 39 | }, 40 | "AllowedUserGroupAliases": { 41 | "type": "array", 42 | "description": "The aliases of the allowed user groups.\nDefaults to empty, which allows all user groups.", 43 | "items": { 44 | "type": "string" 45 | } 46 | } 47 | } 48 | }, 49 | "DeliveryApiExtensionsMediaOptions": { 50 | "type": "object", 51 | "description": "Media preview options.", 52 | "additionalProperties": false, 53 | "properties": { 54 | "Enabled": { 55 | "type": "boolean", 56 | "description": "Whether the preview content app is enabled for media." 57 | } 58 | } 59 | }, 60 | "DeliveryApiExtensionsTypedSwaggerOptions": { 61 | "type": "object", 62 | "description": "Typed swagger options", 63 | "additionalProperties": false, 64 | "properties": { 65 | "Enabled": { 66 | "type": "boolean", 67 | "description": "Whether the typed swagger feature is enabled" 68 | }, 69 | "Mode": { 70 | "description": "The swagger generation mode to use.\nDefaults to 'Auto'.", 71 | "$ref": "#/definitions/DeliveryApiExtensionsOptions/definitions/DeliveryApiExtensionsSwaggerGenerationMode" 72 | } 73 | } 74 | }, 75 | "DeliveryApiExtensionsSwaggerGenerationMode": { 76 | "type": "string", 77 | "description": "The swagger generation mode to use.\n ", 78 | "x-enumNames": [ 79 | "Auto", 80 | "Compatibility", 81 | "Manual" 82 | ], 83 | "enum": [ 84 | "Auto", 85 | "Compatibility", 86 | "Manual" 87 | ] 88 | } 89 | } 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions/buildTransitive/DeliveryApiExtensions.AppSettingsSchema.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/UmbracoDeliveryApiExtensions/buildTransitive/Umbraco.Community.DeliveryApiExtensions.props: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.Playwright/.env: -------------------------------------------------------------------------------- 1 | URL=https://localhost:44363 2 | UMBRACO_USER_LOGIN=admin@umbraco 3 | UMBRACO_USER_PASSWORD='#Umbraco123!' 4 | STORAGE_STAGE_PATH=./playwright/.auth/user.json 5 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.Playwright/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": false, 4 | "node": true, 5 | "es2021": true 6 | }, 7 | "extends": "xo-space", 8 | "overrides": [ 9 | { 10 | "extends": [ 11 | "xo-typescript/space" 12 | ], 13 | "files": ["*.ts", "*.tsx"], 14 | "plugins": ["simple-import-sort"], 15 | "rules": { 16 | "@typescript-eslint/consistent-type-definitions": [ 17 | "warn", "interface" 18 | ], 19 | "@typescript-eslint/naming-convention": [ 20 | "error", 21 | { 22 | "selector": ["class", "interface", "typeAlias", "enum", "typeParameter"], 23 | "format": ["StrictPascalCase"] 24 | } 25 | ], 26 | "@typescript-eslint/no-unsafe-assignment": "off", 27 | "@typescript-eslint/no-unsafe-call": "off", 28 | "simple-import-sort/imports": "error", 29 | "simple-import-sort/exports": "error" 30 | } 31 | } 32 | ], 33 | "parserOptions": { 34 | "ecmaVersion": "latest", 35 | "sourceType": "module" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.Playwright/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /test-results/ 3 | /playwright-report/ 4 | /blob-report/ 5 | /playwright/.cache/ 6 | /playwright/.auth 7 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.Playwright/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "runtimeExecutable": "npm", 8 | "runtimeArgs": [ "run", "test" ], 9 | "cwd": "${workspaceFolder}", 10 | "console": "internalConsole", 11 | "name": "Test" 12 | }, 13 | { 14 | "type": "node", 15 | "request": "launch", 16 | "runtimeExecutable": "npm", 17 | "runtimeArgs": [ "run", "test:ui" ], 18 | "cwd": "${workspaceFolder}", 19 | "console": "internalConsole", 20 | "name": "Test - UI" 21 | }, 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.Playwright/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": "explicit" 4 | }, 5 | "typescript.tsdk": "node_modules\\typescript\\lib" 6 | } 7 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.Playwright/UmbracoDeliveryApiExtensions.Playwright.esproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | npx playwright test 4 | false 5 | 6 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.Playwright/fixtures/mediaLibrary/File.txt: -------------------------------------------------------------------------------- 1 | Test file! 2 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.Playwright/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "umbracodeliveryapiextensions.playwright", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "", 6 | "main": "index.js", 7 | "scripts": { 8 | "build": "tsc", 9 | "test": "playwright test", 10 | "test:headed": "playwright test --headed", 11 | "test:ui": "playwright test --ui", 12 | "postinstall": "playwright install chromium" 13 | }, 14 | "keywords": [], 15 | "author": "", 16 | "license": "ISC", 17 | "devDependencies": { 18 | "@playwright/test": "^1.48.2", 19 | "@types/node": "^20.17.6", 20 | "eslint": "^8.57.0", 21 | "eslint-config-xo-space": "^0.35.0", 22 | "eslint-config-xo-typescript": "^1.0.1", 23 | "eslint-plugin-n": "^16.6.2", 24 | "eslint-plugin-promise": "^6.6.0", 25 | "eslint-plugin-simple-import-sort": "^10.0.0", 26 | "tslib": "^2.8.1", 27 | "typescript": "^5.6.3" 28 | }, 29 | "volta": { 30 | "node": "20.9.0" 31 | }, 32 | "dependencies": { 33 | "@umbraco/json-models-builders": "^2.0.25", 34 | "@umbraco/playwright-testhelpers": "^15.0.0-beta.6", 35 | "dotenv": "^16.4.5" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.Playwright/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig, devices} from '@playwright/test'; 2 | import dotenv from 'dotenv'; 3 | 4 | dotenv.config(); 5 | 6 | export default defineConfig({ 7 | testDir: './tests/', 8 | timeout: 60000, 9 | fullyParallel: true, 10 | forbidOnly: Boolean(process.env.CI), 11 | retries: process.env.CI ? 2 : 0, 12 | workers: 1, 13 | reporter: 'html', 14 | use: { 15 | baseURL: process.env.URL, 16 | trace: 'on-first-retry', 17 | ignoreHTTPSErrors: true, 18 | }, 19 | 20 | /* Configure projects for major browsers */ 21 | projects: [ 22 | {name: 'setup', testMatch: /.*\.setup\.ts/}, 23 | { 24 | name: 'chromium', 25 | dependencies: ['setup'], 26 | use: { 27 | ...devices['Desktop Chrome'], 28 | storageState: 'playwright/.auth/user.json', 29 | }, 30 | }, 31 | ], 32 | 33 | /* Run your local dev server before starting the tests */ 34 | webServer: { 35 | command: 'dotnet run --project ..\\UmbracoDeliveryApiExtensions.TestSite', 36 | url: process.env.URL + '/umbraco', 37 | reuseExistingServer: true, 38 | ignoreHTTPSErrors: true, 39 | stdout: process.env.CI ? 'ignore' : 'pipe', 40 | cwd: '..\\UmbracoDeliveryApiExtensions.TestSite', 41 | }, 42 | }); 43 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.Playwright/tests/auth.setup.ts: -------------------------------------------------------------------------------- 1 | import {expect, test as setup} from '@playwright/test'; 2 | import {ConstantHelper, UiHelpers} from '@umbraco/playwright-testhelpers'; 3 | 4 | const authFile = 'playwright/.auth/user.json'; 5 | 6 | setup('authenticate', async ({page}) => { 7 | const umbracoUi = new UiHelpers(page); 8 | 9 | await umbracoUi.goToBackOffice(); 10 | await expect(page.locator('[name="username"]')).toBeVisible({timeout: 10000}); 11 | await umbracoUi.login.enterEmail(process.env.UMBRACO_USER_LOGIN ?? ''); 12 | await umbracoUi.login.enterPassword(process.env.UMBRACO_USER_PASSWORD ?? ''); 13 | await umbracoUi.login.clickLoginButton(); 14 | await expect(page.getByRole('tab', {name: ConstantHelper.sections.settings})).toBeVisible({timeout: 10000}); 15 | await umbracoUi.page.context().storageState({path: authFile}); 16 | }); 17 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.Playwright/tests/preview/preview-content.spec.ts: -------------------------------------------------------------------------------- 1 | import {expect} from '@playwright/test'; 2 | import {DocumentBuilder, DocumentTypeBuilder} from '@umbraco/json-models-builders'; 3 | import {type ApiHelpers, ConstantHelper, test} from '@umbraco/playwright-testhelpers'; 4 | 5 | test.describe('API preview - Content', () => { 6 | const docTypeName = 'PlaywrightTestDocType'; 7 | const nodeName = 'PlaywrightTestNode'; 8 | 9 | test.beforeEach(async ({umbracoApi}) => { 10 | await cleanTestContent(umbracoApi); 11 | await createTestContent(umbracoApi); 12 | }); 13 | 14 | test.afterEach(async ({umbracoApi}) => { 15 | await cleanTestContent(umbracoApi); 16 | }); 17 | 18 | test('Preview content app is visible in saved document', async ({page, umbracoUi}) => { 19 | await umbracoUi.goToBackOffice(); 20 | 21 | // Go to test node 22 | await page.getByRole('tab', {name: ConstantHelper.sections.content}).click(); 23 | await umbracoUi.content.goToContentWithName(nodeName); 24 | 25 | // Check that the content app is visible 26 | const apiTab = page.getByRole('tab', {name: 'API'}); 27 | await apiTab.click({force: true}); 28 | 29 | // Verify that the preview component is visible 30 | const apiPreviewElement = page.locator('bc-api-preview'); 31 | await expect(apiPreviewElement).toBeVisible(); 32 | }); 33 | 34 | test('Preview content app is not visible in new document', async ({page, umbracoUi}) => { 35 | await umbracoUi.goToBackOffice(); 36 | 37 | // Create new document 38 | await page.getByRole('tab', {name: ConstantHelper.sections.content}).click(); 39 | await umbracoUi.content.clickActionsMenuAtRoot(); 40 | await umbracoUi.content.clickCreateButton(); 41 | await umbracoUi.content.chooseDocumentType(docTypeName); 42 | 43 | // Verify that the content app is not visible 44 | await expect(page.locator('button[data-element="sub-view-deliveryApiPreview"]')).toBeHidden(); 45 | }); 46 | 47 | test('Preview content app shows only Preview section in saved document', async ({page, umbracoUi}) => { 48 | await umbracoUi.goToBackOffice(); 49 | 50 | // Navigate to content app 51 | await page.getByRole('tab', {name: ConstantHelper.sections.content}).click(); 52 | await umbracoUi.content.goToContentWithName(nodeName); 53 | const apiTab = page.getByRole('tab', {name: 'API'}); 54 | await apiTab.click({force: true}); 55 | 56 | // Check that only the preview section is being displayed 57 | const sectionsLocator = page.locator('bc-api-preview-section'); 58 | await expect(sectionsLocator).toHaveCount(1); 59 | 60 | const previewSection = sectionsLocator.first(); 61 | await expect(previewSection).toHaveAttribute('preview'); 62 | 63 | await expect(previewSection.locator('bc-json-preview')).toBeVisible(); 64 | }); 65 | 66 | async function createTestContent(umbracoApi: ApiHelpers) { 67 | const groupId = crypto.randomUUID(); 68 | 69 | const dataTypeData = await umbracoApi.dataType.getByName('Textstring'); 70 | expect(dataTypeData).toBeDefined(); 71 | 72 | const docType = new DocumentTypeBuilder() 73 | .withName(docTypeName) 74 | .withAlias(docTypeName) 75 | .withAllowedAsRoot(true) 76 | .addContainer() 77 | .withName('Content') 78 | .withId(groupId) 79 | .withType('Group') 80 | .done() 81 | .addProperty() 82 | .withContainerId(groupId) 83 | .withName('Title') 84 | .withAlias('title') 85 | .withDataTypeId(dataTypeData.id as string) 86 | .done() 87 | .build(); 88 | 89 | const createdDocType = await umbracoApi.documentType.create(docType); 90 | expect(createdDocType).toBeDefined(); 91 | 92 | const rootContentNode = new DocumentBuilder() 93 | .withDocumentTypeId(createdDocType!) 94 | .addVariant() 95 | .withName(nodeName) 96 | .done() 97 | .build(); 98 | 99 | await umbracoApi.document.create(rootContentNode); 100 | } 101 | 102 | async function cleanTestContent(umbracoApi: ApiHelpers) { 103 | await umbracoApi.document.ensureNameNotExists(nodeName); 104 | await umbracoApi.documentType.ensureNameNotExists(docTypeName); 105 | } 106 | }); 107 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.Playwright/tests/preview/preview-media.spec.ts: -------------------------------------------------------------------------------- 1 | import {expect} from '@playwright/test'; 2 | import {type ApiHelpers, ConstantHelper, test} from '@umbraco/playwright-testhelpers'; 3 | 4 | test.describe('API preview - Media', () => { 5 | const mediaName = 'PlaywrightTestMedia'; 6 | 7 | test.beforeEach(async ({umbracoApi}) => { 8 | await cleanTestMedia(umbracoApi); 9 | await createTestMedia(umbracoApi); 10 | }); 11 | 12 | test.afterEach(async ({umbracoApi}) => { 13 | await cleanTestMedia(umbracoApi); 14 | }); 15 | 16 | test('Preview content app is visible in saved media', async ({page, umbracoUi}) => { 17 | await umbracoUi.goToBackOffice(); 18 | 19 | await page.getByRole('tab', {name: ConstantHelper.sections.media}).click(); 20 | await umbracoUi.media.mediaCardItems.filter({hasText: mediaName}).locator('button').click(); 21 | 22 | // Check that the content app is visible 23 | const apiTab = page.getByRole('tab', {name: 'API'}); 24 | await apiTab.click({force: true}); 25 | 26 | // Verify that the preview component is visible 27 | const apiPreviewElement = page.locator('bc-api-preview'); 28 | await expect(apiPreviewElement).toBeVisible(); 29 | }); 30 | 31 | async function createTestMedia(umbracoApi: ApiHelpers) { 32 | await umbracoApi.media.createDefaultMediaFile(mediaName); 33 | } 34 | 35 | async function cleanTestMedia(umbracoApi: ApiHelpers) { 36 | await umbracoApi.media.ensureNameNotExists(mediaName); 37 | } 38 | }); 39 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.Playwright/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "experimentalDecorators": true, 5 | "useDefineForClassFields": false, 6 | "module": "NodeNext", 7 | "lib": [ "ES2022", "DOM", "DOM.Iterable" ], 8 | "skipLibCheck": true, 9 | 10 | "moduleResolution": "NodeNext", 11 | "allowImportingTsExtensions": true, 12 | "esModuleInterop": true, 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noUncheckedIndexedAccess": true, 22 | "noFallthroughCasesInSwitch": true 23 | }, 24 | "include": ["tests", "playwright.config.ts"] 25 | } 26 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": false, 4 | "tools": { 5 | "swashbuckle.aspnetcore.cli": { 6 | "version": "6.6.2", 7 | "commands": [ 8 | "swagger" 9 | ] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/Custom/AlwaysEnabledSwaggerPipelineFilter.cs: -------------------------------------------------------------------------------- 1 | using Umbraco.Cms.Api.Common.OpenApi; 2 | 3 | namespace UmbracoDeliveryApiExtensions.TestSite.Custom; 4 | 5 | public class AlwaysEnabledSwaggerPipelineFilter : SwaggerRouteTemplatePipelineFilter 6 | { 7 | public AlwaysEnabledSwaggerPipelineFilter(string name) : base(name) { } 8 | 9 | protected override bool SwaggerIsEnabled(IApplicationBuilder applicationBuilder) 10 | { 11 | return true; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/Program.cs: -------------------------------------------------------------------------------- 1 | using Umbraco.Cms.Api.Common.OpenApi; 2 | using Umbraco.Cms.Web.Common.ApplicationBuilder; 3 | using UmbracoDeliveryApiExtensions.TestSite.Custom; 4 | 5 | WebApplicationBuilder builder = WebApplication.CreateBuilder(args); 6 | 7 | builder.CreateUmbracoBuilder() 8 | .AddBackOffice() 9 | .AddWebsite() 10 | .AddDeliveryApi() 11 | .AddComposers() 12 | .Build(); 13 | 14 | // Always enable swagger (also in Production, which is the environment used by the tests) 15 | builder.Services.Configure(options => 16 | { 17 | options.PipelineFilters.RemoveAll(filter => filter is SwaggerRouteTemplatePipelineFilter); 18 | options.AddFilter(new AlwaysEnabledSwaggerPipelineFilter("UmbracoApiCommon")); 19 | }); 20 | 21 | WebApplication app = builder.Build(); 22 | 23 | await app.BootUmbracoAsync(); 24 | 25 | app.UseUmbraco() 26 | .WithMiddleware(u => 27 | { 28 | u.UseBackOffice(); 29 | u.UseWebsite(); 30 | }) 31 | .WithEndpoints(u => 32 | { 33 | u.UseBackOfficeEndpoints(); 34 | u.UseWebsiteEndpoints(); 35 | }); 36 | 37 | await app.RunAsync(); 38 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Development - IIS Express": { 4 | "commandName": "IISExpress", 5 | "launchBrowser": true, 6 | "launchUrl": "umbraco", 7 | "environmentVariables": { 8 | "ASPNETCORE_ENVIRONMENT": "Development" 9 | } 10 | }, 11 | "Development - Kestrel": { 12 | "commandName": "Project", 13 | "launchBrowser": true, 14 | "launchUrl": "umbraco", 15 | "environmentVariables": { 16 | "ASPNETCORE_ENVIRONMENT": "Development" 17 | }, 18 | "dotnetRunMessages": true, 19 | "applicationUrl": "https://localhost:44363;http://localhost:34962" 20 | }, 21 | "CI": { 22 | "commandName": "Project", 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Production" 25 | }, 26 | "dotnetRunMessages": true, 27 | "applicationUrl": "https://localhost:44363;http://localhost:34962" 28 | } 29 | }, 30 | "$schema": "https://json.schemastore.org/launchsettings.json", 31 | "iisSettings": { 32 | "windowsAuthentication": false, 33 | "anonymousAuthentication": true, 34 | "iisExpress": { 35 | "applicationUrl": "http://localhost:34962", 36 | "sslPort": 44363 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/UmbracoDeliveryApiExtensions.TestSite.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | true 16 | 17 | 18 | 19 | 20 | false 21 | false 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/Views/Partials/blockgrid/area.cshtml: -------------------------------------------------------------------------------- 1 | @using Umbraco.Extensions 2 | @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage 3 | 4 |
9 | @await Html.GetBlockGridItemsHtmlAsync(Model) 10 |
11 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/Views/Partials/blockgrid/areas.cshtml: -------------------------------------------------------------------------------- 1 | @using Umbraco.Extensions 2 | @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage 3 | @{ 4 | if (Model?.Areas.Any() != true) { return; } 5 | } 6 | 7 |
9 | @foreach (var area in Model.Areas) 10 | { 11 | @await Html.GetBlockGridItemAreaHtmlAsync(area) 12 | } 13 |
14 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/Views/Partials/blockgrid/default.cshtml: -------------------------------------------------------------------------------- 1 | @using Umbraco.Extensions 2 | @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage 3 | @{ 4 | if (Model?.Any() != true) { return; } 5 | } 6 | 7 |
10 | @await Html.GetBlockGridItemsHtmlAsync(Model) 11 |
12 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/Views/Partials/blockgrid/items.cshtml: -------------------------------------------------------------------------------- 1 | @using Umbraco.Cms.Core.Models.Blocks 2 | @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage> 3 | @{ 4 | if (Model?.Any() != true) { return; } 5 | } 6 | 7 |
8 | @foreach (var item in Model) 9 | { 10 | 11 |
19 | @{ 20 | var partialViewName = "blockgrid/Components/" + item.Content.ContentType.Alias; 21 | try 22 | { 23 | @await Html.PartialAsync(partialViewName, item) 24 | } 25 | catch (InvalidOperationException) 26 | { 27 |

28 | Could not render component of type: @(item.Content.ContentType.Alias) 29 |
30 | This likely happened because the partial view @partialViewName could not be found. 31 |

32 | } 33 | } 34 |
35 | } 36 |
37 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/Views/Partials/blocklist/default.cshtml: -------------------------------------------------------------------------------- 1 | @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage 2 | @{ 3 | if (Model?.Any() != true) { return; } 4 | } 5 |
6 | @foreach (var block in Model) 7 | { 8 | if (block?.ContentUdi == null) { continue; } 9 | var data = block.Content; 10 | 11 | @await Html.PartialAsync("blocklist/Components/" + data.ContentType.Alias, block) 12 | } 13 |
14 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/Views/Partials/grid/bootstrap3-fluid.cshtml: -------------------------------------------------------------------------------- 1 | @using System.Web 2 | @using Microsoft.AspNetCore.Html 3 | @using Newtonsoft.Json.Linq 4 | @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage 5 | 6 | @* 7 | Razor helpers located at the bottom of this file 8 | *@ 9 | 10 | @if (Model is JObject && Model?.sections is not null) 11 | { 12 | var oneColumn = ((System.Collections.ICollection)Model.sections).Count == 1; 13 | 14 |
15 | @if (oneColumn) 16 | { 17 | foreach (var section in Model.sections) 18 | { 19 |
20 | @foreach (var row in section.rows) 21 | { 22 | renderRow(row); 23 | } 24 |
25 | } 26 | } 27 | else 28 | { 29 |
30 | @foreach (var sec in Model.sections) 31 | { 32 |
33 |
34 | @foreach (var row in sec.rows) 35 | { 36 | renderRow(row); 37 | } 38 |
39 |
40 | } 41 |
42 | } 43 |
44 | } 45 | 46 | @functions{ 47 | 48 | private async Task renderRow(dynamic row) 49 | { 50 |
51 |
52 | @foreach (var area in row.areas) 53 | { 54 |
55 |
56 | @foreach (var control in area.controls) 57 | { 58 | if (control?.editor?.view != null) 59 | { 60 | @await Html.PartialAsync("grid/editors/base", (object)control) 61 | } 62 | } 63 |
64 |
65 | } 66 |
67 |
68 | } 69 | } 70 | 71 | @functions{ 72 | 73 | public static HtmlString RenderElementAttributes(dynamic contentItem) 74 | { 75 | var attrs = new List(); 76 | JObject cfg = contentItem.config; 77 | 78 | if (cfg != null) 79 | { 80 | foreach (JProperty property in cfg.Properties()) 81 | { 82 | var propertyValue = HttpUtility.HtmlAttributeEncode(property.Value.ToString()); 83 | attrs.Add(property.Name + "=\"" + propertyValue + "\""); 84 | } 85 | } 86 | 87 | JObject style = contentItem.styles; 88 | 89 | if (style != null) { 90 | var cssVals = new List(); 91 | foreach (JProperty property in style.Properties()) 92 | { 93 | var propertyValue = property.Value.ToString(); 94 | if (string.IsNullOrWhiteSpace(propertyValue) == false) 95 | { 96 | cssVals.Add(property.Name + ":" + propertyValue + ";"); 97 | } 98 | } 99 | 100 | if (cssVals.Any()) 101 | attrs.Add("style='" + HttpUtility.HtmlAttributeEncode(string.Join(" ", cssVals)) + "'"); 102 | } 103 | 104 | return new HtmlString(string.Join(" ", attrs)); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/Views/Partials/grid/bootstrap3.cshtml: -------------------------------------------------------------------------------- 1 | @using System.Web 2 | @using Microsoft.AspNetCore.Html 3 | @using Newtonsoft.Json.Linq 4 | @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage 5 | 6 | @if (Model is JObject && Model?.sections is not null) 7 | { 8 | var oneColumn = ((System.Collections.ICollection)Model.sections).Count == 1; 9 | 10 |
11 | @if (oneColumn) 12 | { 13 | foreach (var section in Model.sections) 14 | { 15 |
16 | @foreach (var row in section.rows) 17 | { 18 | renderRow(row, true); 19 | } 20 |
21 | } 22 | } 23 | else 24 | { 25 |
26 |
27 | @foreach (var sec in Model.sections) 28 | { 29 |
30 |
31 | @foreach (var row in sec.rows) 32 | { 33 | renderRow(row, false); 34 | } 35 |
36 |
37 | } 38 |
39 |
40 | } 41 |
42 | } 43 | 44 | @functions{ 45 | 46 | private async Task renderRow(dynamic row, bool singleColumn) 47 | { 48 |
49 | @if (singleColumn) { 50 | @:
51 | } 52 |
53 | @foreach (var area in row.areas) 54 | { 55 |
56 |
57 | @foreach (var control in area.controls) 58 | { 59 | if (control?.editor?.view != null) 60 | { 61 | @await Html.PartialAsync("grid/editors/base", (object)control) 62 | } 63 | } 64 |
65 |
66 | } 67 |
68 | @if (singleColumn) { 69 | @:
70 | } 71 |
72 | } 73 | 74 | } 75 | 76 | @functions{ 77 | 78 | public static HtmlString RenderElementAttributes(dynamic contentItem) 79 | { 80 | var attrs = new List(); 81 | JObject cfg = contentItem.config; 82 | 83 | if (cfg != null) 84 | { 85 | foreach (JProperty property in cfg.Properties()) 86 | { 87 | var propertyValue = HttpUtility.HtmlAttributeEncode(property.Value.ToString()); 88 | attrs.Add(property.Name + "=\"" + propertyValue + "\""); 89 | } 90 | } 91 | 92 | JObject style = contentItem.styles; 93 | 94 | if (style != null) 95 | { 96 | var cssVals = new List(); 97 | foreach (JProperty property in style.Properties()) 98 | { 99 | var propertyValue = property.Value.ToString(); 100 | if (string.IsNullOrWhiteSpace(propertyValue) == false) 101 | { 102 | cssVals.Add(property.Name + ":" + propertyValue + ";"); 103 | } 104 | } 105 | 106 | if (cssVals.Any()) 107 | attrs.Add("style=\"" + HttpUtility.HtmlAttributeEncode(string.Join(" ", cssVals)) + "\""); 108 | } 109 | 110 | return new HtmlString(string.Join(" ", attrs)); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/Views/Partials/grid/editors/base.cshtml: -------------------------------------------------------------------------------- 1 | @model dynamic 2 | 3 | @try 4 | { 5 | string editor = EditorView(Model); 6 | @await Html.PartialAsync(editor, Model as object) 7 | } 8 | catch (Exception ex) 9 | { 10 |
@ex.ToString()
11 | } 12 | 13 | @functions{ 14 | 15 | public static string EditorView(dynamic contentItem) 16 | { 17 | string view = contentItem.editor.render != null ? contentItem.editor.render.ToString() : contentItem.editor.view.ToString(); 18 | view = view.Replace(".html", ".cshtml"); 19 | 20 | if (!view.Contains("/")) 21 | { 22 | view = "grid/editors/" + view; 23 | } 24 | 25 | return view; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/Views/Partials/grid/editors/embed.cshtml: -------------------------------------------------------------------------------- 1 | @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage 2 | 3 | @if (Model is not null) 4 | { 5 | string embedValue = Convert.ToString(Model.value); 6 | embedValue = embedValue.DetectIsJson() ? Model.value.preview : Model.value; 7 | 8 |
9 | @Html.Raw(embedValue) 10 |
11 | } 12 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/Views/Partials/grid/editors/macro.cshtml: -------------------------------------------------------------------------------- 1 | @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage 2 | 3 | @if (Model?.value is not null) 4 | { 5 | string macroAlias = Model.value.macroAlias.ToString(); 6 | var parameters = new Dictionary(); 7 | foreach (var mpd in Model.value.macroParamsDictionary) 8 | { 9 | parameters.Add(mpd.Name, mpd.Value); 10 | } 11 | 12 | 13 | @await Umbraco.RenderMacroAsync(macroAlias, parameters) 14 | 15 | } 16 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/Views/Partials/grid/editors/media.cshtml: -------------------------------------------------------------------------------- 1 | @model dynamic 2 | @using Umbraco.Cms.Core.Media 3 | @using Umbraco.Cms.Core.PropertyEditors.ValueConverters 4 | @inject IImageUrlGenerator ImageUrlGenerator 5 | 6 | @if (Model?.value is not null) 7 | { 8 | var url = Model.value.image; 9 | 10 | if (Model.editor.config != null && Model.editor.config.size != null) 11 | { 12 | if (Model.value.coordinates != null) 13 | { 14 | url = ImageCropperTemplateCoreExtensions.GetCropUrl( 15 | (string)url, 16 | ImageUrlGenerator, 17 | width: (int)Model.editor.config.size.width, 18 | height: (int)Model.editor.config.size.height, 19 | cropAlias: "default", 20 | cropDataSet: new ImageCropperValue 21 | { 22 | Crops = new[] 23 | { 24 | new ImageCropperValue.ImageCropperCrop 25 | { 26 | Alias = "default", 27 | Coordinates = new ImageCropperValue.ImageCropperCropCoordinates 28 | { 29 | X1 = (decimal)Model.value.coordinates.x1, 30 | Y1 = (decimal)Model.value.coordinates.y1, 31 | X2 = (decimal)Model.value.coordinates.x2, 32 | Y2 = (decimal)Model.value.coordinates.y2 33 | } 34 | } 35 | } 36 | }); 37 | } 38 | else 39 | { 40 | url = ImageCropperTemplateCoreExtensions.GetCropUrl( 41 | (string)url, 42 | ImageUrlGenerator, 43 | width: (int)Model.editor.config.size.width, 44 | height: (int)Model.editor.config.size.height, 45 | cropDataSet: new ImageCropperValue 46 | { 47 | FocalPoint = new ImageCropperValue.ImageCropperFocalPoint 48 | { 49 | Top = Model.value.focalPoint == null ? 0.5m : Model.value.focalPoint.top, 50 | Left = Model.value.focalPoint == null ? 0.5m : Model.value.focalPoint.left 51 | } 52 | }); 53 | } 54 | } 55 | 56 | var altText = Model.value.altText ?? Model.value.caption ?? string.Empty; 57 | 58 | @altText 59 | 60 | if (Model.value.caption != null) 61 | { 62 |

@Model.value.caption

63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/Views/Partials/grid/editors/rte.cshtml: -------------------------------------------------------------------------------- 1 | @using Umbraco.Cms.Core.Templates 2 | @model dynamic 3 | @inject HtmlLocalLinkParser HtmlLocalLinkParser; 4 | @inject HtmlUrlParser HtmlUrlParser; 5 | @inject HtmlImageSourceParser HtmlImageSourceParser; 6 | 7 | @{ 8 | var value = HtmlLocalLinkParser.EnsureInternalLinks(Model?.value.ToString()); 9 | value = HtmlUrlParser.EnsureUrls(value); 10 | value = HtmlImageSourceParser.EnsureImageSources(value); 11 | } 12 | 13 | @Html.Raw(value) 14 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/Views/Partials/grid/editors/textstring.cshtml: -------------------------------------------------------------------------------- 1 | @model dynamic 2 | 3 | @if (Model?.editor.config.markup is not null) 4 | { 5 | string markup = Model.editor.config.markup.ToString(); 6 | markup = markup.Replace("#value#", Html.ReplaceLineBreaks((string)Model.value.ToString()).ToString()); 7 | 8 | if (Model.editor.config.style != null) 9 | { 10 | markup = markup.Replace("#style#", Model.editor.config.style.ToString()); 11 | } 12 | 13 | 14 | @Html.Raw(markup) 15 | 16 | } 17 | else 18 | { 19 | 20 |
@Model?.value
21 |
22 | } 23 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using Umbraco.Extensions 2 | @using Umbraco.Cms.Web.Common.PublishedModels 3 | @using Umbraco.Cms.Web.Common.Views 4 | @using Umbraco.Cms.Core.Models.PublishedContent 5 | @using Microsoft.AspNetCore.Html 6 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 7 | @addTagHelper *, Smidge 8 | @inject Smidge.SmidgeHelper SmidgeHelper 9 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "appsettings-schema.json", 3 | "Umbraco": { 4 | "CMS": { 5 | "Content": { 6 | "MacroErrors": "Throw" 7 | }, 8 | "Hosting": { 9 | "Debug": true 10 | }, 11 | "RuntimeMinification": { 12 | "UseInMemoryCache": true, 13 | "CacheBuster": "Timestamp" 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "appsettings-schema.json", 3 | "Serilog": { 4 | "MinimumLevel": { 5 | "Default": "Information", 6 | "Override": { 7 | "Microsoft": "Warning", 8 | "Microsoft.Hosting.Lifetime": "Information", 9 | "System": "Warning" 10 | } 11 | }, 12 | "WriteTo": [ 13 | { 14 | "Name": "Async", 15 | "Args": { 16 | "configure": [ 17 | { 18 | "Name": "Console" 19 | } 20 | ] 21 | } 22 | } 23 | ] 24 | }, 25 | "ConnectionStrings": { 26 | "umbracoDbDSN": "Data Source=|DataDirectory|/Umbraco.sqlite.db;Cache=Shared;Foreign Keys=True;Pooling=True", 27 | "umbracoDbDSN_ProviderName": "Microsoft.Data.Sqlite" 28 | }, 29 | "Umbraco": { 30 | "CMS": { 31 | "Global": { 32 | "Id": "a20e2bed-08c3-44a3-b756-c925580826b6", 33 | "SanitizeTinyMce": true, 34 | "TimeOut": "04:00:00" 35 | }, 36 | "Unattended": { 37 | "InstallUnattended": true, 38 | "UpgradeUnattended": true, 39 | "UnattendedUserName": "Test", 40 | "UnattendedUserEmail": "admin@umbraco", 41 | "UnattendedUserPassword": "#Umbraco123!" 42 | }, 43 | "Content": { 44 | "AllowEditInvariantFromNonDefault": true, 45 | "ContentVersionCleanupPolicy": { 46 | "EnableCleanup": true 47 | } 48 | }, 49 | "Security": { 50 | "KeepUserLoggedIn": true 51 | }, 52 | "DeliveryApi": { 53 | "Enabled": true, 54 | "Media": { 55 | "Enabled": true 56 | } 57 | } 58 | } 59 | }, 60 | "uSync": { 61 | "Settings": { 62 | "ImportOnFirstBoot": true, 63 | "ImportAtStartup": "All" 64 | } 65 | }, 66 | "DeliveryApiExtensions": { 67 | "Preview": { 68 | "Enabled": true, 69 | "Media": { 70 | "Enabled": true 71 | }, 72 | "AllowedUserGroupAliases": [] 73 | }, 74 | "TypedSwagger": { 75 | "Enabled": true, 76 | "Mode": "Auto" 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/ContentTypes/blocksettings.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Block Settings 5 | icon-umb-settings color-deep-purple 6 | folder.png 7 | 8 | False 9 | 00000000-0000-0000-0000-000000000000 10 | Nothing 11 | true 12 | 13 | False 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 549996a7-de20-49de-8ce5-3f9df3592697 25 | Id 26 | anchorId 27 | 0cc0eba1-9960-42c9-bf9b-60e150b429ae 28 | Umbraco.TextBox 29 | false 30 | 31 | 32 | 0 33 | Content 34 | Nothing 35 | 36 | 37 | false 38 | 39 | 40 | 41 | 42 | 7b6eb0b9-90a8-415d-be86-bee9bdd8188e 43 | Content 44 | content 45 | Group 46 | 0 47 | 48 | 49 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/ContentTypes/testblock.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Test Block 5 | icon-science color-purple 6 | folder.png 7 | 8 | False 9 | 00000000-0000-0000-0000-000000000000 10 | Nothing 11 | true 12 | 13 | False 14 | 15 | 16 | 17 | 18 | testComposition 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | f9fd6b54-dde0-46cf-8d1f-d1e66a7295f0 27 | Blocks 28 | blocks 29 | b99d38a6-00a4-443f-8fa5-437a81d61479 30 | Umbraco.BlockList 31 | false 32 | 33 | 34 | 2 35 | Content 36 | Nothing 37 | 38 | 39 | false 40 | 41 | 42 | 9fbe7788-bc40-40a6-ad24-3f3c8a241709 43 | Multinode Treepicker 44 | multinodeTreepicker 45 | 99d35128-9961-4ed6-a2b5-b97c7d00faa7 46 | Umbraco.MultiNodeTreePicker 47 | false 48 | 49 | 50 | 1 51 | Content 52 | Nothing 53 | 54 | 55 | false 56 | 57 | 58 | 9a5b40d4-b3b8-42c9-ad2b-028ed4717558 59 | String 60 | string 61 | 0cc0eba1-9960-42c9-bf9b-60e150b429ae 62 | Umbraco.TextBox 63 | false 64 | 65 | 66 | 0 67 | Content 68 | Nothing 69 | 70 | 71 | false 72 | 73 | 74 | 75 | 76 | 00b193ef-be36-4b1c-9dcb-b5e14d00a2b5 77 | Content 78 | content 79 | Group 80 | 0 81 | 82 | 83 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/ContentTypes/testblock2.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Test Block 2 5 | icon-science color-cyan 6 | folder.png 7 | 8 | False 9 | 00000000-0000-0000-0000-000000000000 10 | Nothing 11 | true 12 | 13 | False 14 | 15 | 16 | 17 | 18 | testComposition 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 75d1e20d-c380-4673-80c8-efa34a365e25 27 | Test Block 2 28 | thisIsTestBlock2 29 | 0cc0eba1-9960-42c9-bf9b-60e150b429ae 30 | Umbraco.TextBox 31 | false 32 | 33 | 34 | 0 35 | Content 36 | Nothing 37 | 38 | 39 | false 40 | 41 | 42 | 43 | 44 | 8cdf0d55-d22a-4940-b9f6-6e238cfad518 45 | Content 46 | content 47 | Group 48 | 0 49 | 50 | 51 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/ContentTypes/testcomposition.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Test Composition 5 | icon-defrag color-purple 6 | folder.png 7 | 8 | False 9 | 00000000-0000-0000-0000-000000000000 10 | Culture 11 | true 12 | 13 | False 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 33d68a8c-96d1-4c85-874f-03a79dec8607 25 | Shared string 26 | sharedString 27 | 0cc0eba1-9960-42c9-bf9b-60e150b429ae 28 | Umbraco.TextBox 29 | false 30 | 31 | 32 | 1 33 | Shared 34 | Culture 35 | 36 | 37 | false 38 | 39 | 40 | c59ce969-de16-44a8-91ff-82768181ccb6 41 | Shared Toggle 42 | sharedToggle 43 | 92897bc6-a5f3-4ffe-ae27-f2e7e33dda49 44 | Umbraco.TrueFalse 45 | false 46 | 47 | 48 | 0 49 | Shared 50 | Culture 51 | 52 | 53 | false 54 | 55 | 56 | 57 | 58 | d0a7ebce-25c6-4992-b4b0-d0ad630d80e7 59 | Shared 60 | shared 61 | Group 62 | 0 63 | 64 | 65 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/ContentTypes/testcomposition2.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Test Composition 2 5 | icon-defrag color-cyan 6 | folder.png 7 | 8 | False 9 | 00000000-0000-0000-0000-000000000000 10 | Culture 11 | true 12 | 13 | False 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | f37175bc-b3ae-4299-81c6-14196344586d 25 | Shared radiobox 26 | sharedRadiobox 27 | bb5f57c9-ce2b-4bb9-b697-4caca783a805 28 | Umbraco.RadioButtonList 29 | false 30 | 31 | 32 | 0 33 | Shared 2 34 | Culture 35 | 36 | 37 | false 38 | 39 | 40 | 5b5c44ac-a193-4870-a505-91a46d50f6e8 41 | Shared rich text 42 | sharedRichText 43 | ca90c950-0aff-4e72-b976-a30b1ac57dad 44 | Umbraco.RichText 45 | false 46 | 47 | 48 | 1 49 | Shared 2 50 | Culture 51 | 52 | 53 | false 54 | 55 | 56 | 57 | 58 | d53dd67a-70f2-4da2-adfc-40c1a84fc786 59 | Shared 2 60 | shared2 61 | Group 62 | 0 63 | 64 | 65 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/ApprovedColor.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Approved Color 5 | Umbraco.ColorPicker 6 | Umb.PropertyEditorUi.ColorPicker 7 | 8 | 22 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/BlockGrid.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Block Grid 5 | Umbraco.BlockGrid 6 | Umb.PropertyEditorUi.BlockGrid 7 | 8 | 27 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/BlockList.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Block List 5 | Umbraco.BlockList 6 | Umb.PropertyEditorUi.BlockList 7 | 8 | 27 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/CheckboxList.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Checkbox list 5 | Umbraco.CheckBoxList 6 | Umb.PropertyEditorUi.CheckBoxList 7 | 8 | 16 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/ContentPicker.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Content Picker 5 | Umbraco.ContentPicker 6 | Umb.PropertyEditorUi.DocumentPicker 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/DatePicker.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Date Picker 5 | Umbraco.DateTime 6 | Umb.PropertyEditorUi.DatePicker 7 | 8 | 11 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/DatePickerWithTime.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Date Picker with time 5 | Umbraco.DateTime 6 | Umb.PropertyEditorUi.DatePicker 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/Decimal.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Decimal 5 | Umbraco.Decimal 6 | Umb.PropertyEditorUi.Decimal 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/Dropdown.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Dropdown 5 | Umbraco.DropDown.Flexible 6 | Umb.PropertyEditorUi.Dropdown 7 | 8 | 17 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/DropdownMultiple.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Dropdown multiple 5 | Umbraco.DropDown.Flexible 6 | Umb.PropertyEditorUi.Dropdown 7 | 8 | 11 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/EmailAddress.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Email address 5 | Umbraco.EmailAddress 6 | Umb.PropertyEditorUi.EmailAddress 7 | 8 | 11 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/EyeDropperColorPicker.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Eye Dropper Color Picker 5 | Umbraco.ColorPicker.EyeDropper 6 | Umb.PropertyEditorUi.EyeDropper 7 | 8 | 12 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/ImageCropper.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Image Cropper 5 | Umbraco.ImageCropper 6 | Umb.PropertyEditorUi.ImageCropper 7 | 8 | 17 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/ImageMediaPicker.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Image Media Picker 5 | Umbraco.MediaPicker3 6 | Umb.PropertyEditorUi.MediaPicker 7 | 8 | 17 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/LabelBigint.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Label (bigint) 5 | Umbraco.Label 6 | Umb.PropertyEditorUi.Label 7 | 8 | 11 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/LabelDatetime.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Label (datetime) 5 | Umbraco.Label 6 | Umb.PropertyEditorUi.Label 7 | 8 | 11 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/LabelDecimal.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Label (decimal) 5 | Umbraco.Label 6 | Umb.PropertyEditorUi.Label 7 | 8 | 11 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/LabelInteger.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Label (integer) 5 | Umbraco.Label 6 | Umb.PropertyEditorUi.Label 7 | 8 | 11 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/LabelString.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Label (string) 5 | Umbraco.Label 6 | Umb.PropertyEditorUi.Label 7 | 8 | 11 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/LabelTime.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Label (time) 5 | Umbraco.Label 6 | Umb.PropertyEditorUi.Label 7 | 8 | 11 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/ListViewContent.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | List View - Content 5 | Umbraco.ListView 6 | Umb.PropertyEditorUi.Collection 7 | 8 | 54 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/ListViewMedia.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | List View - Media 5 | Umbraco.ListView 6 | Umb.PropertyEditorUi.Collection 7 | 8 | 54 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/ListViewMembers.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | List View - Members 5 | Umbraco.ListView 6 | Umb.PropertyEditorUi.Collection 7 | 8 | 60 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/MarkdownEditor.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Markdown editor 5 | Umbraco.MarkdownEditor 6 | Umb.PropertyEditorUi.MarkdownEditor 7 | 8 | 12 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/MediaPicker.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Media Picker 5 | Umbraco.MediaPicker3 6 | Umb.PropertyEditorUi.MediaPicker 7 | 8 | 25 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/MemberGroupPicker.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Member Group Picker 5 | Umbraco.MemberGroupPicker 6 | Umb.PropertyEditorUi.MemberGroupPicker 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/MemberPicker.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Member Picker 5 | Umbraco.MemberPicker 6 | Umb.PropertyEditorUi.MemberPicker 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/MultiURLPicker.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Multi URL Picker 5 | Umbraco.MultiUrlPicker 6 | Umb.PropertyEditorUi.MultiUrlPicker 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/MultinodeTreepicker.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Multinode Treepicker 5 | Umbraco.MultiNodeTreePicker 6 | Umb.PropertyEditorUi.ContentPicker 7 | 8 | 20 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/MultipleImageMediaPicker.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Multiple Image Media Picker 5 | Umbraco.MediaPicker3 6 | Umb.PropertyEditorUi.MediaPicker 7 | 8 | 13 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/MultipleMediaPicker.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Multiple Media Picker 5 | Umbraco.MediaPicker3 6 | Umb.PropertyEditorUi.MediaPicker 7 | 8 | 11 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/Numeric.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Numeric 5 | Umbraco.Integer 6 | Umb.PropertyEditorUi.Integer 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/Radiobox.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Radiobox 5 | Umbraco.RadioButtonList 6 | Umb.PropertyEditorUi.RadioButtonList 7 | 8 | 16 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/RepeatableTextstrings.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Repeatable textstrings 5 | Umbraco.MultipleTextstring 6 | Umb.PropertyEditorUi.MultipleTextString 7 | 8 | 13 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/RichtextEditor.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Richtext editor 5 | Umbraco.RichText 6 | Umb.PropertyEditorUi.TinyMCE 7 | 8 | 11 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/Slider.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Slider 5 | Umbraco.Slider 6 | Umb.PropertyEditorUi.Slider 7 | 8 | 16 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/Tags.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Tags 5 | Umbraco.Tags 6 | Umb.PropertyEditorUi.Tags 7 | 8 | 12 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/Textarea.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Textarea 5 | Umbraco.TextArea 6 | Umb.PropertyEditorUi.TextArea 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/Textstring.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Textstring 5 | Umbraco.TextBox 6 | Umb.PropertyEditorUi.TextBox 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/Truefalse.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | True/false 5 | Umbraco.TrueFalse 6 | Umb.PropertyEditorUi.Toggle 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/UploadArticle.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Upload Article 5 | Umbraco.UploadField 6 | Umb.PropertyEditorUi.UploadField 7 | 8 | 16 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/UploadAudio.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Upload Audio 5 | Umbraco.UploadField 6 | Umb.PropertyEditorUi.UploadField 7 | 8 | 17 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/UploadFile.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Upload File 5 | Umbraco.UploadField 6 | Umb.PropertyEditorUi.UploadField 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/UploadVectorGraphics.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Upload Vector Graphics 5 | Umbraco.UploadField 6 | Umb.PropertyEditorUi.UploadField 7 | 8 | 14 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/UploadVideo.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Upload Video 5 | Umbraco.UploadField 6 | Umb.PropertyEditorUi.UploadField 7 | 8 | 16 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/DataTypes/UserPicker.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | User Picker 5 | Umbraco.UserPicker 6 | Umb.PropertyEditorUi.UserPicker 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/Domains/_en-us.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | false 5 | en-US 6 | /Test 7 | 0 8 | 9 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/Domains/invariant_en-us.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | false 5 | en-US 6 | /Test (invariant) 7 | 0 8 | 9 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/Domains/pt_pt-pt.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | false 5 | pt-PT 6 | /Test 7 | 1 8 | 9 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/Languages/en-us.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | English (United States) 4 | en-US 5 | false 6 | true 7 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/Languages/pt-pt.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | Portuguese (Portugal) 4 | pt-PT 5 | false 6 | false 7 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/Media/dark-blue.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | /DarkBlue 6 | false 7 | Image 8 | 2023-10-29T10:33:56 9 | 10 | 0 11 | 12 | 13 | 14 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/MediaTypes/file.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | File 5 | icon-document 6 | icon-document 7 | 8 | True 9 | 00000000-0000-0000-0000-000000000000 10 | Nothing 11 | false 12 | 13 | System.Threading.Tasks.Task`1[System.Xml.Linq.XElement]50899f9c-023a-4466-b623-aba9049885feFilefileGroup1 -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/MediaTypes/folder.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Folder 5 | icon-folder 6 | icon-folder 7 | 8 | True 9 | 3a0156c4-3b8c-4803-bdc1-6871faa83fff 10 | Nothing 11 | false 12 | 13 | System.Threading.Tasks.Task`1[System.Xml.Linq.XElement]FolderImageFileumbracoMediaVideoumbracoMediaAudioumbracoMediaArticleumbracoMediaVectorGraphics -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/MediaTypes/image.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Image 5 | icon-picture 6 | icon-picture 7 | 8 | True 9 | 00000000-0000-0000-0000-000000000000 10 | Nothing 11 | false 12 | 13 | System.Threading.Tasks.Task`1[System.Xml.Linq.XElement]79ed4d07-254a-42cf-8fa9-ebe1c116a596ImageimageGroup1 -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/MediaTypes/umbracomediaarticle.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Article 5 | icon-article 6 | icon-article 7 | 8 | True 9 | 00000000-0000-0000-0000-000000000000 10 | Nothing 11 | false 12 | 13 | System.Threading.Tasks.Task`1[System.Xml.Linq.XElement]9af3bd65-f687-4453-9518-5f180d1898ecArticlearticleGroup1 -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/MediaTypes/umbracomediaaudio.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Audio 5 | icon-sound-waves 6 | icon-sound-waves 7 | 8 | True 9 | 00000000-0000-0000-0000-000000000000 10 | Nothing 11 | false 12 | 13 | System.Threading.Tasks.Task`1[System.Xml.Linq.XElement]335fb495-0a87-4e82-b902-30eb367b767cAudioaudioGroup1 -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/MediaTypes/umbracomediavectorgraphics.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Vector Graphics (SVG) 5 | icon-picture 6 | icon-picture 7 | 8 | True 9 | 00000000-0000-0000-0000-000000000000 10 | Nothing 11 | false 12 | 13 | System.Threading.Tasks.Task`1[System.Xml.Linq.XElement]f199b4d7-9e84-439f-8531-f87d9af37711Vector GraphicsvectorGraphicsGroup1 -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/MediaTypes/umbracomediavideo.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Video 5 | icon-video 6 | icon-video 7 | 8 | True 9 | 00000000-0000-0000-0000-000000000000 10 | Nothing 11 | false 12 | 13 | System.Threading.Tasks.Task`1[System.Xml.Linq.XElement]2f0a61b6-cf92-4ff4-b437-751ab35eb254VideovideoGroup1 -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/MemberTypes/member.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Member 5 | icon-user 6 | icon-user 7 | 8 | False 9 | 00000000-0000-0000-0000-000000000000 10 | Nothing 11 | false 12 | 13 | System.Threading.Tasks.Task`1[System.Xml.Linq.XElement]0756729d-d665-46e3-b84a-37aceaa614f8MembershipmembershipGroup1 -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/uSync/v15/usync.config: -------------------------------------------------------------------------------- 1 |  2 | -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ByteCrumb/Umbraco.Community.DeliveryApiExtensions/c54639acb3f164da022f96cff3365c059f365d64/tests/UmbracoDeliveryApiExtensions.TestSite/wwwroot/favicon.ico -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/wwwroot/media/0hljflgw/pink.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ByteCrumb/Umbraco.Community.DeliveryApiExtensions/c54639acb3f164da022f96cff3365c059f365d64/tests/UmbracoDeliveryApiExtensions.TestSite/wwwroot/media/0hljflgw/pink.jpg -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/wwwroot/media/fw2bqwqg/pink.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ByteCrumb/Umbraco.Community.DeliveryApiExtensions/c54639acb3f164da022f96cff3365c059f365d64/tests/UmbracoDeliveryApiExtensions.TestSite/wwwroot/media/fw2bqwqg/pink.jpg -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/wwwroot/media/mvvhicln/dark-blue.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ByteCrumb/Umbraco.Community.DeliveryApiExtensions/c54639acb3f164da022f96cff3365c059f365d64/tests/UmbracoDeliveryApiExtensions.TestSite/wwwroot/media/mvvhicln/dark-blue.jpg -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/wwwroot/media/otgga1kg/dark-blue.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ByteCrumb/Umbraco.Community.DeliveryApiExtensions/c54639acb3f164da022f96cff3365c059f365d64/tests/UmbracoDeliveryApiExtensions.TestSite/wwwroot/media/otgga1kg/dark-blue.jpg -------------------------------------------------------------------------------- /tests/UmbracoDeliveryApiExtensions.TestSite/wwwroot/media/q0yb0acw/dark-blue.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ByteCrumb/Umbraco.Community.DeliveryApiExtensions/c54639acb3f164da022f96cff3365c059f365d64/tests/UmbracoDeliveryApiExtensions.TestSite/wwwroot/media/q0yb0acw/dark-blue.jpg -------------------------------------------------------------------------------- /tests/clients/nswag/ClientTemplateOverrides/File.Header.liquid: -------------------------------------------------------------------------------- 1 | #pragma warning disable 108 // Disable "CS0108 '{derivedDto}.ToJson()' hides inherited member '{dtoBase}.ToJson()'. Use the new keyword if hiding was intended." 2 | #pragma warning disable 114 // Disable "CS0114 '{derivedDto}.RaisePropertyChanged(String)' hides inherited member 'dtoBase.RaisePropertyChanged(String)'. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword." 3 | #pragma warning disable 472 // Disable "CS0472 The result of the expression is always 'false' since a value of type 'Int32' is never equal to 'null' of type 'Int32?' 4 | #pragma warning disable 612 // Disable "CS0612 '...' is obsolete" 5 | #pragma warning disable 1573 // Disable "CS1573 Parameter '...' has no matching param tag in the XML comment for ... 6 | #pragma warning disable 1591 // Disable "CS1591 Missing XML comment for publicly visible type or member ..." 7 | #pragma warning disable 8073 // Disable "CS8073 The result of the expression is always 'false' since a value of type 'T' is never equal to 'null' of type 'T?'" 8 | #pragma warning disable 3016 // Disable "CS3016 Arrays as attribute arguments is not CLS-compliant" 9 | #pragma warning disable 8603 // Disable "CS8603 Possible null reference return" 10 | #pragma warning disable 8604 // Disable "CS8604 Possible null reference argument for parameter" 11 | #pragma warning disable 8765 // Disable "CS8765 Nullability of type of parameter doesn't match overridden member (possibly because of nullability attributes)." 12 | #pragma warning disable 8618 // Disable "CS8618 Non-nullable variable must contain a non-null value when exiting constructor. Consider declaring it as nullable." 13 | #pragma warning disable 8600 // Disable "CS8600 Converting null literal or possible null value to non-nullable type." 14 | #pragma warning disable 8602 // Disable "CS8602 Dereference of a possibly null reference." 15 | -------------------------------------------------------------------------------- /tests/clients/nswag/nswag.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | 6 | 7 | 8 | 9 | all 10 | runtime; build; native; contentfiles; analyzers; buildtransitive 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | $(ProjectDir)UmbracoApi.g.cs 25 | UmbracoApi 26 | nswag 27 | /JsonLibrary:SystemTextJson /GenerateClientInterfaces:true /GenerateNullableReferenceTypes:true /GenerateOptionalPropertiesAsNullable:true /GenerateOptionalParameters:true /TemplateDirectory:ClientTemplateOverrides 28 | 29 | 30 | -------------------------------------------------------------------------------- /tests/clients/openapi-typescript/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": false, 4 | "node": true, 5 | "es2021": true 6 | }, 7 | "extends": "xo-space", 8 | "ignorePatterns": ["api/*"], 9 | "overrides": [ 10 | { 11 | "extends": [ 12 | "xo-typescript/space" 13 | ], 14 | "files": ["*.ts", "*.tsx"], 15 | "plugins": ["simple-import-sort"], 16 | "rules": { 17 | "@typescript-eslint/consistent-type-definitions": [ 18 | "warn", "interface" 19 | ], 20 | "@typescript-eslint/naming-convention": [ 21 | "error", 22 | { 23 | "selector": ["class", "interface", "typeAlias", "enum", "typeParameter"], 24 | "format": ["StrictPascalCase"] 25 | } 26 | ], 27 | "new-cap": [ 28 | "error", 29 | { 30 | "capIsNewExceptionPattern": "^GET|PUT|POST|DELETE|OPTIONS|HEAD|PATCH|TRACE$" 31 | } 32 | ], 33 | "simple-import-sort/imports": "error", 34 | "simple-import-sort/exports": "error" 35 | } 36 | } 37 | ], 38 | "parserOptions": { 39 | "ecmaVersion": "latest", 40 | "sourceType": "module" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/clients/openapi-typescript/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /tests/clients/openapi-typescript/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Start", 6 | "type": "node", 7 | "request": "launch", 8 | "runtimeExecutable": "npm", 9 | "runtimeArgs": [ "run", "start" ], 10 | "cwd": "${workspaceFolder}", 11 | "console": "internalConsole" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tests/clients/openapi-typescript/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": "explicit" 4 | }, 5 | "typescript.tsdk": "node_modules\\typescript\\lib" 6 | } 7 | -------------------------------------------------------------------------------- /tests/clients/openapi-typescript/README.md: -------------------------------------------------------------------------------- 1 | # openapi-typescript 2 | 3 | 4 | -------------------------------------------------------------------------------- /tests/clients/openapi-typescript/app.ts: -------------------------------------------------------------------------------- 1 | import createClient from 'openapi-fetch'; 2 | 3 | import {type components, type paths} from './api/umbraco-api'; 4 | 5 | (async () => { 6 | const {GET} = createClient({baseUrl: 'http://localhost:34962'}); 7 | 8 | const {data} = await GET('/umbraco/delivery/api/v2/content/item/{path}', { 9 | params: { 10 | query: { 11 | expand: 'properties[$all]', 12 | }, 13 | path: { 14 | path: '/', 15 | }, 16 | }, 17 | }); 18 | 19 | if (data) { 20 | renderPage(data); 21 | } 22 | })(); 23 | 24 | function renderPage(content: components['schemas']['IApiContentResponseModel']) { 25 | console.log(' Name: ', content.name); 26 | console.log(' Path: ', content.route?.path); 27 | 28 | if (content.contentType === 'testPage') { 29 | renderTestPage(content); 30 | } 31 | } 32 | 33 | function renderTestPage(content: components['schemas']['TestPageContentResponseModel']) { 34 | const {properties} = content; 35 | 36 | console.log('\n **Common**'); 37 | print('textString', properties?.textString); 38 | print('textArea', properties?.textArea); 39 | print('datePickerWithTime', properties?.datePickerWithTime); 40 | print('datePicker', properties?.datePicker); 41 | print('toggle', properties?.toggle); 42 | print('numeric', properties?.numeric); 43 | print('decimal', properties?.decimal); 44 | print('slider', properties?.slider); 45 | print('tags', properties?.tags); 46 | print('email', properties?.email); 47 | 48 | console.log('\n **Pickers**'); 49 | print('colorPicker', properties?.colorPicker); 50 | print('contentPicker', ''); 51 | print('eyeDropperColorPicker', properties?.eyeDropperColorPicker); 52 | print('urlPicker', properties?.urlPicker); 53 | print('multinodeTreepicker', ''); 54 | 55 | console.log('\n **Rich content**'); 56 | print('richText', properties?.richText); 57 | print('blockGrid', ''); 58 | print('markdown', properties?.markdown); 59 | 60 | console.log('\n **People**'); 61 | print('memberGroupPicker', properties?.memberGroupPicker); 62 | print('memberPicker', properties?.memberPicker); 63 | print('userPicker', properties?.userPicker); 64 | 65 | console.log('\n **Lists**'); 66 | print('blockList', ''); 67 | print('checkboxList', properties?.checkboxList); 68 | print('dropdown', properties?.dropdown); 69 | print('radiobox', properties?.radiobox); 70 | print('repeatableTextstrings', properties?.repeatableTextstrings); 71 | 72 | console.log('\n **Media**'); 73 | // TODO: print('uploadFile', properties?.uploadFile); 74 | // TODO: print('imageCropper', properties?.imageCropper); 75 | print('mediaPicker', properties?.mediaPicker); 76 | 77 | console.log('\n **Content Picker**'); 78 | print('name', properties?.contentPicker?.name); 79 | print('route>path', properties?.contentPicker?.route?.path); 80 | print('properties>textString', properties?.contentPicker?.properties?.textString); 81 | 82 | console.log('\n **Multinode Treepicker**'); 83 | print('name', properties?.multinodeTreepicker?.[0]?.name); 84 | print('route>path', properties?.multinodeTreepicker?.[0]?.route?.path); 85 | print('properties>textString', properties?.multinodeTreepicker?.[0]?.properties?.textString); 86 | 87 | console.log('\n **Block List**'); 88 | content.properties?.blockList?.items?.forEach((block, i) => { 89 | console.log(` Block[${i}]:`); 90 | renderBlock(block); 91 | }); 92 | 93 | console.log('\n **Block Grid**'); 94 | content.properties?.blockGrid?.items?.forEach((block, i) => { 95 | console.log(` Block[${i}]:`); 96 | renderBlock(block); 97 | }); 98 | 99 | console.log('\n **From composition(s)**'); 100 | print(' sharedToggle', properties?.sharedToggle); 101 | print(' sharedString', properties?.sharedString); 102 | print(' sharedRadiobox', properties?.sharedRadiobox); 103 | print(' sharedRichText', properties?.sharedRichText); 104 | } 105 | 106 | function renderBlock(block: components['schemas']['ApiBlockItemModel']) { 107 | console.log(' Type: ', block.content?.contentType); 108 | switch (block.content?.contentType) { 109 | case 'testBlock': { 110 | console.log(' String: ', block.content.properties?.string); 111 | console.log(' Multinode Treepicker: ', block.content.properties?.multinodeTreepicker?.[0]?.id); 112 | console.log(' Shared string: ', block.content.properties?.sharedString); 113 | const nestedBlock = block.content.properties?.blocks?.items?.[0]; 114 | if (nestedBlock) { 115 | console.log(' **Nested block**'); 116 | renderBlock(nestedBlock); 117 | } 118 | 119 | break; 120 | } 121 | 122 | case 'testBlock2': { 123 | console.log(' Shared string (testBlock2): ', block.content.properties?.sharedString); 124 | if (block.settings?.contentType === 'blockSettings') { 125 | console.log(' Block id (settings): ', block.settings?.properties?.anchorId); 126 | } 127 | 128 | break; 129 | } 130 | 131 | default: 132 | console.error(' Unknown block type '); 133 | break; 134 | } 135 | } 136 | 137 | function print(propertyName: string, value: unknown) { 138 | console.log(` ${propertyName} (${typeof value}):`, JSON.stringify(value)); 139 | } 140 | -------------------------------------------------------------------------------- /tests/clients/openapi-typescript/openapi-typescript.esproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | npm run build 5 | 6 | npm run clean 7 | npm run start 8 | 9 | -------------------------------------------------------------------------------- /tests/clients/openapi-typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openapi-typescript", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "openapi-typescript", 6 | "main": "dist/app.js", 7 | "scripts": { 8 | "start": "npm run generate-api-client && npm run build && node dist/app.js", 9 | "build": "tsc --build", 10 | "clean": "tsc --build --clean", 11 | "generate-api-client": "openapi-typescript ../../UmbracoDeliveryApiExtensions.TestSite/delivery.swagger.g.json -o ./api/umbraco-api.d.ts" 12 | }, 13 | "devDependencies": { 14 | "@types/node": "^22.9.0", 15 | "@typescript-eslint/eslint-plugin": "^8.13.0", 16 | "@typescript-eslint/parser": "^8.13.0", 17 | "eslint": "^9.14.0", 18 | "eslint-config-xo-space": "^0.35.0", 19 | "eslint-config-xo-typescript": "^7.0.0", 20 | "eslint-plugin-n": "^17.13.1", 21 | "eslint-plugin-promise": "^7.1.0", 22 | "eslint-plugin-simple-import-sort": "^12.1.1", 23 | "openapi-typescript": "^6.7.6", 24 | "typescript": "^5.6.3" 25 | }, 26 | "dependencies": { 27 | "openapi-fetch": "^0.13.0" 28 | }, 29 | "volta": { 30 | "node": "22.11.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/clients/openapi-typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "experimentalDecorators": true, 5 | "useDefineForClassFields": false, 6 | "module": "NodeNext", 7 | "lib": ["ES2022"], 8 | "skipLibCheck": true, 9 | 10 | "moduleResolution": "NodeNext", 11 | "esModuleInterop": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": false, 15 | "sourceMap": true, 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noUncheckedIndexedAccess": true, 22 | "noFallthroughCasesInSwitch": true, 23 | 24 | "outDir": "./dist", 25 | }, 26 | "include": ["**/*.ts"] 27 | } 28 | -------------------------------------------------------------------------------- /tests/clients/orval/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": false, 4 | "node": true, 5 | "es2021": true 6 | }, 7 | "extends": "xo-space", 8 | "ignorePatterns": ["api/*"], 9 | "overrides": [ 10 | { 11 | "extends": [ 12 | "xo-typescript/space" 13 | ], 14 | "files": ["*.ts", "*.tsx"], 15 | "plugins": ["simple-import-sort"], 16 | "rules": { 17 | "@typescript-eslint/consistent-type-definitions": [ 18 | "warn", "interface" 19 | ], 20 | "@typescript-eslint/naming-convention": [ 21 | "error", 22 | { 23 | "selector": ["class", "interface", "typeAlias", "enum", "typeParameter"], 24 | "format": ["StrictPascalCase"] 25 | } 26 | ], 27 | "simple-import-sort/imports": "error", 28 | "simple-import-sort/exports": "error" 29 | } 30 | } 31 | ], 32 | "parserOptions": { 33 | "ecmaVersion": "latest", 34 | "sourceType": "module" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/clients/orval/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /tests/clients/orval/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Start", 6 | "type": "node", 7 | "request": "launch", 8 | "runtimeExecutable": "npm", 9 | "runtimeArgs": [ "run", "start" ], 10 | "cwd": "${workspaceFolder}", 11 | "console": "internalConsole" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tests/clients/orval/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": "explicit" 4 | }, 5 | "typescript.tsdk": "node_modules\\typescript\\lib" 6 | } 7 | -------------------------------------------------------------------------------- /tests/clients/orval/README.md: -------------------------------------------------------------------------------- 1 | # orval 2 | 3 | 4 | -------------------------------------------------------------------------------- /tests/clients/orval/app.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import {type ApiBlockListModelItemsItem, getContentItemByPath20, type IApiContentResponseModel, type TestPageContentResponseModel} from './api/umbraco-api'; 3 | 4 | // Workaround for Umbraco Delivery API bug: https://github.com/umbraco/Umbraco-CMS/issues/17476 5 | axios.defaults.headers.common['Accept-Language'] = 'en-US'; 6 | 7 | (async () => { 8 | console.log('** Page - Default **'); 9 | const content = (await getContentItemByPath20('/', {expand: 'properties[$all]'})).data; 10 | renderPage(content); 11 | })(); 12 | 13 | function renderPage(content: IApiContentResponseModel) { 14 | console.log(' Name: ', content.name); 15 | console.log(' Path: ', content.route?.path); 16 | 17 | if (content.contentType === 'testPage') { 18 | renderTestPage(content); 19 | } 20 | } 21 | 22 | function renderTestPage(content: TestPageContentResponseModel) { 23 | const {properties} = content; 24 | 25 | console.log('\n **Common**'); 26 | print('textString', properties?.textString); 27 | print('textArea', properties?.textArea); 28 | print('datePickerWithTime', properties?.datePickerWithTime); 29 | print('datePicker', properties?.datePicker); 30 | print('toggle', properties?.toggle); 31 | print('numeric', properties?.numeric); 32 | print('decimal', properties?.decimal); 33 | print('slider', properties?.slider); 34 | print('tags', properties?.tags); 35 | print('email', properties?.email); 36 | 37 | console.log('\n **Pickers**'); 38 | print('colorPicker', properties?.colorPicker); 39 | print('contentPicker', ''); 40 | print('eyeDropperColorPicker', properties?.eyeDropperColorPicker); 41 | print('urlPicker', properties?.urlPicker); 42 | print('multinodeTreepicker', ''); 43 | 44 | console.log('\n **Rich content**'); 45 | print('richText', properties?.richText); 46 | print('blockGrid', ''); 47 | print('markdown', properties?.markdown); 48 | 49 | console.log('\n **People**'); 50 | print('memberGroupPicker', properties?.memberGroupPicker); 51 | print('memberPicker', properties?.memberPicker); 52 | print('userPicker', properties?.userPicker); 53 | 54 | console.log('\n **Lists**'); 55 | print('blockList', ''); 56 | print('checkboxList', properties?.checkboxList); 57 | print('dropdown', properties?.dropdown); 58 | print('radiobox', properties?.radiobox); 59 | print('repeatableTextstrings', properties?.repeatableTextstrings); 60 | 61 | console.log('\n **Media**'); 62 | // TODO: print('uploadFile', properties?.uploadFile); 63 | // TODO: print('imageCropper', properties?.imageCropper); 64 | print('mediaPicker', properties?.mediaPicker); 65 | 66 | console.log('\n **Content Picker**'); 67 | print('name', properties?.contentPicker?.name); 68 | print('route>path', properties?.contentPicker?.route?.path); 69 | print('properties>textString', properties?.contentPicker?.properties?.textString); 70 | 71 | console.log('\n **Multinode Treepicker**'); 72 | print('name', properties?.multinodeTreepicker?.[0]?.name); 73 | print('route>path', properties?.multinodeTreepicker?.[0]?.route?.path); 74 | print('properties>textString', properties?.multinodeTreepicker?.[0]?.properties?.textString); 75 | 76 | console.log('\n **Block List**'); 77 | content.properties?.blockList?.items?.forEach((block, i) => { 78 | console.log(` Block[${i}]:`); 79 | renderBlock(block); 80 | }); 81 | 82 | console.log('\n **Block Grid**'); 83 | content.properties?.blockGrid?.items?.forEach((block, i) => { 84 | console.log(` Block[${i}]:`); 85 | renderBlock(block); 86 | }); 87 | 88 | console.log('\n **From composition(s)**'); 89 | print(' sharedToggle', properties?.sharedToggle); 90 | print(' sharedString', properties?.sharedString); 91 | print(' sharedRadiobox', properties?.sharedRadiobox); 92 | print(' sharedRichText', properties?.sharedRichText); 93 | } 94 | 95 | function renderBlock(block: ApiBlockListModelItemsItem) { 96 | console.log(' Type: ', block.content?.contentType); 97 | switch (block.content?.contentType) { 98 | case 'testBlock': { 99 | console.log(' String: ', block.content.properties?.string); 100 | console.log(' Multinode Treepicker: ', block.content.properties?.multinodeTreepicker?.[0]?.id); 101 | console.log(' Shared string: ', block.content.properties?.sharedString); 102 | const nestedBlock = block.content.properties?.blocks?.items?.[0]; 103 | if (nestedBlock) { 104 | console.log(' **Nested block**'); 105 | renderBlock(nestedBlock); 106 | } 107 | 108 | break; 109 | } 110 | 111 | case 'testBlock2': { 112 | console.log(' Shared string (testBlock2): ', block.content.properties?.sharedString); 113 | if (block.settings?.contentType === 'blockSettings') { 114 | console.log(' Block id (settings): ', block.settings?.properties?.anchorId); 115 | } 116 | 117 | break; 118 | } 119 | 120 | default: 121 | console.error(' Unknown block type '); 122 | break; 123 | } 124 | } 125 | 126 | function print(propertyName: string, value: unknown) { 127 | console.log(` ${propertyName} (${typeof value}):`, JSON.stringify(value)); 128 | } 129 | 130 | -------------------------------------------------------------------------------- /tests/clients/orval/orval.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'orval'; 2 | 3 | export default defineConfig({ 4 | 'umbraco-api': { 5 | input: '../../UmbracoDeliveryApiExtensions.TestSite/delivery.swagger.g.json', 6 | output: { 7 | target: 'api/umbraco-api.ts', 8 | baseUrl: 'http://localhost:34962', 9 | }, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /tests/clients/orval/orval.esproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | npm run build 5 | 6 | npm run clean 7 | npm run start 8 | 9 | -------------------------------------------------------------------------------- /tests/clients/orval/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "orval", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "orval", 6 | "main": "dist/app.js", 7 | "scripts": { 8 | "start": "npm run generate-api-client && npm run build && node dist/app.js", 9 | "build": "tsc --build", 10 | "clean": "tsc --build --clean", 11 | "generate-api-client": "orval --config orval.config.ts" 12 | }, 13 | "devDependencies": { 14 | "@types/node": "^22.9.0", 15 | "@typescript-eslint/eslint-plugin": "^8.13.0", 16 | "@typescript-eslint/parser": "^8.13.0", 17 | "eslint": "^9.14.0", 18 | "eslint-config-xo-space": "^0.35.0", 19 | "eslint-config-xo-typescript": "^7.0.0", 20 | "eslint-plugin-n": "^17.13.1", 21 | "eslint-plugin-promise": "^7.1.0", 22 | "eslint-plugin-simple-import-sort": "^12.1.1", 23 | "orval": "^7.2.0", 24 | "typescript": "^5.6.3" 25 | }, 26 | "dependencies": { 27 | "axios": "^1.7.7" 28 | }, 29 | "volta": { 30 | "node": "22.11.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/clients/orval/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "experimentalDecorators": true, 5 | "useDefineForClassFields": false, 6 | "module": "NodeNext", 7 | "lib": [ "ES2022" ], 8 | "skipLibCheck": true, 9 | 10 | "moduleResolution": "NodeNext", 11 | "esModuleInterop": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": false, 15 | "sourceMap": true, 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noUncheckedIndexedAccess": true, 22 | "noFallthroughCasesInSwitch": true, 23 | 24 | "outDir": "./dist" 25 | }, 26 | "include": ["**/*.ts"] 27 | } 28 | -------------------------------------------------------------------------------- /tools/UmbracoDeliveryApiExtensions.JsonSchemaGenerator/CommandLineArguments.cs: -------------------------------------------------------------------------------- 1 | using CommandLine; 2 | 3 | namespace UmbracoDeliveryApiExtensions.JsonSchemaGenerator; 4 | internal sealed class CommandLineArguments 5 | { 6 | [Option('i', "inputFile", Required = true)] 7 | public required string InputDll { get; set; } 8 | 9 | [Option('t', "targetType", Required = true)] 10 | public required string TargetType { get; set; } 11 | 12 | [Option('o', "outputFile", Required = true)] 13 | public required string OutputFile { get; set; } 14 | 15 | [Option('n', "packageName", Required = false)] 16 | public string PackageName { get; set; } = "DeliveryApiExtensions"; 17 | } 18 | -------------------------------------------------------------------------------- /tools/UmbracoDeliveryApiExtensions.JsonSchemaGenerator/PrefixedSchemaNameGenerator.cs: -------------------------------------------------------------------------------- 1 | using NJsonSchema.Generation; 2 | 3 | namespace UmbracoDeliveryApiExtensions.JsonSchemaGenerator; 4 | 5 | internal sealed class PrefixedSchemaNameGenerator : DefaultSchemaNameGenerator 6 | { 7 | private readonly string _prefix; 8 | 9 | public PrefixedSchemaNameGenerator(string prefix) 10 | { 11 | _prefix = prefix; 12 | } 13 | 14 | public override string Generate(Type type) 15 | { 16 | string schemaName = base.Generate(type); 17 | 18 | return schemaName.StartsWith(_prefix, StringComparison.OrdinalIgnoreCase) ? schemaName : _prefix + schemaName; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tools/UmbracoDeliveryApiExtensions.JsonSchemaGenerator/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | using CommandLine; 5 | using NJsonSchema; 6 | using NJsonSchema.Generation; 7 | using UmbracoDeliveryApiExtensions.JsonSchemaGenerator; 8 | 9 | CommandLineArguments arguments = Parser.Default.ParseArguments(args).Value; 10 | 11 | JsonSchemaGenerator schemaGenerator = new( 12 | new SystemTextJsonSchemaGeneratorSettings 13 | { 14 | SchemaNameGenerator = new PrefixedSchemaNameGenerator(arguments.PackageName), 15 | SerializerOptions = new JsonSerializerOptions 16 | { 17 | Converters = { new JsonStringEnumConverter() }, 18 | }, 19 | IgnoreObsoleteProperties = true, 20 | AllowReferencesWithProperties = true, 21 | }); 22 | 23 | Assembly assembly = Assembly.LoadFrom(arguments.InputDll); 24 | Type type = assembly.GetType(arguments.TargetType) ?? throw new InvalidOperationException($"Type {arguments.TargetType} couldn't be found."); 25 | 26 | JsonSchema typeSchema = schemaGenerator.Generate(type); 27 | JsonSchema wrapperSchema = new() 28 | { 29 | Title = $"{arguments.PackageName}Schema", 30 | Type = JsonObjectType.Object, 31 | Properties = 32 | { 33 | [arguments.PackageName] = new JsonSchemaProperty 34 | { 35 | Type = JsonObjectType.Object, 36 | Reference = typeSchema, 37 | }, 38 | }, 39 | Definitions = 40 | { 41 | [typeSchema.Title!] = typeSchema, 42 | }, 43 | }; 44 | 45 | await File.WriteAllTextAsync(arguments.OutputFile, wrapperSchema.ToJson()); 46 | -------------------------------------------------------------------------------- /tools/UmbracoDeliveryApiExtensions.JsonSchemaGenerator/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "UmbracoDeliveryApiExtensions.JsonSchemaGenerator": { 4 | "commandName": "Project", 5 | "commandLineArgs": "-i ../../src/UmbracoDeliveryApiExtensions/bin/Debug/net7.0/UmbracoDeliveryApiExtensions.dll -o ../../src/UmbracoDeliveryApiExtensions/appsettings-schema.DeliveryApiExtensions.json -t Umbraco.Community.DeliveryApiExtensions.Configuration.Options.DeliveryApiExtensionsOptions", 6 | "workingDirectory": "." 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tools/UmbracoDeliveryApiExtensions.JsonSchemaGenerator/UmbracoDeliveryApiExtensions.JsonSchemaGenerator.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /umbraco-marketplace.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://marketplace.umbraco.com/umbraco-marketplace-schema.json", 3 | "AlternateCategory": "Headless", 4 | "AuthorDetails": { 5 | "Name": "ByteCrumb", 6 | "Description": "Developer duo with a passion for Umbraco and .NET, building packages together.", 7 | "ImageUrl": "https://github.com/ByteCrumb.png", 8 | "Contributors": [ 9 | { 10 | "Name": "Laura Neto", 11 | "Url": "https://github.com/lauraneto" 12 | }, 13 | { 14 | "Name": "Vitor Rodrigues", 15 | "Url": "https://github.com/vsilvar" 16 | } 17 | ], 18 | "SyncContributorsFromRepository": false 19 | }, 20 | "Category": "Developer Tools", 21 | "Description": "Extensions for the Delivery API, including typed swagger and backoffice preview of the API responses.", 22 | "DocumentationUrl": "https://github.com/ByteCrumb/Umbraco.Community.DeliveryApiExtensions", 23 | "LicenseTypes": [ "Free" ], 24 | "IssueTrackerUrl": "https://github.com/ByteCrumb/Umbraco.Community.DeliveryApiExtensions/issues", 25 | "PackageType": "Package", 26 | "Screenshots": [ 27 | { 28 | "ImageUrl": "https://raw.githubusercontent.com/ByteCrumb/Umbraco.Community.DeliveryApiExtensions/v13/main/docs/screenshots/api-preview.png", 29 | "Caption": "Delivery API preview content app" 30 | }, 31 | { 32 | "ImageUrl": "https://raw.githubusercontent.com/ByteCrumb/Umbraco.Community.DeliveryApiExtensions/v13/main/docs/screenshots/typed-swagger-schema.png", 33 | "Caption": "Typed swagger schema example" 34 | } 35 | ], 36 | "Tags": [ "headless", "delivery api", "swagger", "openapi", "backoffice" ], 37 | "Title": "Delivery Api Extensions" 38 | } 39 | --------------------------------------------------------------------------------